2017-06-12 10:23:00 +00:00
// 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 } ) ;
// Polyfill for Array.isArray
Array . isArray || ( Array . isArray = function ( r ) { return "[object Array]" === Object . prototype . toString . call ( r ) } ) ;
// Polyfill for Array.prototype.map
Array . prototype . map || ( Array . prototype . map = function ( r ) { var t , n , o ; if ( null == this ) throw new TypeError ( "this is null or not defined" ) ; var e = Object ( this ) , i = e . length >>> 0 ; if ( "function" != typeof r ) throw new TypeError ( r + " is not a function" ) ; for ( arguments . length > 1 && ( t = arguments [ 1 ] ) , n = new Array ( i ) , o = 0 ; o < i ; ) { var a , p ; o in e & & ( a = e [ o ] , p = r.call ( t , a , o , e ) , n [ o ] = p ) , o + + } return n } ) ;
// Polyfill for Array.prototype.every
Array . prototype . every || ( Array . prototype . every = function ( r , t ) { "use strict" ; var e , n ; if ( null == this ) throw new TypeError ( "this is null or not defined" ) ; var o = Object ( this ) , i = o . length >>> 0 ; if ( "function" != typeof r ) throw new TypeError ; for ( arguments . length > 1 && ( e = t ) , n = 0 ; n < i ; ) { var y ; if ( n in o & & ( y = o [ n ] , ! r.call ( e , y , n , o ) ) ) return ! 1 ; n + + } return ! 0 } ) ;
// Polyfill for Array.prototype.find
Array . prototype . find || ( Array . prototype . find = function ( r ) { if ( null === this ) throw new TypeError ( "Array.prototype.find called on null or undefined" ) ; if ( "function" != typeof r ) throw new TypeError ( "callback must be a function" ) ; for ( var n = Object ( this ) , t = n . length >>> 0 , o = arguments [ 1 ] , e = 0 ; e < t ; e + + ) { var f = n [ e ] ; if ( r.call ( o , f , e , n ) ) return f }} ) ;
// Polyfill for Array.prototype.filter
Array . prototype . filter || ( Array . prototype . filter = function ( r ) { "use strict" ; if ( void 0 === this || null === this ) throw new TypeError ; var t = Object ( this ) , e = t . length >>> 0 ; if ( "function" != typeof r ) throw new TypeError ; for ( var i = [ ] , o = arguments . length >= 2 ? arguments [ 1 ] : void 0 , n = 0 ; n < e ; n + + ) if ( n in t ) { var f = t [ n ] ; r.call ( o , f , n , t ) & & i.push ( f ) } return i } ) ;
// Polyfill for Array.prototype.forEach
Array . prototype . forEach || ( Array . prototype . forEach = function ( a , b ) { var c , d ; if ( null === this ) throw new TypeError ( " this is null or not defined" ) ; var e = Object ( this ) , f = e . length >>> 0 ; if ( "function" != typeof a ) throw new TypeError ( a + " is not a function" ) ; for ( arguments . length > 1 && ( c = b ) , d = 0 ; d < f ; ) { var g ; d in e & & ( g = e [ d ] , a.call ( c , g , d , e ) ) , d + + }} ) ;
// Polyfill for Array.prototype.includes
Array . prototype . includes || ( Array . prototype . includes = function ( r , t ) { if ( null == this ) throw new TypeError ( '"this" is null or not defined' ) ; var e = Object ( this ) , n = e . length >>> 0 ; if ( 0 === n ) return ! 1 ; for ( var i = 0 | t , o = Math . max ( i >= 0 ? i : n - Math . abs ( i ) , 0 ) ; o < n ; ) { if ( function ( r , t ) { return r = = = t | | " number " = = typeof r & & " number " = = typeof t & & isNaN ( r ) & & isNaN ( t ) } ( e [ o ] , r ) ) return ! 0 ; o + + } return ! 1 } ) ;
2017-06-30 20:12:51 +00:00
// Polyfill for Array.prototype.indexOf
Array . prototype . indexOf || ( Array . prototype . indexOf = function ( r , t ) { var n ; if ( null == this ) throw new TypeError ( '"this" is null or not defined' ) ; var e = Object ( this ) , i = e . length >>> 0 ; if ( 0 === i ) return - 1 ; var o = 0 | t ; if ( o >= i ) return - 1 ; for ( n = Math . max ( o >= 0 ? o : i - Math . abs ( o ) , 0 ) ; n < i ; ) { if ( n in e & & e [ n ] = = = r ) return n ; n + + } return - 1 } ) ;
// Polyfill for Array.prototype.some
Array . prototype . some || ( Array . prototype . some = function ( r ) { "use strict" ; if ( null == this ) throw new TypeError ( "Array.prototype.some called on null or undefined" ) ; if ( "function" != typeof r ) throw new TypeError ; for ( var e = Object ( this ) , o = e . length >>> 0 , t = arguments . length >= 2 ? arguments [ 1 ] : void 0 , n = 0 ; n < o ; n + + ) if ( n in e & & r.call ( t , e [ n ] , n , e ) ) return ! 0 ; return ! 1 } ) ;
// Polyfill for String.prototype.trim
String . prototype . trim || ( String . prototype . trim = function ( ) { return this . replace ( /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g , "" ) } ) ;
2017-06-12 10:23:00 +00:00
// 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 " ) } ) } ( ) ;
2017-06-16 19:13:28 +00:00
function last ( array ) {
return array [ array . length - 1 ] ;
}
2017-06-30 20:12:51 +00:00
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 v . toString ( 16 ) ;
} ) ;
}
2017-06-16 19:13:28 +00:00
function toArray ( list ) {
var result = [ ] ;
for ( var i = 0 ; i < list . length ; i ++ ) {
result . push ( list [ i ] ) ;
}
return result ;
}
function toArrayBase1 ( list ) {
var result = [ ] ;
for ( var i = 1 ; i <= list . length ; i ++ ) {
result . push ( list [ i ] ) ;
}
return result ;
}
2017-06-20 19:42:15 +00:00
function pad ( n , width , z ) {
z = z || '0' ;
n = String ( n ) ;
return n . length >= width ? n : new Array ( width - n . length + 1 ) . join ( z ) + n ;
}
2017-06-16 19:13:58 +00:00
// Checks whether scripts are allowed to write files by creating and deleting a dummy file
function canWriteFiles ( ) {
try {
var file = new File ( ) ;
file . open ( 'w' ) ;
file . writeln ( '' ) ;
file . close ( ) ;
file . remove ( ) ;
return true ;
} catch ( e ) {
return false ;
}
}
2017-06-30 20:12:51 +00:00
function frameToTime ( frameNumber , compItem ) {
2017-06-20 20:30:24 +00:00
return frameNumber * compItem . frameDuration ;
}
2017-06-30 20:12:51 +00:00
function timeToFrame ( time , compItem ) {
return time * compItem . frameRate ;
}
// To prevent rounding errors
var epsilon = 0.001 ;
2017-06-20 20:30:24 +00:00
function isFrameVisible ( compItem , frameNumber ) {
if ( ! compItem ) return false ;
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
var time = frameToTime ( frameNumber + epsilon , compItem ) ;
2017-06-20 20:30:24 +00:00
var videoLayers = toArrayBase1 ( compItem . layers ) . filter ( function ( layer ) {
return layer . hasVideo ;
} ) ;
var result = videoLayers . find ( function ( layer ) {
return layer . activeAtTime ( time ) ;
} ) ;
return Boolean ( result ) ;
}
2017-07-10 18:19:36 +00:00
var appName = 'Rhubarb Lip Sync' ;
2017-07-01 21:12:19 +00:00
var settingsFilePath = Folder . userData . fullName + '/rhubarb-ae-settings.json' ;
2017-06-16 19:13:58 +00:00
2017-06-30 20:12:51 +00:00
function readTextFile ( fileOrPath ) {
var filePath = fileOrPath . fsName || fileOrPath ;
var file = new File ( filePath ) ;
function check ( ) {
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 ( ) ;
return result ;
} finally {
file . close ( ) ; check ( ) ;
}
}
function writeTextFile ( fileOrPath , text ) {
var filePath = fileOrPath . fsName || fileOrPath ;
var file = new File ( filePath ) ;
function check ( ) {
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 ( ) ;
} finally {
file . close ( ) ; check ( ) ;
}
}
2017-06-16 19:13:58 +00:00
function readSettingsFile ( ) {
try {
2017-06-30 20:12:51 +00:00
return JSON . parse ( readTextFile ( settingsFilePath ) ) ;
2017-06-16 19:13:58 +00:00
} catch ( e ) {
return { } ;
}
}
function writeSettingsFile ( settings ) {
try {
2017-06-30 20:12:51 +00:00
writeTextFile ( settingsFilePath , JSON . stringify ( settings , null , 2 ) ) ;
2017-06-16 19:13:58 +00:00
} catch ( e ) {
alert ( 'Error persisting settings. ' + e . message ) ;
}
}
2017-07-01 21:12:19 +00:00
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 , "'\\''" ) + "'" ;
}
function exec ( command ) {
return system . callSystem ( command ) ;
}
function execInWindow ( command ) {
if ( osIsWindows ) {
system . callSystem ( 'cmd /C "' + command + '"' ) ;
} else {
// I didn't think it could be so complicated on OS X to open a new Terminal window,
// execute a command, then close the Terminal window.
// If you know a better solution, let me know!
var escapedCommand = command . replace ( /"/g , '\\"' ) ;
var appleScript = ' \
tell application "Terminal" \
-- Quit terminal \
-- Yes , that \ ' s undesirable if there was an open window before . \
-- But all solutions I could find were at least as hacky . \
quit \
-- Open terminal \
activate \
-- Run command in new tab \
set newTab to do script ( "' + escapedCommand + '" ) \
-- Wait until command is done \
tell newTab \
repeat while busy \
delay 0.1 \
end repeat \
end tell \
quit \
end tell ' ;
exec ( 'osascript -e ' + cliEscape ( appleScript ) ) ;
}
2017-06-30 20:12:51 +00:00
}
2017-07-01 21:12:19 +00:00
var rhubarbPath = osIsWindows ? 'rhubarb.exe' : '/usr/local/bin/rhubarb' ;
2017-06-12 10:23:00 +00:00
// ExtendScript's resource strings are a pain to write.
// This function allows them to be written in JSON notation, then converts them into the required
// format.
// For instance, this string: '{ "__type__": "StaticText", "text": "Hello world" }'
// is converted to this: 'StaticText { "text": "Hello world" }'.
// This code relies on the fact that, contrary to the language specification, all major JavaScript
// implementations keep object properties in insertion order.
function createResourceString ( tree ) {
var result = JSON . stringify ( tree , null , 2 ) ;
result = result . replace ( /(\{\s*)"__type__":\s*"(\w+)",?\s*/g , '$2 $1' ) ;
return result ;
}
// 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 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'
] ;
var result = { } ;
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 ) {
return Object . assign ( { _ _type _ _ : value } , options ) ;
} ;
} ) ;
return result ;
} ) ( ) ;
2017-06-16 19:13:28 +00:00
// Returns the path of a project item within the project
function getItemPath ( item ) {
if ( item === app . project . rootFolder ) {
return '/' ;
}
2017-07-01 21:12:19 +00:00
2017-06-16 19:13:28 +00:00
var result = item . name ;
while ( item . parentFolder !== app . project . rootFolder ) {
result = item . parentFolder . name + ' / ' + result ;
item = item . parentFolder ;
}
return '/ ' + result ;
}
// 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 ) {
return item . text === text ;
} ) ;
if ( ! targetItem && itemControl . items . length ) {
targetItem = itemControl . items [ 0 ] ;
}
if ( targetItem ) {
itemControl . selection = targetItem ;
}
}
function getWaveFileProjectItems ( ) {
var result = toArrayBase1 ( app . project . items ) . filter ( function ( item ) {
var isAudioFootage = item instanceof FootageItem && item . hasAudio && ! item . hasVideo ;
if ( ! isAudioFootage ) return false ;
2017-07-01 21:12:19 +00:00
2017-06-16 19:13:28 +00:00
var isWaveFile = item . file && item . file . exists && item . file . name . match ( /\.wav$/i ) ;
return isWaveFile ;
} ) ;
return result ;
}
2017-06-20 19:42:15 +00:00
var mouthShapeNames = 'ABCDEFGHX' . split ( '' ) ;
var basicMouthShapeCount = 6 ;
2017-06-20 20:30:24 +00:00
var mouthShapeCount = mouthShapeNames . length ;
2017-06-20 19:42:15 +00:00
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 isOptional = i >= basicMouthShapeCount ;
result += '\n00:' + pad ( i , 2 ) + '\t' + mouthShapeName + ( isOptional ? ' (optional)' : '' ) ;
} ) ;
return result ;
}
function createExtendedShapeCheckboxes ( ) {
var result = { } ;
extendedMouthShapeNames . forEach ( function ( shapeName ) {
result [ shapeName . toLowerCase ( ) ] = controlFunctions . Checkbox ( {
text : shapeName ,
helpTip : 'Controls whether to use the optional ' + shapeName + ' shape.'
} ) ;
} ) ;
return result ;
}
2017-06-12 10:23:00 +00:00
function createDialogWindow ( ) {
var resourceString ;
with ( controlFunctions ) {
resourceString = createResourceString (
Dialog ( {
2017-07-10 18:19:36 +00:00
text : appName ,
2017-06-12 10:23:00 +00:00
settings : Group ( {
orientation : 'column' ,
alignChildren : [ 'left' , 'top' ] ,
audioFile : Group ( {
label : StaticText ( {
text : 'Audio file:' ,
// If I don't explicitly activate a control, After Effects has trouble
// with keyboard focus, so I can't type in the text edit field below.
active : true
} ) ,
2017-06-20 17:14:01 +00:00
value : DropDownList ( {
helpTip : 'An audio file containing recorded dialog.\n'
+ 'This field shows all audio files of type .wav that exist in '
+ 'your After Effects project.'
} )
2017-06-12 10:23:00 +00:00
} ) ,
dialogText : Group ( {
label : StaticText ( { text : 'Dialog text (optional):' } ) ,
value : EditText ( {
properties : { multiline : true } ,
characters : 60 ,
2017-06-20 17:14:01 +00:00
minimumSize : [ 0 , 100 ] ,
helpTip : 'For better animation results, you can specify the text of '
+ 'the recording here. This field is optional.'
2017-06-12 10:23:00 +00:00
} )
} ) ,
mouthComp : Group ( {
label : StaticText ( { text : 'Mouth composition:' } ) ,
2017-06-20 19:42:15 +00:00
value : DropDownList ( { helpTip : getMouthCompHelpTip ( ) } )
2017-06-12 10:23:00 +00:00
} ) ,
2017-06-20 19:42:15 +00:00
extendedMouthShapes : Group (
Object . assign (
{ label : StaticText ( { text : 'Extended mouth shapes:' } ) } ,
createExtendedShapeCheckboxes ( )
)
) ,
2017-06-12 10:23:00 +00:00
targetFolder : Group ( {
label : StaticText ( { text : 'Target folder:' } ) ,
2017-06-20 17:14:01 +00:00
value : DropDownList ( {
helpTip : 'The project folder in which to create the animation '
+ 'composition. The composition will be named like the audio file.'
} )
2017-06-12 10:23:00 +00:00
} ) ,
frameRate : Group ( {
label : StaticText ( { text : 'Frame rate:' } ) ,
2017-06-20 17:14:01 +00:00
value : EditText ( {
characters : 8 ,
helpTip : 'The frame rate for the animation.'
} ) ,
auto : Checkbox ( {
text : 'From mouth composition' ,
helpTip : 'If checked, the animation will use the same frame rate as '
+ 'the mouth composition.'
} )
2017-06-12 10:23:00 +00:00
} )
} ) ,
separator : Group ( { preferredSize : [ '' , 3 ] } ) ,
buttons : Group ( {
alignment : 'right' ,
animate : Button ( {
properties : { name : 'ok' } ,
text : 'Animate'
} ) ,
cancel : Button ( {
properties : { name : 'cancel' } ,
text : 'Cancel'
} )
} )
} )
) ;
}
// Create window and child controls
var window = new Window ( resourceString ) ;
var controls = {
audioFile : window . settings . audioFile . value ,
dialogText : window . settings . dialogText . value ,
mouthComp : window . settings . mouthComp . value ,
targetFolder : window . settings . targetFolder . value ,
frameRate : window . settings . frameRate . value ,
autoFrameRate : window . settings . frameRate . auto ,
animateButton : window . buttons . animate ,
cancelButton : window . buttons . cancel
} ;
2017-06-20 19:42:15 +00:00
extendedMouthShapeNames . forEach ( function ( shapeName ) {
controls [ 'mouthShape' + shapeName ] =
window . settings . extendedMouthShapes [ shapeName . toLowerCase ( ) ] ;
} ) ;
2017-07-01 21:12:19 +00:00
2017-06-16 19:13:28 +00:00
// Add audio file options
getWaveFileProjectItems ( ) . forEach ( function ( projectItem ) {
var listItem = controls . audioFile . add ( 'item' , getItemPath ( projectItem ) ) ;
listItem . projectItem = projectItem ;
} ) ;
2017-07-01 21:12:19 +00:00
2017-06-16 19:13:28 +00:00
// Add mouth composition options
var comps = toArrayBase1 ( app . project . items ) . filter ( function ( item ) {
return item instanceof CompItem ;
} ) ;
comps . forEach ( function ( projectItem ) {
var listItem = controls . mouthComp . add ( 'item' , getItemPath ( projectItem ) ) ;
listItem . projectItem = projectItem ;
} ) ;
// Add target folder options
var projectFolders = toArrayBase1 ( app . project . items ) . filter ( function ( item ) {
return item instanceof FolderItem ;
} ) ;
projectFolders . unshift ( app . project . rootFolder ) ;
projectFolders . forEach ( function ( projectFolder ) {
var listItem = controls . targetFolder . add ( 'item' , getItemPath ( projectFolder ) ) ;
listItem . projectItem = projectFolder ;
} ) ;
2017-06-16 19:13:58 +00:00
// Load persisted settings
var settings = readSettingsFile ( ) ;
selectByTextOrFirst ( controls . audioFile , settings . audioFile ) ;
controls . dialogText . text = settings . dialogText || '' ;
selectByTextOrFirst ( controls . mouthComp , settings . mouthComp ) ;
2017-06-20 19:42:15 +00:00
extendedMouthShapeNames . forEach ( function ( shapeName ) {
controls [ 'mouthShape' + shapeName ] . value =
2017-07-01 21:12:19 +00:00
( settings . extendedMouthShapes || { } ) [ shapeName . toLowerCase ( ) ] ;
2017-06-20 19:42:15 +00:00
} ) ;
2017-06-16 19:13:58 +00:00
selectByTextOrFirst ( controls . targetFolder , settings . targetFolder ) ;
controls . frameRate . text = settings . frameRate || '' ;
controls . autoFrameRate . value = settings . autoFrameRate ;
2017-06-16 19:12:48 +00:00
// Align controls
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 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 ) {
return last ( group . children ) . bounds . right - group . children [ 1 ] . bounds . left ;
} ) ;
var maxValueWidth = Math . max . apply ( Math , valueWidths ) ;
groups . forEach ( function ( group ) {
var multipleControls = group . children . length > 2 ;
if ( ! multipleControls ) {
group . children [ 1 ] . size . width = maxValueWidth ;
}
} ) ;
window . layout . layout ( true ) ;
} ;
2017-06-16 19:13:28 +00:00
var updating = false ;
function update ( ) {
if ( updating ) return ;
2017-07-01 21:12:19 +00:00
2017-06-16 19:13:28 +00:00
updating = true ;
try {
// Handle auto frame rate
var autoFrameRate = controls . autoFrameRate . value ;
controls . frameRate . enabled = ! autoFrameRate ;
if ( autoFrameRate ) {
// Take frame rate from mouth comp
var mouthComp = ( controls . mouthComp . selection || { } ) . projectItem ;
controls . frameRate . text = mouthComp ? mouthComp . frameRate : '' ;
} else {
// Sanitize frame rate
var sanitizedFrameRate = controls . frameRate . text . match ( /\d*\.?\d*/ ) [ 0 ] ;
if ( sanitizedFrameRate !== controls . frameRate . text ) {
controls . frameRate . text = sanitizedFrameRate ;
}
}
2017-07-01 21:12:19 +00:00
2017-06-16 19:13:58 +00:00
// Store settings
var settings = {
audioFile : ( controls . audioFile . selection || { } ) . text ,
dialogText : controls . dialogText . text ,
mouthComp : ( controls . mouthComp . selection || { } ) . text ,
2017-06-20 19:42:15 +00:00
extendedMouthShapes : { } ,
2017-06-16 19:13:58 +00:00
targetFolder : ( controls . targetFolder . selection || { } ) . text ,
frameRate : Number ( controls . frameRate . text ) ,
autoFrameRate : controls . autoFrameRate . value
} ;
2017-06-20 19:42:15 +00:00
extendedMouthShapeNames . forEach ( function ( shapeName ) {
settings . extendedMouthShapes [ shapeName . toLowerCase ( ) ] =
controls [ 'mouthShape' + shapeName ] . value ;
} ) ;
2017-06-16 19:13:58 +00:00
writeSettingsFile ( settings ) ;
2017-06-16 19:13:28 +00:00
} finally {
updating = false ;
}
}
2017-06-20 20:30:24 +00:00
// Validate user input. Possible return values:
// * Non-empty string: Validation failed. Show error message.
// * Empty string: Validation failed. Don't show error message.
// * Undefined: Validation succeeded.
function validate ( ) {
// Check input values
if ( ! controls . audioFile . selection ) return 'Please select an audio file.' ;
if ( ! controls . mouthComp . selection ) return 'Please select a mouth composition.' ;
if ( ! controls . targetFolder . selection ) return 'Please select a target folder.' ;
if ( Number ( controls . frameRate . text ) < 12 ) {
return 'Please enter a frame rate of at least 12 fps.' ;
}
2017-07-01 21:12:19 +00:00
2017-06-20 20:30:24 +00:00
// Check mouth shape visibility
var comp = controls . mouthComp . selection . projectItem ;
for ( var i = 0 ; i < mouthShapeCount ; i ++ ) {
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 + '.' ;
}
}
2017-07-01 21:12:19 +00:00
2017-06-20 20:30:24 +00:00
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?' ,
false ,
'Fix composition setting?' ) ;
if ( fix ) {
2017-07-10 18:19:36 +00:00
app . beginUndoGroup ( appName + ': Mouth composition setting' ) ;
2017-06-20 20:30:24 +00:00
comp . preserveNestedFrameRate = true ;
app . endUndoGroup ( ) ;
} else {
return '' ;
}
}
2017-07-01 21:12:19 +00:00
2017-06-20 21:12:26 +00:00
// Check for correct Rhubarb version
2017-07-01 21:12:19 +00:00
var version = exec ( rhubarbPath + ' --version' ) || '' ;
2017-06-20 21:12:26 +00:00
var match = version . match ( /Rhubarb Lip Sync version ((\d+)\.(\d+).(\d+))/ ) ;
if ( ! match ) {
2017-07-01 21:12:19 +00:00
var instructions = osIsWindows
2017-07-10 18:19:36 +00:00
? 'Make sure your PATH environment variable contains the ' + appName + ' '
2017-07-01 21:12:19 +00:00
+ 'application directory.'
2017-07-10 18:19:36 +00:00
: 'Make sure you have created this file as a symbolic link to the ' + appName + ' '
2017-07-01 21:12:19 +00:00
+ 'executable (rhubarb).' ;
return 'Cannot find executable file "' + rhubarbPath + '". \n' + instructions ;
2017-06-20 21:12:26 +00:00
}
var versionString = match [ 1 ] ;
var major = Number ( match [ 2 ] ) ;
var minor = Number ( match [ 3 ] ) ;
2017-06-30 20:12:51 +00:00
if ( major != 1 || minor < 6 ) {
2017-07-10 18:19:36 +00:00
return 'This script requires ' + appName + ' 1.6.0 or a later 1.x version. '
2017-07-01 21:12:19 +00:00
+ 'Your installed version is ' + versionString + ', which is not compatible.' ;
2017-06-20 21:12:26 +00:00
}
2017-06-20 20:30:24 +00:00
}
2017-06-30 20:12:51 +00:00
function generateMouthCues ( audioFileFootage , dialogText , mouthComp , extendedMouthShapeNames ,
targetProjectFolder , frameRate )
{
var basePath = Folder . temp . fsName + '/' + createGuid ( ) ;
var dialogFile = new File ( basePath + '.txt' ) ;
var logFile = new File ( basePath + '.log' ) ;
var jsonFile = new File ( basePath + '.json' ) ;
try {
// Create text file containing dialog
writeTextFile ( dialogFile , dialogText ) ;
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
// Create command line
2017-07-01 21:12:19 +00:00
var commandLine = rhubarbPath
+ ' --dialogFile ' + cliEscape ( dialogFile . fsName )
2017-06-30 20:12:51 +00:00
+ ' --exportFormat json'
2017-07-01 21:12:19 +00:00
+ ' --extendedShapes ' + cliEscape ( extendedMouthShapeNames . join ( '' ) )
+ ' --logFile ' + cliEscape ( logFile . fsName )
2017-06-30 20:12:51 +00:00
+ ' --logLevel fatal'
2017-07-01 21:12:19 +00:00
+ ' --output ' + cliEscape ( jsonFile . fsName )
+ ' ' + cliEscape ( audioFileFootage . file . fsName ) ;
2017-06-30 20:12:51 +00:00
// Run Rhubarb
2017-07-01 21:12:19 +00:00
execInWindow ( commandLine ) ;
2017-06-30 20:12:51 +00:00
// Check log for fatal errors
if ( logFile . exists ) {
var fatalLog = readTextFile ( logFile ) . trim ( ) ;
if ( fatalLog ) {
// Try to extract only the actual error message
var match = fatalLog . match ( /\[Fatal\] ([\s\S]*)/ ) ;
var message = match ? match [ 1 ] : fatalLog ;
2017-07-10 18:19:36 +00:00
throw new Error ( 'Error running ' + appName + '.\n' + message ) ;
2017-06-30 20:12:51 +00:00
}
}
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
var result ;
try {
result = JSON . parse ( readTextFile ( jsonFile ) ) ;
$ . writeln ( readTextFile ( jsonFile ) ) ;
} catch ( e ) {
throw new Error ( 'No animation result. Animation was probably canceled.' ) ;
}
return result ;
} finally {
dialogFile . remove ( ) ;
logFile . remove ( ) ;
jsonFile . remove ( ) ;
}
}
function animateMouthCues ( mouthCues , audioFileFootage , mouthComp , targetProjectFolder ,
frameRate )
{
// Find an unconflicting comp name
// ... strip extension .wav, if present
var baseName = audioFileFootage . name . match ( /^(.*?)(\.wav)?$/i ) [ 1 ] ;
var compName = baseName ;
// ... add numeric suffix, if needed
var existingItems = toArrayBase1 ( targetProjectFolder . items ) ;
var counter = 1 ;
while ( existingItems . some ( function ( item ) { return item . name === compName ; } ) ) {
counter ++ ;
compName = baseName + ' ' + counter ;
}
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
// Create new comp
var comp = targetProjectFolder . items . addComp ( compName , mouthComp . width , mouthComp . height ,
mouthComp . pixelAspect , audioFileFootage . duration , frameRate ) ;
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
// Show new comp
comp . openInViewer ( ) ;
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
// Add audio layer
comp . layers . add ( audioFileFootage ) ;
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
// Add mouth layer
var mouthLayer = comp . layers . add ( mouthComp ) ;
mouthLayer . timeRemapEnabled = true ;
mouthLayer . outPoint = comp . duration ;
2017-07-01 21:12:19 +00:00
2017-06-30 20:12:51 +00:00
// Animate mouth layer
var timeRemap = mouthLayer [ 'Time Remap' ] ;
// Enabling time remapping automatically adds two keys. Remove the second.
timeRemap . removeKey ( 2 ) ;
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 ) ) ;
var time = frame !== 0 ? frameToTime ( frame - epsilon , comp ) : 0 ;
// Set remapped time to *just after* the exact frame to prevent rounding errors
var mouthCompFrame = mouthShapeNames . indexOf ( mouthCue . value ) ;
var remappedTime = frameToTime ( mouthCompFrame + epsilon , mouthComp ) ;
timeRemap . setValueAtTime ( time , remappedTime ) ;
} ) ;
for ( var i = 1 ; i <= timeRemap . numKeys ; i ++ ) {
timeRemap . setInterpolationTypeAtKey ( i , KeyframeInterpolationType . HOLD ) ;
}
}
function animate ( audioFileFootage , dialogText , mouthComp , extendedMouthShapeNames ,
targetProjectFolder , frameRate )
{
try {
var mouthCues = generateMouthCues ( audioFileFootage , dialogText , mouthComp ,
extendedMouthShapeNames , targetProjectFolder , frameRate ) ;
2017-07-10 18:19:36 +00:00
app . beginUndoGroup ( appName + ': Animation' ) ;
2017-06-30 20:12:51 +00:00
animateMouthCues ( mouthCues , audioFileFootage , mouthComp , targetProjectFolder ,
frameRate ) ;
app . endUndoGroup ( ) ;
} catch ( e ) {
2017-07-10 18:19:36 +00:00
Window . alert ( e . message , appName , true ) ;
2017-06-30 20:12:51 +00:00
return ;
}
}
2017-06-16 19:13:28 +00:00
// Handle changes
update ( ) ;
2017-06-16 19:13:58 +00:00
controls . audioFile . onChange = update ;
controls . dialogText . onChanging = update ;
2017-06-16 19:13:28 +00:00
controls . mouthComp . onChange = update ;
2017-06-20 19:42:15 +00:00
extendedMouthShapeNames . forEach ( function ( shapeName ) {
controls [ 'mouthShape' + shapeName ] . onClick = update ;
} ) ;
2017-06-16 19:13:58 +00:00
controls . targetFolder . onChange = update ;
2017-06-16 19:13:28 +00:00
controls . frameRate . onChanging = update ;
controls . autoFrameRate . onClick = update ;
2017-07-01 21:12:19 +00:00
2017-06-12 10:23:00 +00:00
// Handle animation
controls . animateButton . onClick = function ( ) {
2017-06-20 20:30:24 +00:00
var validationError = validate ( ) ;
if ( typeof validationError === 'string' ) {
if ( validationError ) {
2017-07-10 18:19:36 +00:00
Window . alert ( validationError , appName , true ) ;
2017-06-20 20:30:24 +00:00
}
} else {
window . close ( ) ;
2017-06-30 20:12:51 +00:00
animate (
controls . audioFile . selection . projectItem ,
controls . dialogText . text || '' ,
controls . mouthComp . selection . projectItem ,
extendedMouthShapeNames . filter ( function ( shapeName ) {
return controls [ 'mouthShape' + shapeName ] . value ;
} ) ,
controls . targetFolder . selection . projectItem ,
Number ( controls . frameRate . text )
) ;
2017-06-20 20:30:24 +00:00
}
2017-06-12 10:23:00 +00:00
} ;
2017-07-01 21:12:19 +00:00
2017-06-12 10:23:00 +00:00
// Handle cancelation
controls . cancelButton . onClick = function ( ) {
window . close ( ) ;
} ;
return window ;
}
2017-06-16 19:13:58 +00:00
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.' ,
2017-07-10 18:19:36 +00:00
appName , true ) ;
2017-06-16 19:13:58 +00:00
return false ;
}
return true ;
}
if ( checkPreconditions ( ) ) {
createDialogWindow ( ) . show ( ) ;
}