Auto-format code files

This commit is contained in:
Daniel Wolf 2024-12-09 08:25:51 +01:00
parent b365c4c1d5
commit 9d3782a08b
142 changed files with 2557 additions and 2220 deletions

26
.clang-format Normal file
View File

@ -0,0 +1,26 @@
# Config file for clang-format, a C/C++/... code formatter.
BasedOnStyle: Chromium
# TODO: Uncomment once clang-format 20 is out
# BreakBinaryOperations: RespectPrecedence
BreakConstructorInitializers: AfterColon
AccessModifierOffset: -4
AlignAfterOpenBracket: BlockIndent
AlignOperands: DontAlign
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: WithoutElse
BinPackArguments: false
BreakBeforeBinaryOperators: NonAssignment
BreakStringLiterals: false
ColumnLimit: 100
CompactNamespaces: true
IncludeBlocks: Regroup
IndentWidth: 4
InsertNewlineAtEOF: true
LineEnding: LF
PackConstructorInitializers: Never
SeparateDefinitionBlocks: Always
SortIncludes: CaseInsensitive
SpacesBeforeTrailingComments: 1

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# Config file for generic text editors.
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,ts,yaml,yml}]
indent_size = 2

4
.gersemirc Normal file
View File

@ -0,0 +1,4 @@
# Config file for gersemi, a CMake code formatter.
line_length: 100
warn_about_unknown_commands: false

7
.gitignore vendored
View File

@ -1,3 +1,8 @@
.vs/ .vs/
build/ .vscode/
*.user *.user
build/
venv/
__pycache__
.doit.db.*

11
.prettierrc.yml Normal file
View File

@ -0,0 +1,11 @@
# Config file for Prettier, a JavaScript/TypeScript code formatter.
tabWidth: 2
printWidth: 100
singleQuote: true
arrowParens: avoid
overrides:
- files: '*.jsx' # Adobe JSX, not React
options:
trailingComma: none

7
.ruff.toml Normal file
View File

@ -0,0 +1,7 @@
# Config file for Ruff, a Python code formatter.
line-length = 100
[format]
quote-style = "single"
skip-magic-trailing-comma = true

View File

@ -13,10 +13,7 @@ add_subdirectory("extras/MagixVegas")
add_subdirectory("extras/EsotericSoftwareSpine") add_subdirectory("extras/EsotericSoftwareSpine")
# Install misc. files # Install misc. files
install( install(FILES README.adoc LICENSE.md CHANGELOG.md DESTINATION .)
FILES README.adoc LICENSE.md CHANGELOG.md
DESTINATION .
)
# Configure CPack # Configure CPack
function(get_short_system_name variable) function(get_short_system_name variable)

117
dodo.py Normal file
View File

@ -0,0 +1,117 @@
"""Collection of tasks. Run using `doit <task>`."""
from pathlib import Path
import subprocess
from functools import cache
from gitignore_parser import parse_gitignore
from typing import Dict, Optional, List
from enum import Enum
root_dir = Path(__file__).parent
rhubarb_dir = root_dir / 'rhubarb'
def task_format():
"""Format source files"""
files_by_formatters = get_files_by_formatters()
for formatter, files in files_by_formatters.items():
yield {
'name': formatter.value,
'actions': [(format, [files, formatter])],
'file_dep': files,
}
def task_check_formatted():
"""Fails unless source files are formatted"""
files_by_formatters = get_files_by_formatters()
for formatter, files in files_by_formatters.items():
yield {
'basename': 'check-formatted',
'name': formatter.value,
'actions': [(format, [files, formatter], {'check_only': True})],
}
class Formatter(Enum):
"""A source code formatter."""
CLANG_FORMAT = 'clang-format'
GERSEMI = 'gersemi'
PRETTIER = 'prettier'
RUFF = 'ruff'
def format(files: List[Path], formatter: Formatter, *, check_only: bool = False):
match formatter:
case Formatter.CLANG_FORMAT:
subprocess.run(
['clang-format', '--dry-run' if check_only else '-i', '--Werror', *files],
check=True,
)
case Formatter.GERSEMI:
subprocess.run(['gersemi', '--check' if check_only else '-i', *files], check=True)
case Formatter.PRETTIER:
subprocess.run(
[
*['deno', 'run', '-A', 'npm:prettier@3.4.2'],
*['--check' if check_only else '--write', '--log-level', 'warn', *files],
],
check=True,
)
case Formatter.RUFF:
subprocess.run(
['ruff', '--quiet', 'format', *(['--check'] if check_only else []), *files],
check=True,
)
case _:
raise ValueError(f'Unknown formatter: {formatter}')
@cache
def get_files_by_formatters() -> Dict[Formatter, List[Path]]:
"""Returns a dict with all formattable code files grouped by formatter."""
is_gitignored = parse_gitignore(root_dir / '.gitignore')
def is_hidden(path: Path):
return path.name.startswith('.')
def is_third_party(path: Path):
return path.is_relative_to(rhubarb_dir / 'lib') or path.name == 'gradle'
result = {formatter: [] for formatter in Formatter}
def visit(dir: Path):
for path in dir.iterdir():
if is_gitignored(path) or is_hidden(path) or is_third_party(path):
continue
if path.is_file():
formatter = get_formatter(path)
if formatter is not None:
result[formatter].append(path)
else:
visit(path)
visit(root_dir)
return result
def get_formatter(path: Path) -> Optional[Formatter]:
"""Returns the formatter to use for the given code file, if any."""
match path.suffix.lower():
case '.c' | '.cpp' | '.h':
return Formatter.CLANG_FORMAT
case '.cmake':
return Formatter.GERSEMI
case _ if path.name.lower() == 'cmakelists.txt':
return Formatter.GERSEMI
case '.js' | '.jsx' | '.ts':
return Formatter.PRETTIER
case '.py':
return Formatter.RUFF

View File

@ -1,11 +1,5 @@
cmake_minimum_required(VERSION 3.2) cmake_minimum_required(VERSION 3.2)
set(afterEffectsFiles set(afterEffectsFiles "Rhubarb Lip Sync.jsx" "README.adoc")
"Rhubarb Lip Sync.jsx"
"README.adoc"
)
install( install(FILES ${afterEffectsFiles} DESTINATION "extras/AdobeAfterEffects")
FILES ${afterEffectsFiles}
DESTINATION "extras/AdobeAfterEffects"
)

View File

@ -1,4 +1,5 @@
(function polyfill() { // prettier-ignore
(function polyfill() {
// Polyfill for Object.assign // Polyfill for Object.assign
"function"!=typeof Object.assign&&(Object.assign=function(a,b){"use strict";if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var c=Object(a),d=1;d<arguments.length;d++){var e=arguments[d];if(null!=e)for(var f in e)Object.prototype.hasOwnProperty.call(e,f)&&(c[f]=e[f])}return c}); "function"!=typeof Object.assign&&(Object.assign=function(a,b){"use strict";if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var c=Object(a),d=1;d<arguments.length;d++){var e=arguments[d];if(null!=e)for(var f in e)Object.prototype.hasOwnProperty.call(e,f)&&(c[f]=e[f])}return c});
@ -34,7 +35,7 @@
// Polyfill for JSON // Polyfill for JSON
"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(a){return a<10?"0"+a:a}function this_value(){return this.valueOf()}function quote(a){return rx_escapable.lastIndex=0,rx_escapable.test(a)?'"'+a.replace(rx_escapable,function(a){var b=meta[a];return"string"==typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,h,g=gap,i=b[a];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(a)),"function"==typeof rep&&(i=rep.call(b,a,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,h=[],"[object Array]"===Object.prototype.toString.apply(i)){for(f=i.length,c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=0===h.length?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&"object"==typeof rep)for(f=rep.length,c=0;c<f;c+=1)"string"==typeof rep[c]&&(d=rep[c],(e=str(d,i))&&h.push(quote(d)+(gap?": ":":")+e));else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i))&&h.push(quote(d)+(gap?": ":":")+e);return e=0===h.length?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(a,b,c){var d;if(gap="",indent="","number"==typeof c)for(d=0;d<c;d+=1)indent+=" ";else"string"==typeof c&&(indent=c);if(rep=b,b&&"function"!=typeof b&&("object"!=typeof b||"number"!=typeof b.length))throw new Error("JSON.stringify");return str("",{"":a})}),"function"!=typeof JSON.parse&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&"object"==typeof e)for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),void 0!==d?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;if(text=String(text),rx_dangerous.lastIndex=0,rx_dangerous.test(text)&&(text=text.replace(rx_dangerous,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})),rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(); "object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(a){return a<10?"0"+a:a}function this_value(){return this.valueOf()}function quote(a){return rx_escapable.lastIndex=0,rx_escapable.test(a)?'"'+a.replace(rx_escapable,function(a){var b=meta[a];return"string"==typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,h,g=gap,i=b[a];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(a)),"function"==typeof rep&&(i=rep.call(b,a,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,h=[],"[object Array]"===Object.prototype.toString.apply(i)){for(f=i.length,c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=0===h.length?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&"object"==typeof rep)for(f=rep.length,c=0;c<f;c+=1)"string"==typeof rep[c]&&(d=rep[c],(e=str(d,i))&&h.push(quote(d)+(gap?": ":":")+e));else for(d in i)Object.prototype.hasOwnProperty.call(i,d)&&(e=str(d,i))&&h.push(quote(d)+(gap?": ":":")+e);return e=0===h.length?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(a,b,c){var d;if(gap="",indent="","number"==typeof c)for(d=0;d<c;d+=1)indent+=" ";else"string"==typeof c&&(indent=c);if(rep=b,b&&"function"!=typeof b&&("object"!=typeof b||"number"!=typeof b.length))throw new Error("JSON.stringify");return str("",{"":a})}),"function"!=typeof JSON.parse&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&"object"==typeof e)for(c in e)Object.prototype.hasOwnProperty.call(e,c)&&(d=walk(e,c),void 0!==d?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;if(text=String(text),rx_dangerous.lastIndex=0,rx_dangerous.test(text)&&(text=text.replace(rx_dangerous,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})),rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}();
})(); })()
function last(array) { function last(array) {
return array[array.length - 1]; return array[array.length - 1];
@ -42,8 +43,8 @@ function last(array) {
function createGuid() { function createGuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0; var r = (Math.random() * 16) | 0;
var v = c == 'x' ? r : (r & 0x3 | 0x8); var v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16); return v.toString(16);
}); });
} }
@ -119,12 +120,16 @@ function readTextFile(fileOrPath) {
if (file.error) throw new Error('Error reading file "' + filePath + '": ' + file.error); if (file.error) throw new Error('Error reading file "' + filePath + '": ' + file.error);
} }
try { try {
file.open('r'); check(); file.open('r');
file.encoding = 'UTF-8'; check(); check();
var result = file.read(); check(); file.encoding = 'UTF-8';
check();
var result = file.read();
check();
return result; return result;
} finally { } finally {
file.close(); check(); file.close();
check();
} }
} }
@ -135,11 +140,15 @@ function writeTextFile(fileOrPath, text) {
if (file.error) throw new Error('Error writing file "' + filePath + '": ' + file.error); if (file.error) throw new Error('Error writing file "' + filePath + '": ' + file.error);
} }
try { try {
file.open('w'); check(); file.open('w');
file.encoding = 'UTF-8'; check(); check();
file.write(text); check(); file.encoding = 'UTF-8';
check();
file.write(text);
check();
} finally { } finally {
file.close(); check(); file.close();
check();
} }
} }
@ -163,9 +172,7 @@ var osIsWindows = (system.osName || $.os).match(/windows/i);
// Depending on the operating system, the syntax for escaping command-line arguments differs. // Depending on the operating system, the syntax for escaping command-line arguments differs.
function cliEscape(argument) { function cliEscape(argument) {
return osIsWindows return osIsWindows ? '"' + argument + '"' : "'" + argument.replace(/'/g, "'\\''") + "'";
? '"' + argument + '"'
: "'" + argument.replace(/'/g, "'\\''") + "'";
} }
function exec(command) { function exec(command) {
@ -180,7 +187,8 @@ function execInWindow(command) {
// execute a command, then close the Terminal window. // execute a command, then close the Terminal window.
// If you know a better solution, let me know! // If you know a better solution, let me know!
var escapedCommand = command.replace(/"/g, '\\"'); var escapedCommand = command.replace(/"/g, '\\"');
var appleScript = '\ var appleScript =
'\
tell application "Terminal" \ tell application "Terminal" \
-- Quit terminal \ -- Quit terminal \
-- Yes, that\'s undesirable if there was an open window before. \ -- Yes, that\'s undesirable if there was an open window before. \
@ -189,7 +197,9 @@ function execInWindow(command) {
-- Open terminal \ -- Open terminal \
activate \ activate \
-- Run command in new tab \ -- Run command in new tab \
set newTab to do script ("' + escapedCommand + '") \ set newTab to do script ("' +
escapedCommand +
'") \
-- Wait until command is done \ -- Wait until command is done \
tell newTab \ tell newTab \
repeat while busy \ repeat while busy \
@ -223,10 +233,27 @@ function createResourceString(tree) {
var controlFunctions = (function () { var controlFunctions = (function () {
var controlTypes = [ var controlTypes = [
// Strangely, 'dialog' and 'palette' need to start with a lower-case character // Strangely, 'dialog' and 'palette' need to start with a lower-case character
['Dialog', 'dialog'], ['Palette', 'palette'], ['Dialog', 'dialog'],
'Panel', 'Group', 'TabbedPanel', 'Tab', 'Button', 'IconButton', 'Image', 'StaticText', ['Palette', 'palette'],
'EditText', 'Checkbox', 'RadioButton', 'Progressbar', 'Slider', 'Scrollbar', 'ListBox', 'Panel',
'DropDownList', 'TreeView', 'ListItem', 'FlashPlayer' 'Group',
'TabbedPanel',
'Tab',
'Button',
'IconButton',
'Image',
'StaticText',
'EditText',
'Checkbox',
'RadioButton',
'Progressbar',
'Slider',
'Scrollbar',
'ListBox',
'DropDownList',
'TreeView',
'ListItem',
'FlashPlayer'
]; ];
var result = {}; var result = {};
controlTypes.forEach(function (type) { controlTypes.forEach(function (type) {
@ -283,8 +310,9 @@ var basicMouthShapeNames = mouthShapeNames.slice(0, basicMouthShapeCount);
var extendedMouthShapeNames = mouthShapeNames.slice(basicMouthShapeCount); var extendedMouthShapeNames = mouthShapeNames.slice(basicMouthShapeCount);
function getMouthCompHelpTip() { function getMouthCompHelpTip() {
var result = 'A composition containing the mouth shapes, one drawing per frame. They must be ' var result =
+ 'arranged as follows:\n'; 'A composition containing the mouth shapes, one drawing per frame. They must be ' +
'arranged as follows:\n';
mouthShapeNames.forEach(function (mouthShapeName, i) { mouthShapeNames.forEach(function (mouthShapeName, i) {
var isOptional = i >= basicMouthShapeCount; var isOptional = i >= basicMouthShapeCount;
result += '\n00:' + pad(i, 2) + '\t' + mouthShapeName + (isOptional ? ' (optional)' : ''); result += '\n00:' + pad(i, 2) + '\t' + mouthShapeName + (isOptional ? ' (optional)' : '');
@ -320,9 +348,10 @@ function createDialogWindow() {
active: true active: true
}), }),
value: DropDownList({ value: DropDownList({
helpTip: 'An audio file containing recorded dialog.\n' helpTip:
+ 'This field shows all audio files that exist in ' 'An audio file containing recorded dialog.\n' +
+ 'your After Effects project.' 'This field shows all audio files that exist in ' +
'your After Effects project.'
}) })
}), }),
recognizer: Group({ recognizer: Group({
@ -337,8 +366,9 @@ function createDialogWindow() {
properties: { multiline: true }, properties: { multiline: true },
characters: 60, characters: 60,
minimumSize: [0, 100], minimumSize: [0, 100],
helpTip: 'For better animation results, you can specify the text of ' helpTip:
+ 'the recording here. This field is optional.' 'For better animation results, you can specify the text of ' +
'the recording here. This field is optional.'
}) })
}), }),
mouthComp: Group({ mouthComp: Group({
@ -354,8 +384,9 @@ function createDialogWindow() {
targetFolder: Group({ targetFolder: Group({
label: StaticText({ text: 'Target folder:' }), label: StaticText({ text: 'Target folder:' }),
value: DropDownList({ value: DropDownList({
helpTip: 'The project folder in which to create the animation ' helpTip:
+ 'composition. The composition will be named like the audio file.' 'The project folder in which to create the animation ' +
'composition. The composition will be named like the audio file.'
}) })
}), }),
frameRate: Group({ frameRate: Group({
@ -366,8 +397,9 @@ function createDialogWindow() {
}), }),
auto: Checkbox({ auto: Checkbox({
text: 'From mouth composition', text: 'From mouth composition',
helpTip: 'If checked, the animation will use the same frame rate as ' helpTip:
+ 'the mouth composition.' 'If checked, the animation will use the same frame rate as ' +
'the mouth composition.'
}) })
}) })
}), }),
@ -447,8 +479,9 @@ function createDialogWindow() {
selectByTextOrFirst(controls.recognizer, settings.recognizer); selectByTextOrFirst(controls.recognizer, settings.recognizer);
selectByTextOrFirst(controls.mouthComp, settings.mouthComp); selectByTextOrFirst(controls.mouthComp, settings.mouthComp);
extendedMouthShapeNames.forEach(function (shapeName) { extendedMouthShapeNames.forEach(function (shapeName) {
controls['mouthShape' + shapeName].value = controls['mouthShape' + shapeName].value = (settings.extendedMouthShapes || {})[
(settings.extendedMouthShapes || {})[shapeName.toLowerCase()]; shapeName.toLowerCase()
];
}); });
selectByTextOrFirst(controls.targetFolder, settings.targetFolder); selectByTextOrFirst(controls.targetFolder, settings.targetFolder);
controls.frameRate.text = settings.frameRate || ''; controls.frameRate.text = settings.frameRate || '';
@ -458,7 +491,9 @@ function createDialogWindow() {
window.onShow = function () { window.onShow = function () {
// Give uniform width to all labels // Give uniform width to all labels
var groups = toArray(window.settings.children); var groups = toArray(window.settings.children);
var labelWidths = groups.map(function(group) { return group.children[0].size.width; }); var labelWidths = groups.map(function (group) {
return group.children[0].size.width;
});
var maxLabelWidth = Math.max.apply(Math, labelWidths); var maxLabelWidth = Math.max.apply(Math, labelWidths);
groups.forEach(function (group) { groups.forEach(function (group) {
group.children[0].size.width = maxLabelWidth; group.children[0].size.width = maxLabelWidth;
@ -541,18 +576,24 @@ function createDialogWindow() {
var shapeName = mouthShapeNames[i]; var shapeName = mouthShapeNames[i];
var required = i < basicMouthShapeCount || controls['mouthShape' + shapeName].value; var required = i < basicMouthShapeCount || controls['mouthShape' + shapeName].value;
if (required && !isFrameVisible(comp, i)) { if (required && !isFrameVisible(comp, i)) {
return 'The mouth comp does not seem to contain an image for shape ' return (
+ shapeName + ' at frame ' + i + '.'; 'The mouth comp does not seem to contain an image for shape ' +
shapeName +
' at frame ' +
i +
'.'
);
} }
} }
if (!comp.preserveNestedFrameRate) { if (!comp.preserveNestedFrameRate) {
var fix = Window.confirm( var fix = Window.confirm(
'The setting "Preserve frame rate when nested or in render queue" is not active ' 'The setting "Preserve frame rate when nested or in render queue" is not active ' +
+ 'for the mouth composition. This can result in incorrect animation.\n\n' 'for the mouth composition. This can result in incorrect animation.\n\n' +
+ 'Activate this setting now?', 'Activate this setting now?',
false, false,
'Fix composition setting?'); 'Fix composition setting?'
);
if (fix) { if (fix) {
app.beginUndoGroup(appName + ': Mouth composition setting'); app.beginUndoGroup(appName + ': Mouth composition setting');
comp.preserveNestedFrameRate = true; comp.preserveNestedFrameRate = true;
@ -567,10 +608,14 @@ function createDialogWindow() {
var match = version.match(/Rhubarb Lip Sync version ((\d+)\.(\d+).(\d+)(-[0-9A-Za-z-.]+)?)/); var match = version.match(/Rhubarb Lip Sync version ((\d+)\.(\d+).(\d+)(-[0-9A-Za-z-.]+)?)/);
if (!match) { if (!match) {
var instructions = osIsWindows var instructions = osIsWindows
? 'Make sure your PATH environment variable contains the ' + appName + ' ' ? 'Make sure your PATH environment variable contains the ' +
+ 'application directory.' appName +
: 'Make sure you have created this file as a symbolic link to the ' + appName + ' ' ' ' +
+ 'executable (rhubarb).'; 'application directory.'
: 'Make sure you have created this file as a symbolic link to the ' +
appName +
' ' +
'executable (rhubarb).';
return 'Cannot find executable file "' + rhubarbPath + '". \n' + instructions; return 'Cannot find executable file "' + rhubarbPath + '". \n' + instructions;
} }
var versionString = match[1]; var versionString = match[1];
@ -579,15 +624,32 @@ function createDialogWindow() {
var requiredMajor = 1; var requiredMajor = 1;
var minRequiredMinor = 9; var minRequiredMinor = 9;
if (major != requiredMajor || minor < minRequiredMinor) { if (major != requiredMajor || minor < minRequiredMinor) {
return 'This script requires ' + appName + ' ' + requiredMajor + '.' + minRequiredMinor return (
+ '.0 or a later ' + requiredMajor + '.x version. ' 'This script requires ' +
+ 'Your installed version is ' + versionString + ', which is not compatible.'; appName +
' ' +
requiredMajor +
'.' +
minRequiredMinor +
'.0 or a later ' +
requiredMajor +
'.x version. ' +
'Your installed version is ' +
versionString +
', which is not compatible.'
);
} }
} }
function generateMouthCues(audioFileFootage, recognizer, dialogText, mouthComp, extendedMouthShapeNames, function generateMouthCues(
targetProjectFolder, frameRate) audioFileFootage,
{ recognizer,
dialogText,
mouthComp,
extendedMouthShapeNames,
targetProjectFolder,
frameRate
) {
var basePath = Folder.temp.fsName + '/' + createGuid(); var basePath = Folder.temp.fsName + '/' + createGuid();
var dialogFile = new File(basePath + '.txt'); var dialogFile = new File(basePath + '.txt');
var logFile = new File(basePath + '.log'); var logFile = new File(basePath + '.log');
@ -597,15 +659,16 @@ function createDialogWindow() {
writeTextFile(dialogFile, dialogText); writeTextFile(dialogFile, dialogText);
// Create command line // Create command line
var commandLine = rhubarbPath var commandLine =
+ ' --dialogFile ' + cliEscape(dialogFile.fsName) rhubarbPath +
+ ' --recognizer ' + recognizer (' --dialogFile ' + cliEscape(dialogFile.fsName)) +
+ ' --exportFormat json' (' --recognizer ' + recognizer) +
+ ' --extendedShapes ' + cliEscape(extendedMouthShapeNames.join('')) ' --exportFormat json' +
+ ' --logFile ' + cliEscape(logFile.fsName) (' --extendedShapes ' + cliEscape(extendedMouthShapeNames.join(''))) +
+ ' --logLevel fatal' (' --logFile ' + cliEscape(logFile.fsName)) +
+ ' --output ' + cliEscape(jsonFile.fsName) ' --logLevel fatal' +
+ ' ' + cliEscape(audioFileFootage.file.fsName); (' --output ' + cliEscape(jsonFile.fsName)) +
(' ' + cliEscape(audioFileFootage.file.fsName));
// Run Rhubarb // Run Rhubarb
execInWindow(commandLine); execInWindow(commandLine);
@ -635,9 +698,13 @@ function createDialogWindow() {
} }
} }
function animateMouthCues(mouthCues, audioFileFootage, mouthComp, targetProjectFolder, function animateMouthCues(
frameRate) mouthCues,
{ audioFileFootage,
mouthComp,
targetProjectFolder,
frameRate
) {
// Find an unconflicting comp name // Find an unconflicting comp name
// ... strip extension, if present // ... strip extension, if present
var baseName = audioFileFootage.name.match(/^(.*?)(\..*)?$/i)[1]; var baseName = audioFileFootage.name.match(/^(.*?)(\..*)?$/i)[1];
@ -645,14 +712,24 @@ function createDialogWindow() {
// ... add numeric suffix, if needed // ... add numeric suffix, if needed
var existingItems = toArrayBase1(targetProjectFolder.items); var existingItems = toArrayBase1(targetProjectFolder.items);
var counter = 1; var counter = 1;
while (existingItems.some(function(item) { return item.name === compName; })) { while (
existingItems.some(function (item) {
return item.name === compName;
})
) {
counter++; counter++;
compName = baseName + ' ' + counter; compName = baseName + ' ' + counter;
} }
// Create new comp // Create new comp
var comp = targetProjectFolder.items.addComp(compName, mouthComp.width, mouthComp.height, var comp = targetProjectFolder.items.addComp(
mouthComp.pixelAspect, audioFileFootage.duration, frameRate); compName,
mouthComp.width,
mouthComp.height,
mouthComp.pixelAspect,
audioFileFootage.duration,
frameRate
);
// Show new comp // Show new comp
comp.openInViewer(); comp.openInViewer();
@ -684,16 +761,28 @@ function createDialogWindow() {
} }
} }
function animate(audioFileFootage, recognizer, dialogText, mouthComp, extendedMouthShapeNames, function animate(
targetProjectFolder, frameRate) audioFileFootage,
{ recognizer,
dialogText,
mouthComp,
extendedMouthShapeNames,
targetProjectFolder,
frameRate
) {
try { try {
var mouthCues = generateMouthCues(audioFileFootage, recognizer, dialogText, mouthComp, var mouthCues = generateMouthCues(
extendedMouthShapeNames, targetProjectFolder, frameRate); audioFileFootage,
recognizer,
dialogText,
mouthComp,
extendedMouthShapeNames,
targetProjectFolder,
frameRate
);
app.beginUndoGroup(appName + ': Animation'); app.beginUndoGroup(appName + ': Animation');
animateMouthCues(mouthCues, audioFileFootage, mouthComp, targetProjectFolder, animateMouthCues(mouthCues, audioFileFootage, mouthComp, targetProjectFolder, frameRate);
frameRate);
app.endUndoGroup(); app.endUndoGroup();
} catch (e) { } catch (e) {
Window.alert(e.message, appName, true); Window.alert(e.message, appName, true);
@ -747,9 +836,12 @@ function createDialogWindow() {
function checkPreconditions() { function checkPreconditions() {
if (!canWriteFiles()) { if (!canWriteFiles()) {
Window.alert('This script requires file system access.\n\n' Window.alert(
+ 'Please enable Preferences > General > Allow Scripts to Write Files and Access Network.', 'This script requires file system access.\n\n' +
appName, true); 'Please enable Preferences > General > Allow Scripts to Write Files and Access Network.',
appName,
true
);
return false; return false;
} }
return true; return true;

View File

@ -1,18 +1,13 @@
cmake_minimum_required(VERSION 3.2) cmake_minimum_required(VERSION 3.2)
add_custom_target( add_custom_target(
rhubarbForSpine ALL rhubarbForSpine
ALL
"./gradlew" "build" "./gradlew" "build"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Building Rhubarb for Spine through Gradle." COMMENT "Building Rhubarb for Spine through Gradle."
) )
install( install(DIRECTORY "build/libs/" DESTINATION "extras/EsotericSoftwareSpine")
DIRECTORY "build/libs/"
DESTINATION "extras/EsotericSoftwareSpine"
)
install( install(FILES README.adoc DESTINATION "extras/EsotericSoftwareSpine")
FILES README.adoc
DESTINATION "extras/EsotericSoftwareSpine"
)

View File

@ -8,7 +8,4 @@ set(vegasFiles
"README.adoc" "README.adoc"
) )
install( install(FILES ${vegasFiles} DESTINATION "extras/MagixVegas")
FILES ${vegasFiles}
DESTINATION "extras/MagixVegas"
)

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
doit==0.36.0
clang-format==19.1.5
gersemi==0.17.1
gitignore_parser==0.1.11
ruff==0.8.3

View File

@ -59,30 +59,31 @@ include_directories(SYSTEM ${Boost_INCLUDE_DIRS})
link_libraries(${Boost_LIBRARIES}) # Just about every project needs Boost link_libraries(${Boost_LIBRARIES}) # Just about every project needs Boost
# ... C++ Format # ... C++ Format
FILE(GLOB cppFormatFiles "lib/cppformat/*.cc") file(GLOB cppFormatFiles "lib/cppformat/*.cc")
add_library(cppFormat ${cppFormatFiles}) add_library(cppFormat ${cppFormatFiles})
target_include_directories(cppFormat SYSTEM PUBLIC "lib/cppformat") target_include_directories(cppFormat SYSTEM PUBLIC "lib/cppformat")
target_compile_options(cppFormat PRIVATE ${disableWarningsFlags}) target_compile_options(cppFormat PRIVATE ${disableWarningsFlags})
set_target_properties(cppFormat PROPERTIES FOLDER lib) set_target_properties(cppFormat PROPERTIES FOLDER lib)
# ... sphinxbase # ... sphinxbase
FILE(GLOB_RECURSE sphinxbaseFiles "lib/sphinxbase-rev13216/src/libsphinxbase/*.c") file(GLOB_RECURSE sphinxbaseFiles "lib/sphinxbase-rev13216/src/libsphinxbase/*.c")
add_library(sphinxbase ${sphinxbaseFiles}) add_library(sphinxbase ${sphinxbaseFiles})
target_include_directories(sphinxbase SYSTEM PUBLIC target_include_directories(
"lib/sphinxbase-rev13216/include" sphinxbase
"lib/sphinxbase-rev13216/src" SYSTEM
"lib/sphinx_config" PUBLIC "lib/sphinxbase-rev13216/include" "lib/sphinxbase-rev13216/src" "lib/sphinx_config"
) )
target_compile_options(sphinxbase PRIVATE ${disableWarningsFlags}) target_compile_options(sphinxbase PRIVATE ${disableWarningsFlags})
target_compile_definitions(sphinxbase PUBLIC __SPHINXBASE_EXPORT_H__=1 SPHINXBASE_EXPORT=) # Compile as static lib target_compile_definitions(sphinxbase PUBLIC __SPHINXBASE_EXPORT_H__=1 SPHINXBASE_EXPORT=) # Compile as static lib
set_target_properties(sphinxbase PROPERTIES FOLDER lib) set_target_properties(sphinxbase PROPERTIES FOLDER lib)
# ... PocketSphinx # ... PocketSphinx
FILE(GLOB pocketSphinxFiles "lib/pocketsphinx-rev13216/src/libpocketsphinx/*.c") file(GLOB pocketSphinxFiles "lib/pocketsphinx-rev13216/src/libpocketsphinx/*.c")
add_library(pocketSphinx ${pocketSphinxFiles}) add_library(pocketSphinx ${pocketSphinxFiles})
target_include_directories(pocketSphinx SYSTEM PUBLIC target_include_directories(
"lib/pocketsphinx-rev13216/include" pocketSphinx
"lib/pocketsphinx-rev13216/src/libpocketsphinx" SYSTEM
PUBLIC "lib/pocketsphinx-rev13216/include" "lib/pocketsphinx-rev13216/src/libpocketsphinx"
) )
target_link_libraries(pocketSphinx sphinxbase) target_link_libraries(pocketSphinx sphinxbase)
target_compile_options(pocketSphinx PRIVATE ${disableWarningsFlags}) target_compile_options(pocketSphinx PRIVATE ${disableWarningsFlags})
@ -203,34 +204,26 @@ set(fliteFiles
lib/flite-1.4/src/utils/cst_val_user.c lib/flite-1.4/src/utils/cst_val_user.c
) )
add_library(flite ${fliteFiles}) add_library(flite ${fliteFiles})
target_include_directories(flite SYSTEM PUBLIC target_include_directories(flite SYSTEM PUBLIC "lib/flite-1.4/include" "lib/flite-1.4")
"lib/flite-1.4/include"
"lib/flite-1.4"
)
target_compile_options(flite PRIVATE ${disableWarningsFlags}) target_compile_options(flite PRIVATE ${disableWarningsFlags})
set_target_properties(flite PROPERTIES FOLDER lib) set_target_properties(flite PROPERTIES FOLDER lib)
# ... UTF8-CPP # ... UTF8-CPP
add_library(utfcpp add_library(utfcpp lib/header-only.c lib/utfcpp-2.3.5/source/utf8.h)
lib/header-only.c
lib/utfcpp-2.3.5/source/utf8.h
)
target_include_directories(utfcpp SYSTEM PUBLIC "lib/utfcpp-2.3.5/source") target_include_directories(utfcpp SYSTEM PUBLIC "lib/utfcpp-2.3.5/source")
target_compile_options(utfcpp PRIVATE ${disableWarningsFlags}) target_compile_options(utfcpp PRIVATE ${disableWarningsFlags})
set_target_properties(utfcpp PROPERTIES FOLDER lib) set_target_properties(utfcpp PROPERTIES FOLDER lib)
# ... utf8proc # ... utf8proc
add_library(utf8proc add_library(utf8proc lib/utf8proc-2.2.0/utf8proc.c lib/utf8proc-2.2.0/utf8proc.h)
lib/utf8proc-2.2.0/utf8proc.c
lib/utf8proc-2.2.0/utf8proc.h
)
target_include_directories(utf8proc SYSTEM PUBLIC "lib/utf8proc-2.2.0") target_include_directories(utf8proc SYSTEM PUBLIC "lib/utf8proc-2.2.0")
target_compile_options(utf8proc PRIVATE ${disableWarningsFlags}) target_compile_options(utf8proc PRIVATE ${disableWarningsFlags})
target_compile_definitions(utf8proc PUBLIC UTF8PROC_STATIC=1) # Compile as static lib target_compile_definitions(utf8proc PUBLIC UTF8PROC_STATIC=1) # Compile as static lib
set_target_properties(utf8proc PROPERTIES FOLDER lib) set_target_properties(utf8proc PROPERTIES FOLDER lib)
# ... Ogg # ... Ogg
add_library(ogg add_library(
ogg
lib/ogg-1.3.3/include/ogg/ogg.h lib/ogg-1.3.3/include/ogg/ogg.h
lib/ogg-1.3.3/src/bitwise.c lib/ogg-1.3.3/src/bitwise.c
lib/ogg-1.3.3/src/framing.c lib/ogg-1.3.3/src/framing.c
@ -240,7 +233,8 @@ target_compile_options(ogg PRIVATE ${disableWarningsFlags})
set_target_properties(ogg PROPERTIES FOLDER lib) set_target_properties(ogg PROPERTIES FOLDER lib)
# ... Vorbis # ... Vorbis
add_library(vorbis add_library(
vorbis
lib/vorbis-1.3.6/include/vorbis/vorbisfile.h lib/vorbis-1.3.6/include/vorbis/vorbisfile.h
lib/vorbis-1.3.6/lib/bitrate.c lib/vorbis-1.3.6/lib/bitrate.c
lib/vorbis-1.3.6/lib/block.c lib/vorbis-1.3.6/lib/block.c
@ -263,9 +257,7 @@ add_library(vorbis
lib/vorbis-1.3.6/lib/window.c lib/vorbis-1.3.6/lib/window.c
) )
target_include_directories(vorbis SYSTEM PUBLIC "lib/vorbis-1.3.6/include") target_include_directories(vorbis SYSTEM PUBLIC "lib/vorbis-1.3.6/include")
target_link_libraries(vorbis target_link_libraries(vorbis ogg)
ogg
)
target_compile_options(vorbis PRIVATE ${disableWarningsFlags}) target_compile_options(vorbis PRIVATE ${disableWarningsFlags})
set_target_properties(vorbis PROPERTIES FOLDER lib) set_target_properties(vorbis PROPERTIES FOLDER lib)
@ -274,7 +266,8 @@ set_target_properties(vorbis PROPERTIES FOLDER lib)
include_directories("src") include_directories("src")
# ... rhubarb-animation # ... rhubarb-animation
add_library(rhubarb-animation add_library(
rhubarb-animation
src/animation/animationRules.cpp src/animation/animationRules.cpp
src/animation/animationRules.h src/animation/animationRules.h
src/animation/mouthAnimation.cpp src/animation/mouthAnimation.cpp
@ -296,14 +289,11 @@ add_library(rhubarb-animation
src/animation/tweening.h src/animation/tweening.h
) )
target_include_directories(rhubarb-animation PRIVATE "src/animation") target_include_directories(rhubarb-animation PRIVATE "src/animation")
target_link_libraries(rhubarb-animation target_link_libraries(rhubarb-animation rhubarb-core rhubarb-logging rhubarb-time)
rhubarb-core
rhubarb-logging
rhubarb-time
)
# ... rhubarb-audio # ... rhubarb-audio
add_library(rhubarb-audio add_library(
rhubarb-audio
src/audio/AudioClip.cpp src/audio/AudioClip.cpp
src/audio/AudioClip.h src/audio/AudioClip.h
src/audio/audioFileReading.cpp src/audio/audioFileReading.cpp
@ -327,7 +317,8 @@ add_library(rhubarb-audio
src/audio/waveFileWriting.h src/audio/waveFileWriting.h
) )
target_include_directories(rhubarb-audio PRIVATE "src/audio") target_include_directories(rhubarb-audio PRIVATE "src/audio")
target_link_libraries(rhubarb-audio target_link_libraries(
rhubarb-audio
webRtc webRtc
vorbis vorbis
rhubarb-logging rhubarb-logging
@ -337,7 +328,8 @@ target_link_libraries(rhubarb-audio
# ... rhubarb-core # ... rhubarb-core
configure_file(src/core/appInfo.cpp.in appInfo.cpp ESCAPE_QUOTES) configure_file(src/core/appInfo.cpp.in appInfo.cpp ESCAPE_QUOTES)
add_library(rhubarb-core add_library(
rhubarb-core
${CMAKE_CURRENT_BINARY_DIR}/appInfo.cpp ${CMAKE_CURRENT_BINARY_DIR}/appInfo.cpp
src/core/appInfo.h src/core/appInfo.h
src/core/Phone.cpp src/core/Phone.cpp
@ -346,12 +338,11 @@ add_library(rhubarb-core
src/core/Shape.h src/core/Shape.h
) )
target_include_directories(rhubarb-core PRIVATE "src/core") target_include_directories(rhubarb-core PRIVATE "src/core")
target_link_libraries(rhubarb-core target_link_libraries(rhubarb-core rhubarb-tools)
rhubarb-tools
)
# ... rhubarb-exporters # ... rhubarb-exporters
add_library(rhubarb-exporters add_library(
rhubarb-exporters
src/exporters/DatExporter.cpp src/exporters/DatExporter.cpp
src/exporters/DatExporter.h src/exporters/DatExporter.h
src/exporters/Exporter.h src/exporters/Exporter.h
@ -365,19 +356,13 @@ add_library(rhubarb-exporters
src/exporters/XmlExporter.h src/exporters/XmlExporter.h
) )
target_include_directories(rhubarb-exporters PRIVATE "src/exporters") target_include_directories(rhubarb-exporters PRIVATE "src/exporters")
target_link_libraries(rhubarb-exporters target_link_libraries(rhubarb-exporters rhubarb-animation rhubarb-core rhubarb-time)
rhubarb-animation
rhubarb-core
rhubarb-time
)
# ... rhubarb-lib # ... rhubarb-lib
add_library(rhubarb-lib add_library(rhubarb-lib src/lib/rhubarbLib.cpp src/lib/rhubarbLib.h)
src/lib/rhubarbLib.cpp
src/lib/rhubarbLib.h
)
target_include_directories(rhubarb-lib PRIVATE "src/lib") target_include_directories(rhubarb-lib PRIVATE "src/lib")
target_link_libraries(rhubarb-lib target_link_libraries(
rhubarb-lib
rhubarb-animation rhubarb-animation
rhubarb-audio rhubarb-audio
rhubarb-core rhubarb-core
@ -387,7 +372,8 @@ target_link_libraries(rhubarb-lib
) )
# ... rhubarb-logging # ... rhubarb-logging
add_library(rhubarb-logging add_library(
rhubarb-logging
src/logging/Entry.cpp src/logging/Entry.cpp
src/logging/Entry.h src/logging/Entry.h
src/logging/Formatter.h src/logging/Formatter.h
@ -402,12 +388,11 @@ add_library(rhubarb-logging
src/logging/sinks.h src/logging/sinks.h
) )
target_include_directories(rhubarb-logging PRIVATE "src/logging") target_include_directories(rhubarb-logging PRIVATE "src/logging")
target_link_libraries(rhubarb-logging target_link_libraries(rhubarb-logging rhubarb-tools)
rhubarb-tools
)
# ... rhubarb-recognition # ... rhubarb-recognition
add_library(rhubarb-recognition add_library(
rhubarb-recognition
src/recognition/g2p.cpp src/recognition/g2p.cpp
src/recognition/g2p.h src/recognition/g2p.h
src/recognition/languageModels.cpp src/recognition/languageModels.cpp
@ -423,7 +408,8 @@ add_library(rhubarb-recognition
src/recognition/tokenization.h src/recognition/tokenization.h
) )
target_include_directories(rhubarb-recognition PRIVATE "src/recognition") target_include_directories(rhubarb-recognition PRIVATE "src/recognition")
target_link_libraries(rhubarb-recognition target_link_libraries(
rhubarb-recognition
flite flite
pocketSphinx pocketSphinx
rhubarb-audio rhubarb-audio
@ -432,7 +418,8 @@ target_link_libraries(rhubarb-recognition
) )
# ... rhubarb-time # ... rhubarb-time
add_library(rhubarb-time add_library(
rhubarb-time
src/time/BoundedTimeline.h src/time/BoundedTimeline.h
src/time/centiseconds.cpp src/time/centiseconds.cpp
src/time/centiseconds.h src/time/centiseconds.h
@ -444,13 +431,11 @@ add_library(rhubarb-time
src/time/TimeRange.h src/time/TimeRange.h
) )
target_include_directories(rhubarb-time PRIVATE "src/time") target_include_directories(rhubarb-time PRIVATE "src/time")
target_link_libraries(rhubarb-time target_link_libraries(rhubarb-time cppFormat rhubarb-logging)
cppFormat
rhubarb-logging
)
# ... rhubarb-tools # ... rhubarb-tools
add_library(rhubarb-tools add_library(
rhubarb-tools
src/tools/array.h src/tools/array.h
src/tools/EnumConverter.h src/tools/EnumConverter.h
src/tools/exceptions.cpp src/tools/exceptions.cpp
@ -481,15 +466,11 @@ add_library(rhubarb-tools
src/tools/tupleHash.h src/tools/tupleHash.h
) )
target_include_directories(rhubarb-tools PRIVATE "src/tools") target_include_directories(rhubarb-tools PRIVATE "src/tools")
target_link_libraries(rhubarb-tools target_link_libraries(rhubarb-tools cppFormat whereami utfcpp utf8proc)
cppFormat
whereami
utfcpp
utf8proc
)
# Define Rhubarb executable # Define Rhubarb executable
add_executable(rhubarb add_executable(
rhubarb
src/rhubarb/main.cpp src/rhubarb/main.cpp
src/rhubarb/ExportFormat.cpp src/rhubarb/ExportFormat.cpp
src/rhubarb/ExportFormat.h src/rhubarb/ExportFormat.h
@ -501,10 +482,7 @@ add_executable(rhubarb
src/rhubarb/sinks.h src/rhubarb/sinks.h
) )
target_include_directories(rhubarb PUBLIC "src/rhubarb") target_include_directories(rhubarb PUBLIC "src/rhubarb")
target_link_libraries(rhubarb target_link_libraries(rhubarb rhubarb-exporters rhubarb-lib)
rhubarb-exporters
rhubarb-lib
)
target_compile_options(rhubarb PUBLIC ${enableWarningsFlags}) target_compile_options(rhubarb PUBLIC ${enableWarningsFlags})
# Define test project # Define test project
@ -521,7 +499,8 @@ set(TEST_FILES
tests/WaveFileReaderTests.cpp tests/WaveFileReaderTests.cpp
) )
add_executable(runTests ${TEST_FILES}) add_executable(runTests ${TEST_FILES})
target_link_libraries(runTests target_link_libraries(
runTests
gtest gtest
gmock gmock
gmock_main gmock_main
@ -541,16 +520,17 @@ function(copy_and_install sourceGlob relativeTargetDirectory)
get_filename_component(fileName "${sourcePath}" NAME) get_filename_component(fileName "${sourcePath}" NAME)
# Copy file during build # Copy file during build
add_custom_command(TARGET rhubarb POST_BUILD add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy "${sourcePath}" "$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}" TARGET rhubarb
POST_BUILD
COMMAND
${CMAKE_COMMAND} -E copy "${sourcePath}"
"$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}"
COMMENT "Creating '${relativeTargetDirectory}/${fileName}'" COMMENT "Creating '${relativeTargetDirectory}/${fileName}'"
) )
# Install file # Install file
install( install(FILES "${sourcePath}" DESTINATION "${relativeTargetDirectory}")
FILES "${sourcePath}"
DESTINATION "${relativeTargetDirectory}"
)
endif() endif()
endforeach() endforeach()
endfunction() endfunction()
@ -566,8 +546,12 @@ function(copy sourceGlob relativeTargetDirectory)
get_filename_component(fileName "${sourcePath}" NAME) get_filename_component(fileName "${sourcePath}" NAME)
# Copy file during build # Copy file during build
add_custom_command(TARGET rhubarb POST_BUILD add_custom_command(
COMMAND ${CMAKE_COMMAND} -E copy "${sourcePath}" "$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}" TARGET rhubarb
POST_BUILD
COMMAND
${CMAKE_COMMAND} -E copy "${sourcePath}"
"$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}"
COMMENT "Creating '${relativeTargetDirectory}/${fileName}'" COMMENT "Creating '${relativeTargetDirectory}/${fileName}'"
) )
endif() endif()
@ -579,8 +563,4 @@ copy_and_install("lib/cmusphinx-en-us-5.2/*" "res/sphinx/acoustic-model")
copy_and_install("tests/resources/*" "tests/resources") copy_and_install("tests/resources/*" "tests/resources")
install( install(TARGETS rhubarb RUNTIME DESTINATION .)
TARGETS rhubarb
RUNTIME
DESTINATION .
)

View File

@ -1,6 +1,8 @@
#include "ShapeRule.h" #include "ShapeRule.h"
#include <boost/range/adaptor/transformed.hpp> #include <boost/range/adaptor/transformed.hpp>
#include <utility> #include <utility>
#include "time/ContinuousTimeline.h" #include "time/ContinuousTimeline.h"
using boost::optional; using boost::optional;
@ -19,15 +21,10 @@ ContinuousTimeline<optional<T>, AutoJoin> boundedTimelinetoContinuousOptional(
}; };
} }
ShapeRule::ShapeRule( ShapeRule::ShapeRule(ShapeSet shapeSet, optional<Phone> phone, TimeRange phoneTiming) :
ShapeSet shapeSet,
optional<Phone> phone,
TimeRange phoneTiming
) :
shapeSet(std::move(shapeSet)), shapeSet(std::move(shapeSet)),
phone(std::move(phone)), phone(std::move(phone)),
phoneTiming(phoneTiming) phoneTiming(phoneTiming) {}
{}
ShapeRule ShapeRule::getInvalid() { ShapeRule ShapeRule::getInvalid() {
return {{}, boost::none, {0_cs, 0_cs}}; return {{}, boost::none, {0_cs, 0_cs}};
@ -42,8 +39,7 @@ bool ShapeRule::operator!=(const ShapeRule& rhs) const {
} }
bool ShapeRule::operator<(const ShapeRule& rhs) const { bool ShapeRule::operator<(const ShapeRule& rhs) const {
return shapeSet < rhs.shapeSet return shapeSet < rhs.shapeSet || phone < rhs.phone
|| phone < rhs.phone
|| phoneTiming.getStart() < rhs.phoneTiming.getStart() || phoneTiming.getStart() < rhs.phoneTiming.getStart()
|| phoneTiming.getEnd() < rhs.phoneTiming.getEnd(); || phoneTiming.getEnd() < rhs.phoneTiming.getEnd();
} }
@ -54,8 +50,7 @@ ContinuousTimeline<ShapeRule> getShapeRules(const BoundedTimeline<Phone>& phones
// Create timeline of shape rules // Create timeline of shape rules
ContinuousTimeline<ShapeRule> shapeRules( ContinuousTimeline<ShapeRule> shapeRules(
phones.getRange(), phones.getRange(), {{Shape::X}, boost::none, {0_cs, 0_cs}}
{ { 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) {

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "core/Phone.h"
#include "animationRules.h" #include "animationRules.h"
#include "core/Phone.h"
#include "time/BoundedTimeline.h" #include "time/BoundedTimeline.h"
#include "time/ContinuousTimeline.h" #include "time/ContinuousTimeline.h"
#include "time/TimeRange.h" #include "time/TimeRange.h"

View File

@ -1,15 +1,17 @@
#include "animationRules.h" #include "animationRules.h"
#include <boost/algorithm/clamp.hpp>
#include "shapeShorthands.h"
#include "tools/array.h"
#include "time/ContinuousTimeline.h"
using std::chrono::duration_cast; #include <boost/algorithm/clamp.hpp>
using boost::algorithm::clamp;
#include "shapeShorthands.h"
#include "time/ContinuousTimeline.h"
#include "tools/array.h"
using boost::optional; using boost::optional;
using boost::algorithm::clamp;
using std::array; using std::array;
using std::pair;
using std::map; using std::map;
using std::pair;
using std::chrono::duration_cast;
constexpr size_t shapeValueCount = static_cast<size_t>(Shape::EndSentinel); constexpr size_t shapeValueCount = static_cast<size_t>(Shape::EndSentinel);
@ -32,7 +34,8 @@ Shape getClosestShape(Shape reference, ShapeSet shapes) {
// A matrix that for each shape contains all shapes in ascending order of effort required to // A matrix that for each shape contains all shapes in ascending order of effort required to
// move to them // 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),
/* C */ make_array(C, H, B, G, D, A, X, E, F), /* C */ make_array(C, H, B, G, D, A, X, E, F),
@ -63,9 +66,11 @@ optional<pair<Shape, TweenTiming>> getTween(Shape first, Shape second) {
{{D, B}, {C, TweenTiming::Centered}}, {{D, B}, {C, TweenTiming::Centered}},
{{D, G}, {C, TweenTiming::Early}}, {{D, G}, {C, TweenTiming::Early}},
{{D, X}, {C, TweenTiming::Late}}, {{D, X}, {C, TweenTiming::Late}},
{ { C, F }, { E, TweenTiming::Centered } }, { { F, C }, { E, TweenTiming::Centered } }, {{C, F}, {E, TweenTiming::Centered}},
{{F, C}, {E, TweenTiming::Centered}},
{{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}}
}; };
const 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>>();
@ -80,10 +85,7 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
// Returns a timeline with two shape sets, timed as a diphthong // Returns a timeline with two shape sets, timed as a diphthong
const auto diphthong = [duration](ShapeSet first, ShapeSet second) { const auto diphthong = [duration](ShapeSet first, ShapeSet second) {
const 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}, {firstDuration, duration, second}};
{ 0_cs, firstDuration, first },
{ firstDuration, duration, second }
};
}; };
// Returns a timeline with two shape sets, timed as a plosive // Returns a timeline with two shape sets, timed as a plosive
@ -92,10 +94,7 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
const centiseconds maxOcclusionDuration = 12_cs; const centiseconds maxOcclusionDuration = 12_cs;
const centiseconds occlusionDuration = const centiseconds occlusionDuration =
clamp(previousDuration / 2, minOcclusionDuration, maxOcclusionDuration); clamp(previousDuration / 2, minOcclusionDuration, maxOcclusionDuration);
return Timeline<ShapeSet> { return Timeline<ShapeSet>{{-occlusionDuration, 0_cs, first}, {0_cs, duration, second}};
{ -occlusionDuration, 0_cs, first },
{ 0_cs, duration, second }
};
}; };
// Returns the result of `getShapeSets` when called with identical arguments // Returns the result of `getShapeSets` when called with identical arguments

View File

@ -1,9 +1,10 @@
#pragma once #pragma once
#include <set> #include <set>
#include "core/Phone.h"
#include "core/Shape.h" #include "core/Shape.h"
#include "time/Timeline.h" #include "time/Timeline.h"
#include "core/Phone.h"
// Returns the basic shape (A-F) that most closely resembles the specified shape. // Returns the basic shape (A-F) that most closely resembles the specified shape.
Shape getBasicShape(Shape shape); Shape getBasicShape(Shape shape);

View File

@ -1,16 +1,16 @@
#include "mouthAnimation.h" #include "mouthAnimation.h"
#include "time/timedLogging.h"
#include "ShapeRule.h"
#include "roughAnimation.h"
#include "pauseAnimation.h" #include "pauseAnimation.h"
#include "tweening.h" #include "roughAnimation.h"
#include "timingOptimization.h" #include "ShapeRule.h"
#include "targetShapeSet.h"
#include "staticSegments.h" #include "staticSegments.h"
#include "targetShapeSet.h"
#include "time/timedLogging.h"
#include "timingOptimization.h"
#include "tweening.h"
JoiningContinuousTimeline<Shape> animate( JoiningContinuousTimeline<Shape> animate(
const BoundedTimeline<Phone>& phones, const BoundedTimeline<Phone>& phones, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
) { ) {
// Create timeline of shape rules // Create timeline of shape rules
ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones); ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);

View File

@ -2,10 +2,9 @@
#include "core/Phone.h" #include "core/Phone.h"
#include "core/Shape.h" #include "core/Shape.h"
#include "time/ContinuousTimeline.h"
#include "targetShapeSet.h" #include "targetShapeSet.h"
#include "time/ContinuousTimeline.h"
JoiningContinuousTimeline<Shape> animate( JoiningContinuousTimeline<Shape> animate(
const BoundedTimeline<Phone>& phones, const BoundedTimeline<Phone>& phones, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
); );

View File

@ -1,4 +1,5 @@
#include "pauseAnimation.h" #include "pauseAnimation.h"
#include "animationRules.h" #include "animationRules.h"
Shape getPauseShape(Shape previous, Shape next, centiseconds duration) { Shape getPauseShape(Shape previous, Shape next, centiseconds duration) {

View File

@ -1,4 +1,5 @@
#include "roughAnimation.h" #include "roughAnimation.h"
#include <boost/optional.hpp> #include <boost/optional.hpp>
// Create timeline of shapes using a bidirectional algorithm. // Create timeline of shapes using a bidirectional algorithm.
@ -22,9 +23,8 @@ 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 const bool anticipateShape =
&& isVowel(*shapeRule.phone) shapeRule.phone && isVowel(*shapeRule.phone) && shapeRule.shapeSet.size() == 1;
&& shapeRule.shapeSet.size() == 1;
if (anticipateShape) { if (anticipateShape) {
// Animate backwards a little // Animate backwards a little
const Shape anticipatedShape = shape; const Shape anticipatedShape = shape;

View File

@ -1,6 +1,8 @@
#include "staticSegments.h" #include "staticSegments.h"
#include <vector>
#include <numeric> #include <numeric>
#include <vector>
#include "tools/nextCombination.h" #include "tools/nextCombination.h"
using std::vector; using std::vector;
@ -78,8 +80,7 @@ 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( ContinuousTimeline<ShapeRule> applyChanges(
const ContinuousTimeline<ShapeRule>& shapeRules, const ContinuousTimeline<ShapeRule>& shapeRules, const RuleChanges& changes
const RuleChanges& changes
) { ) {
ContinuousTimeline<ShapeRule> result(shapeRules); ContinuousTimeline<ShapeRule> result(shapeRules);
for (centiseconds changedRuleStart : changes) { for (centiseconds changedRuleStart : changes) {
@ -99,8 +100,7 @@ public:
) : ) :
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
@ -133,7 +133,8 @@ private:
[](const double sum, const Timed<Shape>& timedShape) { [](const double sum, const Timed<Shape>& timedShape) {
const double duration = std::chrono::duration_cast<std::chrono::duration<double>>( const double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
timedShape.getDuration() timedShape.getDuration()
).count(); )
.count();
return sum + duration * duration; return sum + duration * duration;
} }
); );
@ -152,8 +153,7 @@ RuleChanges getPossibleRuleChanges(const ContinuousTimeline<ShapeRule>& shapeRul
} }
ContinuousTimeline<ShapeRule> fixStaticSegmentRules( ContinuousTimeline<ShapeRule> fixStaticSegmentRules(
const ContinuousTimeline<ShapeRule>& shapeRules, const ContinuousTimeline<ShapeRule>& shapeRules, const AnimationFunction& animate
const AnimationFunction& animate
) { ) {
// The complexity of this function is exponential with the number of replacements. // The complexity of this function is exponential with the number of replacements.
// So let's cap that value. // So let's cap that value.
@ -164,11 +164,10 @@ ContinuousTimeline<ShapeRule> fixStaticSegmentRules(
// Find best solution. Start with a single replacement, then increase as necessary. // Find best solution. Start with a single replacement, then increase as necessary.
RuleChangeScenario bestScenario(shapeRules, {}, animate); RuleChangeScenario bestScenario(shapeRules, {}, animate);
for ( for (int replacementCount = 1; bestScenario.getStaticSegmentCount() > 0
int replacementCount = 1; && replacementCount
bestScenario.getStaticSegmentCount() > 0 && replacementCount <= std::min(static_cast<int>(possibleRuleChanges.size()), maxReplacementCount); <= std::min(static_cast<int>(possibleRuleChanges.size()), maxReplacementCount);
++replacementCount ++replacementCount) {
) {
// Only the first <replacementCount> elements of `currentRuleChanges` count // Only the first <replacementCount> elements of `currentRuleChanges` count
auto currentRuleChanges(possibleRuleChanges); auto currentRuleChanges(possibleRuleChanges);
do { do {
@ -180,7 +179,11 @@ ContinuousTimeline<ShapeRule> fixStaticSegmentRules(
if (currentScenario.isBetterThan(bestScenario)) { if (currentScenario.isBetterThan(bestScenario)) {
bestScenario = currentScenario; bestScenario = currentScenario;
} }
} while (next_combination(currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount, currentRuleChanges.end())); } while (next_combination(
currentRuleChanges.begin(),
currentRuleChanges.begin() + replacementCount,
currentRuleChanges.end()
));
} }
return bestScenario.getChangedRules(); return bestScenario.getChangedRules();
@ -194,8 +197,7 @@ bool isFlexible(const ShapeRule& rule) {
// Extends the specified time range until it starts and ends with a non-flexible shape rule, if // Extends the specified time range until it starts and ends with a non-flexible shape rule, if
// possible // possible
TimeRange extendToFixedRules( TimeRange extendToFixedRules(
const TimeRange& timeRange, const TimeRange& timeRange, const ContinuousTimeline<ShapeRule>& shapeRules
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())) {
@ -209,8 +211,7 @@ TimeRange extendToFixedRules(
} }
JoiningContinuousTimeline<Shape> avoidStaticSegments( JoiningContinuousTimeline<Shape> avoidStaticSegments(
const ContinuousTimeline<ShapeRule>& shapeRules, const ContinuousTimeline<ShapeRule>& shapeRules, const AnimationFunction& animate
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);
@ -227,8 +228,7 @@ JoiningContinuousTimeline<Shape> avoidStaticSegments(
// Fix shape rules within the static segment // Fix shape rules within the static segment
const auto fixedSegmentShapeRules = fixStaticSegmentRules( const auto fixedSegmentShapeRules = fixStaticSegmentRules(
{ extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules }, {extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules}, animate
animate
); );
for (const auto& timedShapeRule : fixedSegmentShapeRules) { for (const auto& timedShapeRule : fixedSegmentShapeRules) {
fixedShapeRules.set(timedShapeRule); fixedShapeRules.set(timedShapeRule);

View File

@ -1,18 +1,20 @@
#pragma once #pragma once
#include "core/Shape.h"
#include "time/ContinuousTimeline.h"
#include "ShapeRule.h"
#include <functional> #include <functional>
using AnimationFunction = std::function<JoiningContinuousTimeline<Shape>(const ContinuousTimeline<ShapeRule>&)>; #include "core/Shape.h"
#include "ShapeRule.h"
#include "time/ContinuousTimeline.h"
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 // If the resulting animation contains long static segments, the shape rules are tweaked and
// animated again. // 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( JoiningContinuousTimeline<Shape> avoidStaticSegments(
const ContinuousTimeline<ShapeRule>& shapeRules, const ContinuousTimeline<ShapeRule>& shapeRules, const AnimationFunction& animate
const AnimationFunction& animate
); );

View File

@ -7,7 +7,8 @@ Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet) {
const 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( throw std::invalid_argument(
fmt::format("Target shape set must contain basic shape {}.", basicShape)); fmt::format("Target shape set must contain basic shape {}.", basicShape)
);
} }
return basicShape; return basicShape;
} }
@ -21,8 +22,7 @@ ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetS
} }
ContinuousTimeline<ShapeRule> convertToTargetShapeSet( ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
const ContinuousTimeline<ShapeRule>& shapeRules, const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
) { ) {
ContinuousTimeline<ShapeRule> result(shapeRules); ContinuousTimeline<ShapeRule> result(shapeRules);
for (const auto& timedShapeRule : shapeRules) { for (const auto& timedShapeRule : shapeRules) {
@ -34,8 +34,7 @@ ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
} }
JoiningContinuousTimeline<Shape> convertToTargetShapeSet( JoiningContinuousTimeline<Shape> convertToTargetShapeSet(
const JoiningContinuousTimeline<Shape>& animation, const JoiningContinuousTimeline<Shape>& animation, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
) { ) {
JoiningContinuousTimeline<Shape> result(animation); JoiningContinuousTimeline<Shape> result(animation);
for (const auto& timedShape : animation) { for (const auto& timedShape : animation) {

View File

@ -12,13 +12,11 @@ ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetS
// 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( ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
const ContinuousTimeline<ShapeRule>& shapeRules, const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
); );
// Replaces each shape in the specified animation with the closest shape that occurs in the target // Replaces each shape in the specified animation with the closest shape that occurs in the target
// shape set. // shape set.
JoiningContinuousTimeline<Shape> convertToTargetShapeSet( JoiningContinuousTimeline<Shape> convertToTargetShapeSet(
const JoiningContinuousTimeline<Shape>& animation, const JoiningContinuousTimeline<Shape>& animation, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
); );

View File

@ -1,12 +1,14 @@
#include "timingOptimization.h" #include "timingOptimization.h"
#include "time/timedLogging.h"
#include <algorithm>
#include <boost/lexical_cast.hpp> #include <boost/lexical_cast.hpp>
#include <map> #include <map>
#include <algorithm>
#include "ShapeRule.h"
using std::string; #include "ShapeRule.h"
#include "time/timedLogging.h"
using std::map; using std::map;
using std::string;
string getShapesString(const JoiningContinuousTimeline<Shape>& shapes) { string getShapesString(const JoiningContinuousTimeline<Shape>& shapes) {
string result; string result;
@ -32,7 +34,8 @@ Shape getRepresentativeShape(const JoiningTimeline<Shape>& timeline) {
// Select shape with highest total duration within the candidate range // Select shape with highest total duration within the candidate range
const Shape bestShape = std::max_element( const Shape bestShape = std::max_element(
candidateShapeWeights.begin(), candidateShapeWeights.end(), candidateShapeWeights.begin(),
candidateShapeWeights.end(),
[](auto a, auto b) { return a.second < b.second; } [](auto a, auto b) { return a.second < b.second; }
)->first; )->first;
@ -55,8 +58,11 @@ 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, TimeRange getNextMinimalCandidateRange(
const TimeRange targetRange, const centiseconds writePosition) { 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.");
} }
@ -69,9 +75,8 @@ 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 const centiseconds duration =
? minShapeDuration canFitOneOrLess || canFitTwo ? minShapeDuration : remainingTargetDuration / 2;
: remainingTargetDuration / 2;
TimeRange candidateRange(writePosition - duration, writePosition); TimeRange candidateRange(writePosition - duration, writePosition);
if (writePosition == targetRange.getEnd()) { if (writePosition == targetRange.getEnd()) {
@ -102,22 +107,24 @@ ShapeReduction getNextShapeReduction(
// 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, const ShapeReduction minReduction(
getNextMinimalCandidateRange(sourceShapes, targetRange, writePosition)); 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.begin()->getStart(),
minReduction.sourceShapes.getRange().getEnd() minReduction.sourceShapes.getRange().getEnd()}
}
); );
// Determine the shape that might be picked *next* if we choose the shortest-possible candidate // Determine the shape that might be picked *next* if we choose the shortest-possible candidate
// range now // range now
const ShapeReduction nextReduction( const ShapeReduction nextReduction(
sourceShapes, sourceShapes,
getNextMinimalCandidateRange(sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart()) getNextMinimalCandidateRange(
sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart()
)
); );
const bool minEqualsExtended = minReduction.shape == extendedReduction.shape; const bool minEqualsExtended = minReduction.shape == extendedReduction.shape;
@ -129,8 +136,9 @@ ShapeReduction getNextShapeReduction(
// Modifies the timing of the given animation to fit into the specified target time range without // Modifies the timing of the given animation to fit into the specified target time range without
// jitter. // jitter.
JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>& sourceShapes, JoiningContinuousTimeline<Shape> retime(
const TimeRange targetRange) { 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);
@ -139,7 +147,6 @@ JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>&
// Animate backwards // Animate backwards
centiseconds writePosition = targetRange.getEnd(); centiseconds writePosition = targetRange.getEnd();
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 = const ShapeReduction shapeReduction =
getNextShapeReduction(sourceShapes, targetRange, writePosition); getNextShapeReduction(sourceShapes, targetRange, writePosition);
@ -162,30 +169,21 @@ JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>&
} }
JoiningContinuousTimeline<Shape> retime( JoiningContinuousTimeline<Shape> retime(
const JoiningContinuousTimeline<Shape>& animation, const JoiningContinuousTimeline<Shape>& animation, TimeRange sourceRange, TimeRange targetRange
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);
} }
enum class MouthState { enum class MouthState { Idle, Closed, Open };
Idle,
Closed,
Open
};
JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<Shape>& animation) { JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<Shape>& animation) {
// Identify segments with idle, closed, and open mouth shapes // Identify segments with idle, closed, and open mouth shapes
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 = const MouthState mouthState = shape == Shape::X ? MouthState::Idle
shape == Shape::X : shape == Shape::A ? MouthState::Closed
? MouthState::Idle
: shape == Shape::A
? MouthState::Closed
: MouthState::Open; : MouthState::Open;
segments.set(timedShape.getTimeRange(), mouthState); segments.set(timedShape.getTimeRange(), mouthState);
} }
@ -219,11 +217,8 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
// evenly. // evenly.
const auto begin = segmentIt; const auto begin = segmentIt;
auto end = std::next(begin); auto end = std::next(begin);
while ( while (end != segments.rend() && end->getValue() != MouthState::Idle
end != segments.rend() && end->getDuration() < minSegmentDuration) {
&& end->getValue() != MouthState::Idle
&& end->getDuration() < minSegmentDuration
) {
++end; ++end;
} }
@ -232,20 +227,19 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
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() const centiseconds availableExtensionDuration =
? end->getDuration() - 1_cs end != segments.rend() ? end->getDuration() - 1_cs : 0_cs;
: 0_cs; const centiseconds extensionDuration = std::min(
const centiseconds extensionDuration = std::min({ {desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration}
desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration );
});
// Distribute available time range evenly among all short segments // Distribute available time range evenly among all short segments
const centiseconds shortSegmentsTargetStart = const centiseconds shortSegmentsTargetStart =
std::prev(end)->getStart() - extensionDuration; 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) / const centiseconds segmentDuration =
remainingShortSegmentCount; (resultStart - shortSegmentsTargetStart) / remainingShortSegmentCount;
const TimeRange segmentTargetRange(resultStart - segmentDuration, resultStart); const TimeRange segmentTargetRange(resultStart - segmentDuration, resultStart);
const auto retimedSegment = const auto retimedSegment =
retime(animation, shortSegmentIt->getTimeRange(), segmentTargetRange); retime(animation, shortSegmentIt->getTimeRange(), segmentTargetRange);

View File

@ -1,4 +1,5 @@
#include "tweening.h" #include "tweening.h"
#include "animationRules.h" #include "animationRules.h"
JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Shape>& animation) { JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Shape>& animation) {
@ -7,7 +8,10 @@ JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Sh
JoiningContinuousTimeline<Shape> result(animation); JoiningContinuousTimeline<Shape> result(animation);
for_each_adjacent(animation.begin(), animation.end(), [&](const auto& first, const auto& second) { for_each_adjacent(
animation.begin(),
animation.end(),
[&](const auto& first, const auto& second) {
auto pair = getTween(first.getValue(), second.getValue()); auto pair = getTween(first.getValue(), second.getValue());
if (!pair) return; if (!pair) return;
@ -19,28 +23,26 @@ 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(
tweenDuration = std::min({ {firstTimeRange.getDuration() / 4,
firstTimeRange.getDuration() / 4, secondTimeRange.getDuration() / 4, maxTweenDuration 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: default: {
{
throw std::runtime_error("Unexpected tween timing."); throw std::runtime_error("Unexpected tween timing.");
} }
} }
@ -48,7 +50,8 @@ JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Sh
if (tweenDuration < minTweenDuration) return; if (tweenDuration < minTweenDuration) return;
result.set(tweenStart, tweenStart + tweenDuration, tweenShape); result.set(tweenStart, tweenStart + tweenDuration, tweenShape);
}); }
);
return result; return result;
} }

View File

@ -1,4 +1,5 @@
#include "AudioClip.h" #include "AudioClip.h"
#include <format.h> #include <format.h>
using std::invalid_argument; using std::invalid_argument;
@ -11,6 +12,7 @@ class SafeSampleReader {
public: public:
SafeSampleReader(SampleReader unsafeRead, AudioClip::size_type size); SafeSampleReader(SampleReader unsafeRead, AudioClip::size_type size);
AudioClip::value_type operator()(AudioClip::size_type index); AudioClip::value_type operator()(AudioClip::size_type index);
private: private:
SampleReader unsafeRead; SampleReader unsafeRead;
AudioClip::size_type size; AudioClip::size_type size;
@ -20,19 +22,16 @@ private:
SafeSampleReader::SafeSampleReader(SampleReader unsafeRead, AudioClip::size_type size) : SafeSampleReader::SafeSampleReader(SampleReader unsafeRead, AudioClip::size_type size) :
unsafeRead(unsafeRead), unsafeRead(unsafeRead),
size(size) size(size) {}
{}
inline AudioClip::value_type SafeSampleReader::operator()(AudioClip::size_type index) { inline AudioClip::value_type SafeSampleReader::operator()(AudioClip::size_type index) {
if (index < 0) { if (index < 0) {
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( throw invalid_argument(
"Cannot read from sample index {}. Clip size is {}.", fmt::format("Cannot read from sample index {}. Clip size is {}.", index, size)
index, );
size
));
} }
if (index == lastIndex) { if (index == lastIndex) {
return lastSample; return lastSample;
@ -60,10 +59,8 @@ std::unique_ptr<AudioClip> operator|(std::unique_ptr<AudioClip> clip, const Audi
} }
SampleIterator::SampleIterator() : SampleIterator::SampleIterator() :
sampleIndex(0) sampleIndex(0) {}
{}
SampleIterator::SampleIterator(const AudioClip& audioClip, size_type sampleIndex) : SampleIterator::SampleIterator(const AudioClip& audioClip, size_type sampleIndex) :
sampleReader([&audioClip] { return audioClip.createSampleReader(); }), sampleReader([&audioClip] { return audioClip.createSampleReader(); }),
sampleIndex(sampleIndex) sampleIndex(sampleIndex) {}
{}

View File

@ -1,8 +1,9 @@
#pragma once #pragma once
#include <memory>
#include "time/TimeRange.h"
#include <functional> #include <functional>
#include <memory>
#include "time/TimeRange.h"
#include "tools/Lazy.h" #include "tools/Lazy.h"
class AudioClip; class AudioClip;
@ -17,6 +18,7 @@ public:
using SampleReader = std::function<value_type(size_type)>; using SampleReader = std::function<value_type(size_type)>;
virtual ~AudioClip() {} virtual ~AudioClip() {}
virtual std::unique_ptr<AudioClip> clone() const = 0; virtual std::unique_ptr<AudioClip> clone() const = 0;
virtual int getSampleRate() const = 0; virtual int getSampleRate() const = 0;
virtual size_type size() const = 0; virtual size_type size() const = 0;
@ -24,6 +26,7 @@ public:
SampleReader createSampleReader() const; SampleReader createSampleReader() const;
iterator begin() const; iterator begin() const;
iterator end() const; iterator end() const;
private: private:
virtual SampleReader createUnsafeSampleReader() const = 0; virtual SampleReader createUnsafeSampleReader() const = 0;
}; };
@ -137,6 +140,8 @@ inline SampleIterator operator-(const SampleIterator& it, SampleIterator::differ
return result; return result;
} }
inline SampleIterator::difference_type operator-(const SampleIterator& lhs, const SampleIterator& rhs) { inline SampleIterator::difference_type operator-(
const SampleIterator& lhs, const SampleIterator& rhs
) {
return lhs.getSampleIndex() - rhs.getSampleIndex(); return lhs.getSampleIndex() - rhs.getSampleIndex();
} }

View File

@ -1,13 +1,16 @@
#include "AudioSegment.h" #include "AudioSegment.h"
using std::unique_ptr;
using std::make_unique; using std::make_unique;
using std::unique_ptr;
AudioSegment::AudioSegment(std::unique_ptr<AudioClip> inputClip, const TimeRange& range) : AudioSegment::AudioSegment(std::unique_ptr<AudioClip> inputClip, const TimeRange& range) :
inputClip(std::move(inputClip)), inputClip(std::move(inputClip)),
sampleOffset(static_cast<int64_t>(range.getStart().count()) * this->inputClip->getSampleRate() / 100), sampleOffset(
sampleCount(static_cast<int64_t>(range.getDuration().count()) * this->inputClip->getSampleRate() / 100) static_cast<int64_t>(range.getStart().count()) * this->inputClip->getSampleRate() / 100
{ ),
sampleCount(
static_cast<int64_t>(range.getDuration().count()) * this->inputClip->getSampleRate() / 100
) {
if (sampleOffset < 0 || sampleOffset + sampleCount > this->inputClip->size()) { if (sampleOffset < 0 || sampleOffset + sampleCount > this->inputClip->size()) {
throw std::invalid_argument("Segment extends beyond input clip."); throw std::invalid_argument("Segment extends beyond input clip.");
} }

View File

@ -1,25 +1,23 @@
#include "DcOffset.h" #include "DcOffset.h"
#include <cmath> #include <cmath>
using std::unique_ptr;
using std::make_unique; using std::make_unique;
using std::unique_ptr;
DcOffset::DcOffset(unique_ptr<AudioClip> inputClip, float offset) : DcOffset::DcOffset(unique_ptr<AudioClip> inputClip, float offset) :
inputClip(std::move(inputClip)), inputClip(std::move(inputClip)),
offset(offset), offset(offset),
factor(1 / (1 + std::abs(offset))) factor(1 / (1 + std::abs(offset))) {}
{}
unique_ptr<AudioClip> DcOffset::clone() const { unique_ptr<AudioClip> DcOffset::clone() const {
return make_unique<DcOffset>(*this); return make_unique<DcOffset>(*this);
} }
SampleReader DcOffset::createUnsafeSampleReader() const { SampleReader DcOffset::createUnsafeSampleReader() const {
return [ return
read = inputClip->createSampleReader(), [read = inputClip->createSampleReader(), factor = factor, offset = offset](size_type index
factor = factor, ) {
offset = offset
](size_type index) {
const float sample = read(index); const float sample = read(index);
return sample * factor + offset; return sample * factor + offset;
}; };

View File

@ -10,6 +10,7 @@ public:
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;
private: private:
SampleReader createUnsafeSampleReader() const override; SampleReader createUnsafeSampleReader() const override;

View File

@ -1,43 +1,36 @@
#include "OggVorbisFileReader.h" #include "OggVorbisFileReader.h"
#include <format.h>
#include "tools/fileTools.h"
#include "tools/tools.h"
#include "vorbis/codec.h" #include "vorbis/codec.h"
#include "vorbis/vorbisfile.h" #include "vorbis/vorbisfile.h"
#include "tools/tools.h"
#include <format.h>
#include "tools/fileTools.h"
using std::filesystem::path;
using std::vector;
using std::make_shared;
using std::ifstream; using std::ifstream;
using std::ios_base; using std::ios_base;
using std::make_shared;
using std::vector;
using std::filesystem::path;
std::string vorbisErrorToString(int64_t errorCode) { std::string vorbisErrorToString(int64_t errorCode) {
switch (errorCode) { switch (errorCode) {
case OV_EREAD: case OV_EREAD: return "Read error while fetching compressed data for decode.";
return "Read error while fetching compressed data for decode."; case OV_EFAULT: return "Internal logic fault; indicates a bug or heap/stack corruption.";
case OV_EFAULT: case OV_EIMPL: return "Feature not implemented";
return "Internal logic fault; indicates a bug or heap/stack corruption.";
case OV_EIMPL:
return "Feature not implemented";
case OV_EINVAL: case OV_EINVAL:
return "Either an invalid argument, or incompletely initialized argument passed to a call."; return "Either an invalid argument, or incompletely initialized argument passed to a call.";
case OV_ENOTVORBIS: case OV_ENOTVORBIS: return "The given file/data was not recognized as Ogg Vorbis data.";
return "The given file/data was not recognized as Ogg Vorbis data.";
case OV_EBADHEADER: case OV_EBADHEADER:
return "The file/data is apparently an Ogg Vorbis stream, but contains a corrupted or undecipherable header."; return "The file/data is apparently an Ogg Vorbis stream, but contains a corrupted or undecipherable header.";
case OV_EVERSION: case OV_EVERSION:
return "The bitstream format revision of the given Vorbis stream is not supported."; return "The bitstream format revision of the given Vorbis stream is not supported.";
case OV_ENOTAUDIO: case OV_ENOTAUDIO: return "Packet is not an audio packet.";
return "Packet is not an audio packet."; case OV_EBADPACKET: return "Error in packet.";
case OV_EBADPACKET:
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 garbage 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: return "An unexpected Vorbis error occurred.";
default:
return "An unexpected Vorbis error occurred.";
} }
} }
@ -104,8 +97,7 @@ private:
OggVorbisFile::OggVorbisFile(const path& filePath) : OggVorbisFile::OggVorbisFile(const path& filePath) :
oggVorbisHandle(), oggVorbisHandle(),
stream(openFile(filePath)) stream(openFile(filePath)) {
{
// Throw only on badbit, not on failbit. // Throw only on badbit, not on failbit.
// Ogg Vorbis expects read operations past the end of the file to // Ogg Vorbis expects read operations past the end of the file to
// succeed, not to throw. // succeed, not to throw.
@ -119,8 +111,7 @@ OggVorbisFile::OggVorbisFile(const path& filePath) :
} }
OggVorbisFileReader::OggVorbisFileReader(const path& filePath) : OggVorbisFileReader::OggVorbisFileReader(const path& filePath) :
filePath(filePath) filePath(filePath) {
{
OggVorbisFile file(filePath); OggVorbisFile file(filePath);
vorbis_info* vorbisInfo = ov_info(file.get(), -1); vorbis_info* vorbisInfo = ov_info(file.get(), -1);
@ -135,13 +126,11 @@ std::unique_ptr<AudioClip> OggVorbisFileReader::clone() const {
} }
SampleReader OggVorbisFileReader::createUnsafeSampleReader() const { SampleReader OggVorbisFileReader::createUnsafeSampleReader() const {
return [ return [channelCount = channelCount,
channelCount = channelCount,
file = make_shared<OggVorbisFile>(filePath), file = make_shared<OggVorbisFile>(filePath),
buffer = static_cast<value_type**>(nullptr), buffer = static_cast<value_type**>(nullptr),
bufferStart = size_type(0), bufferStart = size_type(0),
bufferSize = size_type(0) bufferSize = size_type(0)](size_type index) mutable {
](size_type index) mutable {
if (index < bufferStart || index >= bufferStart + bufferSize) { if (index < bufferStart || index >= bufferStart + bufferSize) {
// Seek // Seek
throwOnError(ov_pcm_seek(file->get(), index)); throwOnError(ov_pcm_seek(file->get(), index));

View File

@ -1,14 +1,21 @@
#pragma once #pragma once
#include "AudioClip.h"
#include <filesystem> #include <filesystem>
#include "AudioClip.h"
class OggVorbisFileReader : public AudioClip { class OggVorbisFileReader : public AudioClip {
public: public:
OggVorbisFileReader(const std::filesystem::path& filePath); OggVorbisFileReader(const std::filesystem::path& filePath);
std::unique_ptr<AudioClip> clone() const override; std::unique_ptr<AudioClip> clone() const override;
int getSampleRate() const override { return sampleRate; }
size_type size() const override { return sampleCount; } int getSampleRate() const override {
return sampleRate;
}
size_type size() const override {
return sampleCount;
}
private: private:
SampleReader createUnsafeSampleReader() const override; SampleReader createUnsafeSampleReader() const override;

View File

@ -1,25 +1,25 @@
#include <cmath>
#include "SampleRateConverter.h" #include "SampleRateConverter.h"
#include <stdexcept>
#include <format.h> #include <format.h>
#include <cmath>
#include <stdexcept>
using std::invalid_argument; using std::invalid_argument;
using std::unique_ptr;
using std::make_unique; using std::make_unique;
using std::unique_ptr;
SampleRateConverter::SampleRateConverter(unique_ptr<AudioClip> inputClip, int outputSampleRate) : SampleRateConverter::SampleRateConverter(unique_ptr<AudioClip> inputClip, int outputSampleRate) :
inputClip(std::move(inputClip)), inputClip(std::move(inputClip)),
downscalingFactor(static_cast<double>(this->inputClip->getSampleRate()) / outputSampleRate), downscalingFactor(static_cast<double>(this->inputClip->getSampleRate()) / outputSampleRate),
outputSampleRate(outputSampleRate), outputSampleRate(outputSampleRate),
outputSampleCount(std::lround(this->inputClip->size() / downscalingFactor)) outputSampleCount(std::lround(this->inputClip->size() / downscalingFactor)) {
{
if (outputSampleRate <= 0) { if (outputSampleRate <= 0) {
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( throw invalid_argument(fmt::format(
"Upsampling not supported. Input sample rate must not be below {}Hz.", "Upsampling not supported. Input sample rate must not be below {}Hz.", outputSampleRate
outputSampleRate
)); ));
} }
} }
@ -51,11 +51,9 @@ float mean(double inputStart, double inputEnd, const SampleReader& read) {
} }
SampleReader SampleRateConverter::createUnsafeSampleReader() const { SampleReader SampleRateConverter::createUnsafeSampleReader() const {
return [ return [read = inputClip->createSampleReader(),
read = inputClip->createSampleReader(),
downscalingFactor = downscalingFactor, downscalingFactor = downscalingFactor,
size = inputClip->size() size = inputClip->size()](size_type index) {
](size_type index) {
const double inputStart = index * downscalingFactor; const double inputStart = index * downscalingFactor;
const double inputEnd = const double inputEnd =
std::min((index + 1) * downscalingFactor, static_cast<double>(size)); std::min((index + 1) * downscalingFactor, static_cast<double>(size));

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include "AudioClip.h" #include "AudioClip.h"
class SampleRateConverter : public AudioClip { class SampleRateConverter : public AudioClip {
@ -9,6 +10,7 @@ public:
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;
private: private:
SampleReader createUnsafeSampleReader() const override; SampleReader createUnsafeSampleReader() const override;

View File

@ -1,19 +1,22 @@
#include <format.h>
#include "WaveFileReader.h" #include "WaveFileReader.h"
#include "ioTools.h"
#include <iostream>
#include "tools/platformTools.h"
#include "tools/fileTools.h"
using std::runtime_error; #include <format.h>
#include <iostream>
#include "ioTools.h"
#include "tools/fileTools.h"
#include "tools/platformTools.h"
using fmt::format; using fmt::format;
using std::runtime_error;
using std::string; using std::string;
using namespace little_endian; using namespace little_endian;
using std::unique_ptr;
using std::make_unique;
using std::make_shared; using std::make_shared;
using std::filesystem::path; using std::make_unique;
using std::streamoff; using std::streamoff;
using std::unique_ptr;
using std::filesystem::path;
#define INT24_MIN (-8388608) #define INT24_MIN (-8388608)
#define INT24_MAX 8388607 #define INT24_MAX 8388607
@ -34,7 +37,7 @@ namespace Codec {
constexpr int Pcm = 0x01; constexpr int Pcm = 0x01;
constexpr int Float = 0x03; constexpr int Float = 0x03;
constexpr int Extensible = 0xFFFE; constexpr int Extensible = 0xFFFE;
}; }; // namespace Codec
string codecToString(int codec); string codecToString(int codec);
@ -74,8 +77,7 @@ WaveFormatInfo getWaveFormatInfo(const path& filePath) {
const streamoff chunkSize = read<int32_t>(file); const streamoff chunkSize = read<int32_t>(file);
const streamoff chunkEnd = roundUpToEven(file.tellg() + chunkSize); const streamoff chunkEnd = roundUpToEven(file.tellg() + chunkSize);
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);
@ -118,7 +120,8 @@ WaveFormatInfo getWaveFormatInfo(const path& filePath) {
bytesPerSample = 4; bytesPerSample = 4;
} else { } else {
throw runtime_error( throw runtime_error(
format("Unsupported sample format: {}-bit PCM.", bitsPerSample)); format("Unsupported sample format: {}-bit PCM.", bitsPerSample)
);
} }
if (bytesPerSample != bytesPerFrame / formatInfo.channelCount) { if (bytesPerSample != bytesPerFrame / formatInfo.channelCount) {
throw runtime_error("Unsupported sample organization."); throw runtime_error("Unsupported sample organization.");
@ -132,30 +135,30 @@ WaveFormatInfo getWaveFormatInfo(const path& filePath) {
formatInfo.sampleFormat = SampleFormat::Float64; formatInfo.sampleFormat = SampleFormat::Float64;
bytesPerSample = 8; bytesPerSample = 8;
} else { } else {
throw runtime_error( throw runtime_error(format(
format("Unsupported sample format: {}-bit IEEE Float.", bitsPerSample) "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;
processedFormatChunk = true; processedFormatChunk = true;
break; break;
} }
case fourcc('d', 'a', 't', 'a'): case fourcc('d', 'a', 't', 'a'): {
{
formatInfo.dataOffset = file.tellg(); formatInfo.dataOffset = file.tellg();
formatInfo.frameCount = chunkSize / formatInfo.bytesPerFrame; formatInfo.frameCount = chunkSize / formatInfo.bytesPerFrame;
processedDataChunk = true; processedDataChunk = true;
break; break;
} }
default: default: {
{
// Ignore unknown chunk // Ignore unknown chunk
break; break;
} }
@ -180,45 +183,37 @@ unique_ptr<AudioClip> WaveFileReader::clone() const {
} }
inline AudioClip::value_type readSample( inline AudioClip::value_type readSample(
std::ifstream& file, std::ifstream& file, SampleFormat sampleFormat, int channelCount
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: {
{
const 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: {
{
const 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::Int32: case SampleFormat::Int32: {
{
const int32_t raw = read<int32_t>(file); const int32_t raw = read<int32_t>(file);
sum += toNormalizedFloat(raw, INT32_MIN, INT32_MAX); sum += toNormalizedFloat(raw, INT32_MIN, INT32_MAX);
break; break;
} }
case SampleFormat::Float32: case SampleFormat::Float32: {
{
sum += read<float>(file); sum += read<float>(file);
break; break;
} }
case SampleFormat::Float64: case SampleFormat::Float64: {
{
sum += static_cast<float>(read<double>(file)); sum += static_cast<float>(read<double>(file));
break; break;
} }
@ -229,14 +224,11 @@ inline AudioClip::value_type readSample(
} }
SampleReader WaveFileReader::createUnsafeSampleReader() const { SampleReader WaveFileReader::createUnsafeSampleReader() const {
return return [formatInfo = formatInfo,
[
formatInfo = formatInfo,
file = std::make_shared<std::ifstream>(openFile(filePath)), file = std::make_shared<std::ifstream>(openFile(filePath)),
filePos = std::streampos(0) filePos = std::streampos(0)](size_type index) mutable {
](size_type index) mutable { const std::streampos newFilePos =
const std::streampos newFilePos = formatInfo.dataOffset formatInfo.dataOffset + static_cast<streamoff>(index * formatInfo.bytesPerFrame);
+ static_cast<streamoff>(index * formatInfo.bytesPerFrame);
if (newFilePos != filePos) { if (newFilePos != filePos) {
file->seekg(newFilePos); file->seekg(newFilePos);
} }
@ -491,7 +483,6 @@ 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: default: return format("{0:#x}", codec);
return format("{0:#x}", codec);
} }
} }

View File

@ -1,16 +1,10 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include "AudioClip.h" #include "AudioClip.h"
enum class SampleFormat { enum class SampleFormat { UInt8, Int16, Int24, Int32, Float32, Float64 };
UInt8,
Int16,
Int24,
Int32,
Float32,
Float64
};
struct WaveFormatInfo { struct WaveFormatInfo {
int bytesPerFrame; int bytesPerFrame;

View File

@ -1,18 +1,20 @@
#include "audioFileReading.h" #include "audioFileReading.h"
#include <format.h>
#include "WaveFileReader.h"
#include <boost/algorithm/string.hpp>
#include "OggVorbisFileReader.h"
using std::filesystem::path; #include <format.h>
using std::string;
using std::runtime_error; #include <boost/algorithm/string.hpp>
#include "OggVorbisFileReader.h"
#include "WaveFileReader.h"
using fmt::format; using fmt::format;
using std::runtime_error;
using std::string;
using std::filesystem::path;
std::unique_ptr<AudioClip> createAudioFileClip(path filePath) { std::unique_ptr<AudioClip> createAudioFileClip(path filePath) {
try { try {
const string extension = const string extension = boost::algorithm::to_lower_copy(filePath.extension().u8string());
boost::algorithm::to_lower_copy(filePath.extension().u8string());
if (extension == ".wav") { if (extension == ".wav") {
return std::make_unique<WaveFileReader>(filePath); return std::make_unique<WaveFileReader>(filePath);
} }
@ -24,6 +26,8 @@ std::unique_ptr<AudioClip> createAudioFileClip(path filePath) {
extension extension
)); ));
} catch (...) { } catch (...) {
std::throw_with_nested(runtime_error(format("Could not open sound file {}.", filePath.u8string()))); std::throw_with_nested(
runtime_error(format("Could not open sound file {}.", filePath.u8string()))
);
} }
} }

View File

@ -1,7 +1,8 @@
#pragma once #pragma once
#include <memory>
#include "AudioClip.h"
#include <filesystem> #include <filesystem>
#include <memory>
#include "AudioClip.h"
std::unique_ptr<AudioClip> createAudioFileClip(std::filesystem::path filePath); std::unique_ptr<AudioClip> createAudioFileClip(std::filesystem::path filePath);

View File

@ -30,12 +30,7 @@ namespace little_endian {
} }
} }
constexpr uint32_t fourcc( constexpr uint32_t fourcc(unsigned char c0, unsigned char c1, unsigned char c2, unsigned char c3) {
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);
} }
@ -43,4 +38,4 @@ namespace little_endian {
return std::string(reinterpret_cast<char*>(&fourcc), 4); return std::string(reinterpret_cast<char*>(&fourcc), 4);
} }
} } // namespace little_endian

View File

@ -1,4 +1,5 @@
#include "processing.h" #include "processing.h"
#include <algorithm> #include <algorithm>
using std::function; using std::function;
@ -35,7 +36,9 @@ void process16bitAudioClip(
processBuffer(buffer); processBuffer(buffer);
sampleCount += buffer.size(); sampleCount += buffer.size();
progressSink.reportProgress(static_cast<double>(sampleCount) / static_cast<double>(audioClip.size())); progressSink.reportProgress(
static_cast<double>(sampleCount) / static_cast<double>(audioClip.size())
);
} while (!buffer.empty()); } while (!buffer.empty());
} }

View File

@ -1,7 +1,8 @@
#pragma once #pragma once
#include <vector>
#include <functional> #include <functional>
#include <vector>
#include "AudioClip.h" #include "AudioClip.h"
#include "tools/progress.h" #include "tools/progress.h"

View File

@ -1,30 +1,31 @@
#include "voiceActivityDetection.h" #include "voiceActivityDetection.h"
#include "DcOffset.h"
#include "SampleRateConverter.h"
#include "logging/logging.h"
#include "tools/pairs.h"
#include <boost/range/adaptor/transformed.hpp>
#include <webrtc/common_audio/vad/include/webrtc_vad.h>
#include "processing.h"
#include <gsl_util.h> #include <gsl_util.h>
#include "tools/parallel.h" #include <webrtc/common_audio/vad/include/webrtc_vad.h>
#include <webrtc/common_audio/vad/vad_core.h> #include <webrtc/common_audio/vad/vad_core.h>
using std::vector; #include <boost/range/adaptor/transformed.hpp>
#include "DcOffset.h"
#include "logging/logging.h"
#include "processing.h"
#include "SampleRateConverter.h"
#include "tools/pairs.h"
#include "tools/parallel.h"
using boost::adaptors::transformed; using boost::adaptors::transformed;
using fmt::format; using fmt::format;
using std::runtime_error; using std::runtime_error;
using std::unique_ptr; using std::unique_ptr;
using std::vector;
JoiningBoundedTimeline<void> detectVoiceActivity( JoiningBoundedTimeline<void> detectVoiceActivity(
const AudioClip& inputAudioClip, const AudioClip& inputAudioClip, ProgressSink& progressSink
ProgressSink& progressSink
) { ) {
// Prepare audio for VAD // Prepare audio for VAD
constexpr int webRtcSamplingRate = 8000; constexpr int webRtcSamplingRate = 8000;
const unique_ptr<AudioClip> audioClip = inputAudioClip.clone() const unique_ptr<AudioClip> audioClip =
| resample(webRtcSamplingRate) inputAudioClip.clone() | resample(webRtcSamplingRate) | removeDcOffset();
| removeDcOffset();
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.");
@ -46,12 +47,8 @@ JoiningBoundedTimeline<void> detectVoiceActivity(
// WebRTC is picky regarding buffer size // WebRTC is picky regarding buffer size
if (buffer.size() < frameSize) return; if (buffer.size() < frameSize) return;
const int result = WebRtcVad_Process( const int result =
vadHandle, WebRtcVad_Process(vadHandle, webRtcSamplingRate, buffer.data(), buffer.size());
webRtcSamplingRate,
buffer.data(),
buffer.size()
);
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.");
// Ignore the result of WebRtcVad_Process, instead directly interpret the internal VAD flag. // Ignore the result of WebRtcVad_Process, instead directly interpret the internal VAD flag.
@ -86,9 +83,12 @@ JoiningBoundedTimeline<void> detectVoiceActivity(
logging::debugFormat( logging::debugFormat(
"Found {} sections of voice activity: {}", "Found {} sections of voice activity: {}",
activity.size(), activity.size(),
join(activity | transformed([](const Timed<void>& t) { join(
activity | transformed([](const Timed<void>& t) {
return format("{0}-{1}", t.getStart(), t.getEnd()); return format("{0}-{1}", t.getStart(), t.getEnd());
}), ", ") }),
", "
)
); );
return activity; return activity;

View File

@ -4,6 +4,5 @@
#include "tools/progress.h" #include "tools/progress.h"
JoiningBoundedTimeline<void> detectVoiceActivity( JoiningBoundedTimeline<void> detectVoiceActivity(
const AudioClip& audioClip, const AudioClip& audioClip, ProgressSink& progressSink
ProgressSink& progressSink
); );

View File

@ -1,5 +1,7 @@
#include <fstream>
#include "waveFileWriting.h" #include "waveFileWriting.h"
#include <fstream>
#include "ioTools.h" #include "ioTools.h"
using namespace little_endian; using namespace little_endian;

View File

@ -1,7 +1,7 @@
#include "Phone.h" #include "Phone.h"
using std::string;
using boost::optional; using boost::optional;
using std::string;
PhoneConverter& PhoneConverter::get() { PhoneConverter& PhoneConverter::get() {
static PhoneConverter converter; static PhoneConverter converter;
@ -13,54 +13,24 @@ string PhoneConverter::getTypeName() {
} }
EnumConverter<Phone>::member_data PhoneConverter::getMemberData() { EnumConverter<Phone>::member_data PhoneConverter::getMemberData() {
return member_data { return member_data{{Phone::AO, "AO"}, {Phone::AA, "AA"}, {Phone::IY, "IY"},
{ Phone::AO, "AO" }, {Phone::UW, "UW"}, {Phone::EH, "EH"}, {Phone::IH, "IH"},
{ Phone::AA, "AA" }, {Phone::UH, "UH"}, {Phone::AH, "AH"}, {Phone::Schwa, "Schwa"},
{ Phone::IY, "IY" }, {Phone::AE, "AE"}, {Phone::EY, "EY"}, {Phone::AY, "AY"},
{ Phone::UW, "UW" }, {Phone::OW, "OW"}, {Phone::AW, "AW"}, {Phone::OY, "OY"},
{ Phone::EH, "EH" },
{ Phone::IH, "IH" },
{ Phone::UH, "UH" },
{ Phone::AH, "AH" },
{ Phone::Schwa, "Schwa" },
{ Phone::AE, "AE" },
{ Phone::EY, "EY" },
{ Phone::AY, "AY" },
{ Phone::OW, "OW" },
{ Phone::AW, "AW" },
{ Phone::OY, "OY" },
{Phone::ER, "ER"}, {Phone::ER, "ER"},
{ Phone::P, "P" }, {Phone::P, "P"}, {Phone::B, "B"}, {Phone::T, "T"},
{ Phone::B, "B" }, {Phone::D, "D"}, {Phone::K, "K"}, {Phone::G, "G"},
{ Phone::T, "T" }, {Phone::CH, "CH"}, {Phone::JH, "JH"}, {Phone::F, "F"},
{ Phone::D, "D" }, {Phone::V, "V"}, {Phone::TH, "TH"}, {Phone::DH, "DH"},
{ Phone::K, "K" }, {Phone::S, "S"}, {Phone::Z, "Z"}, {Phone::SH, "SH"},
{ Phone::G, "G" }, {Phone::ZH, "ZH"}, {Phone::HH, "HH"}, {Phone::M, "M"},
{ Phone::CH, "CH" }, {Phone::N, "N"}, {Phone::NG, "NG"}, {Phone::L, "L"},
{ Phone::JH, "JH" }, {Phone::R, "R"}, {Phone::Y, "Y"}, {Phone::W, "W"},
{ Phone::F, "F" },
{ Phone::V, "V" },
{ Phone::TH, "TH" },
{ Phone::DH, "DH" },
{ Phone::S, "S" },
{ Phone::Z, "Z" },
{ Phone::SH, "SH" },
{ Phone::ZH, "ZH" },
{ Phone::HH, "HH" },
{ Phone::M, "M" },
{ Phone::N, "N" },
{ Phone::NG, "NG" },
{ Phone::L, "L" },
{ Phone::R, "R" },
{ Phone::Y, "Y" },
{ Phone::W, "W" },
{ Phone::Breath, "Breath" }, {Phone::Breath, "Breath"}, {Phone::Cough, "Cough"}, {Phone::Smack, "Smack"},
{ Phone::Cough, "Cough" }, {Phone::Noise, "Noise"}};
{ Phone::Smack, "Smack" },
{ Phone::Noise, "Noise" }
};
} }
optional<Phone> PhoneConverter::tryParse(const string& s) { optional<Phone> PhoneConverter::tryParse(const string& s) {

View File

@ -81,9 +81,11 @@ enum class Phone {
class PhoneConverter : public EnumConverter<Phone> { class PhoneConverter : public EnumConverter<Phone> {
public: public:
static PhoneConverter& get(); static PhoneConverter& get();
protected: protected:
std::string getTypeName() override; std::string getTypeName() override;
member_data getMemberData() override; member_data getMemberData() override;
public: public:
boost::optional<Phone> tryParse(const std::string& s) override; boost::optional<Phone> tryParse(const std::string& s) override;
}; };

View File

@ -1,7 +1,7 @@
#include "Shape.h" #include "Shape.h"
using std::string;
using std::set; using std::set;
using std::string;
ShapeConverter& ShapeConverter::get() { ShapeConverter& ShapeConverter::get() {
static ShapeConverter converter; static ShapeConverter converter;
@ -22,7 +22,9 @@ set<Shape> ShapeConverter::getBasicShapes() {
set<Shape> ShapeConverter::getExtendedShapes() { set<Shape> ShapeConverter::getExtendedShapes() {
static const set<Shape> result = [] { static const set<Shape> result = [] {
set<Shape> result; set<Shape> result;
for (int i = static_cast<int>(Shape::LastBasicShape) + 1; i < static_cast<int>(Shape::EndSentinel); ++i) { for (int i = static_cast<int>(Shape::LastBasicShape) + 1;
i < static_cast<int>(Shape::EndSentinel);
++i) {
result.insert(static_cast<Shape>(i)); result.insert(static_cast<Shape>(i));
} }
return result; return result;

View File

@ -1,8 +1,9 @@
#pragma once #pragma once
#include "tools/EnumConverter.h"
#include <set> #include <set>
#include "tools/EnumConverter.h"
// The classic Hanna-Barbera mouth shapes A-F plus the common supplements G-H // The classic Hanna-Barbera mouth shapes A-F plus the common supplements G-H
// For reference, see http://sunewatts.dk/lipsync/lipsync/article_02.php // For reference, see http://sunewatts.dk/lipsync/lipsync/article_02.php
// For visual examples, see https://flic.kr/s/aHsj86KR4J. Their shapes "BMP".."L" map to A..H. // For visual examples, see https://flic.kr/s/aHsj86KR4J. Their shapes "BMP".."L" map to A..H.
@ -31,6 +32,7 @@ public:
static ShapeConverter& get(); static ShapeConverter& get();
static std::set<Shape> getBasicShapes(); static std::set<Shape> getBasicShapes();
static 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;

View File

@ -1,12 +1,16 @@
#include "DatExporter.h" #include "DatExporter.h"
#include "animation/targetShapeSet.h"
#include <boost/lexical_cast.hpp> #include <boost/lexical_cast.hpp>
#include "animation/targetShapeSet.h"
using std::string;
using std::chrono::duration; using std::chrono::duration;
using std::chrono::duration_cast; using std::chrono::duration_cast;
using std::string;
DatExporter::DatExporter(const ShapeSet& targetShapeSet, double frameRate, bool convertToPrestonBlair) : DatExporter::DatExporter(
const ShapeSet& targetShapeSet, double frameRate, bool convertToPrestonBlair
) :
frameRate(frameRate), frameRate(frameRate),
convertToPrestonBlair(convertToPrestonBlair), convertToPrestonBlair(convertToPrestonBlair),
prestonBlairShapeNames{ prestonBlairShapeNames{
@ -19,8 +23,7 @@ DatExporter::DatExporter(const ShapeSet& targetShapeSet, double frameRate, bool
{Shape::G, "FV"}, {Shape::G, "FV"},
{Shape::H, "L"}, {Shape::H, "L"},
{Shape::X, "rest"}, {Shape::X, "rest"},
} } {
{
// Animation works with a fixed frame rate of 100. // Animation works with a fixed frame rate of 100.
// Downsampling to much less than 25 fps may result in dropped frames. // Downsampling to much less than 25 fps may result in dropped frames.
// Upsampling to more than 100 fps doesn't make sense. // Upsampling to more than 100 fps doesn't make sense.
@ -28,13 +31,17 @@ DatExporter::DatExporter(const ShapeSet& targetShapeSet, double frameRate, bool
const double maxFrameRate = 100.0; const double maxFrameRate = 100.0;
if (frameRate < minFrameRate || frameRate > maxFrameRate) { if (frameRate < minFrameRate || frameRate > maxFrameRate) {
throw std::runtime_error(fmt::format("Frame rate must be between {} and {} fps.", minFrameRate, maxFrameRate)); throw std::runtime_error(
fmt::format("Frame rate must be between {} and {} fps.", minFrameRate, maxFrameRate)
);
} }
if (convertToPrestonBlair) { if (convertToPrestonBlair) {
for (Shape shape : targetShapeSet) { for (Shape shape : targetShapeSet) {
if (prestonBlairShapeNames.find(shape) == prestonBlairShapeNames.end()) { if (prestonBlairShapeNames.find(shape) == prestonBlairShapeNames.end()) {
throw std::runtime_error(fmt::format("Mouth shape {} cannot be converted to Preston Blair shape names.")); throw std::runtime_error(
fmt::format("Mouth shape {} cannot be converted to Preston Blair shape names.")
);
} }
} }
} }
@ -62,8 +69,7 @@ void DatExporter::exportAnimation(const ExporterInput& input, std::ostream& outp
} }
string DatExporter::toString(Shape shape) const { string DatExporter::toString(Shape shape) const {
return convertToPrestonBlair return convertToPrestonBlair ? prestonBlairShapeNames.at(shape)
? prestonBlairShapeNames.at(shape)
: boost::lexical_cast<std::string>(shape); : boost::lexical_cast<std::string>(shape);
} }

View File

@ -1,10 +1,11 @@
#pragma once #pragma once
#include "Exporter.h"
#include "core/Shape.h"
#include <map> #include <map>
#include <string> #include <string>
#include "core/Shape.h"
#include "Exporter.h"
// Exporter for Moho's switch data file format // Exporter for Moho's switch data file format
class DatExporter : public Exporter { class DatExporter : public Exporter {
public: public:

View File

@ -1,15 +1,17 @@
#pragma once #pragma once
#include <filesystem>
#include "core/Shape.h" #include "core/Shape.h"
#include "time/ContinuousTimeline.h" #include "time/ContinuousTimeline.h"
#include <filesystem>
class ExporterInput { class ExporterInput {
public: public:
ExporterInput( ExporterInput(
const std::filesystem::path& inputFilePath, const std::filesystem::path& inputFilePath,
const JoiningContinuousTimeline<Shape>& animation, const JoiningContinuousTimeline<Shape>& animation,
const ShapeSet& targetShapeSet) : const ShapeSet& targetShapeSet
) :
inputFilePath(inputFilePath), inputFilePath(inputFilePath),
animation(animation), animation(animation),
targetShapeSet(targetShapeSet) {} targetShapeSet(targetShapeSet) {}
@ -22,5 +24,6 @@ public:
class Exporter { class Exporter {
public: public:
virtual ~Exporter() {} virtual ~Exporter() {}
virtual void exportAnimation(const ExporterInput& input, std::ostream& outputStream) = 0; virtual void exportAnimation(const ExporterInput& input, std::ostream& outputStream) = 0;
}; };

View File

@ -1,4 +1,5 @@
#include "JsonExporter.h" #include "JsonExporter.h"
#include "exporterTools.h" #include "exporterTools.h"
#include "tools/stringTools.h" #include "tools/stringTools.h"
@ -10,8 +11,10 @@ void JsonExporter::exportAnimation(const ExporterInput& input, std::ostream& out
// the formatting. // the formatting.
outputStream << "{\n"; outputStream << "{\n";
outputStream << " \"metadata\": {\n"; outputStream << " \"metadata\": {\n";
outputStream << " \"soundFile\": \"" << escapeJsonString(absolute(input.inputFilePath).u8string()) << "\",\n"; outputStream << " \"soundFile\": \""
outputStream << " \"duration\": " << formatDuration(input.animation.getRange().getDuration()) << "\n"; << escapeJsonString(absolute(input.inputFilePath).u8string()) << "\",\n";
outputStream << " \"duration\": " << formatDuration(input.animation.getRange().getDuration())
<< "\n";
outputStream << " },\n"; outputStream << " },\n";
outputStream << " \"mouthCues\": [\n"; outputStream << " \"mouthCues\": [\n";
bool isFirst = true; bool isFirst = true;
@ -19,8 +22,8 @@ void JsonExporter::exportAnimation(const ExporterInput& input, std::ostream& out
if (!isFirst) outputStream << ",\n"; if (!isFirst) outputStream << ",\n";
isFirst = false; isFirst = false;
outputStream << " { \"start\": " << formatDuration(timedShape.getStart()) outputStream << " { \"start\": " << formatDuration(timedShape.getStart())
<< ", \"end\": " << formatDuration(timedShape.getEnd()) << ", \"end\": " << formatDuration(timedShape.getEnd()) << ", \"value\": \""
<< ", \"value\": \"" << timedShape.getValue() << "\" }"; << timedShape.getValue() << "\" }";
} }
outputStream << "\n"; outputStream << "\n";
outputStream << " ]\n"; outputStream << " ]\n";

View File

@ -1,20 +1,15 @@
#include "TsvExporter.h" #include "TsvExporter.h"
#include "animation/targetShapeSet.h" #include "animation/targetShapeSet.h"
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 outputStream << formatDuration(timedShape.getStart()) << "\t" << timedShape.getValue()
<< formatDuration(timedShape.getStart())
<< "\t"
<< timedShape.getValue()
<< "\n"; << "\n";
} }
// Output closed mouth with end time // Output closed mouth with end time
outputStream outputStream << formatDuration(input.animation.getRange().getEnd()) << "\t"
<< formatDuration(input.animation.getRange().getEnd()) << convertToTargetShapeSet(Shape::X, input.targetShapeSet) << "\n";
<< "\t"
<< convertToTargetShapeSet(Shape::X, input.targetShapeSet)
<< "\n";
} }

View File

@ -6,4 +6,3 @@ class TsvExporter : public Exporter {
public: public:
void exportAnimation(const ExporterInput& input, std::ostream& outputStream) override; void exportAnimation(const ExporterInput& input, std::ostream& outputStream) override;
}; };

View File

@ -1,11 +1,13 @@
#include "XmlExporter.h" #include "XmlExporter.h"
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp> #include <boost/property_tree/xml_parser.hpp>
#include <boost/version.hpp> #include <boost/version.hpp>
#include "exporterTools.h" #include "exporterTools.h"
using std::string;
using boost::property_tree::ptree; using boost::property_tree::ptree;
using std::string;
void XmlExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) { void XmlExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
ptree tree; ptree tree;
@ -13,16 +15,13 @@ void XmlExporter::exportAnimation(const ExporterInput& input, std::ostream& outp
// Add metadata // Add metadata
tree.put("rhubarbResult.metadata.soundFile", absolute(input.inputFilePath).u8string()); tree.put("rhubarbResult.metadata.soundFile", absolute(input.inputFilePath).u8string());
tree.put( tree.put(
"rhubarbResult.metadata.duration", "rhubarbResult.metadata.duration", formatDuration(input.animation.getRange().getDuration())
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( ptree& mouthCueElement =
"rhubarbResult.mouthCues.mouthCue", tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue());
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()));
} }

View File

@ -1,10 +1,10 @@
#include "exporterTools.h" #include "exporterTools.h"
#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( std::vector<Timed<Shape>> dummyShapeIfEmpty(
const JoiningTimeline<Shape>& animation, const JoiningTimeline<Shape>& animation, const ShapeSet& targetShapeSet
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));

View File

@ -5,6 +5,5 @@
// Makes sure there is at least one mouth shape // Makes sure there is at least one mouth shape
std::vector<Timed<Shape>> dummyShapeIfEmpty( std::vector<Timed<Shape>> dummyShapeIfEmpty(
const JoiningTimeline<Shape>& animation, const JoiningTimeline<Shape>& animation, const ShapeSet& targetShapeSet
const ShapeSet& targetShapeSet
); );

View File

@ -1,8 +1,9 @@
#include "rhubarbLib.h" #include "rhubarbLib.h"
#include "core/Phone.h"
#include "tools/textFiles.h"
#include "animation/mouthAnimation.h" #include "animation/mouthAnimation.h"
#include "audio/audioFileReading.h" #include "audio/audioFileReading.h"
#include "core/Phone.h"
#include "tools/textFiles.h"
using boost::optional; using boost::optional;
using std::string; using std::string;
@ -14,8 +15,8 @@ JoiningContinuousTimeline<Shape> animateAudioClip(
const Recognizer& recognizer, const Recognizer& recognizer,
const ShapeSet& targetShapeSet, const ShapeSet& targetShapeSet,
int maxThreadCount, int maxThreadCount,
ProgressSink& progressSink) ProgressSink& progressSink
{ ) {
const BoundedTimeline<Phone> phones = const BoundedTimeline<Phone> phones =
recognizer.recognizePhones(audioClip, dialog, maxThreadCount, progressSink); recognizer.recognizePhones(audioClip, dialog, maxThreadCount, progressSink);
JoiningContinuousTimeline<Shape> result = animate(phones, targetShapeSet); JoiningContinuousTimeline<Shape> result = animate(phones, targetShapeSet);
@ -28,8 +29,10 @@ JoiningContinuousTimeline<Shape> animateWaveFile(
const Recognizer& recognizer, const Recognizer& recognizer,
const ShapeSet& targetShapeSet, const ShapeSet& targetShapeSet,
int maxThreadCount, int maxThreadCount,
ProgressSink& progressSink) ProgressSink& progressSink
{ ) {
const auto audioClip = createAudioFileClip(filePath); const auto audioClip = createAudioFileClip(filePath);
return animateAudioClip(*audioClip, dialog, recognizer, targetShapeSet, maxThreadCount, progressSink); return animateAudioClip(
*audioClip, dialog, recognizer, targetShapeSet, maxThreadCount, progressSink
);
} }

View File

@ -1,12 +1,13 @@
#pragma once #pragma once
#include "core/Shape.h"
#include "time/ContinuousTimeline.h"
#include "audio/AudioClip.h"
#include "tools/progress.h"
#include <filesystem> #include <filesystem>
#include "animation/targetShapeSet.h" #include "animation/targetShapeSet.h"
#include "audio/AudioClip.h"
#include "core/Shape.h"
#include "recognition/Recognizer.h" #include "recognition/Recognizer.h"
#include "time/ContinuousTimeline.h"
#include "tools/progress.h"
JoiningContinuousTimeline<Shape> animateAudioClip( JoiningContinuousTimeline<Shape> animateAudioClip(
const AudioClip& audioClip, const AudioClip& audioClip,
@ -14,7 +15,8 @@ JoiningContinuousTimeline<Shape> animateAudioClip(
const Recognizer& recognizer, const Recognizer& recognizer,
const ShapeSet& targetShapeSet, const ShapeSet& targetShapeSet,
int maxThreadCount, int maxThreadCount,
ProgressSink& progressSink); ProgressSink& progressSink
);
JoiningContinuousTimeline<Shape> animateWaveFile( JoiningContinuousTimeline<Shape> animateWaveFile(
std::filesystem::path filePath, std::filesystem::path filePath,
@ -22,4 +24,5 @@ JoiningContinuousTimeline<Shape> animateWaveFile(
const Recognizer& recognizer, const Recognizer& recognizer,
const ShapeSet& targetShapeSet, const ShapeSet& targetShapeSet,
int maxThreadCount, int maxThreadCount,
ProgressSink& progressSink); ProgressSink& progressSink
);

View File

@ -1,12 +1,12 @@
#include "Entry.h" #include "Entry.h"
#include <thread>
#include <mutex> #include <mutex>
#include <thread>
#include <unordered_map> #include <unordered_map>
using std::lock_guard; using std::lock_guard;
using std::unordered_map;
using std::string; using std::string;
using std::unordered_map;
namespace logging { namespace logging {
@ -30,10 +30,9 @@ namespace logging {
Entry::Entry(Level level, const string& message) : Entry::Entry(Level level, const string& message) :
timestamp(), timestamp(),
level(level), level(level),
message(message) message(message) {
{
time(&timestamp); time(&timestamp);
this->threadCounter = getThreadCounter(); this->threadCounter = getThreadCounter();
} }
} } // namespace logging

View File

@ -14,4 +14,4 @@ namespace logging {
std::string message; std::string message;
}; };
} } // namespace logging

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include "Entry.h" #include "Entry.h"
namespace logging { namespace logging {
@ -11,4 +12,4 @@ namespace logging {
virtual std::string format(const Entry& entry) = 0; virtual std::string format(const Entry& entry) = 0;
}; };
} } // namespace logging

View File

@ -32,4 +32,4 @@ namespace logging {
return LevelConverter::get().read(stream, value); return LevelConverter::get().read(stream, value);
} }
} } // namespace logging

View File

@ -4,19 +4,12 @@
namespace logging { namespace logging {
enum class Level { enum class Level { Trace, Debug, Info, Warn, Error, Fatal, EndSentinel };
Trace,
Debug,
Info,
Warn,
Error,
Fatal,
EndSentinel
};
class LevelConverter : public EnumConverter<Level> { class LevelConverter : public EnumConverter<Level> {
public: public:
static LevelConverter& get(); static LevelConverter& get();
protected: protected:
std::string getTypeName() override; std::string getTypeName() override;
member_data getMemberData() override; member_data getMemberData() override;
@ -26,4 +19,4 @@ namespace logging {
std::istream& operator>>(std::istream& stream, Level& value); std::istream& operator>>(std::istream& stream, Level& value);
} } // namespace logging

View File

@ -10,4 +10,4 @@ namespace logging {
virtual void receive(const Entry& entry) = 0; virtual void receive(const Entry& entry) = 0;
}; };
} } // namespace logging

View File

@ -1,5 +1,7 @@
#include "formatters.h" #include "formatters.h"
#include <format.h> #include <format.h>
#include "Entry.h" #include "Entry.h"
#include "tools/tools.h" #include "tools/tools.h"
@ -20,4 +22,4 @@ namespace logging {
); );
} }
} } // namespace logging

View File

@ -12,8 +12,9 @@ namespace logging {
class SimpleFileFormatter : public Formatter { class SimpleFileFormatter : public Formatter {
public: public:
std::string format(const Entry& entry) override; std::string format(const Entry& entry) override;
private: private:
SimpleConsoleFormatter consoleFormatter; SimpleConsoleFormatter consoleFormatter;
}; };
} } // namespace logging

View File

@ -1,13 +1,15 @@
#include "logging.h" #include "logging.h"
#include "tools/tools.h"
#include <mutex> #include <mutex>
#include "Entry.h" #include "Entry.h"
#include "tools/tools.h"
using namespace logging; using namespace logging;
using std::lock_guard;
using std::shared_ptr;
using std::string; using std::string;
using std::vector; using std::vector;
using std::shared_ptr;
using std::lock_guard;
std::mutex& getLogMutex() { std::mutex& getLogMutex() {
static std::mutex mutex; static std::mutex mutex;

View File

@ -1,8 +1,8 @@
#pragma once #pragma once
#include "tools/EnumConverter.h"
#include "Sink.h"
#include "Level.h" #include "Level.h"
#include "Sink.h"
#include "tools/EnumConverter.h"
namespace logging { namespace logging {
@ -34,4 +34,4 @@ namespace logging {
LOG_WITH_LEVEL(warn, Warn) LOG_WITH_LEVEL(warn, Warn)
LOG_WITH_LEVEL(error, Error) LOG_WITH_LEVEL(error, Error)
LOG_WITH_LEVEL(fatal, Fatal) LOG_WITH_LEVEL(fatal, Fatal)
} } // namespace logging

View File

@ -1,16 +1,17 @@
#include "sinks.h" #include "sinks.h"
#include <iostream> #include <iostream>
#include "Entry.h" #include "Entry.h"
using std::string;
using std::shared_ptr; using std::shared_ptr;
using std::string;
namespace logging { namespace logging {
LevelFilter::LevelFilter(shared_ptr<Sink> innerSink, Level minLevel) : LevelFilter::LevelFilter(shared_ptr<Sink> innerSink, Level minLevel) :
innerSink(innerSink), innerSink(innerSink),
minLevel(minLevel) minLevel(minLevel) {}
{}
void LevelFilter::receive(const Entry& entry) { void LevelFilter::receive(const Entry& entry) {
if (entry.level >= minLevel) { if (entry.level >= minLevel) {
@ -20,8 +21,7 @@ namespace logging {
StreamSink::StreamSink(shared_ptr<std::ostream> stream, shared_ptr<Formatter> formatter) : StreamSink::StreamSink(shared_ptr<std::ostream> stream, shared_ptr<Formatter> formatter) :
stream(stream), stream(stream),
formatter(formatter) formatter(formatter) {}
{}
void StreamSink::receive(const Entry& entry) { void StreamSink::receive(const Entry& entry) {
const string line = formatter->format(entry); const string line = formatter->format(entry);
@ -29,7 +29,6 @@ namespace logging {
} }
StdErrSink::StdErrSink(shared_ptr<Formatter> formatter) : StdErrSink::StdErrSink(shared_ptr<Formatter> formatter) :
StreamSink(std::shared_ptr<std::ostream>(&std::cerr, [](void*) {}), formatter) StreamSink(std::shared_ptr<std::ostream>(&std::cerr, [](void*) {}), formatter) {}
{}
} } // namespace logging

View File

@ -1,8 +1,9 @@
#pragma once #pragma once
#include "Sink.h"
#include <memory> #include <memory>
#include "Formatter.h" #include "Formatter.h"
#include "Sink.h"
namespace logging { namespace logging {
enum class Level; enum class Level;
@ -11,6 +12,7 @@ namespace logging {
public: public:
LevelFilter(std::shared_ptr<Sink> innerSink, Level minLevel); LevelFilter(std::shared_ptr<Sink> innerSink, Level minLevel);
void receive(const Entry& entry) override; void receive(const Entry& entry) override;
private: private:
std::shared_ptr<Sink> innerSink; std::shared_ptr<Sink> innerSink;
Level minLevel; Level minLevel;
@ -20,6 +22,7 @@ namespace logging {
public: public:
StreamSink(std::shared_ptr<std::ostream> stream, std::shared_ptr<Formatter> formatter); StreamSink(std::shared_ptr<std::ostream> stream, std::shared_ptr<Formatter> formatter);
void receive(const Entry& entry) override; void receive(const Entry& entry) override;
private: private:
std::shared_ptr<std::ostream> stream; std::shared_ptr<std::ostream> stream;
std::shared_ptr<Formatter> formatter; std::shared_ptr<Formatter> formatter;
@ -30,4 +33,4 @@ namespace logging {
explicit StdErrSink(std::shared_ptr<Formatter> formatter); explicit StdErrSink(std::shared_ptr<Formatter> formatter);
}; };
} } // namespace logging

View File

@ -1,52 +1,66 @@
#include "PhoneticRecognizer.h" #include "PhoneticRecognizer.h"
#include "time/Timeline.h"
#include "audio/AudioSegment.h"
#include "audio/SampleRateConverter.h"
#include "audio/processing.h"
#include "time/timedLogging.h"
using std::runtime_error; #include "audio/AudioSegment.h"
using std::unique_ptr; #include "audio/processing.h"
using std::string; #include "audio/SampleRateConverter.h"
#include "time/timedLogging.h"
#include "time/Timeline.h"
using boost::optional; using boost::optional;
using std::runtime_error;
using std::string;
using std::unique_ptr;
static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialog) { static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialog) {
UNUSED(dialog); UNUSED(dialog);
lambda_unique_ptr<cmd_ln_t> config( lambda_unique_ptr<cmd_ln_t> config(
cmd_ln_init( cmd_ln_init(
nullptr, ps_args(), true, nullptr,
ps_args(),
true,
// Set acoustic model // Set acoustic model
"-hmm", (getSphinxModelDirectory() / "acoustic-model").u8string().c_str(), "-hmm",
(getSphinxModelDirectory() / "acoustic-model").u8string().c_str(),
// Set phonetic language model // Set phonetic language model
"-allphone", (getSphinxModelDirectory() / "en-us-phone.lm.bin").u8string().c_str(), "-allphone",
"-allphone_ci", "yes", (getSphinxModelDirectory() / "en-us-phone.lm.bin").u8string().c_str(),
"-allphone_ci",
"yes",
// Set language model probability weight. // Set language model probability weight.
// Low values (<= 0.4) can lead to fluttering animation. // Low values (<= 0.4) can lead to fluttering animation.
// 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",
// Add noise against zero silence // Add noise against zero silence
// (see http://cmusphinx.sourceforge.net/wiki/faq#qwhy_my_accuracy_is_poor) // (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",
// Perform per-utterance cepstral mean normalization // Perform per-utterance cepstral mean normalization
"-cmn", "batch", "-cmn",
"batch",
// The following settings are recommended at // The following settings are recommended at
// http://cmusphinx.sourceforge.net/wiki/phonemerecognition // 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",
// Set beam width applied to phone transitions // Set beam width applied to phone transitions
"-pbeam", "1e-20", "-pbeam",
nullptr), "1e-20",
[](cmd_ln_t* config) { cmd_ln_free_r(config); }); nullptr
),
[](cmd_ln_t* config) { cmd_ln_free_r(config); }
);
if (!config) throw runtime_error("Error creating configuration."); if (!config) throw runtime_error("Error creating configuration.");
lambda_unique_ptr<ps_decoder_t> decoder( lambda_unique_ptr<ps_decoder_t> decoder(ps_init(config.get()), [](ps_decoder_t* recognizer) {
ps_init(config.get()), ps_free(recognizer);
[](ps_decoder_t* recognizer) { ps_free(recognizer); }); });
if (!decoder) throw runtime_error("Error creating speech decoder."); if (!decoder) throw runtime_error("Error creating speech decoder.");
return decoder; return decoder;
@ -64,9 +78,8 @@ 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() const unique_ptr<AudioClip> clipSegment =
| segment(paddedTimeRange) audioClip.clone() | segment(paddedTimeRange) | resample(sphinxSampleRate);
| resample(sphinxSampleRate);
const auto audioBuffer = copyTo16bitBuffer(*clipSegment); const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
// Detect phones (returned as words) // Detect phones (returned as words)
@ -109,5 +122,7 @@ BoundedTimeline<Phone> PhoneticRecognizer::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
);
} }

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "Recognizer.h"
#include "pocketSphinxTools.h" #include "pocketSphinxTools.h"
#include "Recognizer.h"
class PhoneticRecognizer : public Recognizer { class PhoneticRecognizer : public Recognizer {
public: public:

View File

@ -1,30 +1,33 @@
#include "PocketSphinxRecognizer.h" #include "PocketSphinxRecognizer.h"
#include <regex>
#include <gsl_util.h> #include <gsl_util.h>
#include <regex>
#include "audio/AudioSegment.h" #include "audio/AudioSegment.h"
#include "audio/SampleRateConverter.h"
#include "languageModels.h"
#include "tokenization.h"
#include "g2p.h"
#include "time/ContinuousTimeline.h"
#include "audio/processing.h" #include "audio/processing.h"
#include "audio/SampleRateConverter.h"
#include "g2p.h"
#include "languageModels.h"
#include "time/ContinuousTimeline.h"
#include "time/timedLogging.h" #include "time/timedLogging.h"
#include "tokenization.h"
extern "C" { extern "C" {
#include <state_align_search.h> #include <state_align_search.h>
} }
using std::runtime_error;
using std::invalid_argument;
using std::unique_ptr;
using std::string;
using std::vector;
using std::map;
using std::filesystem::path;
using std::regex;
using std::regex_replace;
using boost::optional; using boost::optional;
using std::array; using std::array;
using std::invalid_argument;
using std::map;
using std::regex;
using std::regex_replace;
using std::runtime_error;
using std::string;
using std::unique_ptr;
using std::vector;
using std::filesystem::path;
bool dictionaryContains(dict_t& dictionary, const string& word) { bool dictionaryContains(dict_t& dictionary, const string& word) {
return dict_wordid(&dictionary, word.c_str()) != BAD_S3WID; return dict_wordid(&dictionary, word.c_str()) != BAD_S3WID;
@ -50,7 +53,9 @@ void addMissingDictionaryWords(const vector<string>& words, ps_decoder_t& decode
} }
for (auto it = missingPronunciations.begin(); it != missingPronunciations.end(); ++it) { for (auto it = missingPronunciations.begin(); it != missingPronunciations.end(); ++it) {
const bool isLast = it == --missingPronunciations.end(); const bool isLast = it == --missingPronunciations.end();
logging::infoFormat("Unknown word '{}'. Guessing pronunciation '{}'.", it->first, it->second); logging::infoFormat(
"Unknown word '{}'. Guessing pronunciation '{}'.", it->first, it->second
);
ps_add_word(&decoder, it->first.c_str(), it->second.c_str(), isLast); ps_add_word(&decoder, it->first.c_str(), it->second.c_str(), isLast);
} }
} }
@ -59,23 +64,24 @@ lambda_unique_ptr<ngram_model_t> createDefaultLanguageModel(ps_decoder_t& decode
path modelPath = getSphinxModelDirectory() / "en-us.lm.bin"; path modelPath = getSphinxModelDirectory() / "en-us.lm.bin";
lambda_unique_ptr<ngram_model_t> result( lambda_unique_ptr<ngram_model_t> result(
ngram_model_read(decoder.config, modelPath.u8string().c_str(), NGRAM_AUTO, decoder.lmath), ngram_model_read(decoder.config, modelPath.u8string().c_str(), NGRAM_AUTO, decoder.lmath),
[](ngram_model_t* lm) { ngram_model_free(lm); }); [](ngram_model_t* lm) { ngram_model_free(lm); }
);
if (!result) { if (!result) {
throw runtime_error(fmt::format("Error reading language model from {}.", modelPath.u8string())); throw runtime_error(
fmt::format("Error reading language model from {}.", modelPath.u8string())
);
} }
return result; return result;
} }
lambda_unique_ptr<ngram_model_t> createDialogLanguageModel( lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(
ps_decoder_t& decoder, ps_decoder_t& decoder, const string& dialog
const string& dialog
) { ) {
// Split dialog into normalized words // Split dialog into normalized words
vector<string> words = tokenizeText( vector<string> words = tokenizeText(dialog, [&](const string& word) {
dialog, return dictionaryContains(*decoder.dict, word);
[&](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);
@ -87,15 +93,13 @@ lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(
} }
lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel( lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(
ps_decoder_t& decoder, ps_decoder_t& decoder, const string& dialog
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{ array<ngram_model_t*, modelCount> languageModels{
defaultLanguageModel.get(), defaultLanguageModel.get(), dialogLanguageModel.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};
@ -107,7 +111,8 @@ lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(
modelWeights.data(), modelWeights.data(),
modelCount 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.");
} }
@ -118,31 +123,40 @@ lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(
static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialog) { static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialog) {
lambda_unique_ptr<cmd_ln_t> config( lambda_unique_ptr<cmd_ln_t> config(
cmd_ln_init( cmd_ln_init(
nullptr, ps_args(), true, nullptr,
ps_args(),
true,
// Set acoustic model // Set acoustic model
"-hmm", (getSphinxModelDirectory() / "acoustic-model").u8string().c_str(), "-hmm",
(getSphinxModelDirectory() / "acoustic-model").u8string().c_str(),
// Set pronunciation dictionary // Set pronunciation dictionary
"-dict", (getSphinxModelDirectory() / "cmudict-en-us.dict").u8string().c_str(), "-dict",
(getSphinxModelDirectory() / "cmudict-en-us.dict").u8string().c_str(),
// Add noise against zero silence // Add noise against zero silence
// (see http://cmusphinx.sourceforge.net/wiki/faq#qwhy_my_accuracy_is_poor) // (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",
// Perform per-utterance cepstral mean normalization // Perform per-utterance cepstral mean normalization
"-cmn", "batch", "-cmn",
nullptr), "batch",
[](cmd_ln_t* config) { cmd_ln_free_r(config); }); nullptr
),
[](cmd_ln_t* config) { cmd_ln_free_r(config); }
);
if (!config) throw runtime_error("Error creating configuration."); if (!config) throw runtime_error("Error creating configuration.");
lambda_unique_ptr<ps_decoder_t> decoder( lambda_unique_ptr<ps_decoder_t> decoder(ps_init(config.get()), [](ps_decoder_t* recognizer) {
ps_init(config.get()), ps_free(recognizer);
[](ps_decoder_t* recognizer) { ps_free(recognizer); }); });
if (!decoder) throw runtime_error("Error creating speech decoder."); if (!decoder) throw runtime_error("Error creating speech decoder.");
// Set language model // Set language model
lambda_unique_ptr<ngram_model_t> languageModel(dialog lambda_unique_ptr<ngram_model_t> languageModel(
? createBiasedLanguageModel(*decoder, *dialog) dialog ? createBiasedLanguageModel(*decoder, *dialog) : createDefaultLanguageModel(*decoder)
: createDefaultLanguageModel(*decoder)); );
ps_set_lm(decoder.get(), "lm", languageModel.get()); ps_set_lm(decoder.get(), "lm", languageModel.get());
ps_set_search(decoder.get(), "lm"); ps_set_search(decoder.get(), "lm");
@ -150,16 +164,15 @@ static lambda_unique_ptr<ps_decoder_t> createDecoder(optional<std::string> dialo
} }
optional<Timeline<Phone>> getPhoneAlignment( optional<Timeline<Phone>> getPhoneAlignment(
const vector<s3wid_t>& wordIds, const vector<s3wid_t>& wordIds, const vector<int16_t>& audioBuffer, ps_decoder_t& decoder
const vector<int16_t>& audioBuffer, ) {
ps_decoder_t& decoder)
{
if (wordIds.empty()) return boost::none; if (wordIds.empty()) return boost::none;
// Create alignment list // Create alignment list
lambda_unique_ptr<ps_alignment_t> alignment( lambda_unique_ptr<ps_alignment_t> alignment(
ps_alignment_init(decoder.d2p), ps_alignment_init(decoder.d2p),
[](ps_alignment_t* alignment) { ps_alignment_free(alignment); }); [](ps_alignment_t* alignment) { ps_alignment_free(alignment); }
);
if (!alignment) throw runtime_error("Error creating alignment."); if (!alignment) throw runtime_error("Error creating alignment.");
for (s3wid_t wordId : wordIds) { for (s3wid_t wordId : wordIds) {
// Add word. Initial value for duration is ignored. // Add word. Initial value for duration is ignored.
@ -172,7 +185,8 @@ optional<Timeline<Phone>> getPhoneAlignment(
acmod_t* acousticModel = decoder.acmod; acmod_t* acousticModel = decoder.acmod;
lambda_unique_ptr<ps_search_t> search( lambda_unique_ptr<ps_search_t> search(
state_align_search_init("state_align", decoder.config, acousticModel, alignment.get()), state_align_search_init("state_align", decoder.config, acousticModel, alignment.get()),
[](ps_search_t* search) { ps_search_free(search); }); [](ps_search_t* search) { ps_search_free(search); }
);
if (!search) throw runtime_error("Error creating search."); if (!search) throw runtime_error("Error creating search.");
// Start recognition // Start recognition
@ -190,7 +204,8 @@ optional<Timeline<Phone>> getPhoneAlignment(
const int16* nextSample = audioBuffer.data(); const int16* nextSample = audioBuffer.data();
size_t remainingSamples = audioBuffer.size(); size_t remainingSamples = audioBuffer.size();
const bool fullUtterance = true; const bool fullUtterance = true;
while (acmod_process_raw(acousticModel, &nextSample, &remainingSamples, fullUtterance) > 0) { while (acmod_process_raw(acousticModel, &nextSample, &remainingSamples, fullUtterance) > 0
) {
while (acousticModel->n_feat_frame > 0) { while (acousticModel->n_feat_frame > 0) {
ps_search_step(search.get(), acousticModel->output_frame); ps_search_step(search.get(), acousticModel->output_frame);
acmod_advance(acousticModel); acmod_advance(acousticModel);
@ -205,11 +220,8 @@ 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 ( for (ps_alignment_iter_t* it = ps_alignment_phones(alignment.get()); it;
ps_alignment_iter_t* it = ps_alignment_phones(alignment.get()); it = ps_alignment_iter_next(it)) {
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;
@ -231,8 +243,8 @@ optional<Timeline<Phone>> getPhoneAlignment(
return result; return result;
} }
// Some words have multiple pronunciations, one of which results in better animation than the others. // Some words have multiple pronunciations, one of which results in better animation than the
// This function returns the optimal pronunciation for a select set of these words. // others. This function returns the optimal pronunciation for a select set of these words.
string fixPronunciation(const string& word) { string fixPronunciation(const string& word) {
const static map<string, string> replacements{ const static map<string, string> replacements{
{"into(2)", "into"}, {"into(2)", "into"},
@ -265,9 +277,8 @@ 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() const unique_ptr<AudioClip> clipSegment =
| segment(paddedTimeRange) audioClip.clone() | segment(paddedTimeRange) | resample(sphinxSampleRate);
| resample(sphinxSampleRate);
const auto audioBuffer = copyTo16bitBuffer(*clipSegment); const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
// Get words // Get words
@ -307,7 +318,8 @@ static Timeline<Phone> utteranceToPhones(
#if BOOST_VERSION < 105600 // Support legacy syntax #if BOOST_VERSION < 105600 // Support legacy syntax
#define value_or get_value_or #define value_or get_value_or
#endif #endif
Timeline<Phone> utterancePhones = getPhoneAlignment(wordIds, audioBuffer, decoder) Timeline<Phone> utterancePhones =
getPhoneAlignment(wordIds, audioBuffer, decoder)
.value_or(ContinuousTimeline<Phone>(clipSegment->getTruncatedRange(), Phone::Noise)); .value_or(ContinuousTimeline<Phone>(clipSegment->getTruncatedRange(), Phone::Noise));
alignmentProgressSink.reportProgress(1.0); alignmentProgressSink.reportProgress(1.0);
utterancePhones.shift(paddedTimeRange.getStart()); utterancePhones.shift(paddedTimeRange.getStart());
@ -338,5 +350,6 @@ BoundedTimeline<Phone> PocketSphinxRecognizer::recognizePhones(
ProgressSink& progressSink ProgressSink& progressSink
) const { ) const {
return ::recognizePhones( return ::recognizePhones(
inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink); inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink
);
} }

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "Recognizer.h"
#include "pocketSphinxTools.h" #include "pocketSphinxTools.h"
#include "Recognizer.h"
class PocketSphinxRecognizer : public Recognizer { class PocketSphinxRecognizer : public Recognizer {
public: public:

View File

@ -2,8 +2,8 @@
#include "audio/AudioClip.h" #include "audio/AudioClip.h"
#include "core/Phone.h" #include "core/Phone.h"
#include "tools/progress.h"
#include "time/BoundedTimeline.h" #include "time/BoundedTimeline.h"
#include "tools/progress.h"
class Recognizer { class Recognizer {
public: public:

View File

@ -1,14 +1,16 @@
#include <g2p.h> #include <g2p.h>
#include <regex>
#include "tools/stringTools.h"
#include "logging/logging.h"
using std::vector; #include <regex>
using std::wstring;
using std::regex; #include "logging/logging.h"
using std::wregex; #include "tools/stringTools.h"
using std::invalid_argument; using std::invalid_argument;
using std::pair; using std::pair;
using std::regex;
using std::vector;
using std::wregex;
using std::wstring;
const vector<pair<wregex, wstring>>& getReplacementRules() { const vector<pair<wregex, wstring>>& getReplacementRules() {
static vector<pair<wregex, wstring>> rules{ static vector<pair<wregex, wstring>> rules{
@ -64,8 +66,7 @@ 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: default: return Phone::Noise;
return Phone::Noise;
} }
} }

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include "core/Phone.h" #include "core/Phone.h"
std::vector<Phone> wordToPhones(const std::string& word); std::vector<Phone> wordToPhones(const std::string& word);

View File

@ -4,42 +4,34 @@
// Rules // Rules
// //
// get rid of some digraphs // get rid of some digraphs
{ wregex(L"ch"), L"ç" }, {wregex(L"ch"), L"ç"}, {wregex(L"sh"), L"$$"}, {wregex(L"ph"), L"f"}, {wregex(L"th"), L"+"},
{ wregex(L"sh"), L"$$" },
{ wregex(L"ph"), L"f" },
{ wregex(L"th"), L"+" },
{wregex(L"qu"), L"kw"}, {wregex(L"qu"), L"kw"},
// and other spelling-level changes // and other spelling-level changes
{ wregex(L"w(r)"), L"$1" }, {wregex(L"w(r)"), L"$1"}, {wregex(L"w(ho)"), L"$1"}, {wregex(L"(w)h"), L"$1"},
{ wregex(L"w(ho)"), L"$1" }, {wregex(L"(^r)h"), L"$1"}, {wregex(L"(x)h"), L"$1"},
{ wregex(L"(w)h"), L"$1" },
{ wregex(L"(^r)h"), L"$1" },
{ wregex(L"(x)h"), L"$1" },
{wregex(L"([aeiouäëïöüâêîôûùò@])h($)"), L"$1$2"}, {wregex(L"([aeiouäëïöüâêîôûùò@])h($)"), L"$1$2"},
{ wregex(L"(^e)x([aeiouäëïöüâêîôûùò@])"), L"$1gz$2" }, {wregex(L"(^e)x([aeiouäëïöüâêîôûùò@])"), L"$1gz$2"}, {wregex(L"x"), L"ks"}, {wregex(L"'"), L""},
{ wregex(L"x"), L"ks" },
{ wregex(L"'"), L"" },
// gh is particularly variable // gh is particularly variable
{wregex(L"gh([aeiouäëïöüâêîôûùò@])"), L"g$1"}, {wregex(L"gh([aeiouäëïöüâêîôûùò@])"), L"g$1"},
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a(gh)"), L"$1ä$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e(gh)"), L"$1ë$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i(gh)"), L"$1ï$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o(gh)"), L"$1ö$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u(gh)"), L"$1ü$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])â(gh)"), L"$1ä$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])ê(gh)"), L"$1ë$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])î(gh)"), L"$1ï$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])ô(gh)"), L"$1ö$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])û(gh)"), L"$1ü$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a(gh)"), L"$1ä$2"},
{ wregex(L"ough(t)"), L"ò$1" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e(gh)"), L"$1ë$2"},
{ wregex(L"augh(t)"), L"ò$1" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i(gh)"), L"$1ï$2"},
{ wregex(L"ough"), L"ö" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o(gh)"), L"$1ö$2"},
{ wregex(L"gh"), L"" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u(gh)"), L"$1ü$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])â(gh)"), L"$1ä$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])ê(gh)"), L"$1ë$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])î(gh)"), L"$1ï$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])ô(gh)"), L"$1ö$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])û(gh)"), L"$1ü$2"}, {wregex(L"ough(t)"), L"ò$1"},
{wregex(L"augh(t)"), L"ò$1"}, {wregex(L"ough"), L"ö"}, {wregex(L"gh"), L""},
// unpronounceable combinations // unpronounceable combinations
{ wregex(L"(^)g(n)"), L"$1$2" }, {wregex(L"(^)g(n)"), L"$1$2"}, {wregex(L"(^)k(n)"), L"$1$2"}, {wregex(L"(^)m(n)"), L"$1$2"},
{ wregex(L"(^)k(n)"), L"$1$2" }, {wregex(L"(^)p(t)"), L"$1$2"}, {wregex(L"(^)p(s)"), L"$1$2"}, {wregex(L"(^)t(m)"), L"$1$2"},
{ wregex(L"(^)m(n)"), L"$1$2" },
{ wregex(L"(^)p(t)"), L"$1$2" },
{ wregex(L"(^)p(s)"), L"$1$2" },
{ wregex(L"(^)t(m)"), L"$1$2" },
// medial y = i // medial y = i
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1ï$2"}, {wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1ï$2"},
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{2})y($)"), L"$1ï$2"}, {wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{2})y($)"), L"$1ï$2"},
{ wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{3})y($)"), L"$1ï$2" }, {wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{3})y($)"), L"$1ï$2"}, {wregex(L"ey"), L"ë"},
{ wregex(L"ey"), L"ë" }, {wregex(L"ay"), L"ä"}, {wregex(L"oy"), L"öy"},
{ wregex(L"ay"), L"ä" },
{ wregex(L"oy"), L"öy" },
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y([bcdfghjklmnpqrstvwxyzç+$ñ])"), L"$1i$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y([bcdfghjklmnpqrstvwxyzç+$ñ])"), L"$1i$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1i$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1i$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y(e$)"), L"$1i$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y(e$)"), L"$1i$2"},
@ -51,61 +43,83 @@
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ci([aeiouäëïöüâêîôûùò@])"), L"$1$$$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ci([aeiouäëïöüâêîôûùò@])"), L"$1$$$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ti([aeiouäëïöüâêîôûùò@])"), L"$1$$$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ti([aeiouäëïöüâêîôûùò@])"), L"$1$$$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])tu([aeiouäëïöüâêîôûùò@])"), L"$1çu$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])tu([aeiouäëïöüâêîôûùò@])"), L"$1çu$2"},
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])tu([rl][aeiouäëïöüâêîôûùò@])"), L"$1çu$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])tu([rl][aeiouäëïöüâêîôûùò@])"),
L"$1çu$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])si(o)"), L"$1$$$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])si(o)"), L"$1$$$2"},
{wregex(L"([aeiouäëïöüâêîôûùò@])si(o)"), L"$1j$2"}, {wregex(L"([aeiouäëïöüâêîôûùò@])si(o)"), L"$1j$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])s(ur)"), L"$1$$$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])s(ur)"), L"$1$$$2"},
{wregex(L"([aeiouäëïöüâêîôûùò@])s(ur)"), L"$1j$2"}, {wregex(L"([aeiouäëïöüâêîôûùò@])s(ur)"), L"$1j$2"},
{ wregex(L"(k)s(u[aeiouäëïöüâêîôûùò@])"), L"$1$$$2" }, {wregex(L"(k)s(u[aeiouäëïöüâêîôûùò@])"), L"$1$$$2"}, {wregex(L"(k)s(u[rl])"), L"$1$$$2"},
{ wregex(L"(k)s(u[rl])"), L"$1$$$2" },
// intervocalic s // intervocalic s
{wregex(L"([eiou])s([aeiouäëïöüâêîôûùò@])"), L"$1z$2"}, {wregex(L"([eiou])s([aeiouäëïöüâêîôûùò@])"), L"$1z$2"},
// al to ol (do this before respelling) // al to ol (do this before respelling)
{ wregex(L"a(ls)"), L"ò$1" }, {wregex(L"a(ls)"), L"ò$1"}, {wregex(L"a(lr)"), L"ò$1"}, {wregex(L"a(l{2}$)"), L"ò$1"},
{ wregex(L"a(lr)"), L"ò$1" },
{ wregex(L"a(l{2}$)"), L"ò$1" },
{wregex(L"a(lm(?:[aeiouäëïöüâêîôûùò@])?$)"), L"ò$1"}, {wregex(L"a(lm(?:[aeiouäëïöüâêîôûùò@])?$)"), L"ò$1"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a(l[td+])"), L"$1ò$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a(l[td+])"), L"$1ò$2"},
{wregex(L"(^)a(l[td+])"), L"$1ò$2"}, {wregex(L"(^)a(l[td+])"), L"$1ò$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])al(k)"), L"$1ò$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])al(k)"), L"$1ò$2"},
// soft c and g // soft c and g
{ wregex(L"c([eiêîy])"), L"s$1" }, {wregex(L"c([eiêîy])"), L"s$1"}, {wregex(L"c"), L"k"},
{ wregex(L"c"), L"k" },
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(a)"), L"$1j$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(a)"), L"$1j$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(o)"), L"$1j$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(o)"), L"$1j$2"},
{wregex(L"g([eiêîy])"), L"j$1"}, {wregex(L"g([eiêîy])"), L"j$1"},
// init/final guF was there just to harden the g // init/final guF was there just to harden the g
{ wregex(L"(^)gu([eiêîy])"), L"$1g$2" }, {wregex(L"(^)gu([eiêîy])"), L"$1g$2"}, {wregex(L"gu(e$)"), L"g$1"},
{ wregex(L"gu(e$)"), L"g$1" },
// untangle reverse-written final liquids // untangle reverse-written final liquids
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])re($)"), L"$1@r$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])re($)"), L"$1@r$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])le($)"), L"$1@l$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])le($)"), L"$1@l$2"},
// vowels are long medially // vowels are long medially
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ä$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ë$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ï$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ö$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ü$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"),
{ wregex(L"(^)a([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ä$2" }, { wregex(L"(^)e([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ë$2" }, { wregex(L"(^)i([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ï$2" }, { wregex(L"(^)o([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ö$2" }, { wregex(L"(^)u([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ü$2" }, L"$1ä$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"),
L"$1ë$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"),
L"$1ï$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"),
L"$1ö$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"),
L"$1ü$2"},
{wregex(L"(^)a([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ä$2"},
{wregex(L"(^)e([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ë$2"},
{wregex(L"(^)i([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ï$2"},
{wregex(L"(^)o([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ö$2"},
{wregex(L"(^)u([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@])"), L"$1ü$2"},
// and short before 2 consonants or a final one // and short before 2 consonants or a final one
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1â$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ê$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1î$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ô$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1û$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1â$2"},
{ wregex(L"(^)a([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1â$2" }, { wregex(L"(^)e([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ê$2" }, { wregex(L"(^)i([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1î$2" }, { wregex(L"(^)o([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ô$2" }, { wregex(L"(^)u([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1û$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ê$2"},
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1â$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ê$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1î$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ô$2" }, { wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1û$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1î$2"},
{ wregex(L"(^)a([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1â$2" }, { wregex(L"(^)e([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ê$2" }, { wregex(L"(^)i([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1î$2" }, { wregex(L"(^)o([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ô$2" }, { wregex(L"(^)u([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1û$2" }, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ô$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1û$2"},
{wregex(L"(^)a([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1â$2"},
{wregex(L"(^)e([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ê$2"},
{wregex(L"(^)i([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1î$2"},
{wregex(L"(^)o([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1ô$2"},
{wregex(L"(^)u([bcdfghjklmnpqrstvwxyzç+$ñ]{2})"), L"$1û$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1â$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])e([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ê$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])i([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1î$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])o([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ô$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])u([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1û$2"},
{wregex(L"(^)a([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1â$2"},
{wregex(L"(^)e([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ê$2"},
{wregex(L"(^)i([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1î$2"},
{wregex(L"(^)o([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1ô$2"},
{wregex(L"(^)u([bcdfghjklmnpqrstvwxyzç+$ñ]$)"), L"$1û$2"},
// special but general rules // special but general rules
{ wregex(L"î(nd$)"), L"ï$1" }, {wregex(L"î(nd$)"), L"ï$1"}, {wregex(L"ô(s{2}$)"), L"ò$1"}, {wregex(L"ô(g$)"), L"ò$1"},
{ wregex(L"ô(s{2}$)"), L"ò$1" }, {wregex(L"ô(f[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ò$1"}, {wregex(L"ô(l[td+])"), L"ö$1"},
{ wregex(L"ô(g$)"), L"ò$1" }, {wregex(L"(w)â(\\$)"), L"$1ò$2"}, {wregex(L"(w)â((?:t)?ç)"), L"$1ò$2"},
{ wregex(L"ô(f[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ò$1" },
{ wregex(L"ô(l[td+])"), L"ö$1" },
{ wregex(L"(w)â(\\$)"), L"$1ò$2" },
{ wregex(L"(w)â((?:t)?ç)"), L"$1ò$2" },
{wregex(L"(w)â([tdns+])"), L"$1ô$2"}, {wregex(L"(w)â([tdns+])"), L"$1ô$2"},
// soft gn // soft gn
{ wregex(L"îg([mnñ]$)"), L"ï$1" }, {wregex(L"îg([mnñ]$)"), L"ï$1"}, {wregex(L"îg([mnñ][bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ï$1"},
{ wregex(L"îg([mnñ][bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ï$1" },
{wregex(L"(ei)g(n)"), L"$1$2"}, {wregex(L"(ei)g(n)"), L"$1$2"},
// handle ous before removing -e // handle ous before removing -e
{ wregex(L"ou(s$)"), L"@$1" }, {wregex(L"ou(s$)"), L"@$1"}, {wregex(L"ou(s[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"@$1"},
{ wregex(L"ou(s[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"@$1" },
// remove silent -e // remove silent -e
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)e($)"), L"$1$2" }, {wregex(
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)e($)"
),
L"$1$2"},
// common suffixes that hide a silent e // common suffixes that hide a silent e
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ë(mênt$)"), L"$1$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ë(mênt$)"), L"$1$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ë(nês{2}$)"), L"$1$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ë(nês{2}$)"), L"$1$2"},
@ -115,101 +129,128 @@
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ï(nês{2}$)"), L"$1ë$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ï(nês{2}$)"), L"$1ë$2"},
// shorten (1-char) weak penults after a long // shorten (1-char) weak penults after a long
// note: this error breaks almost as many words as it fixes... // note: this error breaks almost as many words as it fixes...
{ wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ä([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1â$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ë([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1ê$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ï([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1î$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ö([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1ô$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ü([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1û$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ä([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1â$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ë([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1ê$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ï([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1î$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ö([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1ô$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ü([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1û$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ä([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1â$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ë([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1ê$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ï([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1î$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ö([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1ô$2" }, { wregex(L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ü([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"), L"$1û$2" }, {wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ä([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1â$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ë([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1ê$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ï([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1î$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ö([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1ô$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ü([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1û$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ä([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1â$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ë([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1ê$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ï([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1î$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ö([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1ô$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ü([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1û$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ä([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1â$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ë([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1ê$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ï([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1î$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ö([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1ô$2"},
{wregex(
L"([äëïöüäëïöüäëïöüùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ü([bcdfghjklmnpqrstvwxyzç+$ñ][aeiouäëïöüâêîôûùò@]$)"
),
L"$1û$2"},
// double vowels // double vowels
{ wregex(L"eau"), L"ö" }, {wregex(L"eau"), L"ö"}, {wregex(L"ai"), L"ä"}, {wregex(L"au"), L"ò"}, {wregex(L"âw"), L"ò"},
{ wregex(L"ai"), L"ä" }, {wregex(L"e{2}"), L"ë"}, {wregex(L"ea"), L"ë"}, {wregex(L"(s)ei"), L"$1ë"},
{ wregex(L"au"), L"ò" }, {wregex(L"ei"), L"ä"}, {wregex(L"eo"), L"ë@"}, {wregex(L"êw"), L"ü"}, {wregex(L"eu"), L"ü"},
{ wregex(L"âw"), L"ò" }, {wregex(L"ie"), L"ë"}, {wregex(L"(i)[aeiouäëïöüâêîôûùò@]"), L"$1@"},
{ wregex(L"e{2}"), L"ë" },
{ wregex(L"ea"), L"ë" },
{ wregex(L"(s)ei"), L"$1ë" },
{ wregex(L"ei"), L"ä" },
{ wregex(L"eo"), L"ë@" },
{ wregex(L"êw"), L"ü" },
{ wregex(L"eu"), L"ü" },
{ wregex(L"ie"), L"ë" },
{ wregex(L"(i)[aeiouäëïöüâêîôûùò@]"), L"$1@" },
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)i"), L"$1ï"}, {wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)i"), L"$1ï"},
{ wregex(L"i(@)"), L"ë$1" }, {wregex(L"i(@)"), L"ë$1"}, {wregex(L"oa"), L"ö"}, {wregex(L"oe($)"), L"ö$1"},
{ wregex(L"oa"), L"ö" }, {wregex(L"o{2}(k)"), L"ù$1"}, {wregex(L"o{2}"), L"u"}, {wregex(L"oul(d$)"), L"ù$1"},
{ wregex(L"oe($)"), L"ö$1" }, {wregex(L"ou"), L"ôw"}, {wregex(L"oi"), L"öy"}, {wregex(L"ua"), L"ü@"}, {wregex(L"ue"), L"u"},
{ wregex(L"o{2}(k)"), L"ù$1" }, {wregex(L"ui"), L"u"}, {wregex(L"ôw($)"), L"ö$1"},
{ wregex(L"o{2}"), L"u" },
{ wregex(L"oul(d$)"), L"ù$1" },
{ wregex(L"ou"), L"ôw" },
{ wregex(L"oi"), L"öy" },
{ wregex(L"ua"), L"ü@" },
{ wregex(L"ue"), L"u" },
{ wregex(L"ui"), L"u" },
{ wregex(L"ôw($)"), L"ö$1" },
// those pesky final syllabics // those pesky final syllabics
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[aeiouäëïöüâêîôûùò@])?)[aeiouäëïöüâêîôûùò@](l$)"), L"$1@$2" }, {wregex(
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ê(n$)"), L"$1@$2" }, L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[aeiouäëïöüâêîôûùò@])?)[aeiouäëïöüâêîôûùò@](l$)"
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)î(n$)"), L"$1@$2" }, ),
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)â(n$)"), L"$1@$2" }, L"$1@$2"},
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ô(n$)"), L"$1@$2" }, {wregex(
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ê(n$)"
),
L"$1@$2"},
{wregex(
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)î(n$)"
),
L"$1@$2"},
{wregex(
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)â(n$)"
),
L"$1@$2"},
{wregex(
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)ô(n$)"
),
L"$1@$2"},
// suffix simplifications // suffix simplifications
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})[aâä](b@l$)"), L"$1@$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})[aâä](b@l$)"), L"$1@$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]l)ë(@n$)"), L"$1y$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]l)ë(@n$)"), L"$1y$2"},
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]n)ë(@n$)"), L"$1y$2"}, {wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]n)ë(@n$)"), L"$1y$2"},
// unpronounceable finals // unpronounceable finals
{ wregex(L"(m)b($)"), L"$1$2" }, {wregex(L"(m)b($)"), L"$1$2"}, {wregex(L"(m)n($)"), L"$1$2"},
{ wregex(L"(m)n($)"), L"$1$2" },
// color the final vowels // color the final vowels
{ wregex(L"a($)"), L"@$1" }, {wregex(L"a($)"), L"@$1"}, {wregex(L"e($)"), L"ë$1"}, {wregex(L"i($)"), L"ë$1"},
{ wregex(L"e($)"), L"ë$1" },
{ wregex(L"i($)"), L"ë$1" },
{wregex(L"o($)"), L"ö$1"}, {wregex(L"o($)"), L"ö$1"},
// vowels before r V=aeiouäëïöüâêîôûùò@ // vowels before r V=aeiouäëïöüâêîôûùò@
{wregex(L"ôw(r[bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])"), L"ö$1"}, {wregex(L"ôw(r[bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])"), L"ö$1"},
{ wregex(L"ô(r)"), L"ö$1" }, {wregex(L"ô(r)"), L"ö$1"}, {wregex(L"ò(r)"), L"ö$1"},
{ wregex(L"ò(r)"), L"ö$1" }, {wregex(L"(w)â(r[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"$1ö$2"}, {wregex(L"(w)â(r$)"), L"$1ö$2"},
{ wregex(L"(w)â(r[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"$1ö$2" }, {wregex(L"ê(r{2})"), L"ä$1"}, {wregex(L"ë(r[iîï][bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ä$1"},
{ wregex(L"(w)â(r$)"), L"$1ö$2" }, {wregex(L"â(r{2})"), L"ä$1"}, {wregex(L"â(r[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ô$1"},
{ wregex(L"ê(r{2})"), L"ä$1" }, {wregex(L"â(r$)"), L"ô$1"}, {wregex(L"â(r)"), L"ä$1"}, {wregex(L"ê(r)"), L"@$1"},
{ wregex(L"ë(r[iîï][bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ä$1" }, {wregex(L"î(r)"), L"@$1"}, {wregex(L"û(r)"), L"@$1"}, {wregex(L"ù(r)"), L"@$1"},
{ wregex(L"â(r{2})"), L"ä$1" },
{ wregex(L"â(r[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ô$1" },
{ wregex(L"â(r$)"), L"ô$1" },
{ wregex(L"â(r)"), L"ä$1" },
{ wregex(L"ê(r)"), L"@$1" },
{ wregex(L"î(r)"), L"@$1" },
{ wregex(L"û(r)"), L"@$1" },
{ wregex(L"ù(r)"), L"@$1" },
// handle ng // handle ng
{ wregex(L"ng([fs$+])"), L"ñ$1" }, {wregex(L"ng([fs$+])"), L"ñ$1"}, {wregex(L"ng([bdg])"), L"ñ$1"}, {wregex(L"ng([ptk])"), L"ñ$1"},
{ wregex(L"ng([bdg])"), L"ñ$1" }, {wregex(L"ng($)"), L"ñ$1"}, {wregex(L"n(g)"), L"ñ$1"}, {wregex(L"n(k)"), L"ñ$1"},
{ wregex(L"ng([ptk])"), L"ñ$1" }, {wregex(L"ô(ñ)"), L"ò$1"}, {wregex(L"â(ñ)"), L"ä$1"},
{ wregex(L"ng($)"), L"ñ$1" },
{ wregex(L"n(g)"), L"ñ$1" },
{ wregex(L"n(k)"), L"ñ$1" },
{ wregex(L"ô(ñ)"), L"ò$1" },
{ wregex(L"â(ñ)"), L"ä$1" },
// really a morphophonological rule, but it's cute // really a morphophonological rule, but it's cute
{ wregex(L"([bdg])s($)"), L"$1z$2" }, {wregex(L"([bdg])s($)"), L"$1z$2"}, {wregex(L"s(m$)"), L"z$1"},
{ wregex(L"s(m$)"), L"z$1" },
// double consonants // double consonants
{ wregex(L"s(s)"), L"$1" }, {wregex(L"s(s)"), L"$1"}, {wregex(L"s(\\$)"), L"$1"}, {wregex(L"t(t)"), L"$1"},
{ wregex(L"s(\\$)"), L"$1" }, {wregex(L"t(ç)"), L"$1"}, {wregex(L"p(p)"), L"$1"}, {wregex(L"k(k)"), L"$1"},
{ wregex(L"t(t)"), L"$1" }, {wregex(L"b(b)"), L"$1"}, {wregex(L"d(d)"), L"$1"}, {wregex(L"d(j)"), L"$1"},
{ wregex(L"t(ç)"), L"$1" }, {wregex(L"g(g)"), L"$1"}, {wregex(L"n(n)"), L"$1"}, {wregex(L"m(m)"), L"$1"},
{ wregex(L"p(p)"), L"$1" }, {wregex(L"r(r)"), L"$1"}, {wregex(L"l(l)"), L"$1"}, {wregex(L"f(f)"), L"$1"},
{ wregex(L"k(k)"), L"$1" },
{ wregex(L"b(b)"), L"$1" },
{ wregex(L"d(d)"), L"$1" },
{ wregex(L"d(j)"), L"$1" },
{ wregex(L"g(g)"), L"$1" },
{ wregex(L"n(n)"), L"$1" },
{ wregex(L"m(m)"), L"$1" },
{ wregex(L"r(r)"), L"$1" },
{ wregex(L"l(l)"), L"$1" },
{ wregex(L"f(f)"), L"$1" },
{wregex(L"z(z)"), L"$1"}, {wregex(L"z(z)"), L"$1"},
// There are a number of cases not covered by these rules. // There are a number of cases not covered by these rules.
// Let's add some reasonable fallback rules. // Let's add some reasonable fallback rules.
{ wregex(L"a"), L"â" }, {wregex(L"a"), L"â"}, {wregex(L"e"), L"@"}, {wregex(L"i"), L"ë"}, {wregex(L"o"), L"ö"},
{ wregex(L"e"), L"@" },
{ wregex(L"i"), L"ë" },
{ wregex(L"o"), L"ö" },
{wregex(L"q"), L"k"}, {wregex(L"q"), L"k"},

View File

@ -1,22 +1,25 @@
#include "languageModels.h" #include "languageModels.h"
#include <boost/range/adaptor/map.hpp>
#include <vector>
#include <regex>
#include <map>
#include <tuple>
#include "tools/platformTools.h"
#include <fstream>
#include "core/appInfo.h"
#include <cmath>
#include <gsl_util.h> #include <gsl_util.h>
using std::string; #include <boost/range/adaptor/map.hpp>
using std::vector; #include <cmath>
using std::regex; #include <fstream>
using std::map; #include <map>
using std::tuple; #include <regex>
using std::get; #include <tuple>
#include <vector>
#include "core/appInfo.h"
#include "tools/platformTools.h"
using std::endl; using std::endl;
using std::get;
using std::map;
using std::regex;
using std::string;
using std::tuple;
using std::vector;
using std::filesystem::path; using std::filesystem::path;
using Unigram = string; using Unigram = string;
@ -50,9 +53,7 @@ map<Trigram, int> getTrigramCounts(const vector<string>& words) {
} }
map<Unigram, double> getUnigramProbabilities( map<Unigram, double> getUnigramProbabilities(
const vector<string>& words, const vector<string>& words, const map<Unigram, int>& unigramCounts, const double deflator
const map<Unigram, int>& unigramCounts,
const double deflator
) { ) {
map<Unigram, double> unigramProbabilities; map<Unigram, double> unigramProbabilities;
for (const auto& pair : unigramCounts) { for (const auto& pair : unigramCounts) {
@ -97,8 +98,8 @@ map<Unigram, double> getUnigramBackoffWeights(
const map<Unigram, int>& unigramCounts, const map<Unigram, int>& unigramCounts,
const map<Unigram, double>& unigramProbabilities, const map<Unigram, double>& unigramProbabilities,
const map<Bigram, int>& bigramCounts, const map<Bigram, int>& bigramCounts,
const double discountMass) const double discountMass
{ ) {
map<Unigram, double> unigramBackoffWeights; map<Unigram, double> unigramBackoffWeights;
for (const Unigram& unigram : unigramCounts | boost::adaptors::map_keys) { for (const Unigram& unigram : unigramCounts | boost::adaptors::map_keys) {
double denominator = 1; double denominator = 1;
@ -116,8 +117,8 @@ map<Bigram, double> getBigramBackoffWeights(
const map<Bigram, int>& bigramCounts, const map<Bigram, int>& bigramCounts,
const map<Bigram, double>& bigramProbabilities, const map<Bigram, double>& bigramProbabilities,
const map<Trigram, int>& trigramCounts, const map<Trigram, int>& trigramCounts,
const double discountMass) const double discountMass
{ ) {
map<Bigram, double> bigramBackoffWeights; map<Bigram, double> bigramBackoffWeights;
for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) { for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) {
double denominator = 1; double denominator = 1;
@ -163,24 +164,22 @@ void createLanguageModelFile(const vector<string>& words, const path& filePath)
file.precision(4); file.precision(4);
file << "\\1-grams:" << endl; file << "\\1-grams:" << endl;
for (const Unigram& 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;
} }
file << endl; file << endl;
file << "\\2-grams:" << endl; file << "\\2-grams:" << endl;
for (const Bigram& 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<0>(bigram) << " " << get<1>(bigram) << get<1>(bigram) << " " << log10(bigramBackoffWeights.at(bigram)) << endl;
<< " " << log10(bigramBackoffWeights.at(bigram)) << endl;
} }
file << endl; file << endl;
file << "\\3-grams:" << endl; file << "\\3-grams:" << endl;
for (const Trigram& 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<0>(trigram) << " " << get<1>(trigram) << " " << get<2>(trigram) << endl; << get<1>(trigram) << " " << get<2>(trigram) << endl;
} }
file << endl; file << endl;
@ -188,14 +187,16 @@ void createLanguageModelFile(const vector<string>& words, const path& filePath)
} }
lambda_unique_ptr<ngram_model_t> createLanguageModel( lambda_unique_ptr<ngram_model_t> createLanguageModel(
const vector<string>& words, const vector<string>& words, ps_decoder_t& decoder
ps_decoder_t& decoder
) { ) {
path tempFilePath = getTempFilePath(); path tempFilePath = getTempFilePath();
createLanguageModelFile(words, tempFilePath); createLanguageModelFile(words, tempFilePath);
auto deleteTempFile = gsl::finally([&]() { std::filesystem::remove(tempFilePath); }); auto deleteTempFile = gsl::finally([&]() { std::filesystem::remove(tempFilePath); });
return lambda_unique_ptr<ngram_model_t>( return lambda_unique_ptr<ngram_model_t>(
ngram_model_read(decoder.config, tempFilePath.u8string().c_str(), NGRAM_ARPA, decoder.lmath), ngram_model_read(
[](ngram_model_t* lm) { ngram_model_free(lm); }); decoder.config, tempFilePath.u8string().c_str(), NGRAM_ARPA, decoder.lmath
),
[](ngram_model_t* lm) { ngram_model_free(lm); }
);
} }

View File

@ -1,14 +1,14 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include "tools/tools.h" #include "tools/tools.h"
extern "C" { extern "C" {
#include <pocketsphinx.h>
#include <ngram_search.h> #include <ngram_search.h>
#include <pocketsphinx.h>
} }
lambda_unique_ptr<ngram_model_t> createLanguageModel( lambda_unique_ptr<ngram_model_t> createLanguageModel(
const std::vector<std::string>& words, const std::vector<std::string>& words, ps_decoder_t& decoder
ps_decoder_t& decoder
); );

View File

@ -1,43 +1,39 @@
#include "pocketSphinxTools.h" #include "pocketSphinxTools.h"
#include "tools/platformTools.h"
#include <regex> #include <regex>
#include "audio/DcOffset.h" #include "audio/DcOffset.h"
#include "audio/voiceActivityDetection.h" #include "audio/voiceActivityDetection.h"
#include "tools/parallel.h"
#include "tools/ObjectPool.h"
#include "time/timedLogging.h" #include "time/timedLogging.h"
#include "tools/ObjectPool.h"
#include "tools/parallel.h"
#include "tools/platformTools.h"
extern "C" { extern "C" {
#include <sphinxbase/err.h>
#include <pocketsphinx_internal.h>
#include <ngram_search.h> #include <ngram_search.h>
#include <pocketsphinx_internal.h>
#include <sphinxbase/err.h>
} }
using std::runtime_error;
using std::invalid_argument;
using std::unique_ptr;
using std::string;
using std::vector;
using std::filesystem::path;
using std::regex;
using boost::optional; using boost::optional;
using std::invalid_argument;
using std::regex;
using std::runtime_error;
using std::string;
using std::unique_ptr;
using std::vector;
using std::chrono::duration_cast; using std::chrono::duration_cast;
using std::filesystem::path;
logging::Level convertSphinxErrorLevel(err_lvl_t errorLevel) { logging::Level convertSphinxErrorLevel(err_lvl_t errorLevel) {
switch (errorLevel) { switch (errorLevel) {
case ERR_DEBUG: case ERR_DEBUG:
case ERR_INFO: case ERR_INFO:
case ERR_INFOCONT: case ERR_INFOCONT: return logging::Level::Trace;
return logging::Level::Trace; case ERR_WARN: return logging::Level::Warn;
case ERR_WARN: case ERR_ERROR: return logging::Level::Error;
return logging::Level::Warn; case ERR_FATAL: return logging::Level::Fatal;
case ERR_ERROR: default: throw invalid_argument("Unknown log level.");
return logging::Level::Error;
case ERR_FATAL:
return logging::Level::Fatal;
default:
throw invalid_argument("Unknown log level.");
} }
} }
@ -110,19 +106,18 @@ BoundedTimeline<Phone> recognizePhones(
redirectPocketSphinxOutput(); redirectPocketSphinxOutput();
// Prepare pool of decoders // Prepare pool of decoders
ObjectPool<ps_decoder_t, lambda_unique_ptr<ps_decoder_t>> decoderPool( ObjectPool<ps_decoder_t, lambda_unique_ptr<ps_decoder_t>> decoderPool([&] {
[&] { return createDecoder(dialog); }); return createDecoder(dialog);
});
BoundedTimeline<Phone> phones(audioClip->getTruncatedRange()); BoundedTimeline<Phone> phones(audioClip->getTruncatedRange());
std::mutex resultMutex; std::mutex resultMutex;
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 = utteranceToPhones( Timeline<Phone> utterancePhones = utteranceToPhones(
*audioClip, *audioClip, timedUtterance.getTimeRange(), *decoder, utteranceProgressSink
timedUtterance.getTimeRange(),
*decoder,
utteranceProgressSink
); );
// Copy phones to result timeline // Copy phones to result timeline
@ -139,15 +134,18 @@ BoundedTimeline<Phone> recognizePhones(
// Perform speech recognition // Perform speech recognition
try { try {
// Determine how many parallel threads to use // Determine how many parallel threads to use
int threadCount = std::min({ int threadCount = std::min(
maxThreadCount, {maxThreadCount,
// 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>( static_cast<int>(
duration_cast<std::chrono::seconds>(audioClip->getTruncatedRange().getDuration()).count() / 5 duration_cast<std::chrono::seconds>(audioClip->getTruncatedRange().getDuration())
) .count()
}); / 5
)}
);
if (threadCount < 1) { if (threadCount < 1) {
threadCount = 1; threadCount = 1;
} }
@ -162,7 +160,9 @@ BoundedTimeline<Phone> recognizePhones(
); );
logging::debug("Speech recognition -- end"); logging::debug("Speech recognition -- end");
} catch (...) { } catch (...) {
std::throw_with_nested(runtime_error("Error performing speech recognition via PocketSphinx tools.")); std::throw_with_nested(
runtime_error("Error performing speech recognition via PocketSphinx tools.")
);
} }
return phones; return phones;
@ -206,8 +206,9 @@ BoundedTimeline<string> recognizeWords(const vector<int16_t>& audioBuffer, ps_de
// Process entire audio clip // Process entire audio clip
const bool noRecognition = false; const bool noRecognition = false;
const bool fullUtterance = true; const bool fullUtterance = true;
const int searchedFrameCount = const int searchedFrameCount = ps_process_raw(
ps_process_raw(&decoder, audioBuffer.data(), audioBuffer.size(), noRecognition, fullUtterance); &decoder, audioBuffer.data(), audioBuffer.size(), noRecognition, fullUtterance
);
if (searchedFrameCount < 0) { if (searchedFrameCount < 0) {
throw runtime_error("Error analyzing raw audio data for word recognition."); throw runtime_error("Error analyzing raw audio data for word recognition.");
} }
@ -227,7 +228,8 @@ BoundedTimeline<string> recognizeWords(const vector<int16_t>& audioBuffer, ps_de
// Not every utterance does contain speech, however. In this case, we exit early to prevent // Not every utterance does contain speech, however. In this case, we exit early to prevent
// the log output. // the log output.
// We *don't* to that in phonetic mode because here, the same code would omit valid phones. // We *don't* to that in phonetic mode because here, the same code would omit valid phones.
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;
} }

View File

@ -1,25 +1,26 @@
#pragma once #pragma once
#include "time/BoundedTimeline.h"
#include "core/Phone.h"
#include "audio/AudioClip.h"
#include "tools/progress.h"
#include <filesystem> #include <filesystem>
#include "audio/AudioClip.h"
#include "core/Phone.h"
#include "time/BoundedTimeline.h"
#include "tools/progress.h"
extern "C" { extern "C" {
#include <pocketsphinx.h> #include <pocketsphinx.h>
} }
typedef std::function<lambda_unique_ptr<ps_decoder_t>( typedef std::function<lambda_unique_ptr<ps_decoder_t>(boost::optional<std::string> dialog)>
boost::optional<std::string> dialog decoderFactory;
)> decoderFactory;
typedef std::function<Timeline<Phone>( typedef std::function<Timeline<Phone>(
const AudioClip& audioClip, const AudioClip& audioClip,
TimeRange utteranceTimeRange, TimeRange utteranceTimeRange,
ps_decoder_t& decoder, ps_decoder_t& decoder,
ProgressSink& utteranceProgressSink ProgressSink& utteranceProgressSink
)> utteranceToPhonesFunction; )>
utteranceToPhonesFunction;
BoundedTimeline<Phone> recognizePhones( BoundedTimeline<Phone> recognizePhones(
const AudioClip& inputAudioClip, const AudioClip& inputAudioClip,
@ -37,6 +38,5 @@ const std::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( BoundedTimeline<std::string> recognizeWords(
const std::vector<int16_t>& audioBuffer, const std::vector<int16_t>& audioBuffer, ps_decoder_t& decoder
ps_decoder_t& decoder
); );

View File

@ -1,22 +1,24 @@
#include "tokenization.h" #include "tokenization.h"
#include "tools/tools.h"
#include "tools/stringTools.h"
#include <regex>
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
#include <regex>
#include "tools/stringTools.h"
#include "tools/tools.h"
extern "C" { extern "C" {
#include <cst_utt_utils.h> #include <cst_utt_utils.h>
#include <lang/usenglish/usenglish.h>
#include <lang/cmulex/cmu_lex.h> #include <lang/cmulex/cmu_lex.h>
#include <lang/usenglish/usenglish.h>
} }
using boost::optional;
using std::function;
using std::pair;
using std::regex;
using std::runtime_error; using std::runtime_error;
using std::string; using std::string;
using std::vector; using std::vector;
using std::regex;
using std::pair;
using boost::optional;
using std::function;
lambda_unique_ptr<cst_voice> createDummyVoice() { lambda_unique_ptr<cst_voice> createDummyVoice() {
lambda_unique_ptr<cst_voice> voice(new_voice(), [](cst_voice* voice) { delete_voice(voice); }); lambda_unique_ptr<cst_voice> voice(new_voice(), [](cst_voice* voice) { delete_voice(voice); });
@ -38,10 +40,9 @@ 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( lambda_unique_ptr<cst_utterance> utterance(new_utterance(), [](cst_utterance* utterance) {
new_utterance(), delete_utterance(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());
@ -52,11 +53,8 @@ vector<string> tokenizeViaFlite(const string& text) {
} }
vector<string> result; vector<string> result;
for ( for (cst_item* item = relation_head(utt_relation(utterance.get(), "Word")); item;
cst_item* item = relation_head(utt_relation(utterance.get(), "Word")); item = item_next(item)) {
item;
item = item_next(item)
) {
const char* word = item_feat_string(item, "name"); const char* word = item_feat_string(item, "name");
result.emplace_back(word); result.emplace_back(word);
} }
@ -64,11 +62,11 @@ vector<string> tokenizeViaFlite(const string& text) {
} }
optional<string> findSimilarDictionaryWord( optional<string> findSimilarDictionaryWord(
const string& word, const string& word, const function<bool(const string&)>& dictionaryContains
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;
if (apostropheIndex != -1) { if (apostropheIndex != -1) {
modified.insert(apostropheIndex, "'"); modified.insert(apostropheIndex, "'");
@ -87,8 +85,7 @@ optional<string> findSimilarDictionaryWord(
} }
vector<string> tokenizeText( vector<string> tokenizeText(
const string& text, const string& text, const function<bool(const string&)>& dictionaryContains
const function<bool(const string&)>& dictionaryContains
) { ) {
vector<string> words = tokenizeViaFlite(text); vector<string> words = tokenizeViaFlite(text);

View File

@ -1,10 +1,9 @@
#pragma once #pragma once
#include <vector>
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector>
std::vector<std::string> tokenizeText( std::vector<std::string> tokenizeText(
const std::string& text, const std::string& text, const std::function<bool(const std::string&)>& dictionaryContains
const std::function<bool(const std::string&)>& dictionaryContains
); );

View File

@ -2,16 +2,12 @@
#include "tools/EnumConverter.h" #include "tools/EnumConverter.h"
enum class ExportFormat { enum class ExportFormat { Dat, Tsv, Xml, Json };
Dat,
Tsv,
Xml,
Json
};
class ExportFormatConverter : public EnumConverter<ExportFormat> { class ExportFormatConverter : public EnumConverter<ExportFormat> {
public: public:
static ExportFormatConverter& get(); static ExportFormatConverter& get();
protected: protected:
std::string getTypeName() override; std::string getTypeName() override;
member_data getMemberData() override; member_data getMemberData() override;

View File

@ -13,8 +13,7 @@ string RecognizerTypeConverter::getTypeName() {
EnumConverter<RecognizerType>::member_data RecognizerTypeConverter::getMemberData() { EnumConverter<RecognizerType>::member_data RecognizerTypeConverter::getMemberData() {
return member_data{ return member_data{
{ RecognizerType::PocketSphinx, "pocketSphinx" }, {RecognizerType::PocketSphinx, "pocketSphinx"}, {RecognizerType::Phonetic, "phonetic"}
{ RecognizerType::Phonetic, "phonetic" }
}; };
} }

View File

@ -2,14 +2,12 @@
#include "tools/EnumConverter.h" #include "tools/EnumConverter.h"
enum class RecognizerType { enum class RecognizerType { PocketSphinx, Phonetic };
PocketSphinx,
Phonetic
};
class RecognizerTypeConverter : public EnumConverter<RecognizerType> { class RecognizerTypeConverter : public EnumConverter<RecognizerType> {
public: public:
static RecognizerTypeConverter& get(); static RecognizerTypeConverter& get();
protected: protected:
std::string getTypeName() override; std::string getTypeName() override;
member_data getMemberData() override; member_data getMemberData() override;

View File

@ -1,47 +1,48 @@
#include <iostream>
#include <format.h> #include <format.h>
#include <tclap/CmdLine.h>
#include "core/appInfo.h"
#include "tools/NiceCmdLineOutput.h"
#include "logging/logging.h"
#include "logging/sinks.h"
#include "logging/formatters.h"
#include <gsl_util.h> #include <gsl_util.h>
#include "exporters/Exporter.h" #include <tclap/CmdLine.h>
#include "time/ContinuousTimeline.h"
#include "tools/stringTools.h"
#include <boost/range/adaptor/transformed.hpp> #include <boost/range/adaptor/transformed.hpp>
#include <boost/utility/in_place_factory.hpp>
#include <fstream> #include <fstream>
#include "tools/parallel.h" #include <iostream>
#include "tools/exceptions.h"
#include "tools/textFiles.h" #include "animation/targetShapeSet.h"
#include "lib/rhubarbLib.h" #include "core/appInfo.h"
#include "ExportFormat.h"
#include "exporters/DatExporter.h" #include "exporters/DatExporter.h"
#include "exporters/Exporter.h"
#include "exporters/JsonExporter.h"
#include "exporters/TsvExporter.h" #include "exporters/TsvExporter.h"
#include "exporters/XmlExporter.h" #include "exporters/XmlExporter.h"
#include "exporters/JsonExporter.h" #include "ExportFormat.h"
#include "animation/targetShapeSet.h" #include "lib/rhubarbLib.h"
#include <boost/utility/in_place_factory.hpp> #include "logging/formatters.h"
#include "tools/platformTools.h" #include "logging/logging.h"
#include "sinks.h" #include "logging/sinks.h"
#include "semanticEntries.h"
#include "RecognizerType.h"
#include "recognition/PocketSphinxRecognizer.h"
#include "recognition/PhoneticRecognizer.h" #include "recognition/PhoneticRecognizer.h"
#include "recognition/PocketSphinxRecognizer.h"
#include "RecognizerType.h"
#include "semanticEntries.h"
#include "sinks.h"
#include "time/ContinuousTimeline.h"
#include "tools/exceptions.h"
#include "tools/NiceCmdLineOutput.h"
#include "tools/parallel.h"
#include "tools/platformTools.h"
#include "tools/stringTools.h"
#include "tools/textFiles.h"
using boost::optional;
using boost::adaptors::transformed;
using std::exception; using std::exception;
using std::string; using std::make_shared;
using std::string;
using std::vector;
using std::unique_ptr;
using std::make_unique; using std::make_unique;
using std::shared_ptr; using std::shared_ptr;
using std::make_shared; using std::string;
using std::unique_ptr;
using std::vector;
using std::filesystem::path; using std::filesystem::path;
using std::filesystem::u8path; using std::filesystem::u8path;
using boost::adaptors::transformed;
using boost::optional;
namespace tclap = TCLAP; namespace tclap = TCLAP;
@ -61,7 +62,7 @@ namespace TCLAP {
struct ArgTraits<RecognizerType> { struct ArgTraits<RecognizerType> {
typedef ValueLike ValueCategory; typedef ValueLike ValueCategory;
}; };
} } // namespace TCLAP
shared_ptr<logging::Sink> createFileSink(const path& path, logging::Level minLevel) { shared_ptr<logging::Sink> createFileSink(const path& path, logging::Level minLevel) {
auto file = make_shared<std::ofstream>(); auto file = make_shared<std::ofstream>();
@ -74,12 +75,9 @@ shared_ptr<logging::Sink> createFileSink(const path& path, logging::Level minLev
unique_ptr<Recognizer> createRecognizer(RecognizerType recognizerType) { unique_ptr<Recognizer> createRecognizer(RecognizerType recognizerType) {
switch (recognizerType) { switch (recognizerType) {
case RecognizerType::PocketSphinx: case RecognizerType::PocketSphinx: return make_unique<PocketSphinxRecognizer>();
return make_unique<PocketSphinxRecognizer>(); case RecognizerType::Phonetic: return make_unique<PhoneticRecognizer>();
case RecognizerType::Phonetic: default: throw std::runtime_error("Unknown recognizer.");
return make_unique<PhoneticRecognizer>();
default:
throw std::runtime_error("Unknown recognizer.");
} }
} }
@ -92,14 +90,10 @@ unique_ptr<Exporter> createExporter(
switch (exportFormat) { switch (exportFormat) {
case ExportFormat::Dat: case ExportFormat::Dat:
return make_unique<DatExporter>(targetShapeSet, datFrameRate, datUsePrestonBlair); return make_unique<DatExporter>(targetShapeSet, datFrameRate, datUsePrestonBlair);
case ExportFormat::Tsv: case ExportFormat::Tsv: return make_unique<TsvExporter>();
return make_unique<TsvExporter>(); case ExportFormat::Xml: return make_unique<XmlExporter>();
case ExportFormat::Xml: case ExportFormat::Json: return make_unique<JsonExporter>();
return make_unique<XmlExporter>(); default: throw std::runtime_error("Unknown export format.");
case ExportFormat::Json:
return make_unique<JsonExporter>();
default:
throw std::runtime_error("Unknown export format.");
} }
} }
@ -134,78 +128,118 @@ int main(int platformArgc, char* platformArgv[]) {
cmd.setOutput(new NiceCmdLineOutput()); cmd.setOutput(new NiceCmdLineOutput());
tclap::ValueArg<string> outputFileName( tclap::ValueArg<string> outputFileName(
"o", "output", "The output file path.", "o", "output", "The output file path.", false, string(), "string", cmd
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( tclap::ValueArg<logging::Level> logLevel(
"", "logLevel", "The minimum log level that will be written to the log file", "",
false, logging::Level::Debug, &logLevelConstraint, cmd "logLevel",
"The minimum log level that will be written to the log file",
false,
logging::Level::Debug,
&logLevelConstraint,
cmd
); );
tclap::ValueArg<string> logFileName( tclap::ValueArg<string> logFileName(
"", "logFile", "The log file path.", "", "logFile", "The log file path.", false, string(), "string", cmd
false, string(), "string", cmd
); );
tclap::ValueArg<logging::Level> consoleLevel( tclap::ValueArg<logging::Level> consoleLevel(
"", "consoleLevel", "The minimum log level that will be printed on the console (stderr)", "",
false, defaultMinStderrLevel, &logLevelConstraint, cmd "consoleLevel",
"The minimum log level that will be printed on the console (stderr)",
false,
defaultMinStderrLevel,
&logLevelConstraint,
cmd
); );
tclap::SwitchArg machineReadableMode( tclap::SwitchArg machineReadableMode(
"", "machineReadable", "Formats all output to stderr in a structured JSON format.", "",
cmd, false "machineReadable",
"Formats all output to stderr in a structured JSON format.",
cmd,
false
); );
tclap::SwitchArg quietMode( tclap::SwitchArg quietMode(
"q", "quiet", "Suppresses all output to stderr except for warnings and error messages.", "q",
cmd, false "quiet",
"Suppresses all output to stderr except for warnings and error messages.",
cmd,
false
); );
tclap::ValueArg<int> maxThreadCount( tclap::ValueArg<int> maxThreadCount(
"", "threads", "The maximum number of worker threads to use.", "",
false, getProcessorCoreCount(), "number", cmd "threads",
"The maximum number of worker threads to use.",
false,
getProcessorCoreCount(),
"number",
cmd
); );
tclap::ValueArg<string> extendedShapes( tclap::ValueArg<string> extendedShapes(
"", "extendedShapes", "All extended, optional shapes to use.", "", "extendedShapes", "All extended, optional shapes to use.", false, "GHX", "string", cmd
false, "GHX", "string", cmd
); );
tclap::ValueArg<string> dialogFile( tclap::ValueArg<string> dialogFile(
"d", "dialogFile", "A file containing the text of the dialog.", "d",
false, string(), "string", cmd "dialogFile",
"A file containing the text of the dialog.",
false,
string(),
"string",
cmd
); );
tclap::SwitchArg datUsePrestonBlair( tclap::SwitchArg datUsePrestonBlair(
"", "datUsePrestonBlair", "Only for dat exporter: uses the Preston Blair mouth shape names.", "",
cmd, false "datUsePrestonBlair",
"Only for dat exporter: uses the Preston Blair mouth shape names.",
cmd,
false
); );
tclap::ValueArg<double> datFrameRate( tclap::ValueArg<double> datFrameRate(
"", "datFrameRate", "Only for dat exporter: the desired frame rate.", "",
false, 24.0, "number", cmd "datFrameRate",
"Only for dat exporter: the desired frame rate.",
false,
24.0,
"number",
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( tclap::ValueArg<ExportFormat> exportFormat(
"f", "exportFormat", "The export format.", "f",
false, ExportFormat::Tsv, &exportFormatConstraint, cmd "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( tclap::ValueArg<RecognizerType> recognizerType(
"r", "recognizer", "The dialog recognizer.", "r",
false, RecognizerType::PocketSphinx, &recognizerConstraint, cmd "recognizer",
"The dialog recognizer.",
false,
RecognizerType::PocketSphinx,
&recognizerConstraint,
cmd
); );
tclap::UnlabeledValueArg<string> inputFileName( tclap::UnlabeledValueArg<string> inputFileName(
"inputFile", "The input file. Must be a sound file in WAVE format.", "inputFile", "The input file. Must be a sound file in WAVE format.", true, "", "string", cmd
true, "", "string", cmd
); );
try { try {
@ -247,8 +281,10 @@ int main(int platformArgc, char* platformArgv[]) {
); );
logging::log(StartEntry(inputFilePath)); logging::log(StartEntry(inputFilePath));
logging::debugFormat("Command line: {}", logging::debugFormat(
join(args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " ")); "Command line: {}",
join(args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " ")
);
try { try {
// On progress change: Create log message // On progress change: Create log message
@ -260,13 +296,13 @@ int main(int platformArgc, char* platformArgv[]) {
logging::info("Starting animation."); logging::info("Starting animation.");
JoiningContinuousTimeline<Shape> animation = animateWaveFile( JoiningContinuousTimeline<Shape> animation = animateWaveFile(
inputFilePath, inputFilePath,
dialogFile.isSet() dialogFile.isSet() ? readUtf8File(u8path(dialogFile.getValue()))
? readUtf8File(u8path(dialogFile.getValue()))
: boost::optional<string>(), : boost::optional<string>(),
*createRecognizer(recognizerType.getValue()), *createRecognizer(recognizerType.getValue()),
targetShapeSet, targetShapeSet,
maxThreadCount.getValue(), maxThreadCount.getValue(),
progressSink); progressSink
);
logging::info("Done animating."); logging::info("Done animating.");
// Export animation // Export animation
@ -282,9 +318,9 @@ int main(int platformArgc, char* platformArgv[]) {
logging::log(SuccessEntry()); logging::log(SuccessEntry());
} catch (...) { } catch (...) {
std::throw_with_nested( std::throw_with_nested(std::runtime_error(
std::runtime_error(fmt::format("Error processing file {}.", inputFilePath.u8string())) fmt::format("Error processing file {}.", inputFilePath.u8string())
); ));
} }
return 0; return 0;

View File

@ -4,13 +4,13 @@ using logging::Level;
using std::string; using std::string;
SemanticEntry::SemanticEntry(Level level, const string& message) : SemanticEntry::SemanticEntry(Level level, const string& message) :
Entry(level, message) Entry(level, message) {}
{}
StartEntry::StartEntry(const std::filesystem::path& inputFilePath) : StartEntry::StartEntry(const std::filesystem::path& inputFilePath) :
SemanticEntry(Level::Info, fmt::format("Application startup. Input file: {}.", inputFilePath.u8string())), SemanticEntry(
inputFilePath(inputFilePath) Level::Info, fmt::format("Application startup. Input file: {}.", inputFilePath.u8string())
{} ),
inputFilePath(inputFilePath) {}
std::filesystem::path StartEntry::getInputFilePath() const { std::filesystem::path StartEntry::getInputFilePath() const {
return inputFilePath; return inputFilePath;
@ -18,21 +18,18 @@ std::filesystem::path StartEntry::getInputFilePath() const {
ProgressEntry::ProgressEntry(double progress) : ProgressEntry::ProgressEntry(double progress) :
SemanticEntry(Level::Trace, fmt::format("Progress: {}%", static_cast<int>(progress * 100))), SemanticEntry(Level::Trace, fmt::format("Progress: {}%", static_cast<int>(progress * 100))),
progress(progress) progress(progress) {}
{}
double ProgressEntry::getProgress() const { double ProgressEntry::getProgress() const {
return progress; return progress;
} }
SuccessEntry::SuccessEntry() : SuccessEntry::SuccessEntry() :
SemanticEntry(Level::Info, "Application terminating normally.") SemanticEntry(Level::Info, "Application terminating normally.") {}
{}
FailureEntry::FailureEntry(const string& reason) : FailureEntry::FailureEntry(const string& reason) :
SemanticEntry(Level::Fatal, fmt::format("Application terminating with error: {}", reason)), SemanticEntry(Level::Fatal, fmt::format("Application terminating with error: {}", reason)),
reason(reason) reason(reason) {}
{}
string FailureEntry::getReason() const { string FailureEntry::getReason() const {
return reason; return reason;

View File

@ -1,7 +1,8 @@
#pragma once #pragma once
#include "logging/Entry.h"
#include <filesystem> #include <filesystem>
#include "logging/Entry.h"
// Marker class for semantic entries // Marker class for semantic entries
class SemanticEntry : public logging::Entry { class SemanticEntry : public logging::Entry {
public: public:
@ -12,6 +13,7 @@ class StartEntry : public SemanticEntry {
public: public:
StartEntry(const std::filesystem::path& inputFilePath); StartEntry(const std::filesystem::path& inputFilePath);
std::filesystem::path getInputFilePath() const; std::filesystem::path getInputFilePath() const;
private: private:
std::filesystem::path inputFilePath; std::filesystem::path inputFilePath;
}; };
@ -20,6 +22,7 @@ class ProgressEntry : public SemanticEntry {
public: public:
ProgressEntry(double progress); ProgressEntry(double progress);
double getProgress() const; double getProgress() const;
private: private:
double progress; double progress;
}; };
@ -33,6 +36,7 @@ class FailureEntry : public SemanticEntry {
public: public:
FailureEntry(const std::string& reason); FailureEntry(const std::string& reason);
std::string getReason() const; std::string getReason() const;
private: private:
std::string reason; std::string reason;
}; };

View File

@ -1,31 +1,32 @@
#include "sinks.h" #include "sinks.h"
#include "logging/sinks.h"
#include "logging/formatters.h"
#include "semanticEntries.h"
#include "tools/stringTools.h"
#include "core/appInfo.h"
#include <boost/utility/in_place_factory.hpp> #include <boost/utility/in_place_factory.hpp>
using std::string; #include "core/appInfo.h"
using std::make_shared; #include "logging/formatters.h"
using logging::Level; #include "logging/sinks.h"
using logging::StdErrSink; #include "semanticEntries.h"
using logging::SimpleConsoleFormatter; #include "tools/stringTools.h"
using boost::optional; using boost::optional;
using logging::Level;
using logging::SimpleConsoleFormatter;
using logging::StdErrSink;
using std::make_shared;
using std::string;
NiceStderrSink::NiceStderrSink(Level minLevel) : NiceStderrSink::NiceStderrSink(Level minLevel) :
minLevel(minLevel), minLevel(minLevel),
progress(0.0), progress(0.0),
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>())) innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>())) {}
{}
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 // For selected semantic entries, print a user-friendly message instead of
// the technical log message. // the technical log message.
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) { if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
std::cerr std::cerr << fmt::format(
<< fmt::format("Generating lip sync data for {}.", startEntry->getInputFilePath().u8string()) "Generating lip sync data for {}.", startEntry->getInputFilePath().u8string()
<< std::endl; ) << std::endl;
startProgressIndication(); startProgressIndication();
} else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) { } else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
assert(progressBar); assert(progressBar);
@ -62,8 +63,7 @@ void NiceStderrSink::resumeProgressIndication() {
QuietStderrSink::QuietStderrSink(Level minLevel) : QuietStderrSink::QuietStderrSink(Level minLevel) :
minLevel(minLevel), minLevel(minLevel),
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>())) innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>())) {}
{}
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
@ -75,7 +75,9 @@ void QuietStderrSink::receive(const logging::Entry& entry) {
if (quietSoFar) { if (quietSoFar) {
// This is the first message we print. Give a bit of context. // This is the first message we print. Give a bit of context.
const string intro = inputFilePath const string intro = inputFilePath
? fmt::format("{} {} processing file {}:", appName, appVersion, inputFilePath->u8string()) ? fmt::format(
"{} {} processing file {}:", appName, appVersion, inputFilePath->u8string()
)
: fmt::format("{} {}:", appName, appVersion); : fmt::format("{} {}:", appName, appVersion);
std::cerr << intro << std::endl; std::cerr << intro << std::endl;
quietSoFar = false; quietSoFar = false;
@ -85,8 +87,7 @@ void QuietStderrSink::receive(const logging::Entry& entry) {
} }
MachineReadableStderrSink::MachineReadableStderrSink(Level minLevel) : MachineReadableStderrSink::MachineReadableStderrSink(Level minLevel) :
minLevel(minLevel) minLevel(minLevel) {}
{}
string formatLogProperty(const logging::Entry& entry) { string formatLogProperty(const logging::Entry& entry) {
return fmt::format( return fmt::format(
@ -102,9 +103,7 @@ void MachineReadableStderrSink::receive(const logging::Entry& entry) {
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) { if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
const string file = escapeJsonString(startEntry->getInputFilePath().u8string()); const string file = escapeJsonString(startEntry->getInputFilePath().u8string());
line = fmt::format( line = fmt::format(
R"({{ "type": "start", "file": "{}", {} }})", R"({{ "type": "start", "file": "{}", {} }})", file, formatLogProperty(entry)
file,
formatLogProperty(entry)
); );
} else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&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);
@ -121,9 +120,7 @@ void MachineReadableStderrSink::receive(const logging::Entry& entry) {
} else if (const auto* 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( line = fmt::format(
R"({{ "type": "failure", "reason": "{}", {} }})", R"({{ "type": "failure", "reason": "{}", {} }})", reason, formatLogProperty(entry)
reason,
formatLogProperty(entry)
); );
} else { } else {
throw std::runtime_error("Unsupported type of semantic entry."); throw std::runtime_error("Unsupported type of semantic entry.");
@ -136,7 +133,8 @@ void MachineReadableStderrSink::receive(const logging::Entry& entry) {
if (line) { if (line) {
std::cerr << *line << std::endl; std::cerr << *line << std::endl;
// Make sure the stream is flushed so that applications listening to it get the line immediately // Make sure the stream is flushed so that applications listening to it get the line
// immediately
fflush(stderr); fflush(stderr);
} }
} }

View File

@ -1,16 +1,19 @@
#pragma once #pragma once
#include <filesystem>
#include "logging/Entry.h" #include "logging/Entry.h"
#include "logging/Sink.h" #include "logging/Sink.h"
#include "tools/ProgressBar.h" #include "tools/ProgressBar.h"
#include <filesystem>
// Prints nicely formatted progress to stderr. // Prints nicely formatted progress to stderr.
// Non-semantic entries are only printed if their log level at least matches the specified minimum level. // Non-semantic entries are only printed if their log level at least matches the specified minimum
// level.
class NiceStderrSink : public logging::Sink { class NiceStderrSink : public logging::Sink {
public: public:
NiceStderrSink(logging::Level minLevel); NiceStderrSink(logging::Level minLevel);
void receive(const logging::Entry& entry) override; void receive(const logging::Entry& entry) override;
private: private:
void startProgressIndication(); void startProgressIndication();
void interruptProgressIndication(); void interruptProgressIndication();
@ -28,6 +31,7 @@ class QuietStderrSink : public logging::Sink {
public: public:
QuietStderrSink(logging::Level minLevel); QuietStderrSink(logging::Level minLevel);
void receive(const logging::Entry& entry) override; void receive(const logging::Entry& entry) override;
private: private:
logging::Level minLevel; logging::Level minLevel;
bool quietSoFar = true; bool quietSoFar = true;
@ -36,11 +40,13 @@ private:
}; };
// Prints machine-readable progress to stderr. // Prints machine-readable progress to stderr.
// Non-semantic entries are only printed if their log level at least matches the specified minimum level. // Non-semantic entries are only printed if their log level at least matches the specified minimum
// level.
class MachineReadableStderrSink : public logging::Sink { class MachineReadableStderrSink : public logging::Sink {
public: public:
MachineReadableStderrSink(logging::Level minLevel); MachineReadableStderrSink(logging::Level minLevel);
void receive(const logging::Entry& entry) override; void receive(const logging::Entry& entry) override;
private: private:
logging::Level minLevel; logging::Level minLevel;
int lastProgressPercent = -1; int lastProgressPercent = -1;

View File

@ -12,17 +12,14 @@ public:
using Timeline<T, AutoJoin>::end; using Timeline<T, AutoJoin>::end;
BoundedTimeline() : BoundedTimeline() :
range(TimeRange::zero()) range(TimeRange::zero()) {}
{}
explicit BoundedTimeline(TimeRange range) : explicit BoundedTimeline(TimeRange range) :
range(range) range(range) {}
{}
template <typename InputIterator> template <typename InputIterator>
BoundedTimeline(TimeRange range, InputIterator first, InputIterator last) : BoundedTimeline(TimeRange range, InputIterator first, InputIterator last) :
range(range) range(range) {
{
for (auto it = first; it != last; ++it) { for (auto it = first; it != last; ++it) {
// Virtual function call in constructor. Derived constructors shouldn't call this one! // Virtual function call in constructor. Derived constructors shouldn't call this one!
BoundedTimeline::set(*it); BoundedTimeline::set(*it);
@ -31,12 +28,10 @@ public:
template <typename collection_type> template <typename collection_type>
BoundedTimeline(TimeRange range, collection_type collection) : BoundedTimeline(TimeRange range, collection_type collection) :
BoundedTimeline(range, collection.begin(), collection.end()) BoundedTimeline(range, collection.begin(), collection.end()) {}
{}
BoundedTimeline(TimeRange range, std::initializer_list<Timed<T>> initializerList) : BoundedTimeline(TimeRange range, std::initializer_list<Timed<T>> initializerList) :
BoundedTimeline(range, initializerList.begin(), initializerList.end()) BoundedTimeline(range, initializerList.begin(), initializerList.end()) {}
{}
TimeRange getRange() const override { TimeRange getRange() const override {
return range; return range;
@ -53,8 +48,7 @@ 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( valueRange.resize(
max(range.getStart(), valueRange.getStart()), max(range.getStart(), valueRange.getStart()), min(range.getEnd(), valueRange.getEnd())
min(range.getEnd(), valueRange.getEnd())
); );
return Timeline<T, AutoJoin>::set(timedValue); return Timeline<T, AutoJoin>::set(timedValue);

View File

@ -4,20 +4,17 @@
template <typename T, bool AutoJoin = false> template <typename T, bool AutoJoin = false>
class ContinuousTimeline : public BoundedTimeline<T, AutoJoin> { class ContinuousTimeline : public BoundedTimeline<T, AutoJoin> {
public: public:
ContinuousTimeline(TimeRange range, T defaultValue) : ContinuousTimeline(TimeRange range, T defaultValue) :
BoundedTimeline<T, AutoJoin>(range), BoundedTimeline<T, AutoJoin>(range),
defaultValue(defaultValue) defaultValue(defaultValue) {
{
// Virtual function call in constructor. Derived constructors shouldn't call this one! // Virtual function call in constructor. Derived constructors shouldn't call this one!
ContinuousTimeline::clear(range); ContinuousTimeline::clear(range);
} }
template <typename InputIterator> template <typename InputIterator>
ContinuousTimeline(TimeRange range, T defaultValue, InputIterator first, InputIterator last) : ContinuousTimeline(TimeRange range, T defaultValue, InputIterator first, InputIterator last) :
ContinuousTimeline(range, defaultValue) ContinuousTimeline(range, defaultValue) {
{
// Virtual function calls in constructor. Derived constructors shouldn't call this one! // Virtual function calls in constructor. Derived constructors shouldn't call this one!
for (auto it = first; it != last; ++it) { for (auto it = first; it != last; ++it) {
ContinuousTimeline::set(*it); ContinuousTimeline::set(*it);
@ -26,16 +23,12 @@ public:
template <typename collection_type> template <typename collection_type>
ContinuousTimeline(TimeRange range, T defaultValue, collection_type collection) : ContinuousTimeline(TimeRange range, T defaultValue, collection_type collection) :
ContinuousTimeline(range, defaultValue, collection.begin(), collection.end()) ContinuousTimeline(range, defaultValue, collection.begin(), collection.end()) {}
{}
ContinuousTimeline( ContinuousTimeline(
TimeRange range, TimeRange range, T defaultValue, std::initializer_list<Timed<T>> initializerList
T defaultValue,
std::initializer_list<Timed<T>> initializerList
) : ) :
ContinuousTimeline(range, defaultValue, initializerList.begin(), initializerList.end()) ContinuousTimeline(range, defaultValue, initializerList.begin(), initializerList.end()) {}
{}
using BoundedTimeline<T, AutoJoin>::clear; using BoundedTimeline<T, AutoJoin>::clear;

View File

@ -1,8 +1,10 @@
#include "TimeRange.h" #include "TimeRange.h"
#include <stdexcept>
#include <ostream>
#include <format.h> #include <format.h>
#include <ostream>
#include <stdexcept>
using time_type = TimeRange::time_type; using time_type = TimeRange::time_type;
TimeRange TimeRange::zero() { TimeRange TimeRange::zero() {
@ -12,18 +14,14 @@ TimeRange TimeRange::zero() {
TimeRange::TimeRange() : TimeRange::TimeRange() :
start(0_cs), start(0_cs),
end(0_cs) end(0_cs) {}
{}
TimeRange::TimeRange(time_type start, time_type end) : TimeRange::TimeRange(time_type start, time_type end) :
start(start), start(start),
end(end) end(end) {
{
if (start > end) { if (start > end) {
throw std::invalid_argument(fmt::format( throw std::invalid_argument(fmt::format(
"Time range start must not be less than end. Start: {0}, end: {1}", "Time range start must not be less than end. Start: {0}, end: {1}", start, end
start,
end
)); ));
} }
} }

View File

@ -37,6 +37,7 @@ public:
bool operator==(const TimeRange& rhs) const; bool operator==(const TimeRange& rhs) const;
bool operator!=(const TimeRange& rhs) const; bool operator!=(const TimeRange& rhs) const;
private: private:
time_type start, end; time_type start, end;
}; };

Some files were not shown because too many files have changed in this diff Show More