Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 34 additions & 18 deletions src/main/java/org/moormanity/smpte/timecode/FrameRate.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,81 +18,81 @@ public enum FrameRate {
* 24 fps
* (film, ATSC, 2k, 4k, 6k)
*/
_24("24 fps", 24, 1, 24, false, 0),
_24("24 fps", 24, 1, 24, false, 0),


/**
* 24.98 fps
* This frame rate is commonly used to facilitate transfers between PAL and NTSC video and film sources. It is mostly used to compensate for some error.
*/
_24_98("24.98 fps", 25000, 1001, 25, false, 0),
_24_98("24.98 fps", 25000, 1001, 25, false, 0),

/**
* 25 fps
* (PAL, used in Europe, Uruguay, Argentina, Australia), SECAM, DVB, ATSC)
*/
_25("25 fps", 25, 1, 25, false, 0),
_25("25 fps", 25, 1, 25, false, 0),

/**
* 29.97 fps (30p)
* (NTSC American System (US, Canada, Mexico, Colombia, etc.), ATSC, PAL-M (Brazil))
* (30 / 1.001) frame/sec
*/
_29_97("29.97 fps", 30000, 1001, 30, false, 0),
_29_97("29.97 fps", 30000, 1001, 30, false, 0),
/**
* 29.97 drop fps
*/
_29_97_drop("29.97 fps drop", 30000, 1001, 30, true, 2),
_29_97_drop("29.97 fps drop", 30000, 1001, 30, true, 2),

/**
* 30 fps
* (ATSC) This is the frame count of NTSC broadcast video. However, the actual frame rate or speed of the video format runs at 29.97 fps.
* This timecode clock does not run in realtime. It is slightly slower by 0.1%.
* ie: 1:00:00:00:00 (1 day/24 hours) at 30 fps is approx 1:00:00:00;02 in 29.97dfA
*/
_30("30", 30, 1, 30, false, 0),
_30("30", 30, 1, 30, false, 0),

/**
* 30 drop fps
*/
_30_drop("30 drop", 3, 1, 30, true, 2),
_30_drop("30 drop", 3, 1, 30, true, 2),

/**
* 47.952 (48p?)
* Double 23.976 fps
*/
_47_952("47.952 fps", 48000, 1001, 48, false, 0),
_47_952("47.952 fps", 48000, 1001, 48, false, 0),

/**
* 48 fps
* Double 24 fps
*/
_48("48 fps", 48, 1, 48, false, 0),
_48("48 fps", 48, 1, 48, false, 0),

/**
* 50 fps
* Double 25 fps\
*/
_50("50 fps", 50, 1, 50, false, 0),
_50("50 fps", 50, 1, 50, false, 0),

/**
* 59.94 fps
* Double 29.97 fps
* This video frame rate is supported by high definition cameras and is compatible with NTSC (29.97 fps).
*/
_59_94("59.94 fps", 60000, 1001, 60, false, 0),
_59_94("59.94 fps", 60000, 1001, 60, false, 0),

/**
* 59.94 fps drop
*/
_59_94_drop("59.94 fps drop", 60000, 1001, 60, true, 4),
_59_94_drop("59.94 fps drop", 60000, 1001, 60, true, 4),

/**
* 60 fps
* Double 30 fps
* This video frame rate is supported by many high definition cameras. However, the NTSC compatible 59.94 fps frame rate is much more common.
*/
_60("60 fps", 60, 1, 60, false, 0),
_60("60 fps", 60, 1, 60, false, 0),


/**
Expand All @@ -101,31 +101,31 @@ public enum FrameRate {
* See the description for 30 drop for more info.
* - Warning: This is not a video frame rate - it is a display rate only.
*/
_60_drop("60 fps drop", 60, 1, 60, true, 4),
_60_drop("60 fps drop", 60, 1, 60, true, 4),

/**
* 100 fps
* Double 50 fps / quadruple 25 fps
*/
_100("100 fps", 100, 1, 100, false, 0),
_100("100 fps", 100, 1, 100, false, 0),

/**
* 119.88 fps
* Double 59.94 fps / quadruple 29.97 fps
*/
_119_88("119.88 fps", 120_000, 1001, 120, false, 0),
_119_88("119.88 fps", 120_000, 1001, 120, false, 0),

/**
* 119.88 drop fps
* Double 59.94 drop fps / quadruple 29.97 drop fps
*/
_119_88_drop("119.88 fps", 120_000, 1001, 120, true, 4),
_119_88_drop("119.88 fps", 120_000, 1001, 120, true, 4),

/**
* 120 fps
* Double 60 fps / quadruple 30 fps
*/
_120("120 fps", 120, 1, 120, false, 0),
_120("120 fps", 120, 1, 120, false, 0),

/**
* 120 drop fps
Expand All @@ -146,4 +146,20 @@ public enum FrameRate {
private final boolean isDropFrameMode;
private final int numberOfFramesToDropInOneMinute;

public double frameRateForElapsedFramesCalculation() {
switch (this) {
case _29_97_drop:
return 29.97;
case _59_94_drop:
return 59.94;
case _60_drop:
return 59.94;
case _119_88_drop:
return 119.88;
case _120_drop:
return 119.88;
default:
return getNumberOfElapsedFramesThatCompriseOneSecond();
}
}
}
39 changes: 20 additions & 19 deletions src/main/java/org/moormanity/smpte/timecode/TimecodeOperations.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,41 +43,42 @@ public static String toTimecodeString(@NonNull TimecodeRecord a) {
a.getFrames());
}

public static TimecodeRecord fromElapsedFrames(int frameNumber, @NonNull FrameRate frameRate) {
if (frameNumber < 0) {
throw new IllegalArgumentException("frameNumber must be positive: " + frameNumber);
public static TimecodeRecord fromElapsedFrames(int elapsedFrames, @NonNull FrameRate frameRate) {
if (elapsedFrames < 0) {
throw new IllegalArgumentException("frameNumber must be positive: " + elapsedFrames);
}
int inElapsedFrames;
int adjustedElapsedFrames;
// adjust for dropFrame

// modify input elapsed frame count in the case of a drop rate
int framesPer10Minutes = frameRate.getNumberOfElapsedFramesThatCompriseOneSecond() * 600;
int d = frameNumber / framesPer10Minutes;
int m = frameNumber % framesPer10Minutes;
double framesPer10Minutes = frameRate.frameRateForElapsedFramesCalculation() * 600;
int d = (int) (elapsedFrames / framesPer10Minutes);
int m = (int) (elapsedFrames % framesPer10Minutes);
// don't allow negative numbers
int f = Math.max(0, m - frameRate.getNumberOfFramesToDropInOneMinute());

int part1 = 9 * frameRate.getNumberOfFramesToDropInOneMinute() * d;
int part2 = frameRate.getNumberOfFramesToDropInOneMinute() *
(f / ((framesPer10Minutes - frameRate.getNumberOfFramesToDropInOneMinute()) / 10));
inElapsedFrames = frameNumber + part1 + part2;
int part2 = (frameRate.getNumberOfFramesToDropInOneMinute() *
(int)(f / ((framesPer10Minutes - frameRate.getNumberOfFramesToDropInOneMinute()) / 10)));
adjustedElapsedFrames = elapsedFrames + part1 + part2;


int logicalFps = frameRate.getNumberOfElapsedFramesThatCompriseOneSecond();
int frames = inElapsedFrames % logicalFps;
int seconds = (inElapsedFrames / logicalFps) % 60;
int minutes = (inElapsedFrames / (logicalFps * 60)) % 60;
int hours = inElapsedFrames / (logicalFps * 3600) % 24;
int frames = adjustedElapsedFrames % logicalFps;
int seconds = (adjustedElapsedFrames / logicalFps) % 60;
int minutes = (adjustedElapsedFrames / (logicalFps * 60)) % 60;
int hours = adjustedElapsedFrames / (logicalFps * 3600) % 24;
return new TimecodeRecord(hours, minutes, seconds, frames, frameRate);
}

public static int toElapsedFrameCount(@NonNull TimecodeRecord a) {

int totalMinutes = (60 * a.getHours()) + a.getMinutes();
int logicalFps = a.getFrameRate().getNumberOfElapsedFramesThatCompriseOneSecond();
int base = (logicalFps * 60 * 60 * a.getHours())
+ (logicalFps * 60 * a.getMinutes())
+ (logicalFps * a.getSeconds())
+ a.getFrames();
int frameRateForElapsedFramesCalculation = a.getFrameRate().getNumberOfElapsedFramesThatCompriseOneSecond();
int base = (frameRateForElapsedFramesCalculation * 60 * 60 * a.getHours())
+ (frameRateForElapsedFramesCalculation * 60 * a.getMinutes())
+ (frameRateForElapsedFramesCalculation * a.getSeconds())
+ a.getFrames();
int dropOffset = a.getFrameRate().getNumberOfFramesToDropInOneMinute() * (totalMinutes - (totalMinutes / 10));
return base - dropOffset;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

import org.junit.jupiter.api.Test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;

public class TimecodeOperationsTest {

@Test
public void verifyFramesVsTimecode() {
verifyElapsedFramesVsTimecodeString("00:01:59;29", FrameRate._29_97_drop, 3597);

verifyElapsedFramesVsTimecodeString("00:10:00;00", FrameRate._29_97_drop, 17982);

verifyElapsedFramesVsTimecodeString("00:10:00;00", FrameRate._59_94_drop, 17982 * 2);
verifyElapsedFramesVsTimecodeString("10:00:00;00", FrameRate._29_97_drop, 1078920);
verifyElapsedFramesVsTimecodeString("10:00:00;00", FrameRate._59_94_drop, 1078920 * 2);
verifyElapsedFramesVsTimecodeString("00:01:59;29", FrameRate._29_97_drop, 3597);
verifyElapsedFramesVsTimecodeString("00:01:59;59", FrameRate._59_94_drop, 3597 * 2 + 1);

verifyElapsedFramesVsTimecodeString("00:10:00:00", FrameRate._25, 15000);
Expand All @@ -37,7 +43,7 @@ public void testAdd() {
public void testSubtract() {
TimecodeRecord a = TimecodeOperations.fromTimecodeString("23:30:00;00", FrameRate._29_97_drop);
TimecodeRecord b = TimecodeOperations.fromTimecodeString("01:00:00;00", FrameRate._29_97_drop);
TimecodeRecord expected = TimecodeOperations.fromTimecodeString("22:29:59;28", FrameRate._29_97_drop);
TimecodeRecord expected = TimecodeOperations.fromTimecodeString("22:30:00;00", FrameRate._29_97_drop);
assertEquals(
expected,
TimecodeOperations.subtract(a,b)
Expand Down Expand Up @@ -71,18 +77,60 @@ public void timecodeStringFrameMustBeValid() {
assertThrows(IllegalArgumentException.class, ()-> TimecodeOperations.fromTimecodeString("01:00:00;99", FrameRate._29_97_drop));
}


@Test
public void testVsPythonTimecodePackage() throws IOException {

BufferedReader reader = new BufferedReader(new FileReader(getClass().getResource("/test-case.csv").getFile()));
String line = null;
reader.readLine(); //skip header
while ((line = reader.readLine()) != null) {

String parts[] = line.split(",");
int frames = Integer.parseInt(parts[0]);
String _23_976 = parts[1];
String _23_98 = parts[2];
String _24 = parts[3];
String _25 = parts[4];
String _29_97 = parts[5];
String _30 = parts[6];
String _50 = parts[7];
String _59_94 = parts[8];
String _60 = parts[9];
String _29_97_non_drop = parts[10];
String _59_94_non_drop = parts[11];

verifyElapsedFramesVsTimecodeString(_23_976, FrameRate._23_976,frames);
verifyElapsedFramesVsTimecodeString(_24, FrameRate._24,frames);
verifyElapsedFramesVsTimecodeString(_25, FrameRate._25,frames);
verifyElapsedFramesVsTimecodeString(_29_97, FrameRate._29_97_drop,frames);
verifyElapsedFramesVsTimecodeString(_30, FrameRate._30,frames);
verifyElapsedFramesVsTimecodeString(_50, FrameRate._50,frames);
verifyElapsedFramesVsTimecodeString(_59_94, FrameRate._59_94_drop,frames);
verifyElapsedFramesVsTimecodeString(_60, FrameRate._60,frames);
verifyElapsedFramesVsTimecodeString(_29_97_non_drop, FrameRate._29_97,frames);
verifyElapsedFramesVsTimecodeString(_59_94_non_drop, FrameRate._59_94,frames);

}

}

private void verifyElapsedFramesVsTimecodeString(String timecodeString, FrameRate frameRate, int totalElapsedFrames) {
assertEquals(
totalElapsedFrames,
TimecodeOperations.toElapsedFrameCount(
TimecodeOperations.fromTimecodeString(timecodeString, frameRate)
)
),
timecodeString + " , " + frameRate + " , " + totalElapsedFrames
);

assertEquals(
timecodeString,
TimecodeOperations.toTimecodeString(
TimecodeOperations.fromElapsedFrames(totalElapsedFrames, frameRate)
));
),
timecodeString + " , " + frameRate + " , " + totalElapsedFrames

);
}
}
Loading