!7 sclass VidFile { File file; double length = -1; transient float[] profile; transient L ranges; S id() { ret md5FromFilePathSizeAndDate(file); } File audioFile() { ret prepareCacheProgramFile("preview-" + id() + ".wav"); } } module AJCBatch > DynObjectTable { S outputFile; 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); S originalLength, cutLength; transient ReliableSingleThread rstScan = dm_rst(this, r scan); visualize { JComponent sup = super.visualize(); ret centerAndSouthWithMargins( jHandleMultiFileDrop(vf> onFileDrop, withCenteredTitle("Input videos to jump-cut & concatenate (you can drag&drop files here):", withRightAlignedButtons(sup, tableDependentButton(table, "Move up", r moveUp), tableDependentButton(table, "Move down", r moveDown), tableDependentButton(table, "Remove selected", r removeSelected) ))), vstackWithSpacing( centeredLine(withLabel("Total input duration:", dm_boldLabel('originalLength))), dm_ajc_parametersSection(), centeredLine(withLabel("Total output duration (est.):", dm_boldLabel('cutLength))), withLabel("Output video:", filePathInputWithBrowseButton(dm_textField('outputFile))), rightAlignedLine(fontSizePlus(3, jbutton("Make video", rThread makeVideo))), ffmpegVersionPanel() )); } void onFileDrop(L files) enter { print("Have file drop"); Set haveFiles = collectAsSet file(data()); addAll(map(listMinusSet(files, haveFiles), f -> nu VidFile(file := f))); rstScan.trigger(); } start { itemToMap = func(VidFile v) -> Map { litorderedmap( "Video" := fileName(v.file), "Folder" := dirPath(v.file), "Duration" := !fileExists(v.file) ? "File not found" : v.length < 0 ? "[calculating]" : formatMinuteAndSeconds(iceil(v.length)) ) }; rstScan.trigger(); dm_watchFields(allNonStaticNonTransientFields(AJCParameters), r recut); } void recut { for (VidFile v : clonedList()) v.ranges = null; rstScan.trigger(); } void scan { bool change; double lenIn = 0, lenOut = 0; for (VidFile v : clonedList()) pcall { if (v.ranges == null || v.length < 0 && fileExists(v.file)) { File audio = v.audioFile(); if (fileLength(audio) == 0) { print("Extracting audio from " + v.file); ffmpeg_toMonoAudio_16k(v.file, audio); // TODO: temp file for safety? v.profile = null; } if (v.profile == null) v.profile = decodeWAVToMonoSamples_floatVolumeProfile(audio, audioWindowSize); v.length = l(v.profile)*profileSamplingInterval; v.ranges = ajc_findSpeechPartsFromVolumeProfile(v.profile, shallowCloneToClass AJCParameters(module())); set change; } lenIn += v.length; lenOut += totalLengthOfDoubleRanges(v.ranges); } setField(originalLength := formatMinuteAndSeconds(iceil(lenIn)); setField(cutLength := formatMinuteAndSeconds(iceil(lenOut)); if (change) fireDataChanged(); } void makeVideo { temp dm_tempDisableAllButtons(); while (rstScan.running()) sleep(10); if (emptyAfterTrim(outputFile)) ret with infoBox("Need output file path"); File out = newFile(trim(outputFile)); L l = clonedList(); if (empty(l)) ret with infoBox("No input files"); if (fileExists(out) && !confirmOKCancel("Overwrite " + fileName(out) + "?")) ret; L files = collect file(l); LL ranges = collect ranges(l); temp tempInfoBox_noHide("Splicing video..."); backtickToConsole(ffmpegCmd() + " -y " + ffmpeg_argsForSplice_multipleInputVideos(files, out, ranges)); if (fileExists(out)) infoBox("Done splicing video!" + fileInfo(out)); else infoBox("Something went wrong..."); } void moveUp { L indices = selectedIndices(); int i = first(indices), j = last(indices)+1; if (i <= 0) ret; VidFile v = data.get(i-1); remove(v); add(j-1, v); new L sel; for (int k = i; k < j; k++) sel.add(k-1); selectTableRows(table, asIntArray(sel)); } void moveDown { L indices = selectedIndices(); int i = first(indices), j = last(indices)+1; if (j >= l(data)) ret; VidFile v = data.get(j); remove(v); add(i, v); new L sel; for (int k = i; k < j; k++) sel.add(k+1); selectTableRows(table, asIntArray(sel)); } }