!7 sclass VidFile { File file; double length = -1; transient float[] profile; 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); transient ReliableSingleThread rstScan = dm_rst(this, r scan); visualize { JComponent sup = super.visualize(); ret centerAndSouthWithMargins( jHandleMultiFileDrop(vf> onFileDrop, withCenteredTitle("Input videos (you can drag&drop files here):", withRightAlignedButtons(sup, tableDependentButton(table, "Remove selected", r removeSelected) ))), vstackWithSpacing( dm_ajc_parametersSection(), withLabel("Output video:", filePathInputWithBrowseButton(dm_textField('outputFile))), rightAlignedLine(fontSizePlus(3, jbutton("Make video", rThread makeVideo))) )); } void onFileDrop(L files) enter { print("Have file drop"); Set haveFiles = collectAsSet file(data()); addAll(map(listMinusSet(files, haveFiles), f -> nu VidFile(file := f))); } 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(); } void scan { bool change; for (VidFile v : clonedList()) pcall { if (v.profile == null || v.length < 0 && fileExists(v.file)) { print("Extracting audio from " + v.file); File audio = v.audioFile(); if (fileLength(audio) == 0) ffmpeg_toMonoAudio_16k(v.file, audio); // TODO: temp file for safety? v.profile = decodeWAVToMonoSamples_floatVolumeProfile(audio, audioWindowSize); v.length = l(v.profile)*profileSamplingInterval; set change; } } if (change) fireDataChanged(); } void makeVideo { while (rstScan.running()) sleep(10); // TODO } }