Setting the file. One moment.
Audio Envelope · Embedded Captions · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page 14.42
Hershey Script1
scripts/audio-envelope.cjs
scripts/ audio-envelope.cjs
JavaScript · 95 lines · 3 KB
);
14 const fs = require ( "fs" );
15 const cp = require ( "child_process" );
16
17 const HOP = 0.05 ;
18
19 function findSource ( project ) {
20 for ( const c of [ "source.mp4" ]. concat (
21 fs
22 . readdirSync (project)
23 . filter (
24 ( f ) =>
25 / \. (mp4 | mov | webm | mkv | m4v | wav | m4a | mp3) $ / i . test (f) &&
26 ! / ^ (final | bg_plus_caps | fg_caps | rail | index)/ . test (f),
27 ),
28 )) {
29 const p = path. join (project, c);
30 if (fs. existsSync (p)) return p;
31 }
32 return null ;
33 }
34
35 function main () {
36 const project = path. resolve (process.argv[ 2 ] || "" );
37 if ( ! process.argv[ 2 ]) {
38 console. error ( "usage: audio-envelope.cjs <project-dir>" );
39 process. exit ( 1 );
40 }
41 const src = findSource (project);
42 if ( ! src) {
43 console. error ( "[envelope] no source media found" );
44 process. exit ( 2 );
45 }
46
47 // asetnsamples groups audio into fixed windows; astats prints RMS per window
48 const sr = 16000 ,
49 n = Math. round (sr * HOP );
50 let out;
51 try {
52 // resample INSIDE the filter chain — an output-option -ar applies after the
53 // filtergraph, which would make each window n/<input-rate> seconds instead of HOP
54 out = cp. execFileSync (
55 "ffmpeg" ,
56 [
57 "-v" ,
58 "info" ,
59 "-i" ,
60 src,
61 "-map" ,
62 "a:0" ,
63 "-af" ,
64 `aresample=${ sr },aformat=channel_layouts=mono,asetnsamples=n=${ n }:p=0,astats=metadata=1:reset=1,ametadata=mode=print:key=lavfi.astats.Overall.RMS_level:file=-` ,
65 "-f" ,
66 "null" ,
67 "-" ,
68 ],
69 { encoding: "utf8" , maxBuffer: 64 * 1024 * 1024 , stdio: [ "ignore" , "pipe" , "pipe" ] },
70 );
71 } catch (e) {
72 // ffmpeg writes the metadata to stdout before exiting; some containers still exit 0 — re-throw only if nothing parsed
73 out = (e.stdout || "" ) + "" ;
74 if ( ! out. includes ( "RMS_level" )) {
75 console. error ( `[envelope] ffmpeg failed: ${ e . message }` );
76 process. exit ( 2 );
77 }
78 }
79 const rmsDb = [];
80 for ( const line of String (out). split ( " \n " )) {
81 const m = line. match ( /lavfi \. astats \. Overall \. RMS_level=(- ? [\d.] +| -inf)/ );
82 if (m) rmsDb. push (m[ 1 ] === "-inf" ? - 90 : Math. max ( - 90 , parseFloat (m[ 1 ])));
83 }
84 if ( ! rmsDb. length ) {
85 console. error ( "[envelope] no RMS windows parsed" );
86 process. exit ( 2 );
87 }
88 const rms = rmsDb. map (( db ) => + Math. pow ( 10 , db / 20 ). toFixed ( 5 )); // linear 0..1
89 fs. writeFileSync (path. join (project, "envelope.json" ), JSON . stringify ({ hop: HOP , rms }));
90 const peakAt = rms. indexOf (Math. max ( ... rms)) * HOP ;
91 console. log (
92 `[envelope] ${ rms . length } windows @ ${ HOP }s → envelope.json (loudest beat ~${ peakAt . toFixed ( 2 ) }s)` ,
93 );
94 }
95 main ();