All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.glookast.commons.timecode.AbstractTimecode Maven / Gradle / Ivy

There is a newer version: 2.0.3
Show newest version
package com.glookast.commons.timecode;

import java.io.Serializable;

public abstract class AbstractTimecode implements Serializable
{
    private static final long serialVersionUID = 1L;

    protected int timecodeBase;
    protected boolean dropFrame;
    protected long frameNumber;

    protected int adjustmentPerMinute;
    protected int framesPerMinute;
    protected int framesPerMinuteDropFrame;
    protected int framesPerTenMinutes;

    protected int hours;
    protected int minutes;
    protected int seconds;
    protected int frames;

    public AbstractTimecode()
    {
        this(0);
    }

    public AbstractTimecode(int timecodeBase)
    {
        this(timecodeBase, false);
    }

    public AbstractTimecode(int timecodeBase, boolean dropFrame)
    {
        if (timecodeBase < 0) {
            timecodeBase = 0;
        }

        this.timecodeBase = timecodeBase;

        innerSetDropFrame(dropFrame);
    }

    public AbstractTimecode(int timecodeBase, long frameNumber)
    {
        this(timecodeBase, false);
        innerSetFrameNumber(frameNumber);
    }

    public AbstractTimecode(int timecodeBase, long frameNumber, boolean dropFrame)
    {
        this(timecodeBase, dropFrame);
        innerSetFrameNumber(frameNumber);
    }

    public AbstractTimecode(int timecodeBase, int hours, int minutes, int seconds, int frames)
    {
        this(timecodeBase, false);
        innerSetHMSF(hours, minutes, seconds, frames);
    }

    public AbstractTimecode(int timecodeBase, int hours, int minutes, int seconds, int frames, boolean dropFrame)
    {
        this(timecodeBase, dropFrame);
        innerSetHMSF(hours, minutes, seconds, frames);
    }

    public AbstractTimecode(AbstractTimecode timecode)
    {
        this(timecode.getTimecodeBase(), timecode.isDropFrame());
        innerSetFrameNumber(timecode.getFrameNumber());
    }

    /**
     * Returns whether this timecode represents a valid / initialized.
     *
     * @return isValid
     */
    public boolean isValid()
    {
        return timecodeBase != 0;
    }

    /**
     * Returns whether this timecode represents a invalid / uninitialized / null timecode
     *
     * @return isInvalid
     */
    public boolean isInvalid()
    {
        return timecodeBase == 0;
    }

    public int getTimecodeBase()
    {
        return timecodeBase;
    }

    public boolean isDropFrame()
    {
        return dropFrame;
    }

    protected void innerSetDropFrame(boolean dropFrame)
    {
        if (timecodeBase == 0 || timecodeBase / 30 * 30 != timecodeBase) {
            dropFrame = false;
        }

        this.dropFrame = dropFrame;
        adjustmentPerMinute = (this.dropFrame) ? (timecodeBase / 15) : 0;
        framesPerMinute = 60 * timecodeBase;
        framesPerMinuteDropFrame = framesPerMinute - adjustmentPerMinute;
        framesPerTenMinutes = framesPerMinute + 9 * framesPerMinuteDropFrame;

        this.innerSetHMSF(this.hours, this.minutes, this.seconds, this.frames);
    }

    /**
     * Gets the frame number for current timecode
     *
     * @return Frame number
     */
    public long getFrameNumber()
    {
        return frameNumber;
    }

    protected void innerSetFrameNumber(long frameNumber)
    {
        this.frameNumber = frameNumber;
        calculateHMSF();
    }

    protected int limit(int value, int min, int max)
    {
        return (value < min) ? min : ((value > max) ? max : value);
    }

    protected int limitFrames(int frames)
    {
        return limit(frames, (dropFrame && (minutes % 10) != 0 && ((seconds % 60) == 0)) ? adjustmentPerMinute : 0, timecodeBase - 1);
    }

    protected void innerSetHMSF(int hours, int minutes, int seconds, int frames)
    {
        this.hours = limit(hours, 0, Integer.MAX_VALUE);
        this.minutes = limit(minutes, 0, 59);
        this.seconds = limit(seconds, 0, 59);
        this.frames = limitFrames(frames);
        calculateFrameNumber();
    }

    public int getHours()
    {
        return hours;
    }

    public int getMinutes()
    {
        return minutes;
    }

    public int getSeconds()
    {
        return seconds;
    }

    /**
     * Gets the frames component of the timecode
     *
     * @return frames
     */
    public int getFrames()
    {
        return frames;
    }

    protected void calculateFrameNumber()
    {
        int tenMinutes = hours * 6 + minutes / 10;
        int minutes = this.minutes % 10;

        frameNumber = tenMinutes * framesPerTenMinutes +
                      minutes * framesPerMinuteDropFrame +
                      seconds * timecodeBase +
                      frames;

        calculateHMSF();
    }

    protected void calculateHMSF()
    {
        if (timecodeBase == 0) {
            frameNumber = 0;
            frames = 0;
            seconds = 0;
            minutes = 0;
            hours = 0;
            return;
        }

        frameNumber = truncateFrameNumber(frameNumber);

        long frameNumber = this.frameNumber;

        if (dropFrame) {
            long tenMinutesFrames = frameNumber / framesPerTenMinutes;
            long remainingMinutes = ((frameNumber % framesPerTenMinutes) - adjustmentPerMinute) / framesPerMinuteDropFrame;
            long dropIncidents = 9 * tenMinutesFrames + remainingMinutes;
            frameNumber += adjustmentPerMinute * dropIncidents;
        }

        frames = (int) (frameNumber % timecodeBase);
        frameNumber = (frameNumber / timecodeBase);
        seconds = (int) (frameNumber % 60);
        frameNumber = (frameNumber / 60);
        minutes = (int) (frameNumber % 60);
        hours = (int) (frameNumber / 60);
    }

    protected abstract long truncateFrameNumber(long value);

    public enum StringType
    {
        STORAGE,
        NORMAL,
        NO_FRAMES,
        MILLISECONDS,
        SMPTE_HIGH_FRAME_RATE,
        SMPTE_ST_12M_BINARY_CODED_DECIMALS,
        SMPTE_ST_258
    }

    @Override
    public String toString()
    {
        return toString(this, StringType.NORMAL);
    }

    public String toString(StringType stringType)
    {
        return toString(this, stringType);
    }

    public static String toString(AbstractTimecode timecode, StringType stringType)
    {
        if (timecode == null || timecode.isInvalid()) {
            switch (stringType) {
                case STORAGE:
                    return "null-timecode";
                case MILLISECONDS:
                    return "--:--:--.---";
                case NORMAL:
                case NO_FRAMES:
                case SMPTE_HIGH_FRAME_RATE:
                case SMPTE_ST_258:
                    return "--:--:--:--";
                case SMPTE_ST_12M_BINARY_CODED_DECIMALS:
                    return "FFFFFFFF";
                default:
                    throw new IllegalArgumentException("Unknown String type '" + stringType.toString() + "'");
            }
        } else {
            int correctedFrames = timecode.frames;
            boolean firstField = true;
            if (timecode.timecodeBase >= 40) {
                correctedFrames /= 2;
                firstField = timecode.frames % 2 == 0;
            }

            switch (stringType) {
                case STORAGE:
                    return String.format("%02d:%02d:%02d%c%02d/%d", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ';' : ':'), timecode.frames, timecode.timecodeBase);
                case NORMAL:
                    return String.format("%02d:%02d:%02d%c%02d", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ';' : ':'), timecode.frames);
                case NO_FRAMES:
                    return String.format("%02d:%02d:%02d%c--", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ';' : ':'));
                case MILLISECONDS:
                    return String.format("%02d:%02d:%02d.%03d", timecode.hours, timecode.minutes, timecode.seconds, Math.round(timecode.frames * 1000.0 / timecode.timecodeBase));
                case SMPTE_ST_12M_BINARY_CODED_DECIMALS:
                    long value = 0;
                    value |= timecode.hours % 10 + ((timecode.hours / 10) << 4);
                    value |= (timecode.minutes % 10 + ((timecode.minutes / 10) << 4)) << 8;
                    value |= (timecode.seconds % 10 + ((timecode.seconds / 10) << 4)) << 16;
                    value |= (correctedFrames % 10 + ((correctedFrames / 10) << 4)) << 24;
                    if (timecode.dropFrame) {
                        value |= 0x40000000;
                    }
                    if (!firstField) {
                        if (timecode.timecodeBase == 48 || timecode.timecodeBase == 60) {
                            value |= 0x00800000;
                        } else if (timecode.timecodeBase == 50) {
                            value |= 0x00000080;
                        }
                    }
                    return String.format("%08X", value);
                case SMPTE_ST_258:
                case SMPTE_HIGH_FRAME_RATE:
                    if (timecode.timecodeBase < 40) {
                        return String.format("%02d:%02d:%02d%c%02d", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ';' : ':'), timecode.frames);
                    } else {
                        if (stringType == StringType.SMPTE_ST_258) {
                            if (firstField) {
                                return String.format("%02d:%02d:%02d%c%02d", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ',' : '.'), correctedFrames);
                            } else {
                                return String.format("%02d:%02d:%02d%c%02d", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ';' : ':'), correctedFrames);
                            }
                        } else {
                            return String.format("%02d:%02d:%02d%c%02d.%d", timecode.hours, timecode.minutes, timecode.seconds, ((timecode.dropFrame) ? ';' : ':'), correctedFrames, (firstField ? 0 : 1));
                        }
                    }
                default:
                    throw new UnsupportedOperationException("Timecode.toString() with StringType '" + stringType + "' not implemented");
            }
        }
    }

    protected AbstractTimecode parse(String timecode)
    {
        if (timecode == null || timecode.contains("null") || timecode.contains("--:--:--") || timecode.contains("FFFFFFFF")) {
            return this;
        }

        try {
            String hmsf;
            int timecodeBase;
            int hours;
            int minutes;
            int seconds;
            int frames;
            boolean dropFrame;

            String[] parts = timecode.split("/");

            if (parts.length == 2) {
                hmsf = parts[0];
                dropFrame = hmsf.contains(";");
                timecodeBase = Integer.valueOf(parts[1]);
            } else {
                // supporting the old timecode storage format
                parts = timecode.split(";");

                if (parts.length == 4) {
                    hmsf = parts[0];
                    int numerator = Integer.valueOf(parts[1]);
                    int denominator = Integer.valueOf(parts[2]);
                    dropFrame = Boolean.valueOf(parts[3]);
                    timecodeBase = (numerator + (denominator / 2)) / denominator;
                } else {
                    // if not old format then just parse it regularly and assume timecode base of 30 / 60 / 120 / etc;
                    parts = timecode.split("[:;.,]");
                    if (parts.length == 4) {
                        hmsf = timecode;
                        dropFrame = hmsf.contains(";");
                        frames = Integer.valueOf(parts[3]);
                        timecodeBase = 30;
                        while (timecodeBase < frames) {
                            timecodeBase *= 2;
                        }
                    } else {
                        throw new IllegalArgumentException(timecode + " is not a parsable timecode");
                    }
                }
            }

            parts = hmsf.split("[:;.,]");
            if (parts.length != 4) {
                throw new IllegalArgumentException(timecode + " is not a parsable timecode");
            }

            hours = Integer.valueOf(parts[0]);
            minutes = Integer.valueOf(parts[1]);
            seconds = Integer.valueOf(parts[2]);
            frames = Integer.valueOf(parts[3]);

            return init(timecodeBase, hours, minutes, seconds, frames, dropFrame);
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException(timecode + " is not a parsable timecode");
        }
    }

    protected AbstractTimecode parse(String timecode, int timecodeBase, StringType stringType)
    {
        if (timecodeBase <= 0 || timecode == null || timecode.contains("null") || timecode.contains("--:--:--") || timecode.contains("FFFFFFFF")) {
            return this;
        }

        String[] parts;
        String temp = timecode;

        int hours;
        int minutes;
        int seconds;
        int frames;
        boolean dropFrame;

        try {
            switch (stringType) {
                case STORAGE:
                    parts = timecode.split("/");
                    if (parts.length != 2) {
                        throw new IllegalArgumentException(timecode + " is not a valid format for string type '" + stringType + "'");
                    }
                    temp = parts[0];
                case NORMAL:
                case NO_FRAMES:
                case MILLISECONDS:
                    parts = temp.split("[:;.]");
                    if (parts.length != 4) {
                        throw new IllegalArgumentException(timecode + " is not a valid format for string type '" + stringType + "'");
                    }
                    hours = Integer.valueOf(parts[0]);
                    minutes = Integer.valueOf(parts[1]);
                    seconds = Integer.valueOf(parts[2]);
                    switch (stringType) {
                        case STORAGE:
                        case NORMAL:
                            frames = Integer.valueOf(parts[3]);
                            break;
                        case MILLISECONDS:
                            frames = Math.round(Integer.valueOf(parts[3], 10) * timecodeBase / 1000.0f);
                            break;
                        default:
                            frames = 0;
                            break;
                    }
                    dropFrame = temp.contains(";");
                    break;
                case SMPTE_ST_12M_BINARY_CODED_DECIMALS:
                    long value = Long.parseLong(timecode, 16);
                    hours = (int) ((((value & 0x00000030) >> 4) * 10) + (value & 0x0000000F));
                    minutes = (int) ((((value & 0x00007000) >> 12) * 10) + ((value & 0x00000F00) >> 8));
                    seconds = (int) ((((value & 0x00700000) >> 20) * 10) + ((value & 0x000F0000) >> 16));
                    frames = (int) ((((value & 0x30000000) >> 28) * 10) + ((value & 0x0F000000) >> 24));
                    dropFrame = ((value & 0x40000000) != 0);

                    if (timecodeBase >= 40) {
                        frames *= 2;

                        if (timecodeBase == 48 || timecodeBase == 60) {
                            if ((value & 0x00800000) != 0) {
                                frames += 1;
                            }
                        } else if (timecodeBase == 50) {
                            if ((value & 0x00000080) != 0) {
                                frames += 1;
                            }
                        }
                    }
                    break;
                case SMPTE_ST_258:
                    parts = timecode.split("[:;.,]");
                    if (parts.length != 4) {
                        throw new IllegalArgumentException(timecode + " is not a valid format for string type '" + stringType + "'");
                    }

                    hours = Integer.valueOf(parts[0]);
                    minutes = Integer.valueOf(parts[1]);
                    seconds = Integer.valueOf(parts[2]);
                    frames = Integer.valueOf(parts[3]);
                    dropFrame = timecode.contains(";") || timecode.contains(",");

                    if (timecodeBase >= 40) {
                        frames *= 2;

                        boolean firstField = timecode.contains(",") || timecode.contains(".");

                        if (!firstField) {
                            frames += 1;
                        }
                    }
                    break;
                case SMPTE_HIGH_FRAME_RATE:
                    parts = timecode.split("[:;.]");
                    if (parts.length != 4 && parts.length != 5) {
                        throw new IllegalArgumentException(timecode + " is not a valid format for string type '" + stringType + "'");
                    }

                    hours = Integer.valueOf(parts[0]);
                    minutes = Integer.valueOf(parts[1]);
                    seconds = Integer.valueOf(parts[2]);
                    frames = Integer.valueOf(parts[3]);
                    dropFrame = timecode.contains(";");

                    if (timecodeBase >= 40) {
                        frames *= 2;

                        boolean firstField = parts.length == 5 && Integer.valueOf(parts[4]) == 0;

                        if (!firstField) {
                            frames += 1;
                        }
                    }
                    break;
                default:
                    throw new UnsupportedOperationException("Timecode.valueOf() with StringType '" + stringType + "' not implemented");
            }
        } catch (NumberFormatException ex) {
            throw new IllegalArgumentException(timecode + " is not a valid format for string type '" + stringType + "'");
        }

        return init(timecodeBase, hours, minutes, seconds, frames, dropFrame);
    }

    private AbstractTimecode init(int timecodeBase, int hours, int minutes, int seconds, int frames, boolean dropFrame)
    {
        if (timecodeBase < 0) {
            timecodeBase = 0;
        }

        this.timecodeBase = timecodeBase;
        this.hours = hours;
        this.minutes = minutes;
        this.seconds = seconds;
        this.frames = frames;

        this.innerSetDropFrame(dropFrame);

        return this;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final AbstractTimecode other = (AbstractTimecode) obj;
        if (timecodeBase != other.timecodeBase) {
            return false;
        }
        if (dropFrame != other.dropFrame) {
            return false;
        }
        if (frameNumber != other.frameNumber) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + this.timecodeBase;
        hash = 31 * hash + (this.dropFrame ? 1 : 0);
        hash = 31 * hash + (int) (this.frameNumber ^ (this.frameNumber >>> 32));
        return hash;
    }

    public static int compare(AbstractTimecode t1, AbstractTimecode t2)
    {
        if (t1 == null && t2 == null) {
            return 0;
        }

        if (Timecode.isInvalid(t1)) {
            return -1;
        } else if (Timecode.isInvalid(t2)) {
            return 1;
        }

        int s1 = t1.getHours() * 3600 + t1.getMinutes() * 60 + t1.getSeconds();
        int s2 = t2.getHours() * 3600 + t2.getMinutes() * 60 + t2.getSeconds();

        if (s1 < s2) {
            return -1;
        } else if (s1 > s2) {
            return 1;
        }

        return t1.getFrames() - t2.getFrames();
    }

    /**
     * Calculates duration between given inPoint and outPoint.
     * In case outPoint does not have the same Timecode base and/or dropFrame flag
     * it will convert it to the same Timecode base and dropFrame flag of the inPoint
     *
     * @param inPoint
     * @param outPoint
     * @return duration
     */
    public static TimecodeDuration calculateDuration(Timecode inPoint, Timecode outPoint)
    {
        if (!inPoint.isCompatible(outPoint)) {
            MutableTimecode mutableTimecode = new MutableTimecode(outPoint);
            mutableTimecode.setTimecodeBase(inPoint.getTimecodeBase());
            mutableTimecode.setDropFrame(inPoint.isDropFrame());
            outPoint = new Timecode(mutableTimecode);
        }

        long frameNumber = outPoint.getFrameNumber() - inPoint.getFrameNumber();
        if (frameNumber < 0) {
            frameNumber += (24 * 6 * inPoint.framesPerTenMinutes);
        }

        return new TimecodeDuration(inPoint.getTimecodeBase(), frameNumber, inPoint.isDropFrame());
    }

    /**
     * Calculates inPoint of a given outPoint and duration.
     * In case duration does not have the same Timecode base and/or dropFrame flag
     * it will convert it to the same Timecode base and dropFrame flag of the outPoint
     *
     * @param outPoint
     * @param duration
     * @return inPoint
     */
    public static Timecode calculateInPoint(Timecode outPoint, TimecodeDuration duration)
    {
        if (!outPoint.isCompatible(duration)) {
            MutableTimecodeDuration mutableTimecodeDuration = new MutableTimecodeDuration(duration);
            mutableTimecodeDuration.setTimecodeBase(outPoint.getTimecodeBase());
            mutableTimecodeDuration.setDropFrame(outPoint.isDropFrame());
            duration = new TimecodeDuration(mutableTimecodeDuration);
        }

        MutableTimecode inPoint = new MutableTimecode(outPoint);
        inPoint.addFrames(-duration.getFrameNumber());
        return new Timecode(inPoint);
    }

    /**
     * Calculates outPoint of a given inPoint and duration.
     * In case duration does not have the same Timecode base and/or dropFrame flag
     * it will convert it to the same Timecode base and dropFrame flag of the inPoint
     *
     * @param inPoint
     * @param duration
     * @return outPoint
     */
    public static Timecode calculateOutPoint(Timecode inPoint, TimecodeDuration duration)
    {
        if (!inPoint.isCompatible(duration)) {
            MutableTimecodeDuration mutableTimecodeDuration = new MutableTimecodeDuration(duration);
            mutableTimecodeDuration.setTimecodeBase(inPoint.getTimecodeBase());
            mutableTimecodeDuration.setDropFrame(inPoint.isDropFrame());
            duration = new TimecodeDuration(mutableTimecodeDuration);
        }

        MutableTimecode outPoint = new MutableTimecode(inPoint);
        outPoint.addFrames(duration.getFrameNumber());
        return new Timecode(outPoint);
    }

    public boolean isCompatible(AbstractTimecode other)
    {
        return isCompatible(this, other);
    }

    public static boolean isCompatible(AbstractTimecode t1, AbstractTimecode t2)
    {
        return t1 != null &&
               t2 != null &&
               t1.isValid() &&
               t2.isValid() &&
               t1.getTimecodeBase() == t2.getTimecodeBase() &&
               t1.isDropFrame() == t2.isDropFrame();
    }

    /**
     * Helper function to test whether the timecode is valid, which means it is not null and valid
     *
     * @param timecode
     * @return whether timecode is initialized and valid
     */
    public static boolean isValid(AbstractTimecode timecode)
    {
        return timecode != null && timecode.isValid();
    }

    /**
     * Helper function to test whether a the timecode is invalid, which means it is either null or invalid.
     *
     * @param timecode
     * @return whether timecode is not initialized or invalid
     */
    public static boolean isInvalid(AbstractTimecode timecode)
    {
        return timecode == null || timecode.isInvalid();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy