Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package format
|
||||
|
||||
// DataType classifies entries.
|
||||
type DataType byte
|
||||
|
||||
const (
|
||||
// DataTypeEndOfMedia marks the last entry.
|
||||
DataTypeEndOfMedia = DataType(0)
|
||||
// DataTypeLowResVideo for low resolution (low compression) video.
|
||||
DataTypeLowResVideo = DataType(0x21)
|
||||
// DataTypeHighResVideo for high resolution (high compression) video.
|
||||
DataTypeHighResVideo = DataType(0x79)
|
||||
// DataTypeAudio marks an audio entry.
|
||||
DataTypeAudio = DataType(2)
|
||||
// DataTypeSubtitle control for subtitle entries.
|
||||
DataTypeSubtitle = DataType(3)
|
||||
// DataTypePalette describes a color palette.
|
||||
DataTypePalette = DataType(4)
|
||||
// DataTypePaletteReset is a zero-byte entry immediately before a Palette entry.
|
||||
DataTypePaletteReset = DataType(0x4C)
|
||||
// DataTypePaletteLookupList for high compression video.
|
||||
DataTypePaletteLookupList = DataType(5)
|
||||
// DataTypeControlDictionary for high compression video.
|
||||
DataTypeControlDictionary = DataType(0x0D)
|
||||
)
|
||||
@@ -0,0 +1,305 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
// EntryBucketPriority describes the general priority of the bucket.
|
||||
// Lower numbers are put before higher numbers if the bucket has the same timestamp.
|
||||
type EntryBucketPriority int
|
||||
|
||||
// List of entry bucket priorities.
|
||||
const (
|
||||
EntryBucketPriorityAudio = 0
|
||||
EntryBucketPriorityVideoControl = 1
|
||||
EntryBucketPrioritySubtitleControl = 2
|
||||
EntryBucketPrioritySubtitle = 3
|
||||
EntryBucketPriorityFrame = 4
|
||||
)
|
||||
|
||||
// EntryBucket is a set of entries that need to be together within the stream.
|
||||
// This exists mainly to cover special cases of entries within a stream that have no valid timestamp.
|
||||
type EntryBucket struct {
|
||||
// Priority helps in merging content from multiple buckets.
|
||||
Priority EntryBucketPriority
|
||||
// Timestamp specifies at which time the bucket should be inserted into the stream.
|
||||
// This is typically derived from one of the timestamps of the contained entries.
|
||||
Timestamp Timestamp
|
||||
// Entries is the list of entries within this bucket.
|
||||
Entries []Entry
|
||||
}
|
||||
|
||||
// Entry describes a timestamped block from a MOVI container.
|
||||
type Entry struct {
|
||||
// Timestamp marks the beginning time of the entry.
|
||||
Timestamp Timestamp
|
||||
// Data is a type specific information of a movie.
|
||||
Data EntryData
|
||||
}
|
||||
|
||||
// EntryData describes one entry in a MOVI container.
|
||||
type EntryData interface {
|
||||
// Type describes the content type of the data.
|
||||
Type() DataType
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
// UnknownEntryData is an entry that is not know to this codebase.
|
||||
type UnknownEntryData struct {
|
||||
DataType DataType
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
// UnknownEntryFrom decodes an entry from given data.
|
||||
func UnknownEntryFrom(dataType DataType, r io.Reader, dataSize int) (UnknownEntryData, error) {
|
||||
data := UnknownEntryData{
|
||||
DataType: dataType,
|
||||
Raw: make([]byte, dataSize),
|
||||
}
|
||||
coder := serial.NewDecoder(r)
|
||||
coder.Code(data.Raw)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data UnknownEntryData) Type() DataType {
|
||||
return data.DataType
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
func (data UnknownEntryData) Bytes() []byte {
|
||||
return data.Raw
|
||||
}
|
||||
|
||||
// LowResVideoEntryData is a compressed low-resolution video frame.
|
||||
type LowResVideoEntryData struct {
|
||||
BoundingBox [4]uint16
|
||||
Packed []byte
|
||||
}
|
||||
|
||||
// LowResVideoEntryFrom decodes an entry from given data.
|
||||
func LowResVideoEntryFrom(r io.Reader, dataSize int) (LowResVideoEntryData, error) {
|
||||
var data LowResVideoEntryData
|
||||
coder := serial.NewDecoder(r)
|
||||
coder.Code(&data.BoundingBox)
|
||||
data.Packed = make([]byte, dataSize-int(coder.CurPos()))
|
||||
coder.Code(data.Packed)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data LowResVideoEntryData) Type() DataType {
|
||||
return DataTypeLowResVideo
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
func (data LowResVideoEntryData) Bytes() []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
coder := serial.NewEncoder(buf)
|
||||
coder.Code(data.BoundingBox)
|
||||
coder.Code(data.Packed)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// HighResVideoEntryData is a compressed high-resolution video frame.
|
||||
type HighResVideoEntryData struct {
|
||||
Bitstream []byte
|
||||
Maskstream []byte
|
||||
}
|
||||
|
||||
// HighResVideoEntryFrom decodes an entry from given data.
|
||||
func HighResVideoEntryFrom(r io.Reader, dataSize int) (HighResVideoEntryData, error) {
|
||||
var data HighResVideoEntryData
|
||||
coder := serial.NewDecoder(r)
|
||||
var maskstreamOffset uint16
|
||||
coder.Code(&maskstreamOffset)
|
||||
data.Bitstream = make([]byte, int(maskstreamOffset)-int(coder.CurPos()))
|
||||
coder.Code(data.Bitstream)
|
||||
data.Maskstream = make([]byte, dataSize-int(coder.CurPos()))
|
||||
coder.Code(data.Maskstream)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data HighResVideoEntryData) Type() DataType {
|
||||
return DataTypeHighResVideo
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
func (data HighResVideoEntryData) Bytes() []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
coder := serial.NewEncoder(buf)
|
||||
maskstreamOffset := uint16(len(data.Bitstream)) + 2
|
||||
coder.Code(&maskstreamOffset)
|
||||
coder.Code(data.Bitstream)
|
||||
coder.Code(data.Maskstream)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// AudioEntryData is an entry with audio samples.
|
||||
type AudioEntryData struct {
|
||||
Samples []byte
|
||||
}
|
||||
|
||||
// AudioEntryFrom decodes an entry from given data.
|
||||
func AudioEntryFrom(r io.Reader, dataSize int) (AudioEntryData, error) {
|
||||
var data AudioEntryData
|
||||
data.Samples = make([]byte, dataSize)
|
||||
coder := serial.NewDecoder(r)
|
||||
coder.Code(data.Samples)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data AudioEntryData) Type() DataType {
|
||||
return DataTypeAudio
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
func (data AudioEntryData) Bytes() []byte {
|
||||
return data.Samples
|
||||
}
|
||||
|
||||
// SubtitleEntryData is an entry with Subtitle samples.
|
||||
type SubtitleEntryData struct {
|
||||
Control SubtitleControl
|
||||
Text []byte
|
||||
}
|
||||
|
||||
// SubtitleEntryFrom decodes an entry from given data.
|
||||
func SubtitleEntryFrom(r io.Reader, dataSize int) (SubtitleEntryData, error) {
|
||||
var data SubtitleEntryData
|
||||
coder := serial.NewDecoder(r)
|
||||
var header SubtitleHeader
|
||||
coder.Code(&header)
|
||||
data.Control = header.Control
|
||||
if header.TextOffset > coder.CurPos() {
|
||||
coder.Code(make([]byte, header.TextOffset-coder.CurPos()))
|
||||
}
|
||||
data.Text = make([]byte, dataSize-int(coder.CurPos()))
|
||||
coder.Code(data.Text)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data SubtitleEntryData) Type() DataType {
|
||||
return DataTypeSubtitle
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
func (data SubtitleEntryData) Bytes() []byte {
|
||||
header := SubtitleHeader{
|
||||
Control: data.Control,
|
||||
TextOffset: SubtitleDefaultTextOffset,
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
coder := serial.NewEncoder(buf)
|
||||
coder.Code(&header)
|
||||
coder.Code(make([]byte, int(header.TextOffset-coder.CurPos())))
|
||||
coder.Code(data.Text)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// PaletteEntryData is an entry with a palette.
|
||||
type PaletteEntryData struct {
|
||||
Colors bitmap.Palette
|
||||
}
|
||||
|
||||
// PaletteEntryFrom decodes an entry from given data.
|
||||
func PaletteEntryFrom(r io.Reader) (PaletteEntryData, error) {
|
||||
var data PaletteEntryData
|
||||
coder := serial.NewDecoder(r)
|
||||
coder.Code(&data.Colors)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data PaletteEntryData) Type() DataType {
|
||||
return DataTypePalette
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes of the entry.
|
||||
func (data PaletteEntryData) Bytes() []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
coder := serial.NewEncoder(buf)
|
||||
coder.Code(&data.Colors)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// PaletteResetEntryData marks a reset of the palette.
|
||||
type PaletteResetEntryData struct {
|
||||
}
|
||||
|
||||
// PaletteResetEntryFrom decodes an entry from given data.
|
||||
func PaletteResetEntryFrom() (PaletteResetEntryData, error) {
|
||||
return PaletteResetEntryData{}, nil
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data PaletteResetEntryData) Type() DataType {
|
||||
return DataTypePaletteReset
|
||||
}
|
||||
|
||||
// Bytes serializes the entry.
|
||||
func (data PaletteResetEntryData) Bytes() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PaletteLookupEntryData contains a list of palette indices.
|
||||
type PaletteLookupEntryData struct {
|
||||
List []byte
|
||||
}
|
||||
|
||||
// PaletteLookupEntryFrom decodes an entry from given data.
|
||||
func PaletteLookupEntryFrom(r io.Reader, dataSize int) (PaletteLookupEntryData, error) {
|
||||
var data PaletteLookupEntryData
|
||||
data.List = make([]byte, dataSize)
|
||||
coder := serial.NewDecoder(r)
|
||||
coder.Code(data.List)
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data PaletteLookupEntryData) Type() DataType {
|
||||
return DataTypePaletteLookupList
|
||||
}
|
||||
|
||||
// Bytes serializes the entry.
|
||||
func (data PaletteLookupEntryData) Bytes() []byte {
|
||||
return data.List
|
||||
}
|
||||
|
||||
// ControlDictionaryEntryData contains control words for compression.
|
||||
type ControlDictionaryEntryData struct {
|
||||
Words []compression.ControlWord
|
||||
}
|
||||
|
||||
// ControlDictionaryEntryFrom decodes an entry from given data.
|
||||
func ControlDictionaryEntryFrom(r io.Reader, dataSize int) (ControlDictionaryEntryData, error) {
|
||||
var data ControlDictionaryEntryData
|
||||
coder := serial.NewDecoder(r)
|
||||
b := make([]byte, dataSize)
|
||||
coder.Code(b)
|
||||
if coder.FirstError() != nil {
|
||||
return data, coder.FirstError()
|
||||
}
|
||||
var err error
|
||||
data.Words, err = compression.UnpackControlWords(b)
|
||||
return data, err
|
||||
}
|
||||
|
||||
// Type describes the entry.
|
||||
func (data ControlDictionaryEntryData) Type() DataType {
|
||||
return DataTypeControlDictionary
|
||||
}
|
||||
|
||||
// Bytes serializes the entry.
|
||||
func (data ControlDictionaryEntryData) Bytes() []byte {
|
||||
return compression.PackControlWords(data.Words)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Tag is the identifier key of a MOVI container.
|
||||
const Tag string = "MOVI"
|
||||
|
||||
// HeaderSize specifies the length of a serialized Header in bytes.
|
||||
const HeaderSize = 256
|
||||
|
||||
// Fix represents a fixed-point number, with 16 bits of fraction and 15 bits of integer number.
|
||||
type Fix struct {
|
||||
Fraction uint16
|
||||
Number int16
|
||||
}
|
||||
|
||||
// FixFromFloat returns a fixed-point number from given floating-point number.
|
||||
func FixFromFloat(value float32) Fix {
|
||||
number := int16(value)
|
||||
return Fix{
|
||||
Fraction: uint16(math.Abs(float64(value)-float64(number)) * 0x10000),
|
||||
Number: number,
|
||||
}
|
||||
}
|
||||
|
||||
// ToFloat returns the equivalent floating-point number.
|
||||
func (f Fix) ToFloat() float32 {
|
||||
return float32(f.Number) + float32(f.Fraction)/0x10000
|
||||
}
|
||||
|
||||
// Header is the first entry of a MOVI container.
|
||||
type Header struct {
|
||||
// Tag is the identifier key of a MOVI container
|
||||
Tag [4]byte
|
||||
// IndexEntryCount specifies how many index entries are in the index.
|
||||
IndexEntryCount int32
|
||||
// IndexSize specifies how long the index is.
|
||||
IndexSize int32
|
||||
// ContentSize specifies the length of the content.
|
||||
ContentSize int32
|
||||
// Duration is the length of the media in seconds.
|
||||
Duration Fix
|
||||
|
||||
// VideoFrameRate gives a hint on number of frames per second.
|
||||
VideoFrameRate Fix
|
||||
// VideoWidth specifies the width of the video.
|
||||
VideoWidth uint16
|
||||
// VideoHeight specifies the height of the video.
|
||||
VideoHeight uint16
|
||||
// VideoBitsPerPixel specifies bit count for color information: 8, 15, 24.
|
||||
VideoBitsPerPixel int16
|
||||
// VideoPalettePresent specifies whether a palette is set.
|
||||
VideoPalettePresent int16
|
||||
|
||||
// AudioChannelCount specifies whether there is audio, and how many channels to use. 0 = no audio, 1 = mono, 2 = stereo.
|
||||
AudioChannelCount int16
|
||||
// AudioBytesPerSample specifies how many bytes one audio sample has. 1 = 8-bit, 2 = 16-bit.
|
||||
AudioBytesPerSample int16
|
||||
// AudioSampleRate specifies the sample rate of the audio.
|
||||
AudioSampleRate Fix
|
||||
|
||||
_ [216]byte
|
||||
}
|
||||
|
||||
func (header *Header) String() (result string) {
|
||||
result += fmt.Sprintf("Index: %d entries (%d bytes)\n", header.IndexEntryCount, header.IndexSize)
|
||||
result += fmt.Sprintf("Content: %d bytes, %f seconds\n", header.ContentSize, header.Duration.ToFloat())
|
||||
result += fmt.Sprintf("Video: %dx%d pixel @%f sample rate\n", header.VideoWidth, header.VideoHeight, header.AudioSampleRate.ToFloat())
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
|
||||
)
|
||||
|
||||
func TestHeaderSerializesToProperLength(t *testing.T) {
|
||||
source := bytes.NewReader(make([]byte, 0x200))
|
||||
var header format.Header
|
||||
|
||||
_ = binary.Read(source, binary.LittleEndian, &header)
|
||||
curPos, _ := source.Seek(0, io.SeekCurrent)
|
||||
|
||||
assert.Equal(t, int64(format.HeaderSize), curPos)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package format
|
||||
|
||||
// IndexTableEntrySize specifies how many bytes a serialized IndexTableEntry uses.
|
||||
const IndexTableEntrySize = 8
|
||||
|
||||
const indexHeaderSizeIncrement = 0x0400
|
||||
|
||||
// IndexTableEntry describes one data entry of a MOVI container.
|
||||
type IndexTableEntry struct {
|
||||
TimestampFraction uint16
|
||||
TimestampSecond byte
|
||||
Type byte
|
||||
DataOffset int32
|
||||
}
|
||||
|
||||
// IndexTableSizeFor returns the number of bytes reserved for given amount of table entries.
|
||||
func IndexTableSizeFor(entryCount int) int {
|
||||
size := indexHeaderSizeIncrement
|
||||
requiredSize := entryCount * IndexTableEntrySize
|
||||
|
||||
if requiredSize > size {
|
||||
size *= requiredSize/size + 2
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
|
||||
)
|
||||
|
||||
func TestIndexTableSizeFor_ExistingSizes(t *testing.T) {
|
||||
// These sample values are always the minimum and maximum amount of index entries
|
||||
// found for a given index size.
|
||||
|
||||
assert.Equal(t, 0x0400, format.IndexTableSizeFor(3))
|
||||
assert.Equal(t, 0x0400, format.IndexTableSizeFor(127))
|
||||
|
||||
assert.Equal(t, 0x0C00, format.IndexTableSizeFor(130))
|
||||
assert.Equal(t, 0x0C00, format.IndexTableSizeFor(218))
|
||||
|
||||
assert.Equal(t, 0x1C00, format.IndexTableSizeFor(738))
|
||||
assert.Equal(t, 0x1C00, format.IndexTableSizeFor(755))
|
||||
|
||||
assert.Equal(t, 0x3400, format.IndexTableSizeFor(1475))
|
||||
assert.Equal(t, 0x3400, format.IndexTableSizeFor(1523))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package format
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/resource"
|
||||
|
||||
// SubtitleControl specifies how to interpret a subtitle entry.
|
||||
type SubtitleControl uint32
|
||||
|
||||
// Subtitle constants are based on four-letter ASCII sequences.
|
||||
const (
|
||||
SubtitleArea = SubtitleControl(0x41455241)
|
||||
SubtitleTextStd = SubtitleControl(0x20445453)
|
||||
SubtitleTextFrn = SubtitleControl(0x204E5246)
|
||||
SubtitleTextGer = SubtitleControl(0x20524547)
|
||||
)
|
||||
|
||||
// String returns the string presentation of the control value.
|
||||
func (ctrl SubtitleControl) String() string {
|
||||
return string([]rune{rune((ctrl >> 0) & 0xFF), rune((ctrl >> 8) & 0xFF), rune((ctrl >> 16) & 0xFF), rune((ctrl >> 24) & 0xFF)})
|
||||
}
|
||||
|
||||
// SubtitleControlForLanguage returns the corresponding subtitle control for given language.
|
||||
func SubtitleControlForLanguage(lang resource.Language) SubtitleControl {
|
||||
switch lang {
|
||||
case resource.LangDefault:
|
||||
return SubtitleTextStd
|
||||
case resource.LangFrench:
|
||||
return SubtitleTextFrn
|
||||
case resource.LangGerman:
|
||||
return SubtitleTextGer
|
||||
default:
|
||||
panic("unknown language")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package format
|
||||
|
||||
// SubtitleDefaultTextOffset is the typical offset, in bytes, to text content.
|
||||
const SubtitleDefaultTextOffset = 16
|
||||
|
||||
// SubtitleHeader is the header structure of a subtitle data.
|
||||
type SubtitleHeader struct {
|
||||
// Control specifies how to interpret the string content
|
||||
Control SubtitleControl
|
||||
// TextOffset specifies the offset, in bytes from beginning of entry data, where the text starts.
|
||||
TextOffset uint32
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const fractionDivisor = 0x10000
|
||||
|
||||
// Timestamp represents a point in time using fixed resolution.
|
||||
type Timestamp struct {
|
||||
Second uint8
|
||||
Fraction uint16
|
||||
}
|
||||
|
||||
// TimestampLimit returns the highest possible timestamp value.
|
||||
func TimestampLimit() Timestamp {
|
||||
return Timestamp{
|
||||
Second: math.MaxUint8,
|
||||
Fraction: math.MaxUint16,
|
||||
}
|
||||
}
|
||||
|
||||
// TimestampFromSeconds creates a timestamp instance from given floating point value.
|
||||
func TimestampFromSeconds(value float32) Timestamp {
|
||||
second := uint8(value)
|
||||
return Timestamp{
|
||||
Second: second,
|
||||
Fraction: uint16((value - float32(second)) * fractionDivisor),
|
||||
}
|
||||
}
|
||||
|
||||
// TimestampFromDuration creates a timestamp instance from given duration value.
|
||||
func TimestampFromDuration(d time.Duration) Timestamp {
|
||||
limit := TimestampLimit()
|
||||
if d >= limit.ToDuration() {
|
||||
return limit
|
||||
}
|
||||
return Timestamp{
|
||||
Second: uint8(d / time.Second),
|
||||
Fraction: uint16(((d - time.Second) * fractionDivisor) / time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
// ToDuration returns the equivalent duration for this timestamp.
|
||||
func (ts Timestamp) ToDuration() time.Duration {
|
||||
return (time.Duration(ts.Second) * time.Second) + (time.Duration(ts.Fraction)*time.Second)/fractionDivisor
|
||||
}
|
||||
|
||||
// IsAfter returns true if this timestamp is later than the given one.
|
||||
func (ts Timestamp) IsAfter(other Timestamp) bool {
|
||||
return (ts.Second > other.Second) || ((ts.Second == other.Second) && (ts.Fraction > other.Fraction))
|
||||
}
|
||||
|
||||
// IsBefore returns true if this timestamp is before the given one.
|
||||
func (ts Timestamp) IsBefore(other Timestamp) bool {
|
||||
return (ts.Second < other.Second) || ((ts.Second == other.Second) && (ts.Fraction < other.Fraction))
|
||||
}
|
||||
|
||||
// Plus returns a timestamp with the given one added to the current one.
|
||||
// The result is saturated if the addition is larger than the timestamp can hold.
|
||||
func (ts Timestamp) Plus(other Timestamp) Timestamp {
|
||||
return TimestampFromDuration(ts.ToDuration() + other.ToDuration())
|
||||
}
|
||||
|
||||
// Minus returns a timestamp with given one removed from the current one.
|
||||
// The result is clipped to 0 if subtraction would result in a negative number.
|
||||
func (ts Timestamp) Minus(other Timestamp) Timestamp {
|
||||
otherLinear := other.toLinear()
|
||||
tsLinear := ts.toLinear()
|
||||
if otherLinear >= tsLinear {
|
||||
return timestampFromLinear(0)
|
||||
}
|
||||
return timestampFromLinear(tsLinear - otherLinear)
|
||||
}
|
||||
|
||||
func (ts Timestamp) toLinear() uint32 {
|
||||
return uint32(ts.Second)<<16 + uint32(ts.Fraction)
|
||||
}
|
||||
|
||||
func timestampFromLinear(value uint32) Timestamp {
|
||||
return Timestamp{
|
||||
Second: uint8(value >> 16),
|
||||
Fraction: uint16(value),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
|
||||
)
|
||||
|
||||
func TestTimestampFromSecond(t *testing.T) {
|
||||
ts := format.TimestampFromSeconds(7.25)
|
||||
|
||||
assert.Equal(t, byte(7), ts.Second)
|
||||
assert.Equal(t, uint16(0x4000), ts.Fraction)
|
||||
}
|
||||
|
||||
func TestTimeToRawB(t *testing.T) {
|
||||
ts := format.TimestampFromSeconds(255.75)
|
||||
|
||||
assert.Equal(t, byte(255), ts.Second)
|
||||
assert.Equal(t, uint16(0xC000), ts.Fraction)
|
||||
}
|
||||
|
||||
func TestDurationConversion(t *testing.T) {
|
||||
for fraction := 0; fraction <= math.MaxUint16; fraction++ {
|
||||
ts := format.Timestamp{Second: 0, Fraction: uint16(fraction)}
|
||||
dur := ts.ToDuration()
|
||||
ts2 := format.TimestampFromDuration(dur)
|
||||
|
||||
assert.Equal(t, ts, ts2, fmt.Sprintf("Mismatch for fraction %v", fraction))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestampPlus(t *testing.T) {
|
||||
tt := []struct {
|
||||
a format.Timestamp
|
||||
b format.Timestamp
|
||||
expected format.Timestamp
|
||||
}{
|
||||
{
|
||||
a: format.Timestamp{Second: 0, Fraction: 0},
|
||||
b: format.Timestamp{Second: 1, Fraction: 0x8000},
|
||||
expected: format.Timestamp{Second: 1, Fraction: 0x8000},
|
||||
},
|
||||
{
|
||||
a: format.Timestamp{Second: 2, Fraction: 0x8000},
|
||||
b: format.Timestamp{Second: 1, Fraction: 0x8000},
|
||||
expected: format.Timestamp{Second: 4, Fraction: 0x0000},
|
||||
},
|
||||
{
|
||||
a: format.Timestamp{Second: 0xFF, Fraction: 0x0000},
|
||||
b: format.Timestamp{Second: 2, Fraction: 0x8000},
|
||||
expected: format.Timestamp{Second: 0xFF, Fraction: 0xFFFF},
|
||||
},
|
||||
}
|
||||
for index, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) {
|
||||
result := tc.a.Plus(tc.b)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user