!7 module VideoSplicer > DynPrintLog { S inputFile, outputFile; S timestamps; S originalLength, cutLength; S ffmpegOptions = "", videoCodec = ""; int volumeThresholdPercent = 15; S minSilence = "0.2", leadIn = "0.2", leadOut = "0.2"; transient double profileSamplingInterval = 0.05; // 50 ms transient int audioWindowSize = iround(profileSamplingInterval*16000); transient int timeFieldMinWidth = 40; transient ImageSurface isPreview; transient JAmplitudeGraph graph; transient float[] profile; visual jHandleFileDrop(voidfunc(File f) { setField(inputFile := f2s(f)) }, northCenterAndSouthWithMargins( vstackWithSpacing( withLabel("Input video:", filePathInputWithBrowseButton(dm_textField inputFile(), onChoose := rThread loadVideo)), withLabel("Output video:", filePathInputWithBrowseButton(dm_textField outputFile())), centeredLine( jbutton("Load video", rThread loadVideo), jbutton("Find jump cuts", rThread autoJumpCuts), jhspacer(10), jbutton("Make video", rThread makeVideo)), jCenteredSection("Jump Cut Parameters", centeredLineWithSpacing(20, withLabelLeftAndRight("Volume threshold:", dm_spinner volumeThresholdPercent(0, 100), "%"), withLabelLeftAndRight("Shortest silence to cut:", jMinWidth(timeFieldMinWidth, dm_centeredTextField minSilence()), "s"), jline( withLabel("Lead-in/out:", jMinWidth(timeFieldMinWidth, dm_centeredTextField leadIn())), jlabel("/"), withLabelToTheRight(jMinWidth(timeFieldMinWidth, dm_centeredTextField leadOut()), "s")) ))), jhsplit(0.2, centerAndSouthWithMargin( jLiveValueSection(dm_calculatedLiveValue(S, () -> "Timestamps (" + countLines(timestamps) + ")"), dm_textArea("timestamps")), vstack( rightAlignedLine(withLabel("Total in:", dm_label("originalLength"))), rightAlignedLine(withLabel("Total out (est.):", dm_label("cutLength")))) ), northAndCenterWithMargin( jsection("Audio", jMinHeight(50, graph = setForeground(Color.red, swingNu(JAmplitudeGraph)))), hgridWithSpacing(jsection("Preview", jscroll_center(isPreview = jImageSurface())), super))), jvstackWithSpacing( ffmpegVersionPanel(), hgridWithSpacing(dm_textFieldWithLabel ffmpegOptions(), dm_textFieldWithLabel videoCodec())))); void loadVideo enter { temp dm_tempDisableAllButtons(); File f = newFile(inputFile); if (!fileExists(f)) ret with infoBox("File not found: " + f2s(f)); S id = md5(f2s(f)); File previewFile = prepareCacheProgramFile("preview-" + id + ".jpg"); File audioFile = prepareCacheProgramFile("preview-" + id + ".wav"); if (!fileExists(previewFile)) { print("Getting preview image..."); ffmpeg_getSingleFrame(f, previewFile, 0.0); print("Done"); } else print("Have preview image"); isPreview.setImageAndZoomToDisplay(loadImage2(previewFile)); if (!fileExists(audioFile)) { print("Extracting audio..."); ffmpeg_toMonoAudio_16k(f, audioFile); print("Done - " + fileInfo(audioFile)); } else print("Have audio"); print("Getting volume profile..."); profile = decodeWAVToMonoSamples_floatVolumeProfile(audioFile, audioWindowSize); print("Have volume profile (" + nEntries(l(profile)) + ")"); setField(originalLength := formatMinuteAndSeconds(iceil(l(profile)*profileSamplingInterval))); //printStruct(takeFirstOfFloatArray(100, profile)); graph.setValues(profile); } void makeVideo enter { temp dm_tempDisableAllButtons(); if (empty(outputFile)) ret with infoBox("Need output file path"); File in = newFile(inputFile), out = newFile(outputFile); if (empty(timestamps)) autoJumpCuts(); L ranges = parseTimestampRanges(timestamps); if (empty(ranges)) ret with infoBox("No timestamps"); print(+ranges); if (fileExists(out) && !confirmOKCancel("Overwrite " + fileName(out) + "?")) ret; temp tempInfoBox_noHide("Splicing video..."); backtickToConsole(ffmpegCmd() + " -y " + ffmpegOptions + " " + ffmpeg_argsForSplice_usingFile(in, out, ranges, beforeOut := empty(videoCodec) ? "" : "-vcodec " + videoCodec)); if (fileExists(out)) infoBox("Done splicing video!" + fileInfo(out)); else infoBox("Something went wrong..."); } void autoJumpCuts enter { if (profile == null) loadVideo(); if (profile == null) ret; L ranges = audio_findSpeechPartsFromVolumeProfile(profile, profileSamplingInterval, volumeThreshold := volumeThresholdPercent/100.0, minSilenceDuration := parseDouble(minSilence), pre := parseDouble(leadIn), post := parseDouble(leadOut)); setField(timestamps := formatTimestampRanges(ranges)); infoBox("Created " + nSlices(ranges) + ", ready to make video"); } start { shouldKeepTempFiles = () -> true; dm_watchField timestamps(r { pcall-silent { double len = totalLengthOfDoubleRanges(parseTimestampRanges(timestamps)); setField(cutLength := formatMinuteAndSeconds(iceil(len))); } }); dm_watchField inputFile(r { setField(outputFile := f2s(appendToBaseName(newFile(inputFile), "-cut"))) }); } }