Skip to content

Instantly share code, notes, and snippets.

@jashmenn
Created September 21, 2017 11:44
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jashmenn/66f2806ae6da643a0bb16452629deee8 to your computer and use it in GitHub Desktop.
Save jashmenn/66f2806ae6da643a0bb16452629deee8 to your computer and use it in GitHub Desktop.
Remove Silence from Final Cut Pro clips, automatically, using ffmpeg timecodes and OSX JavaScript Automation - Demo: https://imgur.com/a/Zisav
#!/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
Copy link

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

@jashmenn
Copy link
Author

jashmenn commented Jan 8, 2020

@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.

@happyharis
Copy link

What are the options you have parsed? I have the same error.

@happyharis
Copy link

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 😁

@EnseignantLapro
Copy link

EnseignantLapro commented Apr 7, 2020

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

@EnseignantLapro
Copy link

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.

@elleffcruz
Copy link

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%

@JeanSemeur
Copy link

JeanSemeur commented May 4, 2020

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

@JeanSemeur
Copy link

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

@hamletcat
Copy link

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?

@JeanSemeur
Copy link

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?

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

@rlau1115
Copy link

rlau1115 commented Oct 17, 2020

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?

@rlau1115
Copy link

rlau1115 commented Oct 17, 2020

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
  }

@mathguimaraes
Copy link

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.

@mcbrineellis
Copy link

This worked excellently - thanks a bunch!!! You saved me a ton of time!

@geerlingguy
Copy link

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
Copy link

rlau1115 commented Jan 20, 2023 via email

@geerlingguy
Copy link

@rlau1115 - I opened an issue for that over here: geerlingguy/final-cut-it-out#1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment