diff --git a/music/tracked/it/flags.go b/music/tracked/it/flags.go new file mode 100644 index 0000000..b8d063f --- /dev/null +++ b/music/tracked/it/flags.go @@ -0,0 +1,311 @@ +package it + +// IMPMFlags is a set of flags describing various features in the IT file +type IMPMFlags uint16 + +const ( + // IMPMFlagStereo :: On = Stereo, Off = Mono (panning enablement flag) + IMPMFlagStereo = IMPMFlags(1 << 0) + // IMPMFlagVol0Optimizations :: If on, no mixing occurs if the volume at mixing time is 0 (redundant v1.04+) + IMPMFlagVol0Optimizations = IMPMFlags(1 << 1) + // IMPMFlagUseInstruments :: On = Use instruments, Off = Use samples + IMPMFlagUseInstruments = IMPMFlags(1 << 2) + // IMPMFlagLinearSlides :: On = Linear slides, Off = Amiga slides + IMPMFlagLinearSlides = IMPMFlags(1 << 3) + // IMPMFlagOldEffects :: On = Old Effects, Off = IT Effects + IMPMFlagOldEffects = IMPMFlags(1 << 4) + // IMPMFlagEFGLinking :: On = Link Effect G's memory with Effect E/F + IMPMFlagEFGLinking = IMPMFlags(1 << 5) + // IMPMFlagMidiPitchController :: Use MIDI pitch controller, Pitch depth given by PitchWheelDepth + IMPMFlagMidiPitchController = IMPMFlags(1 << 6) + // IMPMFlagReqEmbedMidi :: Request embedded MIDI configuration + IMPMFlagReqEmbedMidi = IMPMFlags(1 << 7) +) + +// IsStereo returns true if stereo (panning) is enabled +func (f IMPMFlags) IsStereo() bool { + return (f & IMPMFlagStereo) != 0 +} + +// IsVol0Optimizations returns true if vol-0 optimization is enabled +func (f IMPMFlags) IsVol0Optimizations() bool { + return (f & IMPMFlagVol0Optimizations) != 0 +} + +// IsUseInstruments returns true if use-instruments (instead of samples) is enabled +func (f IMPMFlags) IsUseInstruments() bool { + return (f & IMPMFlagUseInstruments) != 0 +} + +// IsLinearSlides returns true if linear slides is enabled +func (f IMPMFlags) IsLinearSlides() bool { + return (f & IMPMFlagLinearSlides) != 0 +} + +// IsOldEffects returns true if old-style effects are enabled +func (f IMPMFlags) IsOldEffects() bool { + return (f & IMPMFlagLinearSlides) != 0 +} + +// IsEFGLinking returns true if effect E/F/G linking is enabled +func (f IMPMFlags) IsEFGLinking() bool { + return (f & IMPMFlagEFGLinking) != 0 +} + +// IsMidiPitchController returns true if midi pitch controller is enabled +func (f IMPMFlags) IsMidiPitchController() bool { + return (f & IMPMFlagMidiPitchController) != 0 +} + +// IsReqEmbedMidi returns true if request embedded midi configuration is enabled +func (f IMPMFlags) IsReqEmbedMidi() bool { + return (f & IMPMFlagReqEmbedMidi) != 0 +} + +// IMPMSpecialFlags is a set of flags describing various special features in the IT file +type IMPMSpecialFlags uint16 + +const ( + // IMPMSpecialFlagMessageAttached :: On = song message attached + IMPMSpecialFlagMessageAttached = IMPMSpecialFlags(1 << 0) + // IMPMSpecialFlagReservedBit1 :: Reserved + IMPMSpecialFlagReservedBit1 = IMPMSpecialFlags(1 << 1) + // IMPMSpecialFlagReservedBit2 :: Reserved + IMPMSpecialFlagReservedBit2 = IMPMSpecialFlags(1 << 2) + // IMPMSpecialFlagEmbedMidi :: MIDI configuration embedded + IMPMSpecialFlagEmbedMidi = IMPMSpecialFlags(1 << 3) + // IMPMSpecialFlagReservedBit4 :: Reserved + IMPMSpecialFlagReservedBit4 = IMPMSpecialFlags(1 << 4) + // IMPMSpecialFlagReservedBit5 :: Reserved + IMPMSpecialFlagReservedBit5 = IMPMSpecialFlags(1 << 5) + // IMPMSpecialFlagReservedBit6 :: Reserved + IMPMSpecialFlagReservedBit6 = IMPMSpecialFlags(1 << 6) + // IMPMSpecialFlagReservedBit7 :: Reserved + IMPMSpecialFlagReservedBit7 = IMPMSpecialFlags(1 << 7) + // IMPMSpecialFlagReservedBit8 :: Reserved + IMPMSpecialFlagReservedBit8 = IMPMSpecialFlags(1 << 8) + // IMPMSpecialFlagReservedBit9 :: Reserved + IMPMSpecialFlagReservedBit9 = IMPMSpecialFlags(1 << 9) + // IMPMSpecialFlagReservedBit10 :: Reserved + IMPMSpecialFlagReservedBit10 = IMPMSpecialFlags(1 << 10) + // IMPMSpecialFlagReservedBit11 :: Reserved + IMPMSpecialFlagReservedBit11 = IMPMSpecialFlags(1 << 11) + // IMPMSpecialFlagReservedBit12 :: Reserved + IMPMSpecialFlagReservedBit12 = IMPMSpecialFlags(1 << 12) + // IMPMSpecialFlagReservedBit13 :: Reserved + IMPMSpecialFlagReservedBit13 = IMPMSpecialFlags(1 << 13) + // IMPMSpecialFlagReservedBit14 :: Reserved + IMPMSpecialFlagReservedBit14 = IMPMSpecialFlags(1 << 14) + // IMPMSpecialFlagReservedBit15 :: Reserved + IMPMSpecialFlagReservedBit15 = IMPMSpecialFlags(1 << 15) +) + +// IsMessageAttached returns true if there is a special message attached to the file +func (sf IMPMSpecialFlags) IsMessageAttached() bool { + return (sf & IMPMSpecialFlagMessageAttached) != 0 +} + +// IsEmbedMidi returns true if embedded midi configuration is enabled +func (sf IMPMSpecialFlags) IsEmbedMidi() bool { + return (sf & IMPMSpecialFlagEmbedMidi) != 0 +} + +// SampleFlags defines the flags associated to the IT format sample header +type SampleFlags uint8 + +const ( + // SampleFlagSampleExists :: On = sample associated with header + SampleFlagSampleExists = SampleFlags(1 << 0) + // SampleFlag16Bit :: On = 16 bit, Off = 8 bit + SampleFlag16Bit = SampleFlags(1 << 1) + // SampleFlagStereo :: On = stereo, Off = mono + SampleFlagStereo = SampleFlags(1 << 2) + // SampleFlagCompressed :: On = compressed samples + SampleFlagCompressed = SampleFlags(1 << 3) + // SampleFlagUseLoop :: On = Use loop + SampleFlagUseLoop = SampleFlags(1 << 4) + // SampleFlagUseSustainLoop :: On = Use sustain loop + SampleFlagUseSustainLoop = SampleFlags(1 << 5) + // SampleFlagPingPongLoop :: On = Ping Pong loop, Off = Forwards loop + SampleFlagPingPongLoop = SampleFlags(1 << 6) + // SampleFlagPingPongSustainLoop :: On = Ping Pong Sustain loop, Off = Forwards Sustain loop + SampleFlagPingPongSustainLoop = SampleFlags(1 << 7) +) + +// DoesSampleExist returns true if the sample header has a sample associated with it. +func (sf SampleFlags) DoesSampleExist() bool { + return (sf & SampleFlagSampleExists) != 0 +} + +// Is16Bit returns true if the sample is 16-bit +func (sf SampleFlags) Is16Bit() bool { + return (sf & SampleFlag16Bit) != 0 +} + +// IsStereo returns true if the sample is stereo +func (sf SampleFlags) IsStereo() bool { + return (sf & SampleFlagStereo) != 0 +} + +// IsCompressed returns true if the sample data is compressed +func (sf SampleFlags) IsCompressed() bool { + return (sf & SampleFlagCompressed) != 0 +} + +// IsLoopEnabled returns true if the sample loop is enabled +func (sf SampleFlags) IsLoopEnabled() bool { + return (sf & SampleFlagUseLoop) != 0 +} + +// IsSustainLoopEnabled returns true if the sample sustain loop is enabled +func (sf SampleFlags) IsSustainLoopEnabled() bool { + return (sf & SampleFlagUseSustainLoop) != 0 +} + +// IsLoopPingPong returns true if the sample loop mode is ping-pong +func (sf SampleFlags) IsLoopPingPong() bool { + return (sf & SampleFlagPingPongLoop) != 0 +} + +// IsSustainLoopPingPong returns true if the sample sustain loop mode is ping-pong +func (sf SampleFlags) IsSustainLoopPingPong() bool { + return (sf & SampleFlagPingPongSustainLoop) != 0 +} + +// ConvertFlags defines the flags associated to the IT format sample data conversion settings +type ConvertFlags uint8 + +const ( + // ConvertFlagSignedSamples :: On = Samples are signed. Off = Samples are unsigned + ConvertFlagSignedSamples = ConvertFlags(1 << 0) + // ConvertFlagBigEndian :: On = 16 bit big endian, Off = 16 bit little endian + ConvertFlagBigEndian = ConvertFlags(1 << 1) + // ConvertFlagSampleDelta :: On = stored as delta values, Off = stored as PCM values + ConvertFlagSampleDelta = ConvertFlags(1 << 2) + // ConvertFlagByteDelta :: On = stored as byte delta values + ConvertFlagByteDelta = ConvertFlags(1 << 3) + // ConvertFlagTXWave12Bit :: On = stored as TX-Wave 12-bit values + ConvertFlagTXWave12Bit = ConvertFlags(1 << 4) + // ConvertFlagChannelSelectionPrompt :: On = Left/Right/All Stereo prompt + ConvertFlagChannelSelectionPrompt = ConvertFlags(1 << 5) + // ConvertFlagReserved6 :: Reserved + ConvertFlagReserved6 = ConvertFlags(1 << 6) + // ConvertFlagReserved7 :: Reserved + ConvertFlagReserved7 = ConvertFlags(1 << 7) +) + +// IsSignedSamples returns true if the sample data is in signed values +func (sf ConvertFlags) IsSignedSamples() bool { + return (sf & ConvertFlagSignedSamples) != 0 +} + +// IsBigEndian returns true if the sample is 16-bit big endian +func (sf ConvertFlags) IsBigEndian() bool { + return (sf & ConvertFlagBigEndian) != 0 +} + +// IsSampleDelta returns true if the sample is stored in sample delta format +func (sf ConvertFlags) IsSampleDelta() bool { + return (sf & ConvertFlagSampleDelta) != 0 +} + +// IsByteDelta returns true if the sample data is stored in byte delta format +func (sf ConvertFlags) IsByteDelta() bool { + return (sf & ConvertFlagByteDelta) != 0 +} + +// IsTXWave12Bit returns true if the sample loop is stored in TX-Wave 12-bit format +func (sf ConvertFlags) IsTXWave12Bit() bool { + return (sf & ConvertFlagTXWave12Bit) != 0 +} + +// IsChannelSelectPrompt returns true if the channel selection prompt is enabled +func (sf ConvertFlags) IsChannelSelectPrompt() bool { + return (sf & ConvertFlagChannelSelectionPrompt) != 0 +} + +// IMPIOldFlags is the flagset for IMPIInstrumentOld instruments +type IMPIOldFlags uint8 + +const ( + // IMPIOldFlagUseVolumeEnvelope :: On = Use volume envelope + IMPIOldFlagUseVolumeEnvelope = IMPIOldFlags(1 << 0) + // IMPIOldFlagUseVolumeLoop :: On = Use volume loop + IMPIOldFlagUseVolumeLoop = IMPIOldFlags(1 << 1) + // IMPIOldFlagUseSustainVolumeLoop :: On = Use sustain volume loop + IMPIOldFlagUseSustainVolumeLoop = IMPIOldFlags(1 << 2) +) + +// EnvelopeFlags is the flagset for new instrument envelopes +type EnvelopeFlags uint8 + +const ( + // EnvelopeFlagEnvelopeOn :: On = Use envelope + EnvelopeFlagEnvelopeOn = EnvelopeFlags(1 << 0) + // EnvelopeFlagLoopOn :: On = Use loop + EnvelopeFlagLoopOn = EnvelopeFlags(1 << 1) + // EnvelopeFlagSustainLoopOn :: On = Use sustain loop + EnvelopeFlagSustainLoopOn = EnvelopeFlags(1 << 2) +) + +// ChannelDataFlags is a set of flags specifying what data is available in the packed pattern and/or in this channel data +type ChannelDataFlags uint8 + +const ( + // ChannelDataFlagNote :: On = Note is available + ChannelDataFlagNote = ChannelDataFlags(1 << 0) + // ChannelDataFlagInstrument :: On = Instrument is available + ChannelDataFlagInstrument = ChannelDataFlags(1 << 1) + // ChannelDataFlagVolPan :: On = VolPan is available + ChannelDataFlagVolPan = ChannelDataFlags(1 << 2) + // ChannelDataFlagCommand :: On = Command/CommandData is available + ChannelDataFlagCommand = ChannelDataFlags(1 << 3) + // ChannelDataFlagUseLastNote :: On = Use the previous Note value in the channel + ChannelDataFlagUseLastNote = ChannelDataFlags(1 << 4) + // ChannelDataFlagUseLastInstrument :: On = Use the previous Instrument value in the channel + ChannelDataFlagUseLastInstrument = ChannelDataFlags(1 << 5) + // ChannelDataFlagUseLastVolPan :: On = Use the previous VolPan value in the channel + ChannelDataFlagUseLastVolPan = ChannelDataFlags(1 << 6) + // ChannelDataFlagUseLastCommand :: On = Use the previous Command/CommandData value in the channel + ChannelDataFlagUseLastCommand = ChannelDataFlags(1 << 7) +) + +// HasNote returns true if there is a Note value present +func (cdf ChannelDataFlags) HasNote() bool { + return (cdf & ChannelDataFlagNote) != 0 +} + +// HasInstrument returns true if there is a Instrument value present +func (cdf ChannelDataFlags) HasInstrument() bool { + return (cdf & ChannelDataFlagInstrument) != 0 +} + +// HasVolPan returns true if there is a VolPan value present +func (cdf ChannelDataFlags) HasVolPan() bool { + return (cdf & ChannelDataFlagVolPan) != 0 +} + +// HasCommand returns true if there is a Command value present +func (cdf ChannelDataFlags) HasCommand() bool { + return (cdf & ChannelDataFlagCommand) != 0 +} + +// IsUseLastNote returns true if the previous Note value should be used +func (cdf ChannelDataFlags) IsUseLastNote() bool { + return (cdf & ChannelDataFlagUseLastNote) != 0 +} + +// IsUseLastInstrument returns true if the previous Instrument value should be used +func (cdf ChannelDataFlags) IsUseLastInstrument() bool { + return (cdf & ChannelDataFlagUseLastInstrument) != 0 +} + +// IsUseLastVolPan returns true if the previous VolPan value should be used +func (cdf ChannelDataFlags) IsUseLastVolPan() bool { + return (cdf & ChannelDataFlagUseLastVolPan) != 0 +} + +// IsUseLastCommand returns true if the previous Command/CommandData value should be used +func (cdf ChannelDataFlags) IsUseLastCommand() bool { + return (cdf & ChannelDataFlagUseLastCommand) != 0 +} diff --git a/music/tracked/it/instrument.go b/music/tracked/it/instrument.go new file mode 100644 index 0000000..ed97026 --- /dev/null +++ b/music/tracked/it/instrument.go @@ -0,0 +1,58 @@ +package it + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + + "github.com/gotracker/goaudiofile/internal/util" +) + +var ( + // ErrInvalidInstrumentFormat is an error for when an invalid instrument format is encountered + ErrInvalidInstrumentFormat = errors.New("invalid instrument format") +) + +// IMPIIntf is an interface to the IT instruments +type IMPIIntf interface{} + +func readIMPI(data []byte, ptr ParaPointer, cmwt uint16) (IMPIIntf, error) { + ofs := ptr.Offset() + r := bytes.NewBuffer(data[ofs:]) + + switch { + case cmwt < 0x200: + return readIMPIOld(r) + default: + return readIMPINew(r) + } +} + +func readIMPIOld(r io.Reader) (*IMPIInstrumentOld, error) { + inst := IMPIInstrumentOld{} + + if err := binary.Read(r, binary.LittleEndian, &inst); err != nil { + return nil, err + } + + if util.GetString(inst.IMPI[:]) != "IMPI" { + return nil, ErrInvalidInstrumentFormat + } + + return &inst, nil +} + +func readIMPINew(r io.Reader) (*IMPIInstrument, error) { + inst := IMPIInstrument{} + + if err := binary.Read(r, binary.LittleEndian, &inst); err != nil { + return nil, err + } + + if util.GetString(inst.IMPI[:]) != "IMPI" { + return nil, ErrInvalidInstrumentFormat + } + + return &inst, nil +} diff --git a/music/tracked/it/instrument_new.go b/music/tracked/it/instrument_new.go new file mode 100644 index 0000000..ab3325c --- /dev/null +++ b/music/tracked/it/instrument_new.go @@ -0,0 +1,43 @@ +package it + +import "github.com/gotracker/goaudiofile/internal/util" + +// IMPIInstrument is the format of the IMPI Instrument for tracker compatibility versions >= 0x0200 +type IMPIInstrument struct { + IMPI [4]byte + Filename [12]byte + Nul10 uint8 + NewNoteAction NewNoteAction + DuplicateCheckType DuplicateCheckType + DuplicateCheckAction DuplicateCheckAction + Fadeout uint16 + PitchPanSeparation int8 + PitchPanCenter uint8 + GlobalVolume FineVolume + DefaultPan PanValue + RandomVolumeVariation Percentage8 + RandomPanVariation Percentage8 + TrackerVersion uint16 + SampleCount uint8 + Reserved1F uint8 + Name [26]byte + InitialFilterCutoff uint8 + InitialFilterResonance uint8 + MidiChannel uint8 + MidiProgram uint8 + MidiBank uint16 + NoteSampleKeyboard [240]uint8 + VolumeEnvelope Envelope + PanningEnvelope Envelope + PitchEnvelope Envelope +} + +// GetName returns a string representation of the data stored in the Name field +func (i *IMPIInstrument) GetName() string { + return util.GetString(i.Name[:]) +} + +// GetFilename returns a string representation of the data stored in the Filename field +func (i *IMPIInstrument) GetFilename() string { + return util.GetString(i.Filename[:]) +} diff --git a/music/tracked/it/instrument_old.go b/music/tracked/it/instrument_old.go new file mode 100644 index 0000000..04756bb --- /dev/null +++ b/music/tracked/it/instrument_old.go @@ -0,0 +1,36 @@ +package it + +import "github.com/gotracker/goaudiofile/internal/util" + +// IMPIInstrumentOld is the format of the IMPI Instrument for tracker compatibility versions < 0x0200 +type IMPIInstrumentOld struct { + IMPI [4]byte + Filename [12]byte + Nul10 uint8 + Flags IMPIOldFlags + VolumeLoopStart uint8 + VolumeLoopEnd uint8 + SustainLoopStart uint8 + SustainLoopEnd uint8 + Fadeout uint16 + NewNoteAction NewNoteAction + DuplicateNoteCheck DuplicateNoteCheck + TrackerVersion uint16 + SampleCount uint8 + Reserved1F uint8 + Name [26]byte + Reserved3A [6]uint8 + NoteSampleKeyboard [240]uint8 + VolumeEnvelope [200]uint8 + NodePoints [25]NodePoint16 +} + +// GetName returns a string representation of the data stored in the Name field +func (i *IMPIInstrumentOld) GetName() string { + return util.GetString(i.Name[:]) +} + +// GetFilename returns a string representation of the data stored in the Filename field +func (i *IMPIInstrumentOld) GetFilename() string { + return util.GetString(i.Filename[:]) +} diff --git a/music/tracked/it/it.go b/music/tracked/it/it.go new file mode 100644 index 0000000..6ec5c5d --- /dev/null +++ b/music/tracked/it/it.go @@ -0,0 +1,134 @@ +package it + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + + "github.com/gotracker/goaudiofile/internal/util" +) + +var ( + // ErrInvalidFileFormat is for when an invalid file format is encountered + ErrInvalidFileFormat = errors.New("invalid file format") +) + +// File is an IT internal file representation +type File struct { + Head ModuleHeader + OrderList []uint8 + InstrumentPointers []ParaPointer32 + SamplePointers []ParaPointer32 + PatternPointers []ParaPointer32 + Instruments []IMPIIntf + Samples []FullSample + Patterns []PackedPattern +} + +// FullSample is a full sample, header + data +type FullSample struct { + Header Sample + Data []byte +} + +// Read reads an IT file from the reader `r` and creates an internal File representation +func Read(r io.Reader) (*File, error) { + buffer := &bytes.Buffer{} + if _, err := buffer.ReadFrom(r); err != nil { + return nil, err + } + data := buffer.Bytes() + + fh, err := ReadModuleHeader(buffer) + if err != nil { + return nil, err + } + if util.GetString(fh.IMPM[:]) != "IMPM" { + return nil, ErrInvalidFileFormat + } + + f := File{ + Head: *fh, + OrderList: make([]uint8, int(fh.OrderCount)), + InstrumentPointers: make([]ParaPointer32, int(fh.InstrumentCount)), + SamplePointers: make([]ParaPointer32, int(fh.SampleCount)), + PatternPointers: make([]ParaPointer32, int(fh.PatternCount)), + Instruments: make([]IMPIIntf, 0), + Samples: make([]FullSample, 0), + Patterns: make([]PackedPattern, 0), + } + if err := binary.Read(buffer, binary.LittleEndian, &f.OrderList); err != nil { + return nil, err + } + if err := binary.Read(buffer, binary.LittleEndian, &f.InstrumentPointers); err != nil { + return nil, err + } + if err := binary.Read(buffer, binary.LittleEndian, &f.SamplePointers); err != nil { + return nil, err + } + if err := binary.Read(buffer, binary.LittleEndian, &f.PatternPointers); err != nil { + return nil, err + } + // the earliest valid position to read from + valPos := ParaPointer32(0x00C0 + len(f.OrderList) + len(f.InstrumentPointers)*4 + len(f.SamplePointers)*4 + len(f.PatternPointers)*4) + + for _, ptr := range f.InstrumentPointers { + if ptr < valPos { + return nil, ErrInvalidFileFormat + } + + impi, err := readIMPI(data, ptr, f.Head.TrackerCompatVersion) + if err != nil { + return nil, ErrInvalidFileFormat + } + f.Instruments = append(f.Instruments, impi) + } + + for _, ptr := range f.SamplePointers { + if ptr < valPos { + return nil, ErrInvalidFileFormat + } + + imps, err := readIMPS(data, ptr, f.Head.TrackerCompatVersion) + if err != nil { + return nil, ErrInvalidFileFormat + } + + fs := FullSample{ + Header: *imps, + Data: make([]byte, 0), + } + + if fs.Header.Flags.DoesSampleExist() { + slen := fs.Header.Length + if fs.Header.Flags.Is16Bit() { + slen *= 2 + } + if fs.Header.Flags.IsStereo() { + slen *= 2 + } + + fs.Data = make([]byte, slen) + if err := readSampleData(data, fs.Header.SamplePointer, f.Head.TrackerCompatVersion, fs.Data); err != nil { + return nil, err + } + } + + f.Samples = append(f.Samples, fs) + } + + for _, ptr := range f.PatternPointers { + if ptr < valPos { + return nil, ErrInvalidFileFormat + } + + pat, err := readPackedPattern(data, ptr, f.Head.TrackerCompatVersion) + if err != nil { + return nil, ErrInvalidFileFormat + } + f.Patterns = append(f.Patterns, *pat) + } + + return &f, nil +} diff --git a/music/tracked/it/moduleheader.go b/music/tracked/it/moduleheader.go new file mode 100644 index 0000000..07b99f7 --- /dev/null +++ b/music/tracked/it/moduleheader.go @@ -0,0 +1,49 @@ +package it + +import ( + "encoding/binary" + "io" + + "github.com/gotracker/goaudiofile/internal/util" +) + +// ModuleHeader is the initial header definition of an IT file +type ModuleHeader struct { + IMPM [4]byte + Name [26]byte + PHighlight uint16 + OrderCount uint16 + InstrumentCount uint16 + SampleCount uint16 + PatternCount uint16 + TrackerVersion uint16 + TrackerCompatVersion uint16 + Flags IMPMFlags + SpecialFlags IMPMSpecialFlags + GlobalVolume FineVolume + MixingVolume FineVolume + InitialSpeed uint8 + InitialTempo uint8 + PanningSeparation PanSeparation + PitchWheelDepth uint8 + MessageLength uint16 + MessageOffset ParaPointer32 + Reserved3C [4]uint8 + ChannelPan [64]PanValue + ChannelVol [64]Volume +} + +// GetName returns a string representation of the data stored in the Name field +func (mh *ModuleHeader) GetName() string { + return util.GetString(mh.Name[:]) +} + +// ReadModuleHeader reads a ModuleHeader from the input stream +func ReadModuleHeader(r io.Reader) (*ModuleHeader, error) { + var mh ModuleHeader + if err := binary.Read(r, binary.LittleEndian, &mh); err != nil { + return nil, err + } + + return &mh, nil +} diff --git a/music/tracked/it/parapointer.go b/music/tracked/it/parapointer.go new file mode 100644 index 0000000..aff7801 --- /dev/null +++ b/music/tracked/it/parapointer.go @@ -0,0 +1,33 @@ +package it + +// ParaPointer is a pointer offset within the IT file format +type ParaPointer interface { + Offset() int +} + +// ParaPointer16 is a 16-bit pointer offset within the IT file format +type ParaPointer16 uint16 + +// Offset returns the actual offset +func (p ParaPointer16) Offset() int { + return int(p) << 4 +} + +// ParaPointer24 is a 24-bit pointer offset within the IT file format +type ParaPointer24 struct { + Hi uint8 + Lo ParaPointer16 +} + +// Offset returns the actual offset +func (p ParaPointer24) Offset() int { + return (int(p.Hi)<<16 | int(p.Lo)) << 4 +} + +// ParaPointer32 is a 32-bit pointer offset within the IT file format +type ParaPointer32 uint32 + +// Offset returns the actual offset +func (p ParaPointer32) Offset() int { + return int(p) +} diff --git a/music/tracked/it/pattern.go b/music/tracked/it/pattern.go new file mode 100644 index 0000000..f7a2c81 --- /dev/null +++ b/music/tracked/it/pattern.go @@ -0,0 +1,153 @@ +package it + +import ( + "bytes" + "encoding/binary" + "errors" +) + +// PackedPattern is a packed pattern from the IT format +type PackedPattern struct { + Length uint16 + Rows uint16 + Reserved04 [4]byte + Data []uint8 +} + +// ChannelData is the partially decoded channel data from the packed pattern +type ChannelData struct { + ChannelNumber int8 + Flags ChannelDataFlags + Note Note + Instrument uint8 + VolPan uint8 + Command uint8 + CommandData uint8 +} + +func readPackedPattern(data []byte, ptr ParaPointer, cmwt uint16) (*PackedPattern, error) { + ofs := ptr.Offset() + if ofs == 0 { + p := PackedPattern{ + Length: 64, + Rows: 64, + Data: make([]uint8, 64), // filled with zeroes is desired + } + return &p, nil + } + + var p PackedPattern + r := bytes.NewBuffer(data[ofs:]) + if err := binary.Read(r, binary.LittleEndian, &p.Length); err != nil { + return nil, err + } + + if err := binary.Read(r, binary.LittleEndian, &p.Rows); err != nil { + return nil, err + } + + if err := binary.Read(r, binary.LittleEndian, &p.Reserved04); err != nil { + return nil, err + } + + p.Data = make([]uint8, int(p.Length)) + if _, err := r.Read(p.Data); err != nil { + return nil, err + } + + return &p, nil +} + +// ReadChannelData decodes a packed pattern from the position indicated (`pos`) and returns an +// integer equal to the number of bytes used to decode the channel data, a ChannelData structure, +// and a possible error value. +// If there is no error and the row is completed, the ChannelData structure returned will be nil. +func (p *PackedPattern) ReadChannelData(pos int, rowMem []ChannelData) (int, *ChannelData, error) { + if pos > len(p.Data) { + return 0, nil, errors.New("position out of bounds") + } + + var cd ChannelData + + r := bytes.NewBuffer(p.Data[pos:]) + if r.Len() == 0 { + return 0, nil, nil + } + + s := 0 + var c uint8 + if err := binary.Read(r, binary.LittleEndian, &c); err != nil { + return 0, nil, err + } + s++ + + if c == 0 { + return s, nil, nil + } + + cd.ChannelNumber = (int8(c&^0x80) - 1) & 63 + + mem := &rowMem[int(cd.ChannelNumber)] + + if (c & 0x80) != 0 { + if err := binary.Read(r, binary.LittleEndian, &cd.Flags); err != nil { + return s, nil, err + } + mem.Flags = cd.Flags + s++ + } else { + cd.Flags = mem.Flags + } + + if cd.Flags.HasNote() { + if err := binary.Read(r, binary.LittleEndian, &cd.Note); err != nil { + return s, nil, err + } + mem.Note = cd.Note + s++ + } else if cd.Flags.IsUseLastNote() { + cd.Note = mem.Note + cd.Flags |= ChannelDataFlagNote + } + + if cd.Flags.HasInstrument() { + if err := binary.Read(r, binary.LittleEndian, &cd.Instrument); err != nil { + return s, nil, err + } + mem.Instrument = cd.Instrument + s++ + } else if cd.Flags.IsUseLastInstrument() { + cd.Instrument = mem.Instrument + cd.Flags |= ChannelDataFlagInstrument + } + + if cd.Flags.HasVolPan() { + if err := binary.Read(r, binary.LittleEndian, &cd.VolPan); err != nil { + return s, nil, err + } + mem.VolPan = cd.VolPan + s++ + } else if cd.Flags.IsUseLastVolPan() { + cd.VolPan = mem.VolPan + cd.Flags |= ChannelDataFlagVolPan + } + + if cd.Flags.HasCommand() { + if err := binary.Read(r, binary.LittleEndian, &cd.Command); err != nil { + return s, nil, err + } + mem.Command = cd.Command + s++ + if err := binary.Read(r, binary.LittleEndian, &cd.CommandData); err != nil { + return s, nil, err + } + mem.CommandData = cd.CommandData + s++ + } else if cd.Flags.IsUseLastCommand() { + cd.Command = mem.Command + cd.CommandData = mem.CommandData + cd.Flags |= ChannelDataFlagCommand + } + + return s, &cd, nil +} diff --git a/music/tracked/it/sample.go b/music/tracked/it/sample.go new file mode 100644 index 0000000..1b9c5a9 --- /dev/null +++ b/music/tracked/it/sample.go @@ -0,0 +1,65 @@ +package it + +import ( + "bytes" + "encoding/binary" + + "github.com/gotracker/goaudiofile/internal/util" +) + +// Sample is a sample from the IT format +type Sample struct { + IMPS [4]byte + Filename [12]byte + Reserved10 [1]byte + GlobalVolume Volume + Flags SampleFlags + Volume Volume + Name [26]byte + ConvertFlags ConvertFlags + DefaultPan SamplePanValue + Length uint32 + LoopBegin uint32 + LoopEnd uint32 + C5Speed uint32 + SustainLoopBegin uint32 + SustainLoopEnd uint32 + SamplePointer ParaPointer32 + VibratoSpeed uint8 + VibratoDepth uint8 + VibratoSweep uint8 + VibratoType uint8 +} + +// GetName returns a string representation of the data stored in the Name field +func (s *Sample) GetName() string { + return util.GetString(s.Name[:]) +} + +// GetFilename returns a string representation of the data stored in the Filename field +func (s *Sample) GetFilename() string { + return util.GetString(s.Filename[:]) +} + +func readIMPS(data []byte, ptr ParaPointer, cmwt uint16) (*Sample, error) { + ofs := ptr.Offset() + r := bytes.NewBuffer(data[ofs:]) + + sample := Sample{} + if err := binary.Read(r, binary.LittleEndian, &sample); err != nil { + return nil, err + } + + return &sample, nil +} + +func readSampleData(data []byte, ptr ParaPointer, cmwt uint16, out []byte) error { + ofs := ptr.Offset() + r := bytes.NewBuffer(data[ofs:]) + + if _, err := r.Read(out); err != nil { + return err + } + + return nil +} diff --git a/music/tracked/it/util.go b/music/tracked/it/util.go new file mode 100644 index 0000000..a4f35e7 --- /dev/null +++ b/music/tracked/it/util.go @@ -0,0 +1,209 @@ +package it + +// Volume defines a volume value +type Volume uint8 + +// Value returns the value of the volume as a floating point value between 0 and 1, inclusively +func (p Volume) Value() float32 { + switch { + case p >= 0 && p <= 64: + return float32(p) / 64 + default: + panic("unexpected value") + } +} + +// FineVolume defines a volume value with double precision +type FineVolume uint8 + +// Value returns the value of the fine volume as a floating point value between 0 and 1, inclusively +func (p FineVolume) Value() float32 { + switch { + case p >= 0 && p <= 128: + return float32(p) / 128 + default: + panic("unexpected value") + } +} + +const ( + // DefaultVolume is the default volume for many things in IT files + DefaultVolume = Volume(64) + + // DefaultFineVolume is the default volume for fine volumes in IT files + DefaultFineVolume = Volume(128) +) + +// PanSeparation is the panning separation value +type PanSeparation uint8 + +// Value returns the value of the panning separation as a floating point value between 0 and 1, inclusively +func (p PanSeparation) Value() float32 { + switch { + case p >= 0 && p <= 128: + return float32(p) / 128 + default: + panic("unexpected value") + } +} + +// PanValue describes a panning value in the IT format +type PanValue uint8 + +// IsSurround returns true if the panning is in surround-sound mode +func (p PanValue) IsSurround() bool { + return (p &^ 128) == 100 +} + +// IsDisabled returns true if the channel this panning value is attached to is muted +// Effects in muted channels are still processed +func (p PanValue) IsDisabled() bool { + return (p & 128) != 0 +} + +// Value returns the value of the panning as a floating point value between 0 and 1, inclusively +// 0 = absolute left, 0.5 = center, 1 = absolute right +func (p PanValue) Value() float32 { + pv := p &^ 128 + switch { + case pv >= 0 && pv <= 64: + return float32(pv) / 64 + case pv == 100: + return float32(0.5) + default: + panic("unexpected value") + } +} + +// SamplePanValue describes a panning value in the IT format's sample header +type SamplePanValue uint8 + +// IsSurround returns true if the panning is in surround-sound mode +func (p SamplePanValue) IsSurround() bool { + return (p &^ 128) == 100 +} + +// IsDisabled returns true if the channel this panning value is attached to is muted +// Effects in muted channels are still processed +func (p SamplePanValue) IsDisabled() bool { + return (p & 128) == 0 +} + +// Value returns the value of the panning as a floating point value between 0 and 1, inclusively +// 0 = absolute left, 0.5 = center, 1 = absolute right +func (p SamplePanValue) Value() float32 { + pv := p &^ 128 + switch { + case pv >= 0 && pv <= 64: + return float32(pv) / 64 + case pv == 100: + return float32(0.5) + default: + panic("unexpected value") + } +} + +// NewNoteAction is what to do when a new note occurs +type NewNoteAction uint8 + +const ( + // NewNoteActionCut means to cut the previous playback when a new note occurs + NewNoteActionCut = NewNoteAction(0) + // NewNoteActionContinue means to continue the previous playback when a new note occurs + NewNoteActionContinue = NewNoteAction(1) + // NewNoteActionOff means to note-off the previous playback when a new note occurs + NewNoteActionOff = NewNoteAction(2) + // NewNoteActionFade means to fade the previous playback when a new note occurs + NewNoteActionFade = NewNoteAction(3) +) + +// Percentage8 is a percentage stored as a uint8 +type Percentage8 uint8 + +// Value returns the value of the percentage +func (p Percentage8) Value() float32 { + return float32(p) / 100 +} + +// DuplicateCheckType is the duplicate check type +type DuplicateCheckType uint8 + +const ( + // DuplicateCheckTypeOff is for when the duplicate check type is disabled + DuplicateCheckTypeOff = DuplicateCheckType(0) + // DuplicateCheckTypeNote is for when the duplicate check type is set to note mode + DuplicateCheckTypeNote = DuplicateCheckType(1) + // DuplicateCheckTypeSample is for when the duplicate check type is set to sample mode + DuplicateCheckTypeSample = DuplicateCheckType(2) + // DuplicateCheckTypeInstrument is for when the duplicate check type is set to instrument mode + DuplicateCheckTypeInstrument = DuplicateCheckType(3) +) + +// DuplicateCheckAction is the action to perform when a duplicate is detected +type DuplicateCheckAction uint8 + +const ( + // DuplicateCheckActionCut cuts the playback when a duplicate is detected + DuplicateCheckActionCut = DuplicateCheckAction(0) + // DuplicateCheckActionOff performs a note-off on the playback when a duplicate is detected + DuplicateCheckActionOff = DuplicateCheckAction(1) + // DuplicateCheckActionFade performs a fade-out on the playback when a duplicate is detected + DuplicateCheckActionFade = DuplicateCheckAction(2) +) + +// NodePoint16 is a node point in the old instrument format +type NodePoint16 struct { + Tick uint8 + Magnitude uint8 +} + +// NodePoint24 is a node point in the new instrument format +type NodePoint24 struct { + Y int8 + Tick uint16 +} + +// Envelope is an envelope structure +type Envelope struct { + Flags EnvelopeFlags + Count uint8 + LoopBegin uint8 + LoopEnd uint8 + SustainLoopBegin uint8 + SustainLoopEnd uint8 + NodePoints [25]NodePoint24 + Reserved51 uint8 +} + +// DuplicateNoteCheck activates or deactivates the duplicate note checking +type DuplicateNoteCheck uint8 + +const ( + // DuplicateNoteCheckOff disables the duplicate note checking + DuplicateNoteCheckOff = DuplicateNoteCheck(0) + // DuplicateNoteCheckOn activates the duplicate note checking + DuplicateNoteCheckOn = DuplicateNoteCheck(1) +) + +// Note is a note field value +type Note uint8 + +// IsNoteOff returns true if the note is a note-off command +func (n Note) IsNoteOff() bool { + return n == 255 +} + +// IsNoteCut returns true if the note is a note-cut command +func (n Note) IsNoteCut() bool { + return n == 254 +} + +// IsNoteFade returns true if the note is a note-fade command +func (n Note) IsNoteFade() bool { + return n >= 120 && n < 254 +} + +// IsSpecial returns true if the note is actually a special value (see above) +func (n Note) IsSpecial() bool { + return n >= 120 +}