Skip to content

Commit

Permalink
tests: add exhaustive tests of all timecodes
Browse files Browse the repository at this point in the history
  • Loading branch information
connerdouglass committed Mar 18, 2024
1 parent c7d1d34 commit cbb9cee
Show file tree
Hide file tree
Showing 12 changed files with 19,691,540 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/node_modules
/dist
/docs
/bin
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ npm install --save @spiretechnology/js-timecode
### Parse a timecode (drop frame)

```ts
import { Parse, Rate_23_976 } from '@spiretechnology/js-timecode';
import { Parse, Rate_29_97 } from '@spiretechnology/js-timecode';

const tc = Parse('00:01:02;23', Rate_23_976);
const tc = Parse('00:01:02;23', Rate_29_97);
tc.toString(); // => 00:01:02;23
tc.frame; // => 1509
tc.frame; // => 1881
```

### Parse a timecode (non-drop frame)
Expand Down Expand Up @@ -51,7 +51,7 @@ tc.frame; // => 1514

Drop frame timecodes skip the first 2 frames of each minute, unless the minute is a multiple of 10. This changes to the first 4 frames of each minute if the frame rate is 59.94.

For instance, in `23.976`, the timecode `00:00:59:23` is immediately followed by `00:01:00:02`. Two timecodes were dropped: `00:01:00:00` and `00:01:00:01`
For instance, in `29.97`, the timecode `00:00:59:29` is immediately followed by `00:01:00:02`. Two timecodes were dropped: `00:01:00:00` and `00:01:00:01`

Those dropped timecodes don't correspond to any actual frame number, and so we need to choose how to resolve those frames. The choice we have made with this library is to round up the next valid frame. If you try to parse `00:01:00:00`, the result will be rounded up to `00:01:00:02`, which is the next valid frame in the sequence.

Expand Down
19 changes: 19 additions & 0 deletions scripts/generate-timecodes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

BMXTIMECODE="./bin/bmxtimecode"
OUTDIR="./testdata"

generate_timecodes() {
local ratename="$1"
local fraction="$2"
local output="$3"

${BMXTIMECODE} --output "tc-drop" --rate "${fraction}" all | awk '{$1=$1};1' | sed 's/^[^:]*: //' > "${OUTDIR}/tc-all-${ratename}.txt"
}

generate_timecodes "23_976" "24000/1001"
generate_timecodes "24" "24"
generate_timecodes "29_97" "30000/1001"
generate_timecodes "30" "30"
generate_timecodes "59_94" "60000/1001"
generate_timecodes "60" "60"
25 changes: 15 additions & 10 deletions src/rate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ export function ParseRate(str: string): Rate | null {
}

export function RateFromFraction(num: number, den: number): Rate {
if (num === 24000 && den === 1001) {
return Rate_23_976;
} else if (num === 24 && den === 1) {
return Rate_24;
} else if (num === 30000 && den === 1001) {
return Rate_29_97;
} else if (num === 30 && den === 1) {
return Rate_30;
} else if (num === 60000 && den === 1001) {
return Rate_59_94;
} else if (num === 60 && den === 1) {
return Rate_60;
}

// Calculate the nominal frame rate (number of frames in a second without drops)
const nominal = num % den === 0 ? num / den : den - (num % den);

Expand Down Expand Up @@ -58,27 +72,18 @@ export function RateFromFraction(num: number, den: number): Rate {
};
}

// 23.976 has exactly 24 frames per second. However, the textual representation of timecodes using this rate
// skip two frames every minute, except when the minute is a multiple of 10. This is because 23.976 footage
// actually does display at a rate of 23.976 frames each second on televisions. To ensure that the first
// timecode in an hour of footage is 00:00:00;00 and the last timecode in that hour is 01:00:00;00, drop
// frame was invented. It is purely a matter of presentation.
export const Rate_23_976: Rate = {
nominal: 24,
drop: 2,
drop: 0,
num: 24000,
den: 1001,
};

// Standard 24 FPS, with no drop frame
export const Rate_24: Rate = {
nominal: 24,
drop: 0,
num: 24,
den: 1,
};

// Other formats...
export const Rate_30: Rate = {
nominal: 30,
drop: 0,
Expand Down
6 changes: 3 additions & 3 deletions src/timecode.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Parse } from './parse';
import { Rate_23_976, Rate_24, Rate_59_94 } from './rate';
import { Rate_24, Rate_29_97, Rate_59_94 } from './rate';
import { Timecode } from './timecode';

describe('testing 59.94 fps (DF) parse to frame count', () => {
Expand Down Expand Up @@ -89,9 +89,9 @@ describe('testing 24 fps (NDF) offsets', () => {

describe('readme example tests', () => {
test('parse a timecode (drop frame)', () => {
const tc = Parse('00:01:02;23', Rate_23_976);
const tc = Parse('00:01:02;23', Rate_29_97);
expect(tc.toString()).toBe('00:01:02;23');
expect(tc.frame).toBe(1509);
expect(tc.frame).toBe(1881);
});
test('parse a timecode (non-drop frame)', () => {
const tc = Parse('00:01:02:23', Rate_24);
Expand Down
74 changes: 74 additions & 0 deletions src/timecodes-all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import fs from 'fs';
import path from 'path';
import readline from 'readline';
import { Parse } from './parse';
import {
Rate,
Rate_23_976,
Rate_24,
Rate_29_97,
Rate_30,
Rate_59_94,
Rate_60,
} from './rate';
import { Timecode } from './timecode';

async function runTimecodesTest(
rate: Rate,
filename: string,
maxFrames?: number
) {
const fileStream = fs.createReadStream(
path.resolve(__dirname, '../testdata', filename)
);
const rl = readline.createInterface({
input: fileStream,
terminal: false,
});

let prevTimecode: Timecode | null = null;
let frameIndex = -1;
for await (const line of rl) {
frameIndex++;
if (maxFrames && frameIndex >= maxFrames) break;
if (line.trim() === '') continue;

// Frame index -> timecode string
const tcFromIndex = new Timecode(frameIndex, rate, rate.drop > 0);
expect(tcFromIndex.toString()).toBe(line);

// Timecode string -> frameIndex
const tcFromStr = Parse(line, rate);
expect(tcFromStr.frame).toBe(frameIndex);

// Compare to the previous timecode
if (prevTimecode != null) {
const prevPlusOne = prevTimecode.add(1);
expect(prevPlusOne.toString()).toBe(tcFromStr.toString());
expect(prevPlusOne.frame).toBe(frameIndex);
}

prevTimecode = tcFromStr;
}
}

describe('test all timecodes exhaustively', () => {
test('all timecodes - 23.976', async () => {
await runTimecodesTest(Rate_23_976, 'tc-all-23_976.txt', 10000);
});
test('all timecodes - 24', async () => {
await runTimecodesTest(Rate_24, 'tc-all-24.txt', 10000);
});
test('all timecodes - 29.97', async () => {
await runTimecodesTest(Rate_29_97, 'tc-all-29_97.txt', 10000);
});
test('all timecodes - 30', async () => {
await runTimecodesTest(Rate_30, 'tc-all-30.txt', 10000);
});
test('all timecodes - 59.94', async () => {
await runTimecodesTest(Rate_59_94, 'tc-all-59_94.txt', 10000);
});
test('all timecodes - 60', async () => {
await runTimecodesTest(Rate_60, 'tc-all-60.txt', 10000);
});
});
Loading

0 comments on commit cbb9cee

Please sign in to comment.