Auto-format code files
This commit is contained in:
parent
b365c4c1d5
commit
9d3782a08b
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
# Config file for gersemi, a CMake code formatter.
|
||||
|
||||
line_length: 100
|
||||
warn_about_unknown_commands: false
|
|
@ -1,3 +1,8 @@
|
|||
.vs/
|
||||
.vscode/
|
||||
*.user
|
||||
build/
|
||||
*.user
|
||||
venv/
|
||||
|
||||
__pycache__
|
||||
.doit.db.*
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
# Config file for Ruff, a Python code formatter.
|
||||
|
||||
line-length = 100
|
||||
|
||||
[format]
|
||||
quote-style = "single"
|
||||
skip-magic-trailing-comma = true
|
|
@ -13,14 +13,11 @@ add_subdirectory("extras/MagixVegas")
|
|||
add_subdirectory("extras/EsotericSoftwareSpine")
|
||||
|
||||
# Install misc. files
|
||||
install(
|
||||
FILES README.adoc LICENSE.md CHANGELOG.md
|
||||
DESTINATION .
|
||||
)
|
||||
install(FILES README.adoc LICENSE.md CHANGELOG.md DESTINATION .)
|
||||
|
||||
# Configure CPack
|
||||
function(get_short_system_name variable)
|
||||
if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
|
||||
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
|
||||
set(${variable} "macOS" PARENT_SCOPE)
|
||||
else()
|
||||
set(${variable} "${CMAKE_SYSTEM_NAME}" PARENT_SCOPE)
|
||||
|
|
|
@ -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
|
|
@ -1,11 +1,5 @@
|
|||
cmake_minimum_required(VERSION 3.2)
|
||||
|
||||
set(afterEffectsFiles
|
||||
"Rhubarb Lip Sync.jsx"
|
||||
"README.adoc"
|
||||
)
|
||||
set(afterEffectsFiles "Rhubarb Lip Sync.jsx" "README.adoc")
|
||||
|
||||
install(
|
||||
FILES ${afterEffectsFiles}
|
||||
DESTINATION "extras/AdobeAfterEffects"
|
||||
)
|
||||
install(FILES ${afterEffectsFiles} DESTINATION "extras/AdobeAfterEffects")
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
(function polyfill() {
|
||||
// prettier-ignore
|
||||
(function polyfill() {
|
||||
// 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});
|
||||
|
||||
|
@ -34,16 +35,16 @@
|
|||
|
||||
// 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")})}();
|
||||
})();
|
||||
})()
|
||||
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
function createGuid() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = Math.random() * 16 | 0;
|
||||
var v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0;
|
||||
var v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
@ -99,10 +100,10 @@ function isFrameVisible(compItem, frameNumber) {
|
|||
if (!compItem) return false;
|
||||
|
||||
var time = frameToTime(frameNumber + epsilon, compItem);
|
||||
var videoLayers = toArrayBase1(compItem.layers).filter(function(layer) {
|
||||
return layer.hasVideo;
|
||||
var videoLayers = toArrayBase1(compItem.layers).filter(function (layer) {
|
||||
return layer.hasVideo;
|
||||
});
|
||||
var result = videoLayers.find(function(layer) {
|
||||
var result = videoLayers.find(function (layer) {
|
||||
return layer.activeAtTime(time);
|
||||
});
|
||||
return Boolean(result);
|
||||
|
@ -119,12 +120,16 @@ function readTextFile(fileOrPath) {
|
|||
if (file.error) throw new Error('Error reading file "' + filePath + '": ' + file.error);
|
||||
}
|
||||
try {
|
||||
file.open('r'); check();
|
||||
file.encoding = 'UTF-8'; check();
|
||||
var result = file.read(); check();
|
||||
file.open('r');
|
||||
check();
|
||||
file.encoding = 'UTF-8';
|
||||
check();
|
||||
var result = file.read();
|
||||
check();
|
||||
return result;
|
||||
} 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);
|
||||
}
|
||||
try {
|
||||
file.open('w'); check();
|
||||
file.encoding = 'UTF-8'; check();
|
||||
file.write(text); check();
|
||||
file.open('w');
|
||||
check();
|
||||
file.encoding = 'UTF-8';
|
||||
check();
|
||||
file.write(text);
|
||||
check();
|
||||
} 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.
|
||||
function cliEscape(argument) {
|
||||
return osIsWindows
|
||||
? '"' + argument + '"'
|
||||
: "'" + argument.replace(/'/g, "'\\''") + "'";
|
||||
return osIsWindows ? '"' + argument + '"' : "'" + argument.replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
function exec(command) {
|
||||
|
@ -180,7 +187,8 @@ function execInWindow(command) {
|
|||
// execute a command, then close the Terminal window.
|
||||
// If you know a better solution, let me know!
|
||||
var escapedCommand = command.replace(/"/g, '\\"');
|
||||
var appleScript = '\
|
||||
var appleScript =
|
||||
'\
|
||||
tell application "Terminal" \
|
||||
-- Quit terminal \
|
||||
-- Yes, that\'s undesirable if there was an open window before. \
|
||||
|
@ -189,7 +197,9 @@ function execInWindow(command) {
|
|||
-- Open terminal \
|
||||
activate \
|
||||
-- Run command in new tab \
|
||||
set newTab to do script ("' + escapedCommand + '") \
|
||||
set newTab to do script ("' +
|
||||
escapedCommand +
|
||||
'") \
|
||||
-- Wait until command is done \
|
||||
tell newTab \
|
||||
repeat while busy \
|
||||
|
@ -220,20 +230,37 @@ function createResourceString(tree) {
|
|||
// Object containing functions to create control description trees.
|
||||
// For instance, `controls.StaticText({ text: 'Hello world' })`
|
||||
// returns `{ __type__: StaticText, text: 'Hello world' }`.
|
||||
var controlFunctions = (function() {
|
||||
var controlFunctions = (function () {
|
||||
var controlTypes = [
|
||||
// Strangely, 'dialog' and 'palette' need to start with a lower-case character
|
||||
['Dialog', 'dialog'], ['Palette', 'palette'],
|
||||
'Panel', 'Group', 'TabbedPanel', 'Tab', 'Button', 'IconButton', 'Image', 'StaticText',
|
||||
'EditText', 'Checkbox', 'RadioButton', 'Progressbar', 'Slider', 'Scrollbar', 'ListBox',
|
||||
'DropDownList', 'TreeView', 'ListItem', 'FlashPlayer'
|
||||
['Dialog', 'dialog'],
|
||||
['Palette', 'palette'],
|
||||
'Panel',
|
||||
'Group',
|
||||
'TabbedPanel',
|
||||
'Tab',
|
||||
'Button',
|
||||
'IconButton',
|
||||
'Image',
|
||||
'StaticText',
|
||||
'EditText',
|
||||
'Checkbox',
|
||||
'RadioButton',
|
||||
'Progressbar',
|
||||
'Slider',
|
||||
'Scrollbar',
|
||||
'ListBox',
|
||||
'DropDownList',
|
||||
'TreeView',
|
||||
'ListItem',
|
||||
'FlashPlayer'
|
||||
];
|
||||
var result = {};
|
||||
controlTypes.forEach(function(type){
|
||||
controlTypes.forEach(function (type) {
|
||||
var isArray = Array.isArray(type);
|
||||
var key = isArray ? type[0] : type;
|
||||
var value = isArray ? type[1] : type;
|
||||
result[key] = function(options) {
|
||||
result[key] = function (options) {
|
||||
return Object.assign({ __type__: value }, options);
|
||||
};
|
||||
});
|
||||
|
@ -257,7 +284,7 @@ function getItemPath(item) {
|
|||
// Selects the item within an item control whose text matches the specified text.
|
||||
// If no such item exists, selects the first item, if present.
|
||||
function selectByTextOrFirst(itemControl, text) {
|
||||
var targetItem = toArray(itemControl.items).find(function(item) {
|
||||
var targetItem = toArray(itemControl.items).find(function (item) {
|
||||
return item.text === text;
|
||||
});
|
||||
if (!targetItem && itemControl.items.length) {
|
||||
|
@ -269,7 +296,7 @@ function selectByTextOrFirst(itemControl, text) {
|
|||
}
|
||||
|
||||
function getAudioFileProjectItems() {
|
||||
var result = toArrayBase1(app.project.items).filter(function(item) {
|
||||
var result = toArrayBase1(app.project.items).filter(function (item) {
|
||||
var isAudioFootage = item instanceof FootageItem && item.hasAudio && !item.hasVideo;
|
||||
return isAudioFootage;
|
||||
});
|
||||
|
@ -283,9 +310,10 @@ var basicMouthShapeNames = mouthShapeNames.slice(0, basicMouthShapeCount);
|
|||
var extendedMouthShapeNames = mouthShapeNames.slice(basicMouthShapeCount);
|
||||
|
||||
function getMouthCompHelpTip() {
|
||||
var result = 'A composition containing the mouth shapes, one drawing per frame. They must be '
|
||||
+ 'arranged as follows:\n';
|
||||
mouthShapeNames.forEach(function(mouthShapeName, i) {
|
||||
var result =
|
||||
'A composition containing the mouth shapes, one drawing per frame. They must be ' +
|
||||
'arranged as follows:\n';
|
||||
mouthShapeNames.forEach(function (mouthShapeName, i) {
|
||||
var isOptional = i >= basicMouthShapeCount;
|
||||
result += '\n00:' + pad(i, 2) + '\t' + mouthShapeName + (isOptional ? ' (optional)' : '');
|
||||
});
|
||||
|
@ -294,7 +322,7 @@ function getMouthCompHelpTip() {
|
|||
|
||||
function createExtendedShapeCheckboxes() {
|
||||
var result = {};
|
||||
extendedMouthShapeNames.forEach(function(shapeName) {
|
||||
extendedMouthShapeNames.forEach(function (shapeName) {
|
||||
result[shapeName.toLowerCase()] = controlFunctions.Checkbox({
|
||||
text: shapeName,
|
||||
helpTip: 'Controls whether to use the optional ' + shapeName + ' shape.'
|
||||
|
@ -320,9 +348,10 @@ function createDialogWindow() {
|
|||
active: true
|
||||
}),
|
||||
value: DropDownList({
|
||||
helpTip: 'An audio file containing recorded dialog.\n'
|
||||
+ 'This field shows all audio files that exist in '
|
||||
+ 'your After Effects project.'
|
||||
helpTip:
|
||||
'An audio file containing recorded dialog.\n' +
|
||||
'This field shows all audio files that exist in ' +
|
||||
'your After Effects project.'
|
||||
})
|
||||
}),
|
||||
recognizer: Group({
|
||||
|
@ -337,8 +366,9 @@ function createDialogWindow() {
|
|||
properties: { multiline: true },
|
||||
characters: 60,
|
||||
minimumSize: [0, 100],
|
||||
helpTip: 'For better animation results, you can specify the text of '
|
||||
+ 'the recording here. This field is optional.'
|
||||
helpTip:
|
||||
'For better animation results, you can specify the text of ' +
|
||||
'the recording here. This field is optional.'
|
||||
})
|
||||
}),
|
||||
mouthComp: Group({
|
||||
|
@ -354,8 +384,9 @@ function createDialogWindow() {
|
|||
targetFolder: Group({
|
||||
label: StaticText({ text: 'Target folder:' }),
|
||||
value: DropDownList({
|
||||
helpTip: 'The project folder in which to create the animation '
|
||||
+ 'composition. The composition will be named like the audio file.'
|
||||
helpTip:
|
||||
'The project folder in which to create the animation ' +
|
||||
'composition. The composition will be named like the audio file.'
|
||||
})
|
||||
}),
|
||||
frameRate: Group({
|
||||
|
@ -366,8 +397,9 @@ function createDialogWindow() {
|
|||
}),
|
||||
auto: Checkbox({
|
||||
text: 'From mouth composition',
|
||||
helpTip: 'If checked, the animation will use the same frame rate as '
|
||||
+ 'the mouth composition.'
|
||||
helpTip:
|
||||
'If checked, the animation will use the same frame rate as ' +
|
||||
'the mouth composition.'
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
@ -400,13 +432,13 @@ function createDialogWindow() {
|
|||
animateButton: window.buttons.animate,
|
||||
cancelButton: window.buttons.cancel
|
||||
};
|
||||
extendedMouthShapeNames.forEach(function(shapeName) {
|
||||
extendedMouthShapeNames.forEach(function (shapeName) {
|
||||
controls['mouthShape' + shapeName] =
|
||||
window.settings.extendedMouthShapes[shapeName.toLowerCase()];
|
||||
});
|
||||
|
||||
// Add audio file options
|
||||
getAudioFileProjectItems().forEach(function(projectItem) {
|
||||
getAudioFileProjectItems().forEach(function (projectItem) {
|
||||
var listItem = controls.audioFile.add('item', getItemPath(projectItem));
|
||||
listItem.projectItem = projectItem;
|
||||
});
|
||||
|
@ -416,7 +448,7 @@ function createDialogWindow() {
|
|||
{ text: 'PocketSphinx (use for English recordings)', value: 'pocketSphinx' },
|
||||
{ text: 'Phonetic (use for non-English recordings)', value: 'phonetic' }
|
||||
];
|
||||
recognizerOptions.forEach(function(option) {
|
||||
recognizerOptions.forEach(function (option) {
|
||||
var listItem = controls.recognizer.add('item', option.text);
|
||||
listItem.value = option.value;
|
||||
});
|
||||
|
@ -425,7 +457,7 @@ function createDialogWindow() {
|
|||
var comps = toArrayBase1(app.project.items).filter(function (item) {
|
||||
return item instanceof CompItem;
|
||||
});
|
||||
comps.forEach(function(projectItem) {
|
||||
comps.forEach(function (projectItem) {
|
||||
var listItem = controls.mouthComp.add('item', getItemPath(projectItem));
|
||||
listItem.projectItem = projectItem;
|
||||
});
|
||||
|
@ -435,7 +467,7 @@ function createDialogWindow() {
|
|||
return item instanceof FolderItem;
|
||||
});
|
||||
projectFolders.unshift(app.project.rootFolder);
|
||||
projectFolders.forEach(function(projectFolder) {
|
||||
projectFolders.forEach(function (projectFolder) {
|
||||
var listItem = controls.targetFolder.add('item', getItemPath(projectFolder));
|
||||
listItem.projectItem = projectFolder;
|
||||
});
|
||||
|
@ -446,26 +478,29 @@ function createDialogWindow() {
|
|||
controls.dialogText.text = settings.dialogText || '';
|
||||
selectByTextOrFirst(controls.recognizer, settings.recognizer);
|
||||
selectByTextOrFirst(controls.mouthComp, settings.mouthComp);
|
||||
extendedMouthShapeNames.forEach(function(shapeName) {
|
||||
controls['mouthShape' + shapeName].value =
|
||||
(settings.extendedMouthShapes || {})[shapeName.toLowerCase()];
|
||||
extendedMouthShapeNames.forEach(function (shapeName) {
|
||||
controls['mouthShape' + shapeName].value = (settings.extendedMouthShapes || {})[
|
||||
shapeName.toLowerCase()
|
||||
];
|
||||
});
|
||||
selectByTextOrFirst(controls.targetFolder, settings.targetFolder);
|
||||
controls.frameRate.text = settings.frameRate || '';
|
||||
controls.autoFrameRate.value = settings.autoFrameRate;
|
||||
|
||||
// Align controls
|
||||
window.onShow = function() {
|
||||
window.onShow = function () {
|
||||
// Give uniform width to all labels
|
||||
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);
|
||||
groups.forEach(function (group) {
|
||||
group.children[0].size.width = maxLabelWidth;
|
||||
});
|
||||
|
||||
// Give uniform width to inputs
|
||||
var valueWidths = groups.map(function(group) {
|
||||
var valueWidths = groups.map(function (group) {
|
||||
return last(group.children).bounds.right - group.children[1].bounds.left;
|
||||
});
|
||||
var maxValueWidth = Math.max.apply(Math, valueWidths);
|
||||
|
@ -512,7 +547,7 @@ function createDialogWindow() {
|
|||
frameRate: Number(controls.frameRate.text),
|
||||
autoFrameRate: controls.autoFrameRate.value
|
||||
};
|
||||
extendedMouthShapeNames.forEach(function(shapeName) {
|
||||
extendedMouthShapeNames.forEach(function (shapeName) {
|
||||
settings.extendedMouthShapes[shapeName.toLowerCase()] =
|
||||
controls['mouthShape' + shapeName].value;
|
||||
});
|
||||
|
@ -541,18 +576,24 @@ function createDialogWindow() {
|
|||
var shapeName = mouthShapeNames[i];
|
||||
var required = i < basicMouthShapeCount || controls['mouthShape' + shapeName].value;
|
||||
if (required && !isFrameVisible(comp, i)) {
|
||||
return 'The mouth comp does not seem to contain an image for shape '
|
||||
+ shapeName + ' at frame ' + i + '.';
|
||||
return (
|
||||
'The mouth comp does not seem to contain an image for shape ' +
|
||||
shapeName +
|
||||
' at frame ' +
|
||||
i +
|
||||
'.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!comp.preserveNestedFrameRate) {
|
||||
var fix = Window.confirm(
|
||||
'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'
|
||||
+ 'Activate this setting now?',
|
||||
'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' +
|
||||
'Activate this setting now?',
|
||||
false,
|
||||
'Fix composition setting?');
|
||||
'Fix composition setting?'
|
||||
);
|
||||
if (fix) {
|
||||
app.beginUndoGroup(appName + ': Mouth composition setting');
|
||||
comp.preserveNestedFrameRate = true;
|
||||
|
@ -567,10 +608,14 @@ function createDialogWindow() {
|
|||
var match = version.match(/Rhubarb Lip Sync version ((\d+)\.(\d+).(\d+)(-[0-9A-Za-z-.]+)?)/);
|
||||
if (!match) {
|
||||
var instructions = osIsWindows
|
||||
? 'Make sure your PATH environment variable contains the ' + appName + ' '
|
||||
+ 'application directory.'
|
||||
: 'Make sure you have created this file as a symbolic link to the ' + appName + ' '
|
||||
+ 'executable (rhubarb).';
|
||||
? 'Make sure your PATH environment variable contains the ' +
|
||||
appName +
|
||||
' ' +
|
||||
'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;
|
||||
}
|
||||
var versionString = match[1];
|
||||
|
@ -579,15 +624,32 @@ function createDialogWindow() {
|
|||
var requiredMajor = 1;
|
||||
var minRequiredMinor = 9;
|
||||
if (major != requiredMajor || minor < minRequiredMinor) {
|
||||
return 'This script requires ' + appName + ' ' + requiredMajor + '.' + minRequiredMinor
|
||||
+ '.0 or a later ' + requiredMajor + '.x version. '
|
||||
+ 'Your installed version is ' + versionString + ', which is not compatible.';
|
||||
return (
|
||||
'This script requires ' +
|
||||
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,
|
||||
targetProjectFolder, frameRate)
|
||||
{
|
||||
function generateMouthCues(
|
||||
audioFileFootage,
|
||||
recognizer,
|
||||
dialogText,
|
||||
mouthComp,
|
||||
extendedMouthShapeNames,
|
||||
targetProjectFolder,
|
||||
frameRate
|
||||
) {
|
||||
var basePath = Folder.temp.fsName + '/' + createGuid();
|
||||
var dialogFile = new File(basePath + '.txt');
|
||||
var logFile = new File(basePath + '.log');
|
||||
|
@ -597,15 +659,16 @@ function createDialogWindow() {
|
|||
writeTextFile(dialogFile, dialogText);
|
||||
|
||||
// Create command line
|
||||
var commandLine = rhubarbPath
|
||||
+ ' --dialogFile ' + cliEscape(dialogFile.fsName)
|
||||
+ ' --recognizer ' + recognizer
|
||||
+ ' --exportFormat json'
|
||||
+ ' --extendedShapes ' + cliEscape(extendedMouthShapeNames.join(''))
|
||||
+ ' --logFile ' + cliEscape(logFile.fsName)
|
||||
+ ' --logLevel fatal'
|
||||
+ ' --output ' + cliEscape(jsonFile.fsName)
|
||||
+ ' ' + cliEscape(audioFileFootage.file.fsName);
|
||||
var commandLine =
|
||||
rhubarbPath +
|
||||
(' --dialogFile ' + cliEscape(dialogFile.fsName)) +
|
||||
(' --recognizer ' + recognizer) +
|
||||
' --exportFormat json' +
|
||||
(' --extendedShapes ' + cliEscape(extendedMouthShapeNames.join(''))) +
|
||||
(' --logFile ' + cliEscape(logFile.fsName)) +
|
||||
' --logLevel fatal' +
|
||||
(' --output ' + cliEscape(jsonFile.fsName)) +
|
||||
(' ' + cliEscape(audioFileFootage.file.fsName));
|
||||
|
||||
// Run Rhubarb
|
||||
execInWindow(commandLine);
|
||||
|
@ -635,9 +698,13 @@ function createDialogWindow() {
|
|||
}
|
||||
}
|
||||
|
||||
function animateMouthCues(mouthCues, audioFileFootage, mouthComp, targetProjectFolder,
|
||||
frameRate)
|
||||
{
|
||||
function animateMouthCues(
|
||||
mouthCues,
|
||||
audioFileFootage,
|
||||
mouthComp,
|
||||
targetProjectFolder,
|
||||
frameRate
|
||||
) {
|
||||
// Find an unconflicting comp name
|
||||
// ... strip extension, if present
|
||||
var baseName = audioFileFootage.name.match(/^(.*?)(\..*)?$/i)[1];
|
||||
|
@ -645,14 +712,24 @@ function createDialogWindow() {
|
|||
// ... add numeric suffix, if needed
|
||||
var existingItems = toArrayBase1(targetProjectFolder.items);
|
||||
var counter = 1;
|
||||
while (existingItems.some(function(item) { return item.name === compName; })) {
|
||||
while (
|
||||
existingItems.some(function (item) {
|
||||
return item.name === compName;
|
||||
})
|
||||
) {
|
||||
counter++;
|
||||
compName = baseName + ' ' + counter;
|
||||
}
|
||||
|
||||
// Create new comp
|
||||
var comp = targetProjectFolder.items.addComp(compName, mouthComp.width, mouthComp.height,
|
||||
mouthComp.pixelAspect, audioFileFootage.duration, frameRate);
|
||||
var comp = targetProjectFolder.items.addComp(
|
||||
compName,
|
||||
mouthComp.width,
|
||||
mouthComp.height,
|
||||
mouthComp.pixelAspect,
|
||||
audioFileFootage.duration,
|
||||
frameRate
|
||||
);
|
||||
|
||||
// Show new comp
|
||||
comp.openInViewer();
|
||||
|
@ -669,7 +746,7 @@ function createDialogWindow() {
|
|||
var timeRemap = mouthLayer['Time Remap'];
|
||||
// Enabling time remapping automatically adds two keys. Remove the second.
|
||||
timeRemap.removeKey(2);
|
||||
mouthCues.mouthCues.forEach(function(mouthCue) {
|
||||
mouthCues.mouthCues.forEach(function (mouthCue) {
|
||||
// Round down keyframe time. In animation, earlier is better than later.
|
||||
// Set keyframe time to *just before* the exact frame to prevent rounding errors
|
||||
var frame = Math.floor(timeToFrame(mouthCue.start, comp));
|
||||
|
@ -684,16 +761,28 @@ function createDialogWindow() {
|
|||
}
|
||||
}
|
||||
|
||||
function animate(audioFileFootage, recognizer, dialogText, mouthComp, extendedMouthShapeNames,
|
||||
targetProjectFolder, frameRate)
|
||||
{
|
||||
function animate(
|
||||
audioFileFootage,
|
||||
recognizer,
|
||||
dialogText,
|
||||
mouthComp,
|
||||
extendedMouthShapeNames,
|
||||
targetProjectFolder,
|
||||
frameRate
|
||||
) {
|
||||
try {
|
||||
var mouthCues = generateMouthCues(audioFileFootage, recognizer, dialogText, mouthComp,
|
||||
extendedMouthShapeNames, targetProjectFolder, frameRate);
|
||||
var mouthCues = generateMouthCues(
|
||||
audioFileFootage,
|
||||
recognizer,
|
||||
dialogText,
|
||||
mouthComp,
|
||||
extendedMouthShapeNames,
|
||||
targetProjectFolder,
|
||||
frameRate
|
||||
);
|
||||
|
||||
app.beginUndoGroup(appName + ': Animation');
|
||||
animateMouthCues(mouthCues, audioFileFootage, mouthComp, targetProjectFolder,
|
||||
frameRate);
|
||||
animateMouthCues(mouthCues, audioFileFootage, mouthComp, targetProjectFolder, frameRate);
|
||||
app.endUndoGroup();
|
||||
} catch (e) {
|
||||
Window.alert(e.message, appName, true);
|
||||
|
@ -707,7 +796,7 @@ function createDialogWindow() {
|
|||
controls.recognizer.onChange = update;
|
||||
controls.dialogText.onChanging = update;
|
||||
controls.mouthComp.onChange = update;
|
||||
extendedMouthShapeNames.forEach(function(shapeName) {
|
||||
extendedMouthShapeNames.forEach(function (shapeName) {
|
||||
controls['mouthShape' + shapeName].onClick = update;
|
||||
});
|
||||
controls.targetFolder.onChange = update;
|
||||
|
@ -715,7 +804,7 @@ function createDialogWindow() {
|
|||
controls.autoFrameRate.onClick = update;
|
||||
|
||||
// Handle animation
|
||||
controls.animateButton.onClick = function() {
|
||||
controls.animateButton.onClick = function () {
|
||||
var validationError = validate();
|
||||
if (typeof validationError === 'string') {
|
||||
if (validationError) {
|
||||
|
@ -728,7 +817,7 @@ function createDialogWindow() {
|
|||
controls.recognizer.selection.value,
|
||||
controls.dialogText.text || '',
|
||||
controls.mouthComp.selection.projectItem,
|
||||
extendedMouthShapeNames.filter(function(shapeName) {
|
||||
extendedMouthShapeNames.filter(function (shapeName) {
|
||||
return controls['mouthShape' + shapeName].value;
|
||||
}),
|
||||
controls.targetFolder.selection.projectItem,
|
||||
|
@ -738,7 +827,7 @@ function createDialogWindow() {
|
|||
};
|
||||
|
||||
// Handle cancelation
|
||||
controls.cancelButton.onClick = function() {
|
||||
controls.cancelButton.onClick = function () {
|
||||
window.close();
|
||||
};
|
||||
|
||||
|
@ -747,9 +836,12 @@ function createDialogWindow() {
|
|||
|
||||
function checkPreconditions() {
|
||||
if (!canWriteFiles()) {
|
||||
Window.alert('This script requires file system access.\n\n'
|
||||
+ 'Please enable Preferences > General > Allow Scripts to Write Files and Access Network.',
|
||||
appName, true);
|
||||
Window.alert(
|
||||
'This script requires file system access.\n\n' +
|
||||
'Please enable Preferences > General > Allow Scripts to Write Files and Access Network.',
|
||||
appName,
|
||||
true
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
cmake_minimum_required(VERSION 3.2)
|
||||
|
||||
add_custom_target(
|
||||
rhubarbForSpine ALL
|
||||
rhubarbForSpine
|
||||
ALL
|
||||
"./gradlew" "build"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
COMMENT "Building Rhubarb for Spine through Gradle."
|
||||
)
|
||||
|
||||
install(
|
||||
DIRECTORY "build/libs/"
|
||||
DESTINATION "extras/EsotericSoftwareSpine"
|
||||
)
|
||||
install(DIRECTORY "build/libs/" DESTINATION "extras/EsotericSoftwareSpine")
|
||||
|
||||
install(
|
||||
FILES README.adoc
|
||||
DESTINATION "extras/EsotericSoftwareSpine"
|
||||
)
|
||||
install(FILES README.adoc DESTINATION "extras/EsotericSoftwareSpine")
|
||||
|
|
|
@ -8,7 +8,4 @@ set(vegasFiles
|
|||
"README.adoc"
|
||||
)
|
||||
|
||||
install(
|
||||
FILES ${vegasFiles}
|
||||
DESTINATION "extras/MagixVegas"
|
||||
)
|
||||
install(FILES ${vegasFiles} DESTINATION "extras/MagixVegas")
|
||||
|
|
|
@ -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
|
|
@ -10,7 +10,7 @@ set(CMAKE_CXX_STANDARD 17)
|
|||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Enable POSIX threads
|
||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
|
||||
endif()
|
||||
|
||||
|
@ -59,30 +59,31 @@ include_directories(SYSTEM ${Boost_INCLUDE_DIRS})
|
|||
link_libraries(${Boost_LIBRARIES}) # Just about every project needs Boost
|
||||
|
||||
# ... C++ Format
|
||||
FILE(GLOB cppFormatFiles "lib/cppformat/*.cc")
|
||||
file(GLOB cppFormatFiles "lib/cppformat/*.cc")
|
||||
add_library(cppFormat ${cppFormatFiles})
|
||||
target_include_directories(cppFormat SYSTEM PUBLIC "lib/cppformat")
|
||||
target_compile_options(cppFormat PRIVATE ${disableWarningsFlags})
|
||||
set_target_properties(cppFormat PROPERTIES FOLDER lib)
|
||||
|
||||
# ... 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})
|
||||
target_include_directories(sphinxbase SYSTEM PUBLIC
|
||||
"lib/sphinxbase-rev13216/include"
|
||||
"lib/sphinxbase-rev13216/src"
|
||||
"lib/sphinx_config"
|
||||
target_include_directories(
|
||||
sphinxbase
|
||||
SYSTEM
|
||||
PUBLIC "lib/sphinxbase-rev13216/include" "lib/sphinxbase-rev13216/src" "lib/sphinx_config"
|
||||
)
|
||||
target_compile_options(sphinxbase PRIVATE ${disableWarningsFlags})
|
||||
target_compile_definitions(sphinxbase PUBLIC __SPHINXBASE_EXPORT_H__=1 SPHINXBASE_EXPORT=) # Compile as static lib
|
||||
set_target_properties(sphinxbase PROPERTIES FOLDER lib)
|
||||
|
||||
# ... PocketSphinx
|
||||
FILE(GLOB pocketSphinxFiles "lib/pocketsphinx-rev13216/src/libpocketsphinx/*.c")
|
||||
file(GLOB pocketSphinxFiles "lib/pocketsphinx-rev13216/src/libpocketsphinx/*.c")
|
||||
add_library(pocketSphinx ${pocketSphinxFiles})
|
||||
target_include_directories(pocketSphinx SYSTEM PUBLIC
|
||||
"lib/pocketsphinx-rev13216/include"
|
||||
"lib/pocketsphinx-rev13216/src/libpocketsphinx"
|
||||
target_include_directories(
|
||||
pocketSphinx
|
||||
SYSTEM
|
||||
PUBLIC "lib/pocketsphinx-rev13216/include" "lib/pocketsphinx-rev13216/src/libpocketsphinx"
|
||||
)
|
||||
target_link_libraries(pocketSphinx sphinxbase)
|
||||
target_compile_options(pocketSphinx PRIVATE ${disableWarningsFlags})
|
||||
|
@ -129,10 +130,10 @@ set(webRtcFiles
|
|||
add_library(webRtc ${webRtcFiles})
|
||||
target_include_directories(webRtc SYSTEM PUBLIC "lib/webrtc-8d2248ff")
|
||||
target_compile_options(webRtc PRIVATE ${disableWarningsFlags})
|
||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
target_compile_options(webRtc PRIVATE -pthread -lpthread)
|
||||
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
target_compile_options(webRtc PRIVATE -pthread -lpthread)
|
||||
endif()
|
||||
if (NOT WIN32)
|
||||
if(NOT WIN32)
|
||||
target_compile_definitions(webRtc PRIVATE WEBRTC_POSIX)
|
||||
endif()
|
||||
set_target_properties(webRtc PROPERTIES FOLDER lib)
|
||||
|
@ -203,34 +204,26 @@ set(fliteFiles
|
|||
lib/flite-1.4/src/utils/cst_val_user.c
|
||||
)
|
||||
add_library(flite ${fliteFiles})
|
||||
target_include_directories(flite SYSTEM PUBLIC
|
||||
"lib/flite-1.4/include"
|
||||
"lib/flite-1.4"
|
||||
)
|
||||
target_include_directories(flite SYSTEM PUBLIC "lib/flite-1.4/include" "lib/flite-1.4")
|
||||
target_compile_options(flite PRIVATE ${disableWarningsFlags})
|
||||
set_target_properties(flite PROPERTIES FOLDER lib)
|
||||
|
||||
# ... UTF8-CPP
|
||||
add_library(utfcpp
|
||||
lib/header-only.c
|
||||
lib/utfcpp-2.3.5/source/utf8.h
|
||||
)
|
||||
add_library(utfcpp 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_compile_options(utfcpp PRIVATE ${disableWarningsFlags})
|
||||
set_target_properties(utfcpp PROPERTIES FOLDER lib)
|
||||
|
||||
# ... utf8proc
|
||||
add_library(utf8proc
|
||||
lib/utf8proc-2.2.0/utf8proc.c
|
||||
lib/utf8proc-2.2.0/utf8proc.h
|
||||
)
|
||||
add_library(utf8proc 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_compile_options(utf8proc PRIVATE ${disableWarningsFlags})
|
||||
target_compile_definitions(utf8proc PUBLIC UTF8PROC_STATIC=1) # Compile as static lib
|
||||
set_target_properties(utf8proc PROPERTIES FOLDER lib)
|
||||
|
||||
# ... Ogg
|
||||
add_library(ogg
|
||||
add_library(
|
||||
ogg
|
||||
lib/ogg-1.3.3/include/ogg/ogg.h
|
||||
lib/ogg-1.3.3/src/bitwise.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)
|
||||
|
||||
# ... Vorbis
|
||||
add_library(vorbis
|
||||
add_library(
|
||||
vorbis
|
||||
lib/vorbis-1.3.6/include/vorbis/vorbisfile.h
|
||||
lib/vorbis-1.3.6/lib/bitrate.c
|
||||
lib/vorbis-1.3.6/lib/block.c
|
||||
|
@ -263,9 +257,7 @@ add_library(vorbis
|
|||
lib/vorbis-1.3.6/lib/window.c
|
||||
)
|
||||
target_include_directories(vorbis SYSTEM PUBLIC "lib/vorbis-1.3.6/include")
|
||||
target_link_libraries(vorbis
|
||||
ogg
|
||||
)
|
||||
target_link_libraries(vorbis ogg)
|
||||
target_compile_options(vorbis PRIVATE ${disableWarningsFlags})
|
||||
set_target_properties(vorbis PROPERTIES FOLDER lib)
|
||||
|
||||
|
@ -274,7 +266,8 @@ set_target_properties(vorbis PROPERTIES FOLDER lib)
|
|||
include_directories("src")
|
||||
|
||||
# ... rhubarb-animation
|
||||
add_library(rhubarb-animation
|
||||
add_library(
|
||||
rhubarb-animation
|
||||
src/animation/animationRules.cpp
|
||||
src/animation/animationRules.h
|
||||
src/animation/mouthAnimation.cpp
|
||||
|
@ -296,14 +289,11 @@ add_library(rhubarb-animation
|
|||
src/animation/tweening.h
|
||||
)
|
||||
target_include_directories(rhubarb-animation PRIVATE "src/animation")
|
||||
target_link_libraries(rhubarb-animation
|
||||
rhubarb-core
|
||||
rhubarb-logging
|
||||
rhubarb-time
|
||||
)
|
||||
target_link_libraries(rhubarb-animation rhubarb-core rhubarb-logging rhubarb-time)
|
||||
|
||||
# ... rhubarb-audio
|
||||
add_library(rhubarb-audio
|
||||
add_library(
|
||||
rhubarb-audio
|
||||
src/audio/AudioClip.cpp
|
||||
src/audio/AudioClip.h
|
||||
src/audio/audioFileReading.cpp
|
||||
|
@ -327,7 +317,8 @@ add_library(rhubarb-audio
|
|||
src/audio/waveFileWriting.h
|
||||
)
|
||||
target_include_directories(rhubarb-audio PRIVATE "src/audio")
|
||||
target_link_libraries(rhubarb-audio
|
||||
target_link_libraries(
|
||||
rhubarb-audio
|
||||
webRtc
|
||||
vorbis
|
||||
rhubarb-logging
|
||||
|
@ -337,7 +328,8 @@ target_link_libraries(rhubarb-audio
|
|||
|
||||
# ... rhubarb-core
|
||||
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
|
||||
src/core/appInfo.h
|
||||
src/core/Phone.cpp
|
||||
|
@ -346,12 +338,11 @@ add_library(rhubarb-core
|
|||
src/core/Shape.h
|
||||
)
|
||||
target_include_directories(rhubarb-core PRIVATE "src/core")
|
||||
target_link_libraries(rhubarb-core
|
||||
rhubarb-tools
|
||||
)
|
||||
target_link_libraries(rhubarb-core rhubarb-tools)
|
||||
|
||||
# ... rhubarb-exporters
|
||||
add_library(rhubarb-exporters
|
||||
add_library(
|
||||
rhubarb-exporters
|
||||
src/exporters/DatExporter.cpp
|
||||
src/exporters/DatExporter.h
|
||||
src/exporters/Exporter.h
|
||||
|
@ -365,19 +356,13 @@ add_library(rhubarb-exporters
|
|||
src/exporters/XmlExporter.h
|
||||
)
|
||||
target_include_directories(rhubarb-exporters PRIVATE "src/exporters")
|
||||
target_link_libraries(rhubarb-exporters
|
||||
rhubarb-animation
|
||||
rhubarb-core
|
||||
rhubarb-time
|
||||
)
|
||||
target_link_libraries(rhubarb-exporters rhubarb-animation rhubarb-core rhubarb-time)
|
||||
|
||||
# ... rhubarb-lib
|
||||
add_library(rhubarb-lib
|
||||
src/lib/rhubarbLib.cpp
|
||||
src/lib/rhubarbLib.h
|
||||
)
|
||||
add_library(rhubarb-lib src/lib/rhubarbLib.cpp src/lib/rhubarbLib.h)
|
||||
target_include_directories(rhubarb-lib PRIVATE "src/lib")
|
||||
target_link_libraries(rhubarb-lib
|
||||
target_link_libraries(
|
||||
rhubarb-lib
|
||||
rhubarb-animation
|
||||
rhubarb-audio
|
||||
rhubarb-core
|
||||
|
@ -387,7 +372,8 @@ target_link_libraries(rhubarb-lib
|
|||
)
|
||||
|
||||
# ... rhubarb-logging
|
||||
add_library(rhubarb-logging
|
||||
add_library(
|
||||
rhubarb-logging
|
||||
src/logging/Entry.cpp
|
||||
src/logging/Entry.h
|
||||
src/logging/Formatter.h
|
||||
|
@ -402,12 +388,11 @@ add_library(rhubarb-logging
|
|||
src/logging/sinks.h
|
||||
)
|
||||
target_include_directories(rhubarb-logging PRIVATE "src/logging")
|
||||
target_link_libraries(rhubarb-logging
|
||||
rhubarb-tools
|
||||
)
|
||||
target_link_libraries(rhubarb-logging rhubarb-tools)
|
||||
|
||||
# ... rhubarb-recognition
|
||||
add_library(rhubarb-recognition
|
||||
add_library(
|
||||
rhubarb-recognition
|
||||
src/recognition/g2p.cpp
|
||||
src/recognition/g2p.h
|
||||
src/recognition/languageModels.cpp
|
||||
|
@ -423,7 +408,8 @@ add_library(rhubarb-recognition
|
|||
src/recognition/tokenization.h
|
||||
)
|
||||
target_include_directories(rhubarb-recognition PRIVATE "src/recognition")
|
||||
target_link_libraries(rhubarb-recognition
|
||||
target_link_libraries(
|
||||
rhubarb-recognition
|
||||
flite
|
||||
pocketSphinx
|
||||
rhubarb-audio
|
||||
|
@ -432,7 +418,8 @@ target_link_libraries(rhubarb-recognition
|
|||
)
|
||||
|
||||
# ... rhubarb-time
|
||||
add_library(rhubarb-time
|
||||
add_library(
|
||||
rhubarb-time
|
||||
src/time/BoundedTimeline.h
|
||||
src/time/centiseconds.cpp
|
||||
src/time/centiseconds.h
|
||||
|
@ -444,13 +431,11 @@ add_library(rhubarb-time
|
|||
src/time/TimeRange.h
|
||||
)
|
||||
target_include_directories(rhubarb-time PRIVATE "src/time")
|
||||
target_link_libraries(rhubarb-time
|
||||
cppFormat
|
||||
rhubarb-logging
|
||||
)
|
||||
target_link_libraries(rhubarb-time cppFormat rhubarb-logging)
|
||||
|
||||
# ... rhubarb-tools
|
||||
add_library(rhubarb-tools
|
||||
add_library(
|
||||
rhubarb-tools
|
||||
src/tools/array.h
|
||||
src/tools/EnumConverter.h
|
||||
src/tools/exceptions.cpp
|
||||
|
@ -481,15 +466,11 @@ add_library(rhubarb-tools
|
|||
src/tools/tupleHash.h
|
||||
)
|
||||
target_include_directories(rhubarb-tools PRIVATE "src/tools")
|
||||
target_link_libraries(rhubarb-tools
|
||||
cppFormat
|
||||
whereami
|
||||
utfcpp
|
||||
utf8proc
|
||||
)
|
||||
target_link_libraries(rhubarb-tools cppFormat whereami utfcpp utf8proc)
|
||||
|
||||
# Define Rhubarb executable
|
||||
add_executable(rhubarb
|
||||
add_executable(
|
||||
rhubarb
|
||||
src/rhubarb/main.cpp
|
||||
src/rhubarb/ExportFormat.cpp
|
||||
src/rhubarb/ExportFormat.h
|
||||
|
@ -501,10 +482,7 @@ add_executable(rhubarb
|
|||
src/rhubarb/sinks.h
|
||||
)
|
||||
target_include_directories(rhubarb PUBLIC "src/rhubarb")
|
||||
target_link_libraries(rhubarb
|
||||
rhubarb-exporters
|
||||
rhubarb-lib
|
||||
)
|
||||
target_link_libraries(rhubarb rhubarb-exporters rhubarb-lib)
|
||||
target_compile_options(rhubarb PUBLIC ${enableWarningsFlags})
|
||||
|
||||
# Define test project
|
||||
|
@ -521,7 +499,8 @@ set(TEST_FILES
|
|||
tests/WaveFileReaderTests.cpp
|
||||
)
|
||||
add_executable(runTests ${TEST_FILES})
|
||||
target_link_libraries(runTests
|
||||
target_link_libraries(
|
||||
runTests
|
||||
gtest
|
||||
gmock
|
||||
gmock_main
|
||||
|
@ -534,23 +513,24 @@ target_link_libraries(runTests
|
|||
function(copy_and_install sourceGlob relativeTargetDirectory)
|
||||
# Set `sourcePaths`
|
||||
file(GLOB sourcePaths "${sourceGlob}")
|
||||
|
||||
|
||||
foreach(sourcePath ${sourcePaths})
|
||||
if(NOT IS_DIRECTORY ${sourcePath})
|
||||
# Set `fileName`
|
||||
get_filename_component(fileName "${sourcePath}" NAME)
|
||||
|
||||
# Copy file during build
|
||||
add_custom_command(TARGET rhubarb POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${sourcePath}" "$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}"
|
||||
add_custom_command(
|
||||
TARGET rhubarb
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E copy "${sourcePath}"
|
||||
"$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}"
|
||||
COMMENT "Creating '${relativeTargetDirectory}/${fileName}'"
|
||||
)
|
||||
|
||||
# Install file
|
||||
install(
|
||||
FILES "${sourcePath}"
|
||||
DESTINATION "${relativeTargetDirectory}"
|
||||
)
|
||||
install(FILES "${sourcePath}" DESTINATION "${relativeTargetDirectory}")
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
@ -559,15 +539,19 @@ endfunction()
|
|||
function(copy sourceGlob relativeTargetDirectory)
|
||||
# Set `sourcePaths`
|
||||
file(GLOB sourcePaths "${sourceGlob}")
|
||||
|
||||
|
||||
foreach(sourcePath ${sourcePaths})
|
||||
if(NOT IS_DIRECTORY ${sourcePath})
|
||||
# Set `fileName`
|
||||
get_filename_component(fileName "${sourcePath}" NAME)
|
||||
|
||||
# Copy file during build
|
||||
add_custom_command(TARGET rhubarb POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${sourcePath}" "$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}"
|
||||
add_custom_command(
|
||||
TARGET rhubarb
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E copy "${sourcePath}"
|
||||
"$<TARGET_FILE_DIR:rhubarb>/${relativeTargetDirectory}/${fileName}"
|
||||
COMMENT "Creating '${relativeTargetDirectory}/${fileName}'"
|
||||
)
|
||||
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")
|
||||
|
||||
install(
|
||||
TARGETS rhubarb
|
||||
RUNTIME
|
||||
DESTINATION .
|
||||
)
|
||||
install(TARGETS rhubarb RUNTIME DESTINATION .)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
#include "ShapeRule.h"
|
||||
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <utility>
|
||||
|
||||
#include "time/ContinuousTimeline.h"
|
||||
|
||||
using boost::optional;
|
||||
using boost::adaptors::transformed;
|
||||
|
||||
template<typename T, bool AutoJoin>
|
||||
template <typename T, bool AutoJoin>
|
||||
ContinuousTimeline<optional<T>, AutoJoin> boundedTimelinetoContinuousOptional(
|
||||
const BoundedTimeline<T, AutoJoin>& timeline
|
||||
) {
|
||||
|
@ -19,18 +21,13 @@ ContinuousTimeline<optional<T>, AutoJoin> boundedTimelinetoContinuousOptional(
|
|||
};
|
||||
}
|
||||
|
||||
ShapeRule::ShapeRule(
|
||||
ShapeSet shapeSet,
|
||||
optional<Phone> phone,
|
||||
TimeRange phoneTiming
|
||||
) :
|
||||
ShapeRule::ShapeRule(ShapeSet shapeSet, optional<Phone> phone, TimeRange phoneTiming) :
|
||||
shapeSet(std::move(shapeSet)),
|
||||
phone(std::move(phone)),
|
||||
phoneTiming(phoneTiming)
|
||||
{}
|
||||
phoneTiming(phoneTiming) {}
|
||||
|
||||
ShapeRule ShapeRule::getInvalid() {
|
||||
return { {}, boost::none, { 0_cs, 0_cs } };
|
||||
return {{}, boost::none, {0_cs, 0_cs}};
|
||||
}
|
||||
|
||||
bool ShapeRule::operator==(const ShapeRule& rhs) const {
|
||||
|
@ -42,8 +39,7 @@ bool ShapeRule::operator!=(const ShapeRule& rhs) const {
|
|||
}
|
||||
|
||||
bool ShapeRule::operator<(const ShapeRule& rhs) const {
|
||||
return shapeSet < rhs.shapeSet
|
||||
|| phone < rhs.phone
|
||||
return shapeSet < rhs.shapeSet || phone < rhs.phone
|
||||
|| phoneTiming.getStart() < rhs.phoneTiming.getStart()
|
||||
|| phoneTiming.getEnd() < rhs.phoneTiming.getEnd();
|
||||
}
|
||||
|
@ -54,8 +50,7 @@ ContinuousTimeline<ShapeRule> getShapeRules(const BoundedTimeline<Phone>& phones
|
|||
|
||||
// Create timeline of shape rules
|
||||
ContinuousTimeline<ShapeRule> shapeRules(
|
||||
phones.getRange(),
|
||||
{ { Shape::X }, boost::none, { 0_cs, 0_cs } }
|
||||
phones.getRange(), {{Shape::X}, boost::none, {0_cs, 0_cs}}
|
||||
);
|
||||
centiseconds previousDuration = 0_cs;
|
||||
for (const auto& timedPhone : continuousPhones) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "core/Phone.h"
|
||||
#include "animationRules.h"
|
||||
#include "core/Phone.h"
|
||||
#include "time/BoundedTimeline.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "time/TimeRange.h"
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
#include "animationRules.h"
|
||||
#include <boost/algorithm/clamp.hpp>
|
||||
#include "shapeShorthands.h"
|
||||
#include "tools/array.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
|
||||
using std::chrono::duration_cast;
|
||||
using boost::algorithm::clamp;
|
||||
#include <boost/algorithm/clamp.hpp>
|
||||
|
||||
#include "shapeShorthands.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "tools/array.h"
|
||||
|
||||
using boost::optional;
|
||||
using boost::algorithm::clamp;
|
||||
using std::array;
|
||||
using std::pair;
|
||||
using std::map;
|
||||
using std::pair;
|
||||
using std::chrono::duration_cast;
|
||||
|
||||
constexpr size_t shapeValueCount = static_cast<size_t>(Shape::EndSentinel);
|
||||
|
||||
|
@ -32,17 +34,18 @@ Shape getClosestShape(Shape reference, ShapeSet shapes) {
|
|||
|
||||
// A matrix that for each shape contains all shapes in ascending order of effort required to
|
||||
// move to them
|
||||
constexpr static array<array<Shape, shapeValueCount>, shapeValueCount> effortMatrix = make_array(
|
||||
/* A */ make_array(A, X, G, B, 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),
|
||||
/* D */ make_array(D, C, H, B, G, A, X, E, F),
|
||||
/* E */ make_array(E, C, H, B, G, A, X, D, F),
|
||||
/* F */ make_array(F, B, G, A, X, C, H, E, D),
|
||||
/* G */ make_array(G, A, B, C, H, X, E, D, F),
|
||||
/* H */ make_array(H, C, B, G, D, A, X, E, F), // Like C
|
||||
/* X */ make_array(X, A, G, B, C, H, E, D, F) // Like A
|
||||
);
|
||||
constexpr static array<array<Shape, shapeValueCount>, shapeValueCount> effortMatrix =
|
||||
make_array(
|
||||
/* A */ make_array(A, X, G, B, 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),
|
||||
/* D */ make_array(D, C, H, B, G, A, X, E, F),
|
||||
/* E */ make_array(E, C, H, B, G, A, X, D, F),
|
||||
/* F */ make_array(F, B, G, A, X, C, H, E, D),
|
||||
/* G */ make_array(G, A, B, C, H, X, E, D, F),
|
||||
/* H */ make_array(H, C, B, G, D, A, X, E, F), // Like C
|
||||
/* X */ make_array(X, A, G, B, C, H, E, D, F) // Like A
|
||||
);
|
||||
|
||||
auto& closestShapes = effortMatrix.at(static_cast<size_t>(reference));
|
||||
for (Shape closestShape : closestShapes) {
|
||||
|
@ -58,32 +61,31 @@ optional<pair<Shape, TweenTiming>> getTween(Shape first, Shape second) {
|
|||
// Note that most of the following rules work in one direction only.
|
||||
// That's because in animation, the mouth should usually "pop" open without inbetweens,
|
||||
// then close slowly.
|
||||
static const map<pair<Shape, Shape>, pair<Shape, TweenTiming>> lookup {
|
||||
{ { D, A }, { C, TweenTiming::Early } },
|
||||
{ { D, B }, { C, TweenTiming::Centered } },
|
||||
{ { D, G }, { C, TweenTiming::Early } },
|
||||
{ { D, X }, { C, TweenTiming::Late } },
|
||||
{ { C, F }, { E, TweenTiming::Centered } }, { { F, C }, { E, TweenTiming::Centered } },
|
||||
{ { D, F }, { E, TweenTiming::Centered } },
|
||||
{ { H, F }, { E, TweenTiming::Late } }, { { F, H }, { E, TweenTiming::Early } }
|
||||
static const map<pair<Shape, Shape>, pair<Shape, TweenTiming>> lookup{
|
||||
{{D, A}, {C, TweenTiming::Early}},
|
||||
{{D, B}, {C, TweenTiming::Centered}},
|
||||
{{D, G}, {C, TweenTiming::Early}},
|
||||
{{D, X}, {C, TweenTiming::Late}},
|
||||
{{C, F}, {E, TweenTiming::Centered}},
|
||||
{{F, C}, {E, TweenTiming::Centered}},
|
||||
{{D, F}, {E, TweenTiming::Centered}},
|
||||
{{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>>();
|
||||
}
|
||||
|
||||
Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds previousDuration) {
|
||||
// Returns a timeline with a single shape set
|
||||
const auto single = [duration](ShapeSet value) {
|
||||
return Timeline<ShapeSet> { { 0_cs, duration, value } };
|
||||
return Timeline<ShapeSet>{{0_cs, duration, value}};
|
||||
};
|
||||
|
||||
// Returns a timeline with two shape sets, timed as a diphthong
|
||||
const auto diphthong = [duration](ShapeSet first, ShapeSet second) {
|
||||
const centiseconds firstDuration = duration_cast<centiseconds>(duration * 0.6);
|
||||
return Timeline<ShapeSet> {
|
||||
{ 0_cs, firstDuration, first },
|
||||
{ firstDuration, duration, second }
|
||||
};
|
||||
return Timeline<ShapeSet>{{0_cs, firstDuration, first}, {firstDuration, duration, second}};
|
||||
};
|
||||
|
||||
// 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 occlusionDuration =
|
||||
clamp(previousDuration / 2, minOcclusionDuration, maxOcclusionDuration);
|
||||
return Timeline<ShapeSet> {
|
||||
{ -occlusionDuration, 0_cs, first },
|
||||
{ 0_cs, duration, second }
|
||||
};
|
||||
return Timeline<ShapeSet>{{-occlusionDuration, 0_cs, first}, {0_cs, duration, second}};
|
||||
};
|
||||
|
||||
// Returns the result of `getShapeSets` when called with identical arguments
|
||||
|
@ -104,8 +103,8 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
|
|||
return getShapeSets(referencePhone, duration, previousDuration);
|
||||
};
|
||||
|
||||
static const ShapeSet any { A, B, C, D, E, F, G, H, X };
|
||||
static const ShapeSet anyOpen { B, C, D, E, F, G, H };
|
||||
static const ShapeSet any{A, B, C, D, E, F, G, H, X};
|
||||
static const ShapeSet anyOpen{B, C, D, E, F, G, H};
|
||||
|
||||
// Note:
|
||||
// The shapes {A, B, G, X} are very similar. You should avoid regular shape sets containing more
|
||||
|
@ -114,52 +113,52 @@ Timeline<ShapeSet> getShapeSets(Phone phone, centiseconds duration, centiseconds
|
|||
// As an exception, a very flexible rule may contain *all* these shapes.
|
||||
|
||||
switch (phone) {
|
||||
case Phone::AO: return single({ E });
|
||||
case Phone::AA: return single({ D });
|
||||
case Phone::IY: return single({ B });
|
||||
case Phone::UW: return single({ F });
|
||||
case Phone::EH: return single({ C });
|
||||
case Phone::IH: return single({ B });
|
||||
case Phone::UH: return single({ F });
|
||||
case Phone::AH: return duration < 20_cs ? single({ C }) : single({ D });
|
||||
case Phone::Schwa: return single({ B, C });
|
||||
case Phone::AE: return single({ C });
|
||||
case Phone::EY: return diphthong({ C }, { B });
|
||||
case Phone::AY: return duration < 20_cs ? diphthong({ C }, { B }) : diphthong({ D }, { B });
|
||||
case Phone::OW: return diphthong({ E }, { F });
|
||||
case Phone::AW: return duration < 30_cs ? diphthong({ C }, { E }) : diphthong({ D }, { E });
|
||||
case Phone::OY: return diphthong({ E }, { B });
|
||||
case Phone::ER: return duration < 7_cs ? like(Phone::Schwa) : single({ E });
|
||||
case Phone::AO: return single({E});
|
||||
case Phone::AA: return single({D});
|
||||
case Phone::IY: return single({B});
|
||||
case Phone::UW: return single({F});
|
||||
case Phone::EH: return single({C});
|
||||
case Phone::IH: return single({B});
|
||||
case Phone::UH: return single({F});
|
||||
case Phone::AH: return duration < 20_cs ? single({C}) : single({D});
|
||||
case Phone::Schwa: return single({B, C});
|
||||
case Phone::AE: return single({C});
|
||||
case Phone::EY: return diphthong({C}, {B});
|
||||
case Phone::AY: return duration < 20_cs ? diphthong({C}, {B}) : diphthong({D}, {B});
|
||||
case Phone::OW: return diphthong({E}, {F});
|
||||
case Phone::AW: return duration < 30_cs ? diphthong({C}, {E}) : diphthong({D}, {E});
|
||||
case Phone::OY: return diphthong({E}, {B});
|
||||
case Phone::ER: return duration < 7_cs ? like(Phone::Schwa) : single({E});
|
||||
|
||||
case Phone::P:
|
||||
case Phone::B: return plosive({ A }, any);
|
||||
case Phone::B: return plosive({A}, any);
|
||||
case Phone::T:
|
||||
case Phone::D: return plosive({ B, F }, anyOpen);
|
||||
case Phone::D: return plosive({B, F}, anyOpen);
|
||||
case Phone::K:
|
||||
case Phone::G: return plosive({ B, C, E, F, H }, anyOpen);
|
||||
case Phone::G: return plosive({B, C, E, F, H}, anyOpen);
|
||||
case Phone::CH:
|
||||
case Phone::JH: return single({ B, F });
|
||||
case Phone::JH: return single({B, F});
|
||||
case Phone::F:
|
||||
case Phone::V: return single({ G });
|
||||
case Phone::V: return single({G});
|
||||
case Phone::TH:
|
||||
case Phone::DH:
|
||||
case Phone::S:
|
||||
case Phone::Z:
|
||||
case Phone::SH:
|
||||
case Phone::ZH: return single({ B, F });
|
||||
case Phone::ZH: return single({B, F});
|
||||
case Phone::HH: return single(any); // think "m-hm"
|
||||
case Phone::M: return single({ A });
|
||||
case Phone::N: return single({ B, C, F, H });
|
||||
case Phone::NG: return single({ B, C, E, F });
|
||||
case Phone::L: return duration < 20_cs ? single({ B, E, F, H }) : single({ H });
|
||||
case Phone::R: return single({ B, E, F });
|
||||
case Phone::Y: return single({ B, C, F });
|
||||
case Phone::W: return single({ F });
|
||||
case Phone::M: return single({A});
|
||||
case Phone::N: return single({B, C, F, H});
|
||||
case Phone::NG: return single({B, C, E, F});
|
||||
case Phone::L: return duration < 20_cs ? single({B, E, F, H}) : single({H});
|
||||
case Phone::R: return single({B, E, F});
|
||||
case Phone::Y: return single({B, C, F});
|
||||
case Phone::W: return single({F});
|
||||
|
||||
case Phone::Breath:
|
||||
case Phone::Cough:
|
||||
case Phone::Smack: return single({ C });
|
||||
case Phone::Noise: return single({ B });
|
||||
case Phone::Smack: return single({C});
|
||||
case Phone::Noise: return single({B});
|
||||
|
||||
default: throw std::invalid_argument("Unexpected phone.");
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "core/Phone.h"
|
||||
#include "core/Shape.h"
|
||||
#include "time/Timeline.h"
|
||||
#include "core/Phone.h"
|
||||
|
||||
// Returns the basic shape (A-F) that most closely resembles the specified shape.
|
||||
Shape getBasicShape(Shape shape);
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
#include "mouthAnimation.h"
|
||||
#include "time/timedLogging.h"
|
||||
#include "ShapeRule.h"
|
||||
#include "roughAnimation.h"
|
||||
|
||||
#include "pauseAnimation.h"
|
||||
#include "tweening.h"
|
||||
#include "timingOptimization.h"
|
||||
#include "targetShapeSet.h"
|
||||
#include "roughAnimation.h"
|
||||
#include "ShapeRule.h"
|
||||
#include "staticSegments.h"
|
||||
#include "targetShapeSet.h"
|
||||
#include "time/timedLogging.h"
|
||||
#include "timingOptimization.h"
|
||||
#include "tweening.h"
|
||||
|
||||
JoiningContinuousTimeline<Shape> animate(
|
||||
const BoundedTimeline<Phone>& phones,
|
||||
const ShapeSet& targetShapeSet
|
||||
const BoundedTimeline<Phone>& phones, const ShapeSet& targetShapeSet
|
||||
) {
|
||||
// Create timeline of shape rules
|
||||
ContinuousTimeline<ShapeRule> shapeRules = getShapeRules(phones);
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
#include "core/Phone.h"
|
||||
#include "core/Shape.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "targetShapeSet.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
|
||||
JoiningContinuousTimeline<Shape> animate(
|
||||
const BoundedTimeline<Phone>& phones,
|
||||
const ShapeSet& targetShapeSet
|
||||
const BoundedTimeline<Phone>& phones, const ShapeSet& targetShapeSet
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "pauseAnimation.h"
|
||||
|
||||
#include "animationRules.h"
|
||||
|
||||
Shape getPauseShape(Shape previous, Shape next, centiseconds duration) {
|
||||
|
@ -30,7 +31,7 @@ Shape getPauseShape(Shape previous, Shape next, centiseconds duration) {
|
|||
|
||||
JoiningContinuousTimeline<Shape> animatePauses(const JoiningContinuousTimeline<Shape>& animation) {
|
||||
JoiningContinuousTimeline<Shape> result(animation);
|
||||
|
||||
|
||||
for_each_adjacent(
|
||||
animation.begin(),
|
||||
animation.end(),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "roughAnimation.h"
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
// Create timeline of shapes using a bidirectional algorithm.
|
||||
|
@ -22,9 +23,8 @@ JoiningContinuousTimeline<Shape> animateRough(const ContinuousTimeline<ShapeRule
|
|||
const ShapeRule shapeRule = it->getValue();
|
||||
const Shape shape = getClosestShape(referenceShape, shapeRule.shapeSet);
|
||||
animation.set(it->getTimeRange(), shape);
|
||||
const bool anticipateShape = shapeRule.phone
|
||||
&& isVowel(*shapeRule.phone)
|
||||
&& shapeRule.shapeSet.size() == 1;
|
||||
const bool anticipateShape =
|
||||
shapeRule.phone && isVowel(*shapeRule.phone) && shapeRule.shapeSet.size() == 1;
|
||||
if (anticipateShape) {
|
||||
// Animate backwards a little
|
||||
const Shape anticipatedShape = shape;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include "staticSegments.h"
|
||||
#include <vector>
|
||||
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/nextCombination.h"
|
||||
|
||||
using std::vector;
|
||||
|
@ -49,7 +51,7 @@ vector<TimeRange> getStaticSegments(
|
|||
result.push_back(timeRange);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -67,8 +69,8 @@ ShapeRule getChangedShapeRule(const ShapeRule& rule) {
|
|||
ShapeRule result(rule);
|
||||
// So far, I've only encountered B as a static shape.
|
||||
// If there is ever a problem with another static shape, this function can easily be extended.
|
||||
if (rule.shapeSet == ShapeSet { Shape::B }) {
|
||||
result.shapeSet = { Shape::C };
|
||||
if (rule.shapeSet == ShapeSet{Shape::B}) {
|
||||
result.shapeSet = {Shape::C};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -78,8 +80,7 @@ using RuleChanges = vector<centiseconds>;
|
|||
|
||||
// Replaces the indicated shape rules with slightly different ones, breaking up long static segments
|
||||
ContinuousTimeline<ShapeRule> applyChanges(
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||
const RuleChanges& changes
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules, const RuleChanges& changes
|
||||
) {
|
||||
ContinuousTimeline<ShapeRule> result(shapeRules);
|
||||
for (centiseconds changedRuleStart : changes) {
|
||||
|
@ -99,8 +100,7 @@ public:
|
|||
) :
|
||||
changedRules(applyChanges(originalRules, changes)),
|
||||
animation(animate(changedRules)),
|
||||
staticSegments(getStaticSegments(changedRules, animation))
|
||||
{}
|
||||
staticSegments(getStaticSegments(changedRules, animation)) {}
|
||||
|
||||
bool isBetterThan(const RuleChangeScenario& rhs) const {
|
||||
// We want zero static segments
|
||||
|
@ -132,8 +132,9 @@ private:
|
|||
0.0,
|
||||
[](const double sum, const Timed<Shape>& timedShape) {
|
||||
const double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||
timedShape.getDuration()
|
||||
).count();
|
||||
timedShape.getDuration()
|
||||
)
|
||||
.count();
|
||||
return sum + duration * duration;
|
||||
}
|
||||
);
|
||||
|
@ -152,8 +153,7 @@ RuleChanges getPossibleRuleChanges(const ContinuousTimeline<ShapeRule>& shapeRul
|
|||
}
|
||||
|
||||
ContinuousTimeline<ShapeRule> fixStaticSegmentRules(
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||
const AnimationFunction& animate
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules, const AnimationFunction& animate
|
||||
) {
|
||||
// The complexity of this function is exponential with the number of replacements.
|
||||
// So let's cap that value.
|
||||
|
@ -164,23 +164,26 @@ ContinuousTimeline<ShapeRule> fixStaticSegmentRules(
|
|||
|
||||
// Find best solution. Start with a single replacement, then increase as necessary.
|
||||
RuleChangeScenario bestScenario(shapeRules, {}, animate);
|
||||
for (
|
||||
int replacementCount = 1;
|
||||
bestScenario.getStaticSegmentCount() > 0 && replacementCount <= std::min(static_cast<int>(possibleRuleChanges.size()), maxReplacementCount);
|
||||
++replacementCount
|
||||
) {
|
||||
for (int replacementCount = 1; bestScenario.getStaticSegmentCount() > 0
|
||||
&& replacementCount
|
||||
<= std::min(static_cast<int>(possibleRuleChanges.size()), maxReplacementCount);
|
||||
++replacementCount) {
|
||||
// Only the first <replacementCount> elements of `currentRuleChanges` count
|
||||
auto currentRuleChanges(possibleRuleChanges);
|
||||
do {
|
||||
RuleChangeScenario currentScenario(
|
||||
shapeRules,
|
||||
{ currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount },
|
||||
{currentRuleChanges.begin(), currentRuleChanges.begin() + replacementCount},
|
||||
animate
|
||||
);
|
||||
if (currentScenario.isBetterThan(bestScenario)) {
|
||||
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();
|
||||
|
@ -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
|
||||
// possible
|
||||
TimeRange extendToFixedRules(
|
||||
const TimeRange& timeRange,
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules
|
||||
const TimeRange& timeRange, const ContinuousTimeline<ShapeRule>& shapeRules
|
||||
) {
|
||||
auto first = shapeRules.find(timeRange.getStart());
|
||||
while (first != shapeRules.begin() && isFlexible(first->getValue())) {
|
||||
|
@ -205,12 +207,11 @@ TimeRange extendToFixedRules(
|
|||
while (std::next(last) != shapeRules.end() && isFlexible(last->getValue())) {
|
||||
++last;
|
||||
}
|
||||
return { first->getStart(), last->getEnd() };
|
||||
return {first->getStart(), last->getEnd()};
|
||||
}
|
||||
|
||||
JoiningContinuousTimeline<Shape> avoidStaticSegments(
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||
const AnimationFunction& animate
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules, const AnimationFunction& animate
|
||||
) {
|
||||
const auto animation = animate(shapeRules);
|
||||
const vector<TimeRange> staticSegments = getStaticSegments(shapeRules, animation);
|
||||
|
@ -227,8 +228,7 @@ JoiningContinuousTimeline<Shape> avoidStaticSegments(
|
|||
|
||||
// Fix shape rules within the static segment
|
||||
const auto fixedSegmentShapeRules = fixStaticSegmentRules(
|
||||
{ extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules },
|
||||
animate
|
||||
{extendedStaticSegment, ShapeRule::getInvalid(), fixedShapeRules}, animate
|
||||
);
|
||||
for (const auto& timedShapeRule : fixedSegmentShapeRules) {
|
||||
fixedShapeRules.set(timedShapeRule);
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include "core/Shape.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "ShapeRule.h"
|
||||
#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.
|
||||
// If the resulting animation contains long static segments, the shape rules are tweaked and
|
||||
// animated again.
|
||||
// Static segments happen rather often.
|
||||
// See http://animateducated.blogspot.de/2016/10/lip-sync-animation-2.html?showComment=1478861729702#c2940729096183546458.
|
||||
// See
|
||||
// http://animateducated.blogspot.de/2016/10/lip-sync-animation-2.html?showComment=1478861729702#c2940729096183546458.
|
||||
JoiningContinuousTimeline<Shape> avoidStaticSegments(
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||
const AnimationFunction& animate
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules, const AnimationFunction& animate
|
||||
);
|
||||
|
|
|
@ -7,7 +7,8 @@ Shape convertToTargetShapeSet(Shape shape, const ShapeSet& targetShapeSet) {
|
|||
const Shape basicShape = getBasicShape(shape);
|
||||
if (targetShapeSet.find(basicShape) == targetShapeSet.end()) {
|
||||
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;
|
||||
}
|
||||
|
@ -21,8 +22,7 @@ ShapeSet convertToTargetShapeSet(const ShapeSet& shapes, const ShapeSet& targetS
|
|||
}
|
||||
|
||||
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||
const ShapeSet& targetShapeSet
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet
|
||||
) {
|
||||
ContinuousTimeline<ShapeRule> result(shapeRules);
|
||||
for (const auto& timedShapeRule : shapeRules) {
|
||||
|
@ -34,8 +34,7 @@ ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
|
|||
}
|
||||
|
||||
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(
|
||||
const JoiningContinuousTimeline<Shape>& animation,
|
||||
const ShapeSet& targetShapeSet
|
||||
const JoiningContinuousTimeline<Shape>& animation, const ShapeSet& targetShapeSet
|
||||
) {
|
||||
JoiningContinuousTimeline<Shape> result(animation);
|
||||
for (const auto& timedShape : animation) {
|
||||
|
|
|
@ -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.
|
||||
ContinuousTimeline<ShapeRule> convertToTargetShapeSet(
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules,
|
||||
const ShapeSet& targetShapeSet
|
||||
const ContinuousTimeline<ShapeRule>& shapeRules, const ShapeSet& targetShapeSet
|
||||
);
|
||||
|
||||
// Replaces each shape in the specified animation with the closest shape that occurs in the target
|
||||
// shape set.
|
||||
JoiningContinuousTimeline<Shape> convertToTargetShapeSet(
|
||||
const JoiningContinuousTimeline<Shape>& animation,
|
||||
const ShapeSet& targetShapeSet
|
||||
const JoiningContinuousTimeline<Shape>& animation, const ShapeSet& targetShapeSet
|
||||
);
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
#include "timingOptimization.h"
|
||||
#include "time/timedLogging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include "ShapeRule.h"
|
||||
|
||||
using std::string;
|
||||
#include "ShapeRule.h"
|
||||
#include "time/timedLogging.h"
|
||||
|
||||
using std::map;
|
||||
using std::string;
|
||||
|
||||
string getShapesString(const JoiningContinuousTimeline<Shape>& shapes) {
|
||||
string result;
|
||||
|
@ -32,8 +34,9 @@ Shape getRepresentativeShape(const JoiningTimeline<Shape>& timeline) {
|
|||
|
||||
// Select shape with highest total duration within the candidate range
|
||||
const Shape bestShape = std::max_element(
|
||||
candidateShapeWeights.begin(), candidateShapeWeights.end(),
|
||||
[](auto a, auto b) { return a.second < b.second; }
|
||||
candidateShapeWeights.begin(),
|
||||
candidateShapeWeights.end(),
|
||||
[](auto a, auto b) { return a.second < b.second; }
|
||||
)->first;
|
||||
|
||||
// Shapes C and D are similar, but D is more interesting.
|
||||
|
@ -55,8 +58,11 @@ struct ShapeReduction {
|
|||
|
||||
// Returns a time range of candidate shapes for the next shape to draw.
|
||||
// Guaranteed to be non-empty.
|
||||
TimeRange getNextMinimalCandidateRange(const JoiningContinuousTimeline<Shape>& sourceShapes,
|
||||
const TimeRange targetRange, const centiseconds writePosition) {
|
||||
TimeRange getNextMinimalCandidateRange(
|
||||
const JoiningContinuousTimeline<Shape>& sourceShapes,
|
||||
const TimeRange targetRange,
|
||||
const centiseconds writePosition
|
||||
) {
|
||||
if (sourceShapes.empty()) {
|
||||
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 bool canFitOneOrLess = remainingTargetDuration <= minShapeDuration;
|
||||
const bool canFitTwo = remainingTargetDuration >= 2 * minShapeDuration;
|
||||
const centiseconds duration = canFitOneOrLess || canFitTwo
|
||||
? minShapeDuration
|
||||
: remainingTargetDuration / 2;
|
||||
const centiseconds duration =
|
||||
canFitOneOrLess || canFitTwo ? minShapeDuration : remainingTargetDuration / 2;
|
||||
|
||||
TimeRange candidateRange(writePosition - duration, writePosition);
|
||||
if (writePosition == targetRange.getEnd()) {
|
||||
|
@ -102,22 +107,24 @@ ShapeReduction getNextShapeReduction(
|
|||
// Determine the next time range of candidate shapes. Consider two scenarios:
|
||||
|
||||
// ... the shortest-possible candidate range
|
||||
const ShapeReduction minReduction(sourceShapes,
|
||||
getNextMinimalCandidateRange(sourceShapes, targetRange, writePosition));
|
||||
const ShapeReduction minReduction(
|
||||
sourceShapes, getNextMinimalCandidateRange(sourceShapes, targetRange, writePosition)
|
||||
);
|
||||
|
||||
// ... a candidate range extended to the left to fully encompass its left-most shape
|
||||
const ShapeReduction extendedReduction(sourceShapes,
|
||||
{
|
||||
minReduction.sourceShapes.begin()->getStart(),
|
||||
minReduction.sourceShapes.getRange().getEnd()
|
||||
}
|
||||
const ShapeReduction extendedReduction(
|
||||
sourceShapes,
|
||||
{minReduction.sourceShapes.begin()->getStart(),
|
||||
minReduction.sourceShapes.getRange().getEnd()}
|
||||
);
|
||||
|
||||
// Determine the shape that might be picked *next* if we choose the shortest-possible candidate
|
||||
// range now
|
||||
const ShapeReduction nextReduction(
|
||||
sourceShapes,
|
||||
getNextMinimalCandidateRange(sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart())
|
||||
getNextMinimalCandidateRange(
|
||||
sourceShapes, targetRange, minReduction.sourceShapes.getRange().getStart()
|
||||
)
|
||||
);
|
||||
|
||||
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
|
||||
// jitter.
|
||||
JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>& sourceShapes,
|
||||
const TimeRange targetRange) {
|
||||
JoiningContinuousTimeline<Shape> retime(
|
||||
const JoiningContinuousTimeline<Shape>& sourceShapes, const TimeRange targetRange
|
||||
) {
|
||||
logTimedEvent("segment", targetRange, getShapesString(sourceShapes));
|
||||
|
||||
JoiningContinuousTimeline<Shape> result(targetRange, Shape::X);
|
||||
|
@ -139,7 +147,6 @@ JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>&
|
|||
// Animate backwards
|
||||
centiseconds writePosition = targetRange.getEnd();
|
||||
while (writePosition > targetRange.getStart()) {
|
||||
|
||||
// Decide which shape to show next, possibly discarding short shapes
|
||||
const ShapeReduction shapeReduction =
|
||||
getNextShapeReduction(sourceShapes, targetRange, writePosition);
|
||||
|
@ -162,31 +169,22 @@ JoiningContinuousTimeline<Shape> retime(const JoiningContinuousTimeline<Shape>&
|
|||
}
|
||||
|
||||
JoiningContinuousTimeline<Shape> retime(
|
||||
const JoiningContinuousTimeline<Shape>& animation,
|
||||
TimeRange sourceRange,
|
||||
TimeRange targetRange
|
||||
const JoiningContinuousTimeline<Shape>& animation, TimeRange sourceRange, TimeRange targetRange
|
||||
) {
|
||||
const auto sourceShapes = JoiningContinuousTimeline<Shape>(sourceRange, Shape::X, animation);
|
||||
return retime(sourceShapes, targetRange);
|
||||
}
|
||||
|
||||
enum class MouthState {
|
||||
Idle,
|
||||
Closed,
|
||||
Open
|
||||
};
|
||||
enum class MouthState { Idle, Closed, Open };
|
||||
|
||||
JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<Shape>& animation) {
|
||||
// Identify segments with idle, closed, and open mouth shapes
|
||||
JoiningContinuousTimeline<MouthState> segments(animation.getRange(), MouthState::Idle);
|
||||
for (const auto& timedShape : animation) {
|
||||
const Shape shape = timedShape.getValue();
|
||||
const MouthState mouthState =
|
||||
shape == Shape::X
|
||||
? MouthState::Idle
|
||||
: shape == Shape::A
|
||||
? MouthState::Closed
|
||||
: MouthState::Open;
|
||||
const MouthState mouthState = shape == Shape::X ? MouthState::Idle
|
||||
: shape == Shape::A ? MouthState::Closed
|
||||
: MouthState::Open;
|
||||
segments.set(timedShape.getTimeRange(), mouthState);
|
||||
}
|
||||
|
||||
|
@ -219,11 +217,8 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
|
|||
// evenly.
|
||||
const auto begin = segmentIt;
|
||||
auto end = std::next(begin);
|
||||
while (
|
||||
end != segments.rend()
|
||||
&& end->getValue() != MouthState::Idle
|
||||
&& end->getDuration() < minSegmentDuration
|
||||
) {
|
||||
while (end != segments.rend() && end->getValue() != MouthState::Idle
|
||||
&& end->getDuration() < minSegmentDuration) {
|
||||
++end;
|
||||
}
|
||||
|
||||
|
@ -232,20 +227,19 @@ JoiningContinuousTimeline<Shape> optimizeTiming(const JoiningContinuousTimeline<
|
|||
const centiseconds desiredDuration = minSegmentDuration * shortSegmentCount;
|
||||
const centiseconds currentDuration = begin->getEnd() - std::prev(end)->getStart();
|
||||
const centiseconds desiredExtensionDuration = desiredDuration - currentDuration;
|
||||
const centiseconds availableExtensionDuration = end != segments.rend()
|
||||
? end->getDuration() - 1_cs
|
||||
: 0_cs;
|
||||
const centiseconds extensionDuration = std::min({
|
||||
desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration
|
||||
});
|
||||
const centiseconds availableExtensionDuration =
|
||||
end != segments.rend() ? end->getDuration() - 1_cs : 0_cs;
|
||||
const centiseconds extensionDuration = std::min(
|
||||
{desiredExtensionDuration, availableExtensionDuration, maxExtensionDuration}
|
||||
);
|
||||
|
||||
// Distribute available time range evenly among all short segments
|
||||
const centiseconds shortSegmentsTargetStart =
|
||||
std::prev(end)->getStart() - extensionDuration;
|
||||
for (auto shortSegmentIt = begin; shortSegmentIt != end; ++shortSegmentIt) {
|
||||
size_t remainingShortSegmentCount = std::distance(shortSegmentIt, end);
|
||||
const centiseconds segmentDuration = (resultStart - shortSegmentsTargetStart) /
|
||||
remainingShortSegmentCount;
|
||||
const centiseconds segmentDuration =
|
||||
(resultStart - shortSegmentsTargetStart) / remainingShortSegmentCount;
|
||||
const TimeRange segmentTargetRange(resultStart - segmentDuration, resultStart);
|
||||
const auto retimedSegment =
|
||||
retime(animation, shortSegmentIt->getTimeRange(), segmentTargetRange);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "tweening.h"
|
||||
|
||||
#include "animationRules.h"
|
||||
|
||||
JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Shape>& animation) {
|
||||
|
@ -7,48 +8,50 @@ JoiningContinuousTimeline<Shape> insertTweens(const JoiningContinuousTimeline<Sh
|
|||
|
||||
JoiningContinuousTimeline<Shape> result(animation);
|
||||
|
||||
for_each_adjacent(animation.begin(), animation.end(), [&](const auto& first, const auto& second) {
|
||||
auto pair = getTween(first.getValue(), second.getValue());
|
||||
if (!pair) return;
|
||||
for_each_adjacent(
|
||||
animation.begin(),
|
||||
animation.end(),
|
||||
[&](const auto& first, const auto& second) {
|
||||
auto pair = getTween(first.getValue(), second.getValue());
|
||||
if (!pair) return;
|
||||
|
||||
Shape tweenShape;
|
||||
TweenTiming tweenTiming;
|
||||
std::tie(tweenShape, tweenTiming) = *pair;
|
||||
TimeRange firstTimeRange = first.getTimeRange();
|
||||
TimeRange secondTimeRange = second.getTimeRange();
|
||||
Shape tweenShape;
|
||||
TweenTiming tweenTiming;
|
||||
std::tie(tweenShape, tweenTiming) = *pair;
|
||||
TimeRange firstTimeRange = first.getTimeRange();
|
||||
TimeRange secondTimeRange = second.getTimeRange();
|
||||
|
||||
centiseconds tweenStart, tweenDuration;
|
||||
switch (tweenTiming) {
|
||||
case TweenTiming::Early:
|
||||
{
|
||||
tweenDuration = std::min(firstTimeRange.getDuration() / 3, maxTweenDuration);
|
||||
tweenStart = firstTimeRange.getEnd() - tweenDuration;
|
||||
break;
|
||||
}
|
||||
case TweenTiming::Centered:
|
||||
{
|
||||
tweenDuration = std::min({
|
||||
firstTimeRange.getDuration() / 4, secondTimeRange.getDuration() / 4, maxTweenDuration
|
||||
});
|
||||
tweenStart = firstTimeRange.getEnd() - tweenDuration / 2;
|
||||
break;
|
||||
}
|
||||
case TweenTiming::Late:
|
||||
{
|
||||
tweenDuration = std::min(secondTimeRange.getDuration() / 3, maxTweenDuration);
|
||||
tweenStart = secondTimeRange.getStart();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw std::runtime_error("Unexpected tween timing.");
|
||||
centiseconds tweenStart, tweenDuration;
|
||||
switch (tweenTiming) {
|
||||
case TweenTiming::Early: {
|
||||
tweenDuration = std::min(firstTimeRange.getDuration() / 3, maxTweenDuration);
|
||||
tweenStart = firstTimeRange.getEnd() - tweenDuration;
|
||||
break;
|
||||
}
|
||||
case TweenTiming::Centered: {
|
||||
tweenDuration = std::min(
|
||||
{firstTimeRange.getDuration() / 4,
|
||||
secondTimeRange.getDuration() / 4,
|
||||
maxTweenDuration}
|
||||
);
|
||||
tweenStart = firstTimeRange.getEnd() - tweenDuration / 2;
|
||||
break;
|
||||
}
|
||||
case TweenTiming::Late: {
|
||||
tweenDuration = std::min(secondTimeRange.getDuration() / 3, maxTweenDuration);
|
||||
tweenStart = secondTimeRange.getStart();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw std::runtime_error("Unexpected tween timing.");
|
||||
}
|
||||
}
|
||||
|
||||
if (tweenDuration < minTweenDuration) return;
|
||||
|
||||
result.set(tweenStart, tweenStart + tweenDuration, tweenShape);
|
||||
}
|
||||
|
||||
if (tweenDuration < minTweenDuration) return;
|
||||
|
||||
result.set(tweenStart, tweenStart + tweenDuration, tweenShape);
|
||||
});
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "AudioClip.h"
|
||||
|
||||
#include <format.h>
|
||||
|
||||
using std::invalid_argument;
|
||||
|
@ -11,6 +12,7 @@ class SafeSampleReader {
|
|||
public:
|
||||
SafeSampleReader(SampleReader unsafeRead, AudioClip::size_type size);
|
||||
AudioClip::value_type operator()(AudioClip::size_type index);
|
||||
|
||||
private:
|
||||
SampleReader unsafeRead;
|
||||
AudioClip::size_type size;
|
||||
|
@ -20,19 +22,16 @@ private:
|
|||
|
||||
SafeSampleReader::SafeSampleReader(SampleReader unsafeRead, AudioClip::size_type size) :
|
||||
unsafeRead(unsafeRead),
|
||||
size(size)
|
||||
{}
|
||||
size(size) {}
|
||||
|
||||
inline AudioClip::value_type SafeSampleReader::operator()(AudioClip::size_type index) {
|
||||
if (index < 0) {
|
||||
throw invalid_argument(fmt::format("Cannot read from sample index {}. Index < 0.", index));
|
||||
}
|
||||
if (index >= size) {
|
||||
throw invalid_argument(fmt::format(
|
||||
"Cannot read from sample index {}. Clip size is {}.",
|
||||
index,
|
||||
size
|
||||
));
|
||||
throw invalid_argument(
|
||||
fmt::format("Cannot read from sample index {}. Clip size is {}.", index, size)
|
||||
);
|
||||
}
|
||||
if (index == lastIndex) {
|
||||
return lastSample;
|
||||
|
@ -60,10 +59,8 @@ std::unique_ptr<AudioClip> operator|(std::unique_ptr<AudioClip> clip, const Audi
|
|||
}
|
||||
|
||||
SampleIterator::SampleIterator() :
|
||||
sampleIndex(0)
|
||||
{}
|
||||
sampleIndex(0) {}
|
||||
|
||||
SampleIterator::SampleIterator(const AudioClip& audioClip, size_type sampleIndex) :
|
||||
sampleReader([&audioClip] { return audioClip.createSampleReader(); }),
|
||||
sampleIndex(sampleIndex)
|
||||
{}
|
||||
sampleIndex(sampleIndex) {}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "time/TimeRange.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "time/TimeRange.h"
|
||||
#include "tools/Lazy.h"
|
||||
|
||||
class AudioClip;
|
||||
|
@ -17,6 +18,7 @@ public:
|
|||
using SampleReader = std::function<value_type(size_type)>;
|
||||
|
||||
virtual ~AudioClip() {}
|
||||
|
||||
virtual std::unique_ptr<AudioClip> clone() const = 0;
|
||||
virtual int getSampleRate() const = 0;
|
||||
virtual size_type size() const = 0;
|
||||
|
@ -24,6 +26,7 @@ public:
|
|||
SampleReader createSampleReader() const;
|
||||
iterator begin() const;
|
||||
iterator end() const;
|
||||
|
||||
private:
|
||||
virtual SampleReader createUnsafeSampleReader() const = 0;
|
||||
};
|
||||
|
@ -137,6 +140,8 @@ inline SampleIterator operator-(const SampleIterator& it, SampleIterator::differ
|
|||
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();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
#include "AudioSegment.h"
|
||||
|
||||
using std::unique_ptr;
|
||||
using std::make_unique;
|
||||
using std::unique_ptr;
|
||||
|
||||
AudioSegment::AudioSegment(std::unique_ptr<AudioClip> inputClip, const TimeRange& range) :
|
||||
inputClip(std::move(inputClip)),
|
||||
sampleOffset(static_cast<int64_t>(range.getStart().count()) * this->inputClip->getSampleRate() / 100),
|
||||
sampleCount(static_cast<int64_t>(range.getDuration().count()) * this->inputClip->getSampleRate() / 100)
|
||||
{
|
||||
sampleOffset(
|
||||
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()) {
|
||||
throw std::invalid_argument("Segment extends beyond input clip.");
|
||||
}
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
#include "DcOffset.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using std::unique_ptr;
|
||||
using std::make_unique;
|
||||
using std::unique_ptr;
|
||||
|
||||
DcOffset::DcOffset(unique_ptr<AudioClip> inputClip, float offset) :
|
||||
inputClip(std::move(inputClip)),
|
||||
offset(offset),
|
||||
factor(1 / (1 + std::abs(offset)))
|
||||
{}
|
||||
factor(1 / (1 + std::abs(offset))) {}
|
||||
|
||||
unique_ptr<AudioClip> DcOffset::clone() const {
|
||||
return make_unique<DcOffset>(*this);
|
||||
}
|
||||
|
||||
SampleReader DcOffset::createUnsafeSampleReader() const {
|
||||
return [
|
||||
read = inputClip->createSampleReader(),
|
||||
factor = factor,
|
||||
offset = offset
|
||||
](size_type index) {
|
||||
const float sample = read(index);
|
||||
return sample * factor + offset;
|
||||
};
|
||||
return
|
||||
[read = inputClip->createSampleReader(), factor = factor, offset = offset](size_type index
|
||||
) {
|
||||
const float sample = read(index);
|
||||
return sample * factor + offset;
|
||||
};
|
||||
}
|
||||
|
||||
float getDcOffset(const AudioClip& audioClip) {
|
||||
|
|
|
@ -10,6 +10,7 @@ public:
|
|||
std::unique_ptr<AudioClip> clone() const override;
|
||||
int getSampleRate() const override;
|
||||
size_type size() const override;
|
||||
|
||||
private:
|
||||
SampleReader createUnsafeSampleReader() const override;
|
||||
|
||||
|
|
|
@ -1,47 +1,40 @@
|
|||
#include "OggVorbisFileReader.h"
|
||||
|
||||
#include <format.h>
|
||||
|
||||
#include "tools/fileTools.h"
|
||||
#include "tools/tools.h"
|
||||
#include "vorbis/codec.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::ios_base;
|
||||
using std::make_shared;
|
||||
using std::vector;
|
||||
using std::filesystem::path;
|
||||
|
||||
std::string vorbisErrorToString(int64_t errorCode) {
|
||||
switch (errorCode) {
|
||||
case OV_EREAD:
|
||||
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_EIMPL:
|
||||
return "Feature not implemented";
|
||||
case OV_EREAD: 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_EIMPL: return "Feature not implemented";
|
||||
case OV_EINVAL:
|
||||
return "Either an invalid argument, or incompletely initialized argument passed to a call.";
|
||||
case OV_ENOTVORBIS:
|
||||
return "The given file/data was not recognized as Ogg Vorbis data.";
|
||||
case OV_ENOTVORBIS: return "The given file/data was not recognized as Ogg Vorbis data.";
|
||||
case OV_EBADHEADER:
|
||||
return "The file/data is apparently an Ogg Vorbis stream, but contains a corrupted or undecipherable header.";
|
||||
case OV_EVERSION:
|
||||
return "The bitstream format revision of the given Vorbis stream is not supported.";
|
||||
case OV_ENOTAUDIO:
|
||||
return "Packet is not an audio packet.";
|
||||
case OV_EBADPACKET:
|
||||
return "Error in packet.";
|
||||
case OV_ENOTAUDIO: return "Packet is not an audio packet.";
|
||||
case OV_EBADPACKET: return "Error in packet.";
|
||||
case OV_EBADLINK:
|
||||
return "The given link exists in the Vorbis data stream, but is not decipherable due to garbage or corruption.";
|
||||
case OV_ENOSEEK:
|
||||
return "The given stream is not seekable.";
|
||||
default:
|
||||
return "An unexpected Vorbis error occurred.";
|
||||
case OV_ENOSEEK: return "The given stream is not seekable.";
|
||||
default: return "An unexpected Vorbis error occurred.";
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
template <typename T>
|
||||
T throwOnError(T code) {
|
||||
// OV_HOLE, though technically an error code, is only informational
|
||||
const bool error = code < 0 && code != OV_HOLE;
|
||||
|
@ -64,7 +57,7 @@ size_t readCallback(void* buffer, size_t elementSize, size_t elementCount, void*
|
|||
}
|
||||
|
||||
int seekCallback(void* dataSource, ogg_int64_t offset, int origin) {
|
||||
static const vector<ios_base::seekdir> seekDirections {
|
||||
static const vector<ios_base::seekdir> seekDirections{
|
||||
ios_base::beg, ios_base::cur, ios_base::end
|
||||
};
|
||||
|
||||
|
@ -104,8 +97,7 @@ private:
|
|||
|
||||
OggVorbisFile::OggVorbisFile(const path& filePath) :
|
||||
oggVorbisHandle(),
|
||||
stream(openFile(filePath))
|
||||
{
|
||||
stream(openFile(filePath)) {
|
||||
// Throw only on badbit, not on failbit.
|
||||
// Ogg Vorbis expects read operations past the end of the file to
|
||||
// succeed, not to throw.
|
||||
|
@ -114,19 +106,18 @@ OggVorbisFile::OggVorbisFile(const path& filePath) :
|
|||
// Ogg Vorbis normally uses the `FILE` API from the C standard library.
|
||||
// This doesn't handle Unicode paths on Windows.
|
||||
// Use wrapper functions around `ifstream` instead.
|
||||
const ov_callbacks callbacks { readCallback, seekCallback, nullptr, tellCallback };
|
||||
const ov_callbacks callbacks{readCallback, seekCallback, nullptr, tellCallback};
|
||||
throwOnError(ov_open_callbacks(&stream, &oggVorbisHandle, nullptr, 0, callbacks));
|
||||
}
|
||||
|
||||
OggVorbisFileReader::OggVorbisFileReader(const path& filePath) :
|
||||
filePath(filePath)
|
||||
{
|
||||
filePath(filePath) {
|
||||
OggVorbisFile file(filePath);
|
||||
|
||||
|
||||
vorbis_info* vorbisInfo = ov_info(file.get(), -1);
|
||||
sampleRate = vorbisInfo->rate;
|
||||
channelCount = vorbisInfo->channels;
|
||||
|
||||
|
||||
sampleCount = throwOnError(ov_pcm_total(file.get(), -1));
|
||||
}
|
||||
|
||||
|
@ -135,13 +126,11 @@ std::unique_ptr<AudioClip> OggVorbisFileReader::clone() const {
|
|||
}
|
||||
|
||||
SampleReader OggVorbisFileReader::createUnsafeSampleReader() const {
|
||||
return [
|
||||
channelCount = channelCount,
|
||||
file = make_shared<OggVorbisFile>(filePath),
|
||||
buffer = static_cast<value_type**>(nullptr),
|
||||
bufferStart = size_type(0),
|
||||
bufferSize = size_type(0)
|
||||
](size_type index) mutable {
|
||||
return [channelCount = channelCount,
|
||||
file = make_shared<OggVorbisFile>(filePath),
|
||||
buffer = static_cast<value_type**>(nullptr),
|
||||
bufferStart = size_type(0),
|
||||
bufferSize = size_type(0)](size_type index) mutable {
|
||||
if (index < bufferStart || index >= bufferStart + bufferSize) {
|
||||
// Seek
|
||||
throwOnError(ov_pcm_seek(file->get(), index));
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include "AudioClip.h"
|
||||
#include <filesystem>
|
||||
|
||||
#include "AudioClip.h"
|
||||
|
||||
class OggVorbisFileReader : public AudioClip {
|
||||
public:
|
||||
OggVorbisFileReader(const std::filesystem::path& filePath);
|
||||
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:
|
||||
SampleReader createUnsafeSampleReader() const override;
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
#include <cmath>
|
||||
#include "SampleRateConverter.h"
|
||||
#include <stdexcept>
|
||||
|
||||
#include <format.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
using std::invalid_argument;
|
||||
using std::unique_ptr;
|
||||
using std::make_unique;
|
||||
using std::unique_ptr;
|
||||
|
||||
SampleRateConverter::SampleRateConverter(unique_ptr<AudioClip> inputClip, int outputSampleRate) :
|
||||
inputClip(std::move(inputClip)),
|
||||
downscalingFactor(static_cast<double>(this->inputClip->getSampleRate()) / outputSampleRate),
|
||||
outputSampleRate(outputSampleRate),
|
||||
outputSampleCount(std::lround(this->inputClip->size() / downscalingFactor))
|
||||
{
|
||||
outputSampleCount(std::lround(this->inputClip->size() / downscalingFactor)) {
|
||||
if (outputSampleRate <= 0) {
|
||||
throw invalid_argument("Sample rate must be positive.");
|
||||
}
|
||||
if (this->inputClip->getSampleRate() < outputSampleRate) {
|
||||
throw invalid_argument(fmt::format(
|
||||
"Upsampling not supported. Input sample rate must not be below {}Hz.",
|
||||
outputSampleRate
|
||||
"Upsampling not supported. Input sample rate must not be below {}Hz.", outputSampleRate
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -51,11 +51,9 @@ float mean(double inputStart, double inputEnd, const SampleReader& read) {
|
|||
}
|
||||
|
||||
SampleReader SampleRateConverter::createUnsafeSampleReader() const {
|
||||
return [
|
||||
read = inputClip->createSampleReader(),
|
||||
downscalingFactor = downscalingFactor,
|
||||
size = inputClip->size()
|
||||
](size_type index) {
|
||||
return [read = inputClip->createSampleReader(),
|
||||
downscalingFactor = downscalingFactor,
|
||||
size = inputClip->size()](size_type index) {
|
||||
const double inputStart = index * downscalingFactor;
|
||||
const double inputEnd =
|
||||
std::min((index + 1) * downscalingFactor, static_cast<double>(size));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "AudioClip.h"
|
||||
|
||||
class SampleRateConverter : public AudioClip {
|
||||
|
@ -9,6 +10,7 @@ public:
|
|||
std::unique_ptr<AudioClip> clone() const override;
|
||||
int getSampleRate() const override;
|
||||
size_type size() const override;
|
||||
|
||||
private:
|
||||
SampleReader createUnsafeSampleReader() const override;
|
||||
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
#include <format.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 std::runtime_error;
|
||||
using std::string;
|
||||
using namespace little_endian;
|
||||
using std::unique_ptr;
|
||||
using std::make_unique;
|
||||
using std::make_shared;
|
||||
using std::filesystem::path;
|
||||
using std::make_unique;
|
||||
using std::streamoff;
|
||||
using std::unique_ptr;
|
||||
using std::filesystem::path;
|
||||
|
||||
#define INT24_MIN (-8388608)
|
||||
#define INT24_MAX 8388607
|
||||
|
@ -31,15 +34,15 @@ streamoff roundUpToEven(streamoff i) {
|
|||
}
|
||||
|
||||
namespace Codec {
|
||||
constexpr int Pcm = 0x01;
|
||||
constexpr int Float = 0x03;
|
||||
constexpr int Extensible = 0xFFFE;
|
||||
};
|
||||
constexpr int Pcm = 0x01;
|
||||
constexpr int Float = 0x03;
|
||||
constexpr int Extensible = 0xFFFE;
|
||||
}; // namespace Codec
|
||||
|
||||
string codecToString(int codec);
|
||||
|
||||
WaveFormatInfo getWaveFormatInfo(const path& filePath) {
|
||||
WaveFormatInfo formatInfo {};
|
||||
WaveFormatInfo formatInfo{};
|
||||
|
||||
auto file = openFile(filePath);
|
||||
|
||||
|
@ -74,8 +77,7 @@ WaveFormatInfo getWaveFormatInfo(const path& filePath) {
|
|||
const streamoff chunkSize = read<int32_t>(file);
|
||||
const streamoff chunkEnd = roundUpToEven(file.tellg() + chunkSize);
|
||||
switch (chunkId) {
|
||||
case fourcc('f', 'm', 't', ' '):
|
||||
{
|
||||
case fourcc('f', 'm', 't', ' '): {
|
||||
// Read relevant data
|
||||
uint16_t codec = read<uint16_t>(file);
|
||||
formatInfo.channelCount = read<uint16_t>(file);
|
||||
|
@ -118,7 +120,8 @@ WaveFormatInfo getWaveFormatInfo(const path& filePath) {
|
|||
bytesPerSample = 4;
|
||||
} else {
|
||||
throw runtime_error(
|
||||
format("Unsupported sample format: {}-bit PCM.", bitsPerSample));
|
||||
format("Unsupported sample format: {}-bit PCM.", bitsPerSample)
|
||||
);
|
||||
}
|
||||
if (bytesPerSample != bytesPerFrame / formatInfo.channelCount) {
|
||||
throw runtime_error("Unsupported sample organization.");
|
||||
|
@ -132,30 +135,30 @@ WaveFormatInfo getWaveFormatInfo(const path& filePath) {
|
|||
formatInfo.sampleFormat = SampleFormat::Float64;
|
||||
bytesPerSample = 8;
|
||||
} else {
|
||||
throw runtime_error(
|
||||
format("Unsupported sample format: {}-bit IEEE Float.", bitsPerSample)
|
||||
);
|
||||
throw runtime_error(format(
|
||||
"Unsupported sample format: {}-bit IEEE Float.", bitsPerSample
|
||||
));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw runtime_error(format(
|
||||
"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;
|
||||
processedFormatChunk = true;
|
||||
break;
|
||||
}
|
||||
case fourcc('d', 'a', 't', 'a'):
|
||||
{
|
||||
case fourcc('d', 'a', 't', 'a'): {
|
||||
formatInfo.dataOffset = file.tellg();
|
||||
formatInfo.frameCount = chunkSize / formatInfo.bytesPerFrame;
|
||||
processedDataChunk = true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
default: {
|
||||
// Ignore unknown chunk
|
||||
break;
|
||||
}
|
||||
|
@ -180,45 +183,37 @@ unique_ptr<AudioClip> WaveFileReader::clone() const {
|
|||
}
|
||||
|
||||
inline AudioClip::value_type readSample(
|
||||
std::ifstream& file,
|
||||
SampleFormat sampleFormat,
|
||||
int channelCount
|
||||
std::ifstream& file, SampleFormat sampleFormat, int channelCount
|
||||
) {
|
||||
float sum = 0;
|
||||
for (int channelIndex = 0; channelIndex < channelCount; channelIndex++) {
|
||||
switch (sampleFormat) {
|
||||
case SampleFormat::UInt8:
|
||||
{
|
||||
case SampleFormat::UInt8: {
|
||||
const uint8_t raw = read<uint8_t>(file);
|
||||
sum += toNormalizedFloat(raw, 0, UINT8_MAX);
|
||||
break;
|
||||
}
|
||||
case SampleFormat::Int16:
|
||||
{
|
||||
case SampleFormat::Int16: {
|
||||
const int16_t raw = read<int16_t>(file);
|
||||
sum += toNormalizedFloat(raw, INT16_MIN, INT16_MAX);
|
||||
break;
|
||||
}
|
||||
case SampleFormat::Int24:
|
||||
{
|
||||
case SampleFormat::Int24: {
|
||||
int raw = read<int, 24>(file);
|
||||
if (raw & 0x800000) raw |= 0xFF000000; // Fix two's complement
|
||||
sum += toNormalizedFloat(raw, INT24_MIN, INT24_MAX);
|
||||
break;
|
||||
}
|
||||
case SampleFormat::Int32:
|
||||
{
|
||||
case SampleFormat::Int32: {
|
||||
const int32_t raw = read<int32_t>(file);
|
||||
sum += toNormalizedFloat(raw, INT32_MIN, INT32_MAX);
|
||||
break;
|
||||
}
|
||||
case SampleFormat::Float32:
|
||||
{
|
||||
case SampleFormat::Float32: {
|
||||
sum += read<float>(file);
|
||||
break;
|
||||
}
|
||||
case SampleFormat::Float64:
|
||||
{
|
||||
case SampleFormat::Float64: {
|
||||
sum += static_cast<float>(read<double>(file));
|
||||
break;
|
||||
}
|
||||
|
@ -229,14 +224,11 @@ inline AudioClip::value_type readSample(
|
|||
}
|
||||
|
||||
SampleReader WaveFileReader::createUnsafeSampleReader() const {
|
||||
return
|
||||
[
|
||||
formatInfo = formatInfo,
|
||||
return [formatInfo = formatInfo,
|
||||
file = std::make_shared<std::ifstream>(openFile(filePath)),
|
||||
filePos = std::streampos(0)
|
||||
](size_type index) mutable {
|
||||
const std::streampos newFilePos = formatInfo.dataOffset
|
||||
+ static_cast<streamoff>(index * formatInfo.bytesPerFrame);
|
||||
filePos = std::streampos(0)](size_type index) mutable {
|
||||
const std::streampos newFilePos =
|
||||
formatInfo.dataOffset + static_cast<streamoff>(index * formatInfo.bytesPerFrame);
|
||||
if (newFilePos != filePos) {
|
||||
file->seekg(newFilePos);
|
||||
}
|
||||
|
@ -491,7 +483,6 @@ string codecToString(int codec) {
|
|||
case 0xf1ac: return "Free Lossless Audio Codec FLAC";
|
||||
case 0xfffe: return "Extensible";
|
||||
case 0xffff: return "Development";
|
||||
default:
|
||||
return format("{0:#x}", codec);
|
||||
default: return format("{0:#x}", codec);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "AudioClip.h"
|
||||
|
||||
enum class SampleFormat {
|
||||
UInt8,
|
||||
Int16,
|
||||
Int24,
|
||||
Int32,
|
||||
Float32,
|
||||
Float64
|
||||
};
|
||||
enum class SampleFormat { UInt8, Int16, Int24, Int32, Float32, Float64 };
|
||||
|
||||
struct WaveFormatInfo {
|
||||
int bytesPerFrame;
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
#include "audioFileReading.h"
|
||||
#include <format.h>
|
||||
#include "WaveFileReader.h"
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include "OggVorbisFileReader.h"
|
||||
|
||||
using std::filesystem::path;
|
||||
using std::string;
|
||||
using std::runtime_error;
|
||||
#include <format.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include "OggVorbisFileReader.h"
|
||||
#include "WaveFileReader.h"
|
||||
|
||||
using fmt::format;
|
||||
using std::runtime_error;
|
||||
using std::string;
|
||||
using std::filesystem::path;
|
||||
|
||||
std::unique_ptr<AudioClip> createAudioFileClip(path filePath) {
|
||||
try {
|
||||
const string extension =
|
||||
boost::algorithm::to_lower_copy(filePath.extension().u8string());
|
||||
const string extension = boost::algorithm::to_lower_copy(filePath.extension().u8string());
|
||||
if (extension == ".wav") {
|
||||
return std::make_unique<WaveFileReader>(filePath);
|
||||
}
|
||||
|
@ -24,6 +26,8 @@ std::unique_ptr<AudioClip> createAudioFileClip(path filePath) {
|
|||
extension
|
||||
));
|
||||
} 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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include "AudioClip.h"
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "AudioClip.h"
|
||||
|
||||
std::unique_ptr<AudioClip> createAudioFileClip(std::filesystem::path filePath);
|
||||
|
|
|
@ -4,43 +4,38 @@
|
|||
|
||||
namespace little_endian {
|
||||
|
||||
template<typename Type, int bitsToRead = 8 * sizeof(Type)>
|
||||
Type read(std::istream& stream) {
|
||||
static_assert(bitsToRead % 8 == 0, "Cannot read fractional bytes.");
|
||||
static_assert(bitsToRead <= sizeof(Type) * 8, "Bits to read exceed target type size.");
|
||||
template <typename Type, int bitsToRead = 8 * sizeof(Type)>
|
||||
Type read(std::istream& stream) {
|
||||
static_assert(bitsToRead % 8 == 0, "Cannot read fractional bytes.");
|
||||
static_assert(bitsToRead <= sizeof(Type) * 8, "Bits to read exceed target type size.");
|
||||
|
||||
Type result = 0;
|
||||
char* p = reinterpret_cast<char*>(&result);
|
||||
const int bytesToRead = bitsToRead / 8;
|
||||
for (int byteIndex = 0; byteIndex < bytesToRead; byteIndex++) {
|
||||
*(p + byteIndex) = static_cast<char>(stream.get());
|
||||
}
|
||||
return result;
|
||||
Type result = 0;
|
||||
char* p = reinterpret_cast<char*>(&result);
|
||||
const int bytesToRead = bitsToRead / 8;
|
||||
for (int byteIndex = 0; byteIndex < bytesToRead; byteIndex++) {
|
||||
*(p + byteIndex) = static_cast<char>(stream.get());
|
||||
}
|
||||
|
||||
template<typename Type, int bitsToWrite = 8 * sizeof(Type)>
|
||||
void write(Type value, std::ostream& stream) {
|
||||
static_assert(bitsToWrite % 8 == 0, "Cannot write fractional bytes.");
|
||||
static_assert(bitsToWrite <= sizeof(Type) * 8, "Bits to write exceed target type size.");
|
||||
|
||||
char* p = reinterpret_cast<char*>(&value);
|
||||
const int bytesToWrite = bitsToWrite / 8;
|
||||
for (int byteIndex = 0; byteIndex < bytesToWrite; byteIndex++) {
|
||||
stream.put(*(p + byteIndex));
|
||||
}
|
||||
}
|
||||
|
||||
constexpr uint32_t fourcc(
|
||||
unsigned char c0,
|
||||
unsigned char c1,
|
||||
unsigned char c2,
|
||||
unsigned char c3
|
||||
) {
|
||||
return c0 | (c1 << 8) | (c2 << 16) | (c3 << 24);
|
||||
}
|
||||
|
||||
inline std::string fourccToString(uint32_t fourcc) {
|
||||
return std::string(reinterpret_cast<char*>(&fourcc), 4);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Type, int bitsToWrite = 8 * sizeof(Type)>
|
||||
void write(Type value, std::ostream& stream) {
|
||||
static_assert(bitsToWrite % 8 == 0, "Cannot write fractional bytes.");
|
||||
static_assert(bitsToWrite <= sizeof(Type) * 8, "Bits to write exceed target type size.");
|
||||
|
||||
char* p = reinterpret_cast<char*>(&value);
|
||||
const int bytesToWrite = bitsToWrite / 8;
|
||||
for (int byteIndex = 0; byteIndex < bytesToWrite; byteIndex++) {
|
||||
stream.put(*(p + byteIndex));
|
||||
}
|
||||
}
|
||||
|
||||
constexpr uint32_t fourcc(unsigned char c0, unsigned char c1, unsigned char c2, unsigned char c3) {
|
||||
return c0 | (c1 << 8) | (c2 << 16) | (c3 << 24);
|
||||
}
|
||||
|
||||
inline std::string fourccToString(uint32_t fourcc) {
|
||||
return std::string(reinterpret_cast<char*>(&fourcc), 4);
|
||||
}
|
||||
|
||||
} // namespace little_endian
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "processing.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using std::function;
|
||||
|
@ -35,7 +36,9 @@ void process16bitAudioClip(
|
|||
processBuffer(buffer);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "AudioClip.h"
|
||||
#include "tools/progress.h"
|
||||
|
||||
|
@ -18,4 +19,4 @@ void process16bitAudioClip(
|
|||
ProgressSink& progressSink
|
||||
);
|
||||
|
||||
std::vector<int16_t> copyTo16bitBuffer(const AudioClip& audioClip);
|
||||
std::vector<int16_t> copyTo16bitBuffer(const AudioClip& audioClip);
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
#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 "tools/parallel.h"
|
||||
#include <webrtc/common_audio/vad/include/webrtc_vad.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 fmt::format;
|
||||
using std::runtime_error;
|
||||
using std::unique_ptr;
|
||||
using std::vector;
|
||||
|
||||
JoiningBoundedTimeline<void> detectVoiceActivity(
|
||||
const AudioClip& inputAudioClip,
|
||||
ProgressSink& progressSink
|
||||
const AudioClip& inputAudioClip, ProgressSink& progressSink
|
||||
) {
|
||||
// Prepare audio for VAD
|
||||
constexpr int webRtcSamplingRate = 8000;
|
||||
const unique_ptr<AudioClip> audioClip = inputAudioClip.clone()
|
||||
| resample(webRtcSamplingRate)
|
||||
| removeDcOffset();
|
||||
const unique_ptr<AudioClip> audioClip =
|
||||
inputAudioClip.clone() | resample(webRtcSamplingRate) | removeDcOffset();
|
||||
|
||||
VadInst* vadHandle = WebRtcVad_Create();
|
||||
if (!vadHandle) throw runtime_error("Error creating WebRTC VAD handle.");
|
||||
|
@ -46,12 +47,8 @@ JoiningBoundedTimeline<void> detectVoiceActivity(
|
|||
// WebRTC is picky regarding buffer size
|
||||
if (buffer.size() < frameSize) return;
|
||||
|
||||
const int result = WebRtcVad_Process(
|
||||
vadHandle,
|
||||
webRtcSamplingRate,
|
||||
buffer.data(),
|
||||
buffer.size()
|
||||
);
|
||||
const int result =
|
||||
WebRtcVad_Process(vadHandle, webRtcSamplingRate, buffer.data(), buffer.size());
|
||||
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.
|
||||
|
@ -86,9 +83,12 @@ JoiningBoundedTimeline<void> detectVoiceActivity(
|
|||
logging::debugFormat(
|
||||
"Found {} sections of voice activity: {}",
|
||||
activity.size(),
|
||||
join(activity | transformed([](const Timed<void>& t) {
|
||||
return format("{0}-{1}", t.getStart(), t.getEnd());
|
||||
}), ", ")
|
||||
join(
|
||||
activity | transformed([](const Timed<void>& t) {
|
||||
return format("{0}-{1}", t.getStart(), t.getEnd());
|
||||
}),
|
||||
", "
|
||||
)
|
||||
);
|
||||
|
||||
return activity;
|
||||
|
|
|
@ -4,6 +4,5 @@
|
|||
#include "tools/progress.h"
|
||||
|
||||
JoiningBoundedTimeline<void> detectVoiceActivity(
|
||||
const AudioClip& audioClip,
|
||||
ProgressSink& progressSink
|
||||
const AudioClip& audioClip, ProgressSink& progressSink
|
||||
);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include <fstream>
|
||||
#include "waveFileWriting.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "ioTools.h"
|
||||
|
||||
using namespace little_endian;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#include "Phone.h"
|
||||
|
||||
using std::string;
|
||||
using boost::optional;
|
||||
using std::string;
|
||||
|
||||
PhoneConverter& PhoneConverter::get() {
|
||||
static PhoneConverter converter;
|
||||
|
@ -13,54 +13,24 @@ string PhoneConverter::getTypeName() {
|
|||
}
|
||||
|
||||
EnumConverter<Phone>::member_data PhoneConverter::getMemberData() {
|
||||
return member_data {
|
||||
{ Phone::AO, "AO" },
|
||||
{ Phone::AA, "AA" },
|
||||
{ Phone::IY, "IY" },
|
||||
{ Phone::UW, "UW" },
|
||||
{ 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" },
|
||||
return member_data{{Phone::AO, "AO"}, {Phone::AA, "AA"}, {Phone::IY, "IY"},
|
||||
{Phone::UW, "UW"}, {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::P, "P" },
|
||||
{ Phone::B, "B" },
|
||||
{ Phone::T, "T" },
|
||||
{ Phone::D, "D" },
|
||||
{ Phone::K, "K" },
|
||||
{ Phone::G, "G" },
|
||||
{ Phone::CH, "CH" },
|
||||
{ Phone::JH, "JH" },
|
||||
{ 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::P, "P"}, {Phone::B, "B"}, {Phone::T, "T"},
|
||||
{Phone::D, "D"}, {Phone::K, "K"}, {Phone::G, "G"},
|
||||
{Phone::CH, "CH"}, {Phone::JH, "JH"}, {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::Cough, "Cough" },
|
||||
{ Phone::Smack, "Smack" },
|
||||
{ Phone::Noise, "Noise" }
|
||||
};
|
||||
{Phone::Breath, "Breath"}, {Phone::Cough, "Cough"}, {Phone::Smack, "Smack"},
|
||||
{Phone::Noise, "Noise"}};
|
||||
}
|
||||
|
||||
optional<Phone> PhoneConverter::tryParse(const string& s) {
|
||||
|
|
|
@ -8,66 +8,66 @@ enum class Phone {
|
|||
// Vowels
|
||||
|
||||
// ... monophthongs
|
||||
AO, // [ɔ] as in [o]ff, f[a]ll, fr[o]st
|
||||
AA, // [ɑ] as in f[a]ther
|
||||
IY, // [i] as in b[ee], sh[e]
|
||||
UW, // [u] as in y[ou], n[ew], f[oo]d
|
||||
EH, // [ɛ] as in r[e]d, m[e]n
|
||||
IH, // [ɪ] as in b[i]g, w[i]n
|
||||
UH, // [ʊ] as in sh[ou]ld, c[ou]ld
|
||||
AH, // [ʌ] as in b[u]t, s[u]n
|
||||
Schwa, // [ə] as in [a]lone, disc[u]s
|
||||
AE, // [æ] as in [a]t, b[a]t
|
||||
AO, // [ɔ] as in [o]ff, f[a]ll, fr[o]st
|
||||
AA, // [ɑ] as in f[a]ther
|
||||
IY, // [i] as in b[ee], sh[e]
|
||||
UW, // [u] as in y[ou], n[ew], f[oo]d
|
||||
EH, // [ɛ] as in r[e]d, m[e]n
|
||||
IH, // [ɪ] as in b[i]g, w[i]n
|
||||
UH, // [ʊ] as in sh[ou]ld, c[ou]ld
|
||||
AH, // [ʌ] as in b[u]t, s[u]n
|
||||
Schwa, // [ə] as in [a]lone, disc[u]s
|
||||
AE, // [æ] as in [a]t, b[a]t
|
||||
|
||||
// ... diphthongs
|
||||
EY, // [eɪ] as in s[ay], [ei]ght
|
||||
AY, // [aɪ] as in m[y], wh[y], r[i]de
|
||||
OW, // [oʊ] as in sh[ow], c[oa]t
|
||||
AW, // [aʊ] as in h[ow], n[ow]
|
||||
OY, // [ɔɪ] as in b[oy], t[oy]
|
||||
EY, // [eɪ] as in s[ay], [ei]ght
|
||||
AY, // [aɪ] as in m[y], wh[y], r[i]de
|
||||
OW, // [oʊ] as in sh[ow], c[oa]t
|
||||
AW, // [aʊ] as in h[ow], n[ow]
|
||||
OY, // [ɔɪ] as in b[oy], t[oy]
|
||||
|
||||
// ... r-colored
|
||||
ER, // [ɝ] as in h[er], b[ir]d, h[ur]t
|
||||
ER, // [ɝ] as in h[er], b[ir]d, h[ur]t
|
||||
LastVowel = ER,
|
||||
|
||||
/////////////
|
||||
// Consonants
|
||||
|
||||
// ... stops
|
||||
P, // [p] as in [p]ay
|
||||
B, // [b] as in [b]uy
|
||||
T, // [t] as in [t]ake
|
||||
D, // [d] as in [d]ay
|
||||
K, // [k] as in [k]ey
|
||||
G, // [g] as in [g]o
|
||||
P, // [p] as in [p]ay
|
||||
B, // [b] as in [b]uy
|
||||
T, // [t] as in [t]ake
|
||||
D, // [d] as in [d]ay
|
||||
K, // [k] as in [k]ey
|
||||
G, // [g] as in [g]o
|
||||
|
||||
// ... affricates
|
||||
CH, // [tʃ] as in [ch]air
|
||||
JH, // [dʒ] as in [j]ust
|
||||
CH, // [tʃ] as in [ch]air
|
||||
JH, // [dʒ] as in [j]ust
|
||||
|
||||
// ... fricatives
|
||||
F, // [f] as in [f]or
|
||||
V, // [v] as in [v]ery
|
||||
TH, // [θ] as in [th]anks
|
||||
DH, // [ð] as in [th]at
|
||||
S, // [s] as in [s]ay
|
||||
Z, // [z] as in [z]oo
|
||||
SH, // [ʃ] as in [sh]ow
|
||||
ZH, // [ʒ] as in mea[s]ure, plea[s]ure
|
||||
HH, // [h] as in [h]ouse
|
||||
F, // [f] as in [f]or
|
||||
V, // [v] as in [v]ery
|
||||
TH, // [θ] as in [th]anks
|
||||
DH, // [ð] as in [th]at
|
||||
S, // [s] as in [s]ay
|
||||
Z, // [z] as in [z]oo
|
||||
SH, // [ʃ] as in [sh]ow
|
||||
ZH, // [ʒ] as in mea[s]ure, plea[s]ure
|
||||
HH, // [h] as in [h]ouse
|
||||
|
||||
// ... nasals
|
||||
M, // [m] as in [m]an
|
||||
N, // [n] as in [no]
|
||||
NG, // [ŋ] as in si[ng]
|
||||
M, // [m] as in [m]an
|
||||
N, // [n] as in [no]
|
||||
NG, // [ŋ] as in si[ng]
|
||||
|
||||
// ... liquids
|
||||
L, // [ɫ] as in [l]ate
|
||||
R, // [r, ɹ] as in [r]un
|
||||
L, // [ɫ] as in [l]ate
|
||||
R, // [r, ɹ] as in [r]un
|
||||
|
||||
// ... semivowels
|
||||
Y, // [j] as in [y]es
|
||||
W, // [w] as in [w]ay
|
||||
Y, // [j] as in [y]es
|
||||
W, // [w] as in [w]ay
|
||||
|
||||
/////////////
|
||||
// Misc.
|
||||
|
@ -81,9 +81,11 @@ enum class Phone {
|
|||
class PhoneConverter : public EnumConverter<Phone> {
|
||||
public:
|
||||
static PhoneConverter& get();
|
||||
|
||||
protected:
|
||||
std::string getTypeName() override;
|
||||
member_data getMemberData() override;
|
||||
|
||||
public:
|
||||
boost::optional<Phone> tryParse(const std::string& s) override;
|
||||
};
|
||||
|
@ -92,4 +94,4 @@ std::ostream& operator<<(std::ostream& stream, Phone value);
|
|||
|
||||
std::istream& operator>>(std::istream& stream, Phone& value);
|
||||
|
||||
bool isVowel(Phone phone);
|
||||
bool isVowel(Phone phone);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#include "Shape.h"
|
||||
|
||||
using std::string;
|
||||
using std::set;
|
||||
using std::string;
|
||||
|
||||
ShapeConverter& ShapeConverter::get() {
|
||||
static ShapeConverter converter;
|
||||
|
@ -22,7 +22,9 @@ set<Shape> ShapeConverter::getBasicShapes() {
|
|||
set<Shape> ShapeConverter::getExtendedShapes() {
|
||||
static const 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));
|
||||
}
|
||||
return result;
|
||||
|
@ -35,16 +37,16 @@ string ShapeConverter::getTypeName() {
|
|||
}
|
||||
|
||||
EnumConverter<Shape>::member_data ShapeConverter::getMemberData() {
|
||||
return member_data {
|
||||
{ Shape::A, "A" },
|
||||
{ Shape::B, "B" },
|
||||
{ Shape::C, "C" },
|
||||
{ Shape::D, "D" },
|
||||
{ Shape::E, "E" },
|
||||
{ Shape::F, "F" },
|
||||
{ Shape::G, "G" },
|
||||
{ Shape::H, "H" },
|
||||
{ Shape::X, "X" }
|
||||
return member_data{
|
||||
{Shape::A, "A"},
|
||||
{Shape::B, "B"},
|
||||
{Shape::C, "C"},
|
||||
{Shape::D, "D"},
|
||||
{Shape::E, "E"},
|
||||
{Shape::F, "F"},
|
||||
{Shape::G, "G"},
|
||||
{Shape::H, "H"},
|
||||
{Shape::X, "X"}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "tools/EnumConverter.h"
|
||||
#include <set>
|
||||
|
||||
#include "tools/EnumConverter.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 visual examples, see https://flic.kr/s/aHsj86KR4J. Their shapes "BMP".."L" map to A..H.
|
||||
enum class Shape {
|
||||
// Basic shapes
|
||||
|
||||
A, // Closed mouth (M, B, P)
|
||||
B, // Clenched teeth (most consonants, some vowels like EE as in b[ee])
|
||||
C, // Open mouth (vowels like m[e]n, s[u]n, s[a]y)
|
||||
D, // Mouth wide open (vowels like f[a]ther, b[a]t, wh[y])
|
||||
E, // Rounded mouth (vowels like [o]ff)
|
||||
F, // Puckered lips (y[ou], b[o]y, [w]ay)
|
||||
|
||||
A, // Closed mouth (M, B, P)
|
||||
B, // Clenched teeth (most consonants, some vowels like EE as in b[ee])
|
||||
C, // Open mouth (vowels like m[e]n, s[u]n, s[a]y)
|
||||
D, // Mouth wide open (vowels like f[a]ther, b[a]t, wh[y])
|
||||
E, // Rounded mouth (vowels like [o]ff)
|
||||
F, // Puckered lips (y[ou], b[o]y, [w]ay)
|
||||
LastBasicShape = F,
|
||||
|
||||
// Extended shapes
|
||||
|
||||
G, // "F", "V"
|
||||
H, // "L"
|
||||
X, // Idle
|
||||
G, // "F", "V"
|
||||
H, // "L"
|
||||
X, // Idle
|
||||
|
||||
EndSentinel
|
||||
};
|
||||
|
@ -31,6 +32,7 @@ public:
|
|||
static ShapeConverter& get();
|
||||
static std::set<Shape> getBasicShapes();
|
||||
static std::set<Shape> getExtendedShapes();
|
||||
|
||||
protected:
|
||||
std::string getTypeName() override;
|
||||
member_data getMemberData() override;
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
#include "DatExporter.h"
|
||||
#include "animation/targetShapeSet.h"
|
||||
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
#include "animation/targetShapeSet.h"
|
||||
|
||||
using std::string;
|
||||
using std::chrono::duration;
|
||||
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),
|
||||
convertToPrestonBlair(convertToPrestonBlair),
|
||||
prestonBlairShapeNames {
|
||||
{ Shape::A, "MBP" },
|
||||
{ Shape::B, "etc" },
|
||||
{ Shape::C, "E" },
|
||||
{ Shape::D, "AI" },
|
||||
{ Shape::E, "O" },
|
||||
{ Shape::F, "U" },
|
||||
{ Shape::G, "FV" },
|
||||
{ Shape::H, "L" },
|
||||
{ Shape::X, "rest" },
|
||||
}
|
||||
{
|
||||
prestonBlairShapeNames{
|
||||
{Shape::A, "MBP"},
|
||||
{Shape::B, "etc"},
|
||||
{Shape::C, "E"},
|
||||
{Shape::D, "AI"},
|
||||
{Shape::E, "O"},
|
||||
{Shape::F, "U"},
|
||||
{Shape::G, "FV"},
|
||||
{Shape::H, "L"},
|
||||
{Shape::X, "rest"},
|
||||
} {
|
||||
// Animation works with a fixed frame rate of 100.
|
||||
// Downsampling to much less than 25 fps may result in dropped frames.
|
||||
// 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;
|
||||
|
||||
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) {
|
||||
for (Shape shape : targetShapeSet) {
|
||||
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,9 +69,8 @@ void DatExporter::exportAnimation(const ExporterInput& input, std::ostream& outp
|
|||
}
|
||||
|
||||
string DatExporter::toString(Shape shape) const {
|
||||
return convertToPrestonBlair
|
||||
? prestonBlairShapeNames.at(shape)
|
||||
: boost::lexical_cast<std::string>(shape);
|
||||
return convertToPrestonBlair ? prestonBlairShapeNames.at(shape)
|
||||
: boost::lexical_cast<std::string>(shape);
|
||||
}
|
||||
|
||||
int DatExporter::toFrameNumber(centiseconds time) const {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include "Exporter.h"
|
||||
#include "core/Shape.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "core/Shape.h"
|
||||
#include "Exporter.h"
|
||||
|
||||
// Exporter for Moho's switch data file format
|
||||
class DatExporter : public Exporter {
|
||||
public:
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "core/Shape.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include <filesystem>
|
||||
|
||||
class ExporterInput {
|
||||
public:
|
||||
ExporterInput(
|
||||
const std::filesystem::path& inputFilePath,
|
||||
const JoiningContinuousTimeline<Shape>& animation,
|
||||
const ShapeSet& targetShapeSet) :
|
||||
const ShapeSet& targetShapeSet
|
||||
) :
|
||||
inputFilePath(inputFilePath),
|
||||
animation(animation),
|
||||
targetShapeSet(targetShapeSet) {}
|
||||
|
@ -22,5 +24,6 @@ public:
|
|||
class Exporter {
|
||||
public:
|
||||
virtual ~Exporter() {}
|
||||
|
||||
virtual void exportAnimation(const ExporterInput& input, std::ostream& outputStream) = 0;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "JsonExporter.h"
|
||||
|
||||
#include "exporterTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
|
@ -10,8 +11,10 @@ void JsonExporter::exportAnimation(const ExporterInput& input, std::ostream& out
|
|||
// the formatting.
|
||||
outputStream << "{\n";
|
||||
outputStream << " \"metadata\": {\n";
|
||||
outputStream << " \"soundFile\": \"" << escapeJsonString(absolute(input.inputFilePath).u8string()) << "\",\n";
|
||||
outputStream << " \"duration\": " << formatDuration(input.animation.getRange().getDuration()) << "\n";
|
||||
outputStream << " \"soundFile\": \""
|
||||
<< escapeJsonString(absolute(input.inputFilePath).u8string()) << "\",\n";
|
||||
outputStream << " \"duration\": " << formatDuration(input.animation.getRange().getDuration())
|
||||
<< "\n";
|
||||
outputStream << " },\n";
|
||||
outputStream << " \"mouthCues\": [\n";
|
||||
bool isFirst = true;
|
||||
|
@ -19,8 +22,8 @@ void JsonExporter::exportAnimation(const ExporterInput& input, std::ostream& out
|
|||
if (!isFirst) outputStream << ",\n";
|
||||
isFirst = false;
|
||||
outputStream << " { \"start\": " << formatDuration(timedShape.getStart())
|
||||
<< ", \"end\": " << formatDuration(timedShape.getEnd())
|
||||
<< ", \"value\": \"" << timedShape.getValue() << "\" }";
|
||||
<< ", \"end\": " << formatDuration(timedShape.getEnd()) << ", \"value\": \""
|
||||
<< timedShape.getValue() << "\" }";
|
||||
}
|
||||
outputStream << "\n";
|
||||
outputStream << " ]\n";
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
#include "TsvExporter.h"
|
||||
|
||||
#include "animation/targetShapeSet.h"
|
||||
|
||||
void TsvExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
|
||||
// Output shapes with start times
|
||||
for (auto& timedShape : input.animation) {
|
||||
outputStream
|
||||
<< formatDuration(timedShape.getStart())
|
||||
<< "\t"
|
||||
<< timedShape.getValue()
|
||||
<< "\n";
|
||||
outputStream << formatDuration(timedShape.getStart()) << "\t" << timedShape.getValue()
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
// Output closed mouth with end time
|
||||
outputStream
|
||||
<< formatDuration(input.animation.getRange().getEnd())
|
||||
<< "\t"
|
||||
<< convertToTargetShapeSet(Shape::X, input.targetShapeSet)
|
||||
<< "\n";
|
||||
outputStream << formatDuration(input.animation.getRange().getEnd()) << "\t"
|
||||
<< convertToTargetShapeSet(Shape::X, input.targetShapeSet) << "\n";
|
||||
}
|
||||
|
|
|
@ -6,4 +6,3 @@ class TsvExporter : public Exporter {
|
|||
public:
|
||||
void exportAnimation(const ExporterInput& input, std::ostream& outputStream) override;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
#include "XmlExporter.h"
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
#include <boost/version.hpp>
|
||||
|
||||
#include "exporterTools.h"
|
||||
|
||||
using std::string;
|
||||
using boost::property_tree::ptree;
|
||||
using std::string;
|
||||
|
||||
void XmlExporter::exportAnimation(const ExporterInput& input, std::ostream& outputStream) {
|
||||
ptree tree;
|
||||
|
@ -13,22 +15,19 @@ void XmlExporter::exportAnimation(const ExporterInput& input, std::ostream& outp
|
|||
// Add metadata
|
||||
tree.put("rhubarbResult.metadata.soundFile", absolute(input.inputFilePath).u8string());
|
||||
tree.put(
|
||||
"rhubarbResult.metadata.duration",
|
||||
formatDuration(input.animation.getRange().getDuration())
|
||||
"rhubarbResult.metadata.duration", formatDuration(input.animation.getRange().getDuration())
|
||||
);
|
||||
|
||||
// Add mouth cues
|
||||
for (auto& timedShape : dummyShapeIfEmpty(input.animation, input.targetShapeSet)) {
|
||||
ptree& mouthCueElement = tree.add(
|
||||
"rhubarbResult.mouthCues.mouthCue",
|
||||
timedShape.getValue()
|
||||
);
|
||||
ptree& mouthCueElement =
|
||||
tree.add("rhubarbResult.mouthCues.mouthCue", timedShape.getValue());
|
||||
mouthCueElement.put("<xmlattr>.start", formatDuration(timedShape.getStart()));
|
||||
mouthCueElement.put("<xmlattr>.end", formatDuration(timedShape.getEnd()));
|
||||
}
|
||||
|
||||
#ifndef BOOST_VERSION //present in version.hpp
|
||||
#error "Could not detect Boost version."
|
||||
#ifndef BOOST_VERSION // present in version.hpp
|
||||
#error "Could not detect Boost version."
|
||||
#endif
|
||||
|
||||
#if BOOST_VERSION < 105600 // Support legacy syntax
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#include "exporterTools.h"
|
||||
|
||||
#include "animation/targetShapeSet.h"
|
||||
|
||||
// Makes sure there is at least one mouth shape
|
||||
std::vector<Timed<Shape>> dummyShapeIfEmpty(
|
||||
const JoiningTimeline<Shape>& animation,
|
||||
const ShapeSet& targetShapeSet
|
||||
const JoiningTimeline<Shape>& animation, const ShapeSet& targetShapeSet
|
||||
) {
|
||||
std::vector<Timed<Shape>> result;
|
||||
std::copy(animation.begin(), animation.end(), std::back_inserter(result));
|
||||
|
|
|
@ -5,6 +5,5 @@
|
|||
|
||||
// Makes sure there is at least one mouth shape
|
||||
std::vector<Timed<Shape>> dummyShapeIfEmpty(
|
||||
const JoiningTimeline<Shape>& animation,
|
||||
const ShapeSet& targetShapeSet
|
||||
const JoiningTimeline<Shape>& animation, const ShapeSet& targetShapeSet
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#include "rhubarbLib.h"
|
||||
#include "core/Phone.h"
|
||||
#include "tools/textFiles.h"
|
||||
|
||||
#include "animation/mouthAnimation.h"
|
||||
#include "audio/audioFileReading.h"
|
||||
#include "core/Phone.h"
|
||||
#include "tools/textFiles.h"
|
||||
|
||||
using boost::optional;
|
||||
using std::string;
|
||||
|
@ -14,8 +15,8 @@ JoiningContinuousTimeline<Shape> animateAudioClip(
|
|||
const Recognizer& recognizer,
|
||||
const ShapeSet& targetShapeSet,
|
||||
int maxThreadCount,
|
||||
ProgressSink& progressSink)
|
||||
{
|
||||
ProgressSink& progressSink
|
||||
) {
|
||||
const BoundedTimeline<Phone> phones =
|
||||
recognizer.recognizePhones(audioClip, dialog, maxThreadCount, progressSink);
|
||||
JoiningContinuousTimeline<Shape> result = animate(phones, targetShapeSet);
|
||||
|
@ -28,8 +29,10 @@ JoiningContinuousTimeline<Shape> animateWaveFile(
|
|||
const Recognizer& recognizer,
|
||||
const ShapeSet& targetShapeSet,
|
||||
int maxThreadCount,
|
||||
ProgressSink& progressSink)
|
||||
{
|
||||
ProgressSink& progressSink
|
||||
) {
|
||||
const auto audioClip = createAudioFileClip(filePath);
|
||||
return animateAudioClip(*audioClip, dialog, recognizer, targetShapeSet, maxThreadCount, progressSink);
|
||||
return animateAudioClip(
|
||||
*audioClip, dialog, recognizer, targetShapeSet, maxThreadCount, progressSink
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "core/Shape.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "audio/AudioClip.h"
|
||||
#include "tools/progress.h"
|
||||
#include <filesystem>
|
||||
|
||||
#include "animation/targetShapeSet.h"
|
||||
#include "audio/AudioClip.h"
|
||||
#include "core/Shape.h"
|
||||
#include "recognition/Recognizer.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "tools/progress.h"
|
||||
|
||||
JoiningContinuousTimeline<Shape> animateAudioClip(
|
||||
const AudioClip& audioClip,
|
||||
|
@ -14,7 +15,8 @@ JoiningContinuousTimeline<Shape> animateAudioClip(
|
|||
const Recognizer& recognizer,
|
||||
const ShapeSet& targetShapeSet,
|
||||
int maxThreadCount,
|
||||
ProgressSink& progressSink);
|
||||
ProgressSink& progressSink
|
||||
);
|
||||
|
||||
JoiningContinuousTimeline<Shape> animateWaveFile(
|
||||
std::filesystem::path filePath,
|
||||
|
@ -22,4 +24,5 @@ JoiningContinuousTimeline<Shape> animateWaveFile(
|
|||
const Recognizer& recognizer,
|
||||
const ShapeSet& targetShapeSet,
|
||||
int maxThreadCount,
|
||||
ProgressSink& progressSink);
|
||||
ProgressSink& progressSink
|
||||
);
|
||||
|
|
|
@ -1,39 +1,38 @@
|
|||
#include "Entry.h"
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
using std::lock_guard;
|
||||
using std::unordered_map;
|
||||
using std::string;
|
||||
using std::unordered_map;
|
||||
|
||||
namespace logging {
|
||||
|
||||
// Returns an int representing the current thread.
|
||||
// This used to be a simple thread_local variable, but Xcode doesn't support that yet
|
||||
int getThreadCounter() {
|
||||
using thread_id = std::thread::id;
|
||||
// Returns an int representing the current thread.
|
||||
// This used to be a simple thread_local variable, but Xcode doesn't support that yet
|
||||
int getThreadCounter() {
|
||||
using thread_id = std::thread::id;
|
||||
|
||||
static std::mutex counterMutex;
|
||||
lock_guard<std::mutex> lock(counterMutex);
|
||||
static std::mutex counterMutex;
|
||||
lock_guard<std::mutex> lock(counterMutex);
|
||||
|
||||
static unordered_map<thread_id, int> threadCounters;
|
||||
static int lastThreadId = 0;
|
||||
thread_id threadId = std::this_thread::get_id();
|
||||
if (threadCounters.find(threadId) == threadCounters.end()) {
|
||||
threadCounters.insert({ threadId, ++lastThreadId });
|
||||
}
|
||||
return threadCounters.find(threadId)->second;
|
||||
static unordered_map<thread_id, int> threadCounters;
|
||||
static int lastThreadId = 0;
|
||||
thread_id threadId = std::this_thread::get_id();
|
||||
if (threadCounters.find(threadId) == threadCounters.end()) {
|
||||
threadCounters.insert({threadId, ++lastThreadId});
|
||||
}
|
||||
|
||||
Entry::Entry(Level level, const string& message) :
|
||||
timestamp(),
|
||||
level(level),
|
||||
message(message)
|
||||
{
|
||||
time(×tamp);
|
||||
this->threadCounter = getThreadCounter();
|
||||
}
|
||||
|
||||
return threadCounters.find(threadId)->second;
|
||||
}
|
||||
|
||||
Entry::Entry(Level level, const string& message) :
|
||||
timestamp(),
|
||||
level(level),
|
||||
message(message) {
|
||||
time(×tamp);
|
||||
this->threadCounter = getThreadCounter();
|
||||
}
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
#include "Level.h"
|
||||
|
||||
namespace logging {
|
||||
|
||||
struct Entry {
|
||||
Entry(Level level, const std::string& message);
|
||||
virtual ~Entry() = default;
|
||||
|
||||
time_t timestamp;
|
||||
int threadCounter;
|
||||
Level level;
|
||||
std::string message;
|
||||
};
|
||||
struct Entry {
|
||||
Entry(Level level, const std::string& message);
|
||||
virtual ~Entry() = default;
|
||||
|
||||
}
|
||||
time_t timestamp;
|
||||
int threadCounter;
|
||||
Level level;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "Entry.h"
|
||||
|
||||
namespace logging {
|
||||
|
||||
class Formatter {
|
||||
public:
|
||||
virtual ~Formatter() = default;
|
||||
virtual std::string format(const Entry& entry) = 0;
|
||||
};
|
||||
class Formatter {
|
||||
public:
|
||||
virtual ~Formatter() = default;
|
||||
virtual std::string format(const Entry& entry) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace logging
|
||||
|
|
|
@ -4,32 +4,32 @@ using std::string;
|
|||
|
||||
namespace logging {
|
||||
|
||||
LevelConverter& LevelConverter::get() {
|
||||
static LevelConverter converter;
|
||||
return converter;
|
||||
}
|
||||
|
||||
string LevelConverter::getTypeName() {
|
||||
return "Level";
|
||||
}
|
||||
|
||||
EnumConverter<Level>::member_data LevelConverter::getMemberData() {
|
||||
return member_data {
|
||||
{ Level::Trace, "Trace" },
|
||||
{ Level::Debug, "Debug" },
|
||||
{ Level::Info, "Info" },
|
||||
{ Level::Warn, "Warn" },
|
||||
{ Level::Error, "Error" },
|
||||
{ Level::Fatal, "Fatal" }
|
||||
};
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& stream, Level value) {
|
||||
return LevelConverter::get().write(stream, value);
|
||||
}
|
||||
|
||||
std::istream& operator >>(std::istream& stream, Level& value) {
|
||||
return LevelConverter::get().read(stream, value);
|
||||
}
|
||||
|
||||
LevelConverter& LevelConverter::get() {
|
||||
static LevelConverter converter;
|
||||
return converter;
|
||||
}
|
||||
|
||||
string LevelConverter::getTypeName() {
|
||||
return "Level";
|
||||
}
|
||||
|
||||
EnumConverter<Level>::member_data LevelConverter::getMemberData() {
|
||||
return member_data{
|
||||
{Level::Trace, "Trace"},
|
||||
{Level::Debug, "Debug"},
|
||||
{Level::Info, "Info"},
|
||||
{Level::Warn, "Warn"},
|
||||
{Level::Error, "Error"},
|
||||
{Level::Fatal, "Fatal"}
|
||||
};
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& stream, Level value) {
|
||||
return LevelConverter::get().write(stream, value);
|
||||
}
|
||||
|
||||
std::istream& operator>>(std::istream& stream, Level& value) {
|
||||
return LevelConverter::get().read(stream, value);
|
||||
}
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -4,26 +4,19 @@
|
|||
|
||||
namespace logging {
|
||||
|
||||
enum class Level {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Fatal,
|
||||
EndSentinel
|
||||
};
|
||||
enum class Level { Trace, Debug, Info, Warn, Error, Fatal, EndSentinel };
|
||||
|
||||
class LevelConverter : public EnumConverter<Level> {
|
||||
public:
|
||||
static LevelConverter& get();
|
||||
protected:
|
||||
std::string getTypeName() override;
|
||||
member_data getMemberData() override;
|
||||
};
|
||||
class LevelConverter : public EnumConverter<Level> {
|
||||
public:
|
||||
static LevelConverter& get();
|
||||
|
||||
std::ostream& operator<<(std::ostream& stream, Level value);
|
||||
protected:
|
||||
std::string getTypeName() override;
|
||||
member_data getMemberData() override;
|
||||
};
|
||||
|
||||
std::istream& operator >>(std::istream& stream, Level& value);
|
||||
std::ostream& operator<<(std::ostream& stream, Level value);
|
||||
|
||||
}
|
||||
std::istream& operator>>(std::istream& stream, Level& value);
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
namespace logging {
|
||||
|
||||
class Sink {
|
||||
public:
|
||||
virtual ~Sink() = default;
|
||||
virtual void receive(const Entry& entry) = 0;
|
||||
};
|
||||
class Sink {
|
||||
public:
|
||||
virtual ~Sink() = default;
|
||||
virtual void receive(const Entry& entry) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace logging
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#include "formatters.h"
|
||||
|
||||
#include <format.h>
|
||||
|
||||
#include "Entry.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
|
@ -7,17 +9,17 @@ using std::string;
|
|||
|
||||
namespace logging {
|
||||
|
||||
string SimpleConsoleFormatter::format(const Entry& entry) {
|
||||
return fmt::format("[{0}] {1}", entry.level, entry.message);
|
||||
}
|
||||
|
||||
string SimpleFileFormatter::format(const Entry& entry) {
|
||||
return fmt::format(
|
||||
"[{0}] {1} {2}",
|
||||
formatTime(entry.timestamp, "%F %H:%M:%S"),
|
||||
entry.threadCounter,
|
||||
consoleFormatter.format(entry)
|
||||
);
|
||||
}
|
||||
|
||||
string SimpleConsoleFormatter::format(const Entry& entry) {
|
||||
return fmt::format("[{0}] {1}", entry.level, entry.message);
|
||||
}
|
||||
|
||||
string SimpleFileFormatter::format(const Entry& entry) {
|
||||
return fmt::format(
|
||||
"[{0}] {1} {2}",
|
||||
formatTime(entry.timestamp, "%F %H:%M:%S"),
|
||||
entry.threadCounter,
|
||||
consoleFormatter.format(entry)
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
|
||||
namespace logging {
|
||||
|
||||
class SimpleConsoleFormatter : public Formatter {
|
||||
public:
|
||||
std::string format(const Entry& entry) override;
|
||||
};
|
||||
class SimpleConsoleFormatter : public Formatter {
|
||||
public:
|
||||
std::string format(const Entry& entry) override;
|
||||
};
|
||||
|
||||
class SimpleFileFormatter : public Formatter {
|
||||
public:
|
||||
std::string format(const Entry& entry) override;
|
||||
private:
|
||||
SimpleConsoleFormatter consoleFormatter;
|
||||
};
|
||||
class SimpleFileFormatter : public Formatter {
|
||||
public:
|
||||
std::string format(const Entry& entry) override;
|
||||
|
||||
}
|
||||
private:
|
||||
SimpleConsoleFormatter consoleFormatter;
|
||||
};
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
#include "logging.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "Entry.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
using namespace logging;
|
||||
using std::lock_guard;
|
||||
using std::shared_ptr;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::shared_ptr;
|
||||
using std::lock_guard;
|
||||
|
||||
std::mutex& getLogMutex() {
|
||||
static std::mutex mutex;
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include "tools/EnumConverter.h"
|
||||
#include "Sink.h"
|
||||
#include "Level.h"
|
||||
#include "Sink.h"
|
||||
#include "tools/EnumConverter.h"
|
||||
|
||||
namespace logging {
|
||||
|
||||
bool addSink(std::shared_ptr<Sink> sink);
|
||||
bool addSink(std::shared_ptr<Sink> sink);
|
||||
|
||||
bool removeSink(std::shared_ptr<Sink> sink);
|
||||
bool removeSink(std::shared_ptr<Sink> sink);
|
||||
|
||||
void log(const Entry& entry);
|
||||
void log(const Entry& entry);
|
||||
|
||||
void log(Level level, const std::string& message);
|
||||
void log(Level level, const std::string& message);
|
||||
|
||||
template<typename... Args>
|
||||
void logFormat(Level level, fmt::CStringRef format, const Args&... args) {
|
||||
log(level, fmt::format(format, args...));
|
||||
}
|
||||
|
||||
#define LOG_WITH_LEVEL(levelName, levelEnum) \
|
||||
inline void levelName(const std::string& message) { \
|
||||
log(Level::levelEnum, message); \
|
||||
} \
|
||||
template <typename... Args> \
|
||||
void levelName ## Format(fmt::CStringRef format, const Args&... args) { \
|
||||
logFormat(Level::levelEnum, format, args...); \
|
||||
}
|
||||
|
||||
LOG_WITH_LEVEL(trace, Trace)
|
||||
LOG_WITH_LEVEL(debug, Debug)
|
||||
LOG_WITH_LEVEL(info, Info)
|
||||
LOG_WITH_LEVEL(warn, Warn)
|
||||
LOG_WITH_LEVEL(error, Error)
|
||||
LOG_WITH_LEVEL(fatal, Fatal)
|
||||
template <typename... Args>
|
||||
void logFormat(Level level, fmt::CStringRef format, const Args&... args) {
|
||||
log(level, fmt::format(format, args...));
|
||||
}
|
||||
|
||||
#define LOG_WITH_LEVEL(levelName, levelEnum) \
|
||||
inline void levelName(const std::string& message) { \
|
||||
log(Level::levelEnum, message); \
|
||||
} \
|
||||
template <typename... Args> \
|
||||
void levelName##Format(fmt::CStringRef format, const Args&... args) { \
|
||||
logFormat(Level::levelEnum, format, args...); \
|
||||
}
|
||||
|
||||
LOG_WITH_LEVEL(trace, Trace)
|
||||
LOG_WITH_LEVEL(debug, Debug)
|
||||
LOG_WITH_LEVEL(info, Info)
|
||||
LOG_WITH_LEVEL(warn, Warn)
|
||||
LOG_WITH_LEVEL(error, Error)
|
||||
LOG_WITH_LEVEL(fatal, Fatal)
|
||||
} // namespace logging
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
#include "sinks.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "Entry.h"
|
||||
|
||||
using std::string;
|
||||
using std::shared_ptr;
|
||||
using std::string;
|
||||
|
||||
namespace logging {
|
||||
|
||||
LevelFilter::LevelFilter(shared_ptr<Sink> innerSink, Level minLevel) :
|
||||
innerSink(innerSink),
|
||||
minLevel(minLevel)
|
||||
{}
|
||||
LevelFilter::LevelFilter(shared_ptr<Sink> innerSink, Level minLevel) :
|
||||
innerSink(innerSink),
|
||||
minLevel(minLevel) {}
|
||||
|
||||
void LevelFilter::receive(const Entry& entry) {
|
||||
if (entry.level >= minLevel) {
|
||||
innerSink->receive(entry);
|
||||
}
|
||||
void LevelFilter::receive(const Entry& entry) {
|
||||
if (entry.level >= minLevel) {
|
||||
innerSink->receive(entry);
|
||||
}
|
||||
|
||||
StreamSink::StreamSink(shared_ptr<std::ostream> stream, shared_ptr<Formatter> formatter) :
|
||||
stream(stream),
|
||||
formatter(formatter)
|
||||
{}
|
||||
|
||||
void StreamSink::receive(const Entry& entry) {
|
||||
const string line = formatter->format(entry);
|
||||
*stream << line << std::endl;
|
||||
}
|
||||
|
||||
StdErrSink::StdErrSink(shared_ptr<Formatter> formatter) :
|
||||
StreamSink(std::shared_ptr<std::ostream>(&std::cerr, [](void*) {}), formatter)
|
||||
{}
|
||||
|
||||
}
|
||||
|
||||
StreamSink::StreamSink(shared_ptr<std::ostream> stream, shared_ptr<Formatter> formatter) :
|
||||
stream(stream),
|
||||
formatter(formatter) {}
|
||||
|
||||
void StreamSink::receive(const Entry& entry) {
|
||||
const string line = formatter->format(entry);
|
||||
*stream << line << std::endl;
|
||||
}
|
||||
|
||||
StdErrSink::StdErrSink(shared_ptr<Formatter> formatter) :
|
||||
StreamSink(std::shared_ptr<std::ostream>(&std::cerr, [](void*) {}), formatter) {}
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -1,33 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include "Sink.h"
|
||||
#include <memory>
|
||||
|
||||
#include "Formatter.h"
|
||||
#include "Sink.h"
|
||||
|
||||
namespace logging {
|
||||
enum class Level;
|
||||
enum class Level;
|
||||
|
||||
class LevelFilter : public Sink {
|
||||
public:
|
||||
LevelFilter(std::shared_ptr<Sink> innerSink, Level minLevel);
|
||||
void receive(const Entry& entry) override;
|
||||
private:
|
||||
std::shared_ptr<Sink> innerSink;
|
||||
Level minLevel;
|
||||
};
|
||||
class LevelFilter : public Sink {
|
||||
public:
|
||||
LevelFilter(std::shared_ptr<Sink> innerSink, Level minLevel);
|
||||
void receive(const Entry& entry) override;
|
||||
|
||||
class StreamSink : public Sink {
|
||||
public:
|
||||
StreamSink(std::shared_ptr<std::ostream> stream, std::shared_ptr<Formatter> formatter);
|
||||
void receive(const Entry& entry) override;
|
||||
private:
|
||||
std::shared_ptr<std::ostream> stream;
|
||||
std::shared_ptr<Formatter> formatter;
|
||||
};
|
||||
private:
|
||||
std::shared_ptr<Sink> innerSink;
|
||||
Level minLevel;
|
||||
};
|
||||
|
||||
class StdErrSink : public StreamSink {
|
||||
public:
|
||||
explicit StdErrSink(std::shared_ptr<Formatter> formatter);
|
||||
};
|
||||
class StreamSink : public Sink {
|
||||
public:
|
||||
StreamSink(std::shared_ptr<std::ostream> stream, std::shared_ptr<Formatter> formatter);
|
||||
void receive(const Entry& entry) override;
|
||||
|
||||
}
|
||||
private:
|
||||
std::shared_ptr<std::ostream> stream;
|
||||
std::shared_ptr<Formatter> formatter;
|
||||
};
|
||||
|
||||
class StdErrSink : public StreamSink {
|
||||
public:
|
||||
explicit StdErrSink(std::shared_ptr<Formatter> formatter);
|
||||
};
|
||||
|
||||
} // namespace logging
|
||||
|
|
|
@ -1,52 +1,66 @@
|
|||
#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;
|
||||
using std::unique_ptr;
|
||||
using std::string;
|
||||
#include "audio/AudioSegment.h"
|
||||
#include "audio/processing.h"
|
||||
#include "audio/SampleRateConverter.h"
|
||||
#include "time/timedLogging.h"
|
||||
#include "time/Timeline.h"
|
||||
|
||||
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) {
|
||||
UNUSED(dialog);
|
||||
|
||||
lambda_unique_ptr<cmd_ln_t> config(
|
||||
cmd_ln_init(
|
||||
nullptr, ps_args(), true,
|
||||
nullptr,
|
||||
ps_args(),
|
||||
true,
|
||||
// Set acoustic model
|
||||
"-hmm", (getSphinxModelDirectory() / "acoustic-model").u8string().c_str(),
|
||||
"-hmm",
|
||||
(getSphinxModelDirectory() / "acoustic-model").u8string().c_str(),
|
||||
// Set phonetic language model
|
||||
"-allphone", (getSphinxModelDirectory() / "en-us-phone.lm.bin").u8string().c_str(),
|
||||
"-allphone_ci", "yes",
|
||||
"-allphone",
|
||||
(getSphinxModelDirectory() / "en-us-phone.lm.bin").u8string().c_str(),
|
||||
"-allphone_ci",
|
||||
"yes",
|
||||
// Set language model probability weight.
|
||||
// Low values (<= 0.4) can lead to fluttering animation.
|
||||
// High values (>= 1.0) can lead to imprecise or freezing animation.
|
||||
"-lw", "0.8",
|
||||
"-lw",
|
||||
"0.8",
|
||||
// Add noise against zero silence
|
||||
// (see http://cmusphinx.sourceforge.net/wiki/faq#qwhy_my_accuracy_is_poor)
|
||||
"-dither", "yes",
|
||||
"-dither",
|
||||
"yes",
|
||||
// Disable VAD -- we're doing that ourselves
|
||||
"-remove_silence", "no",
|
||||
"-remove_silence",
|
||||
"no",
|
||||
// Perform per-utterance cepstral mean normalization
|
||||
"-cmn", "batch",
|
||||
"-cmn",
|
||||
"batch",
|
||||
|
||||
// The following settings are recommended at
|
||||
// http://cmusphinx.sourceforge.net/wiki/phonemerecognition
|
||||
|
||||
// Set beam width applied to every frame in Viterbi search
|
||||
"-beam", "1e-20",
|
||||
"-beam",
|
||||
"1e-20",
|
||||
// Set beam width applied to phone transitions
|
||||
"-pbeam", "1e-20",
|
||||
nullptr),
|
||||
[](cmd_ln_t* config) { cmd_ln_free_r(config); });
|
||||
"-pbeam",
|
||||
"1e-20",
|
||||
nullptr
|
||||
),
|
||||
[](cmd_ln_t* config) { cmd_ln_free_r(config); }
|
||||
);
|
||||
if (!config) throw runtime_error("Error creating configuration.");
|
||||
|
||||
lambda_unique_ptr<ps_decoder_t> decoder(
|
||||
ps_init(config.get()),
|
||||
[](ps_decoder_t* recognizer) { ps_free(recognizer); });
|
||||
lambda_unique_ptr<ps_decoder_t> decoder(ps_init(config.get()), [](ps_decoder_t* recognizer) {
|
||||
ps_free(recognizer);
|
||||
});
|
||||
if (!decoder) throw runtime_error("Error creating speech decoder.");
|
||||
|
||||
return decoder;
|
||||
|
@ -64,9 +78,8 @@ static Timeline<Phone> utteranceToPhones(
|
|||
paddedTimeRange.grow(padding);
|
||||
paddedTimeRange.trim(audioClip.getTruncatedRange());
|
||||
|
||||
const unique_ptr<AudioClip> clipSegment = audioClip.clone()
|
||||
| segment(paddedTimeRange)
|
||||
| resample(sphinxSampleRate);
|
||||
const unique_ptr<AudioClip> clipSegment =
|
||||
audioClip.clone() | segment(paddedTimeRange) | resample(sphinxSampleRate);
|
||||
const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
|
||||
|
||||
// Detect phones (returned as words)
|
||||
|
@ -109,5 +122,7 @@ BoundedTimeline<Phone> PhoneticRecognizer::recognizePhones(
|
|||
int maxThreadCount,
|
||||
ProgressSink& progressSink
|
||||
) const {
|
||||
return ::recognizePhones(inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink);
|
||||
return ::recognizePhones(
|
||||
inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "Recognizer.h"
|
||||
#include "pocketSphinxTools.h"
|
||||
#include "Recognizer.h"
|
||||
|
||||
class PhoneticRecognizer : public Recognizer {
|
||||
public:
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
#include "PocketSphinxRecognizer.h"
|
||||
#include <regex>
|
||||
|
||||
#include <gsl_util.h>
|
||||
|
||||
#include <regex>
|
||||
|
||||
#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/SampleRateConverter.h"
|
||||
#include "g2p.h"
|
||||
#include "languageModels.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "time/timedLogging.h"
|
||||
#include "tokenization.h"
|
||||
|
||||
extern "C" {
|
||||
#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 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) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -59,23 +64,24 @@ lambda_unique_ptr<ngram_model_t> createDefaultLanguageModel(ps_decoder_t& decode
|
|||
path modelPath = getSphinxModelDirectory() / "en-us.lm.bin";
|
||||
lambda_unique_ptr<ngram_model_t> result(
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(
|
||||
ps_decoder_t& decoder,
|
||||
const string& dialog
|
||||
ps_decoder_t& decoder, const string& dialog
|
||||
) {
|
||||
// Split dialog into normalized words
|
||||
vector<string> words = tokenizeText(
|
||||
dialog,
|
||||
[&](const string& word) { return dictionaryContains(*decoder.dict, word); }
|
||||
);
|
||||
vector<string> words = tokenizeText(dialog, [&](const string& word) {
|
||||
return dictionaryContains(*decoder.dict, word);
|
||||
});
|
||||
|
||||
// Add dialog-specific words to the dictionary
|
||||
addMissingDictionaryWords(words, decoder);
|
||||
|
@ -87,18 +93,16 @@ lambda_unique_ptr<ngram_model_t> createDialogLanguageModel(
|
|||
}
|
||||
|
||||
lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(
|
||||
ps_decoder_t& decoder,
|
||||
const string& dialog
|
||||
ps_decoder_t& decoder, const string& dialog
|
||||
) {
|
||||
auto defaultLanguageModel = createDefaultLanguageModel(decoder);
|
||||
auto dialogLanguageModel = createDialogLanguageModel(decoder, dialog);
|
||||
constexpr int modelCount = 2;
|
||||
array<ngram_model_t*, modelCount> languageModels {
|
||||
defaultLanguageModel.get(),
|
||||
dialogLanguageModel.get()
|
||||
array<ngram_model_t*, modelCount> languageModels{
|
||||
defaultLanguageModel.get(), dialogLanguageModel.get()
|
||||
};
|
||||
array<const char*, modelCount> modelNames { "defaultLM", "dialogLM" };
|
||||
array<float, modelCount> modelWeights { 0.1f, 0.9f };
|
||||
array<const char*, modelCount> modelNames{"defaultLM", "dialogLM"};
|
||||
array<float, modelCount> modelWeights{0.1f, 0.9f};
|
||||
lambda_unique_ptr<ngram_model_t> result(
|
||||
ngram_model_set_init(
|
||||
nullptr,
|
||||
|
@ -107,7 +111,8 @@ lambda_unique_ptr<ngram_model_t> createBiasedLanguageModel(
|
|||
modelWeights.data(),
|
||||
modelCount
|
||||
),
|
||||
[](ngram_model_t* lm) { ngram_model_free(lm); });
|
||||
[](ngram_model_t* lm) { ngram_model_free(lm); }
|
||||
);
|
||||
if (!result) {
|
||||
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) {
|
||||
lambda_unique_ptr<cmd_ln_t> config(
|
||||
cmd_ln_init(
|
||||
nullptr, ps_args(), true,
|
||||
nullptr,
|
||||
ps_args(),
|
||||
true,
|
||||
// Set acoustic model
|
||||
"-hmm", (getSphinxModelDirectory() / "acoustic-model").u8string().c_str(),
|
||||
"-hmm",
|
||||
(getSphinxModelDirectory() / "acoustic-model").u8string().c_str(),
|
||||
// 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
|
||||
// (see http://cmusphinx.sourceforge.net/wiki/faq#qwhy_my_accuracy_is_poor)
|
||||
"-dither", "yes",
|
||||
"-dither",
|
||||
"yes",
|
||||
// Disable VAD -- we're doing that ourselves
|
||||
"-remove_silence", "no",
|
||||
"-remove_silence",
|
||||
"no",
|
||||
// Perform per-utterance cepstral mean normalization
|
||||
"-cmn", "batch",
|
||||
nullptr),
|
||||
[](cmd_ln_t* config) { cmd_ln_free_r(config); });
|
||||
"-cmn",
|
||||
"batch",
|
||||
nullptr
|
||||
),
|
||||
[](cmd_ln_t* config) { cmd_ln_free_r(config); }
|
||||
);
|
||||
if (!config) throw runtime_error("Error creating configuration.");
|
||||
|
||||
lambda_unique_ptr<ps_decoder_t> decoder(
|
||||
ps_init(config.get()),
|
||||
[](ps_decoder_t* recognizer) { ps_free(recognizer); });
|
||||
lambda_unique_ptr<ps_decoder_t> decoder(ps_init(config.get()), [](ps_decoder_t* recognizer) {
|
||||
ps_free(recognizer);
|
||||
});
|
||||
if (!decoder) throw runtime_error("Error creating speech decoder.");
|
||||
|
||||
// Set language model
|
||||
lambda_unique_ptr<ngram_model_t> languageModel(dialog
|
||||
? createBiasedLanguageModel(*decoder, *dialog)
|
||||
: createDefaultLanguageModel(*decoder));
|
||||
lambda_unique_ptr<ngram_model_t> languageModel(
|
||||
dialog ? createBiasedLanguageModel(*decoder, *dialog) : createDefaultLanguageModel(*decoder)
|
||||
);
|
||||
ps_set_lm(decoder.get(), "lm", languageModel.get());
|
||||
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(
|
||||
const vector<s3wid_t>& wordIds,
|
||||
const vector<int16_t>& audioBuffer,
|
||||
ps_decoder_t& decoder)
|
||||
{
|
||||
const vector<s3wid_t>& wordIds, const vector<int16_t>& audioBuffer, ps_decoder_t& decoder
|
||||
) {
|
||||
if (wordIds.empty()) return boost::none;
|
||||
|
||||
// Create alignment list
|
||||
lambda_unique_ptr<ps_alignment_t> alignment(
|
||||
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.");
|
||||
for (s3wid_t wordId : wordIds) {
|
||||
// Add word. Initial value for duration is ignored.
|
||||
|
@ -172,7 +185,8 @@ optional<Timeline<Phone>> getPhoneAlignment(
|
|||
acmod_t* acousticModel = decoder.acmod;
|
||||
lambda_unique_ptr<ps_search_t> search(
|
||||
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.");
|
||||
|
||||
// Start recognition
|
||||
|
@ -190,7 +204,8 @@ optional<Timeline<Phone>> getPhoneAlignment(
|
|||
const int16* nextSample = audioBuffer.data();
|
||||
size_t remainingSamples = audioBuffer.size();
|
||||
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) {
|
||||
ps_search_step(search.get(), acousticModel->output_frame);
|
||||
acmod_advance(acousticModel);
|
||||
|
@ -205,11 +220,8 @@ optional<Timeline<Phone>> getPhoneAlignment(
|
|||
// Extract phones with timestamps
|
||||
char** phoneNames = decoder.dict->mdef->ciname;
|
||||
Timeline<Phone> result;
|
||||
for (
|
||||
ps_alignment_iter_t* it = ps_alignment_phones(alignment.get());
|
||||
it;
|
||||
it = ps_alignment_iter_next(it)
|
||||
) {
|
||||
for (ps_alignment_iter_t* it = ps_alignment_phones(alignment.get()); it;
|
||||
it = ps_alignment_iter_next(it)) {
|
||||
// Get phone
|
||||
ps_alignment_entry_t* phoneEntry = ps_alignment_iter_get(it);
|
||||
const s3cipid_t phoneId = phoneEntry->id.pid.cipid;
|
||||
|
@ -231,16 +243,16 @@ optional<Timeline<Phone>> getPhoneAlignment(
|
|||
return result;
|
||||
}
|
||||
|
||||
// Some words have multiple pronunciations, one of which results in better animation than the others.
|
||||
// This function returns the optimal pronunciation for a select set of these words.
|
||||
// Some words have multiple pronunciations, one of which results in better animation than the
|
||||
// others. This function returns the optimal pronunciation for a select set of these words.
|
||||
string fixPronunciation(const string& word) {
|
||||
const static map<string, string> replacements {
|
||||
{ "into(2)", "into" },
|
||||
{ "to(2)", "to" },
|
||||
{ "to(3)", "to" },
|
||||
{ "today(2)", "today" },
|
||||
{ "tomorrow(2)", "tomorrow" },
|
||||
{ "tonight(2)", "tonight" }
|
||||
const static map<string, string> replacements{
|
||||
{"into(2)", "into"},
|
||||
{"to(2)", "to"},
|
||||
{"to(3)", "to"},
|
||||
{"today(2)", "today"},
|
||||
{"tomorrow(2)", "tomorrow"},
|
||||
{"tonight(2)", "tonight"}
|
||||
};
|
||||
|
||||
const auto pair = replacements.find(word);
|
||||
|
@ -265,9 +277,8 @@ static Timeline<Phone> utteranceToPhones(
|
|||
paddedTimeRange.grow(padding);
|
||||
paddedTimeRange.trim(audioClip.getTruncatedRange());
|
||||
|
||||
const unique_ptr<AudioClip> clipSegment = audioClip.clone()
|
||||
| segment(paddedTimeRange)
|
||||
| resample(sphinxSampleRate);
|
||||
const unique_ptr<AudioClip> clipSegment =
|
||||
audioClip.clone() | segment(paddedTimeRange) | resample(sphinxSampleRate);
|
||||
const auto audioBuffer = copyTo16bitBuffer(*clipSegment);
|
||||
|
||||
// Get words
|
||||
|
@ -307,8 +318,9 @@ static Timeline<Phone> utteranceToPhones(
|
|||
#if BOOST_VERSION < 105600 // Support legacy syntax
|
||||
#define value_or get_value_or
|
||||
#endif
|
||||
Timeline<Phone> utterancePhones = getPhoneAlignment(wordIds, audioBuffer, decoder)
|
||||
.value_or(ContinuousTimeline<Phone>(clipSegment->getTruncatedRange(), Phone::Noise));
|
||||
Timeline<Phone> utterancePhones =
|
||||
getPhoneAlignment(wordIds, audioBuffer, decoder)
|
||||
.value_or(ContinuousTimeline<Phone>(clipSegment->getTruncatedRange(), Phone::Noise));
|
||||
alignmentProgressSink.reportProgress(1.0);
|
||||
utterancePhones.shift(paddedTimeRange.getStart());
|
||||
|
||||
|
@ -338,5 +350,6 @@ BoundedTimeline<Phone> PocketSphinxRecognizer::recognizePhones(
|
|||
ProgressSink& progressSink
|
||||
) const {
|
||||
return ::recognizePhones(
|
||||
inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink);
|
||||
inputAudioClip, dialog, &createDecoder, &utteranceToPhones, maxThreadCount, progressSink
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "Recognizer.h"
|
||||
#include "pocketSphinxTools.h"
|
||||
#include "Recognizer.h"
|
||||
|
||||
class PocketSphinxRecognizer : public Recognizer {
|
||||
public:
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
#include "audio/AudioClip.h"
|
||||
#include "core/Phone.h"
|
||||
#include "tools/progress.h"
|
||||
#include "time/BoundedTimeline.h"
|
||||
#include "tools/progress.h"
|
||||
|
||||
class Recognizer {
|
||||
public:
|
||||
|
@ -15,4 +15,4 @@ public:
|
|||
int maxThreadCount,
|
||||
ProgressSink& progressSink
|
||||
) const = 0;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
#include <g2p.h>
|
||||
#include <regex>
|
||||
#include "tools/stringTools.h"
|
||||
#include "logging/logging.h"
|
||||
|
||||
using std::vector;
|
||||
using std::wstring;
|
||||
using std::regex;
|
||||
using std::wregex;
|
||||
#include <regex>
|
||||
|
||||
#include "logging/logging.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
using std::invalid_argument;
|
||||
using std::pair;
|
||||
using std::regex;
|
||||
using std::vector;
|
||||
using std::wregex;
|
||||
using std::wstring;
|
||||
|
||||
const vector<pair<wregex, wstring>>& getReplacementRules() {
|
||||
static vector<pair<wregex, wstring>> rules {
|
||||
#include "g2pRules.cpp"
|
||||
static vector<pair<wregex, wstring>> rules{
|
||||
#include "g2pRules.cpp"
|
||||
|
||||
// Turn bigrams into unigrams for easier conversion
|
||||
{ wregex(L"ôw"), L"Ω" },
|
||||
{ wregex(L"öy"), L"ω" },
|
||||
{ wregex(L"@r"), L"ɝ" }
|
||||
{wregex(L"ôw"), L"Ω"},
|
||||
{wregex(L"öy"), L"ω"},
|
||||
{wregex(L"@r"), L"ɝ"}
|
||||
};
|
||||
return rules;
|
||||
}
|
||||
|
@ -64,8 +66,7 @@ Phone charToPhone(wchar_t c) {
|
|||
case L'r': return Phone::R;
|
||||
case L'l': return Phone::L;
|
||||
case L'h': return Phone::HH;
|
||||
default:
|
||||
return Phone::Noise;
|
||||
default: return Phone::Noise;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core/Phone.h"
|
||||
|
||||
std::vector<Phone> wordToPhones(const std::string& word);
|
||||
|
|
|
@ -4,212 +4,253 @@
|
|||
// Rules
|
||||
//
|
||||
// get rid of some digraphs
|
||||
{ wregex(L"ch"), L"ç" },
|
||||
{ wregex(L"sh"), L"$$" },
|
||||
{ wregex(L"ph"), L"f" },
|
||||
{ wregex(L"th"), L"+" },
|
||||
{ wregex(L"qu"), L"kw" },
|
||||
// and other spelling-level changes
|
||||
{ wregex(L"w(r)"), L"$1" },
|
||||
{ wregex(L"w(ho)"), 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"(^e)x([aeiouäëïöüâêîôûùò@])"), L"$1gz$2" },
|
||||
{ wregex(L"x"), L"ks" },
|
||||
{ wregex(L"'"), L"" },
|
||||
// gh is particularly variable
|
||||
{ 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"ough(t)"), L"ò$1" },
|
||||
{ wregex(L"augh(t)"), L"ò$1" },
|
||||
{ wregex(L"ough"), L"ö" },
|
||||
{ wregex(L"gh"), L"" },
|
||||
// unpronounceable combinations
|
||||
{ wregex(L"(^)g(n)"), L"$1$2" },
|
||||
{ wregex(L"(^)k(n)"), 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
|
||||
{ wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1ï$2" },
|
||||
{ wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{2})y($)"), L"$1ï$2" },
|
||||
{ wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{3})y($)"), L"$1ï$2" },
|
||||
{ wregex(L"ey"), L"ë" },
|
||||
{ wregex(L"ay"), L"ä" },
|
||||
{ wregex(L"oy"), L"öy" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y([bcdfghjklmnpqrstvwxyzç+$ñ])"), L"$1i$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1i$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y(e$)"), L"$1i$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ]{2})ie($)"), L"$1ï$2" },
|
||||
{ wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ])ie($)"), L"$1ï$2" },
|
||||
// sSl can simplify
|
||||
{ wregex(L"(s)t(l[aeiouäëïöüâêîôûùò@]$)"), L"$1$2" },
|
||||
// affrication of t + front vowel
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ci([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([rl][aeiouäëïöüâêîôûùò@])"), L"$1çu$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])si(o)"), L"$1$$$2" },
|
||||
{ wregex(L"([aeiouäëïöüâêîôûùò@])si(o)"), L"$1j$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])s(ur)"), L"$1$$$2" },
|
||||
{ wregex(L"([aeiouäëïöüâêîôûùò@])s(ur)"), L"$1j$2" },
|
||||
{ wregex(L"(k)s(u[aeiouäëïöüâêîôûùò@])"), L"$1$$$2" },
|
||||
{ wregex(L"(k)s(u[rl])"), L"$1$$$2" },
|
||||
// intervocalic s
|
||||
{ wregex(L"([eiou])s([aeiouäëïöüâêîôûùò@])"), L"$1z$2" },
|
||||
// al to ol (do this before respelling)
|
||||
{ wregex(L"a(ls)"), L"ò$1" },
|
||||
{ wregex(L"a(lr)"), L"ò$1" },
|
||||
{ wregex(L"a(l{2}$)"), L"ò$1" },
|
||||
{ wregex(L"a(lm(?:[aeiouäëïöüâêîôûùò@])?$)"), L"ò$1" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a(l[td+])"), L"$1ò$2" },
|
||||
{ wregex(L"(^)a(l[td+])"), L"$1ò$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])al(k)"), L"$1ò$2" },
|
||||
// soft c and g
|
||||
{ wregex(L"c([eiêîy])"), L"s$1" },
|
||||
{ wregex(L"c"), L"k" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(a)"), L"$1j$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(o)"), L"$1j$2" },
|
||||
{ wregex(L"g([eiêîy])"), L"j$1" },
|
||||
// init/final guF was there just to harden the g
|
||||
{ wregex(L"(^)gu([eiêîy])"), L"$1g$2" },
|
||||
{ wregex(L"gu(e$)"), L"g$1" },
|
||||
// untangle reverse-written final liquids
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])re($)"), L"$1@r$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])le($)"), L"$1@l$2" },
|
||||
// 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"(^)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
|
||||
{ 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"(^)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
|
||||
{ wregex(L"î(nd$)"), L"ï$1" },
|
||||
{ wregex(L"ô(s{2}$)"), L"ò$1" },
|
||||
{ wregex(L"ô(g$)"), L"ò$1" },
|
||||
{ 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" },
|
||||
// soft gn
|
||||
{ wregex(L"îg([mnñ]$)"), L"ï$1" },
|
||||
{ wregex(L"îg([mnñ][bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ï$1" },
|
||||
{ wregex(L"(ei)g(n)"), L"$1$2" },
|
||||
// handle ous before removing -e
|
||||
{ wregex(L"ou(s$)"), L"@$1" },
|
||||
{ wregex(L"ou(s[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"@$1" },
|
||||
// remove silent -e
|
||||
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)e($)"), L"$1$2" },
|
||||
// common suffixes that hide a silent e
|
||||
{ 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})ë(li$)"), L"$1$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ë(fûl$)"), L"$1$2" },
|
||||
// another common suffix
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ï(nês{2}$)"), L"$1ë$2" },
|
||||
// shorten (1-char) weak penults after a long
|
||||
// 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" },
|
||||
// double vowels
|
||||
{ wregex(L"eau"), L"ö" },
|
||||
{ wregex(L"ai"), L"ä" },
|
||||
{ wregex(L"au"), L"ò" },
|
||||
{ wregex(L"âw"), L"ò" },
|
||||
{ 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"i(@)"), L"ë$1" },
|
||||
{ wregex(L"oa"), L"ö" },
|
||||
{ wregex(L"oe($)"), L"ö$1" },
|
||||
{ wregex(L"o{2}(k)"), 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
|
||||
{ wregex(L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[aeiouäëïöüâêîôûùò@])?)[aeiouäëïöüâêîôûùò@](l$)"), 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
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})[aâä](b@l$)"), L"$1@$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]l)ë(@n$)"), L"$1y$2" },
|
||||
{ wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]n)ë(@n$)"), L"$1y$2" },
|
||||
// unpronounceable finals
|
||||
{ wregex(L"(m)b($)"), L"$1$2" },
|
||||
{ wregex(L"(m)n($)"), L"$1$2" },
|
||||
// color the final vowels
|
||||
{ wregex(L"a($)"), L"@$1" },
|
||||
{ wregex(L"e($)"), L"ë$1" },
|
||||
{ wregex(L"i($)"), L"ë$1" },
|
||||
{ wregex(L"o($)"), L"ö$1" },
|
||||
// vowels before r V=aeiouäëïöüâêîôûùò@
|
||||
{ wregex(L"ôw(r[bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])"), 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"ê(r{2})"), L"ä$1" },
|
||||
{ wregex(L"ë(r[iîï][bcdfghjklmnpqrstvwxyzç+$ñ])"), 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
|
||||
{ wregex(L"ng([fs$+])"), L"ñ$1" },
|
||||
{ wregex(L"ng([bdg])"), L"ñ$1" },
|
||||
{ wregex(L"ng([ptk])"), 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
|
||||
{ wregex(L"([bdg])s($)"), L"$1z$2" },
|
||||
{ wregex(L"s(m$)"), L"z$1" },
|
||||
// double consonants
|
||||
{ wregex(L"s(s)"), L"$1" },
|
||||
{ wregex(L"s(\\$)"), L"$1" },
|
||||
{ wregex(L"t(t)"), L"$1" },
|
||||
{ wregex(L"t(ç)"), L"$1" },
|
||||
{ wregex(L"p(p)"), 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" },
|
||||
// There are a number of cases not covered by these rules.
|
||||
// Let's add some reasonable fallback rules.
|
||||
{ wregex(L"a"), L"â" },
|
||||
{ wregex(L"e"), L"@" },
|
||||
{ wregex(L"i"), L"ë" },
|
||||
{ wregex(L"o"), L"ö" },
|
||||
{ wregex(L"q"), L"k" },
|
||||
{wregex(L"ch"), L"ç"}, {wregex(L"sh"), L"$$"}, {wregex(L"ph"), L"f"}, {wregex(L"th"), L"+"},
|
||||
{wregex(L"qu"), L"kw"},
|
||||
// and other spelling-level changes
|
||||
{wregex(L"w(r)"), L"$1"}, {wregex(L"w(ho)"), 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"(^e)x([aeiouäëïöüâêîôûùò@])"), L"$1gz$2"}, {wregex(L"x"), L"ks"}, {wregex(L"'"), L""},
|
||||
// gh is particularly variable
|
||||
{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"ough(t)"), L"ò$1"},
|
||||
{wregex(L"augh(t)"), L"ò$1"}, {wregex(L"ough"), L"ö"}, {wregex(L"gh"), L""},
|
||||
// unpronounceable combinations
|
||||
{wregex(L"(^)g(n)"), L"$1$2"}, {wregex(L"(^)k(n)"), 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
|
||||
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1ï$2"},
|
||||
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{2})y($)"), L"$1ï$2"},
|
||||
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ]{3})y($)"), L"$1ï$2"}, {wregex(L"ey"), L"ë"},
|
||||
{wregex(L"ay"), L"ä"}, {wregex(L"oy"), L"öy"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y([bcdfghjklmnpqrstvwxyzç+$ñ])"), L"$1i$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y($)"), L"$1i$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])y(e$)"), L"$1i$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ]{2})ie($)"), L"$1ï$2"},
|
||||
{wregex(L"(^[bcdfghjklmnpqrstvwxyzç+$ñ])ie($)"), L"$1ï$2"},
|
||||
// sSl can simplify
|
||||
{wregex(L"(s)t(l[aeiouäëïöüâêîôûùò@]$)"), L"$1$2"},
|
||||
// affrication of t + front vowel
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ci([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([rl][aeiouäëïöüâêîôûùò@])"),
|
||||
L"$1çu$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])si(o)"), L"$1$$$2"},
|
||||
{wregex(L"([aeiouäëïöüâêîôûùò@])si(o)"), L"$1j$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])s(ur)"), L"$1$$$2"},
|
||||
{wregex(L"([aeiouäëïöüâêîôûùò@])s(ur)"), L"$1j$2"},
|
||||
{wregex(L"(k)s(u[aeiouäëïöüâêîôûùò@])"), L"$1$$$2"}, {wregex(L"(k)s(u[rl])"), L"$1$$$2"},
|
||||
// intervocalic s
|
||||
{wregex(L"([eiou])s([aeiouäëïöüâêîôûùò@])"), L"$1z$2"},
|
||||
// al to ol (do this before respelling)
|
||||
{wregex(L"a(ls)"), L"ò$1"}, {wregex(L"a(lr)"), L"ò$1"}, {wregex(L"a(l{2}$)"), L"ò$1"},
|
||||
{wregex(L"a(lm(?:[aeiouäëïöüâêîôûùò@])?$)"), L"ò$1"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])a(l[td+])"), L"$1ò$2"},
|
||||
{wregex(L"(^)a(l[td+])"), L"$1ò$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])al(k)"), L"$1ò$2"},
|
||||
// soft c and g
|
||||
{wregex(L"c([eiêîy])"), L"s$1"}, {wregex(L"c"), L"k"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(a)"), L"$1j$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])ge(o)"), L"$1j$2"},
|
||||
{wregex(L"g([eiêîy])"), L"j$1"},
|
||||
// init/final guF was there just to harden the g
|
||||
{wregex(L"(^)gu([eiêîy])"), L"$1g$2"}, {wregex(L"gu(e$)"), L"g$1"},
|
||||
// untangle reverse-written final liquids
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])re($)"), L"$1@r$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñ])le($)"), L"$1@l$2"},
|
||||
// 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"(^)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
|
||||
{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"(^)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
|
||||
{wregex(L"î(nd$)"), L"ï$1"}, {wregex(L"ô(s{2}$)"), L"ò$1"}, {wregex(L"ô(g$)"), L"ò$1"},
|
||||
{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"},
|
||||
// soft gn
|
||||
{wregex(L"îg([mnñ]$)"), L"ï$1"}, {wregex(L"îg([mnñ][bcdfghjklmnpqrstvwxyzç+$ñ])"), L"ï$1"},
|
||||
{wregex(L"(ei)g(n)"), L"$1$2"},
|
||||
// handle ous before removing -e
|
||||
{wregex(L"ou(s$)"), L"@$1"}, {wregex(L"ou(s[bcdfghjklmnpqrstvwxyzç+$ñ])"), L"@$1"},
|
||||
// remove silent -e
|
||||
{wregex(
|
||||
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[bcdfghjklmnpqrstvwxyzç+$ñ])?(?:[bcdfghjklmnpqrstvwxyzç+$ñ])?)e($)"
|
||||
),
|
||||
L"$1$2"},
|
||||
// common suffixes that hide a silent e
|
||||
{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})ë(li$)"), L"$1$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ë(fûl$)"), L"$1$2"},
|
||||
// another common suffix
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})ï(nês{2}$)"), L"$1ë$2"},
|
||||
// shorten (1-char) weak penults after a long
|
||||
// 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"},
|
||||
// double vowels
|
||||
{wregex(L"eau"), L"ö"}, {wregex(L"ai"), L"ä"}, {wregex(L"au"), L"ò"}, {wregex(L"âw"), L"ò"},
|
||||
{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"i(@)"), L"ë$1"}, {wregex(L"oa"), L"ö"}, {wregex(L"oe($)"), L"ö$1"},
|
||||
{wregex(L"o{2}(k)"), 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
|
||||
{wregex(
|
||||
L"([aeiouäëïöüâêîôûùò@][bcdfghjklmnpqrstvwxyzç+$ñ](?:[aeiouäëïöüâêîôûùò@])?)[aeiouäëïöüâêîôûùò@](l$)"
|
||||
),
|
||||
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
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]{3})[aâä](b@l$)"), L"$1@$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]l)ë(@n$)"), L"$1y$2"},
|
||||
{wregex(L"([bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@]n)ë(@n$)"), L"$1y$2"},
|
||||
// unpronounceable finals
|
||||
{wregex(L"(m)b($)"), L"$1$2"}, {wregex(L"(m)n($)"), L"$1$2"},
|
||||
// color the final vowels
|
||||
{wregex(L"a($)"), L"@$1"}, {wregex(L"e($)"), L"ë$1"}, {wregex(L"i($)"), L"ë$1"},
|
||||
{wregex(L"o($)"), L"ö$1"},
|
||||
// vowels before r V=aeiouäëïöüâêîôûùò@
|
||||
{wregex(L"ôw(r[bcdfghjklmnpqrstvwxyzç+$ñaeiouäëïöüâêîôûùò@])"), 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"ê(r{2})"), L"ä$1"}, {wregex(L"ë(r[iîï][bcdfghjklmnpqrstvwxyzç+$ñ])"), 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
|
||||
{wregex(L"ng([fs$+])"), L"ñ$1"}, {wregex(L"ng([bdg])"), L"ñ$1"}, {wregex(L"ng([ptk])"), 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
|
||||
{wregex(L"([bdg])s($)"), L"$1z$2"}, {wregex(L"s(m$)"), L"z$1"},
|
||||
// double consonants
|
||||
{wregex(L"s(s)"), L"$1"}, {wregex(L"s(\\$)"), L"$1"}, {wregex(L"t(t)"), L"$1"},
|
||||
{wregex(L"t(ç)"), L"$1"}, {wregex(L"p(p)"), 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"},
|
||||
// There are a number of cases not covered by these rules.
|
||||
// Let's add some reasonable fallback rules.
|
||||
{wregex(L"a"), L"â"}, {wregex(L"e"), L"@"}, {wregex(L"i"), L"ë"}, {wregex(L"o"), L"ö"},
|
||||
{wregex(L"q"), L"k"},
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
#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>
|
||||
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::regex;
|
||||
using std::map;
|
||||
using std::tuple;
|
||||
using std::get;
|
||||
#include <boost/range/adaptor/map.hpp>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "core/appInfo.h"
|
||||
#include "tools/platformTools.h"
|
||||
|
||||
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 Unigram = string;
|
||||
|
@ -50,9 +53,7 @@ map<Trigram, int> getTrigramCounts(const vector<string>& words) {
|
|||
}
|
||||
|
||||
map<Unigram, double> getUnigramProbabilities(
|
||||
const vector<string>& words,
|
||||
const map<Unigram, int>& unigramCounts,
|
||||
const double deflator
|
||||
const vector<string>& words, const map<Unigram, int>& unigramCounts, const double deflator
|
||||
) {
|
||||
map<Unigram, double> unigramProbabilities;
|
||||
for (const auto& pair : unigramCounts) {
|
||||
|
@ -97,8 +98,8 @@ map<Unigram, double> getUnigramBackoffWeights(
|
|||
const map<Unigram, int>& unigramCounts,
|
||||
const map<Unigram, double>& unigramProbabilities,
|
||||
const map<Bigram, int>& bigramCounts,
|
||||
const double discountMass)
|
||||
{
|
||||
const double discountMass
|
||||
) {
|
||||
map<Unigram, double> unigramBackoffWeights;
|
||||
for (const Unigram& unigram : unigramCounts | boost::adaptors::map_keys) {
|
||||
double denominator = 1;
|
||||
|
@ -116,8 +117,8 @@ map<Bigram, double> getBigramBackoffWeights(
|
|||
const map<Bigram, int>& bigramCounts,
|
||||
const map<Bigram, double>& bigramProbabilities,
|
||||
const map<Trigram, int>& trigramCounts,
|
||||
const double discountMass)
|
||||
{
|
||||
const double discountMass
|
||||
) {
|
||||
map<Bigram, double> bigramBackoffWeights;
|
||||
for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) {
|
||||
double denominator = 1;
|
||||
|
@ -163,24 +164,22 @@ void createLanguageModelFile(const vector<string>& words, const path& filePath)
|
|||
file.precision(4);
|
||||
file << "\\1-grams:" << endl;
|
||||
for (const Unigram& unigram : unigramCounts | boost::adaptors::map_keys) {
|
||||
file << log10(unigramProbabilities.at(unigram))
|
||||
<< " " << unigram
|
||||
<< " " << log10(unigramBackoffWeights.at(unigram)) << endl;
|
||||
file << log10(unigramProbabilities.at(unigram)) << " " << unigram << " "
|
||||
<< log10(unigramBackoffWeights.at(unigram)) << endl;
|
||||
}
|
||||
file << endl;
|
||||
|
||||
file << "\\2-grams:" << endl;
|
||||
for (const Bigram& bigram : bigramCounts | boost::adaptors::map_keys) {
|
||||
file << log10(bigramProbabilities.at(bigram))
|
||||
<< " " << get<0>(bigram) << " " << get<1>(bigram)
|
||||
<< " " << log10(bigramBackoffWeights.at(bigram)) << endl;
|
||||
file << log10(bigramProbabilities.at(bigram)) << " " << get<0>(bigram) << " "
|
||||
<< get<1>(bigram) << " " << log10(bigramBackoffWeights.at(bigram)) << endl;
|
||||
}
|
||||
file << endl;
|
||||
|
||||
file << "\\3-grams:" << endl;
|
||||
for (const Trigram& trigram : trigramCounts | boost::adaptors::map_keys) {
|
||||
file << log10(trigramProbabilities.at(trigram))
|
||||
<< " " << get<0>(trigram) << " " << get<1>(trigram) << " " << get<2>(trigram) << endl;
|
||||
file << log10(trigramProbabilities.at(trigram)) << " " << get<0>(trigram) << " "
|
||||
<< get<1>(trigram) << " " << get<2>(trigram) << endl;
|
||||
}
|
||||
file << endl;
|
||||
|
||||
|
@ -188,14 +187,16 @@ void createLanguageModelFile(const vector<string>& words, const path& filePath)
|
|||
}
|
||||
|
||||
lambda_unique_ptr<ngram_model_t> createLanguageModel(
|
||||
const vector<string>& words,
|
||||
ps_decoder_t& decoder
|
||||
const vector<string>& words, ps_decoder_t& decoder
|
||||
) {
|
||||
path tempFilePath = getTempFilePath();
|
||||
createLanguageModelFile(words, tempFilePath);
|
||||
auto deleteTempFile = gsl::finally([&]() { std::filesystem::remove(tempFilePath); });
|
||||
|
||||
return lambda_unique_ptr<ngram_model_t>(
|
||||
ngram_model_read(decoder.config, tempFilePath.u8string().c_str(), NGRAM_ARPA, decoder.lmath),
|
||||
[](ngram_model_t* lm) { ngram_model_free(lm); });
|
||||
ngram_model_read(
|
||||
decoder.config, tempFilePath.u8string().c_str(), NGRAM_ARPA, decoder.lmath
|
||||
),
|
||||
[](ngram_model_t* lm) { ngram_model_free(lm); }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "tools/tools.h"
|
||||
|
||||
extern "C" {
|
||||
#include <pocketsphinx.h>
|
||||
#include <ngram_search.h>
|
||||
#include <pocketsphinx.h>
|
||||
}
|
||||
|
||||
lambda_unique_ptr<ngram_model_t> createLanguageModel(
|
||||
const std::vector<std::string>& words,
|
||||
ps_decoder_t& decoder
|
||||
const std::vector<std::string>& words, ps_decoder_t& decoder
|
||||
);
|
||||
|
|
|
@ -1,43 +1,39 @@
|
|||
#include "pocketSphinxTools.h"
|
||||
|
||||
#include "tools/platformTools.h"
|
||||
#include <regex>
|
||||
|
||||
#include "audio/DcOffset.h"
|
||||
#include "audio/voiceActivityDetection.h"
|
||||
#include "tools/parallel.h"
|
||||
#include "tools/ObjectPool.h"
|
||||
#include "time/timedLogging.h"
|
||||
#include "tools/ObjectPool.h"
|
||||
#include "tools/parallel.h"
|
||||
#include "tools/platformTools.h"
|
||||
|
||||
extern "C" {
|
||||
#include <sphinxbase/err.h>
|
||||
#include <pocketsphinx_internal.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 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::filesystem::path;
|
||||
|
||||
logging::Level convertSphinxErrorLevel(err_lvl_t errorLevel) {
|
||||
switch (errorLevel) {
|
||||
case ERR_DEBUG:
|
||||
case ERR_INFO:
|
||||
case ERR_INFOCONT:
|
||||
return logging::Level::Trace;
|
||||
case ERR_WARN:
|
||||
return logging::Level::Warn;
|
||||
case ERR_ERROR:
|
||||
return logging::Level::Error;
|
||||
case ERR_FATAL:
|
||||
return logging::Level::Fatal;
|
||||
default:
|
||||
throw invalid_argument("Unknown log level.");
|
||||
case ERR_INFOCONT: return logging::Level::Trace;
|
||||
case ERR_WARN: return logging::Level::Warn;
|
||||
case ERR_ERROR: 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();
|
||||
|
||||
// Prepare pool of decoders
|
||||
ObjectPool<ps_decoder_t, lambda_unique_ptr<ps_decoder_t>> decoderPool(
|
||||
[&] { return createDecoder(dialog); });
|
||||
ObjectPool<ps_decoder_t, lambda_unique_ptr<ps_decoder_t>> decoderPool([&] {
|
||||
return createDecoder(dialog);
|
||||
});
|
||||
|
||||
BoundedTimeline<Phone> phones(audioClip->getTruncatedRange());
|
||||
std::mutex resultMutex;
|
||||
const auto processUtterance = [&](Timed<void> timedUtterance, ProgressSink& utteranceProgressSink) {
|
||||
const auto processUtterance = [&](Timed<void> timedUtterance,
|
||||
ProgressSink& utteranceProgressSink) {
|
||||
// Detect phones for utterance
|
||||
const auto decoder = decoderPool.acquire();
|
||||
Timeline<Phone> utterancePhones = utteranceToPhones(
|
||||
*audioClip,
|
||||
timedUtterance.getTimeRange(),
|
||||
*decoder,
|
||||
utteranceProgressSink
|
||||
*audioClip, timedUtterance.getTimeRange(), *decoder, utteranceProgressSink
|
||||
);
|
||||
|
||||
// Copy phones to result timeline
|
||||
|
@ -139,15 +134,18 @@ BoundedTimeline<Phone> recognizePhones(
|
|||
// Perform speech recognition
|
||||
try {
|
||||
// Determine how many parallel threads to use
|
||||
int threadCount = std::min({
|
||||
maxThreadCount,
|
||||
// Don't use more threads than there are utterances to be processed
|
||||
static_cast<int>(utterances.size()),
|
||||
// Don't waste time creating additional threads (and decoders!) if the recording is short
|
||||
static_cast<int>(
|
||||
duration_cast<std::chrono::seconds>(audioClip->getTruncatedRange().getDuration()).count() / 5
|
||||
)
|
||||
});
|
||||
int threadCount = std::min(
|
||||
{maxThreadCount,
|
||||
// Don't use more threads than there are utterances to be processed
|
||||
static_cast<int>(utterances.size()),
|
||||
// Don't waste time creating additional threads (and decoders!) if the recording is
|
||||
// short
|
||||
static_cast<int>(
|
||||
duration_cast<std::chrono::seconds>(audioClip->getTruncatedRange().getDuration())
|
||||
.count()
|
||||
/ 5
|
||||
)}
|
||||
);
|
||||
if (threadCount < 1) {
|
||||
threadCount = 1;
|
||||
}
|
||||
|
@ -162,7 +160,9 @@ BoundedTimeline<Phone> recognizePhones(
|
|||
);
|
||||
logging::debug("Speech recognition -- end");
|
||||
} 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;
|
||||
|
@ -206,8 +206,9 @@ BoundedTimeline<string> recognizeWords(const vector<int16_t>& audioBuffer, ps_de
|
|||
// Process entire audio clip
|
||||
const bool noRecognition = false;
|
||||
const bool fullUtterance = true;
|
||||
const int searchedFrameCount =
|
||||
ps_process_raw(&decoder, audioBuffer.data(), audioBuffer.size(), noRecognition, fullUtterance);
|
||||
const int searchedFrameCount = ps_process_raw(
|
||||
&decoder, audioBuffer.data(), audioBuffer.size(), noRecognition, fullUtterance
|
||||
);
|
||||
if (searchedFrameCount < 0) {
|
||||
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
|
||||
// the log output.
|
||||
// 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) {
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include "time/BoundedTimeline.h"
|
||||
#include "core/Phone.h"
|
||||
#include "audio/AudioClip.h"
|
||||
#include "tools/progress.h"
|
||||
#include <filesystem>
|
||||
|
||||
#include "audio/AudioClip.h"
|
||||
#include "core/Phone.h"
|
||||
#include "time/BoundedTimeline.h"
|
||||
#include "tools/progress.h"
|
||||
|
||||
extern "C" {
|
||||
#include <pocketsphinx.h>
|
||||
}
|
||||
|
||||
typedef std::function<lambda_unique_ptr<ps_decoder_t>(
|
||||
boost::optional<std::string> dialog
|
||||
)> decoderFactory;
|
||||
typedef std::function<lambda_unique_ptr<ps_decoder_t>(boost::optional<std::string> dialog)>
|
||||
decoderFactory;
|
||||
|
||||
typedef std::function<Timeline<Phone>(
|
||||
const AudioClip& audioClip,
|
||||
TimeRange utteranceTimeRange,
|
||||
ps_decoder_t& decoder,
|
||||
ProgressSink& utteranceProgressSink
|
||||
)> utteranceToPhonesFunction;
|
||||
)>
|
||||
utteranceToPhonesFunction;
|
||||
|
||||
BoundedTimeline<Phone> recognizePhones(
|
||||
const AudioClip& inputAudioClip,
|
||||
|
@ -37,6 +38,5 @@ const std::filesystem::path& getSphinxModelDirectory();
|
|||
JoiningTimeline<void> getNoiseSounds(TimeRange utteranceTimeRange, const Timeline<Phone>& phones);
|
||||
|
||||
BoundedTimeline<std::string> recognizeWords(
|
||||
const std::vector<int16_t>& audioBuffer,
|
||||
ps_decoder_t& decoder
|
||||
const std::vector<int16_t>& audioBuffer, ps_decoder_t& decoder
|
||||
);
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
#include "tokenization.h"
|
||||
#include "tools/tools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include <regex>
|
||||
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <regex>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
extern "C" {
|
||||
#include <cst_utt_utils.h>
|
||||
#include <lang/usenglish/usenglish.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::string;
|
||||
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> voice(new_voice(), [](cst_voice* voice) { delete_voice(voice); });
|
||||
|
@ -28,9 +30,9 @@ lambda_unique_ptr<cst_voice> createDummyVoice() {
|
|||
}
|
||||
|
||||
static const cst_synth_module synth_method_normalize[] = {
|
||||
{ "tokenizer_func", default_tokenization }, // split text into tokens
|
||||
{ "textanalysis_func", default_textanalysis }, // transform tokens into words
|
||||
{ nullptr, nullptr }
|
||||
{"tokenizer_func", default_tokenization}, // split text into tokens
|
||||
{"textanalysis_func", default_textanalysis}, // transform tokens into words
|
||||
{nullptr, nullptr}
|
||||
};
|
||||
|
||||
vector<string> tokenizeViaFlite(const string& text) {
|
||||
|
@ -38,10 +40,9 @@ vector<string> tokenizeViaFlite(const string& text) {
|
|||
const string asciiText = utf8ToAscii(text);
|
||||
|
||||
// Create utterance object with text
|
||||
lambda_unique_ptr<cst_utterance> utterance(
|
||||
new_utterance(),
|
||||
[](cst_utterance* utterance) { delete_utterance(utterance); }
|
||||
);
|
||||
lambda_unique_ptr<cst_utterance> utterance(new_utterance(), [](cst_utterance* utterance) {
|
||||
delete_utterance(utterance);
|
||||
});
|
||||
utt_set_input_text(utterance.get(), asciiText.c_str());
|
||||
lambda_unique_ptr<cst_voice> voice = createDummyVoice();
|
||||
utt_init(utterance.get(), voice.get());
|
||||
|
@ -52,11 +53,8 @@ vector<string> tokenizeViaFlite(const string& text) {
|
|||
}
|
||||
|
||||
vector<string> result;
|
||||
for (
|
||||
cst_item* item = relation_head(utt_relation(utterance.get(), "Word"));
|
||||
item;
|
||||
item = item_next(item)
|
||||
) {
|
||||
for (cst_item* item = relation_head(utt_relation(utterance.get(), "Word")); item;
|
||||
item = item_next(item)) {
|
||||
const char* word = item_feat_string(item, "name");
|
||||
result.emplace_back(word);
|
||||
}
|
||||
|
@ -64,11 +62,11 @@ vector<string> tokenizeViaFlite(const string& text) {
|
|||
}
|
||||
|
||||
optional<string> findSimilarDictionaryWord(
|
||||
const string& word,
|
||||
const function<bool(const string&)>& dictionaryContains
|
||||
const string& word, const function<bool(const string&)>& dictionaryContains
|
||||
) {
|
||||
for (bool addPeriod : { false, true }) {
|
||||
for (int apostropheIndex = -1; apostropheIndex <= static_cast<int>(word.size()); ++apostropheIndex) {
|
||||
for (bool addPeriod : {false, true}) {
|
||||
for (int apostropheIndex = -1; apostropheIndex <= static_cast<int>(word.size());
|
||||
++apostropheIndex) {
|
||||
string modified = word;
|
||||
if (apostropheIndex != -1) {
|
||||
modified.insert(apostropheIndex, "'");
|
||||
|
@ -87,8 +85,7 @@ optional<string> findSimilarDictionaryWord(
|
|||
}
|
||||
|
||||
vector<string> tokenizeText(
|
||||
const string& text,
|
||||
const function<bool(const string&)>& dictionaryContains
|
||||
const string& text, const function<bool(const string&)>& dictionaryContains
|
||||
) {
|
||||
vector<string> words = tokenizeViaFlite(text);
|
||||
|
||||
|
@ -101,13 +98,13 @@ vector<string> tokenizeText(
|
|||
}
|
||||
|
||||
// Turn some symbols into words, remove the rest
|
||||
const static vector<pair<regex, string>> replacements {
|
||||
{ regex("&"), "and" },
|
||||
{ regex("\\*"), "times" },
|
||||
{ regex("\\+"), "plus" },
|
||||
{ regex("="), "equals" },
|
||||
{ regex("@"), "at" },
|
||||
{ regex("[^a-z']"), "" }
|
||||
const static vector<pair<regex, string>> replacements{
|
||||
{regex("&"), "and"},
|
||||
{regex("\\*"), "times"},
|
||||
{regex("\\+"), "plus"},
|
||||
{regex("="), "equals"},
|
||||
{regex("@"), "at"},
|
||||
{regex("[^a-z']"), ""}
|
||||
};
|
||||
for (auto& word : words) {
|
||||
for (const auto& replacement : replacements) {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
std::vector<std::string> tokenizeText(
|
||||
const std::string& text,
|
||||
const std::function<bool(const std::string&)>& dictionaryContains
|
||||
const std::string& text, const std::function<bool(const std::string&)>& dictionaryContains
|
||||
);
|
||||
|
|
|
@ -12,11 +12,11 @@ string ExportFormatConverter::getTypeName() {
|
|||
}
|
||||
|
||||
EnumConverter<ExportFormat>::member_data ExportFormatConverter::getMemberData() {
|
||||
return member_data {
|
||||
{ ExportFormat::Dat, "dat" },
|
||||
{ ExportFormat::Tsv, "tsv" },
|
||||
{ ExportFormat::Xml, "xml" },
|
||||
{ ExportFormat::Json, "json" }
|
||||
return member_data{
|
||||
{ExportFormat::Dat, "dat"},
|
||||
{ExportFormat::Tsv, "tsv"},
|
||||
{ExportFormat::Xml, "xml"},
|
||||
{ExportFormat::Json, "json"}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,16 +2,12 @@
|
|||
|
||||
#include "tools/EnumConverter.h"
|
||||
|
||||
enum class ExportFormat {
|
||||
Dat,
|
||||
Tsv,
|
||||
Xml,
|
||||
Json
|
||||
};
|
||||
enum class ExportFormat { Dat, Tsv, Xml, Json };
|
||||
|
||||
class ExportFormatConverter : public EnumConverter<ExportFormat> {
|
||||
public:
|
||||
static ExportFormatConverter& get();
|
||||
|
||||
protected:
|
||||
std::string getTypeName() override;
|
||||
member_data getMemberData() override;
|
||||
|
|
|
@ -12,9 +12,8 @@ string RecognizerTypeConverter::getTypeName() {
|
|||
}
|
||||
|
||||
EnumConverter<RecognizerType>::member_data RecognizerTypeConverter::getMemberData() {
|
||||
return member_data {
|
||||
{ RecognizerType::PocketSphinx, "pocketSphinx" },
|
||||
{ RecognizerType::Phonetic, "phonetic" }
|
||||
return member_data{
|
||||
{RecognizerType::PocketSphinx, "pocketSphinx"}, {RecognizerType::Phonetic, "phonetic"}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,12 @@
|
|||
|
||||
#include "tools/EnumConverter.h"
|
||||
|
||||
enum class RecognizerType {
|
||||
PocketSphinx,
|
||||
Phonetic
|
||||
};
|
||||
enum class RecognizerType { PocketSphinx, Phonetic };
|
||||
|
||||
class RecognizerTypeConverter : public EnumConverter<RecognizerType> {
|
||||
public:
|
||||
static RecognizerTypeConverter& get();
|
||||
|
||||
protected:
|
||||
std::string getTypeName() override;
|
||||
member_data getMemberData() override;
|
||||
|
|
|
@ -1,67 +1,68 @@
|
|||
#include <iostream>
|
||||
#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 "exporters/Exporter.h"
|
||||
#include "time/ContinuousTimeline.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include <tclap/CmdLine.h>
|
||||
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/utility/in_place_factory.hpp>
|
||||
#include <fstream>
|
||||
#include "tools/parallel.h"
|
||||
#include "tools/exceptions.h"
|
||||
#include "tools/textFiles.h"
|
||||
#include "lib/rhubarbLib.h"
|
||||
#include "ExportFormat.h"
|
||||
#include <iostream>
|
||||
|
||||
#include "animation/targetShapeSet.h"
|
||||
#include "core/appInfo.h"
|
||||
#include "exporters/DatExporter.h"
|
||||
#include "exporters/Exporter.h"
|
||||
#include "exporters/JsonExporter.h"
|
||||
#include "exporters/TsvExporter.h"
|
||||
#include "exporters/XmlExporter.h"
|
||||
#include "exporters/JsonExporter.h"
|
||||
#include "animation/targetShapeSet.h"
|
||||
#include <boost/utility/in_place_factory.hpp>
|
||||
#include "tools/platformTools.h"
|
||||
#include "sinks.h"
|
||||
#include "semanticEntries.h"
|
||||
#include "RecognizerType.h"
|
||||
#include "recognition/PocketSphinxRecognizer.h"
|
||||
#include "ExportFormat.h"
|
||||
#include "lib/rhubarbLib.h"
|
||||
#include "logging/formatters.h"
|
||||
#include "logging/logging.h"
|
||||
#include "logging/sinks.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::string;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::unique_ptr;
|
||||
using std::make_shared;
|
||||
using std::make_unique;
|
||||
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::u8path;
|
||||
using boost::adaptors::transformed;
|
||||
using boost::optional;
|
||||
|
||||
namespace tclap = TCLAP;
|
||||
|
||||
// Tell TCLAP how to handle our types
|
||||
namespace TCLAP {
|
||||
template<>
|
||||
struct ArgTraits<logging::Level> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
template <>
|
||||
struct ArgTraits<logging::Level> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ArgTraits<ExportFormat> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
template <>
|
||||
struct ArgTraits<ExportFormat> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ArgTraits<RecognizerType> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
}
|
||||
template <>
|
||||
struct ArgTraits<RecognizerType> {
|
||||
typedef ValueLike ValueCategory;
|
||||
};
|
||||
} // namespace TCLAP
|
||||
|
||||
shared_ptr<logging::Sink> createFileSink(const path& path, logging::Level minLevel) {
|
||||
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) {
|
||||
switch (recognizerType) {
|
||||
case RecognizerType::PocketSphinx:
|
||||
return make_unique<PocketSphinxRecognizer>();
|
||||
case RecognizerType::Phonetic:
|
||||
return make_unique<PhoneticRecognizer>();
|
||||
default:
|
||||
throw std::runtime_error("Unknown recognizer.");
|
||||
case RecognizerType::PocketSphinx: return make_unique<PocketSphinxRecognizer>();
|
||||
case RecognizerType::Phonetic: return make_unique<PhoneticRecognizer>();
|
||||
default: throw std::runtime_error("Unknown recognizer.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,14 +90,10 @@ unique_ptr<Exporter> createExporter(
|
|||
switch (exportFormat) {
|
||||
case ExportFormat::Dat:
|
||||
return make_unique<DatExporter>(targetShapeSet, datFrameRate, datUsePrestonBlair);
|
||||
case ExportFormat::Tsv:
|
||||
return make_unique<TsvExporter>();
|
||||
case ExportFormat::Xml:
|
||||
return make_unique<XmlExporter>();
|
||||
case ExportFormat::Json:
|
||||
return make_unique<JsonExporter>();
|
||||
default:
|
||||
throw std::runtime_error("Unknown export format.");
|
||||
case ExportFormat::Tsv: return make_unique<TsvExporter>();
|
||||
case ExportFormat::Xml: return make_unique<XmlExporter>();
|
||||
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());
|
||||
|
||||
tclap::ValueArg<string> outputFileName(
|
||||
"o", "output", "The output file path.",
|
||||
false, string(), "string", cmd
|
||||
"o", "output", "The output file path.", false, string(), "string", cmd
|
||||
);
|
||||
|
||||
auto logLevels = vector<logging::Level>(logging::LevelConverter::get().getValues());
|
||||
tclap::ValuesConstraint<logging::Level> logLevelConstraint(logLevels);
|
||||
tclap::ValueArg<logging::Level> logLevel(
|
||||
"", "logLevel", "The minimum log level that will be written to the log file",
|
||||
false, logging::Level::Debug, &logLevelConstraint, cmd
|
||||
"",
|
||||
"logLevel",
|
||||
"The minimum log level that will be written to the log file",
|
||||
false,
|
||||
logging::Level::Debug,
|
||||
&logLevelConstraint,
|
||||
cmd
|
||||
);
|
||||
|
||||
tclap::ValueArg<string> logFileName(
|
||||
"", "logFile", "The log file path.",
|
||||
false, string(), "string", cmd
|
||||
"", "logFile", "The log file path.", false, string(), "string", cmd
|
||||
);
|
||||
tclap::ValueArg<logging::Level> consoleLevel(
|
||||
"", "consoleLevel", "The minimum log level that will be printed on the console (stderr)",
|
||||
false, defaultMinStderrLevel, &logLevelConstraint, cmd
|
||||
"",
|
||||
"consoleLevel",
|
||||
"The minimum log level that will be printed on the console (stderr)",
|
||||
false,
|
||||
defaultMinStderrLevel,
|
||||
&logLevelConstraint,
|
||||
cmd
|
||||
);
|
||||
|
||||
tclap::SwitchArg machineReadableMode(
|
||||
"", "machineReadable", "Formats all output to stderr in a structured JSON format.",
|
||||
cmd, false
|
||||
"",
|
||||
"machineReadable",
|
||||
"Formats all output to stderr in a structured JSON format.",
|
||||
cmd,
|
||||
false
|
||||
);
|
||||
|
||||
tclap::SwitchArg quietMode(
|
||||
"q", "quiet", "Suppresses all output to stderr except for warnings and error messages.",
|
||||
cmd, false
|
||||
"q",
|
||||
"quiet",
|
||||
"Suppresses all output to stderr except for warnings and error messages.",
|
||||
cmd,
|
||||
false
|
||||
);
|
||||
|
||||
tclap::ValueArg<int> maxThreadCount(
|
||||
"", "threads", "The maximum number of worker threads to use.",
|
||||
false, getProcessorCoreCount(), "number", cmd
|
||||
"",
|
||||
"threads",
|
||||
"The maximum number of worker threads to use.",
|
||||
false,
|
||||
getProcessorCoreCount(),
|
||||
"number",
|
||||
cmd
|
||||
);
|
||||
|
||||
tclap::ValueArg<string> extendedShapes(
|
||||
"", "extendedShapes", "All extended, optional shapes to use.",
|
||||
false, "GHX", "string", cmd
|
||||
"", "extendedShapes", "All extended, optional shapes to use.", false, "GHX", "string", cmd
|
||||
);
|
||||
|
||||
tclap::ValueArg<string> dialogFile(
|
||||
"d", "dialogFile", "A file containing the text of the dialog.",
|
||||
false, string(), "string", cmd
|
||||
"d",
|
||||
"dialogFile",
|
||||
"A file containing the text of the dialog.",
|
||||
false,
|
||||
string(),
|
||||
"string",
|
||||
cmd
|
||||
);
|
||||
|
||||
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(
|
||||
"", "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());
|
||||
tclap::ValuesConstraint<ExportFormat> exportFormatConstraint(exportFormats);
|
||||
tclap::ValueArg<ExportFormat> exportFormat(
|
||||
"f", "exportFormat", "The export format.",
|
||||
false, ExportFormat::Tsv, &exportFormatConstraint, cmd
|
||||
"f",
|
||||
"exportFormat",
|
||||
"The export format.",
|
||||
false,
|
||||
ExportFormat::Tsv,
|
||||
&exportFormatConstraint,
|
||||
cmd
|
||||
);
|
||||
|
||||
auto recognizerTypes = vector<RecognizerType>(RecognizerTypeConverter::get().getValues());
|
||||
tclap::ValuesConstraint<RecognizerType> recognizerConstraint(recognizerTypes);
|
||||
tclap::ValueArg<RecognizerType> recognizerType(
|
||||
"r", "recognizer", "The dialog recognizer.",
|
||||
false, RecognizerType::PocketSphinx, &recognizerConstraint, cmd
|
||||
"r",
|
||||
"recognizer",
|
||||
"The dialog recognizer.",
|
||||
false,
|
||||
RecognizerType::PocketSphinx,
|
||||
&recognizerConstraint,
|
||||
cmd
|
||||
);
|
||||
|
||||
tclap::UnlabeledValueArg<string> inputFileName(
|
||||
"inputFile", "The input file. Must be a sound file in WAVE format.",
|
||||
true, "", "string", cmd
|
||||
"inputFile", "The input file. Must be a sound file in WAVE format.", true, "", "string", cmd
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -247,8 +281,10 @@ int main(int platformArgc, char* platformArgv[]) {
|
|||
);
|
||||
|
||||
logging::log(StartEntry(inputFilePath));
|
||||
logging::debugFormat("Command line: {}",
|
||||
join(args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " "));
|
||||
logging::debugFormat(
|
||||
"Command line: {}",
|
||||
join(args | transformed([](string arg) { return fmt::format("\"{}\"", arg); }), " ")
|
||||
);
|
||||
|
||||
try {
|
||||
// On progress change: Create log message
|
||||
|
@ -260,13 +296,13 @@ int main(int platformArgc, char* platformArgv[]) {
|
|||
logging::info("Starting animation.");
|
||||
JoiningContinuousTimeline<Shape> animation = animateWaveFile(
|
||||
inputFilePath,
|
||||
dialogFile.isSet()
|
||||
? readUtf8File(u8path(dialogFile.getValue()))
|
||||
: boost::optional<string>(),
|
||||
dialogFile.isSet() ? readUtf8File(u8path(dialogFile.getValue()))
|
||||
: boost::optional<string>(),
|
||||
*createRecognizer(recognizerType.getValue()),
|
||||
targetShapeSet,
|
||||
maxThreadCount.getValue(),
|
||||
progressSink);
|
||||
progressSink
|
||||
);
|
||||
logging::info("Done animating.");
|
||||
|
||||
// Export animation
|
||||
|
@ -282,9 +318,9 @@ int main(int platformArgc, char* platformArgv[]) {
|
|||
|
||||
logging::log(SuccessEntry());
|
||||
} catch (...) {
|
||||
std::throw_with_nested(
|
||||
std::runtime_error(fmt::format("Error processing file {}.", inputFilePath.u8string()))
|
||||
);
|
||||
std::throw_with_nested(std::runtime_error(
|
||||
fmt::format("Error processing file {}.", inputFilePath.u8string())
|
||||
));
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
@ -4,13 +4,13 @@ using logging::Level;
|
|||
using std::string;
|
||||
|
||||
SemanticEntry::SemanticEntry(Level level, const string& message) :
|
||||
Entry(level, message)
|
||||
{}
|
||||
Entry(level, message) {}
|
||||
|
||||
StartEntry::StartEntry(const std::filesystem::path& inputFilePath) :
|
||||
SemanticEntry(Level::Info, fmt::format("Application startup. Input file: {}.", inputFilePath.u8string())),
|
||||
inputFilePath(inputFilePath)
|
||||
{}
|
||||
SemanticEntry(
|
||||
Level::Info, fmt::format("Application startup. Input file: {}.", inputFilePath.u8string())
|
||||
),
|
||||
inputFilePath(inputFilePath) {}
|
||||
|
||||
std::filesystem::path StartEntry::getInputFilePath() const {
|
||||
return inputFilePath;
|
||||
|
@ -18,21 +18,18 @@ std::filesystem::path StartEntry::getInputFilePath() const {
|
|||
|
||||
ProgressEntry::ProgressEntry(double progress) :
|
||||
SemanticEntry(Level::Trace, fmt::format("Progress: {}%", static_cast<int>(progress * 100))),
|
||||
progress(progress)
|
||||
{}
|
||||
progress(progress) {}
|
||||
|
||||
double ProgressEntry::getProgress() const {
|
||||
return progress;
|
||||
}
|
||||
|
||||
SuccessEntry::SuccessEntry() :
|
||||
SemanticEntry(Level::Info, "Application terminating normally.")
|
||||
{}
|
||||
SemanticEntry(Level::Info, "Application terminating normally.") {}
|
||||
|
||||
FailureEntry::FailureEntry(const string& reason) :
|
||||
SemanticEntry(Level::Fatal, fmt::format("Application terminating with error: {}", reason)),
|
||||
reason(reason)
|
||||
{}
|
||||
reason(reason) {}
|
||||
|
||||
string FailureEntry::getReason() const {
|
||||
return reason;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#pragma once
|
||||
#include "logging/Entry.h"
|
||||
#include <filesystem>
|
||||
|
||||
#include "logging/Entry.h"
|
||||
|
||||
// Marker class for semantic entries
|
||||
class SemanticEntry : public logging::Entry {
|
||||
public:
|
||||
|
@ -12,6 +13,7 @@ class StartEntry : public SemanticEntry {
|
|||
public:
|
||||
StartEntry(const std::filesystem::path& inputFilePath);
|
||||
std::filesystem::path getInputFilePath() const;
|
||||
|
||||
private:
|
||||
std::filesystem::path inputFilePath;
|
||||
};
|
||||
|
@ -20,6 +22,7 @@ class ProgressEntry : public SemanticEntry {
|
|||
public:
|
||||
ProgressEntry(double progress);
|
||||
double getProgress() const;
|
||||
|
||||
private:
|
||||
double progress;
|
||||
};
|
||||
|
@ -33,6 +36,7 @@ class FailureEntry : public SemanticEntry {
|
|||
public:
|
||||
FailureEntry(const std::string& reason);
|
||||
std::string getReason() const;
|
||||
|
||||
private:
|
||||
std::string reason;
|
||||
};
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
#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>
|
||||
|
||||
using std::string;
|
||||
using std::make_shared;
|
||||
using logging::Level;
|
||||
using logging::StdErrSink;
|
||||
using logging::SimpleConsoleFormatter;
|
||||
#include "core/appInfo.h"
|
||||
#include "logging/formatters.h"
|
||||
#include "logging/sinks.h"
|
||||
#include "semanticEntries.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
using boost::optional;
|
||||
using logging::Level;
|
||||
using logging::SimpleConsoleFormatter;
|
||||
using logging::StdErrSink;
|
||||
using std::make_shared;
|
||||
using std::string;
|
||||
|
||||
NiceStderrSink::NiceStderrSink(Level minLevel) :
|
||||
minLevel(minLevel),
|
||||
progress(0.0),
|
||||
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>()))
|
||||
{}
|
||||
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>())) {}
|
||||
|
||||
void NiceStderrSink::receive(const logging::Entry& entry) {
|
||||
// For selected semantic entries, print a user-friendly message instead of
|
||||
// the technical log message.
|
||||
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
||||
std::cerr
|
||||
<< fmt::format("Generating lip sync data for {}.", startEntry->getInputFilePath().u8string())
|
||||
<< std::endl;
|
||||
std::cerr << fmt::format(
|
||||
"Generating lip sync data for {}.", startEntry->getInputFilePath().u8string()
|
||||
) << std::endl;
|
||||
startProgressIndication();
|
||||
} else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
|
||||
assert(progressBar);
|
||||
|
@ -62,8 +63,7 @@ void NiceStderrSink::resumeProgressIndication() {
|
|||
|
||||
QuietStderrSink::QuietStderrSink(Level minLevel) :
|
||||
minLevel(minLevel),
|
||||
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>()))
|
||||
{}
|
||||
innerSink(make_shared<StdErrSink>(make_shared<SimpleConsoleFormatter>())) {}
|
||||
|
||||
void QuietStderrSink::receive(const logging::Entry& entry) {
|
||||
// Set inputFilePath as soon as we get it
|
||||
|
@ -75,7 +75,9 @@ void QuietStderrSink::receive(const logging::Entry& entry) {
|
|||
if (quietSoFar) {
|
||||
// This is the first message we print. Give a bit of context.
|
||||
const string intro = inputFilePath
|
||||
? fmt::format("{} {} processing file {}:", appName, appVersion, inputFilePath->u8string())
|
||||
? fmt::format(
|
||||
"{} {} processing file {}:", appName, appVersion, inputFilePath->u8string()
|
||||
)
|
||||
: fmt::format("{} {}:", appName, appVersion);
|
||||
std::cerr << intro << std::endl;
|
||||
quietSoFar = false;
|
||||
|
@ -85,8 +87,7 @@ void QuietStderrSink::receive(const logging::Entry& entry) {
|
|||
}
|
||||
|
||||
MachineReadableStderrSink::MachineReadableStderrSink(Level minLevel) :
|
||||
minLevel(minLevel)
|
||||
{}
|
||||
minLevel(minLevel) {}
|
||||
|
||||
string formatLogProperty(const logging::Entry& entry) {
|
||||
return fmt::format(
|
||||
|
@ -102,9 +103,7 @@ void MachineReadableStderrSink::receive(const logging::Entry& entry) {
|
|||
if (const auto* startEntry = dynamic_cast<const StartEntry*>(&entry)) {
|
||||
const string file = escapeJsonString(startEntry->getInputFilePath().u8string());
|
||||
line = fmt::format(
|
||||
R"({{ "type": "start", "file": "{}", {} }})",
|
||||
file,
|
||||
formatLogProperty(entry)
|
||||
R"({{ "type": "start", "file": "{}", {} }})", file, formatLogProperty(entry)
|
||||
);
|
||||
} else if (const auto* progressEntry = dynamic_cast<const ProgressEntry*>(&entry)) {
|
||||
const int progressPercent = static_cast<int>(progressEntry->getProgress() * 100);
|
||||
|
@ -121,9 +120,7 @@ void MachineReadableStderrSink::receive(const logging::Entry& entry) {
|
|||
} else if (const auto* failureEntry = dynamic_cast<const FailureEntry*>(&entry)) {
|
||||
const string reason = escapeJsonString(failureEntry->getReason());
|
||||
line = fmt::format(
|
||||
R"({{ "type": "failure", "reason": "{}", {} }})",
|
||||
reason,
|
||||
formatLogProperty(entry)
|
||||
R"({{ "type": "failure", "reason": "{}", {} }})", reason, formatLogProperty(entry)
|
||||
);
|
||||
} else {
|
||||
throw std::runtime_error("Unsupported type of semantic entry.");
|
||||
|
@ -136,7 +133,8 @@ void MachineReadableStderrSink::receive(const logging::Entry& entry) {
|
|||
|
||||
if (line) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "logging/Entry.h"
|
||||
#include "logging/Sink.h"
|
||||
#include "tools/ProgressBar.h"
|
||||
#include <filesystem>
|
||||
|
||||
// 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 {
|
||||
public:
|
||||
NiceStderrSink(logging::Level minLevel);
|
||||
void receive(const logging::Entry& entry) override;
|
||||
|
||||
private:
|
||||
void startProgressIndication();
|
||||
void interruptProgressIndication();
|
||||
|
@ -28,6 +31,7 @@ class QuietStderrSink : public logging::Sink {
|
|||
public:
|
||||
QuietStderrSink(logging::Level minLevel);
|
||||
void receive(const logging::Entry& entry) override;
|
||||
|
||||
private:
|
||||
logging::Level minLevel;
|
||||
bool quietSoFar = true;
|
||||
|
@ -36,11 +40,13 @@ private:
|
|||
};
|
||||
|
||||
// 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 {
|
||||
public:
|
||||
MachineReadableStderrSink(logging::Level minLevel);
|
||||
void receive(const logging::Entry& entry) override;
|
||||
|
||||
private:
|
||||
logging::Level minLevel;
|
||||
int lastProgressPercent = -1;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include "Timeline.h"
|
||||
|
||||
template<typename T, bool AutoJoin = false>
|
||||
template <typename T, bool AutoJoin = false>
|
||||
class BoundedTimeline : public Timeline<T, AutoJoin> {
|
||||
using typename Timeline<T, AutoJoin>::time_type;
|
||||
using Timeline<T, AutoJoin>::equals;
|
||||
|
@ -12,31 +12,26 @@ public:
|
|||
using Timeline<T, AutoJoin>::end;
|
||||
|
||||
BoundedTimeline() :
|
||||
range(TimeRange::zero())
|
||||
{}
|
||||
range(TimeRange::zero()) {}
|
||||
|
||||
explicit BoundedTimeline(TimeRange range) :
|
||||
range(range)
|
||||
{}
|
||||
range(range) {}
|
||||
|
||||
template<typename InputIterator>
|
||||
template <typename InputIterator>
|
||||
BoundedTimeline(TimeRange range, InputIterator first, InputIterator last) :
|
||||
range(range)
|
||||
{
|
||||
range(range) {
|
||||
for (auto it = first; it != last; ++it) {
|
||||
// Virtual function call in constructor. Derived constructors shouldn't call this one!
|
||||
BoundedTimeline::set(*it);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename collection_type>
|
||||
template <typename collection_type>
|
||||
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(range, initializerList.begin(), initializerList.end())
|
||||
{}
|
||||
BoundedTimeline(range, initializerList.begin(), initializerList.end()) {}
|
||||
|
||||
TimeRange getRange() const override {
|
||||
return range;
|
||||
|
@ -53,8 +48,7 @@ public:
|
|||
// Clip the value's range to bounds
|
||||
TimeRange& valueRange = timedValue.getTimeRange();
|
||||
valueRange.resize(
|
||||
max(range.getStart(), valueRange.getStart()),
|
||||
min(range.getEnd(), valueRange.getEnd())
|
||||
max(range.getStart(), valueRange.getStart()), min(range.getEnd(), valueRange.getEnd())
|
||||
);
|
||||
|
||||
return Timeline<T, AutoJoin>::set(timedValue);
|
||||
|
@ -77,5 +71,5 @@ private:
|
|||
TimeRange range;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
template <typename T>
|
||||
using JoiningBoundedTimeline = BoundedTimeline<T, true>;
|
||||
|
|
|
@ -2,40 +2,33 @@
|
|||
|
||||
#include "BoundedTimeline.h"
|
||||
|
||||
template<typename T, bool AutoJoin = false>
|
||||
template <typename T, bool AutoJoin = false>
|
||||
class ContinuousTimeline : public BoundedTimeline<T, AutoJoin> {
|
||||
|
||||
public:
|
||||
ContinuousTimeline(TimeRange range, T defaultValue) :
|
||||
BoundedTimeline<T, AutoJoin>(range),
|
||||
defaultValue(defaultValue)
|
||||
{
|
||||
defaultValue(defaultValue) {
|
||||
// Virtual function call in constructor. Derived constructors shouldn't call this one!
|
||||
ContinuousTimeline::clear(range);
|
||||
}
|
||||
|
||||
template<typename InputIterator>
|
||||
template <typename InputIterator>
|
||||
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!
|
||||
for (auto it = first; it != last; ++it) {
|
||||
ContinuousTimeline::set(*it);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename collection_type>
|
||||
template <typename collection_type>
|
||||
ContinuousTimeline(TimeRange range, T defaultValue, collection_type collection) :
|
||||
ContinuousTimeline(range, defaultValue, collection.begin(), collection.end())
|
||||
{}
|
||||
ContinuousTimeline(range, defaultValue, collection.begin(), collection.end()) {}
|
||||
|
||||
ContinuousTimeline(
|
||||
TimeRange range,
|
||||
T defaultValue,
|
||||
std::initializer_list<Timed<T>> initializerList
|
||||
TimeRange range, 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;
|
||||
|
||||
|
@ -47,5 +40,5 @@ private:
|
|||
T defaultValue;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
template <typename T>
|
||||
using JoiningContinuousTimeline = ContinuousTimeline<T, true>;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#include "TimeRange.h"
|
||||
#include <stdexcept>
|
||||
#include <ostream>
|
||||
|
||||
#include <format.h>
|
||||
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
|
||||
using time_type = TimeRange::time_type;
|
||||
|
||||
TimeRange TimeRange::zero() {
|
||||
|
@ -12,18 +14,14 @@ TimeRange TimeRange::zero() {
|
|||
|
||||
TimeRange::TimeRange() :
|
||||
start(0_cs),
|
||||
end(0_cs)
|
||||
{}
|
||||
end(0_cs) {}
|
||||
|
||||
TimeRange::TimeRange(time_type start, time_type end) :
|
||||
start(start),
|
||||
end(end)
|
||||
{
|
||||
end(end) {
|
||||
if (start > end) {
|
||||
throw std::invalid_argument(fmt::format(
|
||||
"Time range start must not be less than end. Start: {0}, end: {1}",
|
||||
start,
|
||||
end
|
||||
"Time range start must not be less than end. Start: {0}, end: {1}", start, end
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -97,11 +95,11 @@ void TimeRange::trim(const TimeRange& limits) {
|
|||
}
|
||||
|
||||
void TimeRange::trimLeft(time_type value) {
|
||||
trim({ value, end });
|
||||
trim({value, end});
|
||||
}
|
||||
|
||||
void TimeRange::trimRight(time_type value) {
|
||||
trim({ start, value });
|
||||
trim({start, value});
|
||||
}
|
||||
|
||||
bool TimeRange::operator==(const TimeRange& rhs) const {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue