Skip to content

Instantly share code, notes, and snippets.

@jashmenn
Created September 21, 2017 11:44
Show Gist options
  • 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);
}
}
@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