commit
59f2993174
|
@ -1,7 +1,50 @@
|
||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadAngleBracketsSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadBracesSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadChildStatementIndent/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadColonSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadCommaSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadControlBracesIndent/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadControlBracesLineBreaks/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadDeclarationBracesIndent/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadEmptyBracesLineBreaks/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadExpressionBracesIndent/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadExpressionBracesLineBreaks/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadListLineBreaks/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadMemberAccessSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadNamespaceBracesIndent/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadParensLineBreaks/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadParensSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadSemicolonSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadSpacesAfterKeyword/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadSquareBracketsSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadSwitchBracesIndent/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBadSymbolSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClangTidyHicppUseAuto/@EntryIndexedValue">HINT</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClangTidyMiscUnusedUsingDecls/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClangTidyModernizePassByValue/@EntryIndexedValue">HINT</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClangTidyModernizeRawStringLiteral/@EntryIndexedValue">HINT</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClangTidyModernizeRawStringLiteral/@EntryIndexedValue">HINT</s:String>
|
||||||
|
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFunctionDoesntReturnValue/@EntryIndexedValue">ERROR</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFunctionDoesntReturnValue/@EntryIndexedValue">ERROR</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIncorrectBlankLinesNearBraces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableMayBeConst/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMissingBlankLines/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
|
||||||
|
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMissingIndent/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMissingLinebreak/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMissingSpace/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMultipleSpaces/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppOutdentIsOffPrevLevel/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantBlankLines/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantLinebreak/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantSpace/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTabsAndSpacesMismatch/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=LocalizableElement/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=LocalizableElement/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
|
|
||||||
|
@ -29,9 +72,12 @@
|
||||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/PLACE_WHILE_ON_NEW_LINE/@EntryValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/PLACE_WHILE_ON_NEW_LINE/@EntryValue">False</s:Boolean>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">ON_SINGLE_LINE</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SIMPLE_EMBEDDED_STATEMENT_STYLE/@EntryValue">ON_SINGLE_LINE</s:String>
|
||||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SPACE_AFTER_CAST_EXPRESSION_PARENTHESES/@EntryValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SPACE_AFTER_CAST_EXPRESSION_PARENTHESES/@EntryValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SPACE_BEFORE_INITIALIZER_BRACES/@EntryValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SPACE_BEFORE_TEMPLATE_PARAMS/@EntryValue">False</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SPACE_BEFORE_TEMPLATE_PARAMS/@EntryValue">False</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/SPACE_WITHIN_INITIALIZER_BRACES/@EntryValue">True</s:Boolean>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/TYPE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/TYPE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/WRAP_ENUMERATION_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/WRAP_ENUMERATION_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CppFormatting/WRAP_LINES/@EntryValue">False</s:Boolean>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_OWNER_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_OWNER_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
||||||
|
@ -58,6 +104,7 @@
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/VBFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/VBFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/XmlDocFormatter/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/XmlDocFormatter/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/XmlFormatter/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
<s:String x:Key="/Default/CodeStyle/CodeFormatting/XmlFormatter/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
|
||||||
|
<s:Boolean x:Key="/Default/CodeStyle/CppIntroduceType/InsertTypeAlias/@EntryValue">True</s:Boolean>
|
||||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseExplicitType</s:String>
|
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseExplicitType</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVarWhenEvident</s:String>
|
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVarWhenEvident</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Class_0020and_0020struct_0020fields/@EntryIndexedValue"><NamingElement Priority="10"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="_" Style="aaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Class_0020and_0020struct_0020fields/@EntryIndexedValue"><NamingElement Priority="10"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="_" Style="aaBb" /></NamingElement></s:String>
|
||||||
|
@ -73,7 +120,7 @@
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Namespaces/@EntryIndexedValue"><NamingElement Priority="16"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="namespace" /><type Name="namespace alias" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Namespaces/@EntryIndexedValue"><NamingElement Priority="16"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="namespace" /><type Name="namespace alias" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Other_0020constants/@EntryIndexedValue"><NamingElement Priority="14"><Descriptor Static="True" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="local variable" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Other_0020constants/@EntryIndexedValue"><NamingElement Priority="14"><Descriptor Static="True" Constexpr="Indeterminate" Const="True" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="local variable" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Parameters/@EntryIndexedValue"><NamingElement Priority="5"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="parameter" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Parameters/@EntryIndexedValue"><NamingElement Priority="5"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="parameter" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Template_0020parameters/@EntryIndexedValue"><NamingElement Priority="4"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="template parameter" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement></s:String>
|
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Typedefs/@EntryIndexedValue"><NamingElement Priority="17"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="type alias" /><type Name="typedef" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Typedefs/@EntryIndexedValue"><NamingElement Priority="17"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="type alias" /><type Name="typedef" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement></s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Union_0020members/@EntryIndexedValue"><NamingElement Priority="12"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union member" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Union_0020members/@EntryIndexedValue"><NamingElement Priority="12"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union member" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Unions/@EntryIndexedValue"><NamingElement Priority="3"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Unions/@EntryIndexedValue"><NamingElement Priority="3"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="union" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement></s:String>
|
||||||
|
@ -120,18 +167,59 @@
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/PsiConfigurationSettingsKey/CustomLocation/@EntryValue">C:\Users\Daniel\AppData\Local\JetBrains\Transient\ReSharperPlatformVs14\v09\SolutionCaches</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/PsiConfigurationSettingsKey/CustomLocation/@EntryValue">C:\Users\Daniel\AppData\Local\JetBrains\Transient\ReSharperPlatformVs14\v09\SolutionCaches</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=AutoRecoverer/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=Format/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=ShowAnnotations/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=StartPage_002DIsDownloadRefreshEnabled/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=StartPage_002DOnEnvironmentStatup/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=SyncSettings/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=TextEditor_002DCodeLens/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=TextEditor_002DTrackChanges_002D2/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=VCS/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=VsBulb/@EntryIndexedValue">DO_NOTHING</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=XAML_0020Designer/@EntryIndexedValue">LIVE_MONITOR</s:String>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ESettingsUpgrade_002EFunctionReturnStyleSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ESettingsUpgrade_002EFunctionReturnStyleSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ESettingsUpgrade_002ENamespaceIndentationSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECpp_002ECodeStyle_002ESettingsUpgrade_002ENamespaceIndentationSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EFormat_002ESettingsUpgrade_002EAlignmentTabFillStyleMigration/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EFormat_002ESettingsUpgrade_002EAlignmentTabFillStyleMigration/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=allphone/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=allphone/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Backoff/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=badbit/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Bigram/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=bigrams/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=bitstream/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=centiseconds/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=cepstral/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=cepstral/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=cmudict/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=cmudict/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Codepoints/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=cont_0027d/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=deflator/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Downmix/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=downscaling/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=endian/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=failbit/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flite/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=fourcc/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=inbetween/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=inbetweens/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Matthieu/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=pbeam/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=pbeam/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=qwhy/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=qwhy/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=rbegin/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=resample/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=retime/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=retimed/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=synth/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=tclap/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=timelines/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tweens/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unigram/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=unigrams/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Upsampling/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Viterbi/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Viterbi/@EntryIndexedValue">True</s:Boolean>
|
||||||
</wpf:ResourceDictionary>
|
</wpf:ResourceDictionary>
|
|
@ -1,21 +1,31 @@
|
||||||
#include "ShapeRule.h"
|
#include "ShapeRule.h"
|
||||||
#include <boost/range/adaptor/transformed.hpp>
|
#include <boost/range/adaptor/transformed.hpp>
|
||||||
|
#include <utility>
|
||||||
#include "time/ContinuousTimeline.h"
|
#include "time/ContinuousTimeline.h"
|
||||||
|
|
||||||
using boost::optional;
|
using boost::optional;
|
||||||
using boost::adaptors::transformed;
|
using boost::adaptors::transformed;
|
||||||
|
|
||||||
template<typename T, bool AutoJoin>
|
template<typename T, bool AutoJoin>
|
||||||
ContinuousTimeline<optional<T>, AutoJoin> boundedTimelinetoContinuousOptional(const BoundedTimeline<T, AutoJoin>& timeline) {
|
ContinuousTimeline<optional<T>, AutoJoin> boundedTimelinetoContinuousOptional(
|
||||||
|
const BoundedTimeline<T, AutoJoin>& timeline
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
timeline.getRange(), boost::none,
|
timeline.getRange(),
|
||||||
timeline | transformed([](const Timed<T>& timedValue) { return Timed<optional<T>>(timedValue.getTimeRange(), timedValue.getValue()); })
|
boost::none,
|
||||||
|
timeline | transformed([](const Timed<T>& timedValue) {
|
||||||
|
return Timed<optional<T>>(timedValue.getTimeRange(), timedValue.getValue());
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapeRule::ShapeRule(const ShapeSet& shapeSet, const optional<Phone>& phone, TimeRange phoneTiming) :
|
ShapeRule::ShapeRule(
|
||||||
shapeSet(shapeSet),
|
ShapeSet shapeSet,
|
||||||
phone(phone),
|
optional<Phone> phone,
|
||||||
|
TimeRange phoneTiming
|
||||||
|
) :
|
||||||
|
shapeSet(std::move(shapeSet)),
|
||||||
|
phone(std::move(phone)),
|
||||||
phoneTiming(phoneTiming)
|
phoneTiming(phoneTiming)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
@ -43,11 +53,14 @@ ContinuousTimeline<ShapeRule> getShapeRules(const BoundedTimeline<Phone>& phones
|
||||||
auto continuousPhones = boundedTimelinetoContinuousOptional(phones);
|
auto continuousPhones = boundedTimelinetoContinuousOptional(phones);
|
||||||
|
|
||||||
// Create timeline of shape rules
|
// Create timeline of shape rules
|
||||||
ContinuousTimeline<ShapeRule> shapeRules(phones.getRange(), {{Shape::X}, boost::none, {0_cs, 0_cs}});
|
ContinuousTimeline<ShapeRule> shapeRules(
|
||||||
|
phones.getRange(),
|
||||||
|
{ { Shape::X }, boost::none, { 0_cs, 0_cs } }
|
||||||
|
);
|
||||||
centiseconds previousDuration = 0_cs;
|
centiseconds previousDuration = 0_cs;
|
||||||
for (const auto& timedPhone : continuousPhones) {
|
for (const auto& timedPhone : continuousPhones) {
|
||||||
optional<Phone> phone = timedPhone.getValue();
|
optional<Phone> phone = timedPhone.getValue();
|
||||||
centiseconds duration = timedPhone.getDuration();
|
const centiseconds duration = timedPhone.getDuration();
|
||||||
|
|
||||||
if (phone) {
|
if (phone) {
|
||||||
// Animate one phone
|
// Animate one phone
|
||||||
|
@ -59,7 +72,10 @@ ContinuousTimeline<ShapeRule> getShapeRules(const BoundedTimeline<Phone>& phones
|
||||||
// Copy to timeline.
|
// Copy to timeline.
|
||||||
// Later shape sets may overwrite earlier ones if overlapping.
|
// Later shape sets may overwrite earlier ones if overlapping.
|
||||||
for (const auto& timedShapeSet : phoneShapeSets) {
|
for (const auto& timedShapeSet : phoneShapeSets) {
|
||||||
shapeRules.set(timedShapeSet.getTimeRange(), ShapeRule(timedShapeSet.getValue(), phone, timedPhone.getTimeRange()));
|
shapeRules.set(
|
||||||
|
timedShapeSet.getTimeRange(),
|
||||||
|
ShapeRule(timedShapeSet.getValue(), phone, timedPhone.getTimeRange())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ struct ShapeRule {
|
||||||
boost::optional<Phone> phone;
|
boost::optional<Phone> phone;
|
||||||
TimeRange phoneTiming;
|
TimeRange phoneTiming;
|
||||||
|
|
||||||
ShapeRule(const ShapeSet& shapeSet, const boost::optional<Phone>& phone, TimeRange phoneTiming);
|
ShapeRule(ShapeSet shapeSet, boost::optional<Phone> phone, TimeRange phoneTiming);
|
||||||
|
|
||||||
static ShapeRule getInvalid();
|
static ShapeRule getInvalid();
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,14 @@ using std::map;
|
||||||
constexpr size_t shapeValueCount = static_cast<size_t>(Shape::EndSentinel);
|
constexpr size_t shapeValueCount = static_cast<size_t>(Shape::EndSentinel);
|
||||||
|
|
||||||
Shape getBasicShape(Shape shape) {
|
Shape getBasicShape(Shape shape) {
|
||||||
static constexpr array<Shape, shapeValueCount> basicShapes = make_array(A, B, C, D, E, F, B, C, A);
|
static constexpr array<Shape, shapeValueCount> basicShapes =
|
||||||
|
make_array(A, B, C, D, E, F, B, C, A);
|
||||||
return basicShapes[static_cast<size_t>(shape)];
|
return basicShapes[static_cast<size_t>(shape)];
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape relax(Shape shape) {
|
Shape relax(Shape shape) {
|
||||||
static constexpr array<Shape, shapeValueCount> relaxedShapes = make_array(A, B, B, C, C, B, X, B, X);
|
static constexpr array<Shape, shapeValueCount> relaxedShapes =
|
||||||
|
make_array(A, B, B, C, C, B, X, B, X);
|
||||||
return relaxedShapes[static_cast<size_t>(shape)];
|
return relaxedShapes[static_cast<size_t>(shape)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +30,8 @@ Shape getClosestShape(Shape reference, ShapeSet shapes) {
|
||||||
throw std::invalid_argument("Cannot select from empty set of shapes.");
|
throw std::invalid_argument("Cannot select from empty set of shapes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A matrix that for each shape contains all shapes in ascending order of effort required to move to them
|
// A matrix that for each shape contains all shapes in ascending order of effort required to
|
||||||
|
// move to them
|
||||||
constexpr static array<array<Shape, shapeValueCount>, shapeValueCount> effortMatrix = make_array(
|
constexpr static array<array<Shape, shapeValueCount>, shapeValueCount> effortMatrix = make_array(
|
||||||
/* A */ make_array(A, X, G, B, C, H, E, D, F),
|
/* A */ make_array(A, X, G, B, C, H, E, D, F),
|
||||||
/* B */ make_array(B, G, A, X, C, H, E, D, F),
|
/* B */ make_array(B, G, A, X, C, H, E, D, F),
|
||||||
|
@ -64,19 +67,19 @@ optional<pair<Shape, TweenTiming>> getTween(Shape first, Shape second) {
|
||||||
{ { D, F }, { E, TweenTiming::Centered } },
|
{ { D, F }, { E, TweenTiming::Centered } },
|
||||||
{ { H, F }, { E, TweenTiming::Late } }, { { F, H }, { E, TweenTiming::Early } }
|
{ { H, F }, { E, TweenTiming::Late } }, { { F, H }, { E, TweenTiming::Early } }
|
||||||
};
|
};
|
||||||
auto it = lookup.find({first, second});
|
const auto it = lookup.find({ first, second });
|
||||||
return it != lookup.end() ? it->second : optional<pair<Shape, TweenTiming>>();
|
return it != lookup.end() ? it->second : optional<pair<Shape, TweenTiming>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds previousDuration) {
|
Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds previousDuration) {
|
||||||
// Returns a timeline with a single shape set
|
// Returns a timeline with a single shape set
|
||||||
auto single = [duration](ShapeSet value) {
|
const auto single = [duration](ShapeSet value) {
|
||||||
return Timeline<ShapeSet> { { 0_cs, duration, value } };
|
return Timeline<ShapeSet> { { 0_cs, duration, value } };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a timeline with two shape sets, timed as a diphthong
|
// Returns a timeline with two shape sets, timed as a diphthong
|
||||||
auto diphthong = [duration](ShapeSet first, ShapeSet second) {
|
const auto diphthong = [duration](ShapeSet first, ShapeSet second) {
|
||||||
centiseconds firstDuration = duration_cast<centiseconds>(duration * 0.6);
|
const centiseconds firstDuration = duration_cast<centiseconds>(duration * 0.6);
|
||||||
return Timeline<ShapeSet> {
|
return Timeline<ShapeSet> {
|
||||||
{ 0_cs, firstDuration, first },
|
{ 0_cs, firstDuration, first },
|
||||||
{ firstDuration, duration, second }
|
{ firstDuration, duration, second }
|
||||||
|
@ -84,10 +87,11 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a timeline with two shape sets, timed as a plosive
|
// Returns a timeline with two shape sets, timed as a plosive
|
||||||
auto plosive = [duration, previousDuration](ShapeSet first, ShapeSet second) {
|
const auto plosive = [duration, previousDuration](ShapeSet first, ShapeSet second) {
|
||||||
centiseconds minOcclusionDuration = 4_cs;
|
const centiseconds minOcclusionDuration = 4_cs;
|
||||||
centiseconds maxOcclusionDuration = 12_cs;
|
const centiseconds maxOcclusionDuration = 12_cs;
|
||||||
centiseconds occlusionDuration = clamp(previousDuration / 2, minOcclusionDuration, maxOcclusionDuration);
|
const centiseconds occlusionDuration =
|
||||||
|
clamp(previousDuration / 2, minOcclusionDuration, maxOcclusionDuration);
|
||||||
return Timeline<ShapeSet> {
|
return Timeline<ShapeSet> {
|
||||||
{ -occlusionDuration, 0_cs, first },
|
{ -occlusionDuration, 0_cs, first },
|
||||||
{ 0_cs, duration, second }
|
{ 0_cs, duration, second }
|
||||||
|
@ -96,7 +100,7 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
|
||||||
|
|
||||||
// Returns the result of `getShapeSets` when called with identical arguments
|
// Returns the result of `getShapeSets` when called with identical arguments
|
||||||
// except for a different phone.
|
// except for a different phone.
|
||||||
auto like = [duration, previousDuration](Phone referencePhone) {
|
const auto like = [duration, previousDuration](Phone referencePhone) {
|
||||||
return getShapeSets(referencePhone, duration, previousDuration);
|
return getShapeSets(referencePhone, duration, previousDuration);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,7 +108,8 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
|
||||||
static const ShapeSet anyOpen { B, C, D, E, F, G, H };
|
static const ShapeSet anyOpen { B, C, D, E, F, G, H };
|
||||||
|
|
||||||
// Note:
|
// Note:
|
||||||
// The shapes {A, B, G, X} are very similar. You should avoid regular shape sets containing more than one of these shapes.
|
// The shapes {A, B, G, X} are very similar. You should avoid regular shape sets containing more
|
||||||
|
// than one of these shapes.
|
||||||
// Otherwise, the resulting shape may be more or less random and might not be a good fit.
|
// Otherwise, the resulting shape may be more or less random and might not be a good fit.
|
||||||
// As an exception, a very flexible rule may contain *all* these shapes.
|
// As an exception, a very flexible rule may contain *all* these shapes.
|
||||||
|
|
||||||
|
|
|
@ -31,5 +31,6 @@ boost::optional<std::pair<Shape, TweenTiming>> getTween(Shape first, Shape secon
|
||||||
|
|
||||||
// Returns the shape set(s) to use for a given phone.
|
// Returns the shape set(s) to use for a given phone.
|
||||||
// The resulting timeline will always cover the entire duration of the phone (starting at 0 cs).
|
// The resulting timeline will always cover the entire duration of the phone (starting at 0 cs).
|
||||||
// It may extend into the negative time range if animation is required prior to the sound being heard.
|
// It may extend into the negative time range if animation is required prior to the sound being
|
||||||
|
// heard.
|
||||||
Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds previousDuration);
|
Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds previousDuration);
|
||||||
|
|
|
@ -8,17 +8,21 @@
|
||||||
#include "targetShapeSet.h"
|
#include "targetShapeSet.h"
|
||||||
#include "staticSegments.h"
|
#include "staticSegments.h"
|
||||||
|
|
||||||
JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone> &phones, const ShapeSet& targetShapeSet) {
|
JoiningContinuousTimeline<Shape> animate(
|
||||||
|
const BoundedTimeline<Phone>& phones,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
) {
|
||||||
// Create timeline of shape rules
|
// Create timeline of shape rules
|
||||||
ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);
|
ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);
|
||||||
|
|
||||||
// Modify shape rules to only contain allowed shapes -- plus X, which is needed for pauses and will be replaced later
|
// Modify shape rules to only contain allowed shapes -- plus X, which is needed for pauses and
|
||||||
|
// will be replaced later
|
||||||
ShapeSet targetShapeSetPlusX = targetShapeSet;
|
ShapeSet targetShapeSetPlusX = targetShapeSet;
|
||||||
targetShapeSetPlusX.insert(Shape::X);
|
targetShapeSetPlusX.insert(Shape::X);
|
||||||
shapeRules = convertToTargetShapeSet(shapeRules, targetShapeSetPlusX);
|
shapeRules = convertToTargetShapeSet(shapeRules, targetShapeSetPlusX);
|
||||||
|
|
||||||
// Animate in multiple steps
|
// Animate in multiple steps
|
||||||
auto performMainAnimationSteps = [&targetShapeSet](const auto& shapeRules) {
|
const auto performMainAnimationSteps = [&targetShapeSet](const auto& shapeRules) {
|
||||||
JoiningContinuousTimeline<Shape> animation = animateRough(shapeRules);
|
JoiningContinuousTimeline<Shape> animation = animateRough(shapeRules);
|
||||||
animation = optimizeTiming(animation);
|
animation = optimizeTiming(animation);
|
||||||
animation = animatePauses(animation);
|
animation = animatePauses(animation);
|
||||||
|
@ -26,7 +30,8 @@ JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone> &phones, c
|
||||||
animation = convertToTargetShapeSet(animation, targetShapeSet);
|
animation = convertToTargetShapeSet(animation, targetShapeSet);
|
||||||
return animation;
|
return animation;
|
||||||
};
|
};
|
||||||
const JoiningContinuousTimeline<Shape> result = avoidStaticSegments(shapeRules, performMainAnimationSteps);
|
const JoiningContinuousTimeline<Shape> result =
|
||||||
|
avoidStaticSegments(shapeRules, performMainAnimationSteps);
|
||||||
|
|
||||||
for (const auto& timedShape : result) {
|
for (const auto& timedShape : result) {
|
||||||
logTimedEvent("shape", timedShape);
|
logTimedEvent("shape", timedShape);
|
||||||
|
|
|
@ -5,4 +5,7 @@
|
||||||
#include "time/ContinuousTimeline.h"
|
#include "time/ContinuousTimeline.h"
|
||||||
#include "targetShapeSet.h"
|
#include "targetShapeSet.h"
|
||||||
|
|
||||||
JoiningContinuousTimeline<Shape> animate(const BoundedTimeline<Phone>& phones, const ShapeSet& targetShapeSet);
|
JoiningContinuousTimeline<Shape> animate(
|
||||||
|
const BoundedTimeline<Phone>& phones,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
);
|
||||||
|
|
|
@ -12,7 +12,7 @@ Shape getPauseShape(Shape previous, Shape next, centiseconds duration) {
|
||||||
// It looks odd if the pause shape is identical to the next shape.
|
// It looks odd if the pause shape is identical to the next shape.
|
||||||
// Make sure we find a relaxed shape that's different from the next one.
|
// Make sure we find a relaxed shape that's different from the next one.
|
||||||
for (Shape currentRelaxedShape = previous;;) {
|
for (Shape currentRelaxedShape = previous;;) {
|
||||||
Shape nextRelaxedShape = relax(currentRelaxedShape);
|
const Shape nextRelaxedShape = relax(currentRelaxedShape);
|
||||||
if (nextRelaxedShape != next) {
|
if (nextRelaxedShape != next) {
|
||||||
return nextRelaxedShape;
|
return nextRelaxedShape;
|
||||||
}
|
}
|
||||||
|
@ -31,11 +31,18 @@ Shape getPauseShape(Shape previous, Shape next, centiseconds duration) {
|
||||||
JoiningContinuousTimeline<Shape> animatePauses(const JoiningContinuousTimeline<Shape>& animation) {
|
JoiningContinuousTimeline<Shape> animatePauses(const JoiningContinuousTimeline<Shape>& animation) {
|
||||||
JoiningContinuousTimeline<Shape> result(animation);
|
JoiningContinuousTimeline<Shape> result(animation);
|
||||||
|
|
||||||
for_each_adjacent(animation.begin(), animation.end(), [&](const Timed<Shape>& previous, const Timed<Shape>& pause, const Timed<Shape>& next) {
|
for_each_adjacent(
|
||||||
|
animation.begin(),
|
||||||
|
animation.end(),
|
||||||
|
[&](const Timed<Shape>& previous, const Timed<Shape>& pause, const Timed<Shape>& next) {
|
||||||
if (pause.getValue() != Shape::X) return;
|
if (pause.getValue() != Shape::X) return;
|
||||||
|
|
||||||
result.set(pause.getTimeRange(), getPauseShape(previous.getValue(), next.getValue(), pause.getDuration()));
|
result.set(
|
||||||
});
|
pause.getTimeRange(),
|
||||||
|
getPauseShape(previous.getValue(), next.getValue(), pause.getDuration())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
#include "roughAnimation.h"
|
#include "roughAnimation.h"
|
||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
|
|
||||||
using boost::optional;
|
|
||||||
|
|
||||||
// Create timeline of shapes using a bidirectional algorithm.
|
// Create timeline of shapes using a bidirectional algorithm.
|
||||||
// Here's a rough sketch:
|
// Here's a rough sketch:
|
||||||
//
|
//
|
||||||
// * Most consonants result in shape sets with multiple options; most vowels have only one shape option.
|
// * Most consonants result in shape sets with multiple options; most vowels have only one shape
|
||||||
|
// option.
|
||||||
// * When speaking, we tend to slur mouth shapes into each other. So we animate from start to end,
|
// * When speaking, we tend to slur mouth shapes into each other. So we animate from start to end,
|
||||||
// always choosing a shape from the current set that resembles the last shape and is somewhat relaxed.
|
// always choosing a shape from the current set that resembles the last shape and is somewhat
|
||||||
|
// relaxed.
|
||||||
// * When speaking, we anticipate vowels, trying to form their shape before the actual vowel.
|
// * When speaking, we anticipate vowels, trying to form their shape before the actual vowel.
|
||||||
// So whenever we come across a one-shape vowel, we backtrack a little, spreating that shape to the left.
|
// So whenever we come across a one-shape vowel, we backtrack a little, spreading that shape to
|
||||||
|
// the left.
|
||||||
JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule>& shapeRules) {
|
JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule>& shapeRules) {
|
||||||
JoiningContinuousTimeline<Shape> animation(shapeRules.getRange(), Shape::X);
|
JoiningContinuousTimeline<Shape> animation(shapeRules.getRange(), Shape::X);
|
||||||
|
|
||||||
|
@ -21,7 +22,9 @@ JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule
|
||||||
const ShapeRule shapeRule = it->getValue();
|
const ShapeRule shapeRule = it->getValue();
|
||||||
const Shape shape = getClosestShape(referenceShape, shapeRule.shapeSet);
|
const Shape shape = getClosestShape(referenceShape, shapeRule.shapeSet);
|
||||||
animation.set(it->getTimeRange(), shape);
|
animation.set(it->getTimeRange(), shape);
|
||||||
const bool anticipateShape = shapeRule.phone && isVowel(*shapeRule.phone) && shapeRule.shapeSet.size() == 1;
|
const bool anticipateShape = shapeRule.phone
|
||||||
|
&& isVowel(*shapeRule.phone)
|
||||||
|
&& shapeRule.shapeSet.size() == 1;
|
||||||
if (anticipateShape) {
|
if (anticipateShape) {
|
||||||
// Animate backwards a little
|
// Animate backwards a little
|
||||||
const Shape anticipatedShape = shape;
|
const Shape anticipatedShape = shape;
|
||||||
|
@ -34,11 +37,13 @@ JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule
|
||||||
centiseconds anticipatingShapeStart = reverseIt->getStart();
|
centiseconds anticipatingShapeStart = reverseIt->getStart();
|
||||||
if (anticipatingShapeStart == lastAnticipatedShapeStart) break;
|
if (anticipatingShapeStart == lastAnticipatedShapeStart) break;
|
||||||
const centiseconds maxAnticipationDuration = 20_cs;
|
const centiseconds maxAnticipationDuration = 20_cs;
|
||||||
const centiseconds anticipationDuration = anticipatedShapeStart - anticipatingShapeStart;
|
const centiseconds anticipationDuration =
|
||||||
|
anticipatedShapeStart - anticipatingShapeStart;
|
||||||
if (anticipationDuration > maxAnticipationDuration) break;
|
if (anticipationDuration > maxAnticipationDuration) break;
|
||||||
|
|
||||||
// Overwrite forward-animated shape with backwards-animated, anticipating shape
|
// Overwrite forward-animated shape with backwards-animated, anticipating shape
|
||||||
const Shape anticipatingShape = getClosestShape(referenceShape, reverseIt->getValue().shapeSet);
|
const Shape anticipatingShape =
|
||||||
|
getClosestShape(referenceShape, reverseIt->getValue().shapeSet);
|
||||||
animation.set(reverseIt->getTimeRange(), anticipatingShape);
|
animation.set(reverseIt->getTimeRange(), anticipatingShape);
|
||||||
|
|
||||||
// Make sure the new, backwards-animated shape still resembles the anticipated shape
|
// Make sure the new, backwards-animated shape still resembles the anticipated shape
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
|
|
||||||
#include "ShapeRule.h"
|
#include "ShapeRule.h"
|
||||||
|
|
||||||
// Does a rough animation (no tweening, special pause animation, etc.) using a bidirectional algorithm.
|
// Does a rough animation (no tweening, special pause animation, etc.) using a bidirectional
|
||||||
|
// algorithm.
|
||||||
JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule>& shapeRules);
|
JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule>& shapeRules);
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
#include "tools/nextCombination.h"
|
#include "tools/nextCombination.h"
|
||||||
|
|
||||||
using std::vector;
|
using std::vector;
|
||||||
using boost::optional;
|
|
||||||
|
|
||||||
int getSyllableCount(const ContinuousTimeline<ShapeRule>& shapeRules, TimeRange timeRange) {
|
int getSyllableCount(const ContinuousTimeline<ShapeRule>& shapeRules, TimeRange timeRange) {
|
||||||
if (timeRange.empty()) return 0;
|
if (timeRange.empty()) return 0;
|
||||||
|
@ -31,16 +30,22 @@ int getSyllableCount(const ContinuousTimeline<ShapeRule>& shapeRules, TimeRange
|
||||||
}
|
}
|
||||||
|
|
||||||
// A static segment is a prolonged period during which the mouth shape doesn't change
|
// A static segment is a prolonged period during which the mouth shape doesn't change
|
||||||
vector<TimeRange> getStaticSegments(const ContinuousTimeline<ShapeRule>& shapeRules, const JoiningContinuousTimeline<Shape>& animation) {
|
vector<TimeRange> getStaticSegments(
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const JoiningContinuousTimeline<Shape>& animation
|
||||||
|
) {
|
||||||
// A static segment must contain a certain number of syllables to look distractingly static
|
// A static segment must contain a certain number of syllables to look distractingly static
|
||||||
const int minSyllableCount = 3;
|
const int minSyllableCount = 3;
|
||||||
// It must also have a minimum duration. The same number of syllables in fast speech usually looks good.
|
// It must also have a minimum duration. The same number of syllables in fast speech usually
|
||||||
|
// looks good.
|
||||||
const centiseconds minDuration = 75_cs;
|
const centiseconds minDuration = 75_cs;
|
||||||
|
|
||||||
vector<TimeRange> result;
|
vector<TimeRange> result;
|
||||||
for (const auto& timedShape : animation) {
|
for (const auto& timedShape : animation) {
|
||||||
const TimeRange timeRange = timedShape.getTimeRange();
|
const TimeRange timeRange = timedShape.getTimeRange();
|
||||||
if (timeRange.getDuration() >= minDuration && getSyllableCount(shapeRules, timeRange) >= minSyllableCount) {
|
const bool isStatic = timeRange.getDuration() >= minDuration
|
||||||
|
&& getSyllableCount(shapeRules, timeRange) >= minSyllableCount;
|
||||||
|
if (isStatic) {
|
||||||
result.push_back(timeRange);
|
result.push_back(timeRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,12 +53,14 @@ vector<TimeRange> getStaticSegments(const ContinuousTimeline<ShapeRule>& shapeRu
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indicates whether this shape rule can potentially be replaced by a modified version that breaks up long static segments
|
// Indicates whether this shape rule can potentially be replaced by a modified version that breaks
|
||||||
|
// up long static segments
|
||||||
bool canChange(const ShapeRule& rule) {
|
bool canChange(const ShapeRule& rule) {
|
||||||
return rule.phone && isVowel(*rule.phone) && rule.shapeSet.size() == 1;
|
return rule.phone && isVowel(*rule.phone) && rule.shapeSet.size() == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a new shape rule that is identical to the specified one, except that it leads to a slightly different visualization
|
// Returns a new shape rule that is identical to the specified one, except that it leads to a
|
||||||
|
// slightly different visualization
|
||||||
ShapeRule getChangedShapeRule(const ShapeRule& rule) {
|
ShapeRule getChangedShapeRule(const ShapeRule& rule) {
|
||||||
assert(canChange(rule));
|
assert(canChange(rule));
|
||||||
|
|
||||||
|
@ -70,7 +77,10 @@ ShapeRule getChangedShapeRule(const ShapeRule& rule) {
|
||||||
using RuleChanges = vector<centiseconds>;
|
using RuleChanges = vector<centiseconds>;
|
||||||
|
|
||||||
// Replaces the indicated shape rules with slightly different ones, breaking up long static segments
|
// Replaces the indicated shape rules with slightly different ones, breaking up long static segments
|
||||||
ContinuousTimeline<ShapeRule> applyChanges(const ContinuousTimeline<ShapeRule>& shapeRules, const RuleChanges& changes) {
|
ContinuousTimeline<ShapeRule> applyChanges(
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const RuleChanges& changes
|
||||||
|
) {
|
||||||
ContinuousTimeline<ShapeRule> result(shapeRules);
|
ContinuousTimeline<ShapeRule> result(shapeRules);
|
||||||
for (centiseconds changedRuleStart : changes) {
|
for (centiseconds changedRuleStart : changes) {
|
||||||
const Timed<ShapeRule> timedOriginalRule = *shapeRules.get(changedRuleStart);
|
const Timed<ShapeRule> timedOriginalRule = *shapeRules.get(changedRuleStart);
|
||||||
|
@ -85,14 +95,16 @@ public:
|
||||||
RuleChangeScenario(
|
RuleChangeScenario(
|
||||||
const ContinuousTimeline<ShapeRule>& originalRules,
|
const ContinuousTimeline<ShapeRule>& originalRules,
|
||||||
const RuleChanges& changes,
|
const RuleChanges& changes,
|
||||||
AnimationFunction animate) :
|
const AnimationFunction& animate
|
||||||
|
) :
|
||||||
changedRules(applyChanges(originalRules, changes)),
|
changedRules(applyChanges(originalRules, changes)),
|
||||||
animation(animate(changedRules)),
|
animation(animate(changedRules)),
|
||||||
staticSegments(getStaticSegments(changedRules, animation)) {}
|
staticSegments(getStaticSegments(changedRules, animation))
|
||||||
|
{}
|
||||||
|
|
||||||
bool isBetterThan(const RuleChangeScenario& rhs) const {
|
bool isBetterThan(const RuleChangeScenario& rhs) const {
|
||||||
// We want zero static segments
|
// We want zero static segments
|
||||||
if (staticSegments.size() == 0 && rhs.staticSegments.size() > 0) return true;
|
if (staticSegments.empty() && !rhs.staticSegments.empty()) return true;
|
||||||
|
|
||||||
// Short shapes are better than long ones. Minimize sum-of-squares.
|
// Short shapes are better than long ones. Minimize sum-of-squares.
|
||||||
if (getSumOfShapeDurationSquares() < rhs.getSumOfShapeDurationSquares()) return true;
|
if (getSumOfShapeDurationSquares() < rhs.getSumOfShapeDurationSquares()) return true;
|
||||||
|
@ -114,10 +126,17 @@ private:
|
||||||
vector<TimeRange> staticSegments;
|
vector<TimeRange> staticSegments;
|
||||||
|
|
||||||
double getSumOfShapeDurationSquares() const {
|
double getSumOfShapeDurationSquares() const {
|
||||||
return std::accumulate(animation.begin(), animation.end(), 0.0, [](const double sum, const Timed<Shape>& timedShape) {
|
return std::accumulate(
|
||||||
const double duration = std::chrono::duration_cast<std::chrono::duration<double>>(timedShape.getDuration()).count();
|
animation.begin(),
|
||||||
|
animation.end(),
|
||||||
|
0.0,
|
||||||
|
[](const double sum, const Timed<Shape>& timedShape) {
|
||||||
|
const double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||||
|
timedShape.getDuration()
|
||||||
|
).count();
|
||||||
return sum + duration * duration;
|
return sum + duration * duration;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -132,8 +151,12 @@ RuleChanges getPossibleRuleChanges(const ContinuousTimeline<ShapeRule>& shapeRul
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContinuousTimeline<ShapeRule> fixStaticSegmentRules(const ContinuousTimeline<ShapeRule>& shapeRules, AnimationFunction animate) {
|
ContinuousTimeline<ShapeRule> fixStaticSegmentRules(
|
||||||
// The complexity of this function is exponential with the number of replacements. So let's cap that value.
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const AnimationFunction& animate
|
||||||
|
) {
|
||||||
|
// The complexity of this function is exponential with the number of replacements.
|
||||||
|
// So let's cap that value.
|
||||||
const int maxReplacementCount = 3;
|
const int maxReplacementCount = 3;
|
||||||
|
|
||||||
// All potential changes
|
// All potential changes
|
||||||
|
@ -149,7 +172,11 @@ ContinuousTimeline<ShapeRule> fixStaticSegmentRules(const ContinuousTimeline<Sha
|
||||||
// Only the first <replacementCount> elements of `currentRuleChanges` count
|
// Only the first <replacementCount> elements of `currentRuleChanges` count
|
||||||
auto currentRuleChanges(possibleRuleChanges);
|
auto currentRuleChanges(possibleRuleChanges);
|
||||||
do {
|
do {
|
||||||
RuleChangeScenario currentScenario(shapeRules, {currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount}, animate);
|
RuleChangeScenario currentScenario(
|
||||||
|
shapeRules,
|
||||||
|
{ currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount },
|
||||||
|
animate
|
||||||
|
);
|
||||||
if (currentScenario.isBetterThan(bestScenario)) {
|
if (currentScenario.isBetterThan(bestScenario)) {
|
||||||
bestScenario = currentScenario;
|
bestScenario = currentScenario;
|
||||||
}
|
}
|
||||||
|
@ -164,8 +191,12 @@ bool isFlexible(const ShapeRule& rule) {
|
||||||
return rule.shapeSet.size() > 1;
|
return rule.shapeSet.size() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extends the specified time range until it starts and ends with a non-flexible shape rule, if possible
|
// Extends the specified time range until it starts and ends with a non-flexible shape rule, if
|
||||||
TimeRange extendToFixedRules(const TimeRange& timeRange, const ContinuousTimeline<ShapeRule>& shapeRules) {
|
// possible
|
||||||
|
TimeRange extendToFixedRules(
|
||||||
|
const TimeRange& timeRange,
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules
|
||||||
|
) {
|
||||||
auto first = shapeRules.find(timeRange.getStart());
|
auto first = shapeRules.find(timeRange.getStart());
|
||||||
while (first != shapeRules.begin() && isFlexible(first->getValue())) {
|
while (first != shapeRules.begin() && isFlexible(first->getValue())) {
|
||||||
--first;
|
--first;
|
||||||
|
@ -174,10 +205,13 @@ TimeRange extendToFixedRules(const TimeRange& timeRange, const ContinuousTimelin
|
||||||
while (std::next(last) != shapeRules.end() && isFlexible(last->getValue())) {
|
while (std::next(last) != shapeRules.end() && isFlexible(last->getValue())) {
|
||||||
++last;
|
++last;
|
||||||
}
|
}
|
||||||
return TimeRange(first->getStart(), last->getEnd());
|
return { first->getStart(), last->getEnd() };
|
||||||
}
|
}
|
||||||
|
|
||||||
JoiningContinuousTimeline<Shape> avoidStaticSegments(const ContinuousTimeline<ShapeRule>& shapeRules, AnimationFunction animate) {
|
JoiningContinuousTimeline<Shape> avoidStaticSegments(
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const AnimationFunction& animate
|
||||||
|
) {
|
||||||
const auto animation = animate(shapeRules);
|
const auto animation = animate(shapeRules);
|
||||||
const vector<TimeRange> staticSegments = getStaticSegments(shapeRules, animation);
|
const vector<TimeRange> staticSegments = getStaticSegments(shapeRules, animation);
|
||||||
if (staticSegments.empty()) {
|
if (staticSegments.empty()) {
|
||||||
|
@ -187,11 +221,15 @@ JoiningContinuousTimeline<Shape> avoidStaticSegments(const ContinuousTimeline<Sh
|
||||||
// Modify shape rules to eliminate static segments
|
// Modify shape rules to eliminate static segments
|
||||||
ContinuousTimeline<ShapeRule> fixedShapeRules(shapeRules);
|
ContinuousTimeline<ShapeRule> fixedShapeRules(shapeRules);
|
||||||
for (const TimeRange& staticSegment : staticSegments) {
|
for (const TimeRange& staticSegment : staticSegments) {
|
||||||
// Extend time range to the left and right so we don't lose adjacent rules that might influence the animation
|
// Extend time range to the left and right so we don't lose adjacent rules that might
|
||||||
|
// influence the animation
|
||||||
const TimeRange extendedStaticSegment = extendToFixedRules(staticSegment, shapeRules);
|
const TimeRange extendedStaticSegment = extendToFixedRules(staticSegment, shapeRules);
|
||||||
|
|
||||||
// Fix shape rules within the static segment
|
// Fix shape rules within the static segment
|
||||||
const auto fixedSegmentShapeRules = fixStaticSegmentRules({extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules}, animate);
|
const auto fixedSegmentShapeRules = fixStaticSegmentRules(
|
||||||
|
{ extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules },
|
||||||
|
animate
|
||||||
|
);
|
||||||
for (const auto& timedShapeRule : fixedSegmentShapeRules) {
|
for (const auto& timedShapeRule : fixedSegmentShapeRules) {
|
||||||
fixedShapeRules.set(timedShapeRule);
|
fixedShapeRules.set(timedShapeRule);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
using AnimationFunction = std::function<JoiningContinuousTimeline<Shape>(const ContinuousTimeline<ShapeRule>&)>;
|
using AnimationFunction = std::function<JoiningContinuousTimeline<Shape>(const ContinuousTimeline<ShapeRule>&)>;
|
||||||
|
|
||||||
// Calls the specified animation function with the specified shape rules.
|
// Calls the specified animation function with the specified shape rules.
|
||||||
// If the resulting animation contains long static segments, the shape rules are tweaked and animated again.
|
// If the resulting animation contains long static segments, the shape rules are tweaked and
|
||||||
|
// animated again.
|
||||||
// Static segments happen rather often.
|
// Static segments happen rather often.
|
||||||
// See http://animateducated.blogspot.de/2016/10/lip-sync-animation-2.html?showComment=1478861729702#c2940729096183546458.
|
// See http://animateducated.blogspot.de/2016/10/lip-sync-animation-2.html?showComment=1478861729702#c2940729096183546458.
|
||||||
JoiningContinuousTimeline<Shape> avoidStaticSegments(const ContinuousTimeline<ShapeRule>& shapeRules, AnimationFunction animate);
|
JoiningContinuousTimeline<Shape> avoidStaticSegments(
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const AnimationFunction& animate
|
||||||
|
);
|
||||||
|
|
|
@ -4,9 +4,10 @@ Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet) {
|
||||||
if (targetShapeSet.find(shape) != targetShapeSet.end()) {
|
if (targetShapeSet.find(shape) != targetShapeSet.end()) {
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
Shape basicShape = getBasicShape(shape);
|
const Shape basicShape = getBasicShape(shape);
|
||||||
if (targetShapeSet.find(basicShape) == targetShapeSet.end()) {
|
if (targetShapeSet.find(basicShape) == targetShapeSet.end()) {
|
||||||
throw std::invalid_argument(fmt::format("Target shape set must contain basic shape {}.", basicShape));
|
throw std::invalid_argument(
|
||||||
|
fmt::format("Target shape set must contain basic shape {}.", basicShape));
|
||||||
}
|
}
|
||||||
return basicShape;
|
return basicShape;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +20,10 @@ ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetS
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet) {
|
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
) {
|
||||||
ContinuousTimeline<ShapeRule> result(shapeRules);
|
ContinuousTimeline<ShapeRule> result(shapeRules);
|
||||||
for (const auto& timedShapeRule : shapeRules) {
|
for (const auto& timedShapeRule : shapeRules) {
|
||||||
ShapeRule rule = timedShapeRule.getValue();
|
ShapeRule rule = timedShapeRule.getValue();
|
||||||
|
@ -29,10 +33,16 @@ ContinuousTimeline<ShapeRule> convertToTargetShapeSet(const ContinuousTimeline<S
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(const JoiningContinuousTimeline<Shape>& animation, const ShapeSet& targetShapeSet) {
|
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(
|
||||||
|
const JoiningContinuousTimeline<Shape>& animation,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
) {
|
||||||
JoiningContinuousTimeline<Shape> result(animation);
|
JoiningContinuousTimeline<Shape> result(animation);
|
||||||
for (const auto& timedShape : animation) {
|
for (const auto& timedShape : animation) {
|
||||||
result.set(timedShape.getTimeRange(), convertToTargetShapeSet(timedShape.getValue(), targetShapeSet));
|
result.set(
|
||||||
|
timedShape.getTimeRange(),
|
||||||
|
convertToTargetShapeSet(timedShape.getValue(), targetShapeSet)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,19 @@
|
||||||
// Returns the closest shape to the specified one that occurs in the target shape set.
|
// Returns the closest shape to the specified one that occurs in the target shape set.
|
||||||
Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet);
|
Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet);
|
||||||
|
|
||||||
// Replaces each shape in the specified set with the closest shape that occurs in the target shape set.
|
// Replaces each shape in the specified set with the closest shape that occurs in the target shape
|
||||||
|
// set.
|
||||||
ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetShapeSet);
|
ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetShapeSet);
|
||||||
|
|
||||||
// Replaces each shape in each rule with the closest shape that occurs in the target shape set.
|
// Replaces each shape in each rule with the closest shape that occurs in the target shape set.
|
||||||
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet);
|
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
|
||||||
|
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
);
|
||||||
|
|
||||||
// Replaces each shape in the specified animation with the closest shape that occurs in the target shape set.
|
// Replaces each shape in the specified animation with the closest shape that occurs in the target
|
||||||
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(const JoiningContinuousTimeline<Shape>& animation, const ShapeSet& targetShapeSet);
|
// shape set.
|
||||||
|
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(
|
||||||
|
const JoiningContinuousTimeline<Shape>& animation,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
);
|
||||||
|
|
|
@ -11,7 +11,7 @@ using std::map;
|
||||||
string getShapesString(const JoiningContinuousTimeline<Shape>& shapes) {
|
string getShapesString(const JoiningContinuousTimeline<Shape>& shapes) {
|
||||||
string result;
|
string result;
|
||||||
for (const auto& timedShape : shapes) {
|
for (const auto& timedShape : shapes) {
|
||||||
if (result.size()) {
|
if (!result.empty()) {
|
||||||
result.append(" ");
|
result.append(" ");
|
||||||
}
|
}
|
||||||
result.append(boost::lexical_cast<std::string>(timedShape.getValue()));
|
result.append(boost::lexical_cast<std::string>(timedShape.getValue()));
|
||||||
|
@ -44,12 +44,10 @@ Shape getRepresentativeShape(const JoiningTimeline<Shape>& timeline) {
|
||||||
struct ShapeReduction {
|
struct ShapeReduction {
|
||||||
ShapeReduction(const JoiningTimeline<Shape>& sourceShapes) :
|
ShapeReduction(const JoiningTimeline<Shape>& sourceShapes) :
|
||||||
sourceShapes(sourceShapes),
|
sourceShapes(sourceShapes),
|
||||||
shape(getRepresentativeShape(sourceShapes))
|
shape(getRepresentativeShape(sourceShapes)) {}
|
||||||
{}
|
|
||||||
|
|
||||||
ShapeReduction(const JoiningTimeline<Shape>& sourceShapes, TimeRange candidateRange) :
|
ShapeReduction(const JoiningTimeline<Shape>& sourceShapes, TimeRange candidateRange) :
|
||||||
ShapeReduction(JoiningBoundedTimeline<Shape>(candidateRange, sourceShapes))
|
ShapeReduction(JoiningBoundedTimeline<Shape>(candidateRange, sourceShapes)) {}
|
||||||
{}
|
|
||||||
|
|
||||||
JoiningTimeline<Shape> sourceShapes;
|
JoiningTimeline<Shape> sourceShapes;
|
||||||
Shape shape;
|
Shape shape;
|
||||||
|
@ -57,7 +55,8 @@ struct ShapeReduction {
|
||||||
|
|
||||||
// Returns a time range of candidate shapes for the next shape to draw.
|
// Returns a time range of candidate shapes for the next shape to draw.
|
||||||
// Guaranteed to be non-empty.
|
// Guaranteed to be non-empty.
|
||||||
TimeRange getNextMinimalCandidateRange(const JoiningContinuousTimeline<Shape>& sourceShapes, const TimeRange targetRange, const centiseconds writePosition) {
|
TimeRange getNextMinimalCandidateRange(const JoiningContinuousTimeline<Shape>& sourceShapes,
|
||||||
|
const TimeRange targetRange, const centiseconds writePosition) {
|
||||||
if (sourceShapes.empty()) {
|
if (sourceShapes.empty()) {
|
||||||
throw std::invalid_argument("Cannot determine candidate range for empty source timeline.");
|
throw std::invalid_argument("Cannot determine candidate range for empty source timeline.");
|
||||||
}
|
}
|
||||||
|
@ -70,12 +69,15 @@ TimeRange getNextMinimalCandidateRange(const JoiningContinuousTimeline<Shape>& s
|
||||||
const centiseconds remainingTargetDuration = writePosition - targetRange.getStart();
|
const centiseconds remainingTargetDuration = writePosition - targetRange.getStart();
|
||||||
const bool canFitOneOrLess = remainingTargetDuration <= minShapeDuration;
|
const bool canFitOneOrLess = remainingTargetDuration <= minShapeDuration;
|
||||||
const bool canFitTwo = remainingTargetDuration >= 2 * minShapeDuration;
|
const bool canFitTwo = remainingTargetDuration >= 2 * minShapeDuration;
|
||||||
const centiseconds duration = canFitOneOrLess || canFitTwo ? minShapeDuration : remainingTargetDuration / 2;
|
const centiseconds duration = canFitOneOrLess || canFitTwo
|
||||||
|
? minShapeDuration
|
||||||
|
: remainingTargetDuration / 2;
|
||||||
|
|
||||||
TimeRange candidateRange(writePosition - duration, writePosition);
|
TimeRange candidateRange(writePosition - duration, writePosition);
|
||||||
if (writePosition == targetRange.getEnd()) {
|
if (writePosition == targetRange.getEnd()) {
|
||||||
// This is the first iteration.
|
// This is the first iteration.
|
||||||
// Extend the candidate range to the right in order to consider all source shapes after the target range.
|
// Extend the candidate range to the right in order to consider all source shapes after the
|
||||||
|
// target range.
|
||||||
candidateRange.setEndIfLater(sourceShapes.getRange().getEnd());
|
candidateRange.setEndIfLater(sourceShapes.getRange().getEnd());
|
||||||
}
|
}
|
||||||
if (candidateRange.getStart() >= sourceShapes.getRange().getEnd()) {
|
if (candidateRange.getStart() >= sourceShapes.getRange().getEnd()) {
|
||||||
|
@ -92,19 +94,31 @@ TimeRange getNextMinimalCandidateRange(const JoiningContinuousTimeline<Shape>& s
|
||||||
return candidateRange;
|
return candidateRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShapeReduction getNextShapeReduction(const JoiningContinuousTimeline<Shape>& sourceShapes, const TimeRange targetRange, centiseconds writePosition) {
|
ShapeReduction getNextShapeReduction(
|
||||||
|
const JoiningContinuousTimeline<Shape>& sourceShapes,
|
||||||
|
const TimeRange targetRange,
|
||||||
|
centiseconds writePosition
|
||||||
|
) {
|
||||||
// Determine the next time range of candidate shapes. Consider two scenarios:
|
// Determine the next time range of candidate shapes. Consider two scenarios:
|
||||||
|
|
||||||
// ... the shortest-possible candidate range
|
// ... the shortest-possible candidate range
|
||||||
const ShapeReduction minReduction(sourceShapes, getNextMinimalCandidateRange(sourceShapes, targetRange, writePosition));
|
const ShapeReduction minReduction(sourceShapes,
|
||||||
|
getNextMinimalCandidateRange(sourceShapes, targetRange, writePosition));
|
||||||
|
|
||||||
// ... a candidate range extended to the left to fully encompass its left-most shape
|
// ... a candidate range extended to the left to fully encompass its left-most shape
|
||||||
const ShapeReduction extendedReduction(sourceShapes,
|
const ShapeReduction extendedReduction(sourceShapes,
|
||||||
{minReduction.sourceShapes.begin()->getStart(), minReduction.sourceShapes.getRange().getEnd()});
|
{
|
||||||
|
minReduction.sourceShapes.begin()->getStart(),
|
||||||
|
minReduction.sourceShapes.getRange().getEnd()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Determine the shape that might be picked *next* if we choose the shortest-possible candidate range now
|
// Determine the shape that might be picked *next* if we choose the shortest-possible candidate
|
||||||
const ShapeReduction nextReduction(sourceShapes,
|
// range now
|
||||||
getNextMinimalCandidateRange(sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart()));
|
const ShapeReduction nextReduction(
|
||||||
|
sourceShapes,
|
||||||
|
getNextMinimalCandidateRange(sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart())
|
||||||
|
);
|
||||||
|
|
||||||
const bool minEqualsExtended = minReduction.shape == extendedReduction.shape;
|
const bool minEqualsExtended = minReduction.shape == extendedReduction.shape;
|
||||||
const bool extendedIsSpecial = extendedReduction.shape != minReduction.shape
|
const bool extendedIsSpecial = extendedReduction.shape != minReduction.shape
|
||||||
|
@ -113,8 +127,10 @@ ShapeReduction getNextShapeReduction(const JoiningContinuousTimeline<Shape>& sou
|
||||||
return minEqualsExtended || extendedIsSpecial ? extendedReduction : minReduction;
|
return minEqualsExtended || extendedIsSpecial ? extendedReduction : minReduction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifies the timing of the given animation to fit into the specified target time range without jitter.
|
// Modifies the timing of the given animation to fit into the specified target time range without
|
||||||
JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>& sourceShapes, const TimeRange targetRange) {
|
// jitter.
|
||||||
|
JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>& sourceShapes,
|
||||||
|
const TimeRange targetRange) {
|
||||||
logTimedEvent("segment", targetRange, getShapesString(sourceShapes));
|
logTimedEvent("segment", targetRange, getShapesString(sourceShapes));
|
||||||
|
|
||||||
JoiningContinuousTimeline<Shape> result(targetRange, Shape::X);
|
JoiningContinuousTimeline<Shape> result(targetRange, Shape::X);
|
||||||
|
@ -125,7 +141,8 @@ JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>&
|
||||||
while (writePosition > targetRange.getStart()) {
|
while (writePosition > targetRange.getStart()) {
|
||||||
|
|
||||||
// Decide which shape to show next, possibly discarding short shapes
|
// Decide which shape to show next, possibly discarding short shapes
|
||||||
const ShapeReduction shapeReduction = getNextShapeReduction(sourceShapes, targetRange, writePosition);
|
const ShapeReduction shapeReduction =
|
||||||
|
getNextShapeReduction(sourceShapes, targetRange, writePosition);
|
||||||
|
|
||||||
// Determine how long to display the shape
|
// Determine how long to display the shape
|
||||||
TimeRange targetShapeRange(shapeReduction.sourceShapes.getRange());
|
TimeRange targetShapeRange(shapeReduction.sourceShapes.getRange());
|
||||||
|
@ -144,7 +161,11 @@ JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>&
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>& animation, TimeRange sourceRange, TimeRange targetRange) {
|
JoiningContinuousTimeline<Shape> retime(
|
||||||
|
const JoiningContinuousTimeline<Shape>& animation,
|
||||||
|
TimeRange sourceRange,
|
||||||
|
TimeRange targetRange
|
||||||
|
) {
|
||||||
const auto sourceShapes = JoiningContinuousTimeline<Shape>(sourceRange, Shape::X, animation);
|
const auto sourceShapes = JoiningContinuousTimeline<Shape>(sourceRange, Shape::X, animation);
|
||||||
return retime(sourceShapes, targetRange);
|
return retime(sourceShapes, targetRange);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +181,12 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
|
||||||
JoiningContinuousTimeline<MouthState> segments(animation.getRange(), MouthState::Idle);
|
JoiningContinuousTimeline<MouthState> segments(animation.getRange(), MouthState::Idle);
|
||||||
for (const auto& timedShape : animation) {
|
for (const auto& timedShape : animation) {
|
||||||
const Shape shape = timedShape.getValue();
|
const Shape shape = timedShape.getValue();
|
||||||
const MouthState mouthState = shape == Shape::X ? MouthState::Idle : shape == Shape::A ? MouthState::Closed : MouthState::Open;
|
const MouthState mouthState =
|
||||||
|
shape == Shape::X
|
||||||
|
? MouthState::Idle
|
||||||
|
: shape == Shape::A
|
||||||
|
? MouthState::Closed
|
||||||
|
: MouthState::Open;
|
||||||
segments.set(timedShape.getTimeRange(), mouthState);
|
segments.set(timedShape.getTimeRange(), mouthState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +197,8 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
|
||||||
|
|
||||||
// Make sure all open and closed segments are long enough to register visually.
|
// Make sure all open and closed segments are long enough to register visually.
|
||||||
JoiningContinuousTimeline<Shape> result(animation.getRange(), Shape::X);
|
JoiningContinuousTimeline<Shape> result(animation.getRange(), Shape::X);
|
||||||
// ... we're filling the result timeline from right to left, so `resultStart` points to the earliest shape already written
|
// ... we're filling the result timeline from right to left, so `resultStart` points to the
|
||||||
|
// earliest shape already written
|
||||||
centiseconds resultStart = result.getRange().getEnd();
|
centiseconds resultStart = result.getRange().getEnd();
|
||||||
for (auto segmentIt = segments.rbegin(); segmentIt != segments.rend(); ++segmentIt) {
|
for (auto segmentIt = segments.rbegin(); segmentIt != segments.rend(); ++segmentIt) {
|
||||||
// We don't care about idle shapes at this point.
|
// We don't care about idle shapes at this point.
|
||||||
|
@ -188,26 +215,40 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
|
||||||
resultStart = targetRange.getStart();
|
resultStart = targetRange.getStart();
|
||||||
} else {
|
} else {
|
||||||
// The segment is too short; we have to extend it to the left.
|
// The segment is too short; we have to extend it to the left.
|
||||||
// Find all adjacent segments to our left that are also too short, then distribute them evenly.
|
// Find all adjacent segments to our left that are also too short, then distribute them
|
||||||
|
// evenly.
|
||||||
const auto begin = segmentIt;
|
const auto begin = segmentIt;
|
||||||
auto end = std::next(begin);
|
auto end = std::next(begin);
|
||||||
while (end != segments.rend() && end->getValue() != MouthState::Idle && end->getDuration() < minSegmentDuration) ++end;
|
while (
|
||||||
|
end != segments.rend()
|
||||||
|
&& end->getValue() != MouthState::Idle
|
||||||
|
&& end->getDuration() < minSegmentDuration
|
||||||
|
) {
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine how much we should extend the entire set of short segments to the left
|
// Determine how much we should extend the entire set of short segments to the left
|
||||||
const size_t shortSegmentCount = std::distance(begin, end);
|
const size_t shortSegmentCount = std::distance(begin, end);
|
||||||
const centiseconds desiredDuration = minSegmentDuration * shortSegmentCount;
|
const centiseconds desiredDuration = minSegmentDuration * shortSegmentCount;
|
||||||
const centiseconds currentDuration = begin->getEnd() - std::prev(end)->getStart();
|
const centiseconds currentDuration = begin->getEnd() - std::prev(end)->getStart();
|
||||||
const centiseconds desiredExtensionDuration = desiredDuration - currentDuration;
|
const centiseconds desiredExtensionDuration = desiredDuration - currentDuration;
|
||||||
const centiseconds availableExtensionDuration = end != segments.rend() ? end->getDuration() - 1_cs : 0_cs;
|
const centiseconds availableExtensionDuration = end != segments.rend()
|
||||||
const centiseconds extensionDuration = std::min({desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration});
|
? end->getDuration() - 1_cs
|
||||||
|
: 0_cs;
|
||||||
|
const centiseconds extensionDuration = std::min({
|
||||||
|
desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration
|
||||||
|
});
|
||||||
|
|
||||||
// Distribute available time range evenly among all short segments
|
// Distribute available time range evenly among all short segments
|
||||||
const centiseconds shortSegmentsTargetStart = std::prev(end)->getStart() - extensionDuration;
|
const centiseconds shortSegmentsTargetStart =
|
||||||
|
std::prev(end)->getStart() - extensionDuration;
|
||||||
for (auto shortSegmentIt = begin; shortSegmentIt != end; ++shortSegmentIt) {
|
for (auto shortSegmentIt = begin; shortSegmentIt != end; ++shortSegmentIt) {
|
||||||
size_t remainingShortSegmentCount = std::distance(shortSegmentIt, end);
|
size_t remainingShortSegmentCount = std::distance(shortSegmentIt, end);
|
||||||
const centiseconds segmentDuration = (resultStart - shortSegmentsTargetStart) / remainingShortSegmentCount;
|
const centiseconds segmentDuration = (resultStart - shortSegmentsTargetStart) /
|
||||||
|
remainingShortSegmentCount;
|
||||||
const TimeRange segmentTargetRange(resultStart - segmentDuration, resultStart);
|
const TimeRange segmentTargetRange(resultStart - segmentDuration, resultStart);
|
||||||
const auto retimedSegment = retime(animation, shortSegmentIt->getTimeRange(), segmentTargetRange);
|
const auto retimedSegment =
|
||||||
|
retime(animation, shortSegmentIt->getTimeRange(), segmentTargetRange);
|
||||||
for (const auto& timedShape : retimedSegment) {
|
for (const auto& timedShape : retimedSegment) {
|
||||||
result.set(timedShape);
|
result.set(timedShape);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "core/Shape.h"
|
#include "core/Shape.h"
|
||||||
#include "time/ContinuousTimeline.h"
|
#include "time/ContinuousTimeline.h"
|
||||||
|
|
||||||
// Changes the timing of an existing animation to reduce jitter and to make sure all shapes register visually.
|
// Changes the timing of an existing animation to reduce jitter and to make sure all shapes register
|
||||||
|
// visually.
|
||||||
// In some cases, shapes may be omitted.
|
// In some cases, shapes may be omitted.
|
||||||
JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<Shape>& animation);
|
JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<Shape>& animation);
|
||||||
|
|
|
@ -19,21 +19,30 @@ JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Sh
|
||||||
|
|
||||||
centiseconds tweenStart, tweenDuration;
|
centiseconds tweenStart, tweenDuration;
|
||||||
switch (tweenTiming) {
|
switch (tweenTiming) {
|
||||||
case TweenTiming::Early: {
|
case TweenTiming::Early:
|
||||||
|
{
|
||||||
tweenDuration = std::min(firstTimeRange.getDuration() / 3, maxTweenDuration);
|
tweenDuration = std::min(firstTimeRange.getDuration() / 3, maxTweenDuration);
|
||||||
tweenStart = firstTimeRange.getEnd() - tweenDuration;
|
tweenStart = firstTimeRange.getEnd() - tweenDuration;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TweenTiming::Centered: {
|
case TweenTiming::Centered:
|
||||||
tweenDuration = std::min({firstTimeRange.getDuration() / 4, secondTimeRange.getDuration() / 4, maxTweenDuration});
|
{
|
||||||
|
tweenDuration = std::min({
|
||||||
|
firstTimeRange.getDuration() / 4, secondTimeRange.getDuration() / 4, maxTweenDuration
|
||||||
|
});
|
||||||
tweenStart = firstTimeRange.getEnd() - tweenDuration / 2;
|
tweenStart = firstTimeRange.getEnd() - tweenDuration / 2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TweenTiming::Late: {
|
case TweenTiming::Late:
|
||||||
|
{
|
||||||
tweenDuration = std::min(secondTimeRange.getDuration() / 3, maxTweenDuration);
|
tweenDuration = std::min(secondTimeRange.getDuration() / 3, maxTweenDuration);
|
||||||
tweenStart = secondTimeRange.getStart();
|
tweenStart = secondTimeRange.getStart();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Unexpected tween timing.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tweenDuration < minTweenDuration) return;
|
if (tweenDuration < minTweenDuration) return;
|
||||||
|
|
|
@ -28,7 +28,11 @@ inline AudioClip::value_type SafeSampleReader::operator()(AudioClip::size_type i
|
||||||
throw invalid_argument(fmt::format("Cannot read from sample index {}. Index < 0.", index));
|
throw invalid_argument(fmt::format("Cannot read from sample index {}. Index < 0.", index));
|
||||||
}
|
}
|
||||||
if (index >= size) {
|
if (index >= size) {
|
||||||
throw invalid_argument(fmt::format("Cannot read from sample index {}. Clip size is {}.", index, size));
|
throw invalid_argument(fmt::format(
|
||||||
|
"Cannot read from sample index {}. Clip size is {}.",
|
||||||
|
index,
|
||||||
|
size
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (index == lastIndex) {
|
if (index == lastIndex) {
|
||||||
return lastSample;
|
return lastSample;
|
||||||
|
@ -51,7 +55,7 @@ AudioClip::iterator AudioClip::end() const {
|
||||||
return SampleIterator(*this, size());
|
return SampleIterator(*this, size());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<AudioClip> operator|(std::unique_ptr<AudioClip> clip, AudioEffect effect) {
|
std::unique_ptr<AudioClip> operator|(std::unique_ptr<AudioClip> clip, const AudioEffect& effect) {
|
||||||
return effect(std::move(clip));
|
return effect(std::move(clip));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ private:
|
||||||
|
|
||||||
using AudioEffect = std::function<std::unique_ptr<AudioClip>(std::unique_ptr<AudioClip>)>;
|
using AudioEffect = std::function<std::unique_ptr<AudioClip>(std::unique_ptr<AudioClip>)>;
|
||||||
|
|
||||||
std::unique_ptr<AudioClip> operator|(std::unique_ptr<AudioClip> clip, AudioEffect effect);
|
std::unique_ptr<AudioClip> operator|(std::unique_ptr<AudioClip> clip, const AudioEffect& effect);
|
||||||
|
|
||||||
using SampleReader = AudioClip::SampleReader;
|
using SampleReader = AudioClip::SampleReader;
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,19 @@ unique_ptr<AudioClip> DcOffset::clone() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
SampleReader DcOffset::createUnsafeSampleReader() const {
|
SampleReader DcOffset::createUnsafeSampleReader() const {
|
||||||
return [read = inputClip->createSampleReader(), factor = factor, offset = offset](size_type index) {
|
return [
|
||||||
float sample = read(index);
|
read = inputClip->createSampleReader(),
|
||||||
|
factor = factor,
|
||||||
|
offset = offset
|
||||||
|
](size_type index) {
|
||||||
|
const float sample = read(index);
|
||||||
return sample * factor + offset;
|
return sample * factor + offset;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
float getDcOffset(const AudioClip& audioClip) {
|
float getDcOffset(const AudioClip& audioClip) {
|
||||||
int flatMeanSampleCount, fadingMeanSampleCount;
|
int flatMeanSampleCount, fadingMeanSampleCount;
|
||||||
int sampleRate = audioClip.getSampleRate();
|
const int sampleRate = audioClip.getSampleRate();
|
||||||
if (audioClip.size() > 4 * sampleRate) {
|
if (audioClip.size() > 4 * sampleRate) {
|
||||||
// Long audio file. Average over the first 3 seconds, then fade out over the 4th.
|
// Long audio file. Average over the first 3 seconds, then fade out over the 4th.
|
||||||
flatMeanSampleCount = 3 * sampleRate;
|
flatMeanSampleCount = 3 * sampleRate;
|
||||||
|
@ -34,31 +38,32 @@ float getDcOffset(const AudioClip& audioClip) {
|
||||||
fadingMeanSampleCount = 0;
|
fadingMeanSampleCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto read = audioClip.createSampleReader();
|
const auto read = audioClip.createSampleReader();
|
||||||
double sum = 0;
|
double sum = 0;
|
||||||
for (int i = 0; i < flatMeanSampleCount; ++i) {
|
for (int i = 0; i < flatMeanSampleCount; ++i) {
|
||||||
sum += read(i);
|
sum += read(i);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < fadingMeanSampleCount; ++i) {
|
for (int i = 0; i < fadingMeanSampleCount; ++i) {
|
||||||
double weight = static_cast<double>(fadingMeanSampleCount - i) / fadingMeanSampleCount;
|
const double weight =
|
||||||
|
static_cast<double>(fadingMeanSampleCount - i) / fadingMeanSampleCount;
|
||||||
sum += read(flatMeanSampleCount + i) * weight;
|
sum += read(flatMeanSampleCount + i) * weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
double totalWeight = flatMeanSampleCount + (fadingMeanSampleCount + 1) / 2.0;
|
const double totalWeight = flatMeanSampleCount + (fadingMeanSampleCount + 1) / 2.0;
|
||||||
double offset = sum / totalWeight;
|
const double offset = sum / totalWeight;
|
||||||
return static_cast<float>(offset);
|
return static_cast<float>(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioEffect addDcOffset(float offset, float epsilon) {
|
AudioEffect addDcOffset(float offset, float epsilon) {
|
||||||
return [offset, epsilon](unique_ptr<AudioClip> inputClip) -> unique_ptr<AudioClip> {
|
return [offset, epsilon](unique_ptr<AudioClip> inputClip) -> unique_ptr<AudioClip> {
|
||||||
if (std::abs(offset) < epsilon) return std::move(inputClip);
|
if (std::abs(offset) < epsilon) return inputClip;
|
||||||
return make_unique<DcOffset>(std::move(inputClip), offset);
|
return make_unique<DcOffset>(std::move(inputClip), offset);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioEffect removeDcOffset(float epsilon) {
|
AudioEffect removeDcOffset(float epsilon) {
|
||||||
return [epsilon](unique_ptr<AudioClip> inputClip) {
|
return [epsilon](unique_ptr<AudioClip> inputClip) {
|
||||||
float offset = getDcOffset(*inputClip);
|
const float offset = getDcOffset(*inputClip);
|
||||||
return std::move(inputClip) | addDcOffset(-offset, epsilon);
|
return std::move(inputClip) | addDcOffset(-offset, epsilon);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ std::string vorbisErrorToString(int64_t errorCode) {
|
||||||
case OV_EBADPACKET:
|
case OV_EBADPACKET:
|
||||||
return "Error in packet.";
|
return "Error in packet.";
|
||||||
case OV_EBADLINK:
|
case OV_EBADLINK:
|
||||||
return "The given link exists in the Vorbis data stream, but is not decipherable due to garbacge or corruption.";
|
return "The given link exists in the Vorbis data stream, but is not decipherable due to garbage or corruption.";
|
||||||
case OV_ENOSEEK:
|
case OV_ENOSEEK:
|
||||||
return "The given stream is not seekable.";
|
return "The given stream is not seekable.";
|
||||||
default:
|
default:
|
||||||
|
@ -70,7 +70,7 @@ int seekCallback(void* dataSource, ogg_int64_t offset, int origin) {
|
||||||
|
|
||||||
ifstream& stream = *static_cast<ifstream*>(dataSource);
|
ifstream& stream = *static_cast<ifstream*>(dataSource);
|
||||||
stream.seekg(offset, seekDirections.at(origin));
|
stream.seekg(offset, seekDirections.at(origin));
|
||||||
stream.clear(); // In case we seeked to EOF
|
stream.clear(); // In case we sought to EOF
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,26 +82,13 @@ long tellCallback(void* dataSource) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAII wrapper around OggVorbis_File
|
// RAII wrapper around OggVorbis_File
|
||||||
class OggVorbisFile {
|
class OggVorbisFile final {
|
||||||
public:
|
public:
|
||||||
|
OggVorbisFile(const path& filePath);
|
||||||
|
|
||||||
OggVorbisFile(const OggVorbisFile&) = delete;
|
OggVorbisFile(const OggVorbisFile&) = delete;
|
||||||
OggVorbisFile& operator=(const OggVorbisFile&) = delete;
|
OggVorbisFile& operator=(const OggVorbisFile&) = delete;
|
||||||
|
|
||||||
OggVorbisFile(const path& filePath) :
|
|
||||||
stream(openFile(filePath))
|
|
||||||
{
|
|
||||||
// Throw only on badbit, not on failbit.
|
|
||||||
// Ogg Vorbis expects read operations past the end of the file to
|
|
||||||
// succeed, not to throw.
|
|
||||||
stream.exceptions(ifstream::badbit);
|
|
||||||
|
|
||||||
// Ogg Vorbis normally uses the `FILE` API from the C standard library.
|
|
||||||
// This doesn't handle Unicode paths on Windows.
|
|
||||||
// Use wrapper functions around `ifstream` instead.
|
|
||||||
const ov_callbacks callbacks{readCallback, seekCallback, nullptr, tellCallback};
|
|
||||||
throwOnError(ov_open_callbacks(&stream, &oggVorbisHandle, nullptr, 0, callbacks));
|
|
||||||
}
|
|
||||||
|
|
||||||
OggVorbis_File* get() {
|
OggVorbis_File* get() {
|
||||||
return &oggVorbisHandle;
|
return &oggVorbisHandle;
|
||||||
}
|
}
|
||||||
|
@ -115,6 +102,22 @@ private:
|
||||||
ifstream stream;
|
ifstream stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
OggVorbisFile::OggVorbisFile(const path& filePath) :
|
||||||
|
oggVorbisHandle(),
|
||||||
|
stream(openFile(filePath))
|
||||||
|
{
|
||||||
|
// Throw only on badbit, not on failbit.
|
||||||
|
// Ogg Vorbis expects read operations past the end of the file to
|
||||||
|
// succeed, not to throw.
|
||||||
|
stream.exceptions(ifstream::badbit);
|
||||||
|
|
||||||
|
// Ogg Vorbis normally uses the `FILE` API from the C standard library.
|
||||||
|
// This doesn't handle Unicode paths on Windows.
|
||||||
|
// Use wrapper functions around `ifstream` instead.
|
||||||
|
const ov_callbacks callbacks { readCallback, seekCallback, nullptr, tellCallback };
|
||||||
|
throwOnError(ov_open_callbacks(&stream, &oggVorbisHandle, nullptr, 0, callbacks));
|
||||||
|
}
|
||||||
|
|
||||||
OggVorbisFileReader::OggVorbisFileReader(const path& filePath) :
|
OggVorbisFileReader::OggVorbisFileReader(const path& filePath) :
|
||||||
filePath(filePath)
|
filePath(filePath)
|
||||||
{
|
{
|
||||||
|
@ -153,7 +156,7 @@ SampleReader OggVorbisFileReader::createUnsafeSampleReader() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Downmix channels
|
// Downmix channels
|
||||||
size_type bufferIndex = index - bufferStart;
|
const size_type bufferIndex = index - bufferStart;
|
||||||
value_type sum = 0.0f;
|
value_type sum = 0.0f;
|
||||||
for (int channel = 0; channel < channelCount; ++channel) {
|
for (int channel = 0; channel < channelCount; ++channel) {
|
||||||
sum += buffer[channel][bufferIndex];
|
sum += buffer[channel][bufferIndex];
|
||||||
|
|
|
@ -17,7 +17,10 @@ SampleRateConverter::SampleRateConverter(unique_ptr<AudioClip> inputClip, int ou
|
||||||
throw invalid_argument("Sample rate must be positive.");
|
throw invalid_argument("Sample rate must be positive.");
|
||||||
}
|
}
|
||||||
if (this->inputClip->getSampleRate() < outputSampleRate) {
|
if (this->inputClip->getSampleRate() < outputSampleRate) {
|
||||||
throw invalid_argument(fmt::format("Upsampling not supported. Input sample rate must not be below {}Hz.", outputSampleRate));
|
throw invalid_argument(fmt::format(
|
||||||
|
"Upsampling not supported. Input sample rate must not be below {}Hz.",
|
||||||
|
outputSampleRate
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,11 +33,11 @@ float mean(double inputStart, double inputEnd, const SampleReader& read) {
|
||||||
double sum = 0;
|
double sum = 0;
|
||||||
|
|
||||||
// ... first sample (weight <= 1)
|
// ... first sample (weight <= 1)
|
||||||
int64_t startIndex = static_cast<int64_t>(inputStart);
|
const int64_t startIndex = static_cast<int64_t>(inputStart);
|
||||||
sum += read(startIndex) * ((startIndex + 1) - inputStart);
|
sum += read(startIndex) * ((startIndex + 1) - inputStart);
|
||||||
|
|
||||||
// ... middle samples (weight 1 each)
|
// ... middle samples (weight 1 each)
|
||||||
int64_t endIndex = static_cast<int64_t>(inputEnd);
|
const int64_t endIndex = static_cast<int64_t>(inputEnd);
|
||||||
for (int64_t index = startIndex + 1; index < endIndex; ++index) {
|
for (int64_t index = startIndex + 1; index < endIndex; ++index) {
|
||||||
sum += read(index);
|
sum += read(index);
|
||||||
}
|
}
|
||||||
|
@ -48,9 +51,14 @@ float mean(double inputStart, double inputEnd, const SampleReader& read) {
|
||||||
}
|
}
|
||||||
|
|
||||||
SampleReader SampleRateConverter::createUnsafeSampleReader() const {
|
SampleReader SampleRateConverter::createUnsafeSampleReader() const {
|
||||||
return[read = inputClip->createSampleReader(), downscalingFactor = downscalingFactor, size = inputClip->size()](size_type index) {
|
return [
|
||||||
double inputStart = index * downscalingFactor;
|
read = inputClip->createSampleReader(),
|
||||||
double inputEnd = std::min((index + 1) * downscalingFactor, static_cast<double>(size));
|
downscalingFactor = downscalingFactor,
|
||||||
|
size = inputClip->size()
|
||||||
|
](size_type index) {
|
||||||
|
const double inputStart = index * downscalingFactor;
|
||||||
|
const double inputEnd =
|
||||||
|
std::min((index + 1) * downscalingFactor, static_cast<double>(size));
|
||||||
return mean(inputStart, inputEnd, read);
|
return mean(inputStart, inputEnd, read);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include <format.h>
|
#include <format.h>
|
||||||
#include "WaveFileReader.h"
|
#include "WaveFileReader.h"
|
||||||
#include "ioTools.h"
|
#include "ioTools.h"
|
||||||
|
#include <iostream>
|
||||||
#include "tools/platformTools.h"
|
#include "tools/platformTools.h"
|
||||||
#include "tools/fileTools.h"
|
#include "tools/fileTools.h"
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ namespace Codec {
|
||||||
|
|
||||||
string codecToString(int codec);
|
string codecToString(int codec);
|
||||||
|
|
||||||
WaveFileReader::WaveFileReader(path filePath) :
|
WaveFileReader::WaveFileReader(const path& filePath) :
|
||||||
filePath(filePath),
|
filePath(filePath),
|
||||||
formatInfo {}
|
formatInfo {}
|
||||||
{
|
{
|
||||||
|
@ -43,7 +44,7 @@ WaveFileReader::WaveFileReader(path filePath) :
|
||||||
file.seekg(0);
|
file.seekg(0);
|
||||||
|
|
||||||
auto remaining = [&](int byteCount) {
|
auto remaining = [&](int byteCount) {
|
||||||
std::streamoff filePosition = file.tellg();
|
const std::streamoff filePosition = file.tellg();
|
||||||
return byteCount <= fileSize - filePosition;
|
return byteCount <= fileSize - filePosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ WaveFileReader::WaveFileReader(path filePath) :
|
||||||
if (!remaining(10)) {
|
if (!remaining(10)) {
|
||||||
throw runtime_error("WAVE file is corrupt. Header not found.");
|
throw runtime_error("WAVE file is corrupt. Header not found.");
|
||||||
}
|
}
|
||||||
uint32_t rootChunkId = read<uint32_t>(file);
|
auto rootChunkId = read<uint32_t>(file);
|
||||||
if (rootChunkId != fourcc('R', 'I', 'F', 'F')) {
|
if (rootChunkId != fourcc('R', 'I', 'F', 'F')) {
|
||||||
throw runtime_error("Unknown file format. Only WAVE files are supported.");
|
throw runtime_error("Unknown file format. Only WAVE files are supported.");
|
||||||
}
|
}
|
||||||
|
@ -67,7 +68,8 @@ WaveFileReader::WaveFileReader(path filePath) :
|
||||||
uint32_t chunkId = read<uint32_t>(file);
|
uint32_t chunkId = read<uint32_t>(file);
|
||||||
int chunkSize = read<uint32_t>(file);
|
int chunkSize = read<uint32_t>(file);
|
||||||
switch (chunkId) {
|
switch (chunkId) {
|
||||||
case fourcc('f', 'm', 't', ' '): {
|
case fourcc('f', 'm', 't', ' '):
|
||||||
|
{
|
||||||
// Read relevant data
|
// Read relevant data
|
||||||
uint16_t codec = read<uint16_t>(file);
|
uint16_t codec = read<uint16_t>(file);
|
||||||
formatInfo.channelCount = read<uint16_t>(file);
|
formatInfo.channelCount = read<uint16_t>(file);
|
||||||
|
@ -77,15 +79,15 @@ WaveFileReader::WaveFileReader(path filePath) :
|
||||||
int bitsPerSample = read<uint16_t>(file);
|
int bitsPerSample = read<uint16_t>(file);
|
||||||
|
|
||||||
// We've read 16 bytes so far. Skip the remainder.
|
// We've read 16 bytes so far. Skip the remainder.
|
||||||
file.seekg(roundToEven(chunkSize) - 16, file.cur);
|
file.seekg(roundToEven(chunkSize) - 16, std::ios_base::cur);
|
||||||
|
|
||||||
// Determine sample format
|
// Determine sample format
|
||||||
int bytesPerSample;
|
int bytesPerSample;
|
||||||
switch (codec) {
|
switch (codec) {
|
||||||
case Codec::Pcm:
|
case Codec::Pcm:
|
||||||
// Determine sample size.
|
// Determine sample size.
|
||||||
// According to the WAVE standard, sample sizes that are not multiples of 8 bits
|
// According to the WAVE standard, sample sizes that are not multiples of 8
|
||||||
// (e.g. 12 bits) can be treated like the next-larger byte size.
|
// bits (e.g. 12 bits) can be treated like the next-larger byte size.
|
||||||
if (bitsPerSample == 8) {
|
if (bitsPerSample == 8) {
|
||||||
formatInfo.sampleFormat = SampleFormat::UInt8;
|
formatInfo.sampleFormat = SampleFormat::UInt8;
|
||||||
bytesPerSample = 1;
|
bytesPerSample = 1;
|
||||||
|
@ -108,26 +110,31 @@ WaveFileReader::WaveFileReader(path filePath) :
|
||||||
formatInfo.sampleFormat = SampleFormat::Float32;
|
formatInfo.sampleFormat = SampleFormat::Float32;
|
||||||
bytesPerSample = 4;
|
bytesPerSample = 4;
|
||||||
} else {
|
} else {
|
||||||
throw runtime_error(format("Unsupported sample format: {}-bit IEEE Float.", bitsPerSample));
|
throw runtime_error(
|
||||||
|
format("Unsupported sample format: {}-bit IEEE Float.", bitsPerSample)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw runtime_error(format(
|
throw runtime_error(format(
|
||||||
"Unsupported audio codec: '{}'. Only uncompressed codecs ('{}' and '{}') are supported.",
|
"Unsupported audio codec: '{}'. Only uncompressed codecs ('{}' and '{}') are supported.",
|
||||||
codecToString(codec), codecToString(Codec::Pcm), codecToString(Codec::Float)));
|
codecToString(codec), codecToString(Codec::Pcm), codecToString(Codec::Float)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
formatInfo.bytesPerFrame = bytesPerSample * formatInfo.channelCount;
|
formatInfo.bytesPerFrame = bytesPerSample * formatInfo.channelCount;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case fourcc('d', 'a', 't', 'a'): {
|
case fourcc('d', 'a', 't', 'a'):
|
||||||
|
{
|
||||||
reachedDataChunk = true;
|
reachedDataChunk = true;
|
||||||
formatInfo.dataOffset = file.tellg();
|
formatInfo.dataOffset = file.tellg();
|
||||||
formatInfo.frameCount = chunkSize / formatInfo.bytesPerFrame;
|
formatInfo.frameCount = chunkSize / formatInfo.bytesPerFrame;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default:
|
||||||
|
{
|
||||||
// Skip unknown chunk
|
// Skip unknown chunk
|
||||||
file.seekg(roundToEven(chunkSize), file.cur);
|
file.seekg(roundToEven(chunkSize), std::ios_base::cur);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,27 +145,35 @@ unique_ptr<AudioClip> WaveFileReader::clone() const {
|
||||||
return make_unique<WaveFileReader>(*this);
|
return make_unique<WaveFileReader>(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline AudioClip::value_type readSample(std::ifstream& file, SampleFormat sampleFormat, int channelCount) {
|
inline AudioClip::value_type readSample(
|
||||||
|
std::ifstream& file,
|
||||||
|
SampleFormat sampleFormat,
|
||||||
|
int channelCount
|
||||||
|
) {
|
||||||
float sum = 0;
|
float sum = 0;
|
||||||
for (int channelIndex = 0; channelIndex < channelCount; channelIndex++) {
|
for (int channelIndex = 0; channelIndex < channelCount; channelIndex++) {
|
||||||
switch (sampleFormat) {
|
switch (sampleFormat) {
|
||||||
case SampleFormat::UInt8: {
|
case SampleFormat::UInt8:
|
||||||
uint8_t raw = read<uint8_t>(file);
|
{
|
||||||
|
const uint8_t raw = read<uint8_t>(file);
|
||||||
sum += toNormalizedFloat(raw, 0, UINT8_MAX);
|
sum += toNormalizedFloat(raw, 0, UINT8_MAX);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SampleFormat::Int16: {
|
case SampleFormat::Int16:
|
||||||
int16_t raw = read<int16_t>(file);
|
{
|
||||||
|
const int16_t raw = read<int16_t>(file);
|
||||||
sum += toNormalizedFloat(raw, INT16_MIN, INT16_MAX);
|
sum += toNormalizedFloat(raw, INT16_MIN, INT16_MAX);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SampleFormat::Int24: {
|
case SampleFormat::Int24:
|
||||||
|
{
|
||||||
int raw = read<int, 24>(file);
|
int raw = read<int, 24>(file);
|
||||||
if (raw & 0x800000) raw |= 0xFF000000; // Fix two's complement
|
if (raw & 0x800000) raw |= 0xFF000000; // Fix two's complement
|
||||||
sum += toNormalizedFloat(raw, INT24_MIN, INT24_MAX);
|
sum += toNormalizedFloat(raw, INT24_MIN, INT24_MAX);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SampleFormat::Float32: {
|
case SampleFormat::Float32:
|
||||||
|
{
|
||||||
sum += read<float>(file);
|
sum += read<float>(file);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -169,10 +184,17 @@ inline AudioClip::value_type readSample(std::ifstream& file, SampleFormat sample
|
||||||
}
|
}
|
||||||
|
|
||||||
SampleReader WaveFileReader::createUnsafeSampleReader() const {
|
SampleReader WaveFileReader::createUnsafeSampleReader() const {
|
||||||
return [formatInfo = formatInfo, file = std::make_shared<std::ifstream>(openFile(filePath)), filePos = std::streampos(0)](size_type index) mutable {
|
return
|
||||||
std::streampos newFilePos = formatInfo.dataOffset + static_cast<std::streamoff>(index * formatInfo.bytesPerFrame);
|
[
|
||||||
|
formatInfo = formatInfo,
|
||||||
|
file = std::make_shared<std::ifstream>(openFile(filePath)),
|
||||||
|
filePos = std::streampos(0)
|
||||||
|
](size_type index) mutable {
|
||||||
|
const std::streampos newFilePos = formatInfo.dataOffset
|
||||||
|
+ static_cast<std::streamoff>(index * formatInfo.bytesPerFrame);
|
||||||
file->seekg(newFilePos);
|
file->seekg(newFilePos);
|
||||||
value_type result = readSample(*file, formatInfo.sampleFormat, formatInfo.channelCount);
|
const value_type result =
|
||||||
|
readSample(*file, formatInfo.sampleFormat, formatInfo.channelCount);
|
||||||
filePos = newFilePos + static_cast<std::streamoff>(formatInfo.bytesPerFrame);
|
filePos = newFilePos + static_cast<std::streamoff>(formatInfo.bytesPerFrame);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -422,6 +444,7 @@ string codecToString(int codec) {
|
||||||
case 0xf1ac: return "Free Lossless Audio Codec FLAC";
|
case 0xf1ac: return "Free Lossless Audio Codec FLAC";
|
||||||
case 0xfffe: return "Extensible";
|
case 0xfffe: return "Extensible";
|
||||||
case 0xffff: return "Development";
|
case 0xffff: return "Development";
|
||||||
}
|
default:
|
||||||
return format("{0:#x}", codec);
|
return format("{0:#x}", codec);
|
||||||
}
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ enum class SampleFormat {
|
||||||
|
|
||||||
class WaveFileReader : public AudioClip {
|
class WaveFileReader : public AudioClip {
|
||||||
public:
|
public:
|
||||||
WaveFileReader(boost::filesystem::path filePath);
|
WaveFileReader(const boost::filesystem::path& filePath);
|
||||||
std::unique_ptr<AudioClip> clone() const override;
|
std::unique_ptr<AudioClip> clone() const override;
|
||||||
int getSampleRate() const override;
|
int getSampleRate() const override;
|
||||||
size_type size() const override;
|
size_type size() const override;
|
||||||
|
|
|
@ -20,7 +20,9 @@ std::unique_ptr<AudioClip> createAudioFileClip(path filePath) {
|
||||||
return std::make_unique<OggVorbisFileReader>(filePath);
|
return std::make_unique<OggVorbisFileReader>(filePath);
|
||||||
}
|
}
|
||||||
throw runtime_error(format(
|
throw runtime_error(format(
|
||||||
"Unsupported file extension '{}'. Supported extensions are '.wav' and '.ogg'.", extension));
|
"Unsupported file extension '{}'. Supported extensions are '.wav' and '.ogg'.",
|
||||||
|
extension
|
||||||
|
));
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
std::throw_with_nested(runtime_error(format("Could not open sound file {}.", filePath)));
|
std::throw_with_nested(runtime_error(format("Could not open sound file {}.", filePath)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace little_endian {
|
||||||
|
|
||||||
Type result = 0;
|
Type result = 0;
|
||||||
char* p = reinterpret_cast<char*>(&result);
|
char* p = reinterpret_cast<char*>(&result);
|
||||||
int bytesToRead = bitsToRead / 8;
|
const int bytesToRead = bitsToRead / 8;
|
||||||
for (int byteIndex = 0; byteIndex < bytesToRead; byteIndex++) {
|
for (int byteIndex = 0; byteIndex < bytesToRead; byteIndex++) {
|
||||||
*(p + byteIndex) = static_cast<char>(stream.get());
|
*(p + byteIndex) = static_cast<char>(stream.get());
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,18 @@ namespace little_endian {
|
||||||
static_assert(bitsToWrite <= sizeof(Type) * 8, "Bits to write exceed target type size.");
|
static_assert(bitsToWrite <= sizeof(Type) * 8, "Bits to write exceed target type size.");
|
||||||
|
|
||||||
char* p = reinterpret_cast<char*>(&value);
|
char* p = reinterpret_cast<char*>(&value);
|
||||||
int bytesToWrite = bitsToWrite / 8;
|
const int bytesToWrite = bitsToWrite / 8;
|
||||||
for (int byteIndex = 0; byteIndex < bytesToWrite; byteIndex++) {
|
for (int byteIndex = 0; byteIndex < bytesToWrite; byteIndex++) {
|
||||||
stream.put(*(p + byteIndex));
|
stream.put(*(p + byteIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr uint32_t fourcc(unsigned char c0, unsigned char c1, unsigned char c2, unsigned char c3) {
|
constexpr uint32_t fourcc(
|
||||||
|
unsigned char c0,
|
||||||
|
unsigned char c1,
|
||||||
|
unsigned char c2,
|
||||||
|
unsigned char c3
|
||||||
|
) {
|
||||||
return c0 | (c1 << 8) | (c2 << 16) | (c3 << 24);
|
return c0 | (c1 << 8) | (c2 << 16) | (c3 << 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using std::function;
|
using std::function;
|
||||||
using std::vector;
|
using std::vector;
|
||||||
using std::unique_ptr;
|
|
||||||
|
|
||||||
// Converts a float in the range -1..1 to a signed 16-bit int
|
// Converts a float in the range -1..1 to a signed 16-bit int
|
||||||
inline int16_t floatSampleToInt16(float sample) {
|
inline int16_t floatSampleToInt16(float sample) {
|
||||||
|
@ -12,13 +11,18 @@ inline int16_t floatSampleToInt16(float sample) {
|
||||||
return static_cast<int16_t>(((sample + 1) / 2) * (INT16_MAX - INT16_MIN) + INT16_MIN);
|
return static_cast<int16_t>(((sample + 1) / 2) * (INT16_MAX - INT16_MIN) + INT16_MIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
void process16bitAudioClip(const AudioClip& audioClip, function<void(const vector<int16_t>&)> processBuffer, size_t bufferCapacity, ProgressSink& progressSink) {
|
void process16bitAudioClip(
|
||||||
|
const AudioClip& audioClip,
|
||||||
|
const function<void(const vector<int16_t>&)>& processBuffer,
|
||||||
|
size_t bufferCapacity,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
) {
|
||||||
// Process entire sound stream
|
// Process entire sound stream
|
||||||
vector<int16_t> buffer;
|
vector<int16_t> buffer;
|
||||||
buffer.reserve(bufferCapacity);
|
buffer.reserve(bufferCapacity);
|
||||||
int sampleCount = 0;
|
int sampleCount = 0;
|
||||||
auto it = audioClip.begin();
|
auto it = audioClip.begin();
|
||||||
auto end = audioClip.end();
|
const auto end = audioClip.end();
|
||||||
do {
|
do {
|
||||||
// Read to buffer
|
// Read to buffer
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
|
@ -32,10 +36,14 @@ void process16bitAudioClip(const AudioClip& audioClip, function<void(const vecto
|
||||||
|
|
||||||
sampleCount += buffer.size();
|
sampleCount += buffer.size();
|
||||||
progressSink.reportProgress(static_cast<double>(sampleCount) / audioClip.size());
|
progressSink.reportProgress(static_cast<double>(sampleCount) / audioClip.size());
|
||||||
} while (buffer.size());
|
} while (!buffer.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
void process16bitAudioClip(const AudioClip& audioClip, function<void(const vector<int16_t>&)> processBuffer, ProgressSink& progressSink) {
|
void process16bitAudioClip(
|
||||||
|
const AudioClip& audioClip,
|
||||||
|
const function<void(const vector<int16_t>&)>& processBuffer,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
) {
|
||||||
const size_t capacity = 1600; // 0.1 second capacity
|
const size_t capacity = 1600; // 0.1 second capacity
|
||||||
process16bitAudioClip(audioClip, processBuffer, capacity, progressSink);
|
process16bitAudioClip(audioClip, processBuffer, capacity, progressSink);
|
||||||
}
|
}
|
||||||
|
@ -46,5 +54,5 @@ vector<int16_t> copyTo16bitBuffer(const AudioClip& audioClip) {
|
||||||
for (float sample : audioClip) {
|
for (float sample : audioClip) {
|
||||||
result[index++] = floatSampleToInt16(sample);
|
result[index++] = floatSampleToInt16(sample);
|
||||||
}
|
}
|
||||||
return std::move(result);
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,17 @@
|
||||||
#include "AudioClip.h"
|
#include "AudioClip.h"
|
||||||
#include "tools/progress.h"
|
#include "tools/progress.h"
|
||||||
|
|
||||||
void process16bitAudioClip(const AudioClip& audioClip, std::function<void(const std::vector<int16_t>&)> processBuffer, size_t bufferCapacity, ProgressSink& progressSink);
|
void process16bitAudioClip(
|
||||||
void process16bitAudioClip(const AudioClip& audioClip, std::function<void(const std::vector<int16_t>&)> processBuffer, ProgressSink& progressSink);
|
const AudioClip& audioClip,
|
||||||
|
const std::function<void(const std::vector<int16_t>&)>& processBuffer,
|
||||||
|
size_t bufferCapacity,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
);
|
||||||
|
|
||||||
|
void process16bitAudioClip(
|
||||||
|
const AudioClip& audioClip,
|
||||||
|
const std::function<void(const std::vector<int16_t>&)>& processBuffer,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
);
|
||||||
|
|
||||||
std::vector<int16_t> copyTo16bitBuffer(const AudioClip& audioClip);
|
std::vector<int16_t> copyTo16bitBuffer(const AudioClip& audioClip);
|
|
@ -9,7 +9,6 @@
|
||||||
#include <gsl_util.h>
|
#include <gsl_util.h>
|
||||||
#include "tools/parallel.h"
|
#include "tools/parallel.h"
|
||||||
#include "AudioSegment.h"
|
#include "AudioSegment.h"
|
||||||
#include "tools/stringTools.h"
|
|
||||||
|
|
||||||
using std::vector;
|
using std::vector;
|
||||||
using boost::adaptors::transformed;
|
using boost::adaptors::transformed;
|
||||||
|
@ -17,7 +16,10 @@ using fmt::format;
|
||||||
using std::runtime_error;
|
using std::runtime_error;
|
||||||
using std::unique_ptr;
|
using std::unique_ptr;
|
||||||
|
|
||||||
JoiningBoundedTimeline<void> webRtcDetectVoiceActivity(const AudioClip& audioClip, ProgressSink& progressSink) {
|
JoiningBoundedTimeline<void> webRtcDetectVoiceActivity(
|
||||||
|
const AudioClip& audioClip,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
) {
|
||||||
VadInst* vadHandle = WebRtcVad_Create();
|
VadInst* vadHandle = WebRtcVad_Create();
|
||||||
if (!vadHandle) throw runtime_error("Error creating WebRTC VAD handle.");
|
if (!vadHandle) throw runtime_error("Error creating WebRTC VAD handle.");
|
||||||
|
|
||||||
|
@ -38,14 +40,19 @@ JoiningBoundedTimeline<void> webRtcDetectVoiceActivity(const AudioClip& audioCli
|
||||||
JoiningBoundedTimeline<void> activity(audioClip.getTruncatedRange());
|
JoiningBoundedTimeline<void> activity(audioClip.getTruncatedRange());
|
||||||
centiseconds time = 0_cs;
|
centiseconds time = 0_cs;
|
||||||
const size_t bufferCapacity = audioClip.getSampleRate() / 100;
|
const size_t bufferCapacity = audioClip.getSampleRate() / 100;
|
||||||
auto processBuffer = [&](const vector<int16_t>& buffer) {
|
const auto processBuffer = [&](const vector<int16_t>& buffer) {
|
||||||
// WebRTC is picky regarding buffer size
|
// WebRTC is picky regarding buffer size
|
||||||
if (buffer.size() < bufferCapacity) return;
|
if (buffer.size() < bufferCapacity) return;
|
||||||
|
|
||||||
int result = WebRtcVad_Process(vadHandle, audioClip.getSampleRate(), buffer.data(), buffer.size()) == 1;
|
const int result = WebRtcVad_Process(
|
||||||
|
vadHandle,
|
||||||
|
audioClip.getSampleRate(),
|
||||||
|
buffer.data(),
|
||||||
|
buffer.size()
|
||||||
|
) == 1;
|
||||||
if (result == -1) throw runtime_error("Error processing audio buffer using WebRTC VAD.");
|
if (result == -1) throw runtime_error("Error processing audio buffer using WebRTC VAD.");
|
||||||
|
|
||||||
bool isActive = result != 0;
|
const bool isActive = result != 0;
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
activity.set(time, time + 1_cs);
|
activity.set(time, time + 1_cs);
|
||||||
}
|
}
|
||||||
|
@ -54,12 +61,14 @@ JoiningBoundedTimeline<void> webRtcDetectVoiceActivity(const AudioClip& audioCli
|
||||||
process16bitAudioClip(audioClip, processBuffer, bufferCapacity, pass1ProgressSink);
|
process16bitAudioClip(audioClip, processBuffer, bufferCapacity, pass1ProgressSink);
|
||||||
|
|
||||||
// WebRTC adapts to the audio. This means results may not be correct at the very beginning.
|
// WebRTC adapts to the audio. This means results may not be correct at the very beginning.
|
||||||
// It sometimes returns false activity at the very beginning, mistaking the background noise for speech.
|
// It sometimes returns false activity at the very beginning, mistaking the background noise for
|
||||||
|
// speech.
|
||||||
// So we delete the first recognized utterance and re-process the corresponding audio segment.
|
// So we delete the first recognized utterance and re-process the corresponding audio segment.
|
||||||
if (!activity.empty()) {
|
if (!activity.empty()) {
|
||||||
TimeRange firstActivity = activity.begin()->getTimeRange();
|
TimeRange firstActivity = activity.begin()->getTimeRange();
|
||||||
activity.clear(firstActivity);
|
activity.clear(firstActivity);
|
||||||
unique_ptr<AudioClip> streamStart = audioClip.clone() | segment(TimeRange(0_cs, firstActivity.getEnd()));
|
const unique_ptr<AudioClip> streamStart = audioClip.clone()
|
||||||
|
| segment(TimeRange(0_cs, firstActivity.getEnd()));
|
||||||
time = 0_cs;
|
time = 0_cs;
|
||||||
process16bitAudioClip(*streamStart, processBuffer, bufferCapacity, pass2ProgressSink);
|
process16bitAudioClip(*streamStart, processBuffer, bufferCapacity, pass2ProgressSink);
|
||||||
}
|
}
|
||||||
|
@ -67,24 +76,34 @@ JoiningBoundedTimeline<void> webRtcDetectVoiceActivity(const AudioClip& audioCli
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
JoiningBoundedTimeline<void> detectVoiceActivity(const AudioClip& inputAudioClip, int maxThreadCount, ProgressSink& progressSink) {
|
JoiningBoundedTimeline<void> detectVoiceActivity(
|
||||||
|
const AudioClip& inputAudioClip,
|
||||||
|
int maxThreadCount,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
) {
|
||||||
// Prepare audio for VAD
|
// Prepare audio for VAD
|
||||||
const unique_ptr<AudioClip> audioClip = inputAudioClip.clone() | resample(16000) | removeDcOffset();
|
const unique_ptr<AudioClip> audioClip = inputAudioClip.clone()
|
||||||
|
| resample(16000)
|
||||||
|
| removeDcOffset();
|
||||||
|
|
||||||
JoiningBoundedTimeline<void> activity(audioClip->getTruncatedRange());
|
JoiningBoundedTimeline<void> activity(audioClip->getTruncatedRange());
|
||||||
std::mutex activityMutex;
|
std::mutex activityMutex;
|
||||||
|
|
||||||
// Split audio into segments and perform parallel VAD
|
// Split audio into segments and perform parallel VAD
|
||||||
const int segmentCount = maxThreadCount;
|
const int segmentCount = maxThreadCount;
|
||||||
centiseconds audioDuration = audioClip->getTruncatedRange().getDuration();
|
const centiseconds audioDuration = audioClip->getTruncatedRange().getDuration();
|
||||||
vector<TimeRange> audioSegments;
|
vector<TimeRange> audioSegments;
|
||||||
for (int i = 0; i < segmentCount; ++i) {
|
for (int i = 0; i < segmentCount; ++i) {
|
||||||
TimeRange segmentRange = TimeRange(i * audioDuration / segmentCount, (i + 1) * audioDuration / segmentCount);
|
TimeRange segmentRange = TimeRange(
|
||||||
|
i * audioDuration / segmentCount,
|
||||||
|
(i + 1) * audioDuration / segmentCount
|
||||||
|
);
|
||||||
audioSegments.push_back(segmentRange);
|
audioSegments.push_back(segmentRange);
|
||||||
}
|
}
|
||||||
runParallel([&](const TimeRange& segmentRange, ProgressSink& segmentProgressSink) {
|
runParallel([&](const TimeRange& segmentRange, ProgressSink& segmentProgressSink) {
|
||||||
unique_ptr<AudioClip> audioSegment = audioClip->clone() | segment(segmentRange);
|
const unique_ptr<AudioClip> audioSegment = audioClip->clone() | segment(segmentRange);
|
||||||
JoiningBoundedTimeline<void> activitySegment = webRtcDetectVoiceActivity(*audioSegment, segmentProgressSink);
|
JoiningBoundedTimeline<void> activitySegment =
|
||||||
|
webRtcDetectVoiceActivity(*audioSegment, segmentProgressSink);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(activityMutex);
|
std::lock_guard<std::mutex> lock(activityMutex);
|
||||||
for (auto activityRange : activitySegment) {
|
for (auto activityRange : activitySegment) {
|
||||||
|
@ -109,8 +128,13 @@ JoiningBoundedTimeline<void> detectVoiceActivity(const AudioClip& inputAudioClip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logging::debugFormat("Found {} sections of voice activity: {}", activity.size(),
|
logging::debugFormat(
|
||||||
join(activity | transformed([](const Timed<void>& t) { return format("{0}-{1}", t.getStart(), t.getEnd()); }), ", "));
|
"Found {} sections of voice activity: {}",
|
||||||
|
activity.size(),
|
||||||
|
join(activity | transformed([](const Timed<void>& t) {
|
||||||
|
return format("{0}-{1}", t.getStart(), t.getEnd());
|
||||||
|
}), ", ")
|
||||||
|
);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,8 @@
|
||||||
#include "time/BoundedTimeline.h"
|
#include "time/BoundedTimeline.h"
|
||||||
#include "tools/progress.h"
|
#include "tools/progress.h"
|
||||||
|
|
||||||
JoiningBoundedTimeline<void> detectVoiceActivity(const AudioClip& audioClip, int maxThreadCount, ProgressSink& progressSink);
|
JoiningBoundedTimeline<void> detectVoiceActivity(
|
||||||
|
const AudioClip& audioClip,
|
||||||
|
int maxThreadCount,
|
||||||
|
ProgressSink& progressSink
|
||||||
|
);
|
||||||
|
|
|
@ -12,26 +12,26 @@ void createWaveFile(const AudioClip& audioClip, std::string fileName) {
|
||||||
|
|
||||||
// Write RIFF chunk
|
// Write RIFF chunk
|
||||||
write<uint32_t>(fourcc('R', 'I', 'F', 'F'), file);
|
write<uint32_t>(fourcc('R', 'I', 'F', 'F'), file);
|
||||||
uint32_t formatChunkSize = 16;
|
const uint32_t formatChunkSize = 16;
|
||||||
uint16_t channelCount = 1;
|
const uint16_t channelCount = 1;
|
||||||
uint16_t frameSize = static_cast<uint16_t>(channelCount * sizeof(float));
|
const uint16_t frameSize = static_cast<uint16_t>(channelCount * sizeof(float));
|
||||||
uint32_t dataChunkSize = static_cast<uint32_t>(audioClip.size() * frameSize);
|
const uint32_t dataChunkSize = static_cast<uint32_t>(audioClip.size() * frameSize);
|
||||||
uint32_t riffChunkSize = 4 + (8 + formatChunkSize) + (8 + dataChunkSize);
|
const uint32_t riffChunkSize = 4 + (8 + formatChunkSize) + (8 + dataChunkSize);
|
||||||
write<uint32_t>(riffChunkSize, file);
|
write<uint32_t>(riffChunkSize, file);
|
||||||
write<uint32_t>(fourcc('W', 'A', 'V', 'E'), file);
|
write<uint32_t>(fourcc('W', 'A', 'V', 'E'), file);
|
||||||
|
|
||||||
// Write format chunk
|
// Write format chunk
|
||||||
write<uint32_t>(fourcc('f', 'm', 't', ' '), file);
|
write<uint32_t>(fourcc('f', 'm', 't', ' '), file);
|
||||||
write<uint32_t>(formatChunkSize, file);
|
write<uint32_t>(formatChunkSize, file);
|
||||||
uint16_t codec = 0x03; // 32-bit float
|
const uint16_t codec = 0x03; // 32-bit float
|
||||||
write<uint16_t>(codec, file);
|
write<uint16_t>(codec, file);
|
||||||
write<uint16_t>(channelCount, file);
|
write<uint16_t>(channelCount, file);
|
||||||
uint32_t frameRate = static_cast<uint16_t>(audioClip.getSampleRate());
|
const uint32_t frameRate = static_cast<uint16_t>(audioClip.getSampleRate());
|
||||||
write<uint32_t>(frameRate, file);
|
write<uint32_t>(frameRate, file);
|
||||||
uint32_t bytesPerSecond = frameRate * frameSize;
|
const uint32_t bytesPerSecond = frameRate * frameSize;
|
||||||
write<uint32_t>(bytesPerSecond, file);
|
write<uint32_t>(bytesPerSecond, file);
|
||||||
write<uint16_t>(frameSize, file);
|
write<uint16_t>(frameSize, file);
|
||||||
uint16_t bitsPerSample = 8 * sizeof(float);
|
const uint16_t bitsPerSample = 8 * sizeof(float);
|
||||||
write<uint16_t>(bitsPerSample, file);
|
write<uint16_t>(bitsPerSample, file);
|
||||||
|
|
||||||
// Write data chunk
|
// Write data chunk
|
||||||
|
|
|
@ -29,8 +29,8 @@ enum class Shape {
|
||||||
class ShapeConverter : public EnumConverter<Shape> {
|
class ShapeConverter : public EnumConverter<Shape> {
|
||||||
public:
|
public:
|
||||||
static ShapeConverter& get();
|
static ShapeConverter& get();
|
||||||
std::set<Shape> getBasicShapes();
|
static std::set<Shape> getBasicShapes();
|
||||||
std::set<Shape> getExtendedShapes();
|
static std::set<Shape> getExtendedShapes();
|
||||||
protected:
|
protected:
|
||||||
std::string getTypeName() override;
|
std::string getTypeName() override;
|
||||||
member_data getMemberData() override;
|
member_data getMemberData() override;
|
||||||
|
|
|
@ -6,7 +6,8 @@ using std::string;
|
||||||
|
|
||||||
void JsonExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
|
void JsonExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
|
||||||
// Export as JSON.
|
// Export as JSON.
|
||||||
// I'm not using a library because the code is short enough without one and it lets me control the formatting.
|
// I'm not using a library because the code is short enough without one and it lets me control
|
||||||
|
// the formatting.
|
||||||
outputStream << "{\n";
|
outputStream << "{\n";
|
||||||
outputStream << " \"metadata\": {\n";
|
outputStream << " \"metadata\": {\n";
|
||||||
outputStream << " \"soundFile\": \"" << escapeJsonString(input.inputFilePath.string()) << "\",\n";
|
outputStream << " \"soundFile\": \"" << escapeJsonString(input.inputFilePath.string()) << "\",\n";
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
void TsvExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
|
void TsvExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
|
||||||
// Output shapes with start times
|
// Output shapes with start times
|
||||||
for (auto& timedShape : input.animation) {
|
for (auto& timedShape : input.animation) {
|
||||||
outputStream << formatDuration(timedShape.getStart()) << "\t" << timedShape.getValue() << "\n";
|
outputStream
|
||||||
|
<< formatDuration(timedShape.getStart())
|
||||||
|
<< "\t"
|
||||||
|
<< timedShape.getValue()
|
||||||
|
<< "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output closed mouth with end time
|
// Output closed mouth with end time
|
||||||
|
|
|
@ -12,11 +12,17 @@ void XmlExporter::exportAnimation(const ExporterInput& input, std::ostream& outp
|
||||||
|
|
||||||
// Add metadata
|
// Add metadata
|
||||||
tree.put("rhubarbResult.metadata.soundFile", input.inputFilePath.string());
|
tree.put("rhubarbResult.metadata.soundFile", input.inputFilePath.string());
|
||||||
tree.put("rhubarbResult.metadata.duration", formatDuration(input.animation.getRange().getDuration()));
|
tree.put(
|
||||||
|
"rhubarbResult.metadata.duration",
|
||||||
|
formatDuration(input.animation.getRange().getDuration())
|
||||||
|
);
|
||||||
|
|
||||||
// Add mouth cues
|
// Add mouth cues
|
||||||
for (auto& timedShape : dummyShapeIfEmpty(input.animation, input.targetShapeSet)) {
|
for (auto& timedShape : dummyShapeIfEmpty(input.animation, input.targetShapeSet)) {
|
||||||
ptree& mouthCueElement = tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue());
|
ptree& mouthCueElement = tree.add(
|
||||||
|
"rhubarbResult.mouthCues.mouthCue",
|
||||||
|
timedShape.getValue()
|
||||||
|
);
|
||||||
mouthCueElement.put("<xmlattr>.start", formatDuration(timedShape.getStart()));
|
mouthCueElement.put("<xmlattr>.start", formatDuration(timedShape.getStart()));
|
||||||
mouthCueElement.put("<xmlattr>.end", formatDuration(timedShape.getEnd()));
|
mouthCueElement.put("<xmlattr>.end", formatDuration(timedShape.getEnd()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
#include "animation/targetShapeSet.h"
|
#include "animation/targetShapeSet.h"
|
||||||
|
|
||||||
// Makes sure there is at least one mouth shape
|
// Makes sure there is at least one mouth shape
|
||||||
std::vector<Timed<Shape>> dummyShapeIfEmpty(const JoiningTimeline<Shape>& animation, const ShapeSet& targetShapeSet) {
|
std::vector<Timed<Shape>> dummyShapeIfEmpty(
|
||||||
|
const JoiningTimeline<Shape>& animation,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
) {
|
||||||
std::vector<Timed<Shape>> result;
|
std::vector<Timed<Shape>> result;
|
||||||
std::copy(animation.begin(), animation.end(), std::back_inserter(result));
|
std::copy(animation.begin(), animation.end(), std::back_inserter(result));
|
||||||
if (result.empty()) {
|
if (result.empty()) {
|
||||||
// Add zero-length empty mouth
|
// Add zero-length empty mouth
|
||||||
result.push_back(Timed<Shape>(0_cs, 0_cs, convertToTargetShapeSet(Shape::X, targetShapeSet)));
|
result.emplace_back(0_cs, 0_cs, convertToTargetShapeSet(Shape::X, targetShapeSet));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,7 @@
|
||||||
#include "time/Timeline.h"
|
#include "time/Timeline.h"
|
||||||
|
|
||||||
// Makes sure there is at least one mouth shape
|
// Makes sure there is at least one mouth shape
|
||||||
std::vector<Timed<Shape>> dummyShapeIfEmpty(const JoiningTimeline<Shape>& animation, const ShapeSet& targetShapeSet);
|
std::vector<Timed<Shape>> dummyShapeIfEmpty(
|
||||||
|
const JoiningTimeline<Shape>& animation,
|
||||||
|
const ShapeSet& targetShapeSet
|
||||||
|
);
|
||||||
|
|
|
@ -28,6 +28,7 @@ namespace logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry::Entry(Level level, const string& message) :
|
Entry::Entry(Level level, const string& message) :
|
||||||
|
timestamp(),
|
||||||
level(level),
|
level(level),
|
||||||
message(message)
|
message(message)
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,7 +12,12 @@ namespace logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
string SimpleFileFormatter::format(const Entry& entry) {
|
string SimpleFileFormatter::format(const Entry& entry) {
|
||||||
return fmt::format("[{0}] {1} {2}", formatTime(entry.timestamp, "%F %H:%M:%S"), entry.threadCounter, consoleFormatter.format(entry));
|
return fmt::format(
|
||||||
|
"[{0}] {1} {2}",
|
||||||
|
formatTime(entry.timestamp, "%F %H:%M:%S"),
|
||||||
|
entry.threadCounter,
|
||||||
|
consoleFormatter.format(entry)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
#include "Entry.h"
|
#include "Entry.h"
|
||||||
|
|
||||||
using std::string;
|
using std::string;
|
||||||
using std::lock_guard;
|
|
||||||
using std::shared_ptr;
|
using std::shared_ptr;
|
||||||
|
|
||||||
namespace logging {
|
namespace logging {
|
||||||
|
@ -25,7 +24,7 @@ namespace logging {
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void StreamSink::receive(const Entry& entry) {
|
void StreamSink::receive(const Entry& entry) {
|
||||||
string line = formatter->format(entry);
|
const string line = formatter->format(entry);
|
||||||
*stream << line << std::endl;
|
*stream << line << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
#include "Sink.h"
|
#include "Sink.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include "Formatter.h"
|
#include "Formatter.h"
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
namespace logging {
|
namespace logging {
|
||||||
enum class Level;
|
enum class Level;
|
||||||
|
|
|
@ -26,7 +26,8 @@ static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialo
|
||||||
// High values (>= 1.0) can lead to imprecise or freezing animation.
|
// High values (>= 1.0) can lead to imprecise or freezing animation.
|
||||||
"-lw", "0.8",
|
"-lw", "0.8",
|
||||||
|
|
||||||
// The following settings are recommended at http://cmusphinx.sourceforge.net/wiki/phonemerecognition
|
// The following settings are recommended at
|
||||||
|
// http://cmusphinx.sourceforge.net/wiki/phonemerecognition
|
||||||
|
|
||||||
// Set beam width applied to every frame in Viterbi search
|
// Set beam width applied to every frame in Viterbi search
|
||||||
"-beam", "1e-20",
|
"-beam", "1e-20",
|
||||||
|
@ -56,7 +57,9 @@ static Timeline<Phone> utteranceToPhones(
|
||||||
paddedTimeRange.grow(padding);
|
paddedTimeRange.grow(padding);
|
||||||
paddedTimeRange.trim(audioClip.getTruncatedRange());
|
paddedTimeRange.trim(audioClip.getTruncatedRange());
|
||||||
|
|
||||||
const unique_ptr<AudioClip> clipSegment = audioClip.clone() | segment(paddedTimeRange) | resample(sphinxSampleRate);
|
const unique_ptr<AudioClip> clipSegment = audioClip.clone()
|
||||||
|
| segment(paddedTimeRange)
|
||||||
|
| resample(sphinxSampleRate);
|
||||||
const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
|
const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
|
||||||
|
|
||||||
// Detect phones (returned as words)
|
// Detect phones (returned as words)
|
||||||
|
|
|
@ -67,9 +67,15 @@ lambda_unique_ptr<ngram_model_t> createDefaultLanguageModel(ps_decoder_t& decode
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(ps_decoder_t& decoder, const string& dialog) {
|
lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(
|
||||||
|
ps_decoder_t& decoder,
|
||||||
|
const string& dialog
|
||||||
|
) {
|
||||||
// Split dialog into normalized words
|
// Split dialog into normalized words
|
||||||
vector<string> words = tokenizeText(dialog, [&](const string& word) { return dictionaryContains(*decoder.dict, word); });
|
vector<string> words = tokenizeText(
|
||||||
|
dialog,
|
||||||
|
[&](const string& word) { return dictionaryContains(*decoder.dict, word); }
|
||||||
|
);
|
||||||
|
|
||||||
// Add dialog-specific words to the dictionary
|
// Add dialog-specific words to the dictionary
|
||||||
addMissingDictionaryWords(words, decoder);
|
addMissingDictionaryWords(words, decoder);
|
||||||
|
@ -80,15 +86,27 @@ lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(ps_decoder_t& decoder
|
||||||
return createLanguageModel(words, decoder);
|
return createLanguageModel(words, decoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(ps_decoder_t& decoder, const string& dialog) {
|
lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(
|
||||||
|
ps_decoder_t& decoder,
|
||||||
|
const string& dialog
|
||||||
|
) {
|
||||||
auto defaultLanguageModel = createDefaultLanguageModel(decoder);
|
auto defaultLanguageModel = createDefaultLanguageModel(decoder);
|
||||||
auto dialogLanguageModel = createDialogLanguageModel(decoder, dialog);
|
auto dialogLanguageModel = createDialogLanguageModel(decoder, dialog);
|
||||||
constexpr int modelCount = 2;
|
constexpr int modelCount = 2;
|
||||||
array<ngram_model_t*, modelCount> languageModels{ defaultLanguageModel.get(), dialogLanguageModel.get() };
|
array<ngram_model_t*, modelCount> languageModels {
|
||||||
|
defaultLanguageModel.get(),
|
||||||
|
dialogLanguageModel.get()
|
||||||
|
};
|
||||||
array<const char*, modelCount> modelNames { "defaultLM", "dialogLM" };
|
array<const char*, modelCount> modelNames { "defaultLM", "dialogLM" };
|
||||||
array<float, modelCount> modelWeights { 0.1f, 0.9f };
|
array<float, modelCount> modelWeights { 0.1f, 0.9f };
|
||||||
lambda_unique_ptr<ngram_model_t> result(
|
lambda_unique_ptr<ngram_model_t> result(
|
||||||
ngram_model_set_init(nullptr, languageModels.data(), const_cast<char**>(modelNames.data()), modelWeights.data(), modelCount),
|
ngram_model_set_init(
|
||||||
|
nullptr,
|
||||||
|
languageModels.data(),
|
||||||
|
const_cast<char**>(modelNames.data()),
|
||||||
|
modelWeights.data(),
|
||||||
|
modelCount
|
||||||
|
),
|
||||||
[](ngram_model_t* lm) { ngram_model_free(lm); });
|
[](ngram_model_t* lm) { ngram_model_free(lm); });
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw runtime_error("Error creating biased language model.");
|
throw runtime_error("Error creating biased language model.");
|
||||||
|
@ -105,7 +123,8 @@ static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialo
|
||||||
"-hmm", (getSphinxModelDirectory() / "acoustic-model").string().c_str(),
|
"-hmm", (getSphinxModelDirectory() / "acoustic-model").string().c_str(),
|
||||||
// Set pronunciation dictionary
|
// Set pronunciation dictionary
|
||||||
"-dict", (getSphinxModelDirectory() / "cmudict-en-us.dict").string().c_str(),
|
"-dict", (getSphinxModelDirectory() / "cmudict-en-us.dict").string().c_str(),
|
||||||
// Add noise against zero silence (see http://cmusphinx.sourceforge.net/wiki/faq#qwhy_my_accuracy_is_poor)
|
// Add noise against zero silence
|
||||||
|
// (see http://cmusphinx.sourceforge.net/wiki/faq#qwhy_my_accuracy_is_poor)
|
||||||
"-dither", "yes",
|
"-dither", "yes",
|
||||||
// Disable VAD -- we're doing that ourselves
|
// Disable VAD -- we're doing that ourselves
|
||||||
"-remove_silence", "no",
|
"-remove_silence", "no",
|
||||||
|
@ -184,7 +203,11 @@ optional<Timeline<Phone>> getPhoneAlignment(
|
||||||
// Extract phones with timestamps
|
// Extract phones with timestamps
|
||||||
char** phoneNames = decoder.dict->mdef->ciname;
|
char** phoneNames = decoder.dict->mdef->ciname;
|
||||||
Timeline<Phone> result;
|
Timeline<Phone> result;
|
||||||
for (ps_alignment_iter_t* it = ps_alignment_phones(alignment.get()); it; it = ps_alignment_iter_next(it)) {
|
for (
|
||||||
|
ps_alignment_iter_t* it = ps_alignment_phones(alignment.get());
|
||||||
|
it;
|
||||||
|
it = ps_alignment_iter_next(it)
|
||||||
|
) {
|
||||||
// Get phone
|
// Get phone
|
||||||
ps_alignment_entry_t* phoneEntry = ps_alignment_iter_get(it);
|
ps_alignment_entry_t* phoneEntry = ps_alignment_iter_get(it);
|
||||||
const s3cipid_t phoneId = phoneEntry->id.pid.cipid;
|
const s3cipid_t phoneId = phoneEntry->id.pid.cipid;
|
||||||
|
@ -238,7 +261,9 @@ static Timeline<Phone> utteranceToPhones(
|
||||||
paddedTimeRange.grow(padding);
|
paddedTimeRange.grow(padding);
|
||||||
paddedTimeRange.trim(audioClip.getTruncatedRange());
|
paddedTimeRange.trim(audioClip.getTruncatedRange());
|
||||||
|
|
||||||
const unique_ptr<AudioClip> clipSegment = audioClip.clone() | segment(paddedTimeRange) | resample(sphinxSampleRate);
|
const unique_ptr<AudioClip> clipSegment = audioClip.clone()
|
||||||
|
| segment(paddedTimeRange)
|
||||||
|
| resample(sphinxSampleRate);
|
||||||
const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
|
const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
|
||||||
|
|
||||||
// Get words
|
// Get words
|
||||||
|
@ -309,5 +334,6 @@ BoundedTimeline<Phone> PocketSphinxRecognizer::recognizePhones(
|
||||||
int maxThreadCount,
|
int maxThreadCount,
|
||||||
ProgressSink& progressSink
|
ProgressSink& progressSink
|
||||||
) const {
|
) const {
|
||||||
return ::recognizePhones(inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink);
|
return ::recognizePhones(
|
||||||
|
inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,9 +64,10 @@ Phone charToPhone(wchar_t c) {
|
||||||
case L'r': return Phone::R;
|
case L'r': return Phone::R;
|
||||||
case L'l': return Phone::L;
|
case L'l': return Phone::L;
|
||||||
case L'h': return Phone::HH;
|
case L'h': return Phone::HH;
|
||||||
}
|
default:
|
||||||
return Phone::Noise;
|
return Phone::Noise;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vector<Phone> wordToPhones(const std::string& word) {
|
vector<Phone> wordToPhones(const std::string& word) {
|
||||||
static regex validWord("^[a-z']*$");
|
static regex validWord("^[a-z']*$");
|
||||||
|
@ -94,8 +95,11 @@ vector<Phone> wordToPhones(const std::string& word) {
|
||||||
for (wchar_t c : wideWord) {
|
for (wchar_t c : wideWord) {
|
||||||
Phone phone = charToPhone(c);
|
Phone phone = charToPhone(c);
|
||||||
if (phone == Phone::Noise) {
|
if (phone == Phone::Noise) {
|
||||||
logging::errorFormat("G2P error determining pronunciation for '{}': Character '{}' is not a recognized phone shorthand.",
|
logging::errorFormat(
|
||||||
word, static_cast<char>(c));
|
"G2P error determining pronunciation for '{}': Character '{}' is not a recognized phone shorthand.",
|
||||||
|
word,
|
||||||
|
static_cast<char>(c)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (phone != lastPhone) {
|
if (phone != lastPhone) {
|
||||||
|
|
|
@ -15,83 +15,94 @@ using std::vector;
|
||||||
using std::regex;
|
using std::regex;
|
||||||
using std::map;
|
using std::map;
|
||||||
using std::tuple;
|
using std::tuple;
|
||||||
using std::make_tuple;
|
|
||||||
using std::get;
|
using std::get;
|
||||||
using std::endl;
|
using std::endl;
|
||||||
using boost::filesystem::path;
|
using boost::filesystem::path;
|
||||||
|
|
||||||
using unigram_t = string;
|
using Unigram = string;
|
||||||
using bigram_t = tuple<string, string>;
|
using Bigram = tuple<string, string>;
|
||||||
using trigram_t = tuple<string, string, string>;
|
using Trigram = tuple<string, string, string>;
|
||||||
|
|
||||||
map<unigram_t, int> getUnigramCounts(const vector<string>& words) {
|
map<Unigram, int> getUnigramCounts(const vector<string>& words) {
|
||||||
map<unigram_t, int> unigramCounts;
|
map<Unigram, int> unigramCounts;
|
||||||
for (const unigram_t& unigram : words) {
|
for (const Unigram& unigram : words) {
|
||||||
++unigramCounts[unigram];
|
++unigramCounts[unigram];
|
||||||
}
|
}
|
||||||
return unigramCounts;
|
return unigramCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<bigram_t, int> getBigramCounts(const vector<string>& words) {
|
map<Bigram, int> getBigramCounts(const vector<string>& words) {
|
||||||
map<bigram_t, int> bigramCounts;
|
map<Bigram, int> bigramCounts;
|
||||||
for (auto it = words.begin(); it < words.end() - 1; ++it) {
|
for (auto it = words.begin(); it < words.end() - 1; ++it) {
|
||||||
++bigramCounts[bigram_t(*it, *(it + 1))];
|
++bigramCounts[Bigram(*it, *(it + 1))];
|
||||||
}
|
}
|
||||||
return bigramCounts;
|
return bigramCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<trigram_t, int> getTrigramCounts(const vector<string>& words) {
|
map<Trigram, int> getTrigramCounts(const vector<string>& words) {
|
||||||
map<trigram_t, int> trigramCounts;
|
map<Trigram, int> trigramCounts;
|
||||||
if (words.size() >= 3) {
|
if (words.size() >= 3) {
|
||||||
for (auto it = words.begin(); it < words.end() - 2; ++it) {
|
for (auto it = words.begin(); it < words.end() - 2; ++it) {
|
||||||
++trigramCounts[trigram_t(*it, *(it + 1), *(it + 2))];
|
++trigramCounts[Trigram(*it, *(it + 1), *(it + 2))];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return trigramCounts;
|
return trigramCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<unigram_t, double> getUnigramProbabilities(const vector<string>& words, const map<unigram_t, int>& unigramCounts, const double deflator) {
|
map<Unigram, double> getUnigramProbabilities(
|
||||||
map<unigram_t, double> unigramProbabilities;
|
const vector<string>& words,
|
||||||
|
const map<Unigram, int>& unigramCounts,
|
||||||
|
const double deflator
|
||||||
|
) {
|
||||||
|
map<Unigram, double> unigramProbabilities;
|
||||||
for (const auto& pair : unigramCounts) {
|
for (const auto& pair : unigramCounts) {
|
||||||
unigram_t unigram = get<0>(pair);
|
const Unigram& unigram = get<0>(pair);
|
||||||
int unigramCount = get<1>(pair);
|
const int unigramCount = get<1>(pair);
|
||||||
unigramProbabilities[unigram] = double(unigramCount) / words.size() * deflator;
|
unigramProbabilities[unigram] = double(unigramCount) / words.size() * deflator;
|
||||||
}
|
}
|
||||||
return unigramProbabilities;
|
return unigramProbabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<bigram_t, double> getBigramProbabilities(const map<unigram_t, int>& unigramCounts, const map<bigram_t, int>& bigramCounts, const double deflator) {
|
map<Bigram, double> getBigramProbabilities(
|
||||||
map<bigram_t, double> bigramProbabilities;
|
const map<Unigram, int>& unigramCounts,
|
||||||
|
const map<Bigram, int>& bigramCounts,
|
||||||
|
const double deflator
|
||||||
|
) {
|
||||||
|
map<Bigram, double> bigramProbabilities;
|
||||||
for (const auto& pair : bigramCounts) {
|
for (const auto& pair : bigramCounts) {
|
||||||
bigram_t bigram = get<0>(pair);
|
Bigram bigram = get<0>(pair);
|
||||||
int bigramCount = get<1>(pair);
|
const int bigramCount = get<1>(pair);
|
||||||
int unigramPrefixCount = unigramCounts.at(get<0>(bigram));
|
const int unigramPrefixCount = unigramCounts.at(get<0>(bigram));
|
||||||
bigramProbabilities[bigram] = double(bigramCount) / unigramPrefixCount * deflator;
|
bigramProbabilities[bigram] = double(bigramCount) / unigramPrefixCount * deflator;
|
||||||
}
|
}
|
||||||
return bigramProbabilities;
|
return bigramProbabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<trigram_t, double> getTrigramProbabilities(const map<bigram_t, int>& bigramCounts, const map<trigram_t, int>& trigramCounts, const double deflator) {
|
map<Trigram, double> getTrigramProbabilities(
|
||||||
map<trigram_t, double> trigramProbabilities;
|
const map<Bigram, int>& bigramCounts,
|
||||||
|
const map<Trigram, int>& trigramCounts,
|
||||||
|
const double deflator
|
||||||
|
) {
|
||||||
|
map<Trigram, double> trigramProbabilities;
|
||||||
for (const auto& pair : trigramCounts) {
|
for (const auto& pair : trigramCounts) {
|
||||||
trigram_t trigram = get<0>(pair);
|
Trigram trigram = get<0>(pair);
|
||||||
int trigramCount = get<1>(pair);
|
const int trigramCount = get<1>(pair);
|
||||||
int bigramPrefixCount = bigramCounts.at(bigram_t(get<0>(trigram), get<1>(trigram)));
|
const int bigramPrefixCount = bigramCounts.at(Bigram(get<0>(trigram), get<1>(trigram)));
|
||||||
trigramProbabilities[trigram] = double(trigramCount) / bigramPrefixCount * deflator;
|
trigramProbabilities[trigram] = double(trigramCount) / bigramPrefixCount * deflator;
|
||||||
}
|
}
|
||||||
return trigramProbabilities;
|
return trigramProbabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<unigram_t, double> getUnigramBackoffWeights(
|
map<Unigram, double> getUnigramBackoffWeights(
|
||||||
const map<unigram_t, int>& unigramCounts,
|
const map<Unigram, int>& unigramCounts,
|
||||||
const map<unigram_t, double>& unigramProbabilities,
|
const map<Unigram, double>& unigramProbabilities,
|
||||||
const map<bigram_t, int>& bigramCounts,
|
const map<Bigram, int>& bigramCounts,
|
||||||
const double discountMass)
|
const double discountMass)
|
||||||
{
|
{
|
||||||
map<unigram_t, double> unigramBackoffWeights;
|
map<Unigram, double> unigramBackoffWeights;
|
||||||
for (const unigram_t& unigram : unigramCounts | boost::adaptors::map_keys) {
|
for (const Unigram& unigram : unigramCounts | boost::adaptors::map_keys) {
|
||||||
double denominator = 1;
|
double denominator = 1;
|
||||||
for (const bigram_t& bigram : bigramCounts | boost::adaptors::map_keys) {
|
for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) {
|
||||||
if (get<0>(bigram) == unigram) {
|
if (get<0>(bigram) == unigram) {
|
||||||
denominator -= unigramProbabilities.at(get<1>(bigram));
|
denominator -= unigramProbabilities.at(get<1>(bigram));
|
||||||
}
|
}
|
||||||
|
@ -101,18 +112,18 @@ map<unigram_t, double> getUnigramBackoffWeights(
|
||||||
return unigramBackoffWeights;
|
return unigramBackoffWeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
map<bigram_t, double> getBigramBackoffWeights(
|
map<Bigram, double> getBigramBackoffWeights(
|
||||||
const map<bigram_t, int>& bigramCounts,
|
const map<Bigram, int>& bigramCounts,
|
||||||
const map<bigram_t, double>& bigramProbabilities,
|
const map<Bigram, double>& bigramProbabilities,
|
||||||
const map<trigram_t, int>& trigramCounts,
|
const map<Trigram, int>& trigramCounts,
|
||||||
const double discountMass)
|
const double discountMass)
|
||||||
{
|
{
|
||||||
map<bigram_t, double> bigramBackoffWeights;
|
map<Bigram, double> bigramBackoffWeights;
|
||||||
for (const bigram_t& bigram : bigramCounts | boost::adaptors::map_keys) {
|
for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) {
|
||||||
double denominator = 1;
|
double denominator = 1;
|
||||||
for (const trigram_t& trigram : trigramCounts | boost::adaptors::map_keys) {
|
for (const Trigram& trigram : trigramCounts | boost::adaptors::map_keys) {
|
||||||
if (bigram_t(get<0>(trigram), get<1>(trigram)) == bigram) {
|
if (Bigram(get<0>(trigram), get<1>(trigram)) == bigram) {
|
||||||
denominator -= bigramProbabilities.at(bigram_t(get<1>(trigram), get<2>(trigram)));
|
denominator -= bigramProbabilities.at(Bigram(get<1>(trigram), get<2>(trigram)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bigramBackoffWeights[bigram] = discountMass / denominator;
|
bigramBackoffWeights[bigram] = discountMass / denominator;
|
||||||
|
@ -120,20 +131,25 @@ map<bigram_t, double> getBigramBackoffWeights(
|
||||||
return bigramBackoffWeights;
|
return bigramBackoffWeights;
|
||||||
}
|
}
|
||||||
|
|
||||||
void createLanguageModelFile(const vector<string>& words, path filePath) {
|
void createLanguageModelFile(const vector<string>& words, const path& filePath) {
|
||||||
const double discountMass = 0.5;
|
const double discountMass = 0.5;
|
||||||
const double deflator = 1.0 - discountMass;
|
const double deflator = 1.0 - discountMass;
|
||||||
|
|
||||||
map<unigram_t, int> unigramCounts = getUnigramCounts(words);
|
map<Unigram, int> unigramCounts = getUnigramCounts(words);
|
||||||
map<bigram_t, int> bigramCounts = getBigramCounts(words);
|
map<Bigram, int> bigramCounts = getBigramCounts(words);
|
||||||
map<trigram_t, int> trigramCounts = getTrigramCounts(words);
|
map<Trigram, int> trigramCounts = getTrigramCounts(words);
|
||||||
|
|
||||||
map<unigram_t, double> unigramProbabilities = getUnigramProbabilities(words, unigramCounts, deflator);
|
map<Unigram, double> unigramProbabilities =
|
||||||
map<bigram_t, double> bigramProbabilities = getBigramProbabilities(unigramCounts, bigramCounts, deflator);
|
getUnigramProbabilities(words, unigramCounts, deflator);
|
||||||
map<trigram_t, double> trigramProbabilities = getTrigramProbabilities(bigramCounts, trigramCounts, deflator);
|
map<Bigram, double> bigramProbabilities =
|
||||||
|
getBigramProbabilities(unigramCounts, bigramCounts, deflator);
|
||||||
|
map<Trigram, double> trigramProbabilities =
|
||||||
|
getTrigramProbabilities(bigramCounts, trigramCounts, deflator);
|
||||||
|
|
||||||
map<unigram_t, double> unigramBackoffWeights = getUnigramBackoffWeights(unigramCounts, unigramProbabilities, bigramCounts, discountMass);
|
map<Unigram, double> unigramBackoffWeights =
|
||||||
map<bigram_t, double> bigramBackoffWeights = getBigramBackoffWeights(bigramCounts, bigramProbabilities, trigramCounts, discountMass);
|
getUnigramBackoffWeights(unigramCounts, unigramProbabilities, bigramCounts, discountMass);
|
||||||
|
map<Bigram, double> bigramBackoffWeights =
|
||||||
|
getBigramBackoffWeights(bigramCounts, bigramProbabilities, trigramCounts, discountMass);
|
||||||
|
|
||||||
boost::filesystem::ofstream file(filePath);
|
boost::filesystem::ofstream file(filePath);
|
||||||
file << "Generated by " << appName << " " << appVersion << endl << endl;
|
file << "Generated by " << appName << " " << appVersion << endl << endl;
|
||||||
|
@ -146,7 +162,7 @@ void createLanguageModelFile(const vector<string>& words, path filePath) {
|
||||||
file.setf(std::ios::fixed, std::ios::floatfield);
|
file.setf(std::ios::fixed, std::ios::floatfield);
|
||||||
file.precision(4);
|
file.precision(4);
|
||||||
file << "\\1-grams:" << endl;
|
file << "\\1-grams:" << endl;
|
||||||
for (const unigram_t& unigram : unigramCounts | boost::adaptors::map_keys) {
|
for (const Unigram& unigram : unigramCounts | boost::adaptors::map_keys) {
|
||||||
file << log10(unigramProbabilities.at(unigram))
|
file << log10(unigramProbabilities.at(unigram))
|
||||||
<< " " << unigram
|
<< " " << unigram
|
||||||
<< " " << log10(unigramBackoffWeights.at(unigram)) << endl;
|
<< " " << log10(unigramBackoffWeights.at(unigram)) << endl;
|
||||||
|
@ -154,7 +170,7 @@ void createLanguageModelFile(const vector<string>& words, path filePath) {
|
||||||
file << endl;
|
file << endl;
|
||||||
|
|
||||||
file << "\\2-grams:" << endl;
|
file << "\\2-grams:" << endl;
|
||||||
for (const bigram_t& bigram : bigramCounts | boost::adaptors::map_keys) {
|
for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) {
|
||||||
file << log10(bigramProbabilities.at(bigram))
|
file << log10(bigramProbabilities.at(bigram))
|
||||||
<< " " << get<0>(bigram) << " " << get<1>(bigram)
|
<< " " << get<0>(bigram) << " " << get<1>(bigram)
|
||||||
<< " " << log10(bigramBackoffWeights.at(bigram)) << endl;
|
<< " " << log10(bigramBackoffWeights.at(bigram)) << endl;
|
||||||
|
@ -162,7 +178,7 @@ void createLanguageModelFile(const vector<string>& words, path filePath) {
|
||||||
file << endl;
|
file << endl;
|
||||||
|
|
||||||
file << "\\3-grams:" << endl;
|
file << "\\3-grams:" << endl;
|
||||||
for (const trigram_t& trigram : trigramCounts | boost::adaptors::map_keys) {
|
for (const Trigram& trigram : trigramCounts | boost::adaptors::map_keys) {
|
||||||
file << log10(trigramProbabilities.at(trigram))
|
file << log10(trigramProbabilities.at(trigram))
|
||||||
<< " " << get<0>(trigram) << " " << get<1>(trigram) << " " << get<2>(trigram) << endl;
|
<< " " << get<0>(trigram) << " " << get<1>(trigram) << " " << get<2>(trigram) << endl;
|
||||||
}
|
}
|
||||||
|
@ -171,7 +187,10 @@ void createLanguageModelFile(const vector<string>& words, path filePath) {
|
||||||
file << "\\end\\" << endl;
|
file << "\\end\\" << endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
lambda_unique_ptr<ngram_model_t> createLanguageModel(const vector<string>& words, ps_decoder_t& decoder) {
|
lambda_unique_ptr<ngram_model_t> createLanguageModel(
|
||||||
|
const vector<string>& words,
|
||||||
|
ps_decoder_t& decoder
|
||||||
|
) {
|
||||||
path tempFilePath = getTempFilePath();
|
path tempFilePath = getTempFilePath();
|
||||||
createLanguageModelFile(words, tempFilePath);
|
createLanguageModelFile(words, tempFilePath);
|
||||||
auto deleteTempFile = gsl::finally([&]() { boost::filesystem::remove(tempFilePath); });
|
auto deleteTempFile = gsl::finally([&]() { boost::filesystem::remove(tempFilePath); });
|
||||||
|
|
|
@ -8,4 +8,7 @@ extern "C" {
|
||||||
#include <ngram_search.h>
|
#include <ngram_search.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
lambda_unique_ptr<ngram_model_t> createLanguageModel(const std::vector<std::string>& words, ps_decoder_t& decoder);
|
lambda_unique_ptr<ngram_model_t> createLanguageModel(
|
||||||
|
const std::vector<std::string>& words,
|
||||||
|
ps_decoder_t& decoder
|
||||||
|
);
|
||||||
|
|
|
@ -61,7 +61,8 @@ void sphinxLogCallback(void* user_data, err_lvl_t errorLevel, const char* format
|
||||||
if (!success) chars.resize(chars.size() * 2);
|
if (!success) chars.resize(chars.size() * 2);
|
||||||
}
|
}
|
||||||
const regex waste("^(DEBUG|INFO|INFOCONT|WARN|ERROR|FATAL): ");
|
const regex waste("^(DEBUG|INFO|INFOCONT|WARN|ERROR|FATAL): ");
|
||||||
string message = std::regex_replace(chars.data(), waste, "", std::regex_constants::format_first_only);
|
string message =
|
||||||
|
std::regex_replace(chars.data(), waste, "", std::regex_constants::format_first_only);
|
||||||
boost::algorithm::trim(message);
|
boost::algorithm::trim(message);
|
||||||
|
|
||||||
const logging::Level logLevel = convertSphinxErrorLevel(errorLevel);
|
const logging::Level logLevel = convertSphinxErrorLevel(errorLevel);
|
||||||
|
@ -115,8 +116,12 @@ BoundedTimeline<Phone> recognizePhones(
|
||||||
const auto processUtterance = [&](Timed<void> timedUtterance, ProgressSink& utteranceProgressSink) {
|
const auto processUtterance = [&](Timed<void> timedUtterance, ProgressSink& utteranceProgressSink) {
|
||||||
// Detect phones for utterance
|
// Detect phones for utterance
|
||||||
const auto decoder = decoderPool.acquire();
|
const auto decoder = decoderPool.acquire();
|
||||||
Timeline<Phone> utterancePhones =
|
Timeline<Phone> utterancePhones = utteranceToPhones(
|
||||||
utteranceToPhones(*audioClip, timedUtterance.getTimeRange(), *decoder, utteranceProgressSink);
|
*audioClip,
|
||||||
|
timedUtterance.getTimeRange(),
|
||||||
|
*decoder,
|
||||||
|
utteranceProgressSink
|
||||||
|
);
|
||||||
|
|
||||||
// Copy phones to result timeline
|
// Copy phones to result timeline
|
||||||
std::lock_guard<std::mutex> lock(resultMutex);
|
std::lock_guard<std::mutex> lock(resultMutex);
|
||||||
|
@ -137,13 +142,21 @@ BoundedTimeline<Phone> recognizePhones(
|
||||||
// Don't use more threads than there are utterances to be processed
|
// Don't use more threads than there are utterances to be processed
|
||||||
static_cast<int>(utterances.size()),
|
static_cast<int>(utterances.size()),
|
||||||
// Don't waste time creating additional threads (and decoders!) if the recording is short
|
// Don't waste time creating additional threads (and decoders!) if the recording is short
|
||||||
static_cast<int>(duration_cast<std::chrono::seconds>(audioClip->getTruncatedRange().getDuration()).count() / 5)
|
static_cast<int>(
|
||||||
|
duration_cast<std::chrono::seconds>(audioClip->getTruncatedRange().getDuration()).count() / 5
|
||||||
|
)
|
||||||
});
|
});
|
||||||
if (threadCount < 1) {
|
if (threadCount < 1) {
|
||||||
threadCount = 1;
|
threadCount = 1;
|
||||||
}
|
}
|
||||||
logging::debugFormat("Speech recognition using {} threads -- start", threadCount);
|
logging::debugFormat("Speech recognition using {} threads -- start", threadCount);
|
||||||
runParallel(processUtterance, utterances, threadCount, dialogProgressSink, getUtteranceProgressWeight);
|
runParallel(
|
||||||
|
processUtterance,
|
||||||
|
utterances,
|
||||||
|
threadCount,
|
||||||
|
dialogProgressSink,
|
||||||
|
getUtteranceProgressWeight
|
||||||
|
);
|
||||||
logging::debug("Speech recognition -- end");
|
logging::debug("Speech recognition -- end");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
std::throw_with_nested(runtime_error("Error performing speech recognition via PocketSphinx."));
|
std::throw_with_nested(runtime_error("Error performing speech recognition via PocketSphinx."));
|
||||||
|
@ -200,7 +213,9 @@ BoundedTimeline<string> recognizeWords(const vector<int16_t>& audioBuffer, ps_de
|
||||||
error = ps_end_utt(&decoder);
|
error = ps_end_utt(&decoder);
|
||||||
if (error) throw runtime_error("Error ending utterance processing for word recognition.");
|
if (error) throw runtime_error("Error ending utterance processing for word recognition.");
|
||||||
|
|
||||||
BoundedTimeline<string> result(TimeRange(0_cs, centiseconds(100 * audioBuffer.size() / sphinxSampleRate)));
|
BoundedTimeline<string> result(
|
||||||
|
TimeRange(0_cs, centiseconds(100 * audioBuffer.size() / sphinxSampleRate))
|
||||||
|
);
|
||||||
const bool noWordsRecognized = reinterpret_cast<ngram_search_t*>(decoder.search)->bpidx == 0;
|
const bool noWordsRecognized = reinterpret_cast<ngram_search_t*>(decoder.search)->bpidx == 0;
|
||||||
if (noWordsRecognized) {
|
if (noWordsRecognized) {
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -36,4 +36,7 @@ const boost::filesystem::path& getSphinxModelDirectory();
|
||||||
|
|
||||||
JoiningTimeline<void> getNoiseSounds(TimeRange utteranceTimeRange, const Timeline<Phone>& phones);
|
JoiningTimeline<void> getNoiseSounds(TimeRange utteranceTimeRange, const Timeline<Phone>& phones);
|
||||||
|
|
||||||
BoundedTimeline<std::string> recognizeWords(const std::vector<int16_t>& audioBuffer, ps_decoder_t& decoder);
|
BoundedTimeline<std::string> recognizeWords(
|
||||||
|
const std::vector<int16_t>& audioBuffer,
|
||||||
|
ps_decoder_t& decoder
|
||||||
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#include "tools/tools.h"
|
#include "tools/tools.h"
|
||||||
#include "tools/stringTools.h"
|
#include "tools/stringTools.h"
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
#include <boost/optional/optional.hpp>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <cst_utt_utils.h>
|
#include <cst_utt_utils.h>
|
||||||
|
@ -37,7 +38,10 @@ vector<string> tokenizeViaFlite(const string& text) {
|
||||||
const string asciiText = utf8ToAscii(text);
|
const string asciiText = utf8ToAscii(text);
|
||||||
|
|
||||||
// Create utterance object with text
|
// Create utterance object with text
|
||||||
lambda_unique_ptr<cst_utterance> utterance(new_utterance(), [](cst_utterance* utterance) { delete_utterance(utterance); });
|
lambda_unique_ptr<cst_utterance> utterance(
|
||||||
|
new_utterance(),
|
||||||
|
[](cst_utterance* utterance) { delete_utterance(utterance); }
|
||||||
|
);
|
||||||
utt_set_input_text(utterance.get(), asciiText.c_str());
|
utt_set_input_text(utterance.get(), asciiText.c_str());
|
||||||
lambda_unique_ptr<cst_voice> voice = createDummyVoice();
|
lambda_unique_ptr<cst_voice> voice = createDummyVoice();
|
||||||
utt_init(utterance.get(), voice.get());
|
utt_init(utterance.get(), voice.get());
|
||||||
|
@ -48,14 +52,21 @@ vector<string> tokenizeViaFlite(const string& text) {
|
||||||
}
|
}
|
||||||
|
|
||||||
vector<string> result;
|
vector<string> result;
|
||||||
for (cst_item* item = relation_head(utt_relation(utterance.get(), "Word")); item; item = item_next(item)) {
|
for (
|
||||||
|
cst_item* item = relation_head(utt_relation(utterance.get(), "Word"));
|
||||||
|
item;
|
||||||
|
item = item_next(item)
|
||||||
|
) {
|
||||||
const char* word = item_feat_string(item, "name");
|
const char* word = item_feat_string(item, "name");
|
||||||
result.push_back(word);
|
result.emplace_back(word);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<string> findSimilarDictionaryWord(const string& word, function<bool(const string&)> dictionaryContains) {
|
optional<string> findSimilarDictionaryWord(
|
||||||
|
const string& word,
|
||||||
|
const function<bool(const string&)>& dictionaryContains
|
||||||
|
) {
|
||||||
for (bool addPeriod : { false, true }) {
|
for (bool addPeriod : { false, true }) {
|
||||||
for (int apostropheIndex = -1; apostropheIndex <= static_cast<int>(word.size()); ++apostropheIndex) {
|
for (int apostropheIndex = -1; apostropheIndex <= static_cast<int>(word.size()); ++apostropheIndex) {
|
||||||
string modified = word;
|
string modified = word;
|
||||||
|
@ -75,12 +86,15 @@ optional<string> findSimilarDictionaryWord(const string& word, function<bool(con
|
||||||
return boost::none;
|
return boost::none;
|
||||||
}
|
}
|
||||||
|
|
||||||
vector<string> tokenizeText(const string& text, function<bool(const string&)> dictionaryContains) {
|
vector<string> tokenizeText(
|
||||||
|
const string& text,
|
||||||
|
const function<bool(const string&)>& dictionaryContains
|
||||||
|
) {
|
||||||
vector<string> words = tokenizeViaFlite(text);
|
vector<string> words = tokenizeViaFlite(text);
|
||||||
|
|
||||||
// Join words separated by apostophes
|
// Join words separated by apostrophes
|
||||||
for (int i = words.size() - 1; i > 0; --i) {
|
for (int i = words.size() - 1; i > 0; --i) {
|
||||||
if (words[i].size() > 0 && words[i][0] == '\'') {
|
if (!words[i].empty() && words[i][0] == '\'') {
|
||||||
words[i - 1].append(words[i]);
|
words[i - 1].append(words[i]);
|
||||||
words.erase(words.begin() + i);
|
words.erase(words.begin() + i);
|
||||||
}
|
}
|
||||||
|
@ -95,21 +109,24 @@ vector<string> tokenizeText(const string& text, function<bool(const string&)> di
|
||||||
{ regex("@"), "at" },
|
{ regex("@"), "at" },
|
||||||
{ regex("[^a-z']"), "" }
|
{ regex("[^a-z']"), "" }
|
||||||
};
|
};
|
||||||
for (size_t i = 0; i < words.size(); ++i) {
|
for (auto& word : words) {
|
||||||
for (const auto& replacement : replacements) {
|
for (const auto& replacement : replacements) {
|
||||||
words[i] = regex_replace(words[i], replacement.first, replacement.second);
|
word = regex_replace(word, replacement.first, replacement.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove empty words
|
// Remove empty words
|
||||||
words.erase(std::remove_if(words.begin(), words.end(), [](const string& s) { return s.empty(); }), words.end());
|
words.erase(
|
||||||
|
std::remove_if(words.begin(), words.end(), [](const string& s) { return s.empty(); }),
|
||||||
|
words.end()
|
||||||
|
);
|
||||||
|
|
||||||
// Try to replace words that are not in the dictionary with similar ones that are
|
// Try to replace words that are not in the dictionary with similar ones that are
|
||||||
for (size_t i = 0; i < words.size(); ++i) {
|
for (auto& word : words) {
|
||||||
if (!dictionaryContains(words[i])) {
|
if (!dictionaryContains(word)) {
|
||||||
optional<string> modifiedWord = findSimilarDictionaryWord(words[i], dictionaryContains);
|
optional<string> modifiedWord = findSimilarDictionaryWord(word, dictionaryContains);
|
||||||
if (modifiedWord) {
|
if (modifiedWord) {
|
||||||
words[i] = *modifiedWord;
|
word = *modifiedWord;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,7 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
std::vector<std::string> tokenizeText(const std::string& text, std::function<bool(const std::string&)> dictionaryContains);
|
std::vector<std::string> tokenizeText(
|
||||||
|
const std::string& text,
|
||||||
|
const std::function<bool(const std::string&)>& dictionaryContains
|
||||||
|
);
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
#include <tclap/CmdLine.h>
|
#include <tclap/CmdLine.h>
|
||||||
#include "core/appInfo.h"
|
#include "core/appInfo.h"
|
||||||
#include "tools/NiceCmdLineOutput.h"
|
#include "tools/NiceCmdLineOutput.h"
|
||||||
#include "tools/ProgressBar.h"
|
|
||||||
#include "logging/logging.h"
|
#include "logging/logging.h"
|
||||||
#include "logging/sinks.h"
|
#include "logging/sinks.h"
|
||||||
#include "logging/formatters.h"
|
#include "logging/formatters.h"
|
||||||
|
@ -52,21 +51,24 @@ namespace TCLAP {
|
||||||
struct ArgTraits<logging::Level> {
|
struct ArgTraits<logging::Level> {
|
||||||
typedef ValueLike ValueCategory;
|
typedef ValueLike ValueCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
struct ArgTraits<ExportFormat> {
|
struct ArgTraits<ExportFormat> {
|
||||||
typedef ValueLike ValueCategory;
|
typedef ValueLike ValueCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
struct ArgTraits<RecognizerType> {
|
struct ArgTraits<RecognizerType> {
|
||||||
typedef ValueLike ValueCategory;
|
typedef ValueLike ValueCategory;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
shared_ptr<logging::Sink> createFileSink(path path, logging::Level minLevel) {
|
shared_ptr<logging::Sink> createFileSink(const path& path, logging::Level minLevel) {
|
||||||
auto file = make_shared<boost::filesystem::ofstream>();
|
auto file = make_shared<boost::filesystem::ofstream>();
|
||||||
file->exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
file->exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
||||||
file->open(path);
|
file->open(path);
|
||||||
auto FileSink = make_shared<logging::StreamSink>(file, make_shared<logging::SimpleFileFormatter>());
|
auto FileSink =
|
||||||
|
make_shared<logging::StreamSink>(file, make_shared<logging::SimpleFileFormatter>());
|
||||||
return make_shared<logging::LevelFilter>(FileSink, minLevel);
|
return make_shared<logging::LevelFilter>(FileSink, minLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,24 +126,71 @@ int main(int platformArgc, char *platformArgv[]) {
|
||||||
tclap::CmdLine cmd(appName, argumentValueSeparator, appVersion);
|
tclap::CmdLine cmd(appName, argumentValueSeparator, appVersion);
|
||||||
cmd.setExceptionHandling(false);
|
cmd.setExceptionHandling(false);
|
||||||
cmd.setOutput(new NiceCmdLineOutput());
|
cmd.setOutput(new NiceCmdLineOutput());
|
||||||
tclap::ValueArg<string> outputFileName("o", "output", "The output file path.", false, string(), "string", cmd);
|
|
||||||
|
tclap::ValueArg<string> outputFileName(
|
||||||
|
"o", "output", "The output file path.",
|
||||||
|
false, string(), "string", cmd
|
||||||
|
);
|
||||||
|
|
||||||
auto logLevels = vector<logging::Level>(logging::LevelConverter::get().getValues());
|
auto logLevels = vector<logging::Level>(logging::LevelConverter::get().getValues());
|
||||||
tclap::ValuesConstraint<logging::Level> logLevelConstraint(logLevels);
|
tclap::ValuesConstraint<logging::Level> logLevelConstraint(logLevels);
|
||||||
tclap::ValueArg<logging::Level> logLevel("", "logLevel", "The minimum log level that will be written to the log file", false, logging::Level::Debug, &logLevelConstraint, cmd);
|
tclap::ValueArg<logging::Level> logLevel(
|
||||||
tclap::ValueArg<string> logFileName("", "logFile", "The log file path.", false, string(), "string", cmd);
|
"", "logLevel", "The minimum log level that will be written to the log file",
|
||||||
tclap::ValueArg<logging::Level> consoleLevel("", "consoleLevel", "The minimum log level that will be printed on the console (stderr)", false, defaultMinStderrLevel, &logLevelConstraint, cmd);
|
false, logging::Level::Debug, &logLevelConstraint, cmd
|
||||||
tclap::SwitchArg machineReadableMode("", "machineReadable", "Formats all output to stderr in a structured JSON format.", cmd, false);
|
);
|
||||||
tclap::SwitchArg quietMode("q", "quiet", "Suppresses all output to stderr except for warnings and error messages.", cmd, false);
|
|
||||||
tclap::ValueArg<int> maxThreadCount("", "threads", "The maximum number of worker threads to use.", false, getProcessorCoreCount(), "number", cmd);
|
tclap::ValueArg<string> logFileName(
|
||||||
tclap::ValueArg<string> extendedShapes("", "extendedShapes", "All extended, optional shapes to use.", false, "GHX", "string", cmd);
|
"", "logFile", "The log file path.",
|
||||||
tclap::ValueArg<string> dialogFile("d", "dialogFile", "A file containing the text of the dialog.", false, string(), "string", cmd);
|
false, string(), "string", cmd
|
||||||
|
);
|
||||||
|
tclap::ValueArg<logging::Level> consoleLevel(
|
||||||
|
"", "consoleLevel", "The minimum log level that will be printed on the console (stderr)",
|
||||||
|
false, defaultMinStderrLevel, &logLevelConstraint, cmd
|
||||||
|
);
|
||||||
|
|
||||||
|
tclap::SwitchArg machineReadableMode(
|
||||||
|
"", "machineReadable", "Formats all output to stderr in a structured JSON format.",
|
||||||
|
cmd, false
|
||||||
|
);
|
||||||
|
|
||||||
|
tclap::SwitchArg quietMode(
|
||||||
|
"q", "quiet", "Suppresses all output to stderr except for warnings and error messages.",
|
||||||
|
cmd, false
|
||||||
|
);
|
||||||
|
|
||||||
|
tclap::ValueArg<int> maxThreadCount(
|
||||||
|
"", "threads", "The maximum number of worker threads to use.",
|
||||||
|
false, getProcessorCoreCount(), "number", cmd
|
||||||
|
);
|
||||||
|
|
||||||
|
tclap::ValueArg<string> extendedShapes(
|
||||||
|
"", "extendedShapes", "All extended, optional shapes to use.",
|
||||||
|
false, "GHX", "string", cmd
|
||||||
|
);
|
||||||
|
|
||||||
|
tclap::ValueArg<string> dialogFile(
|
||||||
|
"d", "dialogFile", "A file containing the text of the dialog.",
|
||||||
|
false, string(), "string", cmd
|
||||||
|
);
|
||||||
|
|
||||||
auto exportFormats = vector<ExportFormat>(ExportFormatConverter::get().getValues());
|
auto exportFormats = vector<ExportFormat>(ExportFormatConverter::get().getValues());
|
||||||
tclap::ValuesConstraint<ExportFormat> exportFormatConstraint(exportFormats);
|
tclap::ValuesConstraint<ExportFormat> exportFormatConstraint(exportFormats);
|
||||||
tclap::ValueArg<ExportFormat> exportFormat("f", "exportFormat", "The export format.", false, ExportFormat::Tsv, &exportFormatConstraint, cmd);
|
tclap::ValueArg<ExportFormat> exportFormat(
|
||||||
|
"f", "exportFormat", "The export format.",
|
||||||
|
false, ExportFormat::Tsv, &exportFormatConstraint, cmd
|
||||||
|
);
|
||||||
|
|
||||||
auto recognizerTypes = vector<RecognizerType>(RecognizerTypeConverter::get().getValues());
|
auto recognizerTypes = vector<RecognizerType>(RecognizerTypeConverter::get().getValues());
|
||||||
tclap::ValuesConstraint<RecognizerType> recognizerConstraint(recognizerTypes);
|
tclap::ValuesConstraint<RecognizerType> recognizerConstraint(recognizerTypes);
|
||||||
tclap::ValueArg<RecognizerType> recognizerType("r", "recognizer", "The dialog recognizer.", false, RecognizerType::PocketSphinx, &recognizerConstraint, cmd);
|
tclap::ValueArg<RecognizerType> recognizerType(
|
||||||
tclap::UnlabeledValueArg<string> inputFileName("inputFile", "The input file. Must be a sound file in WAVE format.", true, "", "string", cmd);
|
"r", "recognizer", "The dialog recognizer.",
|
||||||
|
false, RecognizerType::PocketSphinx, &recognizerConstraint, cmd
|
||||||
|
);
|
||||||
|
|
||||||
|
tclap::UnlabeledValueArg<string> inputFileName(
|
||||||
|
"inputFile", "The input file. Must be a sound file in WAVE format.",
|
||||||
|
true, "", "string", cmd
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse command line
|
// Parse command line
|
||||||
|
@ -180,13 +229,17 @@ int main(int platformArgc, char *platformArgv[]) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// On progress change: Create log message
|
// On progress change: Create log message
|
||||||
ProgressForwarder progressSink([](double progress) { logging::log(ProgressEntry(progress)); });
|
ProgressForwarder progressSink([](double progress) {
|
||||||
|
logging::log(ProgressEntry(progress));
|
||||||
|
});
|
||||||
|
|
||||||
// Animate the recording
|
// Animate the recording
|
||||||
logging::info("Starting animation.");
|
logging::info("Starting animation.");
|
||||||
JoiningContinuousTimeline<Shape> animation = animateWaveFile(
|
JoiningContinuousTimeline<Shape> animation = animateWaveFile(
|
||||||
inputFilePath,
|
inputFilePath,
|
||||||
dialogFile.isSet() ? readUtf8File(path(dialogFile.getValue())) : boost::optional<string>(),
|
dialogFile.isSet()
|
||||||
|
? readUtf8File(path(dialogFile.getValue()))
|
||||||
|
: boost::optional<string>(),
|
||||||
*createRecognizer(recognizerType.getValue()),
|
*createRecognizer(recognizerType.getValue()),
|
||||||
targetShapeSet,
|
targetShapeSet,
|
||||||
maxThreadCount.getValue(),
|
maxThreadCount.getValue(),
|
||||||
|
@ -207,7 +260,9 @@ int main(int platformArgc, char *platformArgv[]) {
|
||||||
|
|
||||||
logging::log(SuccessEntry());
|
logging::log(SuccessEntry());
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
std::throw_with_nested(std::runtime_error(fmt::format("Error processing file {}.", inputFilePath)));
|
std::throw_with_nested(
|
||||||
|
std::runtime_error(fmt::format("Error processing file {}.", inputFilePath))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
using std::string;
|
using std::string;
|
||||||
using std::make_shared;
|
using std::make_shared;
|
||||||
using logging::Level;
|
using logging::Level;
|
||||||
using logging::LevelFilter;
|
|
||||||
using logging::StdErrSink;
|
using logging::StdErrSink;
|
||||||
using logging::SimpleConsoleFormatter;
|
using logging::SimpleConsoleFormatter;
|
||||||
using boost::optional;
|
using boost::optional;
|
||||||
|
@ -21,11 +20,14 @@ NiceStderrSink::NiceStderrSink(Level minLevel) :
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void NiceStderrSink::receive(const logging::Entry& entry) {
|
void NiceStderrSink::receive(const logging::Entry& entry) {
|
||||||
// For selected semantic entries, print a user-friendly message instead of the technical log message.
|
// For selected semantic entries, print a user-friendly message instead of
|
||||||
if (const StartEntry* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
// the technical log message.
|
||||||
std::cerr << fmt::format("Generating lip sync data for {}.", startEntry->getInputFilePath()) << std::endl;
|
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
||||||
|
std::cerr
|
||||||
|
<< fmt::format("Generating lip sync data for {}.", startEntry->getInputFilePath())
|
||||||
|
<< std::endl;
|
||||||
startProgressIndication();
|
startProgressIndication();
|
||||||
} else if (const ProgressEntry* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
|
} else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
|
||||||
assert(progressBar);
|
assert(progressBar);
|
||||||
progress = progressEntry->getProgress();
|
progress = progressEntry->getProgress();
|
||||||
progressBar->reportProgress(progress);
|
progressBar->reportProgress(progress);
|
||||||
|
@ -65,7 +67,7 @@ QuietStderrSink::QuietStderrSink(Level minLevel) :
|
||||||
|
|
||||||
void QuietStderrSink::receive(const logging::Entry& entry) {
|
void QuietStderrSink::receive(const logging::Entry& entry) {
|
||||||
// Set inputFilePath as soon as we get it
|
// Set inputFilePath as soon as we get it
|
||||||
if (const StartEntry* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
||||||
inputFilePath = startEntry->getInputFilePath();
|
inputFilePath = startEntry->getInputFilePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,26 +89,42 @@ MachineReadableStderrSink::MachineReadableStderrSink(Level minLevel) :
|
||||||
{}
|
{}
|
||||||
|
|
||||||
string formatLogProperty(const logging::Entry& entry) {
|
string formatLogProperty(const logging::Entry& entry) {
|
||||||
return fmt::format(R"("log": {{ "level": "{}", "message": "{}" }})", entry.level, escapeJsonString(entry.message));
|
return fmt::format(
|
||||||
|
R"("log": {{ "level": "{}", "message": "{}" }})",
|
||||||
|
entry.level,
|
||||||
|
escapeJsonString(entry.message)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MachineReadableStderrSink::receive(const logging::Entry& entry) {
|
void MachineReadableStderrSink::receive(const logging::Entry& entry) {
|
||||||
optional<string> line;
|
optional<string> line;
|
||||||
if (dynamic_cast<const SemanticEntry*>(&entry)) {
|
if (dynamic_cast<const SemanticEntry*>(&entry)) {
|
||||||
if (const StartEntry* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
||||||
const string file = escapeJsonString(startEntry->getInputFilePath().string());
|
const string file = escapeJsonString(startEntry->getInputFilePath().string());
|
||||||
line = fmt::format(R"({{ "type": "start", "file": "{}", {} }})", file, formatLogProperty(entry));
|
line = fmt::format(
|
||||||
} else if (const ProgressEntry* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
|
R"({{ "type": "start", "file": "{}", {} }})",
|
||||||
|
file,
|
||||||
|
formatLogProperty(entry)
|
||||||
|
);
|
||||||
|
} else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
|
||||||
const int progressPercent = static_cast<int>(progressEntry->getProgress() * 100);
|
const int progressPercent = static_cast<int>(progressEntry->getProgress() * 100);
|
||||||
if (progressPercent > lastProgressPercent) {
|
if (progressPercent > lastProgressPercent) {
|
||||||
line = fmt::format(R"({{ "type": "progress", "value": {:.2f}, {} }})", progressEntry->getProgress(), formatLogProperty(entry));
|
line = fmt::format(
|
||||||
|
R"({{ "type": "progress", "value": {:.2f}, {} }})",
|
||||||
|
progressEntry->getProgress(),
|
||||||
|
formatLogProperty(entry)
|
||||||
|
);
|
||||||
lastProgressPercent = progressPercent;
|
lastProgressPercent = progressPercent;
|
||||||
}
|
}
|
||||||
} else if (dynamic_cast<const SuccessEntry*>(&entry)) {
|
} else if (dynamic_cast<const SuccessEntry*>(&entry)) {
|
||||||
line = fmt::format(R"({{ "type": "success", {} }})", formatLogProperty(entry));
|
line = fmt::format(R"({{ "type": "success", {} }})", formatLogProperty(entry));
|
||||||
} else if (const FailureEntry* failureEntry = dynamic_cast<const FailureEntry*>(&entry)) {
|
} else if (const auto* failureEntry = dynamic_cast<const FailureEntry*>(&entry)) {
|
||||||
const string reason = escapeJsonString(failureEntry->getReason());
|
const string reason = escapeJsonString(failureEntry->getReason());
|
||||||
line = fmt::format(R"({{ "type": "failure", "reason": "{}", {} }})", reason, formatLogProperty(entry));
|
line = fmt::format(
|
||||||
|
R"({{ "type": "failure", "reason": "{}", {} }})",
|
||||||
|
reason,
|
||||||
|
formatLogProperty(entry)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw std::runtime_error("Unsupported type of semantic entry.");
|
throw std::runtime_error("Unsupported type of semantic entry.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,10 @@ public:
|
||||||
|
|
||||||
// Clip the value's range to bounds
|
// Clip the value's range to bounds
|
||||||
TimeRange& valueRange = timedValue.getTimeRange();
|
TimeRange& valueRange = timedValue.getTimeRange();
|
||||||
valueRange.resize(max(range.getStart(), valueRange.getStart()), min(range.getEnd(), valueRange.getEnd()));
|
valueRange.resize(
|
||||||
|
max(range.getStart(), valueRange.getStart()),
|
||||||
|
min(range.getEnd(), valueRange.getEnd())
|
||||||
|
);
|
||||||
|
|
||||||
return Timeline<T, AutoJoin>::set(timedValue);
|
return Timeline<T, AutoJoin>::set(timedValue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,11 @@ public:
|
||||||
ContinuousTimeline(range, defaultValue, collection.begin(), collection.end())
|
ContinuousTimeline(range, defaultValue, collection.begin(), collection.end())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
ContinuousTimeline(TimeRange range, T defaultValue, std::initializer_list<Timed<T>> initializerList) :
|
ContinuousTimeline(
|
||||||
|
TimeRange range,
|
||||||
|
T defaultValue,
|
||||||
|
std::initializer_list<Timed<T>> initializerList
|
||||||
|
) :
|
||||||
ContinuousTimeline(range, defaultValue, initializerList.begin(), initializerList.end())
|
ContinuousTimeline(range, defaultValue, initializerList.begin(), initializerList.end())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,11 @@ TimeRange::TimeRange(time_type start, time_type end) :
|
||||||
end(end)
|
end(end)
|
||||||
{
|
{
|
||||||
if (start > end) {
|
if (start > end) {
|
||||||
throw std::invalid_argument(fmt::format("Time range start must not be less than end. Start: {0}, end: {1}", start, end));
|
throw std::invalid_argument(fmt::format(
|
||||||
|
"Time range start must not be less than end. Start: {0}, end: {1}",
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +92,7 @@ void TimeRange::shrink(time_type value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TimeRange::trim(const TimeRange& limits) {
|
void TimeRange::trim(const TimeRange& limits) {
|
||||||
TimeRange newRange(std::max(start, limits.start), std::min(end, limits.end));
|
const TimeRange newRange(std::max(start, limits.start), std::min(end, limits.end));
|
||||||
resize(newRange);
|
resize(newRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,12 @@ private:
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::ostream& operator<<(std::ostream& stream, const Timed<T>& timedValue) {
|
std::ostream& operator<<(std::ostream& stream, const Timed<T>& timedValue) {
|
||||||
return stream << "Timed(" << timedValue.getStart() << ", " << timedValue.getEnd() << ", " << timedValue.getValue() << ")";
|
return stream
|
||||||
|
<< "Timed("
|
||||||
|
<< timedValue.getStart() << ", "
|
||||||
|
<< timedValue.getEnd() << ", "
|
||||||
|
<< timedValue.getValue()
|
||||||
|
<< ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
|
@ -130,5 +135,9 @@ private:
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
inline std::ostream& operator<<(std::ostream& stream, const Timed<void>& timedValue) {
|
inline std::ostream& operator<<(std::ostream& stream, const Timed<void>& timedValue) {
|
||||||
return stream << "Timed<void>(" << timedValue.getTimeRange().getStart() << ", " << timedValue.getTimeRange().getEnd() << ")";
|
return stream
|
||||||
|
<< "Timed<void>("
|
||||||
|
<< timedValue.getTimeRange().getStart() << ", "
|
||||||
|
<< timedValue.getTimeRange().getEnd()
|
||||||
|
<< ")";
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,12 +36,15 @@ private:
|
||||||
bool operator()(const Timed<T>& lhs, const Timed<T>& rhs) const {
|
bool operator()(const Timed<T>& lhs, const Timed<T>& rhs) const {
|
||||||
return lhs.getStart() < rhs.getStart();
|
return lhs.getStart() < rhs.getStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator()(const time_type& lhs, const Timed<T>& rhs) const {
|
bool operator()(const time_type& lhs, const Timed<T>& rhs) const {
|
||||||
return lhs < rhs.getStart();
|
return lhs < rhs.getStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator()(const Timed<T>& lhs, const time_type& rhs) const {
|
bool operator()(const Timed<T>& lhs, const time_type& rhs) const {
|
||||||
return lhs.getStart() < rhs;
|
return lhs.getStart() < rhs;
|
||||||
}
|
}
|
||||||
|
|
||||||
using is_transparent = int;
|
using is_transparent = int;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,7 +91,7 @@ public:
|
||||||
time_type time;
|
time_type time;
|
||||||
};
|
};
|
||||||
|
|
||||||
Timeline() {}
|
Timeline() = default;
|
||||||
|
|
||||||
template<typename InputIterator>
|
template<typename InputIterator>
|
||||||
Timeline(InputIterator first, InputIterator last) {
|
Timeline(InputIterator first, InputIterator last) {
|
||||||
|
@ -107,7 +110,7 @@ public:
|
||||||
Timeline(initializerList.begin(), initializerList.end())
|
Timeline(initializerList.begin(), initializerList.end())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
virtual ~Timeline() {}
|
virtual ~Timeline() = default;
|
||||||
|
|
||||||
bool empty() const {
|
bool empty() const {
|
||||||
return elements.empty();
|
return elements.empty();
|
||||||
|
@ -141,22 +144,26 @@ public:
|
||||||
|
|
||||||
iterator find(time_type time, FindMode findMode = FindMode::SampleRight) const {
|
iterator find(time_type time, FindMode findMode = FindMode::SampleRight) const {
|
||||||
switch (findMode) {
|
switch (findMode) {
|
||||||
case FindMode::SampleLeft: {
|
case FindMode::SampleLeft:
|
||||||
|
{
|
||||||
iterator left = find(time, FindMode::SearchLeft);
|
iterator left = find(time, FindMode::SearchLeft);
|
||||||
return left != end() && left->getEnd() >= time ? left : end();
|
return left != end() && left->getEnd() >= time ? left : end();
|
||||||
}
|
}
|
||||||
case FindMode::SampleRight: {
|
case FindMode::SampleRight:
|
||||||
|
{
|
||||||
iterator right = find(time, FindMode::SearchRight);
|
iterator right = find(time, FindMode::SearchRight);
|
||||||
return right != end() && right->getStart() <= time ? right : end();
|
return right != end() && right->getStart() <= time ? right : end();
|
||||||
}
|
}
|
||||||
case FindMode::SearchLeft: {
|
case FindMode::SearchLeft:
|
||||||
|
{
|
||||||
// Get first element starting >= time
|
// Get first element starting >= time
|
||||||
iterator it = elements.lower_bound(time);
|
iterator it = elements.lower_bound(time);
|
||||||
|
|
||||||
// Go one element back
|
// Go one element back
|
||||||
return it != begin() ? --it : end();
|
return it != begin() ? --it : end();
|
||||||
}
|
}
|
||||||
case FindMode::SearchRight: {
|
case FindMode::SearchRight:
|
||||||
|
{
|
||||||
// Get first element starting > time
|
// Get first element starting > time
|
||||||
iterator it = elements.upper_bound(time);
|
iterator it = elements.upper_bound(time);
|
||||||
|
|
||||||
|
@ -187,7 +194,10 @@ public:
|
||||||
splitAt(range.getEnd());
|
splitAt(range.getEnd());
|
||||||
|
|
||||||
// Erase overlapping elements
|
// Erase overlapping elements
|
||||||
elements.erase(find(range.getStart(), FindMode::SearchRight), find(range.getEnd(), FindMode::SearchRight));
|
elements.erase(
|
||||||
|
find(range.getStart(), FindMode::SearchRight),
|
||||||
|
find(range.getEnd(), FindMode::SearchRight)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear(time_type start, time_type end) {
|
void clear(time_type start, time_type end) {
|
||||||
|
@ -220,12 +230,19 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename TElement = T>
|
template<typename TElement = T>
|
||||||
iterator set(const TimeRange& timeRange, const std::enable_if_t<!std::is_void<TElement>::value, T>& value) {
|
iterator set(
|
||||||
|
const TimeRange& timeRange,
|
||||||
|
const std::enable_if_t<!std::is_void<TElement>::value, T>& value
|
||||||
|
) {
|
||||||
return set(Timed<T>(timeRange, value));
|
return set(Timed<T>(timeRange, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename TElement = T>
|
template<typename TElement = T>
|
||||||
iterator set(time_type start, time_type end, const std::enable_if_t<!std::is_void<TElement>::value, T>& value) {
|
iterator set(
|
||||||
|
time_type start,
|
||||||
|
time_type end,
|
||||||
|
const std::enable_if_t<!std::is_void<TElement>::value, T>& value
|
||||||
|
) {
|
||||||
return set(Timed<T>(start, end, value));
|
return set(Timed<T>(start, end, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +268,10 @@ public:
|
||||||
for (auto it = copy.begin(); it != copy.end(); ++it) {
|
for (auto it = copy.begin(); it != copy.end(); ++it) {
|
||||||
const auto rangeBegin = it;
|
const auto rangeBegin = it;
|
||||||
auto rangeEnd = std::next(rangeBegin);
|
auto rangeEnd = std::next(rangeBegin);
|
||||||
while (rangeEnd != copy.end() && rangeEnd->getStart() == rangeBegin->getEnd() && ::internal::valueEquals(*rangeEnd, *rangeBegin)) {
|
while (rangeEnd != copy.end()
|
||||||
|
&& rangeEnd->getStart() == rangeBegin->getEnd()
|
||||||
|
&& ::internal::valueEquals(*rangeEnd, *rangeBegin)
|
||||||
|
) {
|
||||||
++rangeEnd;
|
++rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
|
|
||||||
typedef std::chrono::duration<int, std::centi> centiseconds;
|
using centiseconds = std::chrono::duration<int, std::centi>;
|
||||||
|
|
||||||
std::ostream& operator <<(std::ostream& stream, const centiseconds cs);
|
std::ostream& operator <<(std::ostream& stream, centiseconds cs);
|
||||||
|
|
||||||
#pragma warning(push)
|
#pragma warning(push)
|
||||||
#pragma warning(disable: 4455)
|
#pragma warning(disable: 4455)
|
||||||
|
|
|
@ -7,8 +7,13 @@
|
||||||
|
|
||||||
template<typename TValue>
|
template<typename TValue>
|
||||||
void logTimedEvent(const std::string& eventName, const Timed<TValue> timedValue) {
|
void logTimedEvent(const std::string& eventName, const Timed<TValue> timedValue) {
|
||||||
logging::debugFormat("##{0}[{1}-{2}]: {3}",
|
logging::debugFormat(
|
||||||
eventName, formatDuration(timedValue.getStart()), formatDuration(timedValue.getEnd()), timedValue.getValue());
|
"##{0}[{1}-{2}]: {3}",
|
||||||
|
eventName,
|
||||||
|
formatDuration(timedValue.getStart()),
|
||||||
|
formatDuration(timedValue.getEnd()),
|
||||||
|
timedValue.getValue()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename TValue>
|
template<typename TValue>
|
||||||
|
@ -17,6 +22,11 @@ void logTimedEvent(const std::string& eventName, const TimeRange& timeRange, con
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename TValue>
|
template<typename TValue>
|
||||||
void logTimedEvent(const std::string& eventName, centiseconds start, centiseconds end, const TValue& value) {
|
void logTimedEvent(
|
||||||
|
const std::string& eventName,
|
||||||
|
centiseconds start,
|
||||||
|
centiseconds end,
|
||||||
|
const TValue& value
|
||||||
|
) {
|
||||||
logTimedEvent(eventName, Timed<TValue>(start, end, value));
|
logTimedEvent(eventName, Timed<TValue>(start, end, value));
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <initializer_list>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
@ -30,7 +29,9 @@ public:
|
||||||
auto result = tryToString(value);
|
auto result = tryToString(value);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
auto numericValue = static_cast<typename std::underlying_type<TEnum>::type>(value);
|
auto numericValue = static_cast<typename std::underlying_type<TEnum>::type>(value);
|
||||||
throw std::invalid_argument(fmt::format("{} is not a valid {} value.", numericValue, typeName));
|
throw std::invalid_argument(
|
||||||
|
fmt::format("{} is not a valid {} value.", numericValue, typeName)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return *result;
|
return *result;
|
||||||
|
|
|
@ -55,7 +55,10 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void init() const {
|
void init() const {
|
||||||
std::call_once(state->initialized, [&] { state->value = std::make_unique<T>(state->createValue()); });
|
std::call_once(
|
||||||
|
state->initialized,
|
||||||
|
[&] { state->value = std::make_unique<T>(state->createValue()); }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<State> state = std::make_shared<State>();
|
std::shared_ptr<State> state = std::make_shared<State>();
|
||||||
|
|
|
@ -36,7 +36,10 @@ void NiceCmdLineOutput::failure(CmdLineInterface& cli, TCLAP::ArgException& e) {
|
||||||
std::cerr << "Short usage:" << endl;
|
std::cerr << "Short usage:" << endl;
|
||||||
printShortUsage(cli, std::cerr);
|
printShortUsage(cli, std::cerr);
|
||||||
|
|
||||||
std::cerr << endl << "For complete usage and help, type `" << getBinaryName() << " --help`" << endl << endl;
|
std::cerr
|
||||||
|
<< endl
|
||||||
|
<< "For complete usage and help, type `" << getBinaryName() << " --help`" << endl
|
||||||
|
<< endl;
|
||||||
} else {
|
} else {
|
||||||
usage(cli);
|
usage(cli);
|
||||||
}
|
}
|
||||||
|
@ -76,8 +79,9 @@ void NiceCmdLineOutput::printLongUsage(CmdLineInterface& cli, std::ostream& outS
|
||||||
const vector<vector<TCLAP::Arg*>> xorArgGroups = xorHandler.getXorList();
|
const vector<vector<TCLAP::Arg*>> xorArgGroups = xorHandler.getXorList();
|
||||||
for (const vector<TCLAP::Arg*>& xorArgGroup : xorArgGroups) {
|
for (const vector<TCLAP::Arg*>& xorArgGroup : xorArgGroups) {
|
||||||
for (auto arg : xorArgGroup) {
|
for (auto arg : xorArgGroup) {
|
||||||
if (arg != xorArgGroup[0])
|
if (arg != xorArgGroup[0]) {
|
||||||
outStream << "-- or --" << endl;
|
outStream << "-- or --" << endl;
|
||||||
|
}
|
||||||
|
|
||||||
tablePrinter.printRow({ arg->longID(), arg->getDescription() });
|
tablePrinter.printRow({ arg->longID(), arg->getDescription() });
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,13 +54,13 @@ void ProgressBar::update(bool showSpinner) {
|
||||||
const int blockCount = 20;
|
const int blockCount = 20;
|
||||||
const string animation = "|/-\\";
|
const string animation = "|/-\\";
|
||||||
|
|
||||||
int progressBlockCount = static_cast<int>(currentProgress * blockCount);
|
const int progressBlockCount = static_cast<int>(currentProgress * blockCount);
|
||||||
const double epsilon = 0.0001;
|
const double epsilon = 0.0001;
|
||||||
int percent = static_cast<int>(currentProgress * 100 + epsilon);
|
const int percent = static_cast<int>(currentProgress * 100 + epsilon);
|
||||||
const string spinner = showSpinner
|
const string spinner = showSpinner
|
||||||
? string(1, animation[animationIndex++ % animation.size()])
|
? string(1, animation[animationIndex++ % animation.size()])
|
||||||
: "";
|
: "";
|
||||||
string text = fmt::format("[{0}{1}] {2:3}% {3}",
|
const string text = fmt::format("[{0}{1}] {2:3}% {3}",
|
||||||
string(progressBlockCount, '#'), string(blockCount - progressBlockCount, '-'),
|
string(progressBlockCount, '#'), string(blockCount - progressBlockCount, '-'),
|
||||||
percent,
|
percent,
|
||||||
spinner
|
spinner
|
||||||
|
@ -71,7 +71,7 @@ void ProgressBar::update(bool showSpinner) {
|
||||||
void ProgressBar::updateText(const string& text) {
|
void ProgressBar::updateText(const string& text) {
|
||||||
// Get length of common portion
|
// Get length of common portion
|
||||||
int commonPrefixLength = 0;
|
int commonPrefixLength = 0;
|
||||||
int commonLength = std::min(currentText.size(), text.size());
|
const int commonLength = std::min(currentText.size(), text.size());
|
||||||
while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength]) {
|
while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength]) {
|
||||||
commonPrefixLength++;
|
commonPrefixLength++;
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ void ProgressBar::updateText(const string& text) {
|
||||||
output.append(text, commonPrefixLength, text.size() - commonPrefixLength);
|
output.append(text, commonPrefixLength, text.size() - commonPrefixLength);
|
||||||
|
|
||||||
// ... if the new text is shorter than the old one: delete overlapping characters
|
// ... if the new text is shorter than the old one: delete overlapping characters
|
||||||
int overlapCount = currentText.size() - text.size();
|
const int overlapCount = currentText.size() - text.size();
|
||||||
if (overlapCount > 0) {
|
if (overlapCount > 0) {
|
||||||
output.append(overlapCount, ' ');
|
output.append(overlapCount, ' ');
|
||||||
output.append(overlapCount, '\b');
|
output.append(overlapCount, '\b');
|
||||||
|
|
|
@ -24,7 +24,9 @@ TablePrinter::TablePrinter(ostream *stream, initializer_list<int> columnWidths,
|
||||||
}
|
}
|
||||||
|
|
||||||
void TablePrinter::printRow(initializer_list<string> columns) const {
|
void TablePrinter::printRow(initializer_list<string> columns) const {
|
||||||
if (columns.size() != columnWidths.size()) throw invalid_argument("Number of specified strings does not match number of defined columns.");
|
if (columns.size() != columnWidths.size()) {
|
||||||
|
throw invalid_argument("Number of specified strings does not match number of defined columns.");
|
||||||
|
}
|
||||||
|
|
||||||
// Some cells may span multiple lines.
|
// Some cells may span multiple lines.
|
||||||
// Create matrix of text lines in columns.
|
// Create matrix of text lines in columns.
|
||||||
|
@ -50,7 +52,7 @@ void TablePrinter::printRow(initializer_list<string> columns) const {
|
||||||
|
|
||||||
// Print lines
|
// Print lines
|
||||||
*stream << std::left;
|
*stream << std::left;
|
||||||
string spacer(columnSpacing, ' ');
|
const string spacer(columnSpacing, ' ');
|
||||||
for (size_t rowIndex = 0; rowIndex < lineCount; rowIndex++) {
|
for (size_t rowIndex = 0; rowIndex < lineCount; rowIndex++) {
|
||||||
for (size_t columnIndex = 0; columnIndex < columns.size(); columnIndex++) {
|
for (size_t columnIndex = 0; columnIndex < columns.size(); columnIndex++) {
|
||||||
if (columnIndex != 0) {
|
if (columnIndex != 0) {
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
|
|
||||||
class TablePrinter {
|
class TablePrinter {
|
||||||
public:
|
public:
|
||||||
TablePrinter(std::ostream* stream, std::initializer_list<int> columnWidths, int columnSpacing = 2);
|
TablePrinter(
|
||||||
|
std::ostream* stream,
|
||||||
|
std::initializer_list<int> columnWidths,
|
||||||
|
int columnSpacing = 2
|
||||||
|
);
|
||||||
void printRow(std::initializer_list<std::string> columns) const;
|
void printRow(std::initializer_list<std::string> columns) const;
|
||||||
private:
|
private:
|
||||||
std::ostream* const stream;
|
std::ostream* const stream;
|
||||||
|
|
|
@ -9,13 +9,16 @@ namespace details {
|
||||||
struct negation : std::integral_constant<bool, !B::value> {};
|
struct negation : std::integral_constant<bool, !B::value> {};
|
||||||
|
|
||||||
template<class> struct is_ref_wrapper : std::false_type {};
|
template<class> struct is_ref_wrapper : std::false_type {};
|
||||||
|
|
||||||
template<class T> struct is_ref_wrapper<std::reference_wrapper<T>> : std::true_type {};
|
template<class T> struct is_ref_wrapper<std::reference_wrapper<T>> : std::true_type {};
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
using not_ref_wrapper = negation<is_ref_wrapper<std::decay_t<T>>>;
|
using not_ref_wrapper = negation<is_ref_wrapper<std::decay_t<T>>>;
|
||||||
|
|
||||||
template<class...> struct conjunction : std::true_type { };
|
template<class...> struct conjunction : std::true_type { };
|
||||||
|
|
||||||
template<class B1> struct conjunction<B1> : B1 { };
|
template<class B1> struct conjunction<B1> : B1 { };
|
||||||
|
|
||||||
template<class B1, class... Bn>
|
template<class B1, class... Bn>
|
||||||
struct conjunction<B1, Bn...>
|
struct conjunction<B1, Bn...>
|
||||||
: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};
|
: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};
|
||||||
|
@ -24,6 +27,7 @@ namespace details {
|
||||||
constexpr bool conjunction_v = conjunction<B...>::value;
|
constexpr bool conjunction_v = conjunction<B...>::value;
|
||||||
|
|
||||||
template<class D, class...> struct return_type_helper { using type = D; };
|
template<class D, class...> struct return_type_helper { using type = D; };
|
||||||
|
|
||||||
template<class... Types>
|
template<class... Types>
|
||||||
struct return_type_helper<void, Types...> : std::common_type<Types...> {
|
struct return_type_helper<void, Types...> : std::common_type<Types...> {
|
||||||
static_assert(conjunction_v<not_ref_wrapper<Types>...>,
|
static_assert(conjunction_v<not_ref_wrapper<Types>...>,
|
||||||
|
|
|
@ -10,7 +10,8 @@ std::ifstream openFile(path filePath) {
|
||||||
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
||||||
file.open(filePath.c_str(), std::ios::binary);
|
file.open(filePath.c_str(), std::ios::binary);
|
||||||
|
|
||||||
// Read some dummy data so that we can throw a decent exception in case the file is missing, locked, etc.
|
// Read some dummy data so that we can throw a decent exception in case the file is missing,
|
||||||
|
// locked, etc.
|
||||||
file.seekg(0, std::ios_base::end);
|
file.seekg(0, std::ios_base::end);
|
||||||
if (file.tellg()) {
|
if (file.tellg()) {
|
||||||
file.seekg(0);
|
file.seekg(0);
|
||||||
|
@ -18,7 +19,7 @@ std::ifstream openFile(path filePath) {
|
||||||
file.seekg(0);
|
file.seekg(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::move(file);
|
return file;
|
||||||
} catch (const std::ifstream::failure&) {
|
} catch (const std::ifstream::failure&) {
|
||||||
// Error messages on stream exceptions are mostly useless.
|
// Error messages on stream exceptions are mostly useless.
|
||||||
throw std::runtime_error(errorNumberToString(errno));
|
throw std::runtime_error(errorNumberToString(errno));
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
template<typename TCollection>
|
template<typename TCollection>
|
||||||
std::vector<std::pair<typename TCollection::value_type, typename TCollection::value_type>> getPairs(const TCollection& collection) {
|
std::vector<std::pair<typename TCollection::value_type, typename TCollection::value_type>> getPairs(
|
||||||
|
const TCollection& collection
|
||||||
|
) {
|
||||||
using TElement = typename TCollection::value_type;
|
using TElement = typename TCollection::value_type;
|
||||||
using TPair = std::pair<TElement, TElement>;
|
using TPair = std::pair<TElement, TElement>;
|
||||||
using TIterator = typename TCollection::const_iterator;
|
using TIterator = typename TCollection::const_iterator;
|
||||||
|
|
|
@ -37,7 +37,7 @@ void runParallel(
|
||||||
elementFinished.wait(lock, [&] { return currentThreadCount == 0; });
|
elementFinished.wait(lock, [&] { return currentThreadCount == 0; });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Asyncronously run all elements
|
// Asynchronously run all elements
|
||||||
for (auto it = collection.begin(); it != collection.end(); ++it) {
|
for (auto it = collection.begin(); it != collection.end(); ++it) {
|
||||||
// This variable will later hold the future, but can be value-captured right now
|
// This variable will later hold the future, but can be value-captured right now
|
||||||
auto future = std::make_shared<future_type>();
|
auto future = std::make_shared<future_type>();
|
||||||
|
@ -66,7 +66,7 @@ void runParallel(
|
||||||
// Wait for threads to finish, if necessary
|
// Wait for threads to finish, if necessary
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(mutex);
|
std::unique_lock<std::mutex> lock(mutex);
|
||||||
int targetThreadCount = it == collection.end() ? 0 : maxThreadCount - 1;
|
const int targetThreadCount = it == collection.end() ? 0 : maxThreadCount - 1;
|
||||||
while (currentThreadCount > targetThreadCount) {
|
while (currentThreadCount > targetThreadCount) {
|
||||||
elementFinished.wait(lock);
|
elementFinished.wait(lock);
|
||||||
if (finishedElement.valid()) {
|
if (finishedElement.valid()) {
|
||||||
|
@ -86,7 +86,8 @@ void runParallel(
|
||||||
TCollection& collection,
|
TCollection& collection,
|
||||||
int maxThreadCount,
|
int maxThreadCount,
|
||||||
ProgressSink& progressSink,
|
ProgressSink& progressSink,
|
||||||
std::function<double(const typename TCollection::reference)> getElementProgressWeight = [](typename TCollection::reference) { return 1.0; })
|
std::function<double(typename TCollection::reference)> getElementProgressWeight =
|
||||||
|
[](typename TCollection::reference) { return 1.0; })
|
||||||
{
|
{
|
||||||
// Create a collection of wrapper functions that take care of progress handling
|
// Create a collection of wrapper functions that take care of progress handling
|
||||||
ProgressMerger progressMerger(progressSink);
|
ProgressMerger progressMerger(progressSink);
|
||||||
|
@ -101,7 +102,7 @@ void runParallel(
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int getProcessorCoreCount() {
|
inline int getProcessorCoreCount() {
|
||||||
int coreCount = std::thread::hardware_concurrency();
|
const int coreCount = std::thread::hardware_concurrency();
|
||||||
|
|
||||||
// If the number of cores cannot be determined, use a reasonable default
|
// If the number of cores cannot be determined, use a reasonable default
|
||||||
return coreCount != 0 ? coreCount : 4;
|
return coreCount != 0 ? coreCount : 4;
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
#include <io.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
using boost::filesystem::path;
|
using boost::filesystem::path;
|
||||||
|
@ -26,13 +24,14 @@ path getBinPath() {
|
||||||
static const path binPath = [] {
|
static const path binPath = [] {
|
||||||
try {
|
try {
|
||||||
// Determine path length
|
// Determine path length
|
||||||
int pathLength = wai_getExecutablePath(nullptr, 0, nullptr);
|
const int pathLength = wai_getExecutablePath(nullptr, 0, nullptr);
|
||||||
if (pathLength == -1) {
|
if (pathLength == -1) {
|
||||||
throw std::runtime_error("Error determining path length.");
|
throw std::runtime_error("Error determining path length.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get path
|
// Get path
|
||||||
// Note: According to documentation, pathLength does *not* include the trailing zero. Actually, it does.
|
// Note: According to documentation, pathLength does *not* include the trailing zero.
|
||||||
|
// Actually, it does.
|
||||||
// In case there are situations where it doesn't, we allocate one character more.
|
// In case there are situations where it doesn't, we allocate one character more.
|
||||||
std::vector<char> buffer(pathLength + 1);
|
std::vector<char> buffer(pathLength + 1);
|
||||||
if (wai_getExecutablePath(buffer.data(), buffer.size(), nullptr) == -1) {
|
if (wai_getExecutablePath(buffer.data(), buffer.size(), nullptr) == -1) {
|
||||||
|
@ -41,7 +40,7 @@ path getBinPath() {
|
||||||
buffer[pathLength] = 0;
|
buffer[pathLength] = 0;
|
||||||
|
|
||||||
// Convert to boost::filesystem::path
|
// Convert to boost::filesystem::path
|
||||||
string pathString(buffer.data());
|
const string pathString(buffer.data());
|
||||||
path result(boost::filesystem::canonical(pathString).make_preferred());
|
path result(boost::filesystem::canonical(pathString).make_preferred());
|
||||||
return result;
|
return result;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
|
@ -56,14 +55,14 @@ path getBinDirectory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
path getTempFilePath() {
|
path getTempFilePath() {
|
||||||
path tempDirectory = boost::filesystem::temp_directory_path();
|
const path tempDirectory = boost::filesystem::temp_directory_path();
|
||||||
static boost::uuids::random_generator generateUuid;
|
static boost::uuids::random_generator generateUuid;
|
||||||
string fileName = to_string(generateUuid());
|
const string fileName = to_string(generateUuid());
|
||||||
return tempDirectory / fileName;
|
return tempDirectory / fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tm getLocalTime(const time_t& time) {
|
std::tm getLocalTime(const time_t& time) {
|
||||||
tm timeInfo;
|
tm timeInfo {};
|
||||||
#if (__unix || __linux || __APPLE__)
|
#if (__unix || __linux || __APPLE__)
|
||||||
localtime_r(&time, &timeInfo);
|
localtime_r(&time, &timeInfo);
|
||||||
#else
|
#else
|
||||||
|
@ -92,7 +91,8 @@ vector<string> argsToUtf8(int argc, char* argv[]) {
|
||||||
// Get command-line arguments as UTF16 strings
|
// Get command-line arguments as UTF16 strings
|
||||||
int argumentCount;
|
int argumentCount;
|
||||||
static_assert(sizeof(wchar_t) == sizeof(char16_t), "Expected wchar_t to be a 16-bit type.");
|
static_assert(sizeof(wchar_t) == sizeof(char16_t), "Expected wchar_t to be a 16-bit type.");
|
||||||
char16_t** args = reinterpret_cast<char16_t**>(CommandLineToArgvW(GetCommandLineW(), &argumentCount));
|
char16_t** args =
|
||||||
|
reinterpret_cast<char16_t**>(CommandLineToArgvW(GetCommandLineW(), &argumentCount));
|
||||||
if (!args) {
|
if (!args) {
|
||||||
throw std::runtime_error("Error splitting the UTF-16 command line arguments.");
|
throw std::runtime_error("Error splitting the UTF-16 command line arguments.");
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ void useUtf8ForConsole() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void useUtf8ForBoostFilesystem() {
|
void useUtf8ForBoostFilesystem() {
|
||||||
std::locale globalLocale = std::locale();
|
const std::locale globalLocale = std::locale();
|
||||||
std::locale utf8Locale(globalLocale, new boost::filesystem::detail::utf8_codecvt_facet);
|
const std::locale utf8Locale(globalLocale, new boost::filesystem::detail::utf8_codecvt_facet);
|
||||||
path::imbue(utf8Locale);
|
path::imbue(utf8Locale);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,10 @@ ProgressSink& ProgressMerger::addSink(double weight) {
|
||||||
totalWeight += weight;
|
totalWeight += weight;
|
||||||
int sinkIndex = weightedValues.size();
|
int sinkIndex = weightedValues.size();
|
||||||
weightedValues.push_back(0);
|
weightedValues.push_back(0);
|
||||||
forwarders.push_back(ProgressForwarder([weight, sinkIndex, this](double progress) {
|
forwarders.emplace_back([weight, sinkIndex, this](double progress) {
|
||||||
weightedValues[sinkIndex] = progress * weight;
|
weightedValues[sinkIndex] = progress * weight;
|
||||||
report();
|
report();
|
||||||
}));
|
});
|
||||||
return forwarders.back();
|
return forwarders.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ void ProgressMerger::report() {
|
||||||
for (double weightedValue : weightedValues) {
|
for (double weightedValue : weightedValues) {
|
||||||
weightedSum += weightedValue;
|
weightedSum += weightedValue;
|
||||||
}
|
}
|
||||||
double progress = weightedSum / totalWeight;
|
const double progress = weightedSum / totalWeight;
|
||||||
sink.reportProgress(progress);
|
sink.reportProgress(progress);
|
||||||
} else {
|
} else {
|
||||||
sink.reportProgress(0);
|
sink.reportProgress(0);
|
||||||
|
|
|
@ -9,7 +9,6 @@ using std::string;
|
||||||
using std::wstring;
|
using std::wstring;
|
||||||
using std::u32string;
|
using std::u32string;
|
||||||
using std::vector;
|
using std::vector;
|
||||||
using boost::optional;
|
|
||||||
using std::regex;
|
using std::regex;
|
||||||
using std::regex_replace;
|
using std::regex_replace;
|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ vector<string> splitIntoLines(const string& s) {
|
||||||
vector<string> lines;
|
vector<string> lines;
|
||||||
auto p = &s[0];
|
auto p = &s[0];
|
||||||
auto lineBegin = p;
|
auto lineBegin = p;
|
||||||
auto end = p + s.size();
|
const auto end = p + s.size();
|
||||||
// Iterate over input string
|
// Iterate over input string
|
||||||
while (p <= end) {
|
while (p <= end) {
|
||||||
// Add a new result line when we hit a \n character or the end of the string
|
// Add a new result line when we hit a \n character or the end of the string
|
||||||
|
@ -45,7 +44,7 @@ vector<string> wrapSingleLineString(const string& s, int lineLength, int hanging
|
||||||
auto p = &s[0];
|
auto p = &s[0];
|
||||||
auto lineBegin = p;
|
auto lineBegin = p;
|
||||||
auto lineEnd = p;
|
auto lineEnd = p;
|
||||||
auto end = p + s.size();
|
const auto end = p + s.size();
|
||||||
// Iterate over input string
|
// Iterate over input string
|
||||||
while (p <= end) {
|
while (p <= end) {
|
||||||
// If we're at a word boundary: update lineEnd
|
// If we're at a word boundary: update lineEnd
|
||||||
|
@ -54,7 +53,7 @@ vector<string> wrapSingleLineString(const string& s, int lineLength, int hanging
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've hit lineLength or the end of the string: add a new result line
|
// If we've hit lineLength or the end of the string: add a new result line
|
||||||
int currentIndent = lines.empty() ? 0 : hangingIndent;
|
const int currentIndent = lines.empty() ? 0 : hangingIndent;
|
||||||
if (p == end || p - lineBegin == lineLength - currentIndent) {
|
if (p == end || p - lineBegin == lineLength - currentIndent) {
|
||||||
if (lineEnd == lineBegin) {
|
if (lineEnd == lineBegin) {
|
||||||
// The line contains a single word, which is too long. Split mid-word.
|
// The line contains a single word, which is too long. Split mid-word.
|
||||||
|
@ -80,7 +79,7 @@ vector<string> wrapSingleLineString(const string& s, int lineLength, int hanging
|
||||||
|
|
||||||
vector<string> wrapString(const string& s, int lineLength, int hangingIndent) {
|
vector<string> wrapString(const string& s, int lineLength, int hangingIndent) {
|
||||||
vector<string> lines;
|
vector<string> lines;
|
||||||
for (string paragraph : splitIntoLines(s)) {
|
for (const string& paragraph : splitIntoLines(s)) {
|
||||||
auto paragraphLines = wrapSingleLineString(paragraph, lineLength, hangingIndent);
|
auto paragraphLines = wrapSingleLineString(paragraph, lineLength, hangingIndent);
|
||||||
copy(paragraphLines.cbegin(), paragraphLines.cend(), back_inserter(lines));
|
copy(paragraphLines.cbegin(), paragraphLines.cend(), back_inserter(lines));
|
||||||
}
|
}
|
||||||
|
@ -100,7 +99,7 @@ wstring latin1ToWide(const string& s) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
string utf8ToAscii(const string s) {
|
string utf8ToAscii(const string& s) {
|
||||||
// Normalize string, simplifying it as much as possible
|
// Normalize string, simplifying it as much as possible
|
||||||
const NormalizationOptions options = NormalizationOptions::CompatibilityMode
|
const NormalizationOptions options = NormalizationOptions::CompatibilityMode
|
||||||
| NormalizationOptions::Decompose
|
| NormalizationOptions::Decompose
|
||||||
|
@ -137,7 +136,7 @@ string utf8ToAscii(const string s) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
string normalizeUnicode(const string s, NormalizationOptions options) {
|
string normalizeUnicode(const string& s, NormalizationOptions options) {
|
||||||
char* result;
|
char* result;
|
||||||
const utf8proc_ssize_t charCount = utf8proc_map(
|
const utf8proc_ssize_t charCount = utf8proc_map(
|
||||||
reinterpret_cast<const uint8_t*>(s.data()),
|
reinterpret_cast<const uint8_t*>(s.data()),
|
||||||
|
@ -177,7 +176,7 @@ string escapeJsonString(const string& s) {
|
||||||
case '\t': result += "\\t"; break;
|
case '\t': result += "\\t"; break;
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
bool needsEscaping = c < '\x20' || c >= 0x80;
|
const bool needsEscaping = c < '\x20' || c >= 0x80;
|
||||||
if (needsEscaping) {
|
if (needsEscaping) {
|
||||||
result += fmt::format("\\u{0:04x}", c);
|
result += fmt::format("\\u{0:04x}", c);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <boost/optional.hpp>
|
|
||||||
#include <boost/lexical_cast.hpp>
|
#include <boost/lexical_cast.hpp>
|
||||||
#include <utf8proc.h>
|
#include <utf8proc.h>
|
||||||
|
|
||||||
std::vector<std::string> splitIntoLines(const std::string& s);
|
std::vector<std::string> splitIntoLines(const std::string& s);
|
||||||
|
|
||||||
std::vector<std::string> wrapSingleLineString(const std::string& s, int lineLength, int hangingIndent = 0);
|
std::vector<std::string> wrapSingleLineString(
|
||||||
|
const std::string& s,
|
||||||
|
int lineLength,
|
||||||
|
int hangingIndent = 0
|
||||||
|
);
|
||||||
|
|
||||||
std::vector<std::string> wrapString(const std::string& s, int lineLength, int hangingIndent = 0);
|
std::vector<std::string> wrapString(const std::string& s, int lineLength, int hangingIndent = 0);
|
||||||
|
|
||||||
|
@ -15,9 +18,7 @@ bool isValidUtf8(const std::string& s);
|
||||||
|
|
||||||
std::wstring latin1ToWide(const std::string& s);
|
std::wstring latin1ToWide(const std::string& s);
|
||||||
|
|
||||||
boost::optional<char> toAscii(char32_t ch);
|
std::string utf8ToAscii(const std::string& s);
|
||||||
|
|
||||||
std::string utf8ToAscii(const std::string s);
|
|
||||||
|
|
||||||
enum class NormalizationOptions : int {
|
enum class NormalizationOptions : int {
|
||||||
CompatibilityMode = UTF8PROC_COMPAT,
|
CompatibilityMode = UTF8PROC_COMPAT,
|
||||||
|
@ -35,7 +36,7 @@ operator|(NormalizationOptions a, NormalizationOptions b) {
|
||||||
return static_cast<NormalizationOptions>(static_cast<int>(a) | static_cast<int>(b));
|
return static_cast<NormalizationOptions>(static_cast<int>(a) | static_cast<int>(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string normalizeUnicode(const std::string s, NormalizationOptions options);
|
std::string normalizeUnicode(const std::string& s, NormalizationOptions options);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::string join(T range, const std::string separator) {
|
std::string join(T range, const std::string separator) {
|
||||||
|
|
|
@ -18,8 +18,8 @@ template<unsigned int n, typename iterator_type>
|
||||||
void for_each_adjacent(
|
void for_each_adjacent(
|
||||||
iterator_type begin,
|
iterator_type begin,
|
||||||
iterator_type end,
|
iterator_type end,
|
||||||
std::function<void(const std::deque<std::reference_wrapper<const typename iterator_type::value_type>>&)> f)
|
std::function<void(const std::deque<std::reference_wrapper<const typename iterator_type::value_type>>&)> f
|
||||||
{
|
) {
|
||||||
// Get the first n values
|
// Get the first n values
|
||||||
iterator_type it = begin;
|
iterator_type it = begin;
|
||||||
using element_type = std::reference_wrapper<const typename iterator_type::value_type>;
|
using element_type = std::reference_wrapper<const typename iterator_type::value_type>;
|
||||||
|
@ -42,20 +42,28 @@ template<typename iterator_type>
|
||||||
void for_each_adjacent(
|
void for_each_adjacent(
|
||||||
iterator_type begin,
|
iterator_type begin,
|
||||||
iterator_type end,
|
iterator_type end,
|
||||||
std::function<void(const typename iterator_type::reference a, const typename iterator_type::reference b)> f)
|
std::function<void(const typename iterator_type::reference a, const typename iterator_type::reference b)> f
|
||||||
{
|
) {
|
||||||
for_each_adjacent<2>(begin, end, [&](const std::deque<std::reference_wrapper<const typename iterator_type::value_type>>& args) {
|
for_each_adjacent<2>(
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
[&](const std::deque<std::reference_wrapper<const typename iterator_type::value_type>>& args) {
|
||||||
f(args[0], args[1]);
|
f(args[0], args[1]);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename iterator_type>
|
template<typename iterator_type>
|
||||||
void for_each_adjacent(
|
void for_each_adjacent(
|
||||||
iterator_type begin,
|
iterator_type begin,
|
||||||
iterator_type end,
|
iterator_type end,
|
||||||
std::function<void(const typename iterator_type::reference a, const typename iterator_type::reference b, const typename iterator_type::reference c)> f)
|
std::function<void(const typename iterator_type::reference a, const typename iterator_type::reference b, const typename iterator_type::reference c)> f
|
||||||
{
|
) {
|
||||||
for_each_adjacent<3>(begin, end, [&](const std::deque<std::reference_wrapper<const typename iterator_type::value_type>>& args) {
|
for_each_adjacent<3>(
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
[&](const std::deque<std::reference_wrapper<const typename iterator_type::value_type>>& args) {
|
||||||
f(args[0], args[1], args[2]);
|
f(args[0], args[1], args[2]);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ using boost::optional;
|
||||||
using std::initializer_list;
|
using std::initializer_list;
|
||||||
|
|
||||||
TEST(BoundedTimeline, constructors_initializeState) {
|
TEST(BoundedTimeline, constructors_initializeState) {
|
||||||
TimeRange range(-5_cs, 55_cs);
|
const TimeRange range(-5_cs, 55_cs);
|
||||||
auto args = {
|
auto args = {
|
||||||
Timed<int>(-10_cs, 30_cs, 1),
|
Timed<int>(-10_cs, 30_cs, 1),
|
||||||
Timed<int>(10_cs, 40_cs, 2),
|
Timed<int>(10_cs, 40_cs, 2),
|
||||||
|
@ -52,7 +52,7 @@ TEST(BoundedTimeline, getRange) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(BoundedTimeline, setAndClear) {
|
TEST(BoundedTimeline, setAndClear) {
|
||||||
TimeRange range(0_cs, 10_cs);
|
const TimeRange range(0_cs, 10_cs);
|
||||||
BoundedTimeline<int> timeline(range);
|
BoundedTimeline<int> timeline(range);
|
||||||
|
|
||||||
// Out of range
|
// Out of range
|
||||||
|
@ -83,8 +83,14 @@ TEST(BoundedTimeline, setAndClear) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(BoundedTimeline, shift) {
|
TEST(BoundedTimeline, shift) {
|
||||||
BoundedTimeline<int> timeline(TimeRange(0_cs, 10_cs), { { 1_cs, 2_cs, 1 }, { 2_cs, 5_cs, 2 }, { 7_cs, 9_cs, 3 } });
|
BoundedTimeline<int> timeline(
|
||||||
BoundedTimeline<int> expected(TimeRange(2_cs, 12_cs), { { 3_cs, 4_cs, 1 }, { 4_cs, 7_cs, 2 }, { 9_cs, 11_cs, 3 } });
|
TimeRange(0_cs, 10_cs),
|
||||||
|
{ { 1_cs, 2_cs, 1 }, { 2_cs, 5_cs, 2 }, { 7_cs, 9_cs, 3 } }
|
||||||
|
);
|
||||||
|
BoundedTimeline<int> expected(
|
||||||
|
TimeRange(2_cs, 12_cs),
|
||||||
|
{ { 3_cs, 4_cs, 1 }, { 4_cs, 7_cs, 2 }, { 9_cs, 11_cs, 3 } }
|
||||||
|
);
|
||||||
timeline.shift(2_cs);
|
timeline.shift(2_cs);
|
||||||
EXPECT_EQ(expected, timeline);
|
EXPECT_EQ(expected, timeline);
|
||||||
}
|
}
|
||||||
|
@ -99,9 +105,11 @@ TEST(BoundedTimeline, equality) {
|
||||||
for (size_t i = 0; i < timelines.size(); ++i) {
|
for (size_t i = 0; i < timelines.size(); ++i) {
|
||||||
for (size_t j = 0; j < timelines.size(); ++j) {
|
for (size_t j = 0; j < timelines.size(); ++j) {
|
||||||
if (i == j) {
|
if (i == j) {
|
||||||
EXPECT_EQ(timelines[i], BoundedTimeline<int>(timelines[j])) << "i: " << i << ", j: " << j;
|
EXPECT_EQ(timelines[i], BoundedTimeline<int>(timelines[j]))
|
||||||
|
<< "i: " << i << ", j: " << j;
|
||||||
} else {
|
} else {
|
||||||
EXPECT_NE(timelines[i], timelines[j]) << "i: " << i << ", j: " << j;
|
EXPECT_NE(timelines[i], timelines[j])
|
||||||
|
<< "i: " << i << ", j: " << j;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ using boost::optional;
|
||||||
using std::initializer_list;
|
using std::initializer_list;
|
||||||
|
|
||||||
TEST(ContinuousTimeline, constructors_initializeState) {
|
TEST(ContinuousTimeline, constructors_initializeState) {
|
||||||
TimeRange range(-5_cs, 55_cs);
|
const TimeRange range(-5_cs, 55_cs);
|
||||||
int defaultValue = -1;
|
const int defaultValue = -1;
|
||||||
auto args = {
|
auto args = {
|
||||||
Timed<int>(-10_cs, 30_cs, 1),
|
Timed<int>(-10_cs, 30_cs, 1),
|
||||||
Timed<int>(10_cs, 40_cs, 2),
|
Timed<int>(10_cs, 40_cs, 2),
|
||||||
|
@ -49,8 +49,8 @@ TEST(ContinuousTimeline, empty) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ContinuousTimeline, setAndClear) {
|
TEST(ContinuousTimeline, setAndClear) {
|
||||||
TimeRange range(0_cs, 10_cs);
|
const TimeRange range(0_cs, 10_cs);
|
||||||
int defaultValue = -1;
|
const int defaultValue = -1;
|
||||||
ContinuousTimeline<int> timeline(range, defaultValue);
|
ContinuousTimeline<int> timeline(range, defaultValue);
|
||||||
|
|
||||||
// Out of range
|
// Out of range
|
||||||
|
@ -82,8 +82,16 @@ TEST(ContinuousTimeline, setAndClear) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ContinuousTimeline, shift) {
|
TEST(ContinuousTimeline, shift) {
|
||||||
ContinuousTimeline<int> timeline(TimeRange(0_cs, 10_cs), -1, { { 1_cs, 2_cs, 1 },{ 2_cs, 5_cs, 2 },{ 7_cs, 9_cs, 3 } });
|
ContinuousTimeline<int> timeline(
|
||||||
ContinuousTimeline<int> expected(TimeRange(2_cs, 12_cs), -1, { { 3_cs, 4_cs, 1 },{ 4_cs, 7_cs, 2 },{ 9_cs, 11_cs, 3 } });
|
TimeRange(0_cs, 10_cs),
|
||||||
|
-1,
|
||||||
|
{ { 1_cs, 2_cs, 1 }, { 2_cs, 5_cs, 2 }, { 7_cs, 9_cs, 3 } }
|
||||||
|
);
|
||||||
|
ContinuousTimeline<int> expected(
|
||||||
|
TimeRange(2_cs, 12_cs),
|
||||||
|
-1,
|
||||||
|
{ { 3_cs, 4_cs, 1 }, { 4_cs, 7_cs, 2 }, { 9_cs, 11_cs, 3 } }
|
||||||
|
);
|
||||||
timeline.shift(2_cs);
|
timeline.shift(2_cs);
|
||||||
EXPECT_EQ(expected, timeline);
|
EXPECT_EQ(expected, timeline);
|
||||||
}
|
}
|
||||||
|
@ -99,7 +107,8 @@ TEST(ContinuousTimeline, equality) {
|
||||||
for (size_t i = 0; i < timelines.size(); ++i) {
|
for (size_t i = 0; i < timelines.size(); ++i) {
|
||||||
for (size_t j = 0; j < timelines.size(); ++j) {
|
for (size_t j = 0; j < timelines.size(); ++j) {
|
||||||
if (i == j) {
|
if (i == j) {
|
||||||
EXPECT_EQ(timelines[i], ContinuousTimeline<int>(timelines[j])) << "i: " << i << ", j: " << j;
|
EXPECT_EQ(timelines[i], ContinuousTimeline<int>(timelines[j]))
|
||||||
|
<< "i: " << i << ", j: " << j;
|
||||||
} else {
|
} else {
|
||||||
EXPECT_NE(timelines[i], timelines[j]) << "i: " << i << ", j: " << j;
|
EXPECT_NE(timelines[i], timelines[j]) << "i: " << i << ", j: " << j;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
#include "tools/Lazy.h"
|
#include "tools/Lazy.h"
|
||||||
|
|
||||||
using namespace testing;
|
using namespace testing;
|
||||||
using std::make_unique;
|
|
||||||
|
|
||||||
// Not copyable, no default constrctor, movable
|
// Not copyable, no default constructor, movable
|
||||||
struct Foo {
|
struct Foo {
|
||||||
const int value;
|
const int value;
|
||||||
Foo(int value) : value(value) {}
|
Foo(int value) : value(value) {}
|
||||||
|
@ -44,7 +43,7 @@ TEST(Lazy, constUsage) {
|
||||||
TEST(Lazy, copying) {
|
TEST(Lazy, copying) {
|
||||||
Lazy<Foo> a;
|
Lazy<Foo> a;
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
auto createValue = [&] { return counter++; };
|
const auto createValue = [&] { return counter++; };
|
||||||
Lazy<Foo> b(createValue);
|
Lazy<Foo> b(createValue);
|
||||||
a = b;
|
a = b;
|
||||||
EXPECT_EQ(0, counter);
|
EXPECT_EQ(0, counter);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#include <gmock/gmock.h>
|
#include <gmock/gmock.h>
|
||||||
#include "time/Timeline.h"
|
#include "time/Timeline.h"
|
||||||
#include <limits>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
using namespace testing;
|
using namespace testing;
|
||||||
|
@ -103,17 +102,24 @@ TEST(Timeline, iterators) {
|
||||||
EXPECT_THAT(reversedActual, ElementsAreArray(reversedExpected));
|
EXPECT_THAT(reversedActual, ElementsAreArray(reversedExpected));
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFind(const Timeline<int>& timeline, FindMode findMode, const initializer_list<Timed<int>*> expectedResults) {
|
void testFind(
|
||||||
|
const Timeline<int>& timeline,
|
||||||
|
FindMode findMode,
|
||||||
|
const initializer_list<Timed<int>*> expectedResults
|
||||||
|
) {
|
||||||
int i = -1;
|
int i = -1;
|
||||||
for (Timed<int>* expectedResult : expectedResults) {
|
for (Timed<int>* expectedResult : expectedResults) {
|
||||||
auto it = timeline.find(centiseconds(++i), findMode);
|
auto it = timeline.find(centiseconds(++i), findMode);
|
||||||
if (expectedResult != nullptr) {
|
if (expectedResult != nullptr) {
|
||||||
EXPECT_NE(it, timeline.end()) << "Timeline: " << timeline << "; findMode: " << static_cast<int>(findMode) << "; i: " << i;
|
EXPECT_NE(it, timeline.end())
|
||||||
|
<< "Timeline: " << timeline << "; findMode: " << static_cast<int>(findMode) << "; i: " << i;
|
||||||
if (it != timeline.end()) {
|
if (it != timeline.end()) {
|
||||||
EXPECT_EQ(*expectedResult, *it) << "Timeline: " << timeline << "; findMode: " << static_cast<int>(findMode) << "; i: " << i;
|
EXPECT_EQ(*expectedResult, *it)
|
||||||
|
<< "Timeline: " << timeline << "; findMode: " << static_cast<int>(findMode) << "; i: " << i;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
EXPECT_EQ(timeline.end(), it) << "Timeline: " << timeline << "; findMode: " << static_cast<int>(findMode) << "; i: " << i;
|
EXPECT_EQ(timeline.end(), it)
|
||||||
|
<< "Timeline: " << timeline << "; findMode: " << static_cast<int>(findMode) << "; i: " << i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +128,7 @@ TEST(Timeline, find) {
|
||||||
Timed<int> a = Timed<int>(1_cs, 2_cs, 1);
|
Timed<int> a = Timed<int>(1_cs, 2_cs, 1);
|
||||||
Timed<int> b = Timed<int>(2_cs, 5_cs, 2);
|
Timed<int> b = Timed<int>(2_cs, 5_cs, 2);
|
||||||
Timed<int> c = Timed<int>(7_cs, 9_cs, 3);
|
Timed<int> c = Timed<int>(7_cs, 9_cs, 3);
|
||||||
Timeline<int> timeline{ a, b, c };
|
const Timeline<int> timeline { a, b, c };
|
||||||
|
|
||||||
testFind(timeline, FindMode::SampleLeft, { nullptr, nullptr, &a, &b, &b, &b, nullptr, nullptr, &c, &c, nullptr });
|
testFind(timeline, FindMode::SampleLeft, { nullptr, nullptr, &a, &b, &b, &b, nullptr, nullptr, &c, &c, nullptr });
|
||||||
testFind(timeline, FindMode::SampleRight, { nullptr, &a, &b, &b, &b, nullptr, nullptr, &c, &c, nullptr, nullptr });
|
testFind(timeline, FindMode::SampleRight, { nullptr, &a, &b, &b, &b, nullptr, nullptr, &c, &c, nullptr, nullptr });
|
||||||
|
@ -136,7 +142,8 @@ TEST(Timeline, get) {
|
||||||
Timed<int> c = Timed<int>(7_cs, 9_cs, 3);
|
Timed<int> c = Timed<int>(7_cs, 9_cs, 3);
|
||||||
Timeline<int> timeline { a, b, c };
|
Timeline<int> timeline { a, b, c };
|
||||||
|
|
||||||
initializer_list<Timed<int>*> expectedResults = { nullptr, &a, &b, &b, &b, nullptr, nullptr, &c, &c, nullptr, nullptr };
|
initializer_list<Timed<int>*> expectedResults =
|
||||||
|
{ nullptr, &a, &b, &b, &b, nullptr, nullptr, &c, &c, nullptr, nullptr };
|
||||||
int i = -1;
|
int i = -1;
|
||||||
for (Timed<int>* expectedResult : expectedResults) {
|
for (Timed<int>* expectedResult : expectedResults) {
|
||||||
optional<const Timed<int>&> value = timeline.get(centiseconds(++i));
|
optional<const Timed<int>&> value = timeline.get(centiseconds(++i));
|
||||||
|
@ -152,7 +159,7 @@ TEST(Timeline, get) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Timeline, clear) {
|
TEST(Timeline, clear) {
|
||||||
Timeline<int> original{ { 1_cs, 2_cs, 1 }, { 2_cs, 5_cs, 2 }, { 7_cs, 9_cs, 3 } };
|
const Timeline<int> original { { 1_cs, 2_cs, 1 }, { 2_cs, 5_cs, 2 }, { 7_cs, 9_cs, 3 } };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto timeline = original;
|
auto timeline = original;
|
||||||
|
@ -189,7 +196,7 @@ TEST(Timeline, clear) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void testSetter(std::function<void(const Timed<int>&, Timeline<int>&)> set) {
|
void testSetter(const std::function<void(const Timed<int>&, Timeline<int>&)>& set) {
|
||||||
Timeline<int> timeline;
|
Timeline<int> timeline;
|
||||||
vector<optional<int>> expectedValues(20, none);
|
vector<optional<int>> expectedValues(20, none);
|
||||||
auto newElements = {
|
auto newElements = {
|
||||||
|
@ -218,7 +225,7 @@ void testSetter(std::function<void(const Timed<int>&, Timeline<int>&)> set) {
|
||||||
set(newElement, timeline);
|
set(newElement, timeline);
|
||||||
|
|
||||||
// Update expected value for every index
|
// Update expected value for every index
|
||||||
centiseconds elementStart = max(newElement.getStart(), 0_cs);
|
const centiseconds elementStart = max(newElement.getStart(), 0_cs);
|
||||||
centiseconds elementEnd = newElement.getEnd();
|
centiseconds elementEnd = newElement.getEnd();
|
||||||
for (centiseconds t = elementStart; t < elementEnd; ++t) {
|
for (centiseconds t = elementStart; t < elementEnd; ++t) {
|
||||||
expectedValues[t.count()] = newElement.getValue();
|
expectedValues[t.count()] = newElement.getValue();
|
||||||
|
@ -232,13 +239,14 @@ void testSetter(std::function<void(const Timed<int>&, Timeline<int>&)> set) {
|
||||||
|
|
||||||
// Check timeline via iterators
|
// Check timeline via iterators
|
||||||
for (const auto& element : timeline) {
|
for (const auto& element : timeline) {
|
||||||
// No element shound have zero-length
|
// No element should have zero-length
|
||||||
EXPECT_LT(0_cs, element.getDuration());
|
EXPECT_LT(0_cs, element.getDuration());
|
||||||
|
|
||||||
// Element should match expected values
|
// Element should match expected values
|
||||||
for (centiseconds t = std::max(centiseconds::zero(), element.getStart()); t < element.getEnd(); ++t) {
|
for (centiseconds t = std::max(centiseconds::zero(), element.getStart()); t < element.getEnd(); ++t) {
|
||||||
optional<int> expectedValue = expectedValues[t.count()];
|
optional<int> expectedValue = expectedValues[t.count()];
|
||||||
EXPECT_TRUE(expectedValue) << "Index " << t.count() << " should not have a value, but is within element " << element << ". "
|
EXPECT_TRUE(expectedValue)
|
||||||
|
<< "Index " << t.count() << " should not have a value, but is within element " << element << ". "
|
||||||
<< "newElementIndex: " << newElementIndex;
|
<< "newElementIndex: " << newElementIndex;
|
||||||
if (expectedValue) {
|
if (expectedValue) {
|
||||||
EXPECT_EQ(*expectedValue, element.getValue());
|
EXPECT_EQ(*expectedValue, element.getValue());
|
||||||
|
|
|
@ -26,6 +26,7 @@ TEST(wordToPhones, basic) {
|
||||||
{ "weary", { Phone::W, Phone::IY, Phone::R, Phone::IY } }
|
{ "weary", { Phone::W, Phone::IY, Phone::R, Phone::IY } }
|
||||||
};
|
};
|
||||||
for (const auto& word : words) {
|
for (const auto& word : words) {
|
||||||
EXPECT_THAT(wordToPhones(word.first), ElementsAreArray(word.second)) << "Original word: '" << word.first << "'";
|
EXPECT_THAT(wordToPhones(word.first), ElementsAreArray(word.second))
|
||||||
|
<< "Original word: '" << word.first << "'";
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using namespace testing;
|
using namespace testing;
|
||||||
using std::vector;
|
using std::vector;
|
||||||
using std::initializer_list;
|
|
||||||
using std::pair;
|
using std::pair;
|
||||||
|
|
||||||
TEST(getPairs, emptyCollection) {
|
TEST(getPairs, emptyCollection) {
|
||||||
|
@ -16,18 +15,18 @@ TEST(getPairs, oneElementCollection) {
|
||||||
|
|
||||||
TEST(getPairs, validCollection) {
|
TEST(getPairs, validCollection) {
|
||||||
{
|
{
|
||||||
auto actual = getPairs(vector<int>{ 1, 2 });
|
const auto actual = getPairs(vector<int> { 1, 2 });
|
||||||
vector<pair<int, int>> expected{ {1, 2} };
|
const vector<pair<int, int>> expected { { 1, 2 } };
|
||||||
EXPECT_THAT(actual, ElementsAreArray(expected));
|
EXPECT_THAT(actual, ElementsAreArray(expected));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto actual = getPairs(vector<int>{ 1, 2, 3 });
|
const auto actual = getPairs(vector<int> { 1, 2, 3 });
|
||||||
vector<pair<int, int>> expected{ {1, 2}, {2, 3} };
|
const vector<pair<int, int>> expected { { 1, 2 }, { 2, 3 } };
|
||||||
EXPECT_THAT(actual, ElementsAreArray(expected));
|
EXPECT_THAT(actual, ElementsAreArray(expected));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto actual = getPairs(vector<int>{ 1, 2, 3, 4 });
|
const auto actual = getPairs(vector<int> { 1, 2, 3, 4 });
|
||||||
vector<pair<int, int>> expected{ {1, 2}, {2, 3}, {3, 4} };
|
const vector<pair<int, int>> expected { { 1, 2 }, { 2, 3 }, { 3, 4 } };
|
||||||
EXPECT_THAT(actual, ElementsAreArray(expected));
|
EXPECT_THAT(actual, ElementsAreArray(expected));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,8 @@ TEST(splitIntoLines, handlesEmptyElements) {
|
||||||
// wrapSingleLineString
|
// wrapSingleLineString
|
||||||
|
|
||||||
TEST(wrapSingleLineString, basic) {
|
TEST(wrapSingleLineString, basic) {
|
||||||
const char* lipsum = "Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.";
|
const char* lipsum =
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.";
|
||||||
EXPECT_THAT(wrapSingleLineString(lipsum, 30), ElementsAre("Lorem ipsum dolor sit amet,", "consectetur adipisici elit,", "sed eiusmod tempor incidunt ut", "labore et dolore magna aliqua."));
|
EXPECT_THAT(wrapSingleLineString(lipsum, 30), ElementsAre("Lorem ipsum dolor sit amet,", "consectetur adipisici elit,", "sed eiusmod tempor incidunt ut", "labore et dolore magna aliqua."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,8 +77,10 @@ TEST(wrapString, basic) {
|
||||||
// latin1ToWide
|
// latin1ToWide
|
||||||
|
|
||||||
TEST(latin1ToWide, basic) {
|
TEST(latin1ToWide, basic) {
|
||||||
string pangramLatin1 = "D\350s No\353l o\371 un z\351phyr ha\357 me v\352t de gla\347ons w\374rmiens, je d\356ne d'exquis r\364tis de boeuf au kir \340 l'a\377 d'\342ge m\373r & c\346tera!";
|
const string pangramLatin1 =
|
||||||
wstring pangramWide = L"Dès Noël où un zéphyr haï me vêt de glaçons würmiens, je dîne d'exquis rôtis de boeuf au kir à l'aÿ d'âge mûr & cætera!";
|
"D\350s No\353l o\371 un z\351phyr ha\357 me v\352t de gla\347ons w\374rmiens, je d\356ne d'exquis r\364tis de boeuf au kir \340 l'a\377 d'\342ge m\373r & c\346tera!";
|
||||||
|
wstring pangramWide =
|
||||||
|
L"Dès Noël où un zéphyr haï me vêt de glaçons würmiens, je dîne d'exquis rôtis de boeuf au kir à l'aÿ d'âge mûr & cætera!";
|
||||||
EXPECT_EQ(pangramWide, latin1ToWide(pangramLatin1));
|
EXPECT_EQ(pangramWide, latin1ToWide(pangramLatin1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,15 +40,22 @@ TEST(tokenizeText, numbers) {
|
||||||
|
|
||||||
TEST(tokenizeText, abbreviations) {
|
TEST(tokenizeText, abbreviations) {
|
||||||
EXPECT_THAT(
|
EXPECT_THAT(
|
||||||
tokenizeText("Prof. Foo lives on Dr. Dolittle Dr.", [](const string& word) { return word == "prof."; }),
|
tokenizeText(
|
||||||
|
"Prof. Foo lives on Dr. Dolittle Dr.",
|
||||||
|
[](const string& word) { return word == "prof."; }
|
||||||
|
),
|
||||||
ElementsAre("prof.", "foo", "lives", "on", "doctor", "dolittle", "drive")
|
ElementsAre("prof.", "foo", "lives", "on", "doctor", "dolittle", "drive")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(tokenizeText, apostrophes) {
|
TEST(tokenizeText, apostrophes) {
|
||||||
EXPECT_THAT(
|
EXPECT_THAT(
|
||||||
tokenizeText("'Tis said he'd wish'd for a 'bus 'cause he wouldn't walk.", [](const string& word) { return word == "wouldn't"; }),
|
tokenizeText(
|
||||||
ElementsAreArray(vector<string>{ "tis", "said", "he'd", "wish'd", "for", "a", "bus", "cause", "he", "wouldn't", "walk" })
|
"'Tis said he'd wish'd for a 'bus 'cause he wouldn't walk.",
|
||||||
|
[](const string& word) { return word == "wouldn't"; }
|
||||||
|
),
|
||||||
|
ElementsAreArray(
|
||||||
|
vector<string>{ "tis", "said", "he'd", "wish'd", "for", "a", "bus", "cause", "he", "wouldn't", "walk" })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +82,7 @@ TEST(tokenizeText, wordsUseLimitedCharacters) {
|
||||||
utf8::append(c, back_inserter(input));
|
utf8::append(c, back_inserter(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
regex legal("^[a-z']+$");
|
const regex legal("^[a-z']+$");
|
||||||
auto words = tokenizeText(input, returnTrue);
|
auto words = tokenizeText(input, returnTrue);
|
||||||
for (const string& word : words) {
|
for (const string& word : words) {
|
||||||
EXPECT_TRUE(std::regex_match(word, legal)) << word;
|
EXPECT_TRUE(std::regex_match(word, legal)) << word;
|
||||||
|
|
Loading…
Reference in New Issue