implement CMX3600 EDL import #1340

pull/2298/head
Mikael Finstad 10 months ago
parent a16f467e29
commit a7f6b34975
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -91,9 +91,9 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
},
},
{
label: esc(t('EDL (MPlayer)')),
label: esc(t('EDL')),
click() {
mainWindow.webContents.send('importEdlFile', 'mplayer');
mainWindow.webContents.send('importEdlFile', 'edl');
},
},
{

@ -0,0 +1,35 @@
export interface EDLEvent {
eventNumber: string;
reelNumber: string;
trackType: string;
transition: string;
sourceIn: string;
sourceOut: string;
recordIn: string;
recordOut: string;
}
export default function parseCmx3600(edlContent: string) {
const lines = edlContent.split('\n');
const events: EDLEvent[] = [];
for (const line of lines) {
if (/^\d+\s+/.test(line)) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 8) {
events.push({
eventNumber: parts[0]!,
reelNumber: parts[1]!,
trackType: parts[2]!,
transition: parts[3]!,
sourceIn: parts[4]!,
sourceOut: parts[5]!,
recordIn: parts[6]!,
recordOut: parts[7]!,
});
}
}
}
return { events };
}

@ -1,6 +1,7 @@
import { XMLParser } from 'fast-xml-parser';
import i18n from 'i18next';
import invariant from 'tiny-invariant';
import { Duration } from 'luxon';
import { parse as csvParse } from 'csv-parse/browser/esm/sync';
import { stringify as csvStringify } from 'csv-stringify/browser/esm/sync';
@ -10,6 +11,7 @@ import type { ICueSheet, ITrack } from 'cue-parser/lib/types';
import { formatDuration } from './util/duration';
import { invertSegments, sortSegments } from './segments';
import { GetFrameCount, Segment, SegmentBase } from './types';
import parseCmx3600 from './cmx3600';
export const getTimeFromFrameNum = (detectedFps: number, frameNum: number) => frameNum / detectedFps;
@ -160,6 +162,32 @@ export async function parseMplayerEdl(text: string) {
return out;
}
export async function parseEdlCmx3600(text: string, fps: number) {
const cmx = parseCmx3600(text);
const parseTimecode = (t: string) => {
const match = t.match(/^(\d+)[:;](\d+)[:;](\d+)[:;](\d+)$/);
invariant(match, `Invalid EDL line: ${t}`);
const hours = parseInt(match[1]!, 10);
const minutes = parseInt(match[2]!, 10);
const seconds = parseInt(match[3]!, 10);
const frames = parseInt(match[4]!, 10);
return Duration.fromObject({ hours, minutes, seconds: seconds + (frames / fps) }).as('seconds');
};
return cmx.events.map((event) => ({
start: parseTimecode(event.sourceIn),
end: parseTimecode(event.sourceOut),
name: event.eventNumber,
tags: { reel: event.reelNumber, trackType: event.trackType, transition: event.transition },
}));
}
export async function parseEdl(text: string, fps: number) {
if (text.startsWith('TITLE: ')) return parseEdlCmx3600(text, fps);
return parseMplayerEdl(text);
}
export function parseCuesheet(cuesheet: ICueSheet) {
// There are 75 such frames per second of audio.
// https://en.wikipedia.org/wiki/Cue_sheet_(computing)

@ -2,10 +2,10 @@ import JSON5 from 'json5';
import i18n from 'i18next';
import invariant from 'tiny-invariant';
import { parseSrtToSegments, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parseCutlist, parsePbf, parseMplayerEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
import { parseSrtToSegments, formatSrt, parseCuesheet, parseXmeml, parseFcpXml, parseCsv, parseCutlist, parsePbf, parseEdl, formatCsvHuman, formatTsv, formatCsvFrames, formatCsvSeconds, parseCsvTime, getFrameValParser, parseDvAnalyzerSummaryTxt } from './edlFormats';
import { askForYouTubeInput, showOpenDialog } from './dialogs';
import { getOutPath } from './util';
import { EdlExportType, EdlFileType, EdlImportType, Segment, StateSegment } from './types';
import { EdlExportType, EdlFileType, EdlImportType, GetFrameCount, Segment, StateSegment } from './types';
const { readFile, writeFile } = window.require('fs/promises');
const cueParser = window.require('cue-parser');
@ -13,68 +13,76 @@ const { basename } = window.require('path');
const { dialog } = window.require('@electron/remote');
export async function loadCsvSeconds(path: string) {
async function loadCsvSeconds(path: string) {
return parseCsv(await readFile(path, 'utf8'), parseCsvTime);
}
export async function loadCsvFrames(path: string, fps?: number) {
if (!fps) throw new Error('The loaded file has an unknown framerate');
async function loadCsvFrames(path: string, fps: number) {
return parseCsv(await readFile(path, 'utf8'), getFrameValParser(fps));
}
export async function loadCutlistSeconds(path: string) {
async function loadCutlistSeconds(path: string) {
return parseCutlist(await readFile(path, 'utf8'));
}
export async function loadXmeml(path: string) {
async function loadXmeml(path: string) {
return parseXmeml(await readFile(path, 'utf8'));
}
export async function loadFcpXml(path: string) {
async function loadFcpXml(path: string) {
return parseFcpXml(await readFile(path, 'utf8'));
}
export async function loadDvAnalyzerSummaryTxt(path: string) {
async function loadDvAnalyzerSummaryTxt(path: string) {
return parseDvAnalyzerSummaryTxt(await readFile(path, 'utf8'));
}
export async function loadPbf(path: string) {
async function loadPbf(path: string) {
return parsePbf(await readFile(path));
}
export async function loadMplayerEdl(path: string) {
return parseMplayerEdl(await readFile(path, 'utf8'));
async function loadEdl(path: string, fps: number) {
return parseEdl(await readFile(path, 'utf8'), fps);
}
export async function loadCue(path: string) {
async function loadCue(path: string) {
return parseCuesheet(cueParser.parse(path));
}
export async function loadSrt(path: string) {
async function loadSrt(path: string) {
return parseSrtToSegments(await readFile(path, 'utf8'));
}
export async function saveCsv(path: string, cutSegments) {
export async function saveCsv(path: string, cutSegments: Segment[]) {
await writeFile(path, await formatCsvSeconds(cutSegments));
}
export async function saveCsvHuman(path: string, cutSegments) {
export async function saveCsvHuman(path: string, cutSegments: Segment[]) {
await writeFile(path, await formatCsvHuman(cutSegments));
}
export async function saveCsvFrames({ path, cutSegments, getFrameCount }) {
export async function saveCsvFrames({ path, cutSegments, getFrameCount }: {
path: string,
cutSegments: Segment[],
getFrameCount: GetFrameCount,
}) {
await writeFile(path, await formatCsvFrames({ cutSegments, getFrameCount }));
}
export async function saveTsv(path: string, cutSegments) {
export async function saveTsv(path: string, cutSegments: Segment[]) {
await writeFile(path, await formatTsv(cutSegments));
}
export async function saveSrt(path: string, cutSegments) {
await writeFile(path, await formatSrt(cutSegments));
export async function saveSrt(path: string, cutSegments: Segment[]) {
await writeFile(path, formatSrt(cutSegments));
}
export async function saveLlcProject({ savePath, filePath, cutSegments }) {
export async function saveLlcProject({ savePath, filePath, cutSegments }: {
savePath: string,
filePath: string,
cutSegments: StateSegment[],
}) {
const projectData = {
version: 1,
mediaFileName: basename(filePath),
@ -99,14 +107,20 @@ export async function loadLlcProject(path: string) {
export async function readEdlFile({ type, path, fps }: { type: EdlFileType, path: string, fps?: number | undefined }) {
if (type === 'csv') return loadCsvSeconds(path);
if (type === 'csv-frames') return loadCsvFrames(path, fps);
if (type === 'csv-frames') {
invariant(fps != null, 'The loaded media has an unknown framerate');
return loadCsvFrames(path, fps);
}
if (type === 'cutlist') return loadCutlistSeconds(path);
if (type === 'xmeml') return loadXmeml(path);
if (type === 'fcpxml') return loadFcpXml(path);
if (type === 'dv-analyzer-summary-txt') return loadDvAnalyzerSummaryTxt(path);
if (type === 'cue') return loadCue(path);
if (type === 'pbf') return loadPbf(path);
if (type === 'mplayer') return loadMplayerEdl(path);
if (type === 'edl') {
invariant(fps != null, 'The loaded media has an unknown framerate');
return loadEdl(path, fps);
}
if (type === 'srt') return loadSrt(path);
if (type === 'llc') {
const project = await loadLlcProject(path);
@ -125,7 +139,7 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
else if (type === 'fcpxml') filters = [{ name: i18n.t('FCPXML files'), extensions: ['fcpxml'] }];
else if (type === 'cue') filters = [{ name: i18n.t('CUE files'), extensions: ['cue'] }];
else if (type === 'pbf') filters = [{ name: i18n.t('PBF files'), extensions: ['pbf'] }];
else if (type === 'mplayer') filters = [{ name: i18n.t('MPlayer EDL'), extensions: ['*'] }];
else if (type === 'edl') filters = [{ name: i18n.t('EDL'), extensions: ['*'] }];
else if (type === 'dv-analyzer-summary-txt') filters = [{ name: i18n.t('DV Analyzer Summary.txt'), extensions: ['txt'] }];
else if (type === 'srt') filters = [{ name: i18n.t('Subtitles (SRT)'), extensions: ['srt'] }];
else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }];
@ -137,7 +151,11 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps?
}
export async function exportEdlFile({ type, cutSegments, customOutDir, filePath, getFrameCount }: {
type: EdlExportType, cutSegments: Segment[], customOutDir?: string | undefined, filePath?: string | undefined, getFrameCount: (a: number) => number | undefined,
type: EdlExportType,
cutSegments: StateSegment[],
customOutDir?: string | undefined,
filePath?: string | undefined,
getFrameCount: GetFrameCount,
}) {
invariant(filePath != null);

@ -50,6 +50,10 @@ export default ({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, custo
return;
}
if (debouncedSaveOperation.filePath == null) {
return;
}
await saveLlcProject({ savePath: debouncedSaveOperation.projectFileSavePath, filePath: debouncedSaveOperation.filePath, cutSegments: debouncedSaveOperation.cutSegments });
lastSaveOperation.current = debouncedSaveOperation;
} catch (err) {

@ -72,7 +72,7 @@ export interface InverseCutSegment {
export type PlaybackMode = 'loop-segment-start-end' | 'loop-segment' | 'play-segment-once' | 'loop-selected-segments';
export type EdlFileType = 'csv' | 'csv-frames' | 'cutlist' | 'xmeml' | 'fcpxml' | 'dv-analyzer-summary-txt' | 'cue' | 'pbf' | 'mplayer' | 'srt' | 'llc';
export type EdlFileType = 'csv' | 'csv-frames' | 'cutlist' | 'xmeml' | 'fcpxml' | 'dv-analyzer-summary-txt' | 'cue' | 'pbf' | 'edl' | 'srt' | 'llc';
export type EdlImportType = 'youtube' | EdlFileType;

@ -506,7 +506,7 @@ export async function readDirRecursively(dirPath: string) {
export function getImportProjectType(filePath: string) {
if (filePath.endsWith('Summary.txt')) return 'dv-analyzer-summary-txt';
const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'mplayer', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' };
const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'edl', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' };
const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`));
if (!matchingExt) return undefined;
return edlFormatForExtension[matchingExt];

Loading…
Cancel
Save