-
-
Save jashmenn/66f2806ae6da643a0bb16452629deee8 to your computer and use it in GitHub Desktop.
#!/usr/bin/env osascript -l JavaScript | |
/** | |
* Delete silence from Final Cut Pro timeline using a script. | |
* Demo: https://imgur.com/a/Zisav | |
* | |
* This script accepts an ffmpeg silencedetect log as input. | |
* | |
* To setup, have fcp running along with your clip selected. Ensure that the | |
* timecode will start at zero before running this script. That is, if your clip | |
* is in the middle of your project, create a compound clip first and then enter | |
* into that clip before running this script. | |
* | |
* To generate silence.txt: | |
* /usr/local/bin/ffmpeg -i $1 -af silencedetect=n=-50dB:d=1 -f s16le -y /dev/null 2>&1 | tee silence.txt | |
* | |
* Currently it adds a little margin of silence at the end of each noised clip. You can adjust this below. | |
* If you adjust the silencedetection dB, then this number may need tweaking. | |
* | |
* For example, if your dB filter is higher, you'll filter out more background | |
* noise, but you might clip the audio you want. Extending the margins can help | |
* prevent clipping a natural trailing off of the sound you want. | |
* | |
* Nate Murray 2017 <nate@natemurray.com> | |
**/ | |
function run(argv) { | |
ObjC.import("stdlib"); | |
ObjC.import("Foundation"); | |
console.log(JSON.stringify(argv)); | |
console.log("Start Final Cut Pro and click the matching clip"); | |
// Adjust these to your liking | |
// One way to test, is comment out the delete section below and visually inspect | |
const startMargin = 0.100; | |
const endMargin = 0.000; | |
if (!Application("Final Cut Pro").running()) { | |
console.log("Final Cut Pro isn't running"); | |
$.exit(1); | |
} | |
// let app = Application("Finder"); | |
let app = Application.currentApplication(); | |
app.includeStandardAdditions = true; | |
let se = Application("System Events"); | |
function loadFile(path) { | |
let fm = $.NSFileManager.defaultManager; | |
let contents = fm.contentsAtPath(path.toString()); // NSData | |
contents = $.NSString.alloc.initWithDataEncoding( | |
contents, | |
$.NSUTF8StringEncoding | |
); | |
return ObjC.unwrap(contents); | |
} | |
function parseSilenceFile(contents) { | |
let lines = contents.split("\n"); | |
let silences = []; | |
let pair = {}; | |
for (let i = 0; i < lines.length; i++) { | |
let l = lines[i]; | |
// [silencedetect @ 0x7fd895407da0] silence_start: 272.972 | |
// [silencedetect @ 0x7fd895407da0] silence_end: 274.762 | silence_duration: 1.78948 | |
let startReg = /silence_start: (\d+.?\d+)\b/; | |
let endReg = /silence_end: (\d+.?\d+)\b/; | |
let startMatch = startReg.exec(l); | |
let endMatch = endReg.exec(l); | |
if (startMatch && startMatch.length > 0) { | |
pair["start"] = startMatch[1]; | |
pair["end"] = null; | |
} | |
if (endMatch && endMatch.length > 0) { | |
pair["end"] = endMatch[1]; | |
if (pair["start"]) { | |
(pair => silences.push(pair))(pair); | |
pair = {}; | |
} | |
} | |
} | |
return silences; | |
} | |
// parse the silence points | |
let path = argv[0]; | |
let rawSilenceFileContents = loadFile(path); | |
let silencePoints = parseSilenceFile(rawSilenceFileContents); | |
console.log(JSON.stringify(silencePoints)); | |
// activate fcp | |
let fcp = Application("Final Cut Pro"); | |
fcp.activate(); | |
delay(1.0); | |
function moveToTimecode(timeInSeconds) { | |
delay(0.2); | |
// convert timeInSeconds in decimal to timecode values. | |
let [seconds, deciseconds] = timeInSeconds.split("."); | |
let d = new Date(null); | |
d.setSeconds(parseInt(seconds)); | |
let hourmindays = d.toISOString().substr(11, 8); | |
let decipart = (60 * parseFloat("0." + deciseconds)) | |
.toString() | |
.split(".")[0] | |
.substr(0, 2); | |
if (decipart.length < 2) { | |
decipart = "0" + decipart; | |
} | |
let timecodekeystroke = (hourmindays + decipart).replace(/:/g, ""); | |
console.log( | |
" timecodekeystroke ", | |
timeInSeconds, | |
hourmindays, | |
decipart, | |
timecodekeystroke | |
); | |
se.keystroke("p", { using: "control down" }); | |
se.keystroke(timecodekeystroke); | |
se.keyCode(36); // Press Enter | |
} | |
function blade() { | |
se.keystroke("b", { using: "command down" }); | |
} | |
// extend edges | |
silencePoints = silencePoints.map(sp => ({ | |
start: (parseFloat(sp.start) + startMargin).toString(), | |
end: (parseFloat(sp.end) - endMargin).toString() | |
})); | |
for (let i = 0; i < silencePoints.length; i++) { | |
let sp = silencePoints[i]; | |
console.log(i, JSON.stringify(sp)); | |
delay(0.05); | |
moveToTimecode(sp.start); | |
delay(0.05); | |
blade(); | |
delay(0.05); | |
moveToTimecode(sp.end); | |
delay(0.05); | |
blade(); | |
} | |
console.log("Deleting Silence"); | |
// Go backwards, becase we're changing the total time as we go | |
for (let i = silencePoints.length - 1; i > 0; i--) { | |
let sp = silencePoints[i]; | |
console.log("D", i, JSON.stringify(sp)); | |
moveToTimecode(sp.start); | |
delay(0.1); | |
// select current clip | |
se.keystroke("c"); | |
delay(0.1); | |
// delete the silence | |
se.keyCode(51); // Press Delete | |
delay(0.3); | |
} | |
} |
@shervinshaikh well, notice that your shell says -af: No such file or directory
-- makes me think your ffmpeg
isn't parsing the options in the same way as mine. (It happens, ffmpeg is notorious for having incompatible versions.)
I'd try to cat silence.txt
and make sure you even have any breakpoints in that file.
What are the options you have parsed? I have the same error.
I found the solution: replace the $1 in the command /usr/local/bin/ffmpeg -i $1 -af silencedetect=n=-50dB:d=1 -f s16le -y /dev/null 2>&1 | tee silence.txt
to your file name you want to edit. Thanks for the script 😁
Hi all, with Catalina 10.15.4, osascript is not allowed to send entries (1002)
execution error: Error: Error: osascript n’est pas autorisé à envoyer de saisies. (1002)
How you fix that ?
EDIT Solve : Add Terminal and apple script in security préférence / accessibility and input
If your video is in 30 fps you have to change this line in this script
let decipart = (60 * parseFloat("0." + deciseconds))
to
let decipart = (30 * parseFloat("0." + deciseconds))
and it work great
thanks jashmenn for you work.
It's not working for me :/
Here's what I am doing:
- I have imported my .mov video to FCPX at the beginning of my timeline;
- Then I ran the command to generate the silence.txt file passing my video as parameter (no errors are shown, but not much info as well). Please see my silence.txt below.
- Then I execute the command node final-cut-it-out.js silence.txt (is this the correct way to execute it?).
Silence.txt file content:
ffmpeg version N-97224-g7104c4dd88-tessus https://evermeet.cx/ffmpeg/ Copyright (c) 2000-2020 the FFmpeg developers
built with Apple clang version 11.0.0 (clang-1100.0.33.17)
configuration: --cc=/usr/bin/clang --prefix=/opt/ffmpeg --extra-version=tessus --enable-avisynth --enable-fontconfig --enable-gpl --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libfreetype --enable-libgsm --enable-libmodplug --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopus --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-version3 --pkg-config-flags=--static --disable-ffplay
libavutil 56. 42.102 / 56. 42.102
libavcodec 58. 77.101 / 58. 77.101
libavformat 58. 42.100 / 58. 42.100
libavdevice 58. 9.103 / 58. 9.103
libavfilter 7. 77.101 / 7. 77.101
libswscale 5. 6.101 / 5. 6.101
libswresample 3. 6.100 / 3. 6.100
libpostproc 55. 6.100 / 55. 6.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'IMG_8657.mov':
Metadata:
major_brand : qt
minor_version : 0
compatible_brands: qt
creation_time : 2020-04-10T06:03:44.000000Z
com.apple.quicktime.make: Apple
com.apple.quicktime.model: iPhone XS
com.apple.quicktime.software: 13.2.3
com.apple.quicktime.creationdate: 2020-04-10T06:38:24+0100
Duration: 00:00:17.68, start: 0.000000, bitrate: 15854 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080, 15636 kb/s, 29.97 fps, 29.97 tbr, 600 tbn, 1200 tbc (default)
Metadata:
creation_time : 2020-04-10T06:03:44.000000Z
handler_name : Core Media Video
encoder : H.264
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 187 kb/s (default)
Metadata:
creation_time : 2020-04-10T06:03:44.000000Z
handler_name : Core Media Audio
Stream #0:2(und): Data: none (mebx / 0x7862656D), 23 kb/s (default)
Metadata:
creation_time : 2020-04-10T06:03:44.000000Z
handler_name : Core Media Metadata
Stream #0:3(und): Data: none (mebx / 0x7862656D), 0 kb/s (default)
Metadata:
creation_time : 2020-04-10T06:03:44.000000Z
handler_name : Core Media Metadata
Stream mapping:
Stream #0:1 -> #0:0 (aac (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, s16le, to '/dev/null':
Metadata:
major_brand : qt
minor_version : 0
compatible_brands: qt
com.apple.quicktime.creationdate: 2020-04-10T06:38:24+0100
com.apple.quicktime.make: Apple
com.apple.quicktime.model: iPhone XS
com.apple.quicktime.software: 13.2.3
encoder : Lavf58.42.100
Stream #0:0(und): Audio: pcm_s16le, 44100 Hz, stereo, s16, 1411 kb/s (default)
Metadata:
creation_time : 2020-04-10T06:03:44.000000Z
handler_name : Core Media Audio
encoder : Lavc58.77.101 pcm_s16le
size= 3048kB time=00:00:17.69 bitrate=1411.2kbits/s speed= 624x
video:0kB audio:3048kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%
If your video is in 30 fps you have to change this line in this script
let decipart = (60 * parseFloat("0." + deciseconds))
to
let decipart = (30 * parseFloat("0." + deciseconds))
and it work great
thanks jashmenn for you work.
Bonjour à vous EnseignantLapro,
Je suis le rédacteur d'un ensemble de documents pour FCP X (martingosset.com) et écris quelques modestes application pour les monteurs FCP X (http://martingosset.com/boomerang-app/). Mais uniquement en Applescript.
J'aimerais vous contacter car je pense que ce petit script en intéresserait plus d'un. Mon idée serait de générer un XML grâce au fichier txt que semble gérer ce script; et plutôt que de faire faire le boulot de coupe et suppression par ce dernier. Je pense que je serais capable de le faire en Applescript que je connais et parce que j'ai bine l'habitude des FCPXML.
C'est sauf que j'ai peur de mal m'y prendre pour installer ce premier script ici présent.
Si vous aviez la gentillesse de m'épauler un peu pour cette partie, je vous offrirais avec grand plaisir mes documents ... en échange et pour ne pas que vous vous sentiez trop exploités ;)
En attendant de vous lire
Martin Gosset
I finlay choose another way to delimitate those silents parts : with keyword !!!... and wrote a small app in applescript that create an FCPXML... https://vimeo.com/415340397/f28424553d
Thank Jashmaenn
Martin
I finlay choose another way to delimitate those silents parts : with keyword !!!... and wrote a small app in applescript that create an FCPXML... https://vimeo.com/415340397/f28424553d
Thank Jashmaenn
Martin
That looks amazing and is exactly what I need - something to just identify silence in Fcpx. Would you be willing to share your app?
I finlay choose another way to delimitate those silents parts : with keyword !!!... and wrote a small app in applescript that create an FCPXML... https://vimeo.com/415340397/f28424553d
Thank Jashmaenn
MartinThat looks amazing and is exactly what I need - something to just identify silence in Fcpx. Would you be willing to share your app?
Hello hamletcat... this app is now available on my website : (english : http://martingosset.com/boomerang_english/ and french with other app : http://martingosset.com/boomerang-app/)
Thank for your interest...
Martin
I have some insights as well as an issue.
I've gotten this script to work by running the ffmpeg command with an audio file exported from FCPX:
ffmpeg -i instructional.wav -af silencedetect=n=-30dB:d=0.2 -f s24le -ar 48000 -y /dev/null 2>&1 | tee silence.txt
Then running ./final-cut-it-out.js silence.txt works perfectly.
It works great for shorter clips, but when trying to do this with a 6+ minute clip, the cut points are skewed. I tried doing a test with a 30 second clip, a 6 minute gap, and the same 30 second clip. The tool works fine when dealing with the first 30 second clip, but the cut points are off by 0:12 frames for the second 30 second clip.
Any ideas on how to fix the code to account for this?
Fixed!
This article gave me some insight >> via Frame.io
I'm shooting in 23.98fps, and there's 3.6 seconds of drift between real time and timecode.
function moveToTimecode(timeInSeconds) {
delay(0.1);
/*
https://blog.frame.io/2017/07/17/timecode-and-frame-rates/#:~:text=In%20order%20to%20properly%20fit,a%20standalone%20HD%20video%20format.
Just to get an idea of the numbers, with a camera shooting in
Free Run at 23.98fps, the drift will also be 3.6 seconds after
one hour of real time so the timecode count will be 01:00:03:14.
(0.6 seconds x 24 frames a second = 14.4 frames).
3600 seconds in an hour >> timecode is 1:00:03:14
3600 realtime >> 3603.6 timecode seconds
*/
const driftRatio = 3603.6 / 3600
let realTime = parseFloat(timeInSeconds)
let timecodeSeconds = realTime / driftRatio
// convert timeInSeconds in decimal to timecode values.
let [seconds, deciseconds] = timecodeSeconds.toString().split(".");
let d = new Date(null);
d.setSeconds(parseInt(seconds));
let hourmindays = d.toISOString().substr(11, 8);
let decipart = (23.98 * parseFloat("0." + deciseconds))
.toString()
.split(".")[0]
.substr(0, 2);
if (decipart.length < 2) {
decipart = "0" + decipart;
}
let timecodekeystroke = (hourmindays + decipart).replace(/:/g, "");
console.log(
" timecodekeystroke ",
timeInSeconds,
hourmindays,
decipart,
timecodekeystroke
);
se.keystroke("p", { using: "control down" });
se.keystroke(timecodekeystroke);
se.keyCode(36); // Press Enter
}
Hey! Can you guys create a video tutorial from zero teaching how to use this code?
I don't know nothing about programming and I have been looking for this feature for a long time ago.
Unfortunately I haven't been able to use this code.
This worked excellently - thanks a bunch!!! You saved me a ton of time!
I just wanted to say thanks for posting this script—I messed around a bit with it, and using @rlau1115's modification, I was able to get it to cut gaps in my videos a lot quicker than I was doing by hand before!
I posted a blog entry showing more how I use it (with a step-by-step, since that's kind of lacking here): https://www.jeffgeerling.com/blog/2023/shaving-hours-my-workflow-trimming-silence-fcpx-and-applescript
@rlau1115 - I opened an issue for that over here: geerlingguy/final-cut-it-out#1
When I run the script, I get nothing in the array for console.log(JSON.stringify(silencePoints));
I tried adjusting the DB, with no luck. What do you think I'm doing wrong?
$ /usr/local/bin/ffmpeg -i $1 -af silencedetect=n=-50dB:d=1 -f s16le -y /dev/null 2>&1 | tee silence.txt
ffmpeg version 4.2.2 Copyright (c) 2000-2019 the FFmpeg developers
built with Apple clang version 11.0.0 (clang-1100.0.33.16)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.2.2 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags='-I/Library/Java/JavaVirtualMachines/adoptopenjdk-13.0.1.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-13.0.1.jdk/Contents/Home/include/darwin -fno-stack-check' --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-libsoxr --enable-videotoolbox --disable-libjack --disable-indev=jack
libavutil 56. 31.100 / 56. 31.100
libavcodec 58. 54.100 / 58. 54.100
libavformat 58. 29.100 / 58. 29.100
libavdevice 58. 8.100 / 58. 8.100
libavfilter 7. 57.100 / 7. 57.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 5.100 / 5. 5.100
libswresample 3. 5.100 / 3. 5.100
libpostproc 55. 5.100 / 55. 5.100
-af: No such file or directory
$ ./final-cut-it-out.js silence.txt
["silence.txt"]
Start Final Cut Pro and click the matching clip
[]
Deleting Silence