diff --git a/channel.go b/channel.go deleted file mode 100644 index 0f6e866..0000000 --- a/channel.go +++ /dev/null @@ -1,70 +0,0 @@ -package playback - -import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/voice" - - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" -) - -// Channel is an interface for channel state -type Channel[TMemory, TChannelData any] interface { - ResetRetriggerCount() - SetMemory(*TMemory) - GetMemory() *TMemory - GetActiveVolume() volume.Volume - SetActiveVolume(volume.Volume) - FreezePlayback() - UnfreezePlayback() - GetData() *TChannelData - GetPortaTargetPeriod() period.Period - SetPortaTargetPeriod(period.Period) - GetTargetPeriod() period.Period - SetTargetPeriod(period.Period) - SetPeriodOverride(period.Period) - GetPeriod() period.Period - SetPeriod(period.Period) - SetPeriodDelta(period.PeriodDelta) - GetPeriodDelta() period.PeriodDelta - SetInstrument(*instrument.Instrument) - GetInstrument() *instrument.Instrument - GetVoice() voice.Voice - GetTargetInst() *instrument.Instrument - SetTargetInst(*instrument.Instrument) - GetPrevInst() *instrument.Instrument - GetPrevVoice() voice.Voice - GetNoteSemitone() note.Semitone - SetStoredSemitone(note.Semitone) - SetTargetSemitone(note.Semitone) - SetOverrideSemitone(note.Semitone) - GetTargetPos() sampling.Pos - SetTargetPos(sampling.Pos) - GetPos() sampling.Pos - SetPos(sampling.Pos) - SetNotePlayTick(bool, note.Action, int) - GetRetriggerCount() uint8 - SetRetriggerCount(uint8) - SetPanEnabled(bool) - GetPan() panning.Position - SetPan(panning.Position) - SetRenderChannel(*render.Channel) - GetRenderChannel() *render.Channel - SetVolumeActive(bool) - SetGlobalVolume(volume.Volume) - SetChannelVolume(volume.Volume) - GetChannelVolume() volume.Volume - SetEnvelopePosition(int) - TransitionActiveToPastState() - SetNewNoteAction(note.Action) - GetNewNoteAction() note.Action - DoPastNoteEffect(action note.Action) - SetVolumeEnvelopeEnable(bool) - SetPanningEnvelopeEnable(bool) - SetPitchEnvelopeEnable(bool) - NoteCut() -} diff --git a/channelstate.go b/channelstate.go new file mode 100644 index 0000000..f1f04e1 --- /dev/null +++ b/channelstate.go @@ -0,0 +1,42 @@ +package playback + +import ( + "github.com/gotracker/gomixing/sampling" + + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/voice/types" +) + +// ChannelState is the information needed to make an instrument play +type ChannelState[TPeriod types.Period, TVolume types.Volume, TPanning types.Panning] struct { + Instrument instrument.InstrumentIntf + Period TPeriod + vol TVolume + Pos sampling.Pos + Pan TPanning +} + +// Reset sets the render state to defaults +func (s *ChannelState[TPeriod, TVolume, TPanning]) Reset() { + s.Instrument = nil + var emptyPeriod TPeriod + s.Period = emptyPeriod + s.Pos = sampling.Pos{} + var emptyPan TPanning + s.Pan = emptyPan +} + +func (s *ChannelState[TPeriod, TVolume, TPanning]) GetVolume() TVolume { + return s.vol +} + +func (s *ChannelState[TPeriod, TVolume, TPanning]) SetVolume(vol TVolume) { + if !vol.IsUseInstrumentVol() { + s.vol = vol + } +} + +func (s *ChannelState[TPeriod, TVolume, TPanning]) NoteCut() { + var empty TPeriod + s.Period = empty +} diff --git a/effect.go b/effect.go index 81c686e..4cb28fb 100644 --- a/effect.go +++ b/effect.go @@ -1,142 +1,116 @@ package playback -import "fmt" +import ( + "fmt" + "reflect" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/song" +) // Effect is an interface to command/effect type Effect interface { //fmt.Stringer + TraceData() string } -type effectPreStartIntf[TMemory, TChannelData any] interface { - PreStart(Channel[TMemory, TChannelData], Playback) error +type Effecter[TMemory song.ChannelMemory] interface { + GetEffects(TMemory, period.Period) []Effect } -// EffectPreStart triggers when the effect enters onto the channel state -func EffectPreStart[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChannelData], p Playback) error { - if eff, ok := e.(effectPreStartIntf[TMemory, TChannelData]); ok { - if err := eff.PreStart(cs, p); err != nil { - return err - } +func GetEffects[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](mem TMemory, d TChannelData) []Effect { + var e []Effect + if eff, ok := any(d).(Effecter[TMemory]); ok { + var p TPeriod + e = eff.GetEffects(mem, p) } - return nil + return e } -type effectStartIntf[TMemory, TChannelData any] interface { - Start(Channel[TMemory, TChannelData], Playback) error +type EffectNamer interface { + Names() []string } -// EffectStart triggers on the first tick, but before the Tick() function is called -func EffectStart[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChannelData], p Playback) error { - if eff, ok := e.(effectStartIntf[TMemory, TChannelData]); ok { - if err := eff.Start(cs, p); err != nil { - return err - } +func GetEffectNames(e Effect) []string { + if namer, ok := e.(EffectNamer); ok { + return namer.Names() + } else { + typ := reflect.TypeOf(e) + return []string{typ.Name()} } - return nil } -type effectTickIntf[TMemory, TChannelData any] interface { - Tick(Channel[TMemory, TChannelData], Playback, int) error +// CombinedEffect specifies multiple simultaneous effects into one +type CombinedEffect[TPeriod period.Period, TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume]] struct { + Effects []Effect } -// EffectTick is called on every tick -func EffectTick[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChannelData], p Playback, currentTick int) error { - if eff, ok := e.(effectTickIntf[TMemory, TChannelData]); ok { - if err := eff.Tick(cs, p, currentTick); err != nil { - return err +// String returns the string for the effect list +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) String() string { + for _, eff := range e.Effects { + s := fmt.Sprint(eff) + if s != "" { + return s } } - return nil + return "" } -type effectStopIntf[TMemory, TChannelData any] interface { - Stop(Channel[TMemory, TChannelData], Playback, int) error +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) Names() []string { + var names []string + for _, eff := range e.Effects { + names = append(names, GetEffectNames(eff)...) + } + return names } -// EffectStop is called on the last tick of the row, but after the Tick() function is called -func EffectStop[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChannelData], p Playback, lastTick int) error { - if eff, ok := e.(effectStopIntf[TMemory, TChannelData]); ok { - if err := eff.Stop(cs, p, lastTick); err != nil { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) OrderStart(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + for _, effect := range e.Effects { + if err := m.DoInstructionOrderStart(ch, effect); err != nil { return err } } return nil } -// CombinedEffect specifies multiple simultaneous effects into one -type CombinedEffect[TMemory, TChannelData any] struct { - Effects []Effect -} - -// PreStart triggers when the effect enters onto the channel state -func (e CombinedEffect[TMemory, TChannelData]) PreStart(cs Channel[TMemory, TChannelData], p Playback) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) RowStart(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { for _, effect := range e.Effects { - if err := EffectPreStart(effect, cs, p); err != nil { + if err := m.DoInstructionRowStart(ch, effect); err != nil { return err } } return nil } -// Start triggers on the first tick, but before the Tick() function is called -func (e CombinedEffect[TMemory, TChannelData]) Start(cs Channel[TMemory, TChannelData], p Playback) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) Tick(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error { for _, effect := range e.Effects { - if err := EffectStart(effect, cs, p); err != nil { + if err := m.DoInstructionTick(ch, effect); err != nil { return err } } return nil } -// Tick is called on every tick -func (e CombinedEffect[TMemory, TChannelData]) Tick(cs Channel[TMemory, TChannelData], p Playback, currentTick int) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) RowEnd(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { for _, effect := range e.Effects { - if err := EffectTick(effect, cs, p, currentTick); err != nil { + if err := m.DoInstructionRowEnd(ch, effect); err != nil { return err } } return nil } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e CombinedEffect[TMemory, TChannelData]) Stop(cs Channel[TMemory, TChannelData], p Playback, lastTick int) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) OrderEnd(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { for _, effect := range e.Effects { - if err := EffectStop(effect, cs, p, lastTick); err != nil { + if err := m.DoInstructionOrderEnd(ch, effect); err != nil { return err } } return nil } -// String returns the string for the effect list -func (e CombinedEffect[TMemory, TChannelData]) String() string { - for _, eff := range e.Effects { - s := fmt.Sprint(eff) - if s != "" { - return s - } - } - return "" -} - -// DoEffect runs the standard tick lifetime of an effect -func DoEffect[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChannelData], p Playback, currentTick int, lastTick bool) error { - if e == nil { - return nil - } - - if currentTick == 0 { - if err := EffectStart(e, cs, p); err != nil { - return err - } - } - if err := EffectTick(e, cs, p, currentTick); err != nil { - return err - } - if lastTick { - if err := EffectStop(e, cs, p, currentTick); err != nil { - return err - } - } - return nil +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) TraceData() string { + return e.String() } diff --git a/filter/amigafilter.go b/filter/amigafilter.go index 703c48b..c8e6895 100644 --- a/filter/amigafilter.go +++ b/filter/amigafilter.go @@ -4,76 +4,56 @@ import ( "math" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) -type channelData struct { +type amigaLPFChannelData struct { ynz1 volume.Volume ynz2 volume.Volume } // AmigaLPF is a 12dB/octave 2-pole Butterworth Low-Pass Filter with 3275 Hz cut-off type AmigaLPF struct { - channels []channelData + channels []amigaLPFChannelData a0 volume.Volume b0 volume.Volume b1 volume.Volume - playbackRate period.Frequency + playbackRate frequency.Frequency } // NewAmigaLPF creates a new AmigaLPF -func NewAmigaLPF(instrument, playback period.Frequency) *AmigaLPF { - lpf := AmigaLPF{ - playbackRate: playback, - } - lpf.recalculate() - - return &lpf +func NewAmigaLPF(instrument frequency.Frequency) *AmigaLPF { + var f AmigaLPF + f.SetPlaybackRate(instrument) + return &f } func (f *AmigaLPF) Clone() Filter { c := *f - c.channels = make([]channelData, len(f.channels)) + c.channels = make([]amigaLPFChannelData, len(f.channels)) for i := range f.channels { c.channels[i] = f.channels[i] } return &c } -// Filter processes incoming (dry) samples and produces an outgoing filtered (wet) result -func (f *AmigaLPF) Filter(dry volume.Matrix) volume.Matrix { - if dry.Channels == 0 { - return volume.Matrix{} - } - wet := dry // we can update in-situ and be ok - for i := 0; i < dry.Channels; i++ { - s := dry.StaticMatrix[i] - for len(f.channels) <= i { - f.channels = append(f.channels, channelData{}) - } - c := &f.channels[i] - - xn := s - yn := (xn*f.a0 + c.ynz1*f.b0 + c.ynz2*f.b1) - c.ynz2 = c.ynz1 - c.ynz1 = yn - wet.StaticMatrix[i] = yn +func (f *AmigaLPF) SetPlaybackRate(playback frequency.Frequency) { + if f.playbackRate == playback { + return } - return wet -} + f.playbackRate = playback -func (f *AmigaLPF) recalculate() { freq := 3275.0 - f2 := float64(f.playbackRate) / 2.0 + f2 := float64(playback) / 2.0 if freq > f2 { freq = f2 } fc := freq * 2.0 * math.Pi - r := float64(f.playbackRate) / fc + r := float64(playback) / fc d := r e := r * r @@ -87,6 +67,28 @@ func (f *AmigaLPF) recalculate() { f.b1 = volume.Volume(c) } +// Filter processes incoming (dry) samples and produces an outgoing filtered (wet) result +func (f *AmigaLPF) Filter(dry volume.Matrix) volume.Matrix { + if dry.Channels == 0 { + return volume.Matrix{} + } + wet := dry // we can update in-situ and be ok + for i := 0; i < dry.Channels; i++ { + s := dry.StaticMatrix[i] + for len(f.channels) <= i { + f.channels = append(f.channels, amigaLPFChannelData{}) + } + c := &f.channels[i] + + xn := s + yn := min(max(xn*f.a0+c.ynz1*f.b0+c.ynz2*f.b1, -1), 1) + c.ynz2 = c.ynz1 + c.ynz1 = yn + wet.StaticMatrix[i] = yn + } + return wet +} + // UpdateEnv updates the filter with the value from the filter envelope -func (f *AmigaLPF) UpdateEnv(v int8) { +func (f *AmigaLPF) UpdateEnv(v uint8) { } diff --git a/filter/echofilter.go b/filter/echofilter.go index 62914c4..27b7eb7 100644 --- a/filter/echofilter.go +++ b/filter/echofilter.go @@ -3,9 +3,8 @@ package filter import ( "math" - "github.com/gotracker/playback/period" - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" ) type EchoFilterSettings struct { @@ -22,17 +21,15 @@ type EchoFilterFactory struct { } func (e *EchoFilterFactory) Factory() Factory { - return func(instrument, playback period.Frequency) Filter { + return func(instrument frequency.Frequency) Filter { echo := EchoFilter{ EchoFilterSettings: e.EchoFilterSettings, - sampleRate: playback, } - echo.recalculate() return &echo } } -type delayInfo struct { +type echoFilterDelayInfo struct { buf []volume.Volume delay int } @@ -41,20 +38,37 @@ type delayInfo struct { type EchoFilter struct { EchoFilterSettings - sampleRate period.Frequency initialFeedback volume.Volume writePos int - delay [2]delayInfo // L,R + delay [2]echoFilterDelayInfo // L,R + playbackRate frequency.Frequency +} + +func (e *EchoFilter) SetPlaybackRate(playback frequency.Frequency) { + if e.playbackRate == playback { + return + } + e.playbackRate = playback + + e.initialFeedback = volume.Volume(math.Sqrt(float64(1.0 - (e.Feedback * e.Feedback)))) + + playbackRate := float32(playback) + bufferSize := int(playbackRate * 2) + + for c, delayMs := range [2]float32{e.LeftDelay, e.RightDelay} { + delay := int(delayMs * 2.0 * playbackRate) + e.delay[c].delay = delay + e.delay[c].buf = make([]volume.Volume, bufferSize) + } } func (e *EchoFilter) Clone() Filter { clone := EchoFilter{ EchoFilterSettings: e.EchoFilterSettings, - sampleRate: e.sampleRate, writePos: e.writePos, } - clone.recalculate() for i := range clone.delay { + clone.delay[i].buf = make([]volume.Volume, len(e.delay[i].buf)) copy(clone.delay[i].buf, e.delay[i].buf) } return &clone @@ -113,19 +127,6 @@ func (e *EchoFilter) Filter(dry volume.Matrix) volume.Matrix { return wet } -func (e *EchoFilter) recalculate() { - e.initialFeedback = volume.Volume(math.Sqrt(float64(1.0 - (e.Feedback * e.Feedback)))) - - playbackRate := float32(e.sampleRate) - bufferSize := int(playbackRate * 2) - - for c, delayMs := range [2]float32{e.LeftDelay, e.RightDelay} { - delay := int(delayMs * 2.0 * playbackRate) - e.delay[c].delay = delay - e.delay[c].buf = make([]volume.Volume, bufferSize) - } -} - -func (e *EchoFilter) UpdateEnv(val int8) { +func (e *EchoFilter) UpdateEnv(val uint8) { } diff --git a/filter/filter.go b/filter/filter.go index ddf2274..5ef5a56 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -2,15 +2,21 @@ package filter import ( "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) +type Info struct { + Name string + Params any +} + // Filter is an interface to a filter type Filter interface { Filter(volume.Matrix) volume.Matrix - UpdateEnv(int8) + SetPlaybackRate(playbackRate frequency.Frequency) + UpdateEnv(uint8) Clone() Filter } // Factory is a function type that builds a filter with an input parameter taking a value between 0 and 1 -type Factory func(instrument, playback period.Frequency) Filter +type Factory func(instrument frequency.Frequency) Filter diff --git a/format/it/filter/resonantfilter.go b/filter/it_resonantfilter.go similarity index 58% rename from format/it/filter/resonantfilter.go rename to filter/it_resonantfilter.go index 5e7a035..a04e67e 100644 --- a/format/it/filter/resonantfilter.go +++ b/filter/it_resonantfilter.go @@ -4,20 +4,19 @@ import ( "math" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" "github.com/heucuva/optional" ) -type channelData struct { +type itResonantFilterChannelData struct { ynz1 volume.Volume ynz2 volume.Volume } // ResonantFilter is a modified 2-pole resonant filter type ResonantFilter struct { - channels []channelData + channels []itResonantFilterChannelData a0 volume.Volume b0 volume.Volume b1 volume.Volume @@ -25,15 +24,25 @@ type ResonantFilter struct { enabled bool resonance optional.Value[uint8] cutoff optional.Value[uint8] - playbackRate period.Frequency highpass bool extendedFilterRange bool + + f2 float64 + fr float64 + efr float64 + playbackRate frequency.Frequency +} + +type ITResonantFilterParams struct { + Cutoff uint8 + Resonance uint8 + ExtendedFilterRange bool + Highpass bool } -// NewResonantFilter creates a new resonant filter with the provided cutoff and resonance values -func NewResonantFilter(cutoff uint8, resonance uint8, playbackRate period.Frequency, extendedFilterRange bool, highpass bool) filter.Filter { - rf := &ResonantFilter{ - playbackRate: playbackRate, +// NewITResonantFilter creates a new resonant filter with the provided cutoff and resonance values +func NewITResonantFilter(cutoff uint8, resonance uint8, extendedFilterRange bool, highpass bool) Filter { + rf := ResonantFilter{ highpass: highpass, extendedFilterRange: extendedFilterRange, } @@ -47,13 +56,32 @@ func NewResonantFilter(cutoff uint8, resonance uint8, playbackRate period.Freque rf.cutoff.Set(uint8(c)) } - rf.recalculate(int8(c)) - return rf + return &rf } -func (f *ResonantFilter) Clone() filter.Filter { +func (f *ResonantFilter) SetPlaybackRate(playback frequency.Frequency) { + if f.playbackRate == playback { + return + } + f.playbackRate = playback + + f.f2 = float64(playback) / 2.0 + + f.fr = float64(playback) + if f.fr != 0 { + f.efr = float64(1) / f.fr + } + + c := uint8(0x7F) + if v, set := f.cutoff.Get(); set { + c = v + } + f.recalculate(c) +} + +func (f *ResonantFilter) Clone() Filter { c := *f - c.channels = make([]channelData, len(f.channels)) + c.channels = make([]itResonantFilterChannelData, len(f.channels)) for i := range f.channels { c.channels[i] = f.channels[i] } @@ -69,7 +97,7 @@ func (f *ResonantFilter) Filter(dry volume.Matrix) volume.Matrix { for i := 0; i < dry.Channels; i++ { s := dry.StaticMatrix[i] for len(f.channels) <= i { - f.channels = append(f.channels, channelData{}) + f.channels = append(f.channels, itResonantFilterChannelData{}) } c := &f.channels[i] @@ -77,6 +105,12 @@ func (f *ResonantFilter) Filter(dry volume.Matrix) volume.Matrix { if f.enabled { yn *= f.a0 yn += c.ynz1*f.b0 + c.ynz2*f.b1 + if yn < -1 { + yn = -1 + } + if yn > 1 { + yn = 1 + } } c.ynz2 = c.ynz1 c.ynz1 = yn @@ -88,7 +122,7 @@ func (f *ResonantFilter) Filter(dry volume.Matrix) volume.Matrix { return wet } -func (f *ResonantFilter) recalculate(v int8) { +func (f *ResonantFilter) recalculate(v uint8) { cutoff, useCutoff := f.cutoff.Get() resonance, useResonance := f.resonance.Get() @@ -99,17 +133,15 @@ func (f *ResonantFilter) recalculate(v int8) { if !useCutoff { cutoff = 127 } else { - cutoff = uint8(v) - if cutoff < 0 { - cutoff = 0 - } else if cutoff > 127 { + cutoff = v + if cutoff > 127 { cutoff = 127 } - f.cutoff.Set(uint8(cutoff)) + f.cutoff.Set(cutoff) } - computedCutoff := int(cutoff) * 2 + computedCutoff := cutoff * 2 useFilter := true if computedCutoff >= 254 && resonance == 0 { @@ -134,35 +166,37 @@ func (f *ResonantFilter) recalculate(v int8) { const dampingFactorDivisor = ((24.0 / 128.0) / 20.0) dampingFactor := math.Pow(10.0, -float64(resonance)*dampingFactorDivisor) - f2 := float64(f.playbackRate) / 2.0 - freq := f2 - if computedCutoff < 254 { - fcComputedCutoff := float64(computedCutoff) - freq = 110.0 * math.Pow(2.0, 0.25+(fcComputedCutoff/filterRange)) - if freq < 120.0 { - freq = 120.0 - } else if freq > 20000 { - freq = 20000 - } + freq := f.f2 + fcComputedCutoff := float64(computedCutoff) + freq = 110.0 * math.Pow(2.0, 0.25+(fcComputedCutoff/filterRange)) + if freq < 120.0 { + freq = 120.0 + } else if freq > 20000 { + freq = 20000 } - if freq > f2 { - freq = f2 + if freq > f.f2 && f.f2 >= 120.0 { + freq = f.f2 } fc := freq * 4.0 * math.Pi var d, e float64 if f.extendedFilterRange { - r := fc / float64(f.playbackRate) + r := fc * f.efr d = (1.0 - 2.0*dampingFactor) * r if d > 2.0 { d = 2.0 } - d = (2.0*dampingFactor - d) / r - e = 1.0 / (r * r) + if r != 0 { + d = (2.0*dampingFactor - d) / r + e = 1.0 / (r * r) + } else { + d = 0 + e = 0 + } } else { - r := float64(f.playbackRate) / fc + r := f.fr / fc d = dampingFactor*r + dampingFactor - 1.0 e = r * r @@ -181,12 +215,22 @@ func (f *ResonantFilter) recalculate(v int8) { } } + if math.IsNaN(a) { + panic("a") + } + if math.IsNaN(b) { + panic("b") + } + if math.IsNaN(c) { + panic("c") + } + f.a0 = volume.Volume(a) f.b0 = volume.Volume(b) f.b1 = volume.Volume(c) } // UpdateEnv updates the filter with the value from the filter envelope -func (f *ResonantFilter) UpdateEnv(cutoff int8) { +func (f *ResonantFilter) UpdateEnv(cutoff uint8) { f.recalculate(cutoff) } diff --git a/format.go b/format.go deleted file mode 100644 index 9dcede3..0000000 --- a/format.go +++ /dev/null @@ -1,13 +0,0 @@ -package playback - -import ( - "io" - - "github.com/gotracker/playback/player/feature" -) - -// Format is an interface to a music file format loader -type Format[TChannelData any] interface { - Load(filename string, features []feature.Feature) (Playback, error) - LoadFromReader(r io.Reader, features []feature.Feature) (Playback, error) -} diff --git a/format/common/basesong.go b/format/common/basesong.go new file mode 100644 index 0000000..ba5f2e0 --- /dev/null +++ b/format/common/basesong.go @@ -0,0 +1,187 @@ +package common + +import ( + "reflect" + "time" + + "github.com/gotracker/gomixing/volume" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/system" + "github.com/gotracker/playback/voice/types" +) + +type BaseSong[TPeriod types.Period, TGlobalVolume, TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { + System system.System + MS *settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + + Name string + InitialBPM int + InitialTempo int + GlobalVolume TGlobalVolume + MixingVolume TMixingVolume + InitialOrder index.Order + + Instruments []*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] + Patterns []song.Pattern + OrderList []index.Pattern +} + +func (BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetPeriodType() reflect.Type { + var p TPeriod + return reflect.TypeOf(p) +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetGlobalVolumeType() reflect.Type { + return reflect.TypeOf(s.GlobalVolume) +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelMixingVolumeType() reflect.Type { + return reflect.TypeOf(s.MixingVolume) +} + +func (BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelVolumeType() reflect.Type { + var v TVolume + return reflect.TypeOf(v) +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelPanningType() reflect.Type { + var p TPanning + return reflect.TypeOf(p) +} + +// GetOrderList returns the list of all pattern orders for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetOrderList() []index.Pattern { + return s.OrderList +} + +// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetInitialBPM() int { + return s.InitialBPM +} + +// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetInitialTempo() int { + return s.InitialTempo +} + +// GetGlobalVolumeGeneric returns the initial global volume for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetGlobalVolumeGeneric() volume.Volume { + return s.GlobalVolume.ToVolume() +} + +// GetGlobalVolume returns the initial global volume for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetGlobalVolume() TGlobalVolume { + return s.GlobalVolume +} + +// GetMixingVolumeGeneric returns the initial mixing volume for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetMixingVolumeGeneric() volume.Volume { + return s.MixingVolume.ToVolume() +} + +// GetMixingVolume returns the initial mixing volume for the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetMixingVolume() TMixingVolume { + return s.MixingVolume +} + +const durationPerBpm = time.Duration(2500) * time.Millisecond + +// GetTickDuration calculates the duration of a tick at a particular BPM +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetTickDuration(bpm int) time.Duration { + if bpm == 0 { + return 0 + } + + return durationPerBpm / time.Duration(bpm) +} + +// GetPattern returns a specific pattern indexed by `patNum` +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetPattern(patNum index.Pattern) (song.Pattern, error) { + if int(patNum) >= len(s.Patterns) { + return nil, song.ErrStopSong + } + return s.Patterns[patNum], nil +} + +// GetPatternByOrder returns the pattern specified by the order index provided +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetPatternByOrder(o index.Order) (song.Pattern, error) { + if int(o) >= len(s.OrderList) { + return nil, song.ErrStopSong + } + + pat := s.OrderList[o] + switch pat { + case index.InvalidPattern: + return nil, song.ErrStopSong + case index.NextPattern: + return nil, index.ErrNextPattern + } + + return s.GetPattern(pat) +} + +// GetNumChannels returns the number of channels the song has +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetNumChannels() int { + panic("unimplemented") +} + +// GetChannelSettings returns the channel settings at index `channelNum` +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + panic("unimplemented") +} + +// NumInstruments returns the number of instruments in the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) NumInstruments() int { + return len(s.Instruments) +} + +// GetInstrument returns the instrument interface indexed by `instNum` (0-based) +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetInstrument(instID int, st note.Semitone) (instrument.InstrumentIntf, note.Semitone) { + if instID == 0 { + return nil, st + } + idx := instID - 1 + if idx >= len(s.Instruments) { + return nil, st + } + return s.Instruments[idx], st +} + +// GetName returns the name of the song +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetName() string { + return s.Name +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetPeriodCalculator() song.PeriodCalculatorIntf { + return s.MS.PeriodConverter +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetInitialOrder() index.Order { + return s.InitialOrder +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetRowRenderStringer(row song.Row, channels int, longFormat bool) render.RowStringer { + panic("unimplemented") +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetSystem() system.System { + return s.System +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { + panic("unimplemented") +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IsOPL2Enabled() bool { + return false +} + +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetMachineSettings() any { + return s.MS +} diff --git a/format/common/format.go b/format/common/format.go new file mode 100644 index 0000000..bdbb9e4 --- /dev/null +++ b/format/common/format.go @@ -0,0 +1,35 @@ +package common + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine/settings" +) + +type Format struct{} + +func (Format) ConvertFeaturesToSettings(us *settings.UserSettings, features []feature.Feature) error { + for _, feat := range features { + switch f := feat.(type) { + case feature.SongLoop: + us.SongLoopCount = f.Count + case feature.StartOrderAndRow: + if o, set := f.Order.Get(); set { + us.Start.Order.Set(index.Order(o)) + } + if r, set := f.Row.Get(); set { + us.Start.Row.Set(index.Row(r)) + } + case feature.PlayUntilOrderAndRow: + us.PlayUntil.Order.Set(index.Order(f.Order)) + us.PlayUntil.Row.Set(index.Row(f.Row)) + case feature.SetDefaultTempo: + us.Start.Tempo = f.Tempo + case feature.SetDefaultBPM: + us.Start.BPM = f.BPM + case feature.IgnoreUnknownEffect: + us.IgnoreUnknownEffect = f.Enabled + } + } + return nil +} diff --git a/format/common/loadformat.go b/format/common/loadformat.go index 6afd91d..bd98d53 100644 --- a/format/common/loadformat.go +++ b/format/common/loadformat.go @@ -4,19 +4,11 @@ import ( "io" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" ) -type ReaderFunc[TSong any] func(r io.Reader, features []feature.Feature) (*TSong, error) +type ReaderFunc func(r io.Reader, features []feature.Feature) (song.Data, error) -type ManagerFactory[TSong, TManager any] func(*TSong) (*TManager, error) - -func Load[TSong, TManager any](r io.Reader, reader ReaderFunc[TSong], factory ManagerFactory[TSong, TManager], features []feature.Feature) (*TManager, error) { - song, err := reader(r, features) - if err != nil { - return nil, err - } - - m, err := factory(song) - - return m, err +func Load(r io.Reader, reader ReaderFunc, features []feature.Feature) (song.Data, error) { + return reader(r, features) } diff --git a/format/format.go b/format/format.go index a3ab28e..1ecb71c 100644 --- a/format/format.go +++ b/format/format.go @@ -5,24 +5,31 @@ import ( "io" "os" - "github.com/gotracker/playback" "github.com/gotracker/playback/format/it" "github.com/gotracker/playback/format/mod" "github.com/gotracker/playback/format/s3m" "github.com/gotracker/playback/format/xm" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/song" ) +// Format is an interface to a music file format loader +type Format interface { + Load(filename string, features []feature.Feature) (song.Data, error) + LoadFromReader(r io.Reader, features []feature.Feature) (song.Data, error) + ConvertFeaturesToSettings(us *settings.UserSettings, features []feature.Feature) error +} + var ( - supportedFormats = make(map[string]playback.Format[song.ChannelData]) + supportedFormats = make(map[string]Format) ) // Load loads the a file into a playback manager -func Load(filename string, features ...feature.Feature) (playback.Playback, playback.Format[song.ChannelData], error) { +func Load(filename string, features ...feature.Feature) (song.Data, Format, error) { for _, f := range supportedFormats { - if pb, err := f.Load(filename, features); err == nil { - return pb, f, nil + if s, err := f.Load(filename, features); err == nil { + return s, f, nil } else if os.IsNotExist(err) { return nil, nil, err } @@ -31,7 +38,7 @@ func Load(filename string, features ...feature.Feature) (playback.Playback, play } // LoadFromReader loads a song file on a reader into a playback manager -func LoadFromReader(format string, r io.ReadSeeker, features ...feature.Feature) (playback.Playback, playback.Format[song.ChannelData], error) { +func LoadFromReader(format string, r io.ReadSeeker, features ...feature.Feature) (song.Data, Format, error) { pos, _ := r.Seek(0, io.SeekCurrent) if format != "" { f, ok := supportedFormats[format] @@ -40,8 +47,8 @@ func LoadFromReader(format string, r io.ReadSeeker, features ...feature.Feature) } _, _ = r.Seek(pos, io.SeekStart) - if pb, err := f.LoadFromReader(r, features); err == nil { - return pb, f, nil + if s, err := f.LoadFromReader(r, features); err == nil { + return s, f, nil } else { return nil, nil, err } @@ -49,8 +56,8 @@ func LoadFromReader(format string, r io.ReadSeeker, features ...feature.Feature) for _, f := range supportedFormats { _, _ = r.Seek(pos, io.SeekStart) - if pb, err := f.LoadFromReader(r, features); err == nil { - return pb, f, nil + if s, err := f.LoadFromReader(r, features); err == nil { + return s, f, nil } else if os.IsNotExist(err) { return nil, nil, err } diff --git a/format/it/channel/data.go b/format/it/channel/data.go index 4896593..ad78b7d 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -7,10 +7,16 @@ import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback" itNote "github.com/gotracker/playback/format/it/note" + itPanning "github.com/gotracker/playback/format/it/panning" itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/instruction" + "github.com/gotracker/playback/song" ) const MaxTotalChannels = 64 @@ -30,7 +36,7 @@ func (c Command) ToRune() rune { type DataEffect uint8 // Data is the data for the channel -type Data struct { +type Data[TPeriod period.Period] struct { What itfile.ChannelDataFlags Note itfile.Note Instrument uint8 @@ -40,37 +46,27 @@ type Data struct { } // HasNote returns true if there exists a note on the channel -func (d Data) HasNote() bool { +func (d Data[TPeriod]) HasNote() bool { return d.What.HasNote() } // GetNote returns the note for the channel -func (d Data) GetNote() note.Note { +func (d Data[TPeriod]) GetNote() note.Note { return itNote.FromItNote(d.Note) } // HasInstrument returns true if there exists an instrument on the channel -func (d Data) HasInstrument() bool { +func (d Data[TPeriod]) HasInstrument() bool { return d.What.HasInstrument() } // GetInstrument returns the instrument for the channel -func (d Data) GetInstrument(stmem note.Semitone) instrument.ID { - st := stmem - if d.HasNote() { - n := d.GetNote() - if nn, ok := n.(note.Normal); ok { - st = note.Semitone(nn) - } - } - return SampleID{ - InstID: d.Instrument, - Semitone: st, - } +func (d Data[TPeriod]) GetInstrument() int { + return int(d.Instrument) } // HasVolume returns true if there exists a volume on the channel -func (d Data) HasVolume() bool { +func (d Data[TPeriod]) HasVolume() bool { if !d.What.HasVolPan() { return false } @@ -80,12 +76,16 @@ func (d Data) HasVolume() bool { } // GetVolume returns the volume for the channel -func (d Data) GetVolume() volume.Volume { +func (d Data[TPeriod]) GetVolumeGeneric() volume.Volume { return itVolume.FromVolPan(d.VolPan) } +func (d Data[TPeriod]) GetVolume() itVolume.Volume { + return itVolume.Volume(d.VolPan) +} + // HasCommand returns true if there exists a effect on the channel -func (d Data) HasCommand() bool { +func (d Data[TPeriod]) HasCommand() bool { if d.What.HasCommand() { return true } @@ -98,16 +98,25 @@ func (d Data) HasCommand() bool { } // Channel returns the channel ID for the channel -func (d Data) Channel() uint8 { +func (d Data[TPeriod]) Channel() uint8 { return 0 } -func (Data) getNoteString(n note.Note) string { +func (d Data[TPeriod]) GetEffects(mem *Memory) []playback.Effect { + if e := EffectFactory[TPeriod](mem, d); e != nil { + return []playback.Effect{e} + } + return nil +} + +func (Data[TPeriod]) getNoteString(n note.Note) string { switch note.Type(n) { case note.SpecialTypeRelease: return "===" case note.SpecialTypeStop: return "^^^" + case note.SpecialTypeFadeout: + return "vvv" case note.SpecialTypeNormal: return n.String() default: @@ -115,7 +124,7 @@ func (Data) getNoteString(n note.Note) string { } } -func (d Data) String() string { +func (d Data[TPeriod]) String() string { pieces := []string{ "...", // note "..", // inst @@ -137,9 +146,24 @@ func (d Data) String() string { return strings.Join(pieces, " ") } -func (d Data) ShortString() string { +func (d Data[TPeriod]) ShortString() string { if d.HasNote() { return d.GetNote().String() } return "..." } + +func (d Data[TPeriod]) ToInstructions(m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], ch index.Channel, songData song.Data) ([]instruction.Instruction, error) { + var instructions []instruction.Instruction + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return nil, err + } + + if e := EffectFactory[TPeriod](mem, d); e != nil { + instructions = append(instructions, e) + } + + return instructions, nil +} diff --git a/format/it/channel/effect_arpeggio.go b/format/it/channel/effect_arpeggio.go new file mode 100644 index 0000000..84e4998 --- /dev/null +++ b/format/it/channel/effect_arpeggio.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Arpeggio defines an arpeggio effect +type Arpeggio[TPeriod period.Period] DataEffect // 'J' + +func (e Arpeggio[TPeriod]) String() string { + return fmt.Sprintf("J%0.2x", DataEffect(e)) +} + +func (e Arpeggio[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.Arpeggio(DataEffect(e)) + return doArpeggio(ch, m, tick, int8(x), int8(y)) +} + +func (e Arpeggio[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_channelvolumeslide.go b/format/it/channel/effect_channelvolumeslide.go new file mode 100644 index 0000000..2e28374 --- /dev/null +++ b/format/it/channel/effect_channelvolumeslide.go @@ -0,0 +1,40 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ChannelVolumeSlide defines a set channel volume effect +type ChannelVolumeSlide[TPeriod period.Period] DataEffect // 'Nxy' + +func (e ChannelVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("N%0.2x", DataEffect(e)) +} + +func (e ChannelVolumeSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.ChannelVolumeSlide(DataEffect(e)) + switch { + case y == 0x0 && x != 0xF: + // slide up + return m.SlideChannelMixingVolume(ch, 1, float32(x)) + case y != 0xF && x == 0x0: + // slide down + return m.SlideChannelMixingVolume(ch, 1, -float32(y)) + default: + return nil + } +} + +func (e ChannelVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_extrafineportadown.go b/format/it/channel/effect_extrafineportadown.go new file mode 100644 index 0000000..c3107f6 --- /dev/null +++ b/format/it/channel/effect_extrafineportadown.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ExtraFinePortaDown defines an extra-fine portamento down effect +type ExtraFinePortaDown[TPeriod period.Period] DataEffect // 'EEx' + +func (e ExtraFinePortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e ExtraFinePortaDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := mem.PortaDown(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(y)*1) +} + +func (e ExtraFinePortaDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_extrafineportaup.go b/format/it/channel/effect_extrafineportaup.go new file mode 100644 index 0000000..6c87bd3 --- /dev/null +++ b/format/it/channel/effect_extrafineportaup.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ExtraFinePortaUp defines an extra-fine portamento up effect +type ExtraFinePortaUp[TPeriod period.Period] DataEffect // 'FEx' + +func (e ExtraFinePortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e ExtraFinePortaUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := mem.PortaUp(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + return m.DoChannelPortaUp(ch, period.Delta(y)*1) +} + +func (e ExtraFinePortaUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_finepatterndelay.go b/format/it/channel/effect_finepatterndelay.go new file mode 100644 index 0000000..9d1f59c --- /dev/null +++ b/format/it/channel/effect_finepatterndelay.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePatternDelay defines an fine pattern delay effect +type FinePatternDelay[TPeriod period.Period] DataEffect // 'S6x' + +func (e FinePatternDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e FinePatternDelay[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + x := DataEffect(e) & 0x0F + return m.AddExtraTicks(int(x)) +} + +func (e FinePatternDelay[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_fineportadown.go b/format/it/channel/effect_fineportadown.go new file mode 100644 index 0000000..4787218 --- /dev/null +++ b/format/it/channel/effect_fineportadown.go @@ -0,0 +1,38 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + "github.com/gotracker/playback/format/it/system" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePortaDown defines an fine portamento down effect +type FinePortaDown[TPeriod period.Period] DataEffect // 'EFx' + +func (e FinePortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e FinePortaDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := mem.PortaDown(DataEffect(e)) & 0x0F + + return m.DoChannelPortaDown(ch, period.Delta(y)*system.SlideFinesPerSemitone) +} + +func (e FinePortaDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_fineportaup.go b/format/it/channel/effect_fineportaup.go new file mode 100644 index 0000000..9bab721 --- /dev/null +++ b/format/it/channel/effect_fineportaup.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePortaUp defines an fine portamento up effect +type FinePortaUp[TPeriod period.Period] DataEffect // 'FFx' + +func (e FinePortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e FinePortaUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := mem.PortaUp(DataEffect(e)) + + if tick != 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(y)*4) +} + +func (e FinePortaUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_finevibrato.go b/format/it/channel/effect_finevibrato.go new file mode 100644 index 0000000..409d293 --- /dev/null +++ b/format/it/channel/effect_finevibrato.go @@ -0,0 +1,38 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVibrato defines an fine vibrato effect +type FineVibrato[TPeriod period.Period] DataEffect // 'U' + +func (e FineVibrato[TPeriod]) String() string { + return fmt.Sprintf("U%0.2x", DataEffect(e)) +} + +func (e FineVibrato[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.Vibrato(DataEffect(e)) + + if tick == 0 { + return nil + } + + return withOscillatorDo(ch, m, int(x), float32(y)*1, machine.OscillatorVibrato, func(value float32) error { + return m.SetChannelPeriodDelta(ch, period.Delta(value)) + }) +} + +func (e FineVibrato[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_finevolslidedown.go b/format/it/channel/effect_finevolslidedown.go new file mode 100644 index 0000000..fee402f --- /dev/null +++ b/format/it/channel/effect_finevolslidedown.go @@ -0,0 +1,61 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVolumeSlideDown defines a fine volume slide down effect +type FineVolumeSlideDown[TPeriod period.Period] DataEffect // 'D' + +func (e FineVolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e FineVolumeSlideDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + _, y := mem.VolumeSlide(DataEffect(e)) + if y != 0x0F && tick == 0 { + return m.SlideChannelVolume(ch, 1.0, -float32(y)) + } + return nil +} + +func (e FineVolumeSlideDown[TPeriod]) TraceData() string { + return e.String() +} + +//==================================================== + +// VolChanFineVolumeSlideDown defines a fine volume slide down effect (from the volume channel) +type VolChanFineVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' + +func (e VolChanFineVolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("dF%x", DataEffect(e)) +} + +func (e VolChanFineVolumeSlideDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + _, y := mem.VolumeSlide(DataEffect(e)) + if tick == 0 { + return m.SlideChannelVolume(ch, 1.0, -float32(y)) + } + return nil +} + +func (e VolChanFineVolumeSlideDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_finevolslideup.go b/format/it/channel/effect_finevolslideup.go new file mode 100644 index 0000000..057ffbf --- /dev/null +++ b/format/it/channel/effect_finevolslideup.go @@ -0,0 +1,61 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVolumeSlideUp defines a fine volume slide up effect +type FineVolumeSlideUp[TPeriod period.Period] DataEffect // 'D' + +func (e FineVolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e FineVolumeSlideUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, _ := mem.VolumeSlide(DataEffect(e)) + if x != 0x0F && tick == 0 { + return m.SlideChannelVolume(ch, 1.0, float32(x)) + } + return nil +} + +func (e FineVolumeSlideUp[TPeriod]) TraceData() string { + return e.String() +} + +//==================================================== + +// VolChanFineVolumeSlideUp defines a fine volume slide up effect (from the volume channel) +type VolChanFineVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' + +func (e VolChanFineVolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("d%xF", DataEffect(e)) +} + +func (e VolChanFineVolumeSlideUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, _ := mem.VolumeSlide(DataEffect(e)) + if tick == 0 { + return m.SlideChannelVolume(ch, 1.0, float32(x)) + } + return nil +} + +func (e VolChanFineVolumeSlideUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_globalvolumeslide.go b/format/it/channel/effect_globalvolumeslide.go new file mode 100644 index 0000000..2fcd512 --- /dev/null +++ b/format/it/channel/effect_globalvolumeslide.go @@ -0,0 +1,43 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// GlobalVolumeSlide defines a global volume slide effect +type GlobalVolumeSlide[TPeriod period.Period] DataEffect // 'W' + +func (e GlobalVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("W%0.2x", DataEffect(e)) +} + +func (e GlobalVolumeSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.GlobalVolumeSlide(DataEffect(e)) + + if tick == 0 { + return nil + } + + if x == 0 { + // global vol slide down + return m.SlideGlobalVolume(1, -float32(y)) + } else if y == 0 { + // global vol slide up + return m.SlideGlobalVolume(1, float32(x)) + } + return nil +} + +func (e GlobalVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_highoffset.go b/format/it/channel/effect_highoffset.go new file mode 100644 index 0000000..52f9365 --- /dev/null +++ b/format/it/channel/effect_highoffset.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// HighOffset defines a sample high offset effect +type HighOffset[TPeriod period.Period] DataEffect // 'SAx' + +func (e HighOffset[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e HighOffset[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + mem.HighOffset = int(e) * 0x10000 + return nil +} + +func (e HighOffset[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_newnoteactionnotecontinue.go b/format/it/channel/effect_newnoteactionnotecontinue.go new file mode 100644 index 0000000..998da0b --- /dev/null +++ b/format/it/channel/effect_newnoteactionnotecontinue.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NewNoteActionNoteContinue defines a NewNoteAction: Note Continue effect +type NewNoteActionNoteContinue[TPeriod period.Period] DataEffect // 'S74' + +func (e NewNoteActionNoteContinue[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NewNoteActionNoteContinue[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelNewNoteAction(ch, note.ActionContinue) +} + +func (e NewNoteActionNoteContinue[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_newnoteactionnotecut.go b/format/it/channel/effect_newnoteactionnotecut.go new file mode 100644 index 0000000..7d46776 --- /dev/null +++ b/format/it/channel/effect_newnoteactionnotecut.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NewNoteActionNoteCut defines a NewNoteAction: Note Cut effect +type NewNoteActionNoteCut[TPeriod period.Period] DataEffect // 'S73' + +func (e NewNoteActionNoteCut[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NewNoteActionNoteCut[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelNewNoteAction(ch, note.ActionCut) +} + +func (e NewNoteActionNoteCut[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_newnoteactionnotefade.go b/format/it/channel/effect_newnoteactionnotefade.go new file mode 100644 index 0000000..c9d0768 --- /dev/null +++ b/format/it/channel/effect_newnoteactionnotefade.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NewNoteActionNoteFade defines a NewNoteAction: Note Fade effect +type NewNoteActionNoteFade[TPeriod period.Period] DataEffect // 'S76' + +func (e NewNoteActionNoteFade[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NewNoteActionNoteFade[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelNewNoteAction(ch, note.ActionFadeout) +} + +func (e NewNoteActionNoteFade[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_newnoteactionnoteoff.go b/format/it/channel/effect_newnoteactionnoteoff.go new file mode 100644 index 0000000..7fc2dde --- /dev/null +++ b/format/it/channel/effect_newnoteactionnoteoff.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NewNoteActionNoteOff defines a NewNoteAction: Note Off effect +type NewNoteActionNoteOff[TPeriod period.Period] DataEffect // 'S75' + +func (e NewNoteActionNoteOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NewNoteActionNoteOff[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelNewNoteAction(ch, note.ActionRelease) +} + +func (e NewNoteActionNoteOff[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_notecut.go b/format/it/channel/effect_notecut.go new file mode 100644 index 0000000..420ee7b --- /dev/null +++ b/format/it/channel/effect_notecut.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NoteCut defines a note cut effect +type NoteCut[TPeriod period.Period] DataEffect // 'SCx' + +func (e NoteCut[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NoteCut[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelNoteAction(ch, note.ActionCut, int(e&0x0F)) +} + +func (e NoteCut[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_notedelay.go b/format/it/channel/effect_notedelay.go new file mode 100644 index 0000000..e9e7284 --- /dev/null +++ b/format/it/channel/effect_notedelay.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NoteDelay defines a note delay effect +type NoteDelay[TPeriod period.Period] DataEffect // 'SDx' + +func (e NoteDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NoteDelay[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelNoteAction(ch, note.ActionRetrigger, int(e&0x0F)) +} + +func (e NoteDelay[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_orderjump.go b/format/it/channel/effect_orderjump.go new file mode 100644 index 0000000..1b101a6 --- /dev/null +++ b/format/it/channel/effect_orderjump.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// OrderJump defines an order jump effect +type OrderJump[TPeriod period.Period] DataEffect // 'B' + +func (e OrderJump[TPeriod]) String() string { + return fmt.Sprintf("B%0.2x", DataEffect(e)) +} + +func (e OrderJump[TPeriod]) RowEnd(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetOrder(index.Order(e)) +} + +func (e OrderJump[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_panbrello.go b/format/it/channel/effect_panbrello.go new file mode 100644 index 0000000..c038dcc --- /dev/null +++ b/format/it/channel/effect_panbrello.go @@ -0,0 +1,43 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/types" +) + +// Panbrello defines a panning 'vibrato' effect +type Panbrello[TPeriod period.Period] DataEffect // 'Y' + +func (e Panbrello[TPeriod]) String() string { + return fmt.Sprintf("H%0.2x", DataEffect(e)) +} + +func (e Panbrello[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Panbrello(DataEffect(e)) + + mul := float32(4) + if mem.Shared.OldEffectMode { + if tick == 0 { + return nil + } + mul = 8 + } + return withOscillatorDo(ch, m, int(x), float32(y)*mul, machine.OscillatorPanbrello, func(value float32) error { + return m.SetChannelPanningDelta(ch, types.PanDelta(value)) + }) +} + +func (e Panbrello[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_panningenvelopeoff.go b/format/it/channel/effect_panningenvelopeoff.go new file mode 100644 index 0000000..4a62056 --- /dev/null +++ b/format/it/channel/effect_panningenvelopeoff.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PanningEnvelopeOff defines a panning envelope: off effect +type PanningEnvelopeOff[TPeriod period.Period] DataEffect // 'S79' + +func (e PanningEnvelopeOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PanningEnvelopeOff[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelPanningEnvelopeEnable(ch, false) +} + +func (e PanningEnvelopeOff[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_panningenvelopeon.go b/format/it/channel/effect_panningenvelopeon.go new file mode 100644 index 0000000..51090ec --- /dev/null +++ b/format/it/channel/effect_panningenvelopeon.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PanningEnvelopeOn defines a panning envelope: on effect +type PanningEnvelopeOn[TPeriod period.Period] DataEffect // 'S7A' + +func (e PanningEnvelopeOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PanningEnvelopeOn[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelPanningEnvelopeEnable(ch, true) +} + +func (e PanningEnvelopeOn[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_pastnotecut.go b/format/it/channel/effect_pastnotecut.go new file mode 100644 index 0000000..e52f066 --- /dev/null +++ b/format/it/channel/effect_pastnotecut.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PastNoteCut defines a past note cut effect +type PastNoteCut[TPeriod period.Period] DataEffect // 'S70' + +func (e PastNoteCut[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PastNoteCut[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.DoChannelPastNoteEffect(ch, note.ActionCut) +} + +func (e PastNoteCut[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_pastnotefadeout.go b/format/it/channel/effect_pastnotefadeout.go new file mode 100644 index 0000000..0ab9b01 --- /dev/null +++ b/format/it/channel/effect_pastnotefadeout.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PastNoteFade defines a past note fadeout effect +type PastNoteFade[TPeriod period.Period] DataEffect // 'S72' + +func (e PastNoteFade[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PastNoteFade[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.DoChannelPastNoteEffect(ch, note.ActionFadeout) +} + +func (e PastNoteFade[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_pastnoteoff.go b/format/it/channel/effect_pastnoteoff.go new file mode 100644 index 0000000..295636c --- /dev/null +++ b/format/it/channel/effect_pastnoteoff.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PastNoteOff defines a past note off effect +type PastNoteOff[TPeriod period.Period] DataEffect // 'S71' + +func (e PastNoteOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PastNoteOff[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.DoChannelPastNoteEffect(ch, note.ActionRelease) +} + +func (e PastNoteOff[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_patterndelay.go b/format/it/channel/effect_patterndelay.go new file mode 100644 index 0000000..71d6820 --- /dev/null +++ b/format/it/channel/effect_patterndelay.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PatternDelay defines a pattern delay effect +type PatternDelay[TPeriod period.Period] DataEffect // 'SEx' + +func (e PatternDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PatternDelay[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + times := int(DataEffect(e) & 0x0F) + return m.RowRepeat(times) +} + +func (e PatternDelay[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_patternloop.go b/format/it/channel/effect_patternloop.go new file mode 100644 index 0000000..7c8bafb --- /dev/null +++ b/format/it/channel/effect_patternloop.go @@ -0,0 +1,34 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PatternLoop defines a pattern loop effect +type PatternLoop[TPeriod period.Period] DataEffect // 'SBx' + +func (e PatternLoop[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PatternLoop[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + x := DataEffect(e) & 0x0F + + if x == 0 { + // set loop start + return m.SetPatternLoopStart(ch) + } else { + // set loop end + count + return m.SetPatternLoops(ch, int(x)) + } +} + +func (e PatternLoop[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_pitchenvelopeoff.go b/format/it/channel/effect_pitchenvelopeoff.go new file mode 100644 index 0000000..d0cb531 --- /dev/null +++ b/format/it/channel/effect_pitchenvelopeoff.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PitchEnvelopeOff defines a panning envelope: off effect +type PitchEnvelopeOff[TPeriod period.Period] DataEffect // 'S7B' + +func (e PitchEnvelopeOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PitchEnvelopeOff[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelPitchEnvelopeEnable(ch, false) +} + +func (e PitchEnvelopeOff[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_pitchenvelopeon.go b/format/it/channel/effect_pitchenvelopeon.go new file mode 100644 index 0000000..89973c3 --- /dev/null +++ b/format/it/channel/effect_pitchenvelopeon.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PitchEnvelopeOn defines a panning envelope: on effect +type PitchEnvelopeOn[TPeriod period.Period] DataEffect // 'S7C' + +func (e PitchEnvelopeOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PitchEnvelopeOn[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelPitchEnvelopeEnable(ch, true) +} + +func (e PitchEnvelopeOn[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_portadown.go b/format/it/channel/effect_portadown.go new file mode 100644 index 0000000..4ab15d1 --- /dev/null +++ b/format/it/channel/effect_portadown.go @@ -0,0 +1,33 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaDown defines a portamento down effect +type PortaDown[TPeriod period.Period] DataEffect // 'E' + +func (e PortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e PortaDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.PortaDown(DataEffect(e)) + + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) +} + +func (e PortaDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_portatonote.go b/format/it/channel/effect_portatonote.go new file mode 100644 index 0000000..7948ce5 --- /dev/null +++ b/format/it/channel/effect_portatonote.go @@ -0,0 +1,40 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaToNote defines a portamento-to-note effect +type PortaToNote[TPeriod period.Period] DataEffect // 'G' + +func (e PortaToNote[TPeriod]) String() string { + return fmt.Sprintf("G%0.2x", DataEffect(e)) +} + +func (e PortaToNote[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.StartChannelPortaToNote(ch) +} + +func (e PortaToNote[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.PortaToNote(DataEffect(e)) + + if !mem.Shared.OldEffectMode || tick != 0 { + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) + } + return nil +} + +func (e PortaToNote[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_portaup.go b/format/it/channel/effect_portaup.go new file mode 100644 index 0000000..eba24e2 --- /dev/null +++ b/format/it/channel/effect_portaup.go @@ -0,0 +1,32 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaUp defines a portamento up effect +type PortaUp[TPeriod period.Period] DataEffect // 'F' + +func (e PortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e PortaUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + xx := mem.PortaUp(DataEffect(e)) + + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) +} + +func (e PortaUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_portavolslide.go b/format/it/channel/effect_portavolslide.go new file mode 100644 index 0000000..62fc9ad --- /dev/null +++ b/format/it/channel/effect_portavolslide.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" +) + +// PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect +type PortaVolumeSlide[TPeriod period.Period] struct { // 'L' + playback.CombinedEffect[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning, *Memory, Data[TPeriod]] +} + +// NewPortaVolumeSlide creates a new PortaVolumeSlide object +func NewPortaVolumeSlide[TPeriod period.Period](mem *Memory, cd Command, val DataEffect) PortaVolumeSlide[TPeriod] { + pvs := PortaVolumeSlide[TPeriod]{} + vs := volumeSlideFactory[TPeriod](mem, cd, val) + pvs.Effects = append(pvs.Effects, vs, PortaToNote[TPeriod](0x00)) + return pvs +} + +func (e PortaVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("L%0.2x", any(e.Effects[0]).(DataEffect)) +} + +func (e PortaVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_retrigvolslide.go b/format/it/channel/effect_retrigvolslide.go new file mode 100644 index 0000000..d29a01e --- /dev/null +++ b/format/it/channel/effect_retrigvolslide.go @@ -0,0 +1,73 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RetrigVolumeSlide defines a retriggering volume slide effect +type RetrigVolumeSlide[TPeriod period.Period] DataEffect // 'Q' + +func (e RetrigVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("Q%0.2x", DataEffect(e)) +} + +func (e RetrigVolumeSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + x := DataEffect(e) >> 4 // vol slide instruction + y := DataEffect(e) & 0x0F // number of ticks between retriggers + + if (tick % int(y+1)) != 0 { + return nil + } + + if err := m.SetChannelNoteAction(ch, note.ActionRetrigger, tick); err != nil { + return err + } + + switch x { + case 0: // nothing + fallthrough + default: + + case 1: // -1 + return m.SlideChannelVolume(ch, 1, -1) + case 2: // -2 + return m.SlideChannelVolume(ch, 1, -2) + case 3: // -4 + return m.SlideChannelVolume(ch, 1, -4) + case 4: // -8 + return m.SlideChannelVolume(ch, 1, -8) + case 5: // -16 + return m.SlideChannelVolume(ch, 1, -16) + case 6: // * 2/3 + return m.SlideChannelVolume(ch, 2.0/3.0, 0) + case 7: // * 1/2 + return m.SlideChannelVolume(ch, 1.0/2.0, 0) + case 8: // ? + case 9: // +1 + return m.SlideChannelVolume(ch, 1, 1) + case 10: // +2 + return m.SlideChannelVolume(ch, 1, 2) + case 11: // +4 + return m.SlideChannelVolume(ch, 1, 4) + case 12: // +8 + return m.SlideChannelVolume(ch, 1, 8) + case 13: // +16 + return m.SlideChannelVolume(ch, 1, 16) + case 14: // * 3/2 + return m.SlideChannelVolume(ch, 3.0/2.0, 0) + case 15: // * 2 + return m.SlideChannelVolume(ch, 2, 0) + } + return nil +} + +func (e RetrigVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_rowjump.go b/format/it/channel/effect_rowjump.go new file mode 100644 index 0000000..a4eed4f --- /dev/null +++ b/format/it/channel/effect_rowjump.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RowJump defines a row jump effect +type RowJump[TPeriod period.Period] DataEffect // 'C' + +func (e RowJump[TPeriod]) String() string { + return fmt.Sprintf("C%0.2x", DataEffect(e)) +} + +func (e RowJump[TPeriod]) RowEnd(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetRow(index.Row(e), true) +} + +func (e RowJump[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_sampleoffset.go b/format/it/channel/effect_sampleoffset.go new file mode 100644 index 0000000..2ff2d01 --- /dev/null +++ b/format/it/channel/effect_sampleoffset.go @@ -0,0 +1,48 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/gomixing/sampling" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SampleOffset defines a sample offset effect +type SampleOffset[TPeriod period.Period] DataEffect // 'O' + +func (e SampleOffset[TPeriod]) String() string { + return fmt.Sprintf("O%0.2x", DataEffect(e)) +} + +func (e SampleOffset[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + xx := mem.SampleOffset(DataEffect(e)) + + if tick != 0 { + return nil + } + + pos := sampling.Pos{Pos: mem.HighOffset + int(xx)*0x100} + if mem.Shared.OldEffectMode { + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err + } + if inst == nil || pos.Pos >= inst.GetLength().Pos { + return nil + } + } + return m.SetChannelPos(ch, pos) +} + +func (e SampleOffset[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setchannelvolume.go b/format/it/channel/effect_setchannelvolume.go new file mode 100644 index 0000000..54c8771 --- /dev/null +++ b/format/it/channel/effect_setchannelvolume.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetChannelVolume defines a set channel volume effect +type SetChannelVolume[TPeriod period.Period] DataEffect // 'Mxx' + +func (e SetChannelVolume[TPeriod]) String() string { + return fmt.Sprintf("M%0.2x", DataEffect(e)) +} + +func (e SetChannelVolume[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + v := max(itVolume.FineVolume(e), itVolume.MaxItFineVolume) + return m.SetChannelMixingVolume(ch, v) +} + +func (e SetChannelVolume[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setcoarsepanposition.go b/format/it/channel/effect_setcoarsepanposition.go new file mode 100644 index 0000000..34f550e --- /dev/null +++ b/format/it/channel/effect_setcoarsepanposition.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetCoarsePanPosition defines a set coarse pan position effect +type SetCoarsePanPosition[TPeriod period.Period] DataEffect // 'S8x' + +func (e SetCoarsePanPosition[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetCoarsePanPosition[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + pan := itPanning.Panning((e & 0x0f) << 2) + return m.SetChannelPan(ch, pan) +} + +func (e SetCoarsePanPosition[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setfinetune.go b/format/it/channel/effect_setfinetune.go new file mode 100644 index 0000000..75b5894 --- /dev/null +++ b/format/it/channel/effect_setfinetune.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetFinetune defines a mod-style set finetune effect +type SetFinetune[TPeriod period.Period] DataEffect // 'S2x' + +func (e SetFinetune[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetFinetune[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + x := DataEffect(e) & 0x0F + + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err + } + + ft := (note.Finetune(x) - 8) * 4 + inst.SetFinetune(ft) + return nil +} + +func (e SetFinetune[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setglobalvolume.go b/format/it/channel/effect_setglobalvolume.go new file mode 100644 index 0000000..0805a16 --- /dev/null +++ b/format/it/channel/effect_setglobalvolume.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetGlobalVolume defines a set global volume effect +type SetGlobalVolume[TPeriod period.Period] DataEffect // 'V' + +func (e SetGlobalVolume[TPeriod]) String() string { + return fmt.Sprintf("V%0.2x", DataEffect(e)) +} + +func (e SetGlobalVolume[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + v := max(itVolume.FineVolume(DataEffect(e)), itVolume.MaxItFineVolume) + return m.SetGlobalVolume(v) +} + +func (e SetGlobalVolume[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setpanbrellowaveform.go b/format/it/channel/effect_setpanbrellowaveform.go new file mode 100644 index 0000000..ada1f8f --- /dev/null +++ b/format/it/channel/effect_setpanbrellowaveform.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetPanbrelloWaveform defines a set panbrello waveform effect +type SetPanbrelloWaveform[TPeriod period.Period] DataEffect // 'S5x' + +func (e SetPanbrelloWaveform[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetPanbrelloWaveform[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + x := DataEffect(e) & 0x0f + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorPanbrello, oscillator.WaveTableSelect(x)) +} + +func (e SetPanbrelloWaveform[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setpanposition.go b/format/it/channel/effect_setpanposition.go new file mode 100644 index 0000000..87980ff --- /dev/null +++ b/format/it/channel/effect_setpanposition.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetPanPosition defines a set pan position effect +type SetPanPosition[TPeriod period.Period] DataEffect // 'Xxx' + +func (e SetPanPosition[TPeriod]) String() string { + return fmt.Sprintf("X%0.2x", DataEffect(e)) +} + +func (e SetPanPosition[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + pan := itPanning.Panning(e) + return m.SetChannelPan(ch, pan) +} + +func (e SetPanPosition[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setspeed.go b/format/it/channel/effect_setspeed.go new file mode 100644 index 0000000..5092fda --- /dev/null +++ b/format/it/channel/effect_setspeed.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetSpeed defines a set speed effect +type SetSpeed[TPeriod period.Period] DataEffect // 'A' + +func (e SetSpeed[TPeriod]) String() string { + return fmt.Sprintf("A%0.2x", DataEffect(e)) +} + +func (e SetSpeed[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetTempo(int(e)) +} + +func (e SetSpeed[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_settempo.go b/format/it/channel/effect_settempo.go new file mode 100644 index 0000000..05def43 --- /dev/null +++ b/format/it/channel/effect_settempo.go @@ -0,0 +1,50 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetTempo defines a set tempo effect +type SetTempo[TPeriod period.Period] DataEffect // 'T' + +func (e SetTempo[TPeriod]) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) +} + +func (e SetTempo[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + switch DataEffect(e >> 4) { + case 0: // decrease BPM + if tick != 0 { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + val := int(mem.TempoDecrease(DataEffect(e & 0x0F))) + return m.SlideBPM(-val) + } + case 1: // increase BPM + if tick != 0 { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + val := int(mem.TempoIncrease(DataEffect(e & 0x0F))) + return m.SlideBPM(val) + } + default: + if tick == 0 { + return m.SetBPM(int(e)) + } + } + return nil +} + +func (e SetTempo[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_settremolowaveform.go b/format/it/channel/effect_settremolowaveform.go new file mode 100644 index 0000000..7a74c63 --- /dev/null +++ b/format/it/channel/effect_settremolowaveform.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetTremoloWaveform defines a set tremolo waveform effect +type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'S4x' + +func (e SetTremoloWaveform[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetTremoloWaveform[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + x := DataEffect(e) & 0x0f + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorTremolo, oscillator.WaveTableSelect(x)) +} + +func (e SetTremoloWaveform[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_setvibratowaveform.go b/format/it/channel/effect_setvibratowaveform.go new file mode 100644 index 0000000..b18931d --- /dev/null +++ b/format/it/channel/effect_setvibratowaveform.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetVibratoWaveform defines a set vibrato waveform effect +type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'S3x' + +func (e SetVibratoWaveform[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetVibratoWaveform[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + x := DataEffect(e) & 0x0f + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorVibrato, oscillator.WaveTableSelect(x)) +} + +func (e SetVibratoWaveform[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_surroundon.go b/format/it/channel/effect_surroundon.go new file mode 100644 index 0000000..73eda86 --- /dev/null +++ b/format/it/channel/effect_surroundon.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SurroundOn defines a set surround on effect +type SurroundOn[TPeriod period.Period] DataEffect // 'S91' + +func (e SurroundOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SurroundOn[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + // TODO: support for surround function + return nil +} + +func (e SurroundOn[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_tremolo.go b/format/it/channel/effect_tremolo.go new file mode 100644 index 0000000..db7b40e --- /dev/null +++ b/format/it/channel/effect_tremolo.go @@ -0,0 +1,41 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/types" +) + +// Tremolo defines a tremolo effect +type Tremolo[TPeriod period.Period] DataEffect // 'R' + +func (e Tremolo[TPeriod]) String() string { + return fmt.Sprintf("R%0.2x", DataEffect(e)) +} + +func (e Tremolo[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.Tremolo(DataEffect(e)) + + // NOTE: JBC - IT dos not update on tick 0, but MOD does. + // Maybe need to add a flag for converted MOD backward compatibility? + if tick == 0 { + return nil + } + + return withOscillatorDo(ch, m, int(x), float32(y)*4, machine.OscillatorTremolo, func(value float32) error { + return m.SetChannelVolumeDelta(ch, types.VolumeDelta(value)) + }) +} + +func (e Tremolo[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_tremor.go b/format/it/channel/effect_tremor.go new file mode 100644 index 0000000..1408545 --- /dev/null +++ b/format/it/channel/effect_tremor.go @@ -0,0 +1,32 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Tremor defines a tremor effect +type Tremor[TPeriod period.Period] DataEffect // 'I' + +func (e Tremor[TPeriod]) String() string { + return fmt.Sprintf("I%0.2x", DataEffect(e)) +} + +func (e Tremor[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Tremor(DataEffect(e)) + return doTremor(ch, m, int(x)+1, int(y)+1) +} + +func (e Tremor[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_vibrato.go b/format/it/channel/effect_vibrato.go new file mode 100644 index 0000000..c37f3ad --- /dev/null +++ b/format/it/channel/effect_vibrato.go @@ -0,0 +1,42 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Vibrato defines a vibrato effect +type Vibrato[TPeriod period.Period] DataEffect // 'H' + +func (e Vibrato[TPeriod]) String() string { + return fmt.Sprintf("H%0.2x", DataEffect(e)) +} + +func (e Vibrato[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Vibrato(DataEffect(e)) + + mul := float32(4) + if mem.Shared.OldEffectMode { + if tick == 0 { + return nil + } + mul = 8 + } + return withOscillatorDo(ch, m, int(x), float32(y)*mul, machine.OscillatorVibrato, func(value float32) error { + return m.SetChannelPeriodDelta(ch, period.Delta(value)) + }) +} + +func (e Vibrato[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_vibratovolslide.go b/format/it/channel/effect_vibratovolslide.go new file mode 100644 index 0000000..02504c1 --- /dev/null +++ b/format/it/channel/effect_vibratovolslide.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" +) + +// VibratoVolumeSlide defines a combination vibrato and volume slide effect +type VibratoVolumeSlide[TPeriod period.Period] struct { // 'K' + playback.CombinedEffect[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning, *Memory, Data[TPeriod]] +} + +// NewVibratoVolumeSlide creates a new VibratoVolumeSlide object +func NewVibratoVolumeSlide[TPeriod period.Period](mem *Memory, cd Command, val DataEffect) VibratoVolumeSlide[TPeriod] { + vvs := VibratoVolumeSlide[TPeriod]{} + vs := volumeSlideFactory[TPeriod](mem, cd, val) + vvs.Effects = append(vvs.Effects, vs, Vibrato[TPeriod](0x00)) + return vvs +} + +func (e VibratoVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("K%0.2x", any(e.Effects[0]).(DataEffect)) +} + +func (e VibratoVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_volslidedown.go b/format/it/channel/effect_volslidedown.go new file mode 100644 index 0000000..315e823 --- /dev/null +++ b/format/it/channel/effect_volslidedown.go @@ -0,0 +1,55 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeSlideDown defines a volume slide down effect +type VolumeSlideDown[TPeriod period.Period] DataEffect // 'D' + +func (e VolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e VolumeSlideDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + _, y := mem.VolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1.0, -float32(y)) +} + +func (e VolumeSlideDown[TPeriod]) TraceData() string { + return e.String() +} + +//==================================================== + +// VolChanVolumeSlideDown defines a volume slide down effect (from the volume channel) +type VolChanVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' + +func (e VolChanVolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("d0%x", DataEffect(e)) +} + +func (e VolChanVolumeSlideDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := mem.VolChanVolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1, -float32(y)) +} + +func (e VolChanVolumeSlideDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_volslideup.go b/format/it/channel/effect_volslideup.go new file mode 100644 index 0000000..3120827 --- /dev/null +++ b/format/it/channel/effect_volslideup.go @@ -0,0 +1,55 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeSlideUp defines a volume slide up effect +type VolumeSlideUp[TPeriod period.Period] DataEffect // 'D' + +func (e VolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e VolumeSlideUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, _ := mem.VolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1, float32(x)) +} + +func (e VolumeSlideUp[TPeriod]) TraceData() string { + return e.String() +} + +//==================================================== + +// VolChanVolumeSlideUp defines a volume slide up effect (from the volume channel) +type VolChanVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' + +func (e VolChanVolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("d%x0", DataEffect(e)) +} + +func (e VolChanVolumeSlideUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x := mem.VolChanVolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1, float32(x)) +} + +func (e VolChanVolumeSlideUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_volumeenvelopeoff.go b/format/it/channel/effect_volumeenvelopeoff.go new file mode 100644 index 0000000..a9ddee3 --- /dev/null +++ b/format/it/channel/effect_volumeenvelopeoff.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeEnvelopeOff defines a volume envelope: off effect +type VolumeEnvelopeOff[TPeriod period.Period] DataEffect // 'S77' + +func (e VolumeEnvelopeOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e VolumeEnvelopeOff[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelVolumeEnvelopeEnable(ch, false) +} + +func (e VolumeEnvelopeOff[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effect_volumeenvelopeon.go b/format/it/channel/effect_volumeenvelopeon.go new file mode 100644 index 0000000..8c689cd --- /dev/null +++ b/format/it/channel/effect_volumeenvelopeon.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeEnvelopeOn defines a volume envelope: on effect +type VolumeEnvelopeOn[TPeriod period.Period] DataEffect // 'S78' + +func (e VolumeEnvelopeOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e VolumeEnvelopeOn[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + return m.SetChannelVolumeEnvelopeEnable(ch, true) +} + +func (e VolumeEnvelopeOn[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/channel/effectfactory.go b/format/it/channel/effectfactory.go new file mode 100644 index 0000000..d4fbe0b --- /dev/null +++ b/format/it/channel/effectfactory.go @@ -0,0 +1,250 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" +) + +type EffectIT = playback.Effect + +// VolEff is a combined effect that includes a volume effect and a standard effect +type VolEff[TPeriod period.Period] struct { + playback.CombinedEffect[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning, *Memory, Data[TPeriod]] + eff EffectIT +} + +func (e VolEff[TPeriod]) String() string { + if e.eff == nil { + return "..." + } + return fmt.Sprint(e.eff) +} + +func (e VolEff[TPeriod]) TraceData() string { + return e.String() +} + +// Factory produces an effect for the provided channel pattern data +func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData[itVolume.Volume]) EffectIT { + if data == nil { + return nil + } + + d, _ := data.(Data[TPeriod]) + + if !d.What.HasCommand() && !d.What.HasVolPan() { + return nil + } + + var eff VolEff[TPeriod] + if d.What.HasVolPan() { + ve := volPanEffectFactory[TPeriod](mem, d.VolPan) + if ve != nil { + eff.Effects = append(eff.Effects, ve) + } + } + + if e := standardEffectFactory[TPeriod](mem, d); e != nil { + eff.Effects = append(eff.Effects, e) + eff.eff = e + } + + switch len(eff.Effects) { + case 0: + return nil + case 1: + return eff.Effects[0] + default: + return &eff + } +} + +func standardEffectFactory[TPeriod period.Period](mem *Memory, data Data[TPeriod]) EffectIT { + switch data.Effect + '@' { + case '@': // unused + return nil + case 'A': // Set Speed + return SetSpeed[TPeriod](data.EffectParameter) + case 'B': // Pattern Jump + return OrderJump[TPeriod](data.EffectParameter) + case 'C': // Pattern Break + return RowJump[TPeriod](data.EffectParameter) + case 'D': // Volume Slide / Fine Volume Slide + return volumeSlideFactory[TPeriod](mem, data.Effect, data.EffectParameter) + case 'E': // Porta Down/Fine Porta Down/Xtra Fine Porta + xx := mem.PortaDown(DataEffect(data.EffectParameter)) + x := xx >> 4 + if x == 0x0F { + return FinePortaDown[TPeriod](xx) + } else if x == 0x0E { + return ExtraFinePortaDown[TPeriod](xx) + } + return PortaDown[TPeriod](data.EffectParameter) + case 'F': // Porta Up/Fine Porta Up/Extra Fine Porta Down + xx := mem.PortaUp(DataEffect(data.EffectParameter)) + x := xx >> 4 + if x == 0x0F { + return FinePortaUp[TPeriod](xx) + } else if x == 0x0E { + return ExtraFinePortaUp[TPeriod](xx) + } + return PortaUp[TPeriod](data.EffectParameter) + case 'G': // Porta to note + return PortaToNote[TPeriod](data.EffectParameter) + case 'H': // Vibrato + return Vibrato[TPeriod](data.EffectParameter) + case 'I': // Tremor + return Tremor[TPeriod](data.EffectParameter) + case 'J': // Arpeggio + return Arpeggio[TPeriod](data.EffectParameter) + case 'K': // Vibrato+Volume Slide + return NewVibratoVolumeSlide[TPeriod](mem, data.Effect, data.EffectParameter) + case 'L': // Porta+Volume Slide + return NewPortaVolumeSlide[TPeriod](mem, data.Effect, data.EffectParameter) + case 'M': // Set Channel Volume + return SetChannelVolume[TPeriod](data.EffectParameter) + case 'N': // Channel Volume Slide + return ChannelVolumeSlide[TPeriod](data.EffectParameter) + case 'O': // Sample Offset + return SampleOffset[TPeriod](data.EffectParameter) + case 'P': // Panning Slide + //return panningSlideFactory(mem, data.Effect, data.EffectParameter) + case 'Q': // Retrig + Volume Slide + return RetrigVolumeSlide[TPeriod](data.EffectParameter) + case 'R': // Tremolo + return Tremolo[TPeriod](data.EffectParameter) + case 'S': // Special + return specialEffect[TPeriod](data) + case 'T': // Set Tempo + return SetTempo[TPeriod](data.EffectParameter) + case 'U': // Fine Vibrato + return FineVibrato[TPeriod](data.EffectParameter) + case 'V': // Global Volume + return SetGlobalVolume[TPeriod](data.EffectParameter) + case 'W': // Global Volume Slide + return GlobalVolumeSlide[TPeriod](data.EffectParameter) + case 'X': // Set Pan Position + return SetPanPosition[TPeriod](data.EffectParameter) + case 'Y': // Panbrello + return Panbrello[TPeriod](data.EffectParameter) + case 'Z': // MIDI Macro + return nil // TODO: MIDIMacro + default: + } + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} +} + +func specialEffect[TPeriod period.Period](data Data[TPeriod]) EffectIT { + switch data.EffectParameter >> 4 { + case 0x0: // unused + return nil + //case 0x1: // Set Glissando on/off + + case 0x2: // Set FineTune + return SetFinetune[TPeriod](data.EffectParameter) + case 0x3: // Set Vibrato Waveform + return SetVibratoWaveform[TPeriod](data.EffectParameter) + case 0x4: // Set Tremolo Waveform + return SetTremoloWaveform[TPeriod](data.EffectParameter) + case 0x5: // Set Panbrello Waveform + return SetPanbrelloWaveform[TPeriod](data.EffectParameter) + case 0x6: // Fine Pattern Delay + return FinePatternDelay[TPeriod](data.EffectParameter) + case 0x7: // special note operations + return specialNoteEffects[TPeriod](data) + case 0x8: // Set Coarse Pan Position + return SetCoarsePanPosition[TPeriod](data.EffectParameter) + case 0x9: // Sound Control + return soundControlEffect[TPeriod](data) + case 0xA: // High Offset + return HighOffset[TPeriod](data.EffectParameter) + case 0xB: // Pattern Loop + return PatternLoop[TPeriod](data.EffectParameter) + case 0xC: // Note Cut + return NoteCut[TPeriod](data.EffectParameter) + case 0xD: // Note Delay + return NoteDelay[TPeriod](data.EffectParameter) + case 0xE: // Pattern Delay + return PatternDelay[TPeriod](data.EffectParameter) + case 0xF: // Set Active Macro + return nil // TODO: SetActiveMacro + default: + } + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} +} + +func specialNoteEffects[TPeriod period.Period](data Data[TPeriod]) EffectIT { + switch data.EffectParameter & 0xf { + case 0x0: // Past Note Cut + return PastNoteCut[TPeriod](data.EffectParameter) + case 0x1: // Past Note Off + return PastNoteOff[TPeriod](data.EffectParameter) + case 0x2: // Past Note Fade + return PastNoteFade[TPeriod](data.EffectParameter) + case 0x3: // New Note Action: Note Cut + return NewNoteActionNoteCut[TPeriod](data.EffectParameter) + case 0x4: // New Note Action: Note Continue + return NewNoteActionNoteContinue[TPeriod](data.EffectParameter) + case 0x5: // New Note Action: Note Off + return NewNoteActionNoteOff[TPeriod](data.EffectParameter) + case 0x6: // New Note Action: Note Fade + return NewNoteActionNoteFade[TPeriod](data.EffectParameter) + case 0x7: // Volume Envelope Off + return VolumeEnvelopeOff[TPeriod](data.EffectParameter) + case 0x8: // Volume Envelope On + return VolumeEnvelopeOn[TPeriod](data.EffectParameter) + case 0x9: // Panning Envelope Off + return PanningEnvelopeOff[TPeriod](data.EffectParameter) + case 0xA: // Panning Envelope On + return PanningEnvelopeOn[TPeriod](data.EffectParameter) + case 0xB: // Pitch Envelope Off + return PitchEnvelopeOff[TPeriod](data.EffectParameter) + case 0xC: // Pitch Envelope On + return PitchEnvelopeOn[TPeriod](data.EffectParameter) + case 0xD, 0xE, 0xF: // unused + return nil + } + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} +} + +func volumeSlideFactory[TPeriod period.Period](mem *Memory, cd Command, ce DataEffect) EffectIT { + x, y := mem.VolumeSlide(DataEffect(ce)) + switch { + case x == 0: + return VolumeSlideDown[TPeriod](ce) + case y == 0: + return VolumeSlideUp[TPeriod](ce) + case x == 0x0f: + return FineVolumeSlideDown[TPeriod](ce) + case y == 0x0f: + return FineVolumeSlideUp[TPeriod](ce) + } + // There is a chance that a volume slide command is set with an invalid + // value or is 00, in which case the memory might have the invalid value, + // so we need to handle it by deferring to using a no-op instead of a + // VolumeSlideDown + return nil +} + +func soundControlEffect[TPeriod period.Period](data Data[TPeriod]) EffectIT { + switch data.EffectParameter & 0xF { + case 0x0: // Surround Off + case 0x1: // Surround On + // only S91 is supported directly by IT + return SurroundOn[TPeriod](data.EffectParameter) + case 0x8: // Reverb Off + case 0x9: // Reverb On + case 0xA: // Center Surround + case 0xB: // Quad Surround + case 0xC: // Global Filters + case 0xD: // Local Filters + case 0xE: // Play Forward + case 0xF: // Play Backward + } + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} +} diff --git a/format/it/channel/effectfactory_volume.go b/format/it/channel/effectfactory_volume.go new file mode 100644 index 0000000..18ce90f --- /dev/null +++ b/format/it/channel/effectfactory_volume.go @@ -0,0 +1,65 @@ +package channel + +import ( + "github.com/gotracker/playback/period" +) + +func volPanEffectFactory[TPeriod period.Period](mem *Memory, v uint8) EffectIT { + switch { + case v <= 0x40: // volume set - handled elsewhere + return nil + case v >= 0x41 && v <= 0x4a: // fine volume slide up + return VolChanFineVolumeSlideUp[TPeriod](v - 0x41) + case v >= 0x4b && v <= 0x54: // fine volume slide down + return VolChanFineVolumeSlideDown[TPeriod](v - 0x4b) + case v >= 0x55 && v <= 0x5e: // volume slide up + return VolChanVolumeSlideUp[TPeriod](v - 0x55) + case v >= 0x5f && v <= 0x68: // volume slide down + return VolChanVolumeSlideDown[TPeriod](v - 0x5f) + case v >= 0x69 && v <= 0x72: // portamento down + return volPortaDown[TPeriod](v - 0x69) + case v >= 0x73 && v <= 0x7c: // portamento up + return volPortaUp[TPeriod](v - 0x73) + case v >= 0x80 && v <= 0xc0: // set panning + return SetPanPosition[TPeriod](v - 0x80) + case v >= 0xc1 && v <= 0xca: // portamento to note + return volPortaToNote[TPeriod](v - 0xc1) + case v >= 0xcb && v <= 0xd4: // vibrato + return Vibrato[TPeriod](v - 0xcb) + } + return UnhandledVolCommand[TPeriod]{Vol: v} +} + +func volPortaDown[TPeriod period.Period](v uint8) EffectIT { + return PortaDown[TPeriod](v * 4) +} +func volPortaUp[TPeriod period.Period](v uint8) EffectIT { + return PortaUp[TPeriod](v * 4) +} + +func volPortaToNote[TPeriod period.Period](v uint8) EffectIT { + switch v { + case 0: + return PortaToNote[TPeriod](0x00) + case 1: + return PortaToNote[TPeriod](0x01) + case 2: + return PortaToNote[TPeriod](0x04) + case 3: + return PortaToNote[TPeriod](0x08) + case 4: + return PortaToNote[TPeriod](0x10) + case 5: + return PortaToNote[TPeriod](0x20) + case 6: + return PortaToNote[TPeriod](0x40) + case 7: + return PortaToNote[TPeriod](0x60) + case 8: + return PortaToNote[TPeriod](0x80) + case 9: + return PortaToNote[TPeriod](0xFF) + } + // impossible, but hey... + return UnhandledVolCommand[TPeriod]{Vol: v + 0xc1} +} diff --git a/format/it/channel/machine.go b/format/it/channel/machine.go new file mode 100644 index 0000000..76bf0b2 --- /dev/null +++ b/format/it/channel/machine.go @@ -0,0 +1,51 @@ +package channel + +import ( + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +func withOscillatorDo[TPeriod period.Period](ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], speed int, depth float32, osc machine.Oscillator, fn func(value float32) error) error { + value, err := m.GetNextChannelWavetableValue(ch, speed, depth, machine.OscillatorVibrato) + if err != nil { + return err + } + + return fn(value) +} + +func doTremor[TPeriod period.Period](ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], onTicks int, offTicks int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + tremor := mem.TremorMem() + if tremor.IsActive() { + if tremor.Advance() >= onTicks { + tremor.ToggleAndReset() + } + } else { + if tremor.Advance() >= offTicks { + tremor.ToggleAndReset() + } + } + + return m.SetChannelVolumeActive(ch, tremor.IsActive()) +} + +func doArpeggio[TPeriod period.Period](ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], tick int, arpSemitoneADelta, arpSemitoneBDelta int8) error { + switch tick % 3 { + case 0: + fallthrough + default: + return m.DoChannelArpeggio(ch, 0) + case 1: + return m.DoChannelArpeggio(ch, arpSemitoneADelta) + case 2: + return m.DoChannelArpeggio(ch, arpSemitoneBDelta) + } +} diff --git a/format/it/channel/memory.go b/format/it/channel/memory.go index 0941ff1..ec058db 100644 --- a/format/it/channel/memory.go +++ b/format/it/channel/memory.go @@ -1,12 +1,8 @@ package channel import ( - "github.com/gotracker/playback/voice/oscillator" - "github.com/gotracker/playback/memory" - oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/tremor" - formatutil "github.com/gotracker/playback/util" ) // Memory is the storage object for custom effect/effect values @@ -29,23 +25,12 @@ type Memory struct { panbrello memory.Value[DataEffect] `usage:"Yxy"` volChanVolumeSlide memory.Value[DataEffect] `usage:"vDxy"` - tremorMem tremor.Tremor - vibratoOscillator oscillator.Oscillator - tremoloOscillator oscillator.Oscillator - panbrelloOscillator oscillator.Oscillator - patternLoop formatutil.PatternLoop - HighOffset int + tremorMem tremor.Tremor + HighOffset int Shared *SharedMemory } -// ResetOscillators resets the oscillators to defaults -func (m *Memory) ResetOscillators() { - m.vibratoOscillator = oscillatorImpl.NewImpulseTrackerOscillator(4) - m.tremoloOscillator = oscillatorImpl.NewImpulseTrackerOscillator(4) - m.panbrelloOscillator = oscillatorImpl.NewImpulseTrackerOscillator(1) -} - // VolumeSlide gets or sets the most recent non-zero value (or input) for Volume Slide func (m *Memory) VolumeSlide(input DataEffect) (DataEffect, DataEffect) { return m.volumeSlide.CoalesceXY(input) @@ -133,8 +118,8 @@ func (m *Memory) GlobalVolumeSlide(input DataEffect) (DataEffect, DataEffect) { } // Panbrello gets or sets the most recent non-zero value (or input) for Panbrello -func (m *Memory) Panbrello(input DataEffect) DataEffect { - return m.panbrello.Coalesce(input) +func (m *Memory) Panbrello(input DataEffect) (DataEffect, DataEffect) { + return m.panbrello.CoalesceXY(input) } // TremorMem returns the Tremor object @@ -142,35 +127,12 @@ func (m *Memory) TremorMem() *tremor.Tremor { return &m.tremorMem } -// VibratoOscillator returns the Vibrato oscillator object -func (m *Memory) VibratoOscillator() oscillator.Oscillator { - return m.vibratoOscillator -} - -// TremoloOscillator returns the Tremolo oscillator object -func (m *Memory) TremoloOscillator() oscillator.Oscillator { - return m.tremoloOscillator -} - -// PanbrelloOscillator returns the Panbrello oscillator object -func (m *Memory) PanbrelloOscillator() oscillator.Oscillator { - return m.panbrelloOscillator -} - // Retrigger runs certain operations when a note is retriggered func (m *Memory) Retrigger() { - for _, osc := range []oscillator.Oscillator{m.VibratoOscillator(), m.TremoloOscillator(), m.PanbrelloOscillator()} { - osc.Reset() - } -} - -// GetPatternLoop returns the pattern loop object from the memory -func (m *Memory) GetPatternLoop() *formatutil.PatternLoop { - return &m.patternLoop } // StartOrder is called when the first order's row at tick 0 is started -func (m *Memory) StartOrder() { +func (m *Memory) StartOrder0() { if m.Shared.ResetMemoryAtStartOfOrder0 { m.volumeSlide.Reset() m.portaDown.Reset() diff --git a/format/it/channel/sampleid.go b/format/it/channel/sampleid.go index b529a24..7f6512c 100644 --- a/format/it/channel/sampleid.go +++ b/format/it/channel/sampleid.go @@ -2,14 +2,12 @@ package channel import ( "fmt" - - "github.com/gotracker/playback/note" ) // SampleID is an InstrumentID that is a combination of InstID and SampID type SampleID struct { - InstID uint8 - Semitone note.Semitone + InstID uint8 + SampID uint8 } // IsEmpty returns true if the sample ID is empty @@ -17,6 +15,10 @@ func (s SampleID) IsEmpty() bool { return s.InstID == 0 } +func (s SampleID) GetIndexAndSample() (int, int) { + return int(s.InstID) - 1, int(s.SampID) +} + func (s SampleID) String() string { - return fmt.Sprint(s.InstID) + return fmt.Sprintf("%d(%d)", s.InstID, s.SampID) } diff --git a/format/it/channel/sharedmem.go b/format/it/channel/sharedmem.go index 7422540..59146aa 100644 --- a/format/it/channel/sharedmem.go +++ b/format/it/channel/sharedmem.go @@ -1,8 +1,6 @@ package channel type SharedMemory struct { - // LinearFreqSlides is true if linear frequency slides are enabled (false = amiga-style period-based slides) - LinearFreqSlides bool // OldEffectMode performs somewhat different operations for some effects: // On: // - Vibrato does not operate on tick 0 and has double depth diff --git a/format/it/channel/unhandled.go b/format/it/channel/unhandled.go new file mode 100644 index 0000000..c4f27d2 --- /dev/null +++ b/format/it/channel/unhandled.go @@ -0,0 +1,66 @@ +package channel + +import ( + "fmt" + + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// UnhandledCommand is an unhandled command +type UnhandledCommand[TPeriod period.Period] struct { + Command Command + Info DataEffect +} + +func (e UnhandledCommand[TPeriod]) String() string { + return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) +} + +func (e UnhandledCommand[TPeriod]) Names() []string { + return []string{ + fmt.Sprintf("UnhandledCommand(%s)", e.String()), + } +} + +func (e UnhandledCommand[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + if !m.IgnoreUnknownEffect() { + panic(fmt.Sprintf("unhandled command: ce:%0.2X cp:%0.2X", e.Command, e.Info)) + } + return nil +} + +func (e UnhandledCommand[TPeriod]) TraceData() string { + return e.String() +} + +//////// + +// UnhandledVolCommand is an unhandled volume command +type UnhandledVolCommand[TPeriod period.Period] struct { + Vol uint8 +} + +func (e UnhandledVolCommand[TPeriod]) String() string { + return fmt.Sprintf("v%0.2x", e.Vol) +} + +func (e UnhandledVolCommand[TPeriod]) Names() []string { + return []string{ + fmt.Sprintf("UnhandledVolCommand(%s)", e.String()), + } +} + +func (e UnhandledVolCommand[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + if !m.IgnoreUnknownEffect() { + panic(fmt.Sprintf("unhandled command: volCmd:%0.2X", e.Vol)) + } + return nil +} + +func (e UnhandledVolCommand[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/it/effect/effect_arpeggio.go b/format/it/effect/effect_arpeggio.go deleted file mode 100644 index b877d45..0000000 --- a/format/it/effect/effect_arpeggio.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// Arpeggio defines an arpeggio effect -type Arpeggio channel.DataEffect // 'J' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - cs.SetPos(cs.GetTargetPos()) - return nil -} - -// Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Arpeggio(channel.DataEffect(e)) - return doArpeggio(cs, currentTick, int8(x), int8(y)) -} - -func (e Arpeggio) String() string { - return fmt.Sprintf("J%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_channelvolumeslide.go b/format/it/effect/effect_channelvolumeslide.go deleted file mode 100644 index f3a815a..0000000 --- a/format/it/effect/effect_channelvolumeslide.go +++ /dev/null @@ -1,68 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// ChannelVolumeSlide defines a set channel volume effect -type ChannelVolumeSlide channel.DataEffect // 'Nxy' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ChannelVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - mem := cs.GetMemory() - x, y := mem.ChannelVolumeSlide(channel.DataEffect(e)) - - switch { - case y == 0x0 && x != 0xF: - case y != 0xF && x == 0x0: - case y == 0xF: - vol := cs.GetChannelVolume() + (volume.Volume(x) / 64) - if vol > 1 { - vol = 1 - } - cs.SetChannelVolume(vol) - case x == 0xF: - vol := cs.GetChannelVolume() - (volume.Volume(x) / 64) - if vol < 0 { - vol = 0 - } - cs.SetChannelVolume(vol) - } - return nil -} - -// Tick is called on every tick -func (e ChannelVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.ChannelVolumeSlide(channel.DataEffect(e)) - - switch { - case y == 0x0 && x != 0xF: - vol := cs.GetChannelVolume() + (volume.Volume(x) / 64) - if vol > 1 { - vol = 1 - } - cs.SetChannelVolume(vol) - case y != 0xF && x == 0x0: - vol := cs.GetChannelVolume() - (volume.Volume(x) / 64) - if vol < 0 { - vol = 0 - } - cs.SetChannelVolume(vol) - - case y == 0xF, x == 0xF: - // nothing - } - return nil -} - -func (e ChannelVolumeSlide) String() string { - return fmt.Sprintf("N%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_extrafineportadown.go b/format/it/effect/effect_extrafineportadown.go deleted file mode 100644 index 99d6582..0000000 --- a/format/it/effect/effect_extrafineportadown.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// ExtraFinePortaDown defines an extra-fine portamento down effect -type ExtraFinePortaDown channel.DataEffect // 'EEx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - y := mem.PortaDown(channel.DataEffect(e)) & 0x0F - - return doPortaDown(cs, float32(y), 1, mem.Shared.LinearFreqSlides) -} - -func (e ExtraFinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_extrafineportaup.go b/format/it/effect/effect_extrafineportaup.go deleted file mode 100644 index ff36996..0000000 --- a/format/it/effect/effect_extrafineportaup.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// ExtraFinePortaUp defines an extra-fine portamento up effect -type ExtraFinePortaUp channel.DataEffect // 'FEx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - y := mem.PortaUp(channel.DataEffect(e)) & 0x0F - - return doPortaUp(cs, float32(y), 1, mem.Shared.LinearFreqSlides) -} - -func (e ExtraFinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_finepatterndelay.go b/format/it/effect/effect_finepatterndelay.go deleted file mode 100644 index da66c76..0000000 --- a/format/it/effect/effect_finepatterndelay.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" -) - -// FinePatternDelay defines an fine pattern delay effect -type FinePatternDelay channel.DataEffect // 'S6x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePatternDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - m := p.(effectIntf.IT) - if err := m.AddRowTicks(int(x)); err != nil { - return err - } - return nil -} - -func (e FinePatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_fineportadown.go b/format/it/effect/effect_fineportadown.go deleted file mode 100644 index 2ac702d..0000000 --- a/format/it/effect/effect_fineportadown.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// FinePortaDown defines an fine portamento down effect -type FinePortaDown channel.DataEffect // 'EFx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - y := mem.PortaDown(channel.DataEffect(e)) & 0x0F - - return doPortaDown(cs, float32(y), 4, mem.Shared.LinearFreqSlides) -} - -func (e FinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_fineportaup.go b/format/it/effect/effect_fineportaup.go deleted file mode 100644 index 73df0a2..0000000 --- a/format/it/effect/effect_fineportaup.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// FinePortaUp defines an fine portamento up effect -type FinePortaUp channel.DataEffect // 'FFx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - y := mem.PortaUp(channel.DataEffect(e)) & 0x0F - - return doPortaUp(cs, float32(y), 4, mem.Shared.LinearFreqSlides) -} - -func (e FinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_finevibrato.go b/format/it/effect/effect_finevibrato.go deleted file mode 100644 index 7673b4b..0000000 --- a/format/it/effect/effect_finevibrato.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// FineVibrato defines an fine vibrato effect -type FineVibrato channel.DataEffect // 'U' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVibrato) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e FineVibrato) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) - if currentTick != 0 { - return doVibrato(cs, currentTick, x, y, 1) - } - return nil -} - -func (e FineVibrato) String() string { - return fmt.Sprintf("U%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_finevolslidedown.go b/format/it/effect/effect_finevolslidedown.go deleted file mode 100644 index dff0e00..0000000 --- a/format/it/effect/effect_finevolslidedown.go +++ /dev/null @@ -1,49 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// FineVolumeSlideDown defines a fine volume slide down effect -type FineVolumeSlideDown channel.DataEffect // 'D' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e FineVolumeSlideDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - _, y := mem.VolumeSlide(channel.DataEffect(e)) - - if y != 0x0F && currentTick == 0 { - return doVolSlide(cs, -float32(y), 1.0) - } - return nil -} - -func (e FineVolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} - -//==================================================== - -// VolChanFineVolumeSlideDown defines a fine volume slide down effect (from the volume channel) -type VolChanFineVolumeSlideDown channel.DataEffect // 'd' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - mem := cs.GetMemory() - y := mem.VolChanVolumeSlide(channel.DataEffect(e)) - - return doVolSlide(cs, -float32(y), 1.0) -} - -func (e VolChanFineVolumeSlideDown) String() string { - return fmt.Sprintf("dF%x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_finevolslideup.go b/format/it/effect/effect_finevolslideup.go deleted file mode 100644 index f18c795..0000000 --- a/format/it/effect/effect_finevolslideup.go +++ /dev/null @@ -1,49 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// FineVolumeSlideUp defines a fine volume slide up effect -type FineVolumeSlideUp channel.DataEffect // 'D' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e FineVolumeSlideUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, _ := mem.VolumeSlide(channel.DataEffect(e)) - - if x != 0x0F && currentTick == 0 { - return doVolSlide(cs, float32(x), 1.0) - } - return nil -} - -func (e FineVolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} - -//==================================================== - -// VolChanFineVolumeSlideUp defines a fine volume slide up effect (from the volume channel) -type VolChanFineVolumeSlideUp channel.DataEffect // 'd' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - mem := cs.GetMemory() - x := mem.VolChanVolumeSlide(channel.DataEffect(e)) - - return doVolSlide(cs, float32(x), 1.0) -} - -func (e VolChanFineVolumeSlideUp) String() string { - return fmt.Sprintf("d%xF", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_globalvolumeslide.go b/format/it/effect/effect_globalvolumeslide.go deleted file mode 100644 index b63bdf8..0000000 --- a/format/it/effect/effect_globalvolumeslide.go +++ /dev/null @@ -1,43 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" -) - -// GlobalVolumeSlide defines a global volume slide effect -type GlobalVolumeSlide channel.DataEffect // 'W' - -// Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e GlobalVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) - - if currentTick == 0 { - return nil - } - - m := p.(effectIntf.IT) - - if x == 0 { - // global vol slide down - return doGlobalVolSlide(m, -float32(y), 1.0) - } else if y == 0 { - // global vol slide up - return doGlobalVolSlide(m, float32(y), 1.0) - } - return nil -} - -func (e GlobalVolumeSlide) String() string { - return fmt.Sprintf("W%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_highoffset.go b/format/it/effect/effect_highoffset.go deleted file mode 100644 index 912a23c..0000000 --- a/format/it/effect/effect_highoffset.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// HighOffset defines a sample high offset effect -type HighOffset channel.DataEffect // 'SAx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e HighOffset) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() - - xx := channel.DataEffect(e) - - mem.HighOffset = int(xx) * 0x10000 - return nil -} - -func (e HighOffset) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_newnoteactionnotecontinue.go b/format/it/effect/effect_newnoteactionnotecontinue.go deleted file mode 100644 index aa84905..0000000 --- a/format/it/effect/effect_newnoteactionnotecontinue.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// NewNoteActionNoteContinue defines a NewNoteAction: Note Continue effect -type NewNoteActionNoteContinue channel.DataEffect // 'S74' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteContinue) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionContinue) - return nil -} - -func (e NewNoteActionNoteContinue) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_newnoteactionnotecut.go b/format/it/effect/effect_newnoteactionnotecut.go deleted file mode 100644 index 03e39f7..0000000 --- a/format/it/effect/effect_newnoteactionnotecut.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// NewNoteActionNoteCut defines a NewNoteAction: Note Cut effect -type NewNoteActionNoteCut channel.DataEffect // 'S73' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteCut) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionCut) - return nil -} - -func (e NewNoteActionNoteCut) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_newnoteactionnotefade.go b/format/it/effect/effect_newnoteactionnotefade.go deleted file mode 100644 index d6cff6a..0000000 --- a/format/it/effect/effect_newnoteactionnotefade.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// NewNoteActionNoteFade defines a NewNoteAction: Note Fade effect -type NewNoteActionNoteFade channel.DataEffect // 'S76' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteFade) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionFadeout) - return nil -} - -func (e NewNoteActionNoteFade) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_newnoteactionnoteoff.go b/format/it/effect/effect_newnoteactionnoteoff.go deleted file mode 100644 index 9899615..0000000 --- a/format/it/effect/effect_newnoteactionnoteoff.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// NewNoteActionNoteOff defines a NewNoteAction: Note Off effect -type NewNoteActionNoteOff channel.DataEffect // 'S75' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteOff) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionRelease) - return nil -} - -func (e NewNoteActionNoteOff) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_notecut.go b/format/it/effect/effect_notecut.go deleted file mode 100644 index 5e02c82..0000000 --- a/format/it/effect/effect_notecut.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// NoteCut defines a note cut effect -type NoteCut channel.DataEffect // 'SCx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e NoteCut) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.FreezePlayback() - } - return nil -} - -func (e NoteCut) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_notedelay.go b/format/it/effect/effect_notedelay.go deleted file mode 100644 index 34aae9e..0000000 --- a/format/it/effect/effect_notedelay.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// NoteDelay defines a note delay effect -type NoteDelay channel.DataEffect // 'SDx' - -// PreStart triggers when the effect enters onto the channel state -func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(channel.DataEffect(e)&0x0F)) - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e NoteDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_orderjump.go b/format/it/effect/effect_orderjump.go deleted file mode 100644 index 6188862..0000000 --- a/format/it/effect/effect_orderjump.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/index" -) - -// OrderJump defines an order jump effect -type OrderJump channel.DataEffect // 'B' - -// Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e OrderJump) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - return p.SetNextOrder(index.Order(e)) -} - -func (e OrderJump) String() string { - return fmt.Sprintf("B%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_panningenvelopeoff.go b/format/it/effect/effect_panningenvelopeoff.go deleted file mode 100644 index f43b14b..0000000 --- a/format/it/effect/effect_panningenvelopeoff.go +++ /dev/null @@ -1,23 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PanningEnvelopeOff defines a panning envelope: off effect -type PanningEnvelopeOff channel.DataEffect // 'S79' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOff) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - cs.SetPanningEnvelopeEnable(false) - return nil -} - -func (e PanningEnvelopeOff) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_panningenvelopeon.go b/format/it/effect/effect_panningenvelopeon.go deleted file mode 100644 index 99dc7a6..0000000 --- a/format/it/effect/effect_panningenvelopeon.go +++ /dev/null @@ -1,23 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PanningEnvelopeOn defines a panning envelope: on effect -type PanningEnvelopeOn channel.DataEffect // 'S7A' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOn) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - cs.SetPanningEnvelopeEnable(true) - return nil -} - -func (e PanningEnvelopeOn) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_pastnotecut.go b/format/it/effect/effect_pastnotecut.go deleted file mode 100644 index 7f1202f..0000000 --- a/format/it/effect/effect_pastnotecut.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// PastNoteCut defines a past note cut effect -type PastNoteCut channel.DataEffect // 'S70' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteCut) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.DoPastNoteEffect(note.ActionCut) - return nil -} - -func (e PastNoteCut) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_pastnotefadeout.go b/format/it/effect/effect_pastnotefadeout.go deleted file mode 100644 index 4272248..0000000 --- a/format/it/effect/effect_pastnotefadeout.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// PastNoteFade defines a past note fadeout effect -type PastNoteFade channel.DataEffect // 'S72' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteFade) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.DoPastNoteEffect(note.ActionFadeout) - return nil -} - -func (e PastNoteFade) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_pastnoteoff.go b/format/it/effect/effect_pastnoteoff.go deleted file mode 100644 index da0bdb1..0000000 --- a/format/it/effect/effect_pastnoteoff.go +++ /dev/null @@ -1,22 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// PastNoteOff defines a past note off effect -type PastNoteOff channel.DataEffect // 'S71' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteOff) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.DoPastNoteEffect(note.ActionRelease) - return nil -} - -func (e PastNoteOff) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_patterndelay.go b/format/it/effect/effect_patterndelay.go deleted file mode 100644 index 491cb47..0000000 --- a/format/it/effect/effect_patterndelay.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" -) - -// PatternDelay defines a pattern delay effect -type PatternDelay channel.DataEffect // 'SEx' - -// PreStart triggers when the effect enters onto the channel state -func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - m := p.(effectIntf.IT) - return m.SetPatternDelay(int(channel.DataEffect(e) & 0x0F)) -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e PatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_patternloop.go b/format/it/effect/effect_patternloop.go deleted file mode 100644 index dd5d1a4..0000000 --- a/format/it/effect/effect_patternloop.go +++ /dev/null @@ -1,44 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PatternLoop defines a pattern loop effect -type PatternLoop channel.DataEffect // 'SBx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e PatternLoop) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - x := uint8(e) & 0xF - - mem := cs.GetMemory() - pl := mem.GetPatternLoop() - if x == 0 { - // set loop - pl.Start = p.GetCurrentRow() - } else { - if !pl.Enabled { - pl.Enabled = true - pl.Total = x - pl.End = p.GetCurrentRow() - pl.Count = 0 - } - if row, ok := pl.ContinueLoop(p.GetCurrentRow()); ok { - return p.SetNextRowWithBacktrack(row, true) - } - } - return nil -} - -func (e PatternLoop) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_pitchenvelopeoff.go b/format/it/effect/effect_pitchenvelopeoff.go deleted file mode 100644 index 0a269a7..0000000 --- a/format/it/effect/effect_pitchenvelopeoff.go +++ /dev/null @@ -1,23 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PitchEnvelopeOff defines a panning envelope: off effect -type PitchEnvelopeOff channel.DataEffect // 'S7B' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOff) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - cs.SetPitchEnvelopeEnable(false) - return nil -} - -func (e PitchEnvelopeOff) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_pitchenvelopeon.go b/format/it/effect/effect_pitchenvelopeon.go deleted file mode 100644 index 0131a82..0000000 --- a/format/it/effect/effect_pitchenvelopeon.go +++ /dev/null @@ -1,23 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PitchEnvelopeOn defines a panning envelope: on effect -type PitchEnvelopeOn channel.DataEffect // 'S7C' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOn) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - cs.SetPitchEnvelopeEnable(true) - return nil -} - -func (e PitchEnvelopeOn) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_portadown.go b/format/it/effect/effect_portadown.go deleted file mode 100644 index 10bdf2b..0000000 --- a/format/it/effect/effect_portadown.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PortaDown defines a portamento down effect -type PortaDown channel.DataEffect // 'E' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaDown(channel.DataEffect(e)) - - return doPortaDown(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) -} - -func (e PortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_portatonote.go b/format/it/effect/effect_portatonote.go deleted file mode 100644 index 9d990fd..0000000 --- a/format/it/effect/effect_portatonote.go +++ /dev/null @@ -1,51 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/heucuva/comparison" -) - -// PortaToNote defines a portamento-to-note effect -type PortaToNote channel.DataEffect // 'G' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) - cs.SetNotePlayTick(false, note.ActionContinue, 0) - } - return nil -} - -// Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaToNote(channel.DataEffect(e)) - - // vibrato modifies current period for portamento - cur := cs.GetPeriod() - if cur == nil { - return nil - } - cur = cur.AddDelta(cs.GetPeriodDelta()) - ptp := cs.GetPortaTargetPeriod() - if !mem.Shared.OldEffectMode || currentTick != 0 { - if period.ComparePeriods(cur, ptp) == comparison.SpaceshipRightGreater { - return doPortaUpToNote(cs, float32(xx), 4, ptp, mem.Shared.LinearFreqSlides) // subtracts - } else { - return doPortaDownToNote(cs, float32(xx), 4, ptp, mem.Shared.LinearFreqSlides) // adds - } - } - return nil -} - -func (e PortaToNote) String() string { - return fmt.Sprintf("G%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_portaup.go b/format/it/effect/effect_portaup.go deleted file mode 100644 index 7a1fbdf..0000000 --- a/format/it/effect/effect_portaup.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PortaUp defines a portamento up effect -type PortaUp channel.DataEffect // 'F' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaUp(channel.DataEffect(e)) - - return doPortaUp(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) -} - -func (e PortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_portavolslide.go b/format/it/effect/effect_portavolslide.go deleted file mode 100644 index e9d0cda..0000000 --- a/format/it/effect/effect_portavolslide.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect -type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[channel.Memory, channel.Data] -} - -// NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide(mem *channel.Memory, cd channel.Command, val channel.DataEffect) PortaVolumeSlide { - pvs := PortaVolumeSlide{} - vs := volumeSlideFactory(mem, cd, val) - pvs.Effects = append(pvs.Effects, vs, PortaToNote(0x00)) - return pvs -} - -func (e PortaVolumeSlide) String() string { - return fmt.Sprintf("L%0.2x", e.Effects[0].(channel.DataEffect)) -} diff --git a/format/it/effect/effect_retrigvolslide.go b/format/it/effect/effect_retrigvolslide.go deleted file mode 100644 index ac82f73..0000000 --- a/format/it/effect/effect_retrigvolslide.go +++ /dev/null @@ -1,71 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide channel.DataEffect // 'Q' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.RetrigVolumeSlide(channel.DataEffect(e)) - if y == 0 { - return nil - } - - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= x { - cs.SetPos(sampling.Pos{}) - cs.ResetRetriggerCount() - switch x { - case 1: - return doVolSlide(cs, -1, 1) - case 2: - return doVolSlide(cs, -2, 1) - case 3: - return doVolSlide(cs, -4, 1) - case 4: - return doVolSlide(cs, -8, 1) - case 5: - return doVolSlide(cs, -6, 1) - case 6: - return doVolSlideTwoThirds(cs) - case 7: - return doVolSlide(cs, 0, float32(0.5)) - case 8: // ? - case 9: - return doVolSlide(cs, 1, 1) - case 10: - return doVolSlide(cs, 2, 1) - case 11: - return doVolSlide(cs, 4, 1) - case 12: - return doVolSlide(cs, 8, 1) - case 13: - return doVolSlide(cs, 16, 1) - case 14: - return doVolSlide(cs, 0, float32(1.5)) - case 15: - return doVolSlide(cs, 0, 2) - } - } - return nil -} - -func (e RetrigVolumeSlide) String() string { - return fmt.Sprintf("Q%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_rowjump.go b/format/it/effect/effect_rowjump.go deleted file mode 100644 index eb0cfc6..0000000 --- a/format/it/effect/effect_rowjump.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/index" -) - -// RowJump defines a row jump effect -type RowJump channel.DataEffect // 'C' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RowJump) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e RowJump) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - r := channel.DataEffect(e) - rowIdx := index.Row(r) - return p.SetNextRow(rowIdx) -} - -func (e RowJump) String() string { - return fmt.Sprintf("C%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_sampleoffset.go b/format/it/effect/effect_sampleoffset.go deleted file mode 100644 index ae5733f..0000000 --- a/format/it/effect/effect_sampleoffset.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// SampleOffset defines a sample offset effect -type SampleOffset channel.DataEffect // 'O' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() - xx := mem.SampleOffset(channel.DataEffect(e)) - - pos := sampling.Pos{Pos: mem.HighOffset + int(xx)*0x100} - if mem.Shared.OldEffectMode { - if inst := cs.GetInstrument(); inst != nil && inst.GetLength().Pos < pos.Pos { - cs.SetTargetPos(pos) - } - } else { - cs.SetTargetPos(pos) - } - return nil -} - -func (e SampleOffset) String() string { - return fmt.Sprintf("O%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setchannelvolume.go b/format/it/effect/effect_setchannelvolume.go deleted file mode 100644 index 1427c23..0000000 --- a/format/it/effect/effect_setchannelvolume.go +++ /dev/null @@ -1,35 +0,0 @@ -package effect - -import ( - "fmt" - - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// SetChannelVolume defines a set channel volume effect -type SetChannelVolume channel.DataEffect // 'Mxx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetChannelVolume) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xx := channel.DataEffect(e) - - cv := itfile.Volume(xx) - - vol := volume.Volume(cv.Value()) - if vol > 1 { - vol = 1 - } - - cs.SetChannelVolume(vol) - return nil -} - -func (e SetChannelVolume) String() string { - return fmt.Sprintf("M%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setcoarsepanposition.go b/format/it/effect/effect_setcoarsepanposition.go deleted file mode 100644 index f283ff2..0000000 --- a/format/it/effect/effect_setcoarsepanposition.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - itPanning "github.com/gotracker/playback/format/it/panning" -) - -// SetCoarsePanPosition defines a set coarse pan position effect -type SetCoarsePanPosition channel.DataEffect // 'S8x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - pan := itfile.PanValue(x << 2) - - cs.SetPan(itPanning.FromItPanning(pan)) - return nil -} - -func (e SetCoarsePanPosition) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setfinetune.go b/format/it/effect/effect_setfinetune.go deleted file mode 100644 index 3ce3790..0000000 --- a/format/it/effect/effect_setfinetune.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/note" -) - -// SetFinetune defines a mod-style set finetune effect -type SetFinetune channel.DataEffect // 'S2x' - -// PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - x := channel.DataEffect(e) & 0xf - - inst := cs.GetTargetInst() - if inst != nil { - ft := (note.Finetune(x) - 8) * 4 - inst.SetFinetune(ft) - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetFinetune) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setglobalvolume.go b/format/it/effect/effect_setglobalvolume.go deleted file mode 100644 index 81f413d..0000000 --- a/format/it/effect/effect_setglobalvolume.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// SetGlobalVolume defines a set global volume effect -type SetGlobalVolume channel.DataEffect // 'V' - -// PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - v := volume.Volume(channel.DataEffect(e)) / 0x80 - if v > 1 { - v = 1 - } - cs.SetChannelVolume(v) - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetGlobalVolume) String() string { - return fmt.Sprintf("V%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setpanbrellowaveform.go b/format/it/effect/effect_setpanbrellowaveform.go deleted file mode 100644 index 77bed8b..0000000 --- a/format/it/effect/effect_setpanbrellowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// SetPanbrelloWaveform defines a set panbrello waveform effect -type SetPanbrelloWaveform channel.DataEffect // 'S5x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanbrelloWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - panb := mem.PanbrelloOscillator() - panb.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetPanbrelloWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setpanposition.go b/format/it/effect/effect_setpanposition.go deleted file mode 100644 index db60a4d..0000000 --- a/format/it/effect/effect_setpanposition.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - itPanning "github.com/gotracker/playback/format/it/panning" -) - -// SetPanPosition defines a set pan position effect -type SetPanPosition channel.DataEffect // 'Xxx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) - - pan := itfile.PanValue(x) - - cs.SetPan(itPanning.FromItPanning(pan)) - return nil -} - -func (e SetPanPosition) String() string { - return fmt.Sprintf("X%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setspeed.go b/format/it/effect/effect_setspeed.go deleted file mode 100644 index 7c8258c..0000000 --- a/format/it/effect/effect_setspeed.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" -) - -// SetSpeed defines a set speed effect -type SetSpeed channel.DataEffect // 'A' - -// PreStart triggers when the effect enters onto the channel state -func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - if e != 0 { - m := p.(effectIntf.IT) - if err := m.SetTicks(int(e)); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetSpeed) String() string { - return fmt.Sprintf("A%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_settempo.go b/format/it/effect/effect_settempo.go deleted file mode 100644 index ff82142..0000000 --- a/format/it/effect/effect_settempo.go +++ /dev/null @@ -1,61 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" -) - -// SetTempo defines a set tempo effect -type SetTempo channel.DataEffect // 'T' - -// PreStart triggers when the effect enters onto the channel state -func (e SetTempo) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - if e > 0x20 { - m := p.(effectIntf.IT) - if err := m.SetTempo(int(e)); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e SetTempo) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - m := p.(effectIntf.IT) - switch channel.DataEffect(e >> 4) { - case 0: // decrease tempo - if currentTick != 0 { - mem := cs.GetMemory() - val := int(mem.TempoDecrease(channel.DataEffect(e & 0x0F))) - if err := m.DecreaseTempo(val); err != nil { - return err - } - } - case 1: // increase tempo - if currentTick != 0 { - mem := cs.GetMemory() - val := int(mem.TempoIncrease(channel.DataEffect(e & 0x0F))) - if err := m.IncreaseTempo(val); err != nil { - return err - } - } - default: - if err := m.SetTempo(int(e)); err != nil { - return err - } - } - return nil -} - -func (e SetTempo) String() string { - return fmt.Sprintf("T%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_settremolowaveform.go b/format/it/effect/effect_settremolowaveform.go deleted file mode 100644 index 7449d70..0000000 --- a/format/it/effect/effect_settremolowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// SetTremoloWaveform defines a set tremolo waveform effect -type SetTremoloWaveform channel.DataEffect // 'S4x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - trem := mem.TremoloOscillator() - trem.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetTremoloWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_setvibratowaveform.go b/format/it/effect/effect_setvibratowaveform.go deleted file mode 100644 index 718c799..0000000 --- a/format/it/effect/effect_setvibratowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// SetVibratoWaveform defines a set vibrato waveform effect -type SetVibratoWaveform channel.DataEffect // 'S3x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - vib := mem.VibratoOscillator() - vib.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetVibratoWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_tremolo.go b/format/it/effect/effect_tremolo.go deleted file mode 100644 index 56abe48..0000000 --- a/format/it/effect/effect_tremolo.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// Tremolo defines a tremolo effect -type Tremolo channel.DataEffect // 'R' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremolo) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Tremolo(channel.DataEffect(e)) - // NOTE: JBC - IT dos not update on tick 0, but MOD does. - // Maybe need to add a flag for converted MOD backward compatibility? - if currentTick != 0 { - return doTremolo(cs, currentTick, x, y, 4) - } - return nil -} - -func (e Tremolo) String() string { - return fmt.Sprintf("R%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_tremor.go b/format/it/effect/effect_tremor.go deleted file mode 100644 index 3972b2a..0000000 --- a/format/it/effect/effect_tremor.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// Tremor defines a tremor effect -type Tremor channel.DataEffect // 'I' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremor) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Tremor(channel.DataEffect(e)) - return doTremor(cs, currentTick, int(x)+1, int(y)+1) -} - -func (e Tremor) String() string { - return fmt.Sprintf("I%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_vibrato.go b/format/it/effect/effect_vibrato.go deleted file mode 100644 index ef775c1..0000000 --- a/format/it/effect/effect_vibrato.go +++ /dev/null @@ -1,36 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// Vibrato defines a vibrato effect -type Vibrato channel.DataEffect // 'H' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e Vibrato) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) - if mem.Shared.OldEffectMode { - if currentTick != 0 { - return doVibrato(cs, currentTick, x, y, 8) - } - } else { - return doVibrato(cs, currentTick, x, y, 4) - } - return nil -} - -func (e Vibrato) String() string { - return fmt.Sprintf("H%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_vibratovolslide.go b/format/it/effect/effect_vibratovolslide.go deleted file mode 100644 index 20ac2d9..0000000 --- a/format/it/effect/effect_vibratovolslide.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// VibratoVolumeSlide defines a combination vibrato and volume slide effect -type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[channel.Memory, channel.Data] -} - -// NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide(mem *channel.Memory, cd channel.Command, val channel.DataEffect) VibratoVolumeSlide { - vvs := VibratoVolumeSlide{} - vs := volumeSlideFactory(mem, cd, val) - vvs.Effects = append(vvs.Effects, vs, Vibrato(0x00)) - return vvs -} - -func (e VibratoVolumeSlide) String() string { - return fmt.Sprintf("K%0.2x", e.Effects[0].(channel.DataEffect)) -} diff --git a/format/it/effect/effect_volslidedown.go b/format/it/effect/effect_volslidedown.go deleted file mode 100644 index c461fb5..0000000 --- a/format/it/effect/effect_volslidedown.go +++ /dev/null @@ -1,46 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// VolumeSlideDown defines a volume slide down effect -type VolumeSlideDown channel.DataEffect // 'D' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e VolumeSlideDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - _, y := mem.VolumeSlide(channel.DataEffect(e)) - - return doVolSlide(cs, -float32(y), 1.0) -} - -func (e VolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} - -//==================================================== - -// VolChanVolumeSlideDown defines a volume slide down effect (from the volume channel) -type VolChanVolumeSlideDown channel.DataEffect // 'd' - -// Tick is called on every tick -func (e VolChanVolumeSlideDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - y := mem.VolChanVolumeSlide(channel.DataEffect(e)) - - return doVolSlide(cs, -float32(y), 1.0) -} - -func (e VolChanVolumeSlideDown) String() string { - return fmt.Sprintf("d0%x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_volslideup.go b/format/it/effect/effect_volslideup.go deleted file mode 100644 index 343ce4e..0000000 --- a/format/it/effect/effect_volslideup.go +++ /dev/null @@ -1,46 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// VolumeSlideUp defines a volume slide up effect -type VolumeSlideUp channel.DataEffect // 'D' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e VolumeSlideUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, _ := mem.VolumeSlide(channel.DataEffect(e)) - - return doVolSlide(cs, float32(x), 1.0) -} - -func (e VolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} - -//==================================================== - -// VolChanVolumeSlideUp defines a volume slide up effect (from the volume channel) -type VolChanVolumeSlideUp channel.DataEffect // 'd' - -// Tick is called on every tick -func (e VolChanVolumeSlideUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x := mem.VolChanVolumeSlide(channel.DataEffect(e)) - - return doVolSlide(cs, float32(x), 1.0) -} - -func (e VolChanVolumeSlideUp) String() string { - return fmt.Sprintf("d%x0", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_volumeenvelopeoff.go b/format/it/effect/effect_volumeenvelopeoff.go deleted file mode 100644 index aab597b..0000000 --- a/format/it/effect/effect_volumeenvelopeoff.go +++ /dev/null @@ -1,23 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// VolumeEnvelopeOff defines a volume envelope: off effect -type VolumeEnvelopeOff channel.DataEffect // 'S77' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOff) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - cs.SetVolumeEnvelopeEnable(false) - return nil -} - -func (e VolumeEnvelopeOff) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effect_volumeenvelopeon.go b/format/it/effect/effect_volumeenvelopeon.go deleted file mode 100644 index 4262709..0000000 --- a/format/it/effect/effect_volumeenvelopeon.go +++ /dev/null @@ -1,23 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -// VolumeEnvelopeOn defines a volume envelope: on effect -type VolumeEnvelopeOn channel.DataEffect // 'S78' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOn) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - cs.SetVolumeEnvelopeEnable(true) - return nil -} - -func (e VolumeEnvelopeOn) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/it/effect/effectfactory.go b/format/it/effect/effectfactory.go deleted file mode 100644 index 39a64e0..0000000 --- a/format/it/effect/effectfactory.go +++ /dev/null @@ -1,243 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" -) - -type EffectIT interface { - playback.Effect -} - -// VolEff is a combined effect that includes a volume effect and a standard effect -type VolEff struct { - playback.CombinedEffect[channel.Memory, channel.Data] - eff EffectIT -} - -func (e VolEff) String() string { - if e.eff == nil { - return "..." - } - return fmt.Sprint(e.eff) -} - -// Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data *channel.Data) EffectIT { - if data == nil { - return nil - } - - if !data.What.HasCommand() && !data.What.HasVolPan() { - return nil - } - - eff := VolEff{} - if data.What.HasVolPan() { - ve := volPanEffectFactory(mem, data.VolPan) - if ve != nil { - eff.Effects = append(eff.Effects, ve) - } - } - - if e := standardEffectFactory(mem, data); e != nil { - eff.Effects = append(eff.Effects, e) - eff.eff = e - } - - switch len(eff.Effects) { - case 0: - return nil - case 1: - return eff.Effects[0] - default: - return &eff - } -} - -func standardEffectFactory(mem *channel.Memory, data *channel.Data) EffectIT { - switch data.Effect + '@' { - case '@': // unused - return nil - case 'A': // Set Speed - return SetSpeed(data.EffectParameter) - case 'B': // Pattern Jump - return OrderJump(data.EffectParameter) - case 'C': // Pattern Break - return RowJump(data.EffectParameter) - case 'D': // Volume Slide / Fine Volume Slide - return volumeSlideFactory(mem, data.Effect, data.EffectParameter) - case 'E': // Porta Down/Fine Porta Down/Xtra Fine Porta - xx := mem.PortaDown(channel.DataEffect(data.EffectParameter)) - x := xx >> 4 - if x == 0x0F { - return FinePortaDown(xx) - } else if x == 0x0E { - return ExtraFinePortaDown(xx) - } - return PortaDown(data.EffectParameter) - case 'F': // Porta Up/Fine Porta Up/Extra Fine Porta Down - xx := mem.PortaUp(channel.DataEffect(data.EffectParameter)) - x := xx >> 4 - if x == 0x0F { - return FinePortaUp(xx) - } else if x == 0x0E { - return ExtraFinePortaUp(xx) - } - return PortaUp(data.EffectParameter) - case 'G': // Porta to note - return PortaToNote(data.EffectParameter) - case 'H': // Vibrato - return Vibrato(data.EffectParameter) - case 'I': // Tremor - return Tremor(data.EffectParameter) - case 'J': // Arpeggio - return Arpeggio(data.EffectParameter) - case 'K': // Vibrato+Volume Slide - return NewVibratoVolumeSlide(mem, data.Effect, data.EffectParameter) - case 'L': // Porta+Volume Slide - return NewPortaVolumeSlide(mem, data.Effect, data.EffectParameter) - case 'M': // Set Channel Volume - return SetChannelVolume(data.EffectParameter) - case 'N': // Channel Volume Slide - return ChannelVolumeSlide(data.EffectParameter) - case 'O': // Sample Offset - return SampleOffset(data.EffectParameter) - case 'P': // Panning Slide - //return panningSlideFactory(mem, data.Effect, data.EffectParameter) - case 'Q': // Retrig + Volume Slide - return RetrigVolumeSlide(data.EffectParameter) - case 'R': // Tremolo - return Tremolo(data.EffectParameter) - case 'S': // Special - return specialEffect(data) - case 'T': // Set Tempo - return SetTempo(data.EffectParameter) - case 'U': // Fine Vibrato - return FineVibrato(data.EffectParameter) - case 'V': // Global Volume - return SetGlobalVolume(data.EffectParameter) - case 'W': // Global Volume Slide - return GlobalVolumeSlide(data.EffectParameter) - case 'X': // Set Pan Position - return SetPanPosition(data.EffectParameter) - case 'Y': // Panbrello - //return Panbrello(data.EffectParameter) - case 'Z': // MIDI Macro - return nil // TODO: MIDIMacro - default: - } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} -} - -func specialEffect(data *channel.Data) EffectIT { - switch data.EffectParameter >> 4 { - case 0x0: // unused - return nil - //case 0x1: // Set Glissando on/off - - case 0x2: // Set FineTune - return SetFinetune(data.EffectParameter) - case 0x3: // Set Vibrato Waveform - return SetVibratoWaveform(data.EffectParameter) - case 0x4: // Set Tremolo Waveform - return SetTremoloWaveform(data.EffectParameter) - case 0x5: // Set Panbrello Waveform - return SetPanbrelloWaveform(data.EffectParameter) - case 0x6: // Fine Pattern Delay - return FinePatternDelay(data.EffectParameter) - case 0x7: // special note operations - return specialNoteEffects(data) - case 0x8: // Set Coarse Pan Position - return SetCoarsePanPosition(data.EffectParameter) - case 0x9: // Sound Control - return soundControlEffect(data) - case 0xA: // High Offset - return HighOffset(data.EffectParameter) - case 0xB: // Pattern Loop - return PatternLoop(data.EffectParameter) - case 0xC: // Note Cut - return NoteCut(data.EffectParameter) - case 0xD: // Note Delay - return NoteDelay(data.EffectParameter) - case 0xE: // Pattern Delay - return PatternDelay(data.EffectParameter) - case 0xF: // Set Active Macro - return nil // TODO: SetActiveMacro - default: - } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} -} - -func specialNoteEffects(data *channel.Data) EffectIT { - switch data.EffectParameter & 0xf { - case 0x0: // Past Note Cut - return PastNoteCut(data.EffectParameter) - case 0x1: // Past Note Off - return PastNoteOff(data.EffectParameter) - case 0x2: // Past Note Fade - return PastNoteFade(data.EffectParameter) - case 0x3: // New Note Action: Note Cut - return NewNoteActionNoteCut(data.EffectParameter) - case 0x4: // New Note Action: Note Continue - return NewNoteActionNoteContinue(data.EffectParameter) - case 0x5: // New Note Action: Note Off - return NewNoteActionNoteOff(data.EffectParameter) - case 0x6: // New Note Action: Note Fade - return NewNoteActionNoteFade(data.EffectParameter) - case 0x7: // Volume Envelope Off - return VolumeEnvelopeOff(data.EffectParameter) - case 0x8: // Volume Envelope On - return VolumeEnvelopeOn(data.EffectParameter) - case 0x9: // Panning Envelope Off - return PanningEnvelopeOff(data.EffectParameter) - case 0xA: // Panning Envelope On - return PanningEnvelopeOn(data.EffectParameter) - case 0xB: // Pitch Envelope Off - return PitchEnvelopeOff(data.EffectParameter) - case 0xC: // Pitch Envelope On - return PitchEnvelopeOn(data.EffectParameter) - case 0xD, 0xE, 0xF: // unused - return nil - } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} -} - -func volumeSlideFactory(mem *channel.Memory, cd channel.Command, ce channel.DataEffect) EffectIT { - x, y := mem.VolumeSlide(channel.DataEffect(ce)) - switch { - case x == 0: - return VolumeSlideDown(ce) - case y == 0: - return VolumeSlideUp(ce) - case x == 0x0f: - return FineVolumeSlideDown(ce) - case y == 0x0f: - return FineVolumeSlideUp(ce) - } - // There is a chance that a volume slide command is set with an invalid - // value or is 00, in which case the memory might have the invalid value, - // so we need to handle it by deferring to using a no-op instead of a - // VolumeSlideDown - return nil -} - -func soundControlEffect(data *channel.Data) EffectIT { - switch data.EffectParameter & 0xF { - case 0x0: // Surround Off - case 0x1: // Surround On - // only S91 is supported directly by IT - return nil // TODO: SurroundOn - case 0x8: // Reverb Off - case 0x9: // Reverb On - case 0xA: // Center Surround - case 0xB: // Quad Surround - case 0xC: // Global Filters - case 0xD: // Local Filters - case 0xE: // Play Forward - case 0xF: // Play Backward - } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} -} diff --git a/format/it/effect/effectfactory_volume.go b/format/it/effect/effectfactory_volume.go deleted file mode 100644 index 6c0fd56..0000000 --- a/format/it/effect/effectfactory_volume.go +++ /dev/null @@ -1,65 +0,0 @@ -package effect - -import ( - "github.com/gotracker/playback/format/it/channel" -) - -func volPanEffectFactory(mem *channel.Memory, v uint8) EffectIT { - switch { - case v <= 0x40: // volume set - handled elsewhere - return nil - case v >= 0x41 && v <= 0x4a: // fine volume slide up - return VolChanFineVolumeSlideUp(v - 0x41) - case v >= 0x4b && v <= 0x54: // fine volume slide down - return VolChanFineVolumeSlideDown(v - 0x4b) - case v >= 0x55 && v <= 0x5e: // volume slide up - return VolChanVolumeSlideUp(v - 0x55) - case v >= 0x5f && v <= 0x68: // volume slide down - return VolChanVolumeSlideDown(v - 0x5f) - case v >= 0x69 && v <= 0x72: // portamento down - return volPortaDown(v - 0x69) - case v >= 0x73 && v <= 0x7c: // portamento up - return volPortaUp(v - 0x73) - case v >= 0x80 && v <= 0xc0: // set panning - return SetPanPosition(v - 0x80) - case v >= 0xc1 && v <= 0xca: // portamento to note - return volPortaToNote(v - 0xc1) - case v >= 0xcb && v <= 0xd4: // vibrato - return Vibrato(v - 0xcb) - } - return UnhandledVolCommand{Vol: v} -} - -func volPortaDown(v uint8) EffectIT { - return PortaDown(v * 4) -} -func volPortaUp(v uint8) EffectIT { - return PortaUp(v * 4) -} - -func volPortaToNote(v uint8) EffectIT { - switch v { - case 0: - return PortaToNote(0x00) - case 1: - return PortaToNote(0x01) - case 2: - return PortaToNote(0x04) - case 3: - return PortaToNote(0x08) - case 4: - return PortaToNote(0x10) - case 5: - return PortaToNote(0x20) - case 6: - return PortaToNote(0x40) - case 7: - return PortaToNote(0x60) - case 8: - return PortaToNote(0x80) - case 9: - return PortaToNote(0xFF) - } - // impossible, but hey... - return UnhandledVolCommand{Vol: v + 0xc1} -} diff --git a/format/it/effect/intf/intf.go b/format/it/effect/intf/intf.go deleted file mode 100644 index e586579..0000000 --- a/format/it/effect/intf/intf.go +++ /dev/null @@ -1,23 +0,0 @@ -package intf - -import ( - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/index" -) - -// IT is an interface to IT effect operations -type IT interface { - SetTicks(int) error // Axx - SetNextOrder(index.Order) error // Bxx - SetNextRow(index.Row) error // Cxx - AddRowTicks(int) error // S6x - SetNextRowWithBacktrack(index.Row, bool) error // SBx - GetCurrentRow() index.Row // SBx - SetPatternDelay(int) error // SEx - SetTempo(int) error // Txx - IncreaseTempo(int) error // Txx - DecreaseTempo(int) error // Txx - SetGlobalVolume(volume.Volume) // Vxx, Wxx - GetGlobalVolume() volume.Volume // Vxx, Wxx - IgnoreUnknownEffect() bool // Unhandled -} diff --git a/format/it/effect/unhandled.go b/format/it/effect/unhandled.go deleted file mode 100644 index 493af35..0000000 --- a/format/it/effect/unhandled.go +++ /dev/null @@ -1,44 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" -) - -// UnhandledCommand is an unhandled command -type UnhandledCommand struct { - Command channel.Command - Info channel.DataEffect -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory, channel.Data], m effectIntf.IT) error { - if !m.IgnoreUnknownEffect() { - panic(fmt.Sprintf("unhandled command: ce:%0.2X cp:%0.2X", e.Command, e.Info)) - } - return nil -} - -func (e UnhandledCommand) String() string { - return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) -} - -// UnhandledVolCommand is an unhandled volume command -type UnhandledVolCommand struct { - Vol uint8 -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand) PreStart(cs playback.Channel[channel.Memory, channel.Data], m effectIntf.IT) error { - if !m.IgnoreUnknownEffect() { - panic(fmt.Sprintf("unhandled command: volCmd:%0.2X", e.Vol)) - } - return nil -} - -func (e UnhandledVolCommand) String() string { - return fmt.Sprintf("v%0.2x", e.Vol) -} diff --git a/format/it/effect/util.go b/format/it/effect/util.go deleted file mode 100644 index e542375..0000000 --- a/format/it/effect/util.go +++ /dev/null @@ -1,186 +0,0 @@ -package effect - -import ( - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" - itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" -) - -func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32, multiplier float32) error { - av := cs.GetActiveVolume() - v := itVolume.ToItVolume(av) - vol := int16((float32(v) + delta) * multiplier) - if vol >= 0x40 { - vol = 0x40 - } - if vol < 0x00 { - vol = 0x00 - } - v = itfile.Volume(vol) - nv := itVolume.FromItVolume(v) - cs.SetActiveVolume(nv) - return nil -} - -func doGlobalVolSlide(m effectIntf.IT, delta float32, multiplier float32) error { - gv := m.GetGlobalVolume() - v := itVolume.ToItVolume(gv) - vol := int16((float32(v) + delta) * multiplier) - if vol >= 0x40 { - vol = 0x40 - } - if vol < 0x00 { - vol = 0x00 - } - v = itfile.Volume(vol) - ngv := itVolume.FromItVolume(v) - m.SetGlobalVolume(ngv) - return nil -} - -func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory, channel.Data], delta int) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) - cs.SetPeriod(cur) - return nil -} - -func doPortaByDeltaLinear(cs playback.Channel[channel.Memory, channel.Data], delta int) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - finetune := period.PeriodDelta(delta) - cur = cur.AddDelta(finetune) - cs.SetPeriod(cur) - return nil -} - -func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { - delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, delta) - } - return doPortaByDeltaAmiga(cs, -delta) -} - -func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaUp(cs, amount, multiplier, linearFreqSlides); err != nil { - return err - } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { - cs.SetPeriod(target) - } - return nil -} - -func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { - delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, -delta) - } - return doPortaByDeltaAmiga(cs, delta) -} - -func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaDown(cs, amount, multiplier, linearFreqSlides); err != nil { - return err - } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { - cs.SetPeriod(target) - } - return nil -} - -func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { - mem := cs.GetMemory() - vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) - delta := period.PeriodDelta(vib) - cs.SetPeriodDelta(delta) - return nil -} - -func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int, onTicks int, offTicks int) error { - mem := cs.GetMemory() - tremor := mem.TremorMem() - if tremor.IsActive() { - if tremor.Advance() >= onTicks { - tremor.ToggleAndReset() - } - } else { - if tremor.Advance() >= offTicks { - tremor.ToggleAndReset() - } - } - cs.SetVolumeActive(tremor.IsActive()) - return nil -} - -func doArpeggio(cs playback.Channel[channel.Memory, channel.Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { - ns := cs.GetNoteSemitone() - var arpSemitoneTarget note.Semitone - switch currentTick % 3 { - case 0: - arpSemitoneTarget = ns - case 1: - arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneADelta) - case 2: - arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneBDelta) - } - cs.SetOverrideSemitone(arpSemitoneTarget) - cs.SetTargetPos(cs.GetPos()) - return nil -} - -var ( - volSlideTwoThirdsTable = [...]channel.DataEffect{ - 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9, - 10, 10, 11, 11, 12, 13, 13, 14, 15, 15, 16, 16, 17, 18, 18, 19, - 20, 20, 21, 21, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29, - 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, - } -) - -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) error { - vol := itVolume.ToItVolume(cs.GetActiveVolume()) - if vol >= 0x10 && vol <= 0x50 { - vol -= 0x10 - if vol >= 64 { - vol = 63 - } - - v := volSlideTwoThirdsTable[vol] - if v >= 0x40 { - v = 0x40 - } - - vv := itfile.Volume(v) - cs.SetActiveVolume(itVolume.FromItVolume(vv)) - } - return nil -} - -func doTremolo(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return doVolSlide(cs, delta, 1.0) -} - -func calculateWaveTable(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { - delta := o.GetWave(float32(depth) * multiplier) - o.Advance(int(speed)) - return delta -} diff --git a/format/it/filter/factory.go b/format/it/filter/factory.go new file mode 100644 index 0000000..1a18c57 --- /dev/null +++ b/format/it/filter/factory.go @@ -0,0 +1,39 @@ +package filter + +import ( + "errors" + "fmt" + + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" +) + +func Factory(name string, instrumentRate frequency.Frequency, params any) (filter.Filter, error) { + var f filter.Filter + switch name { + case "": + // nothing + + case "amigalpf": + f = filter.NewAmigaLPF(instrumentRate) + + case "itresonant": + p, ok := params.(filter.ITResonantFilterParams) + if !ok { + return nil, errors.New("could not convert it resonant filter parameters") + } + f = filter.NewITResonantFilter(p.Cutoff, p.Resonance, p.ExtendedFilterRange, p.Highpass) + + case "echo": + p, ok := params.(filter.EchoFilterSettings) + if !ok { + return nil, errors.New("could not convert echo filter parameters") + } + f = &filter.EchoFilter{EchoFilterSettings: p} + + default: + return nil, fmt.Errorf("unsupported filter name: %q", name) + } + + return f, nil +} diff --git a/format/it/it.go b/format/it/it.go index bc74995..3d1e7b5 100644 --- a/format/it/it.go +++ b/format/it/it.go @@ -4,13 +4,21 @@ package it import ( "io" - "github.com/gotracker/playback" + "github.com/gotracker/playback/format/common" + itFeature "github.com/gotracker/playback/format/it/feature" "github.com/gotracker/playback/format/it/load" + itSettings "github.com/gotracker/playback/format/it/settings" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/song" "github.com/gotracker/playback/util" ) -type format struct{} +type format struct { + common.Format +} var ( // IT is the exported interface to the IT file loader @@ -18,7 +26,7 @@ var ( ) // Load loads an IT file into a playback system -func (f format) Load(filename string, features []feature.Feature) (playback.Playback, error) { +func (f format) Load(filename string, features []feature.Feature) (song.Data, error) { r, err := util.ReadFile(filename) if err != nil { return nil, err @@ -28,6 +36,24 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play } // LoadFromReader loads an IT file on a reader into a playback system -func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { +func (format) LoadFromReader(r io.Reader, features []feature.Feature) (song.Data, error) { return load.IT(r, features) } + +func (f format) ConvertFeaturesToSettings(us *settings.UserSettings, features []feature.Feature) error { + for _, feat := range features { + switch f := feat.(type) { + case itFeature.LongChannelOutput: + us.LongChannelOutput = f.Enabled + case itFeature.NewNoteActions: + us.EnableNewNoteActions = f.Enabled + } + } + + return f.Format.ConvertFeaturesToSettings(us, features) +} + +func init() { + machine.RegisterMachine(itSettings.GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(itSettings.GetMachineSettings[period.Linear]()) +} diff --git a/format/it/layout/channelsetting.go b/format/it/layout/channelsetting.go index 925128e..162e09e 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -1,17 +1,80 @@ package layout import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/format/it/channel" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/vol0optimization" ) // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled bool + Muted bool OutputChannelNum int - InitialVolume volume.Volume - ChannelVolume volume.Volume - InitialPanning panning.Position + InitialVolume itVolume.Volume + ChannelVolume itVolume.FineVolume + PanEnabled bool + InitialPanning itPanning.Panning Memory channel.Memory + Vol0OptEnabled bool +} + +var _ song.ChannelSettings = (*ChannelSetting)(nil) + +func (c ChannelSetting) IsEnabled() bool { + return c.Enabled +} + +func (c ChannelSetting) IsMuted() bool { + return c.Muted +} + +func (c ChannelSetting) GetOutputChannelNum() int { + return c.OutputChannelNum +} + +func (c ChannelSetting) GetInitialVolume() itVolume.Volume { + return c.InitialVolume +} + +func (c ChannelSetting) GetMixingVolume() itVolume.FineVolume { + return c.ChannelVolume +} + +func (c ChannelSetting) GetInitialPanning() itPanning.Panning { + if c.PanEnabled { + return c.InitialPanning + } + return itPanning.DefaultPanning +} + +func (c ChannelSetting) GetMemory() song.ChannelMemory { + return &c.Memory +} + +func (c ChannelSetting) IsPanEnabled() bool { + return c.PanEnabled +} + +func (c ChannelSetting) GetDefaultFilterInfo() filter.Info { + return filter.Info{} +} + +func (c ChannelSetting) IsDefaultFilterEnabled() bool { + return false +} + +func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { + return vol0optimization.Vol0OptimizationSettings{ + Enabled: c.Vol0OptEnabled, + MaxRowsAt0: 3, + } +} + +func (ChannelSetting) GetOPLChannel() index.OPLChannel { + return index.InvalidOPLChannel } diff --git a/format/it/layout/header.go b/format/it/layout/header.go index 57b70d4..fcce866 100644 --- a/format/it/layout/header.go +++ b/format/it/layout/header.go @@ -1,12 +1,17 @@ package layout -import "github.com/gotracker/gomixing/volume" +import ( + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" +) // Header is a mildly-decoded IT header definition type Header struct { - Name string - InitialSpeed int - InitialTempo int - GlobalVolume volume.Volume - MixingVolume volume.Volume + Name string + InitialSpeed int + InitialTempo int + GlobalVolume itVolume.FineVolume + MixingVolume itVolume.FineVolume + LinearFreqSlides bool + InitialOrder index.Order } diff --git a/format/it/layout/noteinstrument.go b/format/it/layout/noteinstrument.go index 00b82bd..9850c86 100644 --- a/format/it/layout/noteinstrument.go +++ b/format/it/layout/noteinstrument.go @@ -1,12 +1,15 @@ package layout import ( + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // NoteInstrument is the note remapping and instrument pair -type NoteInstrument struct { +type NoteInstrument[TPeriod period.Period] struct { NoteRemap note.Semitone - Inst *instrument.Instrument + Inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] } diff --git a/format/it/layout/row.go b/format/it/layout/row.go new file mode 100644 index 0000000..3147857 --- /dev/null +++ b/format/it/layout/row.go @@ -0,0 +1,28 @@ +package layout + +import ( + "github.com/gotracker/playback/format/it/channel" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" +) + +type Row[TPeriod period.Period] []channel.Data[TPeriod] + +func (r Row[TPeriod]) Len() int { + return len(r) +} + +func (r Row[TPeriod]) ForEach(fn func(ch index.Channel, cd song.ChannelData[itVolume.Volume]) (bool, error)) error { + for i, c := range r { + cont, err := fn(index.Channel(i), c) + if err != nil { + return err + } + if !cont { + break + } + } + return nil +} diff --git a/format/it/layout/song.go b/format/it/layout/song.go index 5574863..24fdd17 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -2,83 +2,98 @@ package layout import ( "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/it/channel" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" ) -// Song is the full definition of the song data of an Song file -type Song struct { - Head Header - Instruments map[uint8]*instrument.Instrument - InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument - Patterns []pattern.Pattern[channel.Data] - ChannelSettings []ChannelSetting - OrderList []index.Pattern - FilterPlugins map[int]filter.Factory -} +type SemitoneSample uint16 -// GetOrderList returns the list of all pattern orders for the song -func (s Song) GetOrderList() []index.Pattern { - return s.OrderList +func (s SemitoneSample) Split() (int, note.Semitone) { + return int(s >> 8), note.Semitone(s & 0xFF) } -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { - if int(patNum) >= len(s.Patterns) { - return nil - } - return &s.Patterns[patNum] +func NewSemitoneSample(sampIdx int, remap note.Semitone) SemitoneSample { + return SemitoneSample(uint16(sampIdx<<8) | uint16(remap)) } -// IsChannelEnabled returns true if the channel at index `channelNum` is enabled -func (s Song) IsChannelEnabled(channelNum int) bool { - return s.ChannelSettings[channelNum].Enabled -} +type SemitoneSamples [120]SemitoneSample // semitone -> sample + semitone remap + +// Song is the full definition of the song data of an IT file +type Song[TPeriod period.Period] struct { + common.BaseSong[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] -// GetRenderChannel returns the output channel for the channel at index `channelNum` -func (s Song) GetRenderChannel(channelNum int) int { - return s.ChannelSettings[channelNum].OutputChannelNum + InstrumentNoteMap map[uint8]SemitoneSamples + ChannelSettings []ChannelSetting + FilterPlugins map[int]filter.Info } -// NumInstruments returns the number of instruments in the song -func (s Song) NumInstruments() int { - return len(s.Instruments) +// GetNumChannels returns the number of channels the song has +func (s Song[TPeriod]) GetNumChannels() int { + return len(s.ChannelSettings) } -// IsValidInstrumentID returns true if the instrument exists -func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { - if instNum.IsEmpty() { - return false - } - switch id := instNum.(type) { - case channel.SampleID: - _, ok := s.Instruments[id.InstID] - return ok - } - return false +// GetChannelSettings returns the channel settings at index `channelNum` +func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + return s.ChannelSettings[channelNum] } // GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instNum instrument.ID) (*instrument.Instrument, note.Semitone) { - if instNum.IsEmpty() { - return nil, note.UnchangedSemitone +func (s Song[TPeriod]) GetInstrument(instID int, st note.Semitone) (instrument.InstrumentIntf, note.Semitone) { + if instID == 0 { + return nil, st } - switch id := instNum.(type) { - case channel.SampleID: - if nm, ok1 := s.InstrumentNoteMap[id.InstID]; ok1 { - if sm, ok2 := nm[id.Semitone]; ok2 { - return sm.Inst, sm.NoteRemap - } + + idx := instID - 1 + + if inm, ok := s.InstrumentNoteMap[uint8(instID)]; ok { + if rm := inm[st]; rm != 0 { + idx, st = rm.Split() } } - return nil, note.UnchangedSemitone + + if idx < 0 || idx >= len(s.Instruments) { + return nil, st + } + + return s.Instruments[idx], st +} + +func (s Song[TPeriod]) GetRowRenderStringer(row song.Row, channels int, longFormat bool) render.RowStringer { + rt := render.NewRowText[channel.Data[TPeriod]](channels, longFormat) + rowData := make([]channel.Data[TPeriod], channels) + song.ForEachRowChannel(row, func(ch index.Channel, d song.ChannelData[itVolume.Volume]) (bool, error) { + if int(ch) >= channels || !s.ChannelSettings[ch].Enabled || s.ChannelSettings[ch].Muted { + return true, nil + } + rowData[ch] = d.(channel.Data[TPeriod]) + return true, nil + }) + rt.Channels = rowData + return rt } -// GetName returns the name of the song -func (s Song) GetName() string { - return s.Head.Name +func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { + for i, cs := range s.ChannelSettings { + if enabledOnly { + if !cs.Enabled || (cs.Muted && s.MS.Quirks.DoNotProcessEffectsOnMutedChannels) { + continue + } + } + cont, err := fn(index.Channel(i)) + if err != nil { + return err + } + if !cont { + break + } + } + return nil } diff --git a/format/it/layout/stringrow.go b/format/it/layout/stringrow.go new file mode 100644 index 0000000..c642dfb --- /dev/null +++ b/format/it/layout/stringrow.go @@ -0,0 +1,143 @@ +package layout + +import ( + "fmt" + "regexp" + "slices" + "strconv" + "strings" + + itfile "github.com/gotracker/goaudiofile/music/tracked/it" + "github.com/gotracker/playback/format/it/channel" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" +) + +type StringRow[TPeriod period.Period] string + +func (r StringRow[TPeriod]) Len() int { + return len(strings.SplitAfter(string(r), "|")) - 1 +} + +func (r StringRow[TPeriod]) ForEach(fn func(ch index.Channel, d song.ChannelData[itVolume.Volume]) (bool, error)) error { + cstrPieces := strings.SplitAfter(string(r), "|") + cstrPieces = slices.DeleteFunc(cstrPieces, func(s string) bool { + return len(s) == 0 || s == "|" + }) + + row := make(Row[TPeriod], len(cstrPieces)) + for ch, cstr := range cstrPieces { + d, err := r.decodeChannel(strings.TrimSuffix(cstr, "|")) + if err != nil { + return err + } + row[ch] = d + } + + return row.ForEach(fn) +} + +var channelRegex = regexp.MustCompile(`^(...) +(..) +(..) +(...)$`) + +func (StringRow[TPeriod]) decodeChannel(cstr string) (channel.Data[TPeriod], error) { + var d channel.Data[TPeriod] + + pieces := channelRegex.FindStringSubmatch(cstr) + if len(pieces) != 5 { + return d, fmt.Errorf("could not parse channel: %q", cstr) + } + note, instrument, vol, cmd := pieces[1], pieces[2], pieces[3], pieces[4] + + d.Note = 0 + + // note + if note == "===" || note == "== " { + d.What |= itfile.ChannelDataFlagNote + d.Note = 255 + } else if note == "^^^" || note == "^^ " { + d.What |= itfile.ChannelDataFlagNote + d.Note = 254 + } else if note == "vvv" || note == "vv " { + d.What |= itfile.ChannelDataFlagNote + d.Note = 234 + } else if note != "..." { + key := note[0:2] + oct, err := strconv.Atoi(note[2:]) + if err != nil { + return d, err + } + + switch key { + case "C-": + d.Note = itfile.Note(oct*12 + 0) + case "C#": + d.Note = itfile.Note(oct*12 + 1) + case "D-": + d.Note = itfile.Note(oct*12 + 2) + case "D#": + d.Note = itfile.Note(oct*12 + 3) + case "E-": + d.Note = itfile.Note(oct*12 + 4) + case "F-": + d.Note = itfile.Note(oct*12 + 5) + case "F#": + d.Note = itfile.Note(oct*12 + 6) + case "G-": + d.Note = itfile.Note(oct*12 + 7) + case "G#": + d.Note = itfile.Note(oct*12 + 8) + case "A-": + d.Note = itfile.Note(oct*12 + 9) + case "A#": + d.Note = itfile.Note(oct*12 + 10) + case "B-": + d.Note = itfile.Note(oct*12 + 11) + default: + return d, fmt.Errorf("invalid key in note: %q", note) + } + d.What |= itfile.ChannelDataFlagNote + } + + // instrument + if instrument != ".." { + i, err := strconv.ParseUint(strings.TrimSpace(instrument), 16, 8) + if err != nil { + return d, err + } + + if i > 0 { + d.What |= itfile.ChannelDataFlagInstrument + d.Instrument = uint8(i) + } + } + + // vol + if vol != ".." { + v, err := strconv.ParseUint(vol, 16, 8) + if err != nil { + return d, err + } + + d.What |= itfile.ChannelDataFlagVolPan + d.VolPan = uint8(v) + } + + // cmd + if cmd != "..." { + c := cmd[0] + i, err := strconv.ParseUint(cmd[1:], 16, 8) + if err != nil { + return d, err + } + + d.What |= itfile.ChannelDataFlagCommand + if e := c - '@'; e < 26 { + d.Effect = channel.Command(e) + } + d.EffectParameter = channel.DataEffect(i) + } + + return d, nil +} diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 70e2d58..0950a9a 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -8,27 +8,31 @@ import ( "math" itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/util" + "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/envelope" "github.com/gotracker/playback/voice/fadeout" "github.com/gotracker/playback/voice/loop" - "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback/voice/pcm" + "github.com/gotracker/playback/voice/pitchpan" + "github.com/gotracker/playback/voice/types" + "github.com/heucuva/optional" "github.com/gotracker/playback/filter" - itfilter "github.com/gotracker/playback/format/it/filter" itNote "github.com/gotracker/playback/format/it/note" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" oscillatorImpl "github.com/gotracker/playback/oscillator" ) -type convInst struct { - Inst *instrument.Instrument +type convInst[TPeriod period.Period] struct { + Inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] NR []noteRemap } @@ -38,8 +42,8 @@ type convertITInstrumentSettings struct { useHighPassFilter bool } -func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData []itfile.FullSample, convSettings convertITInstrumentSettings, features []feature.Feature) (map[int]*convInst, error) { - outInsts := make(map[int]*convInst) +func convertITInstrumentOldToInstrument[TPeriod period.Period](inst *itfile.IMPIInstrumentOld, pc period.PeriodConverter[TPeriod], sampData []itfile.FullSample, convSettings convertITInstrumentSettings, features []feature.Feature) (map[int]*convInst[TPeriod], error) { + outInsts := make(map[int]*convInst[TPeriod]) if err := buildNoteSampleKeyboard(outInsts, inst.NoteSampleKeyboard[:]); err != nil { return nil, err @@ -57,20 +61,22 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData End: int(inst.SustainLoopEnd), } - id := instrument.PCM{ - Panning: panning.CenterAhead, + id := instrument.PCM[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ FadeOut: fadeout.Settings{ Mode: fadeout.ModeAlwaysActive, Amount: volume.Volume(inst.Fadeout) / 512, }, - VolEnv: envelope.Envelope[volume.Volume]{ + VolEnv: envelope.Envelope[itVolume.Volume]{ Enabled: (inst.Flags & itfile.IMPIOldFlagUseVolumeEnvelope) != 0, - Values: make([]envelope.EnvPoint[volume.Volume], 0), + Values: make([]envelope.Point[itVolume.Volume], 0), }, } - ii := instrument.Instrument{ + ii := instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ Inst: &id, + Static: instrument.StaticValues[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + PC: pc, + }, } switch inst.NewNoteAction { @@ -100,12 +106,13 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData } for i := range inst.VolumeEnvelope { - var out envelope.EnvPoint[volume.Volume] + var out envelope.Point[itVolume.Volume] in1 := inst.VolumeEnvelope[i] - vol := volume.Volume(uint8(in1)) / 64 - if vol > 1 { - vol = 1 + vol := itVolume.Volume(uint8(in1)) + if vol > itVolume.Volume(itVolume.MaxItVolume) { + vol = itVolume.Volume(itVolume.MaxItVolume) } + out.Pos = i out.Y = vol ending := false if i+1 >= len(inst.VolumeEnvelope) { @@ -117,9 +124,10 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData } } if !ending { - out.Ticks = 1 + out.Length = 1 } else { - out.Ticks = math.MaxInt64 + id.VolEnv.Length = i + out.Length = math.MaxInt64 } id.VolEnv.Values = append(id.VolEnv.Values, out) } @@ -132,42 +140,51 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData return outInsts, nil } -func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itfile.FullSample, convSettings convertITInstrumentSettings, pluginFilters map[int]filter.Factory, features []feature.Feature) (map[int]*convInst, error) { - outInsts := make(map[int]*convInst) +func convertITInstrumentToInstrument[TPeriod period.Period](inst *itfile.IMPIInstrument, pc period.PeriodConverter[TPeriod], sampData []itfile.FullSample, convSettings convertITInstrumentSettings, pluginFilters map[int]filter.Info, features []feature.Feature) (map[int]*convInst[TPeriod], error) { + outInsts := make(map[int]*convInst[TPeriod]) if err := buildNoteSampleKeyboard(outInsts, inst.NoteSampleKeyboard[:]); err != nil { return nil, err } var ( - channelFilterFactory filter.Factory - pluginFilterFactory filter.Factory + voiceFilter filter.Info + pluginFilter filter.Info ) if inst.InitialFilterResonance != 0 { - channelFilterFactory = func(instrument, playback period.Frequency) filter.Filter { - return itfilter.NewResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, playback, convSettings.extendedFilterRange, convSettings.useHighPassFilter) + voiceFilter.Name = "itresonant" + voiceFilter.Params = filter.ITResonantFilterParams{ + Cutoff: inst.InitialFilterCutoff, + Resonance: inst.InitialFilterResonance, + ExtendedFilterRange: convSettings.extendedFilterRange, + Highpass: convSettings.useHighPassFilter, } } if inst.MidiChannel >= 0x81 { - if pf, ok := pluginFilters[int(inst.MidiChannel)-0x81]; ok && pf != nil { - pluginFilterFactory = pf + if pf, ok := pluginFilters[int(inst.MidiChannel)-0x81]; ok { + pluginFilter = pf } } for i, ci := range outInsts { - id := instrument.PCM{ - Panning: panning.CenterAhead, + id := instrument.PCM[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ FadeOut: fadeout.Settings{ Mode: fadeout.ModeAlwaysActive, Amount: volume.Volume(inst.Fadeout) / 1024, }, + PitchPan: pitchpan.PitchPan{ + Enabled: inst.PitchPanSeparation != 0, + Center: note.Semitone(inst.PitchPanCenter), + Separation: float32(inst.PitchPanSeparation) / 8, + }, } - ii := instrument.Instrument{ - Static: instrument.StaticValues{ - FilterFactory: channelFilterFactory, - PluginFilter: pluginFilterFactory, + ii := instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + Static: instrument.StaticValues[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + PC: pc, + VoiceFilter: voiceFilter, + PluginFilter: pluginFilter, }, Inst: &id, } @@ -186,6 +203,9 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf } mixVol := volume.Volume(inst.GlobalVolume.Value()) + if !inst.DefaultPan.IsDisabled() { + ii.Static.Panning = optional.NewValue(util.Lerp(float64(inst.DefaultPan.Value()), 0, itPanning.MaxPanning)) + } ci.Inst = &ii if err := addSampleInfoToConvertedInstrument(ci.Inst, &id, &sampData[i], mixVol, convSettings, features); err != nil { @@ -195,9 +215,7 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf if err := convertEnvelope(&id.VolEnv, &inst.VolumeEnvelope, convertVolEnvValue); err != nil { return nil, err } - id.VolEnv.OnFinished = func(v voice.Voice) { - v.Fadeout() - } + id.VolEnvFinishFadesOut = true if err := convertEnvelope(&id.PanEnv, &inst.PanningEnvelope, convertPanEnvValue); err != nil { return nil, err @@ -212,28 +230,31 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf return outInsts, nil } -func convertVolEnvValue(v int8) volume.Volume { - vol := volume.Volume(uint8(v)) / 64 - if vol > 1 { +func convertVolEnvValue(v int8) itVolume.Volume { + vol := itVolume.Volume(uint8(v)) + if vol > itVolume.Volume(itVolume.MaxItVolume) { // NOTE: there might be an incoming Y value == 0xFF, which really // means "end of envelope" and should not mean "full volume", // but we can cheat a little here and probably get away with it... - vol = 1 + vol = itVolume.Volume(itVolume.MaxItVolume) } return vol } -func convertPanEnvValue(v int8) panning.Position { - return panning.MakeStereoPosition(float32(v), -64, 64) +func convertPanEnvValue(v int8) itPanning.Panning { + return itPanning.Panning(int(v) + 128) } -func convertPitchEnvValue(v int8) int8 { - return v +func convertPitchEnvValue(v int8) types.PitchFiltValue { + return types.PitchFiltValue(v) } func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope, convert func(int8) T) error { outEnv.Enabled = (inEnv.Flags & itfile.EnvelopeFlagEnvelopeOn) != 0 if !outEnv.Enabled { + var disabled loop.Disabled + outEnv.Loop = &disabled + outEnv.Sustain = &disabled return nil } @@ -253,20 +274,26 @@ func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope if enabled := (inEnv.Flags & itfile.EnvelopeFlagSustainLoopOn) != 0; enabled { envSustainMode = loop.ModeNormal } - outEnv.Values = make([]envelope.EnvPoint[T], int(inEnv.Count)) + outEnv.Values = make([]envelope.Point[T], int(inEnv.Count)) for i := range outEnv.Values { in1 := inEnv.NodePoints[i] y := convert(in1.Y) - var ticks int if i+1 < len(outEnv.Values) { in2 := inEnv.NodePoints[i+1] - ticks = int(in2.Tick) - int(in1.Tick) + ticks := int(in2.Tick) - int(in1.Tick) + var out envelope.Point[T] + out.Length = ticks + out.Pos = int(in1.Tick) + out.Y = y + outEnv.Values[i] = out } else { - ticks = math.MaxInt64 + outEnv.Values[i] = envelope.Point[T]{ + Pos: int(in1.Tick), + Length: math.MaxInt, + Y: y, + } + outEnv.Length = int(in1.Tick) } - var out envelope.EnvPoint[T] - out.Init(ticks, y) - outEnv.Values[i] = out } outEnv.Loop = loop.NewLoop(envLoopMode, envLoopSettings) @@ -275,7 +302,7 @@ func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope return nil } -func buildNoteSampleKeyboard(noteKeyboard map[int]*convInst, nsk []itfile.NoteSample) error { +func buildNoteSampleKeyboard[TPeriod period.Period](noteKeyboard map[int]*convInst[TPeriod], nsk []itfile.NoteSample) error { for o, ns := range nsk { s := int(ns.Sample) if s == 0 { @@ -290,7 +317,7 @@ func buildNoteSampleKeyboard(noteKeyboard map[int]*convInst, nsk []itfile.NoteSa st := note.Semitone(nn) ci, ok := noteKeyboard[si] if !ok { - ci = &convInst{} + ci = &convInst[TPeriod]{} noteKeyboard[si] = ci } ci.NR = append(ci.NR, noteRemap{ @@ -337,12 +364,11 @@ func itAutoVibratoWSToProtrackerWS(vibtype uint8) uint8 { } } -func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrument.PCM, si *itfile.FullSample, instVol volume.Volume, convSettings convertITInstrumentSettings, features []feature.Feature) error { +func addSampleInfoToConvertedInstrument[TPeriod period.Period](ii *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], id *instrument.PCM[itVolume.FineVolume, itVolume.Volume, itPanning.Panning], si *itfile.FullSample, instVol volume.Volume, convSettings convertITInstrumentSettings, features []feature.Feature) error { instLen := int(si.Header.Length) numChannels := 1 - id.MixingVolume = volume.Volume(si.Header.GlobalVolume.Value()) - id.MixingVolume *= instVol + id.MixingVolume.Set(max(itVolume.FineVolume(float32(itVolume.MaxItFineVolume)*si.Header.GlobalVolume.Value()*float32(instVol)), itVolume.MaxItFineVolume)) loopMode := loop.ModeDisabled loopSettings := loop.Settings{ Begin: int(si.Header.LoopBegin), @@ -442,21 +468,23 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrumen ii.Static.Filename = si.Header.GetFilename() ii.Static.Name = si.Header.GetName() - ii.C2Spd = period.Frequency(si.Header.C5Speed) - ii.Static.AutoVibrato = voice.AutoVibrato{ + ii.SampleRate = frequency.Frequency(si.Header.C5Speed) + ii.Static.AutoVibrato = autovibrato.AutoVibratoConfig[TPeriod]{ Enabled: (si.Header.VibratoDepth != 0 && si.Header.VibratoSpeed != 0 && si.Header.VibratoSweep != 0), Sweep: 255, WaveformSelection: itAutoVibratoWSToProtrackerWS(si.Header.VibratoType), Depth: float32(si.Header.VibratoDepth), Rate: int(si.Header.VibratoSpeed), - Factory: func() oscillator.Oscillator { - return oscillatorImpl.NewImpulseTrackerOscillator(1) - }, + FactoryName: "autovibrato", + } + ii.Static.Volume = itVolume.Volume(si.Header.Volume) + + if ii.SampleRate == 0 { + ii.SampleRate = 8363.0 } - ii.Static.Volume = volume.Volume(si.Header.Volume.Value()) - if ii.C2Spd == 0 { - ii.C2Spd = 8363.0 + if si.Header.Flags.IsStereo() { + ii.SampleRate /= 2.0 } if !convSettings.linearFrequencySlides { @@ -467,7 +495,7 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrumen ii.Static.AutoVibrato.Sweep = int(si.Header.VibratoDepth) * 256 / int(si.Header.VibratoSweep) } if !si.Header.DefaultPan.IsDisabled() { - id.Panning = panning.MakeStereoPosition(si.Header.DefaultPan.Value(), 0, 1) + id.Panning.Set(itPanning.Panning(si.Header.DefaultPan)) } return nil diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index 8f2f995..fc042cf 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -10,17 +10,21 @@ import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" itblock "github.com/gotracker/goaudiofile/music/tracked/it/block" - "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/format/it/layout" itPanning "github.com/gotracker/playback/format/it/panning" + "github.com/gotracker/playback/format/it/settings" + itSystem "github.com/gotracker/playback/format/it/system" + itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" ) func moduleHeaderToHeader(fh *itfile.ModuleHeader) (*layout.Header, error) { @@ -28,32 +32,32 @@ func moduleHeaderToHeader(fh *itfile.ModuleHeader) (*layout.Header, error) { return nil, errors.New("file header is nil") } head := layout.Header{ - Name: fh.GetName(), - InitialSpeed: int(fh.InitialSpeed), - InitialTempo: int(fh.InitialTempo), - GlobalVolume: volume.Volume(fh.GlobalVolume.Value()), + Name: fh.GetName(), + InitialSpeed: int(fh.InitialSpeed), + InitialTempo: int(fh.InitialTempo), + GlobalVolume: itVolume.FineVolume(fh.GlobalVolume), + MixingVolume: itVolume.FineVolume(fh.MixingVolume), + LinearFreqSlides: fh.Flags.IsLinearSlides(), + InitialOrder: 0, } switch { case fh.TrackerCompatVersion < 0x200: - head.MixingVolume = volume.Volume(fh.MixingVolume.Value()) + head.MixingVolume = max(itVolume.FineVolume(fh.MixingVolume*2), itVolume.MaxItFineVolume) case fh.TrackerCompatVersion >= 0x200: - head.MixingVolume = volume.Volume(fh.MixingVolume) / 128 + head.MixingVolume = itVolume.FineVolume(fh.MixingVolume) } return &head, nil } -func convertItPattern(pkt itfile.PackedPattern, channels int) (*pattern.Pattern[channel.Data], int, error) { - pat := &pattern.Pattern[channel.Data]{ - Orig: pkt, - } +func convertItPattern[TPeriod period.Period](pkt itfile.PackedPattern, channels int) (song.Pattern, int, error) { + pat := make(song.Pattern, pkt.Rows) channelMem := make([]itfile.ChannelData, channels) maxCh := uint8(0) pos := 0 for rowNum := 0; rowNum < int(pkt.Rows); rowNum++ { - pat.Rows = append(pat.Rows, pattern.RowData[channel.Data]{}) - row := &pat.Rows[rowNum] - row.Channels = make([]channel.Data, channels) + row := make(layout.Row[TPeriod], channels) + pat[rowNum] = row channelLoop: for { sz, chn, err := pkt.ReadChannelData(pos, channelMem) @@ -67,7 +71,7 @@ func convertItPattern(pkt itfile.PackedPattern, channels int) (*pattern.Pattern[ channelNum := int(chn.ChannelNumber) - cd := channel.Data{ + cd := channel.Data[TPeriod]{ What: chn.Flags, Note: chn.Note, Instrument: chn.Instrument, @@ -76,7 +80,7 @@ func convertItPattern(pkt itfile.PackedPattern, channels int) (*pattern.Pattern[ EffectParameter: channel.DataEffect(chn.CommandData), } - row.Channels[channelNum] = cd + row[channelNum] = cd if maxCh < uint8(channelNum) { maxCh = uint8(channelNum) } @@ -86,7 +90,15 @@ func convertItPattern(pkt itfile.PackedPattern, channels int) (*pattern.Pattern[ return pat, int(maxCh), nil } -func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.Song, error) { +func convertItFileToSong(f *itfile.File, features []feature.Feature) (song.Data, error) { + if f.Head.Flags.IsLinearSlides() { + return convertItFileToTypedSong[period.Linear](f, features) + } else { + return convertItFileToTypedSong[period.Amiga](f, features) + } +} + +func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features []feature.Feature) (*layout.Song[TPeriod], error) { h, err := moduleHeaderToHeader(&f.Head) if err != nil { return nil, err @@ -95,14 +107,27 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So linearFrequencySlides := f.Head.Flags.IsLinearSlides() oldEffectMode := f.Head.Flags.IsOldEffects() efgLinkMode := f.Head.Flags.IsEFGLinking() - - song := layout.Song{ - Head: *h, - Instruments: make(map[uint8]*instrument.Instrument), - InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument), - Patterns: make([]pattern.Pattern[channel.Data], len(f.Patterns)), - OrderList: make([]index.Pattern, int(f.Head.OrderCount)), - FilterPlugins: make(map[int]filter.Factory), + stereoMode := f.Head.Flags.IsStereo() + vol0Enabled := f.Head.Flags.IsVol0Optimizations() + + ms := settings.GetMachineSettings[TPeriod]() + + songData := &layout.Song[TPeriod]{ + BaseSong: common.BaseSong[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + System: itSystem.ITSystem, + MS: ms, + Name: h.Name, + InitialBPM: h.InitialTempo, + InitialTempo: h.InitialSpeed, + GlobalVolume: h.GlobalVolume, + MixingVolume: h.MixingVolume, + InitialOrder: h.InitialOrder, + Instruments: make([]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], 0, f.Head.InstrumentCount), + Patterns: make([]song.Pattern, len(f.Patterns)), + OrderList: make([]index.Pattern, int(f.Head.OrderCount)), + }, + InstrumentNoteMap: make(map[uint8]layout.SemitoneSamples), + FilterPlugins: make(map[int]filter.Info), } for _, block := range f.Blocks { @@ -110,14 +135,14 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So case *itblock.FX: if filter, err := decodeFilter(t); err == nil { if i, err := strconv.Atoi(string(t.Identifier[2:])); err == nil { - song.FilterPlugins[i] = filter + songData.FilterPlugins[i] = filter } } } } for i := 0; i < int(f.Head.OrderCount); i++ { - song.OrderList[i] = index.Pattern(f.OrderList[i]) + songData.OrderList[i] = index.Pattern(f.OrderList[i]) } if f.Head.Flags.IsUseInstruments() { @@ -129,46 +154,44 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So } switch ii := inst.(type) { case *itfile.IMPIInstrumentOld: - instMap, err := convertITInstrumentOldToInstrument(ii, f.Samples, convSettings, features) + instMap, err := convertITInstrumentOldToInstrument(ii, ms.PeriodConverter, f.Samples, convSettings, features) if err != nil { return nil, err } for _, ci := range instMap { - addSampleWithNoteMapToSong(&song, ci.Inst, ci.NR, instNum) + addSampleWithNoteMapToSong(songData, instNum, ci.Inst, ci.NR) } case *itfile.IMPIInstrument: - instMap, err := convertITInstrumentToInstrument(ii, f.Samples, convSettings, song.FilterPlugins, features) + instMap, err := convertITInstrumentToInstrument(ii, ms.PeriodConverter, f.Samples, convSettings, songData.FilterPlugins, features) if err != nil { return nil, err } for _, ci := range instMap { - addSampleWithNoteMapToSong(&song, ci.Inst, ci.NR, instNum) + addSampleWithNoteMapToSong(songData, instNum, ci.Inst, ci.NR) } } } } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern[channel.Data], len(f.Patterns)) for patNum, pkt := range f.Patterns { - pattern, maxCh, err := convertItPattern(pkt, len(f.Head.ChannelVol)) + p, maxCh, err := convertItPattern[TPeriod](pkt, len(f.Head.ChannelVol)) if err != nil { return nil, err } - if pattern == nil { + if p == nil { continue } if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - song.Patterns[patNum] = *pattern + songData.Patterns[patNum] = p } sharedMem := channel.SharedMemory{ - LinearFreqSlides: linearFrequencySlides, OldEffectMode: oldEffectMode, EFGLinkMode: efgLinkMode, ResetMemoryAtStartOfOrder0: true, @@ -179,25 +202,25 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So cs := layout.ChannelSetting{ OutputChannelNum: chNum, Enabled: true, - InitialVolume: volume.Volume(1), - ChannelVolume: volume.Volume(f.Head.ChannelVol[chNum].Value()), - InitialPanning: itPanning.FromItPanning(f.Head.ChannelPan[chNum]), + Muted: false, + InitialVolume: itVolume.Volume(itVolume.DefaultItVolume), + ChannelVolume: min(itVolume.FineVolume(f.Head.ChannelVol[chNum]*2), itVolume.MaxItFineVolume), + PanEnabled: stereoMode, + InitialPanning: itPanning.Panning(f.Head.ChannelPan[chNum]), Memory: channel.Memory{ Shared: &sharedMem, }, + Vol0OptEnabled: vol0Enabled, } - cs.Memory.ResetOscillators() - channels[chNum] = cs } - song.ChannelSettings = channels - - return &song, nil + songData.ChannelSettings = channels + return songData, nil } -func decodeFilter(f *itblock.FX) (filter.Factory, error) { +func decodeFilter(f *itblock.FX) (filter.Info, error) { lib := f.LibraryName.String() name := f.UserPluginName.String() switch { @@ -205,11 +228,16 @@ func decodeFilter(f *itblock.FX) (filter.Factory, error) { r := bytes.NewReader(f.Data) e := filter.EchoFilterFactory{} if err := binary.Read(r, binary.LittleEndian, &e); err != nil { - return nil, err + return filter.Info{}, err } - return e.Factory(), nil + echo := filter.Info{ + Name: "echo", + Params: e.EchoFilterSettings, + } + return echo, nil + default: - return nil, fmt.Errorf("unhandled fx lib[%s] name[%s]", lib, name) + return filter.Info{}, fmt.Errorf("unhandled fx lib[%s] name[%s]", lib, name) } } @@ -218,34 +246,35 @@ type noteRemap struct { Remap note.Semitone } -func addSampleWithNoteMapToSong(song *layout.Song, sample *instrument.Instrument, sts []noteRemap, instNum int) { +func addSampleWithNoteMapToSong[TPeriod period.Period](song *layout.Song[TPeriod], instNum int, sample *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], sts []noteRemap) { if sample == nil { return } + + idx := len(song.Instruments) + song.Instruments = append(song.Instruments, sample) + id := channel.SampleID{ InstID: uint8(instNum + 1), + SampID: uint8(idx), } sample.Static.ID = id - song.Instruments[id.InstID] = sample - id, ok := sample.Static.ID.(channel.SampleID) - if !ok { - return - } - inm, ok := song.InstrumentNoteMap[id.InstID] - if !ok { - inm = make(map[note.Semitone]layout.NoteInstrument) - song.InstrumentNoteMap[id.InstID] = inm - } + inm, _ := song.InstrumentNoteMap[id.InstID] + hasRemap := false for _, st := range sts { - inm[st.Orig] = layout.NoteInstrument{ - NoteRemap: st.Remap, - Inst: sample, + if uint8(instNum) != id.SampID || st.Orig != st.Remap { + hasRemap = true + inm[st.Orig] = layout.NewSemitoneSample(int(id.SampID), st.Remap) } } + + if hasRemap { + song.InstrumentNoteMap[id.InstID] = inm + } } -func readIT(r io.Reader, features []feature.Feature) (*layout.Song, error) { +func readIT(r io.Reader, features []feature.Feature) (song.Data, error) { f, err := itfile.Read(r) if err != nil { return nil, err diff --git a/format/it/load/load.go b/format/it/load/load.go index 735c73e..a1daace 100644 --- a/format/it/load/load.go +++ b/format/it/load/load.go @@ -3,13 +3,12 @@ package load import ( "io" - "github.com/gotracker/playback" "github.com/gotracker/playback/format/common" - itPlayback "github.com/gotracker/playback/format/it/playback" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" ) // IT loads an IT file from a reader -func IT(r io.Reader, features []feature.Feature) (playback.Playback, error) { - return common.Load(r, readIT, itPlayback.NewManager, features) +func IT(r io.Reader, features []feature.Feature) (song.Data, error) { + return common.Load(r, readIT, features) } diff --git a/format/it/note/note.go b/format/it/note/note.go index 3a99598..a42f370 100644 --- a/format/it/note/note.go +++ b/format/it/note/note.go @@ -14,7 +14,7 @@ func FromItNote(in itfile.Note) note.Note { case in.IsNoteCut(): return note.StopNote{} case in.IsNoteFade(): // not really invalid, but... - return note.InvalidNote{} + return note.FadeoutNote{} } an := uint8(in) diff --git a/format/it/oscillator/factory.go b/format/it/oscillator/factory.go new file mode 100644 index 0000000..6c61a77 --- /dev/null +++ b/format/it/oscillator/factory.go @@ -0,0 +1,37 @@ +package oscillator + +import ( + "fmt" + + oscillatorImpl "github.com/gotracker/playback/oscillator" + "github.com/gotracker/playback/voice/oscillator" +) + +func VibratoFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(4), nil +} + +func TremoloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(4), nil +} + +func PanbrelloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(1), nil +} + +func OscillatorFactory(name string) (oscillator.Oscillator, error) { + switch name { + case "": + return nil, nil + case "vibrato": + return VibratoFactory() + case "autovibrato": + return oscillatorImpl.NewImpulseTrackerOscillator(1), nil + case "tremolo": + return TremoloFactory() + case "panbrello": + return PanbrelloFactory() + default: + return nil, fmt.Errorf("unsupported oscillator: %q", name) + } +} diff --git a/format/it/panning/panning.go b/format/it/panning/panning.go index 50ed407..e3f6a20 100644 --- a/format/it/panning/panning.go +++ b/format/it/panning/panning.go @@ -1,19 +1,60 @@ package panning import ( + "math" + itfile "github.com/gotracker/goaudiofile/music/tracked/it" "github.com/gotracker/gomixing/panning" + "github.com/gotracker/playback/voice/types" +) + +var ( + DefaultPanningLeft = Panning(0x30) + // DefaultPanningLeftPosition is the default panning value for left channels + DefaultPanningLeftPosition = FromItPanning(itfile.PanValue(DefaultPanningLeft)) + + DefaultPanning = Panning(0x80) + // DefaultPanningPosition is the default panning value for unconfigured channels + DefaultPanningPosition = FromItPanning(itfile.PanValue(DefaultPanning)) + + DefaultPanningRight = Panning(0xC0) + // DefaultPanningRightPosition is the default panning value for right channels + DefaultPanningRightPosition = FromItPanning(itfile.PanValue(DefaultPanningRight)) + + MaxPanning = Panning(0xFF) ) +type Panning uint8 + var ( - // DefaultPanningLeft is the default panning value for left channels - DefaultPanningLeft = FromItPanning(0x30) - // DefaultPanning is the default panning value for unconfigured channels - DefaultPanning = FromItPanning(0x80) - // DefaultPanningRight is the default panning value for right channels - DefaultPanningRight = FromItPanning(0xC0) + _ types.PanningInformationer[Panning] = Panning(0) + _ types.PanningDeltaer[Panning] = Panning(0) ) +func (p Panning) IsInvalid() bool { + return false +} + +func (p Panning) ToPosition() panning.Position { + return panning.MakeStereoPosition(float32(p), 0, 0xFF) +} + +func (Panning) GetDefault() Panning { + return DefaultPanning +} + +func (Panning) GetMax() Panning { + return MaxPanning +} + +func (p Panning) FMA(multiplier, add float32) Panning { + return Panning(min(max(math.FMA(float64(p), float64(multiplier), float64(add)), 0), 0xFF)) +} + +func (p Panning) AddDelta(d types.PanDelta) Panning { + return Panning(min(max(int16(p)+int16(d), 0), int16(MaxPanning))) +} + // FromItPanning returns a radian panning position from an it panning value func FromItPanning(pos itfile.PanValue) panning.Position { if pos.IsDisabled() { diff --git a/format/it/pattern/pattern.go b/format/it/pattern/pattern.go deleted file mode 100644 index 2269251..0000000 --- a/format/it/pattern/pattern.go +++ /dev/null @@ -1,339 +0,0 @@ -package pattern - -import ( - "errors" - - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" - formatutil "github.com/gotracker/playback/util" - "github.com/heucuva/optional" -) - -// State is the current pattern state -type State struct { - currentOrder index.Order - currentRow index.Row - ticks int - tempo int - patternDelay optional.Value[int] - finePatternDelay int - resetPatternLoops bool - - SongLoop feature.SongLoop - PlayUntilOrderAndRow feature.PlayUntilOrderAndRow - loopDetect formatutil.LoopDetect // when SongLoopEnabled is false, this is used to detect song loops - loopCount int - - Patterns []pattern.Pattern[channel.Data] - Orders []index.Pattern -} - -// GetTempo returns the tempo of the current state -func (state *State) GetTempo() int { - return state.tempo -} - -// GetSpeed returns the row speed of the current state -func (state *State) GetSpeed() int { - return state.ticks -} - -// GetTicksThisRow returns the number of ticks in the current row -func (state *State) GetTicksThisRow() int { - rowLoops := 1 - if patternDelay, ok := state.patternDelay.Get(); ok { - rowLoops = patternDelay - } - extraTicks := state.finePatternDelay - - ticksThisRow := state.ticks*rowLoops + extraTicks - return ticksThisRow -} - -// GetPatNum returns the current pattern number -func (state *State) GetPatNum() index.Pattern { - if int(state.currentOrder) >= len(state.Orders) { - return index.InvalidPattern - } - return state.Orders[state.currentOrder] -} - -// GetNumRows returns the number of rows in the current pattern -func (state *State) GetNumRows() (int, error) { - rows, err := state.GetRows() - if err != nil { - return 0, err - } - if rows != nil { - return rows.NumRows(), nil - } - return 0, nil -} - -// WantsStop returns true when the current pattern wants to end the song -func (state *State) WantsStop() bool { - return state.GetPatNum() == index.InvalidPattern -} - -// setCurrentOrder sets the current order index -func (state *State) setCurrentOrder(order index.Order) { - state.currentOrder = order -} - -func (state *State) advanceOrder() { - state.setCurrentOrder(state.currentOrder + 1) -} - -// GetCurrentOrder returns the current order -func (state *State) GetCurrentOrder() index.Order { - return state.currentOrder -} - -// GetNumOrders returns the number of orders in the song -func (state *State) GetNumOrders() int { - return len(state.Orders) -} - -// GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { - ordLen := len(state.Orders) - - if ordLen == 0 { - // nothing to play, don't even try - return 0, song.ErrStopSong - } - - for loopCount := 0; loopCount < ordLen; loopCount++ { - ordIdx := int(state.GetCurrentOrder()) - if ordIdx >= ordLen { - if !(state.SongLoop.Count < 0 || state.loopCount < state.SongLoop.Count) { - return 0, song.ErrStopSong - } - state.setCurrentOrder(0) - continue - } - - patIdx := state.Orders[ordIdx] - if patIdx == index.NextPattern { - if err := state.nextOrder(true); err != nil { - return 0, err - } - continue - } - - if patIdx == index.InvalidPattern { - if err := state.nextOrder(true); err != nil { - return 0, err - } - continue // this is supposed to be a song break - } - - return patIdx, nil - } - return 0, errors.New("infinite loop detected in order list") -} - -// GetCurrentRow returns the current row -func (state *State) GetCurrentRow() index.Row { - return state.currentRow -} - -// setCurrentRow sets the current row -func (state *State) setCurrentRow(row index.Row) error { - state.currentRow = row - rows, err := state.GetNumRows() - if err != nil { - return err - } - if int(state.GetCurrentRow()) >= rows { - if err := state.nextOrder(true); err != nil { - return err - } - } - return nil -} - -// Observe will attempt to detect a song loop -func (state *State) Observe() error { - if state.SongLoop.Count >= 0 { - if state.loopDetect.Observe(state.currentOrder, state.currentRow) { - if state.SongLoop.Count == 0 || (state.SongLoop.Count > 0 && state.loopCount >= state.SongLoop.Count) { - return song.ErrStopSong - } - state.loopCount += 1 - state.loopDetect.Reset() - } - } - if state.currentOrder == index.Order(state.PlayUntilOrderAndRow.Order) && state.currentRow == index.Row(state.PlayUntilOrderAndRow.Row) { - if state.SongLoop.Count >= 0 && state.loopCount >= state.SongLoop.Count { - return song.ErrStopSong - } - } - return nil -} - -// nextOrder travels to the next pattern in the order list -func (state *State) nextOrder(resetRow ...bool) error { - state.advanceOrder() - state.patternDelay.Reset() - state.finePatternDelay = 0 - // called only to clean up order position info - if _, err := state.GetCurrentPatternIdx(); err != nil { - return err - } - if len(resetRow) > 0 && resetRow[0] { - state.currentRow = 0 - } - return nil -} - -// Reset resets a pattern state back to zeroes -func (state *State) Reset() { - *state = State{ - SongLoop: feature.SongLoop{ - Count: 0, - }, - PlayUntilOrderAndRow: feature.PlayUntilOrderAndRow{ - Order: -1, - Row: -1, - }, - } -} - -// nextRow travels to the next row in the pattern -// or the next order in the order list if the last row has been exhausted -func (state *State) nextRow() error { - state.patternDelay.Reset() - state.finePatternDelay = 0 - - var patNum = state.GetPatNum() - if patNum == index.InvalidPattern { - return nil - } - - if patNum == index.NextPattern { - if err := state.nextOrder(true); err != nil { - return err - } - return nil - } - - rows, err := state.GetNumRows() - if err != nil { - return err - } - if state.currentRow.Increment(rows) { - if err := state.nextOrder(true); err != nil { - return err - } - } - return nil -} - -// GetRows returns all the rows in the pattern -func (state *State) GetRows() (song.Rows[channel.Data], error) { -nextRow: - for loops := 0; loops < len(state.Patterns); loops++ { - var patNum = state.GetPatNum() - switch patNum { - case index.InvalidPattern: - return nil, nil - case index.NextPattern: - if err := state.nextRow(); err != nil { - return nil, err - } - continue nextRow - default: - if int(patNum) >= len(state.Patterns) { - return nil, nil - } - pattern := state.Patterns[patNum] - return pattern.GetRows(), nil - } - } - return nil, nil -} - -// NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State) NeedResetPatternLoops() bool { - rpl := state.resetPatternLoops - state.resetPatternLoops = false - return rpl -} - -// commitTransaction will update the order and row indexes at once, idempotently, from a row update transaction. -func (state *State) commitTransaction(txn *pattern.RowUpdateTransaction) error { - tempo, tempoSet := txn.Tempo.Get() - tempoDelta, tempoDeltaSet := txn.TempoDelta.Get() - if tempoSet || tempoDeltaSet { - newTempo := state.tempo - if tempoSet { - newTempo = tempo - } - if tempoDeltaSet { - newTempo += tempoDelta - } - state.tempo = newTempo - } - - if ticks, ok := txn.Ticks.Get(); ok { - state.ticks = ticks - } - - if finePatternDelay, ok := txn.FinePatternDelay.Get(); ok { - state.finePatternDelay = finePatternDelay - } - - if !state.patternDelay.IsSet() { - if patternDelay, ok := txn.GetPatternDelay(); ok { - state.patternDelay.Set(patternDelay) - } - } - - if txn.BreakOrder { - if err := state.nextOrder(true); err != nil { - return err - } - } - - orderIdx, orderIdxSet := txn.GetOrderIdx() - rowIdx, rowIdxSet := txn.GetRowIdx() - - if orderIdxSet || rowIdxSet { - if orderIdxSet { - state.setCurrentOrder(orderIdx) - if !rowIdxSet { - if err := state.setCurrentRow(0); err != nil { - return err - } - } - } - if rowIdxSet { - if !orderIdxSet && !txn.RowIdxAllowBacktrack && state.currentRow > rowIdx { - if err := state.nextOrder(); err != nil { - return err - } - } - if err := state.setCurrentRow(rowIdx); err != nil { - return err - } - } - } else if txn.AdvanceRow { - if err := state.nextRow(); err != nil { - return err - } - } - return nil -} - -// StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn -} diff --git a/format/it/period/amiga.go b/format/it/period/amiga.go deleted file mode 100644 index 9047d32..0000000 --- a/format/it/period/amiga.go +++ /dev/null @@ -1,84 +0,0 @@ -package period - -import ( - "fmt" - "math" - - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - - "github.com/gotracker/playback/period" -) - -// Amiga defines a sampler period that follows the Amiga-style approach of note -// definition. Useful in calculating resampling. -type Amiga period.AmigaPeriod - -// AddInteger truncates the current period to an integer and adds the delta integer in -// then returns the resulting period -func (p Amiga) AddInteger(delta int) Amiga { - period := Amiga(int(p) + delta) - return period -} - -// Add adds the current period to a delta value then returns the resulting period -func (p Amiga) AddDelta(delta period.Delta) period.Period { - d := period.ToPeriodDelta(delta) - p += Amiga(d) - return p -} - -// Compare returns: -// -1 if the current period is higher frequency than the `rhs` period -// 0 if the current period is equal in frequency to the `rhs` period -// 1 if the current period is lower frequency than the `rhs` period -func (p Amiga) Compare(rhs period.Period) comparison.Spaceship { - lf := p.GetFrequency() - rf := rhs.GetFrequency() - - switch { - case lf < rf: - return comparison.SpaceshipRightGreater - case lf > rf: - return comparison.SpaceshipLeftGreater - default: - return comparison.SpaceshipEqual - } -} - -// Lerp linear-interpolates the current period with the `rhs` period -func (p Amiga) Lerp(t float64, rhs period.Period) period.Period { - right := Amiga(0) - if r, ok := rhs.(Amiga); ok { - right = r - } - - period := Amiga(period.AmigaPeriod(p).Lerp(t, period.AmigaPeriod(right))) - return period -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { - return float64(period.AmigaPeriod(p).GetFrequency(period.Frequency(samplerSpeed))) -} - -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - return period.AmigaPeriod(p).GetFrequency(period.Frequency(ITBaseClock)) -} - -func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%f }", float32(p)) -} - -// ToAmigaPeriod calculates an amiga period for a linear finetune period -func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { - if finetunes < 0 { - finetunes = 0 - } - pow := math.Pow(2, float64(finetunes)/semitonesPerOctave) - linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - - period := Amiga(float64(semitonePeriodTable[0]) / linFreq) - return period -} diff --git a/format/it/period/amigaconverter.go b/format/it/period/amigaconverter.go new file mode 100644 index 0000000..d5572b9 --- /dev/null +++ b/format/it/period/amigaconverter.go @@ -0,0 +1,14 @@ +package period + +import ( + "math" + + "github.com/gotracker/playback/format/it/system" + "github.com/gotracker/playback/period" +) + +var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + System: system.ITSystem, + MinPeriod: 1, + MaxPeriod: math.MaxUint16, +} diff --git a/format/it/period/linear.go b/format/it/period/linear.go deleted file mode 100644 index a2a0380..0000000 --- a/format/it/period/linear.go +++ /dev/null @@ -1,94 +0,0 @@ -package period - -import ( - "fmt" - "math" - - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - - "github.com/gotracker/playback/period" -) - -// Linear is a linear period, based on semitone and finetune values -type Linear struct { - Finetune note.Finetune - C2Spd period.Frequency -} - -// Add adds the current period to a delta value then returns the resulting period -func (p Linear) AddDelta(delta period.Delta) period.Period { - // 0 means "not playing", so keep it that way - if p.Finetune > 0 { - d := period.ToPeriodDelta(delta) - p.Finetune += note.Finetune(d) - if p.Finetune < 1 { - p.Finetune = 1 - } - } - return p -} - -// Compare returns: -// -1 if the current period is higher frequency than the `rhs` period -// 0 if the current period is equal in frequency to the `rhs` period -// 1 if the current period is lower frequency than the `rhs` period -func (p Linear) Compare(rhs period.Period) comparison.Spaceship { - lf := p.GetFrequency() - rf := rhs.GetFrequency() - - switch { - case lf < rf: - return comparison.SpaceshipRightGreater - case lf > rf: - return comparison.SpaceshipLeftGreater - default: - return comparison.SpaceshipEqual - } -} - -// Lerp linear-interpolates the current period with the `rhs` period -func (p Linear) Lerp(t float64, rhs period.Period) period.Period { - right := ToLinearPeriod(rhs) - - lnft := float64(p.Finetune) - rnft := float64(right.Finetune) - - delta := period.PeriodDelta(t * (rnft - lnft)) - p.AddDelta(delta) - return p -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Linear) GetSamplerAdd(samplerSpeed float64) float64 { - return ToAmigaPeriod(p.Finetune, p.C2Spd).GetSamplerAdd(samplerSpeed) -} - -// GetFrequency returns the frequency defined by the period -func (p Linear) GetFrequency() period.Frequency { - am := ToAmigaPeriod(p.Finetune, p.C2Spd) - return am.GetFrequency() -} - -func (p Linear) String() string { - return fmt.Sprintf("Linear{ Finetune:%v C2Spd:%v }", p.Finetune, p.C2Spd) -} - -// ToLinearPeriod returns the linear frequency period for a given period -func ToLinearPeriod(p period.Period) Linear { - switch pp := p.(type) { - case Linear: - return pp - case Amiga: - linFreq := float64(semitonePeriodTable[0]) / float64(pp) - - fts := note.Finetune(semitonesPerOctave * math.Log2(linFreq)) - - lp := Linear{ - Finetune: fts, - C2Spd: DefaultC2Spd, - } - return lp - } - return Linear{} -} diff --git a/format/it/period/linearconverter.go b/format/it/period/linearconverter.go new file mode 100644 index 0000000..fd03f03 --- /dev/null +++ b/format/it/period/linearconverter.go @@ -0,0 +1,10 @@ +package period + +import ( + "github.com/gotracker/playback/format/it/system" + "github.com/gotracker/playback/period" +) + +var LinearConverter period.PeriodConverter[period.Linear] = period.LinearConverter{ + System: system.ITSystem, +} diff --git a/format/it/period/util.go b/format/it/period/util.go deleted file mode 100644 index 6e097ca..0000000 --- a/format/it/period/util.go +++ /dev/null @@ -1,76 +0,0 @@ -package period - -import ( - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" -) - -const ( - // DefaultC2Spd is the default C2SPD for IT samples - DefaultC2Spd = 8363 - // C5Period is the sampler (Amiga-style) period of the C-5 note - C5Period = 428 - - floatDefaultC2Spd = float32(DefaultC2Spd) - - // ITBaseClock is the base clock speed of IT files - ITBaseClock period.Frequency = DefaultC2Spd * C5Period - - notesPerOctave = 12 - semitonesPerNote = 64 - semitonesPerOctave = notesPerOctave * semitonesPerNote -) - -var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} - -// CalcSemitonePeriod calculates the semitone period for it notes -func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequency, linearFreqSlides bool) period.Period { - if semi == note.UnchangedSemitone { - panic("how?") - } - if linearFreqSlides { - nft := int(semi)*semitonesPerNote + int(ft) - return Linear{ - // NOTE: not sure why the magic downshift a whole octave, - // but it makes all the calculations work, so here we are. - Finetune: note.Finetune(nft), - C2Spd: c2spd, - } - } - - key := int(semi.Key()) - octave := uint32(semi.Octave()) - - if key >= len(semitonePeriodTable) { - return nil - } - - if c2spd == 0 { - c2spd = period.Frequency(DefaultC2Spd) - } - - if ft != 0 { - c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) - } - - p := (Amiga(floatDefaultC2Spd*semitonePeriodTable[key]) / Amiga(uint32(c2spd)<= m.GetNumChannels() { - continue - } - - cdata := &channels[channelNum] - - cs := &m.channels[channelNum] - if err := cs.SetData(cdata); err != nil { - return err - } - } - - for ch := range m.channels { - cs := &m.channels[ch] - - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitPreRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - } - } - - if err := preMixRowTxn.Commit(); err != nil { - return err - } - - tickDuration := tickBaseDuration / time.Duration(m.pattern.GetTempo()) - - m.rowRenderState.Duration = tickDuration - m.rowRenderState.Samples = int(tickDuration.Seconds() * float64(s.SampleRate)) - m.rowRenderState.ticksThisRow = m.pattern.GetTicksThisRow() - m.rowRenderState.currentTick = 0 - - // run row processing, now that prestart has completed - for channelNum := range row.GetChannels() { - if channelNum >= m.GetNumChannels() { - continue - } - - cs := &m.channels[channelNum] - - if err := m.processRowForChannel(cs); err != nil { - return err - } - } - - return nil -} - -func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory, channel.Data]) error { - mem := cs.GetMemory() - mem.TremorMem().Reset() - - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - - if err := txn.CommitPostRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - } - return nil -} diff --git a/format/it/playback/playback_render.go b/format/it/playback/playback_render.go deleted file mode 100644 index b2add6c..0000000 --- a/format/it/playback/playback_render.go +++ /dev/null @@ -1,118 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state" -) - -// OnTick runs the IT tick processing -func (m *Manager) OnTick() error { - m.premix = nil - - m.PastNotes.Update() - - premix, err := m.renderTick() - if err != nil { - return err - } - - m.premix = premix - return nil -} - -// GetPremixData gets the current premix data from the manager -func (m *Manager) GetPremixData() (*output.PremixData, error) { - return m.premix, nil -} - -// RenderOneRow renders the next single row from the song pattern data into a RowRender object -func (m *Manager) renderTick() (*output.PremixData, error) { - postMixRowTxn := m.pattern.StartTransaction() - defer func() { - postMixRowTxn.Cancel() - m.postMixRowTxn = nil - }() - m.postMixRowTxn = postMixRowTxn - - if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { - if err := m.processPatternRow(); err != nil { - return nil, err - } - } - - var finalData render.RowRender - premix := &output.PremixData{ - Userdata: &finalData, - SamplesLen: m.rowRenderState.Samples, - } - - if err := m.soundRenderTick(premix); err != nil { - return nil, err - } - - finalData.Order = int(m.pattern.GetCurrentOrder()) - finalData.Row = int(m.pattern.GetCurrentRow()) - finalData.Tick = m.rowRenderState.currentTick - if m.rowRenderState.currentTick == 0 { - finalData.RowText = m.getRowText() - } - - m.rowRenderState.currentTick++ - if m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { - postMixRowTxn.AdvanceRow = true - } - - if err := postMixRowTxn.Commit(); err != nil { - return nil, err - } - - return premix, nil -} - -type rowRenderState struct { - state.RenderDetails - - ticksThisRow int - currentTick int -} - -func (m *Manager) soundRenderTick(premix *output.PremixData) error { - tick := m.rowRenderState.currentTick - var lastTick = (tick+1 == m.rowRenderState.ticksThisRow) - - for ch := range m.channels { - cs := &m.channels[ch] - if m.song.IsChannelEnabled(ch) { - if err := m.processEffect(ch, cs, tick, lastTick); err != nil { - return err - } - - rr, err := cs.RenderRowTick(m.rowRenderState.RenderDetails, m.PastNotes.GetNotesForChannel(ch)) - if err != nil { - return err - } - if rr != nil { - premix.Data = append(premix.Data, rr) - } - } - } - - premix.MixerVolume = m.GetMixerVolume() - return nil -} - -/** unused in IT, so far -func (m *Manager) ensureOPL2() { - if opl2 := m.GetOPL2Chip(); opl2 == nil { - if s := m.GetSampler(); s != nil { - opl2 = render.NewOPL2Chip(uint32(s.SampleRate)) - opl2.WriteReg(0x01, 0x20) // enable all waveforms - opl2.WriteReg(0x04, 0x00) // clear timer flags - opl2.WriteReg(0x08, 0x40) // clear CSW and set NOTE-SEL - opl2.WriteReg(0xBD, 0x00) // set default notes - m.SetOPL2Chip(opl2) - } - } -} -*/ diff --git a/format/it/playback/playback_textoutput.go b/format/it/playback/playback_textoutput.go deleted file mode 100644 index 5a22f57..0000000 --- a/format/it/playback/playback_textoutput.go +++ /dev/null @@ -1,27 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/player/render" -) - -func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { - nCh := 0 - for ch := range m.channels { - if !m.song.IsChannelEnabled(ch) { - continue - } - nCh++ - } - rowText := render.NewRowText[channel.Data](nCh, m.longChannelOutput) - for ch, cs := range m.channels { - if !m.song.IsChannelEnabled(ch) { - continue - } - - if cd := cs.GetData(); cd != nil { - rowText.Channels[ch] = *cd - } - } - return &rowText -} diff --git a/format/it/playback/playback_tracing.go b/format/it/playback/playback_tracing.go deleted file mode 100644 index 4aef8bb..0000000 --- a/format/it/playback/playback_tracing.go +++ /dev/null @@ -1,132 +0,0 @@ -package playback - -import ( - "fmt" - "io" - "reflect" - "text/tabwriter" - - "github.com/gotracker/playback/player" -) - -func (m *Manager) OutputTraces(out chan<- func(w io.Writer)) { - outputs := []func(w io.Writer){ - m.outputGlobalTrace(), - m.outputRenderTrace(), - m.outputChannelsTrace(), - } - out <- func(w io.Writer) { - fmt.Fprintln(w, "################################################") - for _, fn := range outputs { - fn(w) - } - - fmt.Fprintln(w) - } -} - -func (m *Manager) outputGlobalTrace() func(w io.Writer) { - gs := player.NewTracingTable("=== global ===", - "globalVolume", - "mixerVolume", - "currentOrder", - "currentRow", - ) - gs.AddRow( - m.GetGlobalVolume(), - m.GetMixerVolume(), - m.GetCurrentOrder(), - m.GetCurrentRow(), - ) - - return func(w io.Writer) { - fmt.Fprintln(w) - - tw := tabwriter.NewWriter(w, 1, 1, 1, ' ', 0) - defer tw.Flush() - - gs.Fprintln(tw, "\t", false) - } -} - -func (m *Manager) outputRenderTrace() func(w io.Writer) { - r := m.rowRenderState - if r == nil { - return func(w io.Writer) {} - } - - rs := player.NewTracingTable("=== rowRenderState ===", - "samplerSpeed", - "tickDuration", - "samplesPerTick", - "ticksThisRow", - "currentTick", - ) - rs.AddRow( - fmt.Sprint(r.SamplerSpeed), - fmt.Sprint(r.Duration), - fmt.Sprint(r.Samples), - fmt.Sprint(r.ticksThisRow), - fmt.Sprint(r.currentTick), - ) - - return func(w io.Writer) { - fmt.Fprintln(w) - - tw := tabwriter.NewWriter(w, 1, 1, 1, ' ', 0) - defer tw.Flush() - - rs.Fprintln(tw, "\t", false) - } -} - -func (m *Manager) outputChannelsTrace() func(w io.Writer) { - cs := player.NewTracingTable("=== channels ===", - "Channel", - "ChannelVolume", - "ActiveEffect", - "ActiveEffectType", - "TrackData", - "RetriggerCount", - "Semitone", - "UseTargetPeriod", - "PanEnabled", - "NewNoteAction", - ) - - for c, ch := range m.channels { - var ( - activeEffect string - activeEffectType string - trackData string - ) - if effect := ch.GetActiveEffect(); effect != nil { - activeEffect = fmt.Sprint(effect) - activeEffectType = reflect.TypeOf(effect).Name() - } - if cdata := ch.GetData(); cdata != nil { - trackData = fmt.Sprint(cdata) - } - cs.AddRow( - c+1, - ch.GetChannelVolume(), - activeEffect, - activeEffectType, - trackData, - ch.RetriggerCount, - ch.Semitone, - ch.UseTargetPeriod, - ch.PanEnabled, - ch.NewNoteAction, - ) - } - - return func(w io.Writer) { - fmt.Fprintln(w) - - tw := tabwriter.NewWriter(w, 1, 1, 1, ' ', 0) - defer tw.Flush() - - cs.Fprintln(tw, "\t", true) - } -} diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go new file mode 100644 index 0000000..f1b4d80 --- /dev/null +++ b/format/it/settings/machine.go @@ -0,0 +1,45 @@ +package settings + +import ( + itFilter "github.com/gotracker/playback/format/it/filter" + itOscillator "github.com/gotracker/playback/format/it/oscillator" + itPanning "github.com/gotracker/playback/format/it/panning" + itPeriod "github.com/gotracker/playback/format/it/period" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" +) + +func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { + var p TPeriod + switch any(p).(type) { + case period.Amiga: + return any(&amigaMachine).(*settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) + case period.Linear: + return any(&linearMachine).(*settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) + default: + panic("unsupported machine type") + } +} + +var ( + amigaMachine = settings.MachineSettings[period.Amiga, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + PeriodConverter: itPeriod.AmigaConverter, + GetFilterFactory: itFilter.Factory, + GetVibratoFactory: itOscillator.VibratoFactory, + GetTremoloFactory: itOscillator.TremoloFactory, + GetPanbrelloFactory: itOscillator.PanbrelloFactory, + VoiceFactory: amigaVoiceFactory, + OPL2Enabled: false, + } + + linearMachine = settings.MachineSettings[period.Linear, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + PeriodConverter: itPeriod.LinearConverter, + GetFilterFactory: itFilter.Factory, + GetVibratoFactory: itOscillator.VibratoFactory, + GetTremoloFactory: itOscillator.TremoloFactory, + GetPanbrelloFactory: itOscillator.PanbrelloFactory, + VoiceFactory: linearVoiceFactory, + OPL2Enabled: false, + } +) diff --git a/format/it/settings/voicefactory.go b/format/it/settings/voicefactory.go new file mode 100644 index 0000000..b9ab078 --- /dev/null +++ b/format/it/settings/voicefactory.go @@ -0,0 +1,20 @@ +package settings + +import ( + itPanning "github.com/gotracker/playback/format/it/panning" + itVoice "github.com/gotracker/playback/format/it/voice" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" +) + +type voiceFactory[TPeriod period.Period] struct{} + +func (voiceFactory[TPeriod]) NewVoice(config voice.VoiceConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { + return itVoice.New(config) +} + +var ( + amigaVoiceFactory voiceFactory[period.Amiga] + linearVoiceFactory voiceFactory[period.Linear] +) diff --git a/format/it/system/system.go b/format/it/system/system.go new file mode 100644 index 0000000..bdf5049 --- /dev/null +++ b/format/it/system/system.go @@ -0,0 +1,36 @@ +package system + +import ( + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/system" +) + +const ( + // DefaultC5SampleRate is the default sample rate for IT samples + DefaultC5SampleRate = 8363 + // C5Period is the sampler (Amiga-style) period of the C-5 note + C5Period = 428 + + // ITBaseClock is the base clock speed of IT files + ITBaseClock frequency.Frequency = DefaultC5SampleRate * C5Period + + NotesPerOctave = 12 + SlideFinesPerSemitone = 4 + SemitonesPerNote = 16 + SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote + SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave + C5SlideFines = 5 * SlideFinesPerOctave +) + +var semitonePeriodTable = [...]uint16{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} + +var ITSystem system.ClockableSystem = system.ClockedSystem{ + MaxPastNotesPerChannel: 1, + BaseClock: ITBaseClock, + BaseFinetunes: C5SlideFines, + FinetunesPerOctave: SlideFinesPerOctave, + FinetunesPerNote: SlideFinesPerNote, + CommonPeriod: C5Period, + CommonRate: DefaultC5SampleRate, + SemitonePeriods: semitonePeriodTable, +} diff --git a/format/it/voice/enveloper_filter.go b/format/it/voice/enveloper_filter.go new file mode 100644 index 0000000..2f9402e --- /dev/null +++ b/format/it/voice/enveloper_filter.go @@ -0,0 +1,44 @@ +package voice + +// == FilterEnveloper == + +func (v *itVoice[TPeriod]) EnableFilterEnvelope(enabled bool) error { + if !v.pitchAndFilterEnvShared { + return v.filterEnv.SetEnabled(enabled) + } + + // shared filter/pitch envelope + if !v.filterEnvActive { + return nil + } + + return v.filterEnv.SetEnabled(enabled) +} + +func (v itVoice[TPeriod]) IsFilterEnvelopeEnabled() bool { + if v.pitchAndFilterEnvShared && !v.filterEnvActive { + return false + } + return v.filterEnv.IsEnabled() +} + +func (v itVoice[TPeriod]) GetCurrentFilterEnvelope() uint8 { + return v.filterEnv.GetCurrentValue() +} + +func (v *itVoice[TPeriod]) SetFilterEnvelopePosition(pos int) error { + if !v.pitchAndFilterEnvShared || v.filterEnvActive { + doneCB, err := v.filterEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { + doneCB(v) + } + } + return nil +} + +func (v itVoice[TPeriod]) GetFilterEnvelopePosition() int { + return v.filterEnv.GetEnvelopePosition() +} diff --git a/format/it/voice/enveloper_pan.go b/format/it/voice/enveloper_pan.go new file mode 100644 index 0000000..8e6bf5d --- /dev/null +++ b/format/it/voice/enveloper_pan.go @@ -0,0 +1,37 @@ +package voice + +import ( + itPanning "github.com/gotracker/playback/format/it/panning" +) + +// == PanEnveloper == + +func (v *itVoice[TPeriod]) EnablePanEnvelope(enabled bool) error { + return v.panEnv.SetEnabled(enabled) +} + +func (v itVoice[TPeriod]) IsPanEnvelopeEnabled() bool { + return v.panEnv.IsEnabled() +} + +func (v itVoice[TPeriod]) GetCurrentPanEnvelope() itPanning.Panning { + if v.panEnv.IsEnabled() { + return v.panEnv.GetCurrentValue() + } + return itPanning.DefaultPanning +} + +func (v *itVoice[TPeriod]) SetPanEnvelopePosition(pos int) error { + doneCB, err := v.panEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { + doneCB(v) + } + return nil +} + +func (v itVoice[TPeriod]) GetPanEnvelopePosition() int { + return v.panEnv.GetEnvelopePosition() +} diff --git a/format/it/voice/enveloper_pitch.go b/format/it/voice/enveloper_pitch.go new file mode 100644 index 0000000..b9c1d96 --- /dev/null +++ b/format/it/voice/enveloper_pitch.go @@ -0,0 +1,42 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == PitchEnveloper == + +func (v *itVoice[TPeriod]) EnablePitchEnvelope(enabled bool) error { + return v.pitchEnv.SetEnabled(enabled) +} + +func (v itVoice[TPeriod]) IsPitchEnvelopeEnabled() bool { + if v.pitchAndFilterEnvShared && v.filterEnvActive { + return false + } + return v.pitchEnv.IsEnabled() +} + +func (v itVoice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { + if v.pitchEnv.IsEnabled() { + return v.pitchEnv.GetCurrentValue() + } + return 0 +} + +func (v *itVoice[TPeriod]) SetPitchEnvelopePosition(pos int) error { + if !v.pitchAndFilterEnvShared || !v.filterEnvActive { + doneCB, err := v.pitchEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { + doneCB(v) + } + } + return nil +} + +func (v itVoice[TPeriod]) GetPitchEnvelopePosition() int { + return v.pitchEnv.GetEnvelopePosition() +} diff --git a/format/it/voice/enveloper_volume.go b/format/it/voice/enveloper_volume.go new file mode 100644 index 0000000..2730568 --- /dev/null +++ b/format/it/voice/enveloper_volume.go @@ -0,0 +1,37 @@ +package voice + +import ( + itVolume "github.com/gotracker/playback/format/it/volume" +) + +// == VolumeEnveloper == + +func (v *itVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) error { + return v.volEnv.SetEnabled(enabled) +} + +func (v itVoice[TPeriod]) IsVolumeEnvelopeEnabled() bool { + return v.volEnv.IsEnabled() +} + +func (v itVoice[TPeriod]) GetCurrentVolumeEnvelope() itVolume.Volume { + if v.volEnv.IsEnabled() { + return v.volEnv.GetCurrentValue() + } + return itVolume.Volume(itVolume.MaxItVolume) +} + +func (v *itVoice[TPeriod]) SetVolumeEnvelopePosition(pos int) error { + doneCB, err := v.volEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { + doneCB(v) + } + return nil +} + +func (v itVoice[TPeriod]) GetVolumeEnvelopePosition() int { + return v.volEnv.GetEnvelopePosition() +} diff --git a/format/it/voice/modulator_amp.go b/format/it/voice/modulator_amp.go new file mode 100644 index 0000000..99dfb54 --- /dev/null +++ b/format/it/voice/modulator_amp.go @@ -0,0 +1,65 @@ +package voice + +import ( + "github.com/gotracker/gomixing/volume" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/voice/types" + "github.com/heucuva/optional" +) + +// == AmpModulator == + +func (v *itVoice[TPeriod]) SetActive(on bool) error { + return v.amp.SetActive(on) +} + +func (v itVoice[TPeriod]) IsActive() bool { + return v.amp.IsActive() +} + +func (v *itVoice[TPeriod]) SetMixingVolume(vol itVolume.FineVolume) error { + return v.amp.SetMixingVolume(vol) +} + +func (v itVoice[TPeriod]) GetMixingVolume() itVolume.FineVolume { + return v.amp.GetMixingVolume() +} + +func (v *itVoice[TPeriod]) SetMixingVolumeOverride(mvo optional.Value[itVolume.FineVolume]) error { + return v.amp.SetMixingVolumeOverride(mvo) +} + +func (v itVoice[TPeriod]) GetMixingVolumeOverride() optional.Value[itVolume.FineVolume] { + return v.amp.GetMixingVolumeOverride() +} + +func (v *itVoice[TPeriod]) SetVolume(vol itVolume.Volume) error { + if vol.IsUseInstrumentVol() { + vol = v.voicer.GetDefaultVolume() + } + return v.amp.SetVolume(vol) +} + +func (v itVoice[TPeriod]) GetVolume() itVolume.Volume { + return v.amp.GetVolume() +} + +func (v *itVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) error { + return v.amp.SetVolumeDelta(d) +} + +func (v itVoice[TPeriod]) GetVolumeDelta() types.VolumeDelta { + return v.amp.GetVolumeDelta() +} + +func (v itVoice[TPeriod]) IsFadeout() bool { + return v.fadeout.IsActive() +} + +func (v itVoice[TPeriod]) GetFadeoutVolume() volume.Volume { + return v.fadeout.GetVolume() +} + +func (v itVoice[TPeriod]) GetFinalVolume() volume.Volume { + return v.finalVol +} diff --git a/format/it/voice/modulator_freq.go b/format/it/voice/modulator_freq.go new file mode 100644 index 0000000..ce9f946 --- /dev/null +++ b/format/it/voice/modulator_freq.go @@ -0,0 +1,30 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == FreqModulator == + +func (v *itVoice[TPeriod]) SetPeriod(period TPeriod) error { + if period.IsInvalid() { + return nil + } + return v.freq.SetPeriod(period) +} + +func (v itVoice[TPeriod]) GetPeriod() TPeriod { + return v.freq.GetPeriod() +} + +func (v *itVoice[TPeriod]) SetPeriodDelta(delta period.Delta) error { + return v.freq.SetPeriodDelta(delta) +} + +func (v itVoice[TPeriod]) GetPeriodDelta() period.Delta { + return v.freq.GetPeriodDelta() +} + +func (v itVoice[TPeriod]) GetFinalPeriod() (TPeriod, error) { + return v.finalPeriod, nil +} diff --git a/format/it/voice/modulator_pan.go b/format/it/voice/modulator_pan.go new file mode 100644 index 0000000..c4cdf78 --- /dev/null +++ b/format/it/voice/modulator_pan.go @@ -0,0 +1,46 @@ +package voice + +import ( + "github.com/gotracker/gomixing/panning" + itPanning "github.com/gotracker/playback/format/it/panning" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/voice/types" +) + +// == PanModulator == + +func (v *itVoice[TPeriod]) SetPan(pan itPanning.Panning) error { + return v.pan.SetPan(pan) +} + +func (v itVoice[TPeriod]) GetPan() itPanning.Panning { + return v.pan.GetPan() +} + +func (v *itVoice[TPeriod]) SetPanDelta(d types.PanDelta) error { + return v.pan.SetPanDelta(d) +} + +func (v itVoice[TPeriod]) GetPanDelta() types.PanDelta { + return v.pan.GetPanDelta() +} + +func (v itVoice[TPeriod]) GetPanSeparation() float32 { + return v.pitchPan.GetPanSeparation() +} + +func (v *itVoice[TPeriod]) SetPitchPanNote(st note.Semitone) error { + return v.pitchPan.SetPitch(st) +} + +func (v *itVoice[TPeriod]) EnablePitchPan(enabled bool) error { + return v.pitchPan.EnablePitchPan(enabled) +} + +func (v itVoice[TPeriod]) IsPitchPanEnabled() bool { + return v.pitchPan.IsPitchPanEnabled() +} + +func (v itVoice[TPeriod]) GetFinalPan() panning.Position { + return v.finalPan +} diff --git a/format/it/voice/sampler.go b/format/it/voice/sampler.go new file mode 100644 index 0000000..32b0281 --- /dev/null +++ b/format/it/voice/sampler.go @@ -0,0 +1,51 @@ +package voice + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" +) + +type voicerPos interface { + GetPos() sampling.Pos + SetPos(pos sampling.Pos) +} + +type voicerSampler interface { + GetSample(pos sampling.Pos) volume.Matrix +} + +func (v *itVoice[TPeriod]) GetPos() (sampling.Pos, error) { + if vp, ok := v.voicer.(voicerPos); ok { + return vp.GetPos(), nil + } + return sampling.Pos{}, nil +} + +func (v *itVoice[TPeriod]) SetPos(pos sampling.Pos) error { + if vp, ok := v.voicer.(voicerPos); ok { + vp.SetPos(pos) + } + return nil +} + +func (v *itVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { + var samp volume.Matrix + if sampler, ok := v.voicer.(voicerSampler); ok { + samp = sampler.GetSample(pos) + if samp.Channels == 0 { + samp.Channels = v.voicer.GetNumChannels() + } + } + + vol := v.GetFinalVolume() + wet := samp.Apply(vol) + if v.voiceFilter != nil { + wet = v.voiceFilter.Filter(wet) + } + return wet +} + +func (v itVoice[TPeriod]) GetSampleRate() frequency.Frequency { + return v.inst.SampleRate +} diff --git a/format/it/voice/tracing.go b/format/it/voice/tracing.go new file mode 100644 index 0000000..104d1ea --- /dev/null +++ b/format/it/voice/tracing.go @@ -0,0 +1,29 @@ +package voice + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" +) + +func (v itVoice[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer) { + if t == nil { + return + } + + v.KeyModulator.DumpState(ch, t, "itVoice.KeyModulator") + if v.voicer != nil { + v.voicer.DumpState(ch, t, "itVoice.voicer") + } else { + t.TraceChannelWithComment(ch, "nil", "itVoice.voicer") + } + v.amp.DumpState(ch, t, "itVoice.amp") + v.freq.DumpState(ch, t, "itVoice.freq") + v.pan.DumpState(ch, t, "itVoice.pan") + v.volEnv.DumpState(ch, t, "itVoice.volEnv") + v.pitchEnv.DumpState(ch, t, "itVoice.pitchEnv") + v.panEnv.DumpState(ch, t, "itVoice.panEnv") + v.filterEnv.DumpState(ch, t, "itVoice.filterEnv") + v.vol0Opt.DumpState(ch, t, "itVoice.vol0Opt") + //voiceFilter + //pluginFilter +} diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go new file mode 100644 index 0000000..a94df8d --- /dev/null +++ b/format/it/voice/voice.go @@ -0,0 +1,417 @@ +package voice + +import ( + "errors" + "fmt" + + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" + itFilter "github.com/gotracker/playback/format/it/filter" + itOscillator "github.com/gotracker/playback/format/it/oscillator" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/voice/fadeout" +) + +type Period interface { + period.Period +} + +type itVoice[TPeriod Period] struct { + inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + background bool + + pitchAndFilterEnvShared bool + filterEnvActive bool // if pitchAndFilterEnvShared is true, this dictates which is active initially - true=filter, false=pitch + fadeoutMode fadeout.Mode + + component.KeyModulator + + stopped bool + voicer component.Voicer[TPeriod, itVolume.FineVolume, itVolume.Volume] + amp component.AmpModulator[itVolume.FineVolume, itVolume.Volume] + fadeout component.FadeoutModulator + freq component.FreqModulator[TPeriod] + autoVibrato component.AutoVibratoModulator[TPeriod] + pan component.PanModulator[itPanning.Panning] + pitchPan component.PitchPanModulator[itPanning.Panning] + volEnv component.VolumeEnvelope[itVolume.Volume] + pitchEnv component.PitchEnvelope + panEnv component.PanEnvelope[itPanning.Panning] + filterEnv component.FilterEnvelope + vol0Opt component.Vol0Optimization + voiceFilter filter.Filter + + // finals + finalVol volume.Volume + finalPeriod TPeriod + finalPan panning.Position +} + +var ( + _ voice.Sampler = (*itVoice[period.Linear])(nil) + _ voice.AmpModulator[itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume] = (*itVoice[period.Linear])(nil) + _ voice.FadeoutModulator = (*itVoice[period.Linear])(nil) + _ voice.FreqModulator[period.Linear] = (*itVoice[period.Linear])(nil) + _ voice.PanModulator[itPanning.Panning] = (*itVoice[period.Linear])(nil) + _ voice.PitchPanModulator[itPanning.Panning] = (*itVoice[period.Linear])(nil) + _ voice.VolumeEnvelope[itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume] = (*itVoice[period.Linear])(nil) + _ voice.PitchEnvelope[period.Linear] = (*itVoice[period.Linear])(nil) + _ voice.PanEnvelope[itPanning.Panning] = (*itVoice[period.Linear])(nil) + _ voice.FilterEnvelope = (*itVoice[period.Linear])(nil) +) + +func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { + v := &itVoice[TPeriod]{ + pitchAndFilterEnvShared: true, + } + + v.KeyModulator.Setup(component.KeyModulatorSettings{ + Attack: v.doAttack, + Release: v.doRelease, + Fadeout: v.doFadeout, + DeferredAttack: v.doDeferredAttack, + DeferredRelease: v.doDeferredRelease, + }) + + v.amp.Setup(component.AmpModulatorSettings[itVolume.FineVolume, itVolume.Volume]{ + Active: true, + DefaultMixingVolume: config.InitialMixing, + DefaultVolume: config.InitialVolume, + }) + + v.freq.Setup(component.FreqModulatorSettings[TPeriod]{ + PC: config.PC, + }) + + v.pan.Setup(component.PanModulatorSettings[itPanning.Panning]{ + Enabled: config.PanEnabled, + InitialPan: config.InitialPan, + }) + + v.vol0Opt.Setup(config.Vol0Optimization) + + return v +} + +func (v *itVoice[TPeriod]) doAttack() { + v.vol0Opt.Reset() + v.autoVibrato.Reset() + + v.SetVolumeEnvelopePosition(0) + v.SetPitchEnvelopePosition(0) + v.SetPanEnvelopePosition(0) + v.SetFilterEnvelopePosition(0) + + v.fadeout.Reset() + v.volEnv.Attack() + v.pitchEnv.Attack() + v.panEnv.Attack() + v.filterEnv.Attack() + if v.voicer != nil { + v.voicer.Attack() + } + v.updateFinal() +} + +func (v *itVoice[TPeriod]) doRelease() { + v.volEnv.Release() + v.pitchEnv.Release() + v.panEnv.Release() + v.filterEnv.Release() + if v.voicer != nil { + v.voicer.Release() + } + if v.background && !v.volEnv.CanLoop() { + v.KeyModulator.Fadeout() // triggers updateFinal + } else { + v.updateFinal() + } +} + +func (v *itVoice[TPeriod]) doFadeout() { + if v.voicer != nil { + v.voicer.Fadeout() + } + v.updateFinal() +} + +func (v *itVoice[TPeriod]) doDeferredAttack() { + if v.voicer != nil { + v.voicer.DeferredAttack() + } +} + +func (v *itVoice[TPeriod]) doDeferredRelease() { + if v.voicer != nil { + v.voicer.DeferredRelease() + } +} + +func (v itVoice[TPeriod]) getFadeoutEnabled() bool { + return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) +} + +func (v *itVoice[TPeriod]) SetPlaybackRate(outputRate frequency.Frequency) error { + if v.voiceFilter != nil { + v.voiceFilter.SetPlaybackRate(outputRate) + } + return nil +} + +func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { + v.inst = inst + + v.voicer = nil + switch d := inst.GetData().(type) { + case *instrument.PCM[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]: + v.filterEnvActive = d.PitchFiltMode + v.fadeoutMode = d.FadeOut.Mode + + v.fadeout.Setup(component.FadeoutModulatorSettings{ + Enabled: d.FadeOut.Mode != fadeout.ModeDisabled, + GetActive: v.getFadeoutEnabled, + Amount: d.FadeOut.Amount, + }) + + v.pitchPan.Setup(component.PitchPanModulatorSettings[itPanning.Panning]{ + PitchPanEnable: d.PitchPan.Enabled, + PitchPanCenter: d.PitchPan.Center, + PitchPanSeparation: d.PitchPan.Separation, + }) + + volEnvSettings := component.EnvelopeSettings[itVolume.Volume, itVolume.Volume]{ + Envelope: d.VolEnv, + } + if d.VolEnvFinishFadesOut { + volEnvSettings.OnFinished = func(v voice.Voice) { + v.Fadeout() + } + } + v.volEnv.Setup(volEnvSettings) + + v.pitchEnv.Setup(component.EnvelopeSettings[int8, period.Delta]{ + Envelope: d.PitchFiltEnv, + }) + + v.panEnv.Setup(component.EnvelopeSettings[itPanning.Panning, itPanning.Panning]{ + Envelope: d.PanEnv, + }) + + v.filterEnv.Setup(component.EnvelopeSettings[int8, uint8]{ + Envelope: d.PitchFiltEnv, + }) + + if err := v.amp.SetMixingVolumeOverride(d.MixingVolume); err != nil { + return err + } + + var s component.Sampler[TPeriod, itVolume.FineVolume, itVolume.Volume] + s.Setup(component.SamplerSettings[TPeriod, itVolume.FineVolume, itVolume.Volume]{ + Sample: d.Sample, + DefaultVolume: inst.GetDefaultVolume(), + MixVolume: itVolume.MaxItFineVolume, + WholeLoop: d.Loop, + SustainLoop: d.SustainLoop, + }) + v.voicer = &s + + default: + return fmt.Errorf("unhandled instrument type: %T", d) + } + if inst == nil { + return errors.New("instrument is nil") + } + + v.autoVibrato.Setup(autovibrato.AutoVibratoSettings[TPeriod]{ + AutoVibratoConfig: inst.Static.AutoVibrato, + Factory: itOscillator.OscillatorFactory, + }) + + info := inst.GetVoiceFilterInfo() + f, err := itFilter.Factory(info.Name, inst.SampleRate, info.Params) + if err != nil { + return fmt.Errorf("filter factory(%q) error: %w", info.Name, err) + } + v.voiceFilter = f + + v.Reset() + return nil +} + +func (v *itVoice[TPeriod]) Reset() error { + v.KeyModulator.Release() + v.stopped = false + return errors.Join( + v.amp.Reset(), + v.fadeout.Reset(), + v.freq.Reset(), + v.autoVibrato.Reset(), + v.pan.Reset(), + v.pitchPan.Reset(), + v.volEnv.Reset(), + v.pitchEnv.Reset(), + v.panEnv.Reset(), + v.filterEnv.Reset(), + v.vol0Opt.Reset(), + v.updateFinal(), + ) +} + +func (v *itVoice[TPeriod]) Stop() { + v.stopped = true + v.updateFinal() +} + +func (v itVoice[TPeriod]) IsDone() bool { + if v.voicer == nil || v.stopped { + return true + } + + if v.fadeout.IsActive() { + return v.fadeout.GetVolume() <= 0 + } + + return v.vol0Opt.IsDone() +} + +func (v *itVoice[TPeriod]) SetMuted(muted bool) error { + return v.amp.SetMuted(muted) +} + +func (v itVoice[TPeriod]) IsMuted() bool { + return v.amp.IsMuted() +} + +func (v *itVoice[TPeriod]) Tick() error { + v.fadeout.Advance() + v.autoVibrato.Advance() + v.pitchPan.Advance() + if v.IsVolumeEnvelopeEnabled() { + if doneCB := v.volEnv.Advance(); doneCB != nil { + doneCB(v) + } + } + if v.IsPanEnvelopeEnabled() { + if doneCB := v.panEnv.Advance(); doneCB != nil { + doneCB(v) + } + } + if v.IsPitchEnvelopeEnabled() { + if doneCB := v.pitchEnv.Advance(); doneCB != nil { + doneCB(v) + } + } + if v.IsFilterEnvelopeEnabled() { + if doneCB := v.filterEnv.Advance(); doneCB != nil { + doneCB(v) + } + } + + if v.voiceFilter != nil && v.IsFilterEnvelopeEnabled() { + fval := v.GetCurrentFilterEnvelope() + v.voiceFilter.UpdateEnv(fval) + } + + // has to be after the mod/env updates + v.KeyModulator.DeferredUpdate() + + v.KeyModulator.Advance() + + return v.updateFinal() +} + +func (v *itVoice[TPeriod]) RowEnd() error { + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + return nil +} + +func (v *itVoice[TPeriod]) Clone(background bool) voice.Voice { + vv := itVoice[TPeriod]{ + inst: v.inst, + background: background, + pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, + filterEnvActive: v.filterEnvActive, + fadeoutMode: v.fadeoutMode, + stopped: v.stopped, + amp: v.amp.Clone(), + fadeout: v.fadeout.Clone(), + freq: v.freq.Clone(), + autoVibrato: v.autoVibrato.Clone(), + pan: v.pan.Clone(), + pitchPan: v.pitchPan.Clone(), + pitchEnv: v.pitchEnv.Clone(nil), + panEnv: v.panEnv.Clone(nil), + filterEnv: v.filterEnv.Clone(nil), + vol0Opt: v.vol0Opt.Clone(), + } + + vv.volEnv = v.volEnv.Clone(func(v voice.Voice) { + vv.Fadeout() + }) + + vv.KeyModulator = v.KeyModulator.Clone(component.KeyModulatorSettings{ + Attack: vv.doAttack, + Release: vv.doRelease, + Fadeout: vv.doFadeout, + DeferredAttack: vv.doDeferredAttack, + DeferredRelease: vv.doDeferredRelease, + }) + + if v.voicer != nil { + vv.voicer = v.voicer.Clone() + } + + if v.voiceFilter != nil { + vv.voiceFilter = v.voiceFilter.Clone() + } + + return &vv +} + +func (v *itVoice[TPeriod]) updateFinal() error { + if v.IsDone() { + v.finalVol = 0 + return nil + } + + // volume + vol := v.amp.GetFinalVolume() + volEnv := volume.Volume(1) + if v.IsVolumeEnvelopeEnabled() { + volEnv = v.GetCurrentVolumeEnvelope().ToVolume() + } + fadeVol := v.fadeout.GetFinalVolume() + + v.finalVol = vol * volEnv * fadeVol + + // period + p, err := v.freq.GetFinalPeriod() + if err != nil { + return err + } + if v.IsPitchEnvelopeEnabled() { + delta := v.GetCurrentPitchEnvelope() + p, err = v.inst.Static.PC.AddDelta(p, delta) + if err != nil { + return err + } + } + v.finalPeriod = p + + // panning + if !v.IsPanEnvelopeEnabled() { + v.finalPan = v.pan.GetFinalPan() + } else { + envPan := v.panEnv.GetCurrentValue() + v.finalPan = v.pitchPan.GetSeparatedPan(envPan).ToPosition() + } + return err +} diff --git a/format/it/volume/finevolume.go b/format/it/volume/finevolume.go new file mode 100644 index 0000000..53ac5dd --- /dev/null +++ b/format/it/volume/finevolume.go @@ -0,0 +1,58 @@ +package volume + +import ( + "math" + + itfile "github.com/gotracker/goaudiofile/music/tracked/it" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/voice/types" +) + +type FineVolume itfile.FineVolume + +var ( + _ types.VolumeMaxer[FineVolume] = FineVolume(0) + _ types.VolumeDeltaer[FineVolume] = FineVolume(0) +) + +func (v FineVolume) ToVolume() volume.Volume { + return volume.Volume(itfile.FineVolume(v).Value()) +} + +func (v FineVolume) IsInvalid() bool { + return v > 0x80 && v != 0xFF +} + +func (v FineVolume) IsUseInstrumentVol() bool { + return v == 0xFF +} + +func (FineVolume) GetMax() FineVolume { + return MaxItFineVolume +} + +func (v FineVolume) FMA(multiplier, add float32) FineVolume { + if v == FineVolume(0xff) { + return v + } + + return min(FineVolume(max(math.FMA(float64(v), float64(multiplier), float64(add)), 0)), MaxItFineVolume) +} + +func (v FineVolume) AddDelta(d types.VolumeDelta) FineVolume { + return FineVolume(min(max(int16(v)+int16(d), 0), int16(MaxItFineVolume))) +} + +// ToItFineVolume converts a player volume to an it fine volume +func ToItFineVolume(v volume.Volume) FineVolume { + switch { + case v == volume.VolumeUseInstVol: + return FineVolume(0xff) + case v < 0.0: + return 0 + case v > 1.0: + return FineVolume(MaxItFineVolume) + default: + return FineVolume(v * volume.Volume(MaxItFineVolume)) + } +} diff --git a/format/it/volume/volume.go b/format/it/volume/volume.go index 8d0873b..7c0426e 100644 --- a/format/it/volume/volume.go +++ b/format/it/volume/volume.go @@ -1,12 +1,16 @@ package volume import ( + "math" + itfile "github.com/gotracker/goaudiofile/music/tracked/it" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/voice/types" ) var ( MaxItVolume = itfile.Volume(0x40) + MaxItFineVolume = FineVolume(0x80) DefaultItVolume = itfile.DefaultVolume // DefaultVolume is the default volume value for most everything in IT format @@ -16,6 +20,41 @@ var ( DefaultMixingVolume = itfile.FineVolume(0x30).Value() ) +type Volume itfile.Volume + +var ( + _ types.VolumeMaxer[Volume] = Volume(0) + _ types.VolumeDeltaer[Volume] = Volume(0) +) + +func (v Volume) ToVolume() volume.Volume { + return volume.Volume(itfile.Volume(v).Value()) +} + +func (v Volume) IsInvalid() bool { + return v > 64 && v != 0xff +} + +func (v Volume) IsUseInstrumentVol() bool { + return v == 0xff +} + +func (Volume) GetMax() Volume { + return Volume(MaxItVolume) +} + +func (v Volume) FMA(multiplier, add float32) Volume { + if v == Volume(0xff) { + return v + } + + return Volume(min(max(math.FMA(float64(v), float64(multiplier), float64(add)), 0), float64(MaxItVolume))) +} + +func (v Volume) AddDelta(d types.VolumeDelta) Volume { + return Volume(min(max(int16(v)+int16(d), 0), int16(MaxItVolume))) +} + // FromItVolume converts an it volume to a player volume func FromItVolume(vol itfile.Volume) volume.Volume { return volume.Volume(vol.Value()) @@ -32,15 +71,15 @@ func FromVolPan(vp uint8) volume.Volume { } // ToItVolume converts a player volume to an it volume -func ToItVolume(v volume.Volume) itfile.Volume { +func ToItVolume(v volume.Volume) Volume { switch { case v == volume.VolumeUseInstVol: - return 0 + return Volume(0xff) case v < 0.0: return 0 case v > 1.0: - return MaxItVolume + return Volume(MaxItVolume) default: - return itfile.Volume(v * volume.Volume(MaxItVolume)) + return Volume(v * volume.Volume(MaxItVolume)) } } diff --git a/format/mod/mod.go b/format/mod/mod.go index 4585795..391a1cf 100644 --- a/format/mod/mod.go +++ b/format/mod/mod.go @@ -3,13 +3,16 @@ package mod import ( "io" - "github.com/gotracker/playback" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/load" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" "github.com/gotracker/playback/util" ) -type format struct{} +type format struct { + common.Format +} var ( // MOD is the exported interface to the MOD file loader @@ -17,7 +20,7 @@ var ( ) // Load loads an MOD file into a playback system -func (f format) Load(filename string, features []feature.Feature) (playback.Playback, error) { +func (f format) Load(filename string, features []feature.Feature) (song.Data, error) { r, err := util.ReadFile(filename) if err != nil { return nil, err @@ -27,7 +30,7 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play } // LoadFromReader loads a MOD file on a reader into a playback system -func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { +func (format) LoadFromReader(r io.Reader, features []feature.Feature) (song.Data, error) { // we really just load the mod into an S3M layout, since S3M is essentially a superset return load.MOD(r, features) } diff --git a/format/s3m/channel/data.go b/format/s3m/channel/data.go index ce440d9..5116c53 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -7,10 +7,15 @@ import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" "github.com/gotracker/gomixing/volume" - s3mNote "github.com/gotracker/playback/format/s3m/note" + "github.com/gotracker/playback" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" s3mVolume "github.com/gotracker/playback/format/s3m/volume" - "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/instruction" + "github.com/gotracker/playback/song" ) // DataEffect is the type of a channel's EffectParameter value @@ -20,8 +25,8 @@ type DataEffect uint8 type Data struct { What s3mfile.PatternFlags Note s3mfile.Note - Instrument InstID - Volume s3mfile.Volume + Instrument uint8 + Volume s3mVolume.Volume Command uint8 Info DataEffect } @@ -33,7 +38,7 @@ func (d Data) HasNote() bool { // GetNote returns the note for the channel func (d Data) GetNote() note.Note { - return s3mNote.NoteFromS3MNote(d.Note) + return NoteFromS3MNote(d.Note) } // HasInstrument returns true if there exists an instrument on the channel @@ -42,8 +47,8 @@ func (d Data) HasInstrument() bool { } // GetInstrument returns the instrument for the channel -func (d Data) GetInstrument(stmem note.Semitone) instrument.ID { - return d.Instrument +func (d Data) GetInstrument() int { + return int(d.Instrument) } // HasVolume returns true if there exists a volume on the channel @@ -51,9 +56,13 @@ func (d Data) HasVolume() bool { return d.What.HasVolume() } +func (d Data) GetVolumeGeneric() volume.Volume { + return d.Volume.ToVolume() +} + // GetVolume returns the volume for the channel -func (d Data) GetVolume() volume.Volume { - return s3mVolume.VolumeFromS3M(d.Volume) +func (d Data) GetVolume() s3mVolume.Volume { + return d.Volume } // HasCommand returns true if there exists a command on the channel @@ -66,6 +75,16 @@ func (d Data) Channel() uint8 { return d.What.Channel() } +func (d Data) GetEffects(mem *Memory, p period.Period) []playback.Effect { + if d.HasCommand() { + if e := EffectFactory(mem, d); e != nil { + return []playback.Effect{e} + } + } + + return nil +} + func (d Data) String() string { pieces := []string{ "...", // note @@ -94,3 +113,18 @@ func (d Data) ShortString() string { } return "..." } + +func (d Data) ToInstructions(m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], ch index.Channel, songData song.Data) ([]instruction.Instruction, error) { + var instructions []instruction.Instruction + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return nil, err + } + + if e := EffectFactory(mem, d); e != nil { + instructions = append(instructions, e) + } + + return instructions, nil +} diff --git a/format/s3m/channel/effect_arpeggio.go b/format/s3m/channel/effect_arpeggio.go new file mode 100644 index 0000000..ee3cf59 --- /dev/null +++ b/format/s3m/channel/effect_arpeggio.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Arpeggio defines an arpeggio effect +type Arpeggio ChannelCommand // 'J' + +func (e Arpeggio) String() string { + return fmt.Sprintf("J%0.2x", DataEffect(e)) +} + +func (e Arpeggio) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + x, y := DataEffect(e)>>4, DataEffect(e)&0xF + return doArpeggio(ch, m, tick, int8(x), int8(y)) +} + +func (e Arpeggio) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_enablefilter.go b/format/s3m/channel/effect_enablefilter.go new file mode 100644 index 0000000..54a9e38 --- /dev/null +++ b/format/s3m/channel/effect_enablefilter.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// EnableFilter defines a set filter enable effect +type EnableFilter ChannelCommand // 'S0x' + +func (e EnableFilter) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e EnableFilter) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + x := DataEffect(e) & 0xf + return m.SetFilterOnAllChannelsByFilterName("amigalpf", x != 0, nil) +} + +func (e EnableFilter) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_extrafineportadown.go b/format/s3m/channel/effect_extrafineportadown.go new file mode 100644 index 0000000..af3325b --- /dev/null +++ b/format/s3m/channel/effect_extrafineportadown.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ExtraFinePortaDown defines an extra-fine portamento down effect +type ExtraFinePortaDown ChannelCommand // 'EEx' + +func (e ExtraFinePortaDown) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e ExtraFinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + y := DataEffect(e) & 0x0F + return m.DoChannelPortaDown(ch, period.Delta(y)*1) +} + +func (e ExtraFinePortaDown) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_extrafineportaup.go b/format/s3m/channel/effect_extrafineportaup.go new file mode 100644 index 0000000..367f700 --- /dev/null +++ b/format/s3m/channel/effect_extrafineportaup.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ExtraFinePortaUp defines an extra-fine portamento up effect +type ExtraFinePortaUp ChannelCommand // 'FEx' + +func (e ExtraFinePortaUp) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e ExtraFinePortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + y := DataEffect(e) & 0x0F + return m.DoChannelPortaUp(ch, period.Delta(y)*1) +} + +func (e ExtraFinePortaUp) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_finepatterndelay.go b/format/s3m/channel/effect_finepatterndelay.go new file mode 100644 index 0000000..4f8b20c --- /dev/null +++ b/format/s3m/channel/effect_finepatterndelay.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePatternDelay defines an fine pattern delay effect +type FinePatternDelay ChannelCommand // 'S6x' + +func (e FinePatternDelay) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e FinePatternDelay) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + x := DataEffect(e) & 0xf + return m.AddExtraTicks(int(x)) +} + +func (e FinePatternDelay) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_fineportadown.go b/format/s3m/channel/effect_fineportadown.go new file mode 100644 index 0000000..c7aedd2 --- /dev/null +++ b/format/s3m/channel/effect_fineportadown.go @@ -0,0 +1,41 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePortaDown defines an fine portamento down effect +type FinePortaDown ChannelCommand // 'EFx' + +func (e FinePortaDown) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e FinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := DataEffect(e) & 0x0F + + var mul period.Delta = 4 + if mem.Shared.ST300Portas { + mul = 2 + } + return m.DoChannelPortaDown(ch, period.Delta(y)*mul) +} + +func (e FinePortaDown) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_fineportaup.go b/format/s3m/channel/effect_fineportaup.go new file mode 100644 index 0000000..dfc7d2f --- /dev/null +++ b/format/s3m/channel/effect_fineportaup.go @@ -0,0 +1,41 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePortaUp defines an fine portamento up effect +type FinePortaUp ChannelCommand // 'FFx' + +func (e FinePortaUp) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e FinePortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + y := DataEffect(e) & 0x0F + + var mul period.Delta = 4 + if mem.Shared.ST300Portas { + mul = 2 + } + return m.DoChannelPortaUp(ch, period.Delta(y)*mul) +} + +func (e FinePortaUp) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_finevibrato.go b/format/s3m/channel/effect_finevibrato.go new file mode 100644 index 0000000..278cb8a --- /dev/null +++ b/format/s3m/channel/effect_finevibrato.go @@ -0,0 +1,38 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVibrato defines an fine vibrato effect +type FineVibrato ChannelCommand // 'U' + +func (e FineVibrato) String() string { + return fmt.Sprintf("U%0.2x", DataEffect(e)) +} + +func (e FineVibrato) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Vibrato(DataEffect(e)) + // NOTE: JBC - S3M does not update on tick 0, but MOD does. + if tick != 0 || mem.Shared.ModCompatibility { + return withOscillatorDo(ch, m, int(x), float32(y)*1, machine.OscillatorVibrato, func(value float32) error { + return m.SetChannelPeriodDelta(ch, period.Delta(value)) + }) + } + return nil +} + +func (e FineVibrato) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_finevolslidedown.go b/format/s3m/channel/effect_finevolslidedown.go new file mode 100644 index 0000000..c7b0acf --- /dev/null +++ b/format/s3m/channel/effect_finevolslidedown.go @@ -0,0 +1,35 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVolumeSlideDown defines a fine volume slide down effect +type FineVolumeSlideDown ChannelCommand // 'DFy' + +func (e FineVolumeSlideDown) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e FineVolumeSlideDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick == 0 { + return nil + } + + y := DataEffect(e) & 0x0F + + if y != 0x0F { + return m.SlideChannelVolume(ch, 1, -float32(y)) + } + return nil +} + +func (e FineVolumeSlideDown) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_finevolslideup.go b/format/s3m/channel/effect_finevolslideup.go new file mode 100644 index 0000000..96a8eda --- /dev/null +++ b/format/s3m/channel/effect_finevolslideup.go @@ -0,0 +1,35 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVolumeSlideUp defines a fine volume slide up effect +type FineVolumeSlideUp ChannelCommand // 'DxF' + +func (e FineVolumeSlideUp) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e FineVolumeSlideUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick == 0 { + return nil + } + + x := DataEffect(e) >> 4 + + if x != 0x0F { + return m.SlideChannelVolume(ch, 1, float32(x)) + } + return nil +} + +func (e FineVolumeSlideUp) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_notecut.go b/format/s3m/channel/effect_notecut.go new file mode 100644 index 0000000..53eb5fa --- /dev/null +++ b/format/s3m/channel/effect_notecut.go @@ -0,0 +1,35 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NoteCut defines a note cut effect +type NoteCut ChannelCommand // 'SCx' + +func (e NoteCut) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NoteCut) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + if tick == 0 { + return nil + } + + x := DataEffect(e) & 0xf + + if tick == int(x) { + return m.ChannelStop(ch) + } + return nil +} + +func (e NoteCut) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_notedelay.go b/format/s3m/channel/effect_notedelay.go new file mode 100644 index 0000000..2a87cdf --- /dev/null +++ b/format/s3m/channel/effect_notedelay.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NoteDelay defines a note delay effect +type NoteDelay ChannelCommand // 'SDx' + +func (e NoteDelay) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e NoteDelay) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + tick := int(DataEffect(e) & 0x0F) + return m.SetChannelNoteAction(ch, note.ActionRetrigger, tick) +} + +func (e NoteDelay) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_orderjump.go b/format/s3m/channel/effect_orderjump.go new file mode 100644 index 0000000..67c503c --- /dev/null +++ b/format/s3m/channel/effect_orderjump.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// OrderJump defines an order jump effect +type OrderJump ChannelCommand // 'B' + +func (e OrderJump) String() string { + return fmt.Sprintf("B%0.2x", DataEffect(e)) +} + +func (e OrderJump) RowEnd(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + o := index.Order(e) + return m.SetOrder(o) +} + +func (e OrderJump) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_patterndelay.go b/format/s3m/channel/effect_patterndelay.go new file mode 100644 index 0000000..cbf0820 --- /dev/null +++ b/format/s3m/channel/effect_patterndelay.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PatternDelay defines a pattern delay effect +type PatternDelay ChannelCommand // 'SEx' + +func (e PatternDelay) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PatternDelay) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + times := int(DataEffect(e) & 0x0F) + return m.RowRepeat(times) +} + +func (e PatternDelay) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_patternloop.go b/format/s3m/channel/effect_patternloop.go new file mode 100644 index 0000000..3595560 --- /dev/null +++ b/format/s3m/channel/effect_patternloop.go @@ -0,0 +1,33 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PatternLoop defines a pattern loop effect +type PatternLoop ChannelCommand // 'SBx' + +func (e PatternLoop) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PatternLoop) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + x := DataEffect(e) & 0x0F + + if x == 0 { + m.SetPatternLoopStart(ch) + } else { + m.SetPatternLoops(ch, int(x)) + } + return nil +} + +func (e PatternLoop) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go new file mode 100644 index 0000000..008d6c0 --- /dev/null +++ b/format/s3m/channel/effect_portadown.go @@ -0,0 +1,41 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaDown defines a portamento down effect +type PortaDown ChannelCommand // 'E' + +func (e PortaDown) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e PortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.Porta(DataEffect(e)) + + if tick == 0 && !mem.Shared.AmigaSlides { + return nil + } + + var mul period.Delta = 4 + if mem.Shared.ST300Portas { + mul = 2 + } + return m.DoChannelPortaDown(ch, period.Delta(xx)*mul) +} + +func (e PortaDown) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go new file mode 100644 index 0000000..b9c7c1f --- /dev/null +++ b/format/s3m/channel/effect_portatonote.go @@ -0,0 +1,45 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaToNote defines a portamento-to-note effect +type PortaToNote ChannelCommand // 'G' + +func (e PortaToNote) String() string { + return fmt.Sprintf("G%0.2x", DataEffect(e)) +} + +func (e PortaToNote) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + return m.StartChannelPortaToNote(ch) +} + +func (e PortaToNote) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.Porta(DataEffect(e)) + + if tick == 0 && !mem.Shared.AmigaSlides { + return nil + } + + var mul period.Delta = 4 + if mem.Shared.ST300Portas { + mul = 2 + } + return m.DoChannelPortaToNote(ch, period.Delta(xx)*mul) +} + +func (e PortaToNote) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go new file mode 100644 index 0000000..c34aac6 --- /dev/null +++ b/format/s3m/channel/effect_portaup.go @@ -0,0 +1,41 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaUp defines a portamento up effect +type PortaUp ChannelCommand // 'F' + +func (e PortaUp) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e PortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.Porta(DataEffect(e)) + + if tick == 0 && !mem.Shared.AmigaSlides { + return nil + } + + var mul period.Delta = 4 + if mem.Shared.ST300Portas { + mul = 2 + } + return m.DoChannelPortaUp(ch, period.Delta(xx)*mul) +} + +func (e PortaUp) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_portavolslide.go b/format/s3m/channel/effect_portavolslide.go new file mode 100644 index 0000000..04b51af --- /dev/null +++ b/format/s3m/channel/effect_portavolslide.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/period" +) + +// PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect +type PortaVolumeSlide struct { // 'L' + playback.CombinedEffect[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning, *Memory, Data] +} + +// NewPortaVolumeSlide creates a new PortaVolumeSlide object +func NewPortaVolumeSlide(mem *Memory, cd uint8, val DataEffect) PortaVolumeSlide { + pvs := PortaVolumeSlide{} + vs := volumeSlideFactory(mem, cd, val) + pvs.Effects = append(pvs.Effects, vs, PortaToNote(0x00)) + return pvs +} + +func (e PortaVolumeSlide) String() string { + return fmt.Sprintf("L%0.2x", any(e.Effects[0]).(DataEffect)) +} + +func (e PortaVolumeSlide) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_retrigvolslide.go b/format/s3m/channel/effect_retrigvolslide.go new file mode 100644 index 0000000..f174bd7 --- /dev/null +++ b/format/s3m/channel/effect_retrigvolslide.go @@ -0,0 +1,73 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RetrigVolumeSlide defines a retriggering volume slide effect +type RetrigVolumeSlide ChannelCommand // 'Q' + +func (e RetrigVolumeSlide) String() string { + return fmt.Sprintf("Q%0.2x", DataEffect(e)) +} + +func (e RetrigVolumeSlide) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + x := DataEffect(e) >> 4 // vol slide instruction + y := DataEffect(e) & 0x0F // number of ticks between retriggers + + if (tick % int(y+1)) != 0 { + return nil + } + + if err := m.SetChannelNoteAction(ch, note.ActionRetrigger, tick); err != nil { + return err + } + + switch x { + case 0: // nothing + fallthrough + default: + + case 1: // -1 + return m.SlideChannelVolume(ch, 1, -1) + case 2: // -2 + return m.SlideChannelVolume(ch, 1, -2) + case 3: // -4 + return m.SlideChannelVolume(ch, 1, -4) + case 4: // -8 + return m.SlideChannelVolume(ch, 1, -8) + case 5: // -16 + return m.SlideChannelVolume(ch, 1, -16) + case 6: // * 2/3 + return m.SlideChannelVolume(ch, 2.0/3.0, 0) + case 7: // * 1/2 + return m.SlideChannelVolume(ch, 1.0/2.0, 0) + case 8: // ? + case 9: // +1 + return m.SlideChannelVolume(ch, 1, 1) + case 10: // +2 + return m.SlideChannelVolume(ch, 1, 2) + case 11: // +4 + return m.SlideChannelVolume(ch, 1, 4) + case 12: // +8 + return m.SlideChannelVolume(ch, 1, 8) + case 13: // +16 + return m.SlideChannelVolume(ch, 1, 16) + case 14: // * 3/2 + return m.SlideChannelVolume(ch, 3.0/2.0, 0) + case 15: // * 2 + return m.SlideChannelVolume(ch, 2, 0) + } + return nil +} + +func (e RetrigVolumeSlide) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_rowjump.go b/format/s3m/channel/effect_rowjump.go new file mode 100644 index 0000000..1940553 --- /dev/null +++ b/format/s3m/channel/effect_rowjump.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RowJump defines a row jump effect +type RowJump ChannelCommand // 'C' + +func (e RowJump) String() string { + return fmt.Sprintf("C%0.2x", DataEffect(e)) +} + +func (e RowJump) RowEnd(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + r := DataEffect(e) + rowIdx := index.Row((r >> 4) * 10) + rowIdx += index.Row(r & 0xf) + + return m.SetRow(rowIdx, true) +} + +func (e RowJump) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_sampleoffset.go b/format/s3m/channel/effect_sampleoffset.go new file mode 100644 index 0000000..1f40c43 --- /dev/null +++ b/format/s3m/channel/effect_sampleoffset.go @@ -0,0 +1,33 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/gomixing/sampling" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SampleOffset defines a sample offset effect +type SampleOffset ChannelCommand // 'O' + +func (e SampleOffset) String() string { + return fmt.Sprintf("O%0.2x", DataEffect(e)) +} + +func (e SampleOffset) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.SampleOffset(DataEffect(e)) + return m.SetChannelPos(ch, sampling.Pos{Pos: int(xx) * 0x100}) +} + +func (e SampleOffset) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_setfinetune.go b/format/s3m/channel/effect_setfinetune.go new file mode 100644 index 0000000..d2b3111 --- /dev/null +++ b/format/s3m/channel/effect_setfinetune.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetFinetune defines a mod-style set finetune effect +type SetFinetune ChannelCommand // 'S2x' + +func (e SetFinetune) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetFinetune) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + x := DataEffect(e) & 0xf + + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err + } + + if inst != nil { + inst.SetSampleRate(s3mPeriod.CalcFinetuneC4SampleRate(uint8(x))) + } + return nil +} + +func (e SetFinetune) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_setglobalvolume.go b/format/s3m/channel/effect_setglobalvolume.go new file mode 100644 index 0000000..aa59f08 --- /dev/null +++ b/format/s3m/channel/effect_setglobalvolume.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetGlobalVolume defines a set global volume effect +type SetGlobalVolume ChannelCommand // 'V' + +func (e SetGlobalVolume) String() string { + return fmt.Sprintf("V%0.2x", DataEffect(e)) +} + +func (e SetGlobalVolume) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + return m.SetGlobalVolume(s3mVolume.Volume(DataEffect(e))) +} + +func (e SetGlobalVolume) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_setpanposition.go b/format/s3m/channel/effect_setpanposition.go new file mode 100644 index 0000000..89d9294 --- /dev/null +++ b/format/s3m/channel/effect_setpanposition.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetPanPosition defines a set pan position effect +type SetPanPosition ChannelCommand // 'S8x' + +func (e SetPanPosition) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetPanPosition) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + return m.SetChannelPan(ch, s3mPanning.Panning(uint8(e)&0xf)) +} + +func (e SetPanPosition) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_setspeed.go b/format/s3m/channel/effect_setspeed.go new file mode 100644 index 0000000..8302ace --- /dev/null +++ b/format/s3m/channel/effect_setspeed.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetSpeed defines a set speed effect +type SetSpeed ChannelCommand // 'A' + +func (e SetSpeed) String() string { + return fmt.Sprintf("A%0.2x", DataEffect(e)) +} + +func (e SetSpeed) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + return m.SetTempo(int(e)) +} + +func (e SetSpeed) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_settempo.go b/format/s3m/channel/effect_settempo.go new file mode 100644 index 0000000..0247411 --- /dev/null +++ b/format/s3m/channel/effect_settempo.go @@ -0,0 +1,58 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetTempo defines a set tempo effect +type SetTempo ChannelCommand // 'T' + +func (e SetTempo) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) +} + +func (e SetTempo) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + switch DataEffect(e >> 4) { + case 0: // decrease tempo + if tick != 0 { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + val := int(mem.TempoDecrease(DataEffect(e & 0x0F))) + if err := m.SlideBPM(-val); err != nil { + return err + } + } + case 1: // increase tempo + if tick != 0 { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + val := int(mem.TempoIncrease(DataEffect(e & 0x0F))) + if err := m.SlideBPM(val); err != nil { + return err + } + } + default: + if tick == 0 { + if err := m.SetBPM(int(e)); err != nil { + return err + } + } + } + return nil +} + +func (e SetTempo) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_settremolowaveform.go b/format/s3m/channel/effect_settremolowaveform.go new file mode 100644 index 0000000..bffc57a --- /dev/null +++ b/format/s3m/channel/effect_settremolowaveform.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetTremoloWaveform defines a set tremolo waveform effect +type SetTremoloWaveform ChannelCommand // 'S4x' + +func (e SetTremoloWaveform) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetTremoloWaveform) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + x := DataEffect(e) & 0x0f + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorTremolo, oscillator.WaveTableSelect(x)) +} + +func (e SetTremoloWaveform) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_setvibratowaveform.go b/format/s3m/channel/effect_setvibratowaveform.go new file mode 100644 index 0000000..c8b5309 --- /dev/null +++ b/format/s3m/channel/effect_setvibratowaveform.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetVibratoWaveform defines a set vibrato waveform effect +type SetVibratoWaveform ChannelCommand // 'S3x' + +func (e SetVibratoWaveform) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SetVibratoWaveform) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + x := DataEffect(e) & 0x0f + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorVibrato, oscillator.WaveTableSelect(x)) +} + +func (e SetVibratoWaveform) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_stereocontrol.go b/format/s3m/channel/effect_stereocontrol.go new file mode 100644 index 0000000..c05329c --- /dev/null +++ b/format/s3m/channel/effect_stereocontrol.go @@ -0,0 +1,32 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// StereoControl defines a set stereo control effect +type StereoControl ChannelCommand // 'SAx' + +func (e StereoControl) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e StereoControl) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + x := uint8(e) & 0xf + + if x > 7 { + return m.SetChannelPan(ch, s3mPanning.Panning(x-8)) + } else { + return m.SetChannelPan(ch, s3mPanning.Panning(x+8)) + } +} + +func (e StereoControl) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_surroundon.go b/format/s3m/channel/effect_surroundon.go new file mode 100644 index 0000000..8fe638d --- /dev/null +++ b/format/s3m/channel/effect_surroundon.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SurroundOn defines a set surround on effect +type SurroundOn ChannelCommand // 'S91' + +func (e SurroundOn) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e SurroundOn) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + // TODO: support for surround function + return nil +} + +func (e SurroundOn) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_tremolo.go b/format/s3m/channel/effect_tremolo.go new file mode 100644 index 0000000..90e6297 --- /dev/null +++ b/format/s3m/channel/effect_tremolo.go @@ -0,0 +1,38 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/types" +) + +// Tremolo defines a tremolo effect +type Tremolo ChannelCommand // 'R' + +func (e Tremolo) String() string { + return fmt.Sprintf("R%0.2x", DataEffect(e)) +} + +func (e Tremolo) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.Tremolo(DataEffect(e)) + // NOTE: JBC - S3M does not update on tick 0, but MOD does. + if tick != 0 || mem.Shared.ModCompatibility { + return withOscillatorDo(ch, m, int(x), float32(y)*4, machine.OscillatorTremolo, func(value float32) error { + return m.SetChannelVolumeDelta(ch, types.VolumeDelta(value)) + }) + } + return nil +} + +func (e Tremolo) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_tremor.go b/format/s3m/channel/effect_tremor.go new file mode 100644 index 0000000..54a8982 --- /dev/null +++ b/format/s3m/channel/effect_tremor.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Tremor defines a tremor effect +type Tremor ChannelCommand // 'I' + +func (e Tremor) String() string { + return fmt.Sprintf("I%0.2x", DataEffect(e)) +} + +func (e Tremor) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.LastNonZeroXY(DataEffect(e)) + return doTremor(ch, m, int(x)+1, int(y)+1) +} + +func (e Tremor) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_vibrato.go b/format/s3m/channel/effect_vibrato.go new file mode 100644 index 0000000..5d42914 --- /dev/null +++ b/format/s3m/channel/effect_vibrato.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Vibrato defines a vibrato effect +type Vibrato ChannelCommand // 'H' + +func (e Vibrato) String() string { + return fmt.Sprintf("H%0.2x", DataEffect(e)) +} + +func (e Vibrato) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x, y := mem.Vibrato(DataEffect(e)) + // NOTE: JBC - S3M does not update on tick 0, but MOD does. + if tick != 0 || mem.Shared.ModCompatibility { + return withOscillatorDo(ch, m, int(x), float32(y)*4, machine.OscillatorVibrato, func(value float32) error { + return m.SetChannelPeriodDelta(ch, period.Delta(value)) + }) + } + return nil +} + +func (e Vibrato) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_vibratovolslide.go b/format/s3m/channel/effect_vibratovolslide.go new file mode 100644 index 0000000..8a9e377 --- /dev/null +++ b/format/s3m/channel/effect_vibratovolslide.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/period" +) + +// VibratoVolumeSlide defines a combination vibrato and volume slide effect +type VibratoVolumeSlide struct { // 'K' + playback.CombinedEffect[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning, *Memory, Data] +} + +// NewVibratoVolumeSlide creates a new VibratoVolumeSlide object +func NewVibratoVolumeSlide(mem *Memory, cd uint8, val DataEffect) VibratoVolumeSlide { + vvs := VibratoVolumeSlide{} + vs := volumeSlideFactory(mem, cd, val) + vvs.Effects = append(vvs.Effects, vs, Vibrato(0x00)) + return vvs +} + +func (e VibratoVolumeSlide) String() string { + return fmt.Sprintf("K%0.2x", any(e.Effects[0]).(DataEffect)) +} + +func (e VibratoVolumeSlide) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_volslidedown.go b/format/s3m/channel/effect_volslidedown.go new file mode 100644 index 0000000..dc47120 --- /dev/null +++ b/format/s3m/channel/effect_volslidedown.go @@ -0,0 +1,34 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeSlideDown defines a volume slide down effect +type VolumeSlideDown ChannelCommand // 'D0y' + +func (e VolumeSlideDown) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e VolumeSlideDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := DataEffect(e) & 0x0F + if mem.Shared.VolSlideEveryTick || tick != 0 { + return m.SlideChannelVolume(ch, 1, -float32(y)) + } + return nil +} + +func (e VolumeSlideDown) TraceData() string { + return e.String() +} diff --git a/format/s3m/channel/effect_volslideup.go b/format/s3m/channel/effect_volslideup.go new file mode 100644 index 0000000..77fa4d6 --- /dev/null +++ b/format/s3m/channel/effect_volslideup.go @@ -0,0 +1,34 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeSlideUp defines a volume slide up effect +type VolumeSlideUp ChannelCommand // 'Dx0' + +func (e VolumeSlideUp) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e VolumeSlideUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + x := DataEffect(e) >> 4 + if mem.Shared.VolSlideEveryTick || tick != 0 { + return m.SlideChannelVolume(ch, 1, float32(x)) + } + return nil +} + +func (e VolumeSlideUp) TraceData() string { + return e.String() +} diff --git a/format/s3m/effect/effectfactory.go b/format/s3m/channel/effectfactory.go similarity index 61% rename from format/s3m/effect/effectfactory.go rename to format/s3m/channel/effectfactory.go index fb93093..66fbd5e 100644 --- a/format/s3m/effect/effectfactory.go +++ b/format/s3m/channel/effectfactory.go @@ -1,94 +1,94 @@ -package effect +package channel import ( "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/song" ) -type EffectS3M interface { - playback.Effect -} - -type ChannelCommand channel.DataEffect +type ChannelCommand DataEffect // Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data *channel.Data) EffectS3M { +func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) playback.Effect { if data == nil { return nil } - if !data.What.HasCommand() { + d, _ := data.(Data) + if !d.What.HasCommand() { return nil } - mem.LastNonZero(data.Info) - switch data.Command + '@' { + // Store the last non-zero value + _ = mem.LastNonZero(d.Info) + + switch d.Command + '@' { case '@': // unused return nil case 'A': // Set Speed - return SetSpeed(data.Info) + return SetSpeed(d.Info) case 'B': // Pattern Jump - return OrderJump(data.Info) + return OrderJump(d.Info) case 'C': // Pattern Break - return RowJump(data.Info) + return RowJump(d.Info) case 'D': // Volume Slide / Fine Volume Slide - return volumeSlideFactory(mem, data.Command, data.Info) + return volumeSlideFactory(mem, d.Command, d.Info) case 'E': // Porta Down/Fine Porta Down/Xtra Fine Porta - xx := mem.LastNonZero(data.Info) + xx := mem.Porta(d.Info) x := xx >> 4 if x == 0x0F { return FinePortaDown(xx) } else if x == 0x0E { return ExtraFinePortaDown(xx) } - return PortaDown(data.Info) + return PortaDown(d.Info) case 'F': // Porta Up/Fine Porta Up/Extra Fine Porta Down - xx := mem.LastNonZero(data.Info) + xx := mem.Porta(d.Info) x := xx >> 4 if x == 0x0F { return FinePortaUp(xx) } else if x == 0x0E { return ExtraFinePortaUp(xx) } - return PortaUp(data.Info) + return PortaUp(d.Info) case 'G': // Porta to note - return PortaToNote(data.Info) + return PortaToNote(d.Info) case 'H': // Vibrato - return Vibrato(data.Info) + return Vibrato(d.Info) case 'I': // Tremor - return Tremor(data.Info) + return Tremor(d.Info) case 'J': // Arpeggio - return Arpeggio(data.Info) + return Arpeggio(d.Info) case 'K': // Vibrato+Volume Slide - return NewVibratoVolumeSlide(mem, data.Command, data.Info) + return NewVibratoVolumeSlide(mem, d.Command, d.Info) case 'L': // Porta+Volume Slide - return NewPortaVolumeSlide(mem, data.Command, data.Info) + return NewPortaVolumeSlide(mem, d.Command, d.Info) case 'M': // unused return nil case 'N': // unused return nil case 'O': // Sample Offset - return SampleOffset(data.Info) + return SampleOffset(d.Info) case 'P': // unused return nil case 'Q': // Retrig + Volume Slide - return RetrigVolumeSlide(data.Info) + return RetrigVolumeSlide(d.Info) case 'R': // Tremolo - return Tremolo(data.Info) + return Tremolo(d.Info) case 'S': // Special - return specialEffect(mem, data) + return specialEffect(mem, d) case 'T': // Set Tempo - return SetTempo(data.Info) + return SetTempo(d.Info) case 'U': // Fine Vibrato - return FineVibrato(data.Info) + return FineVibrato(d.Info) case 'V': // Global Volume - return SetGlobalVolume(data.Info) + return SetGlobalVolume(d.Info) default: } - return UnhandledCommand{Command: data.Command, Info: data.Info} + return UnhandledCommand{Command: d.Command, Info: d.Info} } -func specialEffect(mem *channel.Memory, data *channel.Data) EffectS3M { +func specialEffect(mem *Memory, data Data) playback.Effect { var cmd = mem.LastNonZero(data.Info) switch cmd >> 4 { case 0x0: // Set Filter on/off @@ -109,6 +109,8 @@ func specialEffect(mem *channel.Memory, data *channel.Data) EffectS3M { return nil case 0x8: // Set Pan Position return SetPanPosition(data.Info) + case 0x9: // Sound Control + return soundControlEffect(data) case 0xA: // Stereo Control return StereoControl(data.Info) case 0xB: // Pattern Loop @@ -125,10 +127,10 @@ func specialEffect(mem *channel.Memory, data *channel.Data) EffectS3M { return UnhandledCommand{Command: data.Command, Info: data.Info} } -func volumeSlideFactory(mem *channel.Memory, cd uint8, ce channel.DataEffect) EffectS3M { +func volumeSlideFactory(mem *Memory, cd uint8, ce DataEffect) playback.Effect { xy := mem.LastNonZero(ce) - x := channel.DataEffect(xy >> 4) - y := channel.DataEffect(xy & 0x0F) + x := DataEffect(xy >> 4) + y := DataEffect(xy & 0x0F) switch { case x == 0: return VolumeSlideDown(xy) @@ -147,3 +149,21 @@ func volumeSlideFactory(mem *channel.Memory, cd uint8, ce channel.DataEffect) Ef return VolumeSlideDown(xy) //return UnhandledCommand{Command: cd, Info: xy} } + +func soundControlEffect(data Data) playback.Effect { + switch data.Info & 0xF { + case 0x0: // Surround Off + case 0x1: // Surround On + // only S91 is supported directly by S3M + return SurroundOn(data.Info) + case 0x8: // Reverb Off + case 0x9: // Reverb On + case 0xA: // Center Surround + case 0xB: // Quad Surround + case 0xC: // Global Filters + case 0xD: // Local Filters + case 0xE: // Play Forward + case 0xF: // Play Backward + } + return UnhandledCommand{Command: data.Command, Info: data.Info} +} diff --git a/format/s3m/channel/instid.go b/format/s3m/channel/instid.go index 9a9b079..cc55a47 100644 --- a/format/s3m/channel/instid.go +++ b/format/s3m/channel/instid.go @@ -1,6 +1,8 @@ package channel -import "fmt" +import ( + "fmt" +) // InstID is an instrument ID in S3M world type InstID uint8 @@ -10,6 +12,11 @@ func (s InstID) IsEmpty() bool { return s == 0 } +func (s InstID) GetIndexAndSample() (int, int) { + idx := int(s) - 1 + return idx, idx +} + func (s InstID) String() string { return fmt.Sprint(uint8(s)) } diff --git a/format/s3m/channel/machine.go b/format/s3m/channel/machine.go new file mode 100644 index 0000000..d9145fa --- /dev/null +++ b/format/s3m/channel/machine.go @@ -0,0 +1,51 @@ +package channel + +import ( + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +func withOscillatorDo(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], speed int, depth float32, osc machine.Oscillator, fn func(value float32) error) error { + value, err := m.GetNextChannelWavetableValue(ch, speed, depth, machine.OscillatorVibrato) + if err != nil { + return err + } + + return fn(value) +} + +func doArpeggio(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], tick int, arpSemitoneADelta, arpSemitoneBDelta int8) error { + switch tick % 3 { + case 0: + fallthrough + default: + return m.DoChannelArpeggio(ch, 0) + case 1: + return m.DoChannelArpeggio(ch, arpSemitoneADelta) + case 2: + return m.DoChannelArpeggio(ch, arpSemitoneBDelta) + } +} + +func doTremor(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], onTicks int, offTicks int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + tremor := mem.TremorMem() + if tremor.IsActive() { + if tremor.Advance() >= onTicks { + tremor.ToggleAndReset() + } + } else { + if tremor.Advance() >= offTicks { + tremor.ToggleAndReset() + } + } + + return m.SetChannelVolumeActive(ch, tremor.IsActive()) +} diff --git a/format/s3m/channel/memory.go b/format/s3m/channel/memory.go index 676fb91..f80aa06 100644 --- a/format/s3m/channel/memory.go +++ b/format/s3m/channel/memory.go @@ -1,17 +1,13 @@ package channel import ( - "github.com/gotracker/playback/voice/oscillator" - "github.com/gotracker/playback/memory" - oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/tremor" - formatutil "github.com/gotracker/playback/util" ) // Memory is the storage object for custom effect/command values type Memory struct { - portaToNote memory.Value[DataEffect] + porta memory.Value[DataEffect] vibratoSpeed memory.Value[DataEffect] vibratoDepth memory.Value[DataEffect] tremoloSpeed memory.Value[DataEffect] @@ -21,23 +17,14 @@ type Memory struct { tempoIncrease memory.Value[DataEffect] lastNonZero memory.Value[DataEffect] - tremorMem tremor.Tremor - vibratoOscillator oscillator.Oscillator - tremoloOscillator oscillator.Oscillator - patternLoop formatutil.PatternLoop + tremorMem tremor.Tremor Shared *SharedMemory } -// ResetOscillators resets the oscillators to defaults -func (m *Memory) ResetOscillators() { - m.vibratoOscillator = oscillatorImpl.NewProtrackerOscillator() - m.tremoloOscillator = oscillatorImpl.NewProtrackerOscillator() -} - -// PortaToNote gets or sets the most recent non-zero value (or input) for Portamento-to-note -func (m *Memory) PortaToNote(input DataEffect) DataEffect { - return m.portaToNote.Coalesce(input) +// Porta gets or sets the most recent non-zero value (or input) for any Portamento command +func (m *Memory) Porta(input DataEffect) DataEffect { + return m.porta.Coalesce(input) } // Vibrato gets or sets the most recent non-zero value (or input) for Vibrato @@ -87,32 +74,14 @@ func (m *Memory) TremorMem() *tremor.Tremor { return &m.tremorMem } -// VibratoOscillator returns the Vibrato oscillator object -func (m *Memory) VibratoOscillator() oscillator.Oscillator { - return m.vibratoOscillator -} - -// TremoloOscillator returns the Tremolo oscillator object -func (m *Memory) TremoloOscillator() oscillator.Oscillator { - return m.tremoloOscillator -} - -// Retrigger runs certain operations when a note is retriggered +// Retrigger is called when a voice is triggered func (m *Memory) Retrigger() { - for _, osc := range []oscillator.Oscillator{m.VibratoOscillator(), m.TremoloOscillator()} { - osc.Reset() - } -} - -// GetPatternLoop returns the pattern loop object from the memory -func (m *Memory) GetPatternLoop() *formatutil.PatternLoop { - return &m.patternLoop } // StartOrder is called when the first order's row at tick 0 is started -func (m *Memory) StartOrder() { +func (m *Memory) StartOrder0() { if m.Shared.ResetMemoryAtStartOfOrder0 { - m.portaToNote.Reset() + m.porta.Reset() m.vibratoSpeed.Reset() m.vibratoDepth.Reset() m.tremoloSpeed.Reset() diff --git a/format/s3m/channel/sharedmem.go b/format/s3m/channel/sharedmem.go index fdc4f8f..e6e9874 100644 --- a/format/s3m/channel/sharedmem.go +++ b/format/s3m/channel/sharedmem.go @@ -1,7 +1,8 @@ package channel type SharedMemory struct { - VolSlideEveryFrame bool + VolSlideEveryTick bool + ST300Portas bool LowPassFilterEnable bool // ResetMemoryAtStartOfOrder0 if true will reset the memory registers when the first tick of the first row of the first order pattern plays ResetMemoryAtStartOfOrder0 bool diff --git a/format/s3m/channel/unhandled.go b/format/s3m/channel/unhandled.go new file mode 100644 index 0000000..db233a2 --- /dev/null +++ b/format/s3m/channel/unhandled.go @@ -0,0 +1,38 @@ +package channel + +import ( + "fmt" + + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// UnhandledCommand is an unhandled command +type UnhandledCommand struct { + Command uint8 + Info DataEffect +} + +func (e UnhandledCommand) String() string { + return fmt.Sprintf("%c%0.2x", e.Command+'@', e.Info) +} + +func (e UnhandledCommand) Names() []string { + return []string{ + fmt.Sprintf("UnhandledCommand(%s)", e.String()), + } +} + +func (e UnhandledCommand) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + if !m.IgnoreUnknownEffect() { + panic("unhandled command") + } + return nil +} + +func (e UnhandledCommand) TraceData() string { + return e.String() +} diff --git a/format/s3m/note/note.go b/format/s3m/channel/util.go similarity index 96% rename from format/s3m/note/note.go rename to format/s3m/channel/util.go index a3815e0..9309a43 100644 --- a/format/s3m/note/note.go +++ b/format/s3m/channel/util.go @@ -1,7 +1,8 @@ -package note +package channel import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + "github.com/gotracker/playback/note" ) diff --git a/format/s3m/effect/effect_arpeggio.go b/format/s3m/effect/effect_arpeggio.go deleted file mode 100644 index f70136c..0000000 --- a/format/s3m/effect/effect_arpeggio.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// Arpeggio defines an arpeggio effect -type Arpeggio ChannelCommand // 'J' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - cs.SetPos(cs.GetTargetPos()) - return nil -} - -// Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.LastNonZeroXY(channel.DataEffect(e)) - return doArpeggio(cs, currentTick, int8(x), int8(y)) -} - -func (e Arpeggio) String() string { - return fmt.Sprintf("J%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_enablefilter.go b/format/s3m/effect/effect_enablefilter.go deleted file mode 100644 index 5933722..0000000 --- a/format/s3m/effect/effect_enablefilter.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" -) - -// EnableFilter defines a set filter enable effect -type EnableFilter ChannelCommand // 'S0x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e EnableFilter) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - on := x != 0 - - pb := p.(effectIntf.S3M) - pb.SetFilterEnable(on) - return nil -} - -func (e EnableFilter) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_extrafineportadown.go b/format/s3m/effect/effect_extrafineportadown.go deleted file mode 100644 index f83af89..0000000 --- a/format/s3m/effect/effect_extrafineportadown.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// ExtraFinePortaDown defines an extra-fine portamento down effect -type ExtraFinePortaDown ChannelCommand // 'EEx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - y := channel.DataEffect(e) & 0x0F - - return doPortaDown(cs, float32(y), 1) -} - -func (e ExtraFinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_extrafineportaup.go b/format/s3m/effect/effect_extrafineportaup.go deleted file mode 100644 index d42a3b7..0000000 --- a/format/s3m/effect/effect_extrafineportaup.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// ExtraFinePortaUp defines an extra-fine portamento up effect -type ExtraFinePortaUp ChannelCommand // 'FEx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - y := channel.DataEffect(e) & 0x0F - - return doPortaUp(cs, float32(y), 1) -} - -func (e ExtraFinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_finepatterndelay.go b/format/s3m/effect/effect_finepatterndelay.go deleted file mode 100644 index ad80702..0000000 --- a/format/s3m/effect/effect_finepatterndelay.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" -) - -// FinePatternDelay defines an fine pattern delay effect -type FinePatternDelay ChannelCommand // 'S6x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePatternDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - m := p.(effectIntf.S3M) - return m.AddRowTicks(int(x)) -} - -func (e FinePatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_fineportadown.go b/format/s3m/effect/effect_fineportadown.go deleted file mode 100644 index 00e965f..0000000 --- a/format/s3m/effect/effect_fineportadown.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// FinePortaDown defines an fine portamento down effect -type FinePortaDown ChannelCommand // 'EFx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - y := channel.DataEffect(e) & 0x0F - - return doPortaDown(cs, float32(y), 4) -} - -func (e FinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_fineportaup.go b/format/s3m/effect/effect_fineportaup.go deleted file mode 100644 index 1735c4b..0000000 --- a/format/s3m/effect/effect_fineportaup.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// FinePortaUp defines an fine portamento up effect -type FinePortaUp ChannelCommand // 'FFx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - y := channel.DataEffect(e) & 0x0F - - return doPortaUp(cs, float32(y), 4) -} - -func (e FinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_finevibrato.go b/format/s3m/effect/effect_finevibrato.go deleted file mode 100644 index 2f0223d..0000000 --- a/format/s3m/effect/effect_finevibrato.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// FineVibrato defines an fine vibrato effect -type FineVibrato ChannelCommand // 'U' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVibrato) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e FineVibrato) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) - // NOTE: JBC - S3M does not update on tick 0, but MOD does. - if currentTick != 0 || mem.Shared.ModCompatibility { - return doVibrato(cs, currentTick, x, y, 1) - } - return nil -} - -func (e FineVibrato) String() string { - return fmt.Sprintf("U%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_finevolslidedown.go b/format/s3m/effect/effect_finevolslidedown.go deleted file mode 100644 index e72e582..0000000 --- a/format/s3m/effect/effect_finevolslidedown.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// FineVolumeSlideDown defines a fine volume slide down effect -type FineVolumeSlideDown ChannelCommand // 'DFy' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e FineVolumeSlideDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - y := channel.DataEffect(e) & 0x0F - - if y != 0x0F && currentTick == 0 { - return doVolSlide(cs, -float32(y), 1.0) - } - return nil -} - -func (e FineVolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_finevolslideup.go b/format/s3m/effect/effect_finevolslideup.go deleted file mode 100644 index db37718..0000000 --- a/format/s3m/effect/effect_finevolslideup.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// FineVolumeSlideUp defines a fine volume slide up effect -type FineVolumeSlideUp ChannelCommand // 'DxF' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e FineVolumeSlideUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) >> 4 - - if x != 0x0F && currentTick == 0 { - return doVolSlide(cs, float32(x), 1.0) - } - return nil -} - -func (e FineVolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_notecut.go b/format/s3m/effect/effect_notecut.go deleted file mode 100644 index 0ab9c05..0000000 --- a/format/s3m/effect/effect_notecut.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// NoteCut defines a note cut effect -type NoteCut ChannelCommand // 'SCx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e NoteCut) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.FreezePlayback() - } - return nil -} - -func (e NoteCut) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_notedelay.go b/format/s3m/effect/effect_notedelay.go deleted file mode 100644 index 4b595a7..0000000 --- a/format/s3m/effect/effect_notedelay.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/note" -) - -// NoteDelay defines a note delay effect -type NoteDelay ChannelCommand // 'SDx' - -// PreStart triggers when the effect enters onto the channel state -func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(channel.DataEffect(e)&0x0F)) - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e NoteDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_orderjump.go b/format/s3m/effect/effect_orderjump.go deleted file mode 100644 index 58168f2..0000000 --- a/format/s3m/effect/effect_orderjump.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/index" -) - -// OrderJump defines an order jump effect -type OrderJump ChannelCommand // 'B' - -// Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e OrderJump) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - return p.SetNextOrder(index.Order(e)) -} - -func (e OrderJump) String() string { - return fmt.Sprintf("B%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_patterndelay.go b/format/s3m/effect/effect_patterndelay.go deleted file mode 100644 index 3ebf8da..0000000 --- a/format/s3m/effect/effect_patterndelay.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" -) - -// PatternDelay defines a pattern delay effect -type PatternDelay ChannelCommand // 'SEx' - -// PreStart triggers when the effect enters onto the channel state -func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - m := p.(effectIntf.S3M) - return m.SetPatternDelay(int(channel.DataEffect(e) & 0x0F)) -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e PatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_patternloop.go b/format/s3m/effect/effect_patternloop.go deleted file mode 100644 index e0c5ade..0000000 --- a/format/s3m/effect/effect_patternloop.go +++ /dev/null @@ -1,44 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// PatternLoop defines a pattern loop effect -type PatternLoop ChannelCommand // 'SBx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e PatternLoop) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - x := channel.DataEffect(e) & 0xF - - mem := cs.GetMemory() - pl := mem.GetPatternLoop() - if x == 0 { - // set loop - pl.Start = p.GetCurrentRow() - } else { - if !pl.Enabled { - pl.Enabled = true - pl.Total = uint8(x) - pl.End = p.GetCurrentRow() - pl.Count = 0 - } - if row, ok := pl.ContinueLoop(p.GetCurrentRow()); ok { - return p.SetNextRowWithBacktrack(row, true) - } - } - return nil -} - -func (e PatternLoop) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_portadown.go b/format/s3m/effect/effect_portadown.go deleted file mode 100644 index da0b32a..0000000 --- a/format/s3m/effect/effect_portadown.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// PortaDown defines a portamento down effect -type PortaDown ChannelCommand // 'E' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.LastNonZero(channel.DataEffect(e)) - - if currentTick != 0 { - return doPortaDown(cs, float32(xx), 4) - } - return nil -} - -func (e PortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_portatonote.go b/format/s3m/effect/effect_portatonote.go deleted file mode 100644 index 37061b5..0000000 --- a/format/s3m/effect/effect_portatonote.go +++ /dev/null @@ -1,51 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/heucuva/comparison" -) - -// PortaToNote defines a portamento-to-note effect -type PortaToNote ChannelCommand // 'G' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) - cs.SetNotePlayTick(false, note.ActionContinue, 0) - } - return nil -} - -// Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaToNote(channel.DataEffect(e)) - - // vibrato modifies current period for portamento - cur := cs.GetPeriod() - if cur == nil { - return nil - } - cur = cur.AddDelta(cs.GetPeriodDelta()) - ptp := cs.GetPortaTargetPeriod() - if currentTick != 0 { - if period.ComparePeriods(cur, ptp) == comparison.SpaceshipRightGreater { - return doPortaUpToNote(cs, float32(xx), 4, ptp) // subtracts - } else { - return doPortaDownToNote(cs, float32(xx), 4, ptp) // adds - } - } - return nil -} - -func (e PortaToNote) String() string { - return fmt.Sprintf("G%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_portaup.go b/format/s3m/effect/effect_portaup.go deleted file mode 100644 index b214bd4..0000000 --- a/format/s3m/effect/effect_portaup.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// PortaUp defines a portamento up effect -type PortaUp ChannelCommand // 'F' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.LastNonZero(channel.DataEffect(e)) - - if currentTick != 0 { - return doPortaUp(cs, float32(xx), 4) - } - return nil -} - -func (e PortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_portavolslide.go b/format/s3m/effect/effect_portavolslide.go deleted file mode 100644 index f7dff38..0000000 --- a/format/s3m/effect/effect_portavolslide.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect -type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[channel.Memory, channel.Data] -} - -// NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide(mem *channel.Memory, cd uint8, val channel.DataEffect) PortaVolumeSlide { - pvs := PortaVolumeSlide{} - vs := volumeSlideFactory(mem, cd, val) - pvs.Effects = append(pvs.Effects, vs, PortaToNote(0x00)) - return pvs -} - -func (e PortaVolumeSlide) String() string { - return fmt.Sprintf("L%0.2x", e.Effects[0].(channel.DataEffect)) -} diff --git a/format/s3m/effect/effect_retrigvolslide.go b/format/s3m/effect/effect_retrigvolslide.go deleted file mode 100644 index dea8389..0000000 --- a/format/s3m/effect/effect_retrigvolslide.go +++ /dev/null @@ -1,71 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide ChannelCommand // 'Q' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) >> 4 - y := channel.DataEffect(e) & 0x0F - if y == 0 { - return nil - } - - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= x { - cs.SetPos(sampling.Pos{}) - cs.ResetRetriggerCount() - switch x { - case 1: - return doVolSlide(cs, -1, 1) - case 2: - return doVolSlide(cs, -2, 1) - case 3: - return doVolSlide(cs, -4, 1) - case 4: - return doVolSlide(cs, -8, 1) - case 5: - return doVolSlide(cs, -6, 1) - case 6: - return doVolSlideTwoThirds(cs) - case 7: - return doVolSlide(cs, 0, float32(0.5)) - case 8: // ? - case 9: - return doVolSlide(cs, 1, 1) - case 10: - return doVolSlide(cs, 2, 1) - case 11: - return doVolSlide(cs, 4, 1) - case 12: - return doVolSlide(cs, 8, 1) - case 13: - return doVolSlide(cs, 16, 1) - case 14: - return doVolSlide(cs, 0, float32(1.5)) - case 15: - return doVolSlide(cs, 0, 2) - } - } - return nil -} - -func (e RetrigVolumeSlide) String() string { - return fmt.Sprintf("Q%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_rowjump.go b/format/s3m/effect/effect_rowjump.go deleted file mode 100644 index 5e5669d..0000000 --- a/format/s3m/effect/effect_rowjump.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/index" -) - -// RowJump defines a row jump effect -type RowJump ChannelCommand // 'C' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RowJump) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e RowJump) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - r := channel.DataEffect(e) - rowIdx := index.Row((r >> 4) * 10) - rowIdx += index.Row(r & 0xf) - return p.SetNextRow(rowIdx) -} - -func (e RowJump) String() string { - return fmt.Sprintf("C%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_sampleoffset.go b/format/s3m/effect/effect_sampleoffset.go deleted file mode 100644 index 66e745a..0000000 --- a/format/s3m/effect/effect_sampleoffset.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// SampleOffset defines a sample offset effect -type SampleOffset ChannelCommand // 'O' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() - xx := mem.SampleOffset(channel.DataEffect(e)) - cs.SetTargetPos(sampling.Pos{Pos: int(xx) * 0x100}) - return nil -} - -func (e SampleOffset) String() string { - return fmt.Sprintf("O%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_setfinetune.go b/format/s3m/effect/effect_setfinetune.go deleted file mode 100644 index 01c85ce..0000000 --- a/format/s3m/effect/effect_setfinetune.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/note" -) - -// SetFinetune defines a mod-style set finetune effect -type SetFinetune ChannelCommand // 'S2x' - -// PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - x := channel.DataEffect(e) & 0xf - - inst := cs.GetTargetInst() - if inst != nil { - ft := (note.Finetune(x) - 8) * 4 - inst.SetFinetune(ft) - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetFinetune) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_setglobalvolume.go b/format/s3m/effect/effect_setglobalvolume.go deleted file mode 100644 index ec7f4d4..0000000 --- a/format/s3m/effect/effect_setglobalvolume.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" -) - -// SetGlobalVolume defines a set global volume effect -type SetGlobalVolume ChannelCommand // 'V' - -// PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - p.SetGlobalVolume(s3mVolume.VolumeFromS3M(s3mfile.Volume(channel.DataEffect(e)))) - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetGlobalVolume) String() string { - return fmt.Sprintf("V%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_setpanposition.go b/format/s3m/effect/effect_setpanposition.go deleted file mode 100644 index e276d9d..0000000 --- a/format/s3m/effect/effect_setpanposition.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - s3mPanning "github.com/gotracker/playback/format/s3m/panning" -) - -// SetPanPosition defines a set pan position effect -type SetPanPosition ChannelCommand // 'S8x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := uint8(e) & 0xf - - cs.SetPan(s3mPanning.PanningFromS3M(x)) - return nil -} - -func (e SetPanPosition) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_setspeed.go b/format/s3m/effect/effect_setspeed.go deleted file mode 100644 index cc43bf5..0000000 --- a/format/s3m/effect/effect_setspeed.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" -) - -// SetSpeed defines a set speed effect -type SetSpeed ChannelCommand // 'A' - -// PreStart triggers when the effect enters onto the channel state -func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - if e != 0 { - m := p.(effectIntf.S3M) - if err := m.SetTicks(int(e)); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetSpeed) String() string { - return fmt.Sprintf("A%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_settempo.go b/format/s3m/effect/effect_settempo.go deleted file mode 100644 index 9567759..0000000 --- a/format/s3m/effect/effect_settempo.go +++ /dev/null @@ -1,61 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" -) - -// SetTempo defines a set tempo effect -type SetTempo ChannelCommand // 'T' - -// PreStart triggers when the effect enters onto the channel state -func (e SetTempo) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - if e > 0x20 { - m := p.(effectIntf.S3M) - if err := m.SetTempo(int(e)); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e SetTempo) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - m := p.(effectIntf.S3M) - switch channel.DataEffect(e >> 4) { - case 0: // decrease tempo - if currentTick != 0 { - mem := cs.GetMemory() - val := int(mem.TempoDecrease(channel.DataEffect(e & 0x0F))) - if err := m.DecreaseTempo(val); err != nil { - return err - } - } - case 1: // increase tempo - if currentTick != 0 { - mem := cs.GetMemory() - val := int(mem.TempoIncrease(channel.DataEffect(e & 0x0F))) - if err := m.IncreaseTempo(val); err != nil { - return err - } - } - default: - if err := m.SetTempo(int(e)); err != nil { - return err - } - } - return nil -} - -func (e SetTempo) String() string { - return fmt.Sprintf("T%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_settremolowaveform.go b/format/s3m/effect/effect_settremolowaveform.go deleted file mode 100644 index 383ec80..0000000 --- a/format/s3m/effect/effect_settremolowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// SetTremoloWaveform defines a set tremolo waveform effect -type SetTremoloWaveform ChannelCommand // 'S4x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - trem := mem.TremoloOscillator() - trem.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetTremoloWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_setvibratowaveform.go b/format/s3m/effect/effect_setvibratowaveform.go deleted file mode 100644 index 113f523..0000000 --- a/format/s3m/effect/effect_setvibratowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// SetVibratoWaveform defines a set vibrato waveform effect -type SetVibratoWaveform ChannelCommand // 'S3x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - vib := mem.VibratoOscillator() - vib.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetVibratoWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_stereocontrol.go b/format/s3m/effect/effect_stereocontrol.go deleted file mode 100644 index e59f5fa..0000000 --- a/format/s3m/effect/effect_stereocontrol.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - s3mPanning "github.com/gotracker/playback/format/s3m/panning" -) - -// StereoControl defines a set stereo control effect -type StereoControl ChannelCommand // 'SAx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e StereoControl) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := uint8(e) & 0xf - - if x > 7 { - cs.SetPan(s3mPanning.PanningFromS3M(x - 8)) - } else { - cs.SetPan(s3mPanning.PanningFromS3M(x + 8)) - } - return nil -} - -func (e StereoControl) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_tremolo.go b/format/s3m/effect/effect_tremolo.go deleted file mode 100644 index 866e4d9..0000000 --- a/format/s3m/effect/effect_tremolo.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// Tremolo defines a tremolo effect -type Tremolo ChannelCommand // 'R' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremolo) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Tremolo(channel.DataEffect(e)) - // NOTE: JBC - S3M does not update on tick 0, but MOD does. - if currentTick != 0 || mem.Shared.ModCompatibility { - return doTremolo(cs, currentTick, channel.DataEffect(x), channel.DataEffect(y), 4) - } - return nil -} - -func (e Tremolo) String() string { - return fmt.Sprintf("R%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_tremor.go b/format/s3m/effect/effect_tremor.go deleted file mode 100644 index 51f0bc8..0000000 --- a/format/s3m/effect/effect_tremor.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// Tremor defines a tremor effect -type Tremor ChannelCommand // 'I' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremor) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.LastNonZeroXY(channel.DataEffect(e)) - return doTremor(cs, currentTick, int(x)+1, int(y)+1) -} - -func (e Tremor) String() string { - return fmt.Sprintf("I%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_vibrato.go b/format/s3m/effect/effect_vibrato.go deleted file mode 100644 index ace0713..0000000 --- a/format/s3m/effect/effect_vibrato.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// Vibrato defines a vibrato effect -type Vibrato ChannelCommand // 'H' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e Vibrato) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) - // NOTE: JBC - S3M dos not update on tick 0, but MOD does. - if currentTick != 0 || mem.Shared.ModCompatibility { - return doVibrato(cs, currentTick, x, y, 4) - } - return nil -} - -func (e Vibrato) String() string { - return fmt.Sprintf("H%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_vibratovolslide.go b/format/s3m/effect/effect_vibratovolslide.go deleted file mode 100644 index 6a40643..0000000 --- a/format/s3m/effect/effect_vibratovolslide.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// VibratoVolumeSlide defines a combination vibrato and volume slide effect -type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[channel.Memory, channel.Data] -} - -// NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide(mem *channel.Memory, cd uint8, val channel.DataEffect) VibratoVolumeSlide { - vvs := VibratoVolumeSlide{} - vs := volumeSlideFactory(mem, cd, val) - vvs.Effects = append(vvs.Effects, vs, Vibrato(0x00)) - return vvs -} - -func (e VibratoVolumeSlide) String() string { - return fmt.Sprintf("K%0.2x", e.Effects[0].(channel.DataEffect)) -} diff --git a/format/s3m/effect/effect_volslidedown.go b/format/s3m/effect/effect_volslidedown.go deleted file mode 100644 index 453cf28..0000000 --- a/format/s3m/effect/effect_volslidedown.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// VolumeSlideDown defines a volume slide down effect -type VolumeSlideDown ChannelCommand // 'D0y' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e VolumeSlideDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - y := channel.DataEffect(e) & 0x0F - - if mem.Shared.VolSlideEveryFrame || currentTick != 0 { - return doVolSlide(cs, -float32(y), 1.0) - } - return nil -} - -func (e VolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/effect_volslideup.go b/format/s3m/effect/effect_volslideup.go deleted file mode 100644 index 7cdcef6..0000000 --- a/format/s3m/effect/effect_volslideup.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" -) - -// VolumeSlideUp defines a volume slide up effect -type VolumeSlideUp ChannelCommand // 'Dx0' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e VolumeSlideUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x := channel.DataEffect(e) >> 4 - - if mem.Shared.VolSlideEveryFrame || currentTick != 0 { - return doVolSlide(cs, float32(x), 1.0) - } - return nil -} - -func (e VolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} diff --git a/format/s3m/effect/intf/intf.go b/format/s3m/effect/intf/intf.go deleted file mode 100644 index 158fd40..0000000 --- a/format/s3m/effect/intf/intf.go +++ /dev/null @@ -1,23 +0,0 @@ -package intf - -import ( - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/index" -) - -// S3M is an interface to S3M effect operations -type S3M interface { - SetTicks(int) error // Axx - SetNextOrder(index.Order) error // Bxx - SetNextRow(index.Row) error // Cxx - SetFilterEnable(bool) // S0x - SetNextRowWithBacktrack(index.Row, bool) error // SBx - GetCurrentRow() index.Row // SBx - SetPatternDelay(int) error // SEx - AddRowTicks(int) error // S6x - SetTempo(int) error // Txx - IncreaseTempo(int) error // Txx - DecreaseTempo(int) error // Txx - SetGlobalVolume(volume.Volume) // Vxx - IgnoreUnknownEffect() bool // Unhandled -} diff --git a/format/s3m/effect/unhandled.go b/format/s3m/effect/unhandled.go deleted file mode 100644 index 2909bdd..0000000 --- a/format/s3m/effect/unhandled.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" -) - -// UnhandledCommand is an unhandled command -type UnhandledCommand struct { - Command uint8 - Info channel.DataEffect -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory, channel.Data], m effectIntf.S3M) error { - if !m.IgnoreUnknownEffect() { - panic("unhandled command") - } - return nil -} - -func (e UnhandledCommand) String() string { - return fmt.Sprintf("%c%0.2x", e.Command+'@', e.Info) -} diff --git a/format/s3m/effect/util.go b/format/s3m/effect/util.go deleted file mode 100644 index 6a7becc..0000000 --- a/format/s3m/effect/util.go +++ /dev/null @@ -1,156 +0,0 @@ -package effect - -import ( - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" -) - -func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32, multiplier float32) error { - av := cs.GetActiveVolume() - v := s3mVolume.VolumeToS3M(av) - vol := int16((float32(v) + delta) * multiplier) - if vol >= 64 { - vol = 63 - } - if vol < 0 { - vol = 0 - } - sv := s3mfile.Volume(channel.DataEffect(vol)) - nv := s3mVolume.VolumeFromS3M(sv) - cs.SetActiveVolume(nv) - return nil -} - -func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - delta := int(amount * multiplier) - d := period.PeriodDelta(-delta) - cur = cur.AddDelta(d) - cs.SetPeriod(cur) - return nil -} - -func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - delta := int(amount * multiplier) - d := period.PeriodDelta(-delta) - cur = cur.AddDelta(d) - if period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { - cur = target - } - cs.SetPeriod(cur) - return nil -} - -func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - delta := int(amount * multiplier) - d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) - cs.SetPeriod(cur) - return nil -} - -func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - delta := int(amount * multiplier) - d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) - if period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { - cur = target - } - cs.SetPeriod(cur) - return nil -} - -func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, channel.DataEffect(speed), channel.DataEffect(depth), multiplier, mem.VibratoOscillator()) - cs.SetPeriodDelta(period.PeriodDelta(delta)) - return nil -} - -func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int, onTicks int, offTicks int) error { - mem := cs.GetMemory() - tremor := mem.TremorMem() - if tremor.IsActive() { - if tremor.Advance() > onTicks { - tremor.ToggleAndReset() - } - } else { - if tremor.Advance() > offTicks { - tremor.ToggleAndReset() - } - } - cs.SetVolumeActive(tremor.IsActive()) - return nil -} - -func doArpeggio(cs playback.Channel[channel.Memory, channel.Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { - ns := cs.GetNoteSemitone() - var arpSemitoneTarget note.Semitone - switch currentTick % 3 { - case 0: - arpSemitoneTarget = ns - case 1: - arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneADelta) - case 2: - arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneBDelta) - } - cs.SetOverrideSemitone(arpSemitoneTarget) - cs.SetTargetPos(cs.GetPos()) - return nil -} - -var ( - volSlideTwoThirdsTable = [...]s3mfile.Volume{ - 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9, - 10, 10, 11, 11, 12, 13, 13, 14, 15, 15, 16, 16, 17, 18, 18, 19, - 20, 20, 21, 21, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29, - 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, - } -) - -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) error { - vol := s3mVolume.VolumeToS3M(cs.GetActiveVolume()) - if vol >= 64 { - vol = 63 - } - cs.SetActiveVolume(s3mVolume.VolumeFromS3M(volSlideTwoThirdsTable[vol])) - return nil -} - -func doTremolo(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return doVolSlide(cs, delta, 1.0) -} - -func calculateWaveTable(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { - delta := o.GetWave(float32(depth)) * multiplier - o.Advance(int(speed)) - return delta -} diff --git a/format/s3m/filter/factory.go b/format/s3m/filter/factory.go new file mode 100644 index 0000000..5d3f852 --- /dev/null +++ b/format/s3m/filter/factory.go @@ -0,0 +1,24 @@ +package filter + +import ( + "fmt" + + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" +) + +func Factory(name string, instrumentRate frequency.Frequency, params any) (filter.Filter, error) { + var f filter.Filter + switch name { + case "": + // nothing + + case "amigalpf": + f = filter.NewAmigaLPF(instrumentRate) + + default: + return nil, fmt.Errorf("unsupported filter name: %q", name) + } + + return f, nil +} diff --git a/format/s3m/layout/channelsetting.go b/format/s3m/layout/channelsetting.go index f844173..5f1833c 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -2,17 +2,85 @@ package layout import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/format/s3m/channel" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/vol0optimization" ) // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled bool + Muted bool OutputChannelNum int Category s3mfile.ChannelCategory - InitialVolume volume.Volume - InitialPanning panning.Position + InitialVolume s3mVolume.Volume + PanEnabled bool + InitialPanning s3mPanning.Panning Memory channel.Memory + DefaultFilter filter.Info +} + +var _ song.ChannelSettings = (*ChannelSetting)(nil) + +func (c ChannelSetting) IsEnabled() bool { + return c.Enabled +} + +func (c ChannelSetting) IsMuted() bool { + return c.Muted +} + +func (c ChannelSetting) GetOutputChannelNum() int { + return c.OutputChannelNum +} + +func (c ChannelSetting) GetInitialVolume() s3mVolume.Volume { + return c.InitialVolume +} + +func (c ChannelSetting) GetMixingVolume() s3mVolume.FineVolume { + return s3mVolume.FineVolume(0x7f) +} + +func (c ChannelSetting) GetInitialPanning() s3mPanning.Panning { + if c.PanEnabled { + return c.InitialPanning + } + return s3mPanning.DefaultPanning +} + +func (c ChannelSetting) GetMemory() song.ChannelMemory { + return &c.Memory +} + +func (c ChannelSetting) IsPanEnabled() bool { + return c.PanEnabled +} + +func (c ChannelSetting) GetDefaultFilterInfo() filter.Info { + return c.DefaultFilter +} + +func (c ChannelSetting) IsDefaultFilterEnabled() bool { + return len(c.DefaultFilter.Name) > 0 +} + +func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { + return vol0optimization.Vol0OptimizationSettings{ + Enabled: c.Memory.Shared.ZeroVolOptimization, + MaxRowsAt0: 3, + } +} + +func (c ChannelSetting) GetOPLChannel() index.OPLChannel { + switch c.Category { + case s3mfile.ChannelCategoryOPL2Melody, s3mfile.ChannelCategoryOPL2Drums: + return index.OPLChannel(c.OutputChannelNum) + default: + return index.InvalidOPLChannel + } } diff --git a/format/s3m/layout/header.go b/format/s3m/layout/header.go index a3b6e73..7a3fe6e 100644 --- a/format/s3m/layout/header.go +++ b/format/s3m/layout/header.go @@ -1,13 +1,16 @@ package layout -import "github.com/gotracker/gomixing/volume" +import ( + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" +) // Header is a mildly-decoded S3M header definition type Header struct { Name string InitialSpeed int InitialTempo int - GlobalVolume volume.Volume - MixingVolume volume.Volume - Stereo bool + GlobalVolume s3mVolume.Volume + MixingVolume s3mVolume.FineVolume + InitialOrder index.Order } diff --git a/format/s3m/layout/row.go b/format/s3m/layout/row.go new file mode 100644 index 0000000..cada245 --- /dev/null +++ b/format/s3m/layout/row.go @@ -0,0 +1,27 @@ +package layout + +import ( + "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" +) + +type Row []channel.Data + +func (r Row) Len() int { + return len(r) +} + +func (r Row) ForEach(fn func(ch index.Channel, cd song.ChannelData[s3mVolume.Volume]) (bool, error)) error { + for i, c := range r { + cont, err := fn(index.Channel(i), c) + if err != nil { + return err + } + if !cont { + break + } + } + return nil +} diff --git a/format/s3m/layout/song.go b/format/s3m/layout/song.go index 11b60e3..a09278c 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -1,78 +1,80 @@ package layout import ( + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/channel" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" ) -// Song is the full definition of the song data of an Song file type Song struct { - Head Header - Instruments []*instrument.Instrument - Patterns []pattern.Pattern[channel.Data] + common.BaseSong[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + ChannelSettings []ChannelSetting - OrderList []index.Pattern + ChannelOrders []index.Channel + NumChannels int } -// GetOrderList returns the list of all pattern orders for the song -func (s Song) GetOrderList() []index.Pattern { - return s.OrderList +// GetNumChannels returns the number of channels the song has +func (s Song) GetNumChannels() int { + return s.NumChannels } -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { - if int(patNum) >= len(s.Patterns) { - return nil - } - return &s.Patterns[patNum] +// GetChannelSettings returns the channel settings at index `channelNum` +func (s Song) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + return s.ChannelSettings[channelNum] } -// IsChannelEnabled returns true if the channel at index `channelNum` is enabled -func (s Song) IsChannelEnabled(channelNum int) bool { - return s.ChannelSettings[channelNum].Enabled +func (s Song) GetRowRenderStringer(row song.Row, channels int, longFormat bool) render.RowStringer { + nch := min(s.NumChannels, channels) + rt := render.NewRowText[channel.Data](nch, longFormat) + rowData := make([]channel.Data, 0, nch) + _ = song.ForEachRowChannel[s3mVolume.Volume](row, func(ch index.Channel, d song.ChannelData[s3mVolume.Volume]) (bool, error) { + if int(ch) >= nch || !s.ChannelSettings[ch].Enabled || s.ChannelSettings[ch].Muted { + return true, nil + } + rowData = append(rowData, d.(channel.Data)) + return true, nil + }) + for len(rowData) < nch { + rowData = append(rowData, channel.Data{}) + } + rt.Channels = rowData + return rt } -// GetRenderChannel returns the output channel for the channel at index `channelNum` -func (s Song) GetRenderChannel(channelNum int) int { - return s.ChannelSettings[channelNum].OutputChannelNum +func (s Song) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { + for _, ch := range s.ChannelOrders { + cs := &s.ChannelSettings[ch] + if enabledOnly { + if !cs.Enabled || (cs.Muted && s.MS.Quirks.DoNotProcessEffectsOnMutedChannels) { + continue + } + } + cont, err := fn(ch) + if err != nil { + return err + } + if !cont { + break + } + } + return nil } -// NumInstruments returns the number of instruments in the song -func (s Song) NumInstruments() int { - return len(s.Instruments) -} +func (s Song) IsOPL2Enabled() bool { + for _, cs := range s.ChannelSettings { + if !cs.Enabled || cs.Muted { + continue + } -// IsValidInstrumentID returns true if the instrument exists -func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { - if instNum.IsEmpty() { - return false - } - switch id := instNum.(type) { - case channel.InstID: - iid := int(id) - return iid > 0 && iid <= len(s.Instruments) + if cs.GetOPLChannel().IsValid() { + return true + } } return false } - -// GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instID instrument.ID) (*instrument.Instrument, note.Semitone) { - if instID.IsEmpty() { - return nil, note.UnchangedSemitone - } - switch id := instID.(type) { - case channel.InstID: - return s.Instruments[int(id)-1], note.UnchangedSemitone - } - - return nil, note.UnchangedSemitone -} - -// GetName returns the name of the song -func (s Song) GetName() string { - return s.Head.Name -} diff --git a/format/s3m/layout/stringrow.go b/format/s3m/layout/stringrow.go new file mode 100644 index 0000000..02d6c88 --- /dev/null +++ b/format/s3m/layout/stringrow.go @@ -0,0 +1,133 @@ +package layout + +import ( + "fmt" + "regexp" + "slices" + "strconv" + "strings" + + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" +) + +type StringRow string + +func (r StringRow) Len() int { + return len(strings.SplitAfter(string(r), "|")) - 1 +} + +func (r StringRow) ForEach(fn func(ch index.Channel, d song.ChannelData[s3mVolume.Volume]) (bool, error)) error { + cstrPieces := strings.SplitAfter(string(r), "|") + cstrPieces = slices.DeleteFunc(cstrPieces, func(s string) bool { + return len(s) == 0 || s == "|" + }) + row := make(Row, len(cstrPieces)) + for ch, cstr := range cstrPieces { + d, err := r.decodeChannel(strings.TrimSuffix(cstr, "|")) + if err != nil { + return err + } + row[ch] = d + } + + return row.ForEach(fn) +} + +var channelRegex = regexp.MustCompile(`^(...) +(..) +(..) +(...)$`) + +func (StringRow) decodeChannel(cstr string) (channel.Data, error) { + var d channel.Data + + pieces := channelRegex.FindStringSubmatch(cstr) + if len(pieces) != 5 { + return d, fmt.Errorf("could not parse channel: %q", cstr) + } + note, instrument, vol, cmd := pieces[1], pieces[2], pieces[3], pieces[4] + + d.Note = s3mfile.EmptyNote + + // note + if note == "^^." { + d.What |= s3mfile.PatternFlagNote + d.Note = s3mfile.StopNote + } else if note != "..." { + key := note[0:2] + oct, err := strconv.Atoi(note[2:]) + if err != nil { + return d, err + } + + switch key { + case "C-": + d.Note = s3mfile.Note(oct<<4 | 0) + case "C#": + d.Note = s3mfile.Note(oct<<4 | 1) + case "D-": + d.Note = s3mfile.Note(oct<<4 | 2) + case "D#": + d.Note = s3mfile.Note(oct<<4 | 3) + case "E-": + d.Note = s3mfile.Note(oct<<4 | 4) + case "F-": + d.Note = s3mfile.Note(oct<<4 | 5) + case "F#": + d.Note = s3mfile.Note(oct<<4 | 6) + case "G-": + d.Note = s3mfile.Note(oct<<4 | 7) + case "G#": + d.Note = s3mfile.Note(oct<<4 | 8) + case "A-": + d.Note = s3mfile.Note(oct<<4 | 9) + case "A#": + d.Note = s3mfile.Note(oct<<4 | 10) + case "B-": + d.Note = s3mfile.Note(oct<<4 | 11) + default: + return d, fmt.Errorf("invalid key in note: %q", note) + } + d.What |= s3mfile.PatternFlagNote + } + + // instrument + if instrument != ".." { + i, err := strconv.Atoi(instrument) + if err != nil { + return d, err + } + + if i > 0 { + d.What |= s3mfile.PatternFlagNote + d.Instrument = uint8(i) + } + } + + // vol + if vol != ".." { + v, err := strconv.Atoi(vol) + if err != nil { + return d, err + } + + d.What |= s3mfile.PatternFlagVolume + d.Volume = s3mVolume.Volume(v) + } + + // cmd + if cmd != "..." { + c := cmd[0] + i, err := strconv.ParseUint(cmd[1:], 16, 8) + if err != nil { + return d, err + } + + d.What |= s3mfile.PatternFlagCommand + d.Command = c - '@' + d.Info = channel.DataEffect(i) + } + + return d, nil +} diff --git a/format/s3m/load/load.go b/format/s3m/load/load.go index 8051631..9be1672 100644 --- a/format/s3m/load/load.go +++ b/format/s3m/load/load.go @@ -3,15 +3,13 @@ package load import ( "io" - "github.com/gotracker/playback" "github.com/gotracker/playback/format/common" - "github.com/gotracker/playback/format/s3m/layout" "github.com/gotracker/playback/format/s3m/load/modconv" - s3mPlayback "github.com/gotracker/playback/format/s3m/playback" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" ) -func readMOD(r io.Reader, features []feature.Feature) (*layout.Song, error) { +func readMOD(r io.Reader, features []feature.Feature) (song.Data, error) { f, err := modconv.Read(r) if err != nil { return nil, err @@ -23,11 +21,11 @@ func readMOD(r io.Reader, features []feature.Feature) (*layout.Song, error) { } // MOD loads a MOD file and upgrades it into an S3M file internally -func MOD(r io.Reader, features []feature.Feature) (playback.Playback, error) { - return common.Load(r, readMOD, s3mPlayback.NewManager, features) +func MOD(r io.Reader, features []feature.Feature) (song.Data, error) { + return common.Load(r, readMOD, features) } // S3M loads an S3M file into a new Playback object -func S3M(r io.Reader, features []feature.Feature) (playback.Playback, error) { - return common.Load(r, readS3M, s3mPlayback.NewManager, features) +func S3M(r io.Reader, features []feature.Feature) (song.Data, error) { + return common.Load(r, readS3M, features) } diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index e7e475c..c5876c3 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -10,6 +10,8 @@ import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/format/s3m/layout" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" ) func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) { @@ -17,7 +19,7 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) for _, row := range mp { worthwhileChannels := 0 - unpackedChannels := make([]channel.Data, len(row)) + unpackedChannels := make(layout.Row, len(row)) for c, chn := range row { sampleNumber := chn.Instrument() samplePeriod := chn.Period() @@ -28,8 +30,8 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) *u = channel.Data{ What: s3mfile.PatternFlags(c & 0x1F), Note: s3mfile.EmptyNote, - Instrument: channel.InstID(sampleNumber), - Volume: s3mfile.EmptyVolume, + Instrument: sampleNumber, + Volume: s3mVolume.Volume(s3mfile.EmptyVolume), Command: uint8(0), Info: channel.DataEffect(0), } @@ -86,7 +88,18 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) u.Command = 'R' - '@' case 0xC: // Set Volume u.What |= s3mfile.PatternFlagVolume - u.Volume = s3mfile.Volume(u.Info) + u.Volume = s3mVolume.Volume(u.Info) + case 0x8: // Set Pan (mod-style) + if effectParameter >= 0x00 && effectParameter <= 0x80 { + u.What |= s3mfile.PatternFlagCommand + u.Command = 'S' - '@' + u.Info = channel.DataEffect(0x80 | (effectParameter >> 4)) + } else if effectParameter == 0xA4 { + // surround + u.What |= s3mfile.PatternFlagCommand + u.Command = 'S' - '@' + u.Info = channel.DataEffect(0x91) + } } if effect == 0xE { @@ -211,7 +224,7 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) } var ( - finetuneC2Spds = [...]s3mfile.C2SPD{ + finetuneC4SampleRates = [...]s3mfile.C2SPD{ 8363, 8413, 8463, 8529, 8581, 8651, 8723, 8757, 7895, 7941, 7985, 8046, 8107, 8169, 8232, 8280, } @@ -254,7 +267,7 @@ func convertMODInstrumentToS3M(num int, inst *modfile.InstrumentHeader, samp []u Lo: uint16(len(samp)), }, C2Spd: s3mfile.HiLo32{ - Lo: uint16(finetuneC2Spds[inst.FineTune&0xF]), + Lo: uint16(finetuneC4SampleRates[inst.FineTune&0xF]), }, Volume: s3mfile.Volume(inst.Volume), LoopBegin: s3mfile.HiLo32{ @@ -277,9 +290,6 @@ func convertMODInstrumentToS3M(num int, inst *modfile.InstrumentHeader, samp []u copy(anc.SampleName[:], inst.Name[:]) scrs.Sample = samp - for i, s := range samp { - samp[i] = modSampleToS3MSample(s) - } return &scrs, nil } @@ -295,14 +305,22 @@ func Read(r io.Reader) (*s3mfile.File, error) { f := s3mfile.File{ Head: s3mfile.ModuleHeader{ - Name: [28]byte{}, - OrderCount: uint16(mf.Head.SongLen), - InstrumentCount: 31, - PatternCount: uint16(len(mf.Patterns)), - GlobalVolume: s3mfile.DefaultVolume, - InitialSpeed: 6, - InitialTempo: 125, - MixingVolume: s3mfile.Volume(0x30) | s3mfile.Volume(0x80), // default mixing volume (0x30) for a converted mod in st3, stereo enabled (0x80) + Name: [28]byte{}, + Reserved1C: 0x1A, // 0x1A = magic + Type: 16, // 16 = ST3 module + OrderCount: uint16(mf.Head.SongLen), + InstrumentCount: 31, + PatternCount: uint16(len(mf.Patterns)), + Flags: 0x0004 | 0x0010 | 0x0020, // amigaSlides (0x0004) | amigaLimits (0x0010) | sbFilterEnable (0x0020) + TrackerVersion: 0x1300, // 0x1300 = specific version to support above flags + FileFormatInformation: 1, // 1 = signed samples + SCRM: [4]byte{'S', 'C', 'R', 'M'}, + GlobalVolume: s3mfile.DefaultVolume, + InitialSpeed: 6, + InitialTempo: 125, + MixingVolume: s3mfile.Volume(0x30) | s3mfile.Volume(0x80), // default mixing volume (0x30) for a converted mod in st3, stereo enabled (0x80) + UltraClickRemoval: uint8(numCh) * 2, + DefaultPanValueFlag: 252, // load pan settings }, } @@ -316,12 +334,12 @@ func Read(r io.Reader) (*s3mfile.File, error) { continue } - isLeft := (i & 1) == 0 - if isLeft { - f.ChannelSettings[i] = s3mfile.MakeChannelSetting(true, s3mfile.ChannelCategoryPCMLeft, i>>1) + // MODs process in 0 -> max channel order, so shove them all in the left category in order + f.ChannelSettings[i] = s3mfile.MakeChannelSetting(true, s3mfile.ChannelCategoryPCMLeft, i) + + if isLeft := (i & 1) == 0; isLeft { f.Panning[i] = s3mfile.DefaultPanningLeft } else { - f.ChannelSettings[i] = s3mfile.MakeChannelSetting(true, s3mfile.ChannelCategoryPCMRight, i>>1) f.Panning[i] = s3mfile.DefaultPanningRight } } @@ -352,7 +370,3 @@ func Read(r io.Reader) (*s3mfile.File, error) { return &f, nil } - -func modSampleToS3MSample(sample uint8) uint8 { - return sample - 0x80 -} diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index d45faf8..21b4a83 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -7,21 +7,24 @@ import ( "io" s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/loop" - "github.com/gotracker/playback/voice/pcm" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/format/s3m/layout" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + "github.com/gotracker/playback/format/s3m/settings" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" ) func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { @@ -32,42 +35,46 @@ func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { Name: fh.GetName(), InitialSpeed: int(fh.InitialSpeed), InitialTempo: int(fh.InitialTempo), - GlobalVolume: s3mVolume.VolumeFromS3M(fh.GlobalVolume), - Stereo: (fh.MixingVolume & 0x80) != 0, + GlobalVolume: s3mVolume.Volume(fh.GlobalVolume), + MixingVolume: s3mVolume.FineVolume(fh.MixingVolume &^ 0x80), + InitialOrder: 0, } z := uint32(fh.MixingVolume & 0x7f) if z < 0x10 { z = 0x10 } - head.MixingVolume = volume.Volume(z) / volume.Volume(0x80) + head.MixingVolume = s3mVolume.FineVolume(z) return &head, nil } -func scrsNoneToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSNoneHeader) (*instrument.Instrument, error) { - sample := instrument.Instrument{ - Static: instrument.StaticValues{ +func scrsNoneToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSNoneHeader) (*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { + sample := instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Static: instrument.StaticValues[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ Filename: scrs.Head.GetFilename(), Name: si.GetSampleName(), - Volume: s3mVolume.VolumeFromS3M(si.Volume), + Volume: s3mVolume.Volume(si.Volume), }, - C2Spd: period.Frequency(si.C2Spd.Lo), + SampleRate: frequency.Frequency(si.C2Spd.Lo), + } + if sample.Static.Volume.IsInvalid() { + sample.Static.Volume = s3mVolume.MaxVolume } return &sample, nil } -func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHeader, signedSamples bool, features []feature.Feature) (*instrument.Instrument, error) { - sample := instrument.Instrument{ - Static: instrument.StaticValues{ +func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHeader, signedSamples bool, features []feature.Feature) (*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { + sample := instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Static: instrument.StaticValues[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ Filename: scrs.Head.GetFilename(), Name: si.GetSampleName(), - Volume: s3mVolume.VolumeFromS3M(si.Volume), + Volume: s3mVolume.Volume(si.Volume), }, - C2Spd: period.Frequency(si.C2Spd.Lo), + SampleRate: frequency.Frequency(si.C2Spd.Lo), } - if sample.C2Spd == 0 { - sample.C2Spd = period.Frequency(s3mfile.DefaultC2Spd) + if sample.SampleRate == 0 { + sample.SampleRate = frequency.Frequency(s3mfile.DefaultC2Spd) } instLen := int(si.Length.Lo) @@ -80,10 +87,8 @@ func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHead End: int(si.LoopEnd.Lo), } - idata := instrument.PCM{ - Loop: &loop.Disabled{}, - Panning: panning.CenterAhead, - MixingVolume: volume.Volume(1), + idata := instrument.PCM[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Loop: &loop.Disabled{}, FadeOut: fadeout.Settings{ Mode: fadeout.ModeDisabled, Amount: volume.Volume(0), @@ -118,14 +123,14 @@ func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHead return &sample, nil } -func scrsOpl2ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSAdlibHeader) (*instrument.Instrument, error) { - inst := instrument.Instrument{ - Static: instrument.StaticValues{ +func scrsOpl2ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSAdlibHeader) (*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { + inst := instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Static: instrument.StaticValues[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ Filename: scrs.Head.GetFilename(), Name: si.GetSampleName(), - Volume: s3mVolume.VolumeFromS3M(si.Volume), + Volume: s3mVolume.Volume(si.Volume), }, - C2Spd: period.Frequency(si.C2Spd.Lo), + SampleRate: frequency.Frequency(si.C2Spd.Lo), } idata := instrument.OPL2{ @@ -165,7 +170,7 @@ func scrsOpl2ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSAdlibHeader) ( return &inst, nil } -func convertSCRSFullToInstrument(scrs *s3mfile.SCRSFull, signedSamples bool, features []feature.Feature) (*instrument.Instrument, error) { +func convertSCRSFullToInstrument(scrs *s3mfile.SCRSFull, signedSamples bool, features []feature.Feature) (*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { if scrs == nil { return nil, errors.New("scrs is nil") } @@ -185,18 +190,15 @@ func convertSCRSFullToInstrument(scrs *s3mfile.SCRSFull, signedSamples bool, fea return nil, errors.New("unhandled scrs ancillary type") } -func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (*pattern.Pattern[channel.Data], int) { - pat := &pattern.Pattern[channel.Data]{ - Orig: pkt, - } +func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (song.Pattern, int) { + pat := make(song.Pattern, numRows) buffer := bytes.NewBuffer(pkt.Data) - rowNum := uint8(0) maxCh := uint8(0) - for rowNum < numRows { - pat.Rows = append(pat.Rows, pattern.RowData[channel.Data]{}) - row := &pat.Rows[rowNum] + for rowNum := uint8(0); rowNum < numRows; rowNum++ { + row := make(layout.Row, 0) + channelLoop: for { var what s3mfile.PatternFlags if err := binary.Read(buffer, binary.LittleEndian, &what); err != nil { @@ -204,15 +206,14 @@ func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (*pattern } if what == 0 { - rowNum++ - break + break channelLoop } channelNum := what.Channel() - for len(row.Channels) <= int(channelNum) { - row.Channels = append(row.Channels, channel.Data{}) + for len(row) <= int(channelNum) { + row = append(row, channel.Data{}) } - temp := &row.Channels[channelNum] + temp := &row[channelNum] if maxCh < channelNum { maxCh = channelNum } @@ -220,7 +221,7 @@ func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (*pattern temp.What = what temp.Note = 0 temp.Instrument = 0 - temp.Volume = s3mfile.EmptyVolume + temp.Volume = s3mVolume.Volume(s3mfile.EmptyVolume) temp.Command = 0 temp.Info = 0 @@ -248,6 +249,7 @@ func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (*pattern } } } + pat[rowNum] = row } return pat, int(maxCh) @@ -259,34 +261,54 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, return nil, err } - song := layout.Song{ - Head: *h, - Instruments: make([]*instrument.Instrument, len(f.InstrumentPointers)), - OrderList: make([]index.Pattern, len(f.OrderList)), + amigaLimits := (f.Head.Flags&0x0010) != 0 || wasModFile + + s := layout.Song{ + BaseSong: common.BaseSong[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + System: s3mSystem.S3MSystem, + MS: settings.GetMachineSettings(amigaLimits), + Name: h.Name, + InitialBPM: h.InitialTempo, + InitialTempo: h.InitialSpeed, + GlobalVolume: h.GlobalVolume, + MixingVolume: h.MixingVolume, + InitialOrder: h.InitialOrder, + Instruments: make([]*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.InstrumentPointers)), + Patterns: nil, + OrderList: make([]index.Pattern, len(f.OrderList)), + }, + } + + f.Head.GlobalVolume = min(f.Head.GlobalVolume, s3mfile.Volume(s3mVolume.MaxVolume)) + if f.Head.GlobalVolume == 0 && f.Head.TrackerVersion < 0x1320 { + s.GlobalVolume = s3mVolume.MaxVolume } - signedSamples := false - if f.Head.FileFormatInformation == 1 { - signedSamples = true + if f.Head.InitialSpeed == 0 || f.Head.InitialSpeed == 255 { + s.InitialTempo = 6 } + if f.Head.InitialTempo < 33 { + s.InitialBPM = 125 + } + + signedSamples := f.Head.FileFormatInformation == 1 + + stereoMode := (f.Head.MixingVolume&0x80) != 0 || wasModFile st2Vibrato := (f.Head.Flags & 0x0001) != 0 st2Tempo := (f.Head.Flags & 0x0002) != 0 - amigaSlides := (f.Head.Flags & 0x0004) != 0 - zeroVolOpt := (f.Head.Flags & 0x0008) != 0 - amigaLimits := (f.Head.Flags & 0x0010) != 0 - sbFilterEnable := (f.Head.Flags & 0x0020) != 0 - st300volSlides := (f.Head.Flags & 0x0040) != 0 - if f.Head.TrackerVersion == 0x1300 { - st300volSlides = true - } + amigaSlides := (f.Head.Flags&0x0004) != 0 || wasModFile + zeroVolOpt := (f.Head.Flags&0x0008) != 0 && !wasModFile + sbFilterEnable := (f.Head.Flags&0x0020) != 0 || wasModFile + st300volSlides := (f.Head.Flags&0x0040) != 0 || f.Head.TrackerVersion == 0x1300 + st300portas := f.Head.TrackerVersion == 0x1300 //ptrSpecialIsValid := (f.Head.Flags & 0x0080) != 0 for i, o := range f.OrderList { - song.OrderList[i] = index.Pattern(o) + s.OrderList[i] = index.Pattern(o) } - song.Instruments = make([]*instrument.Instrument, len(f.Instruments)) + s.Instruments = make([]*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.Instruments)) for instNum, scrs := range f.Instruments { sample, err := convertSCRSFullToInstrument(&scrs, signedSamples, features) if err != nil { @@ -296,24 +318,25 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, continue } sample.Static.ID = channel.InstID(uint8(instNum + 1)) - song.Instruments[instNum] = sample + s.Instruments[instNum] = sample } - lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern[channel.Data], len(f.Patterns)) + maxPatternChannel := 3 + s.Patterns = make([]song.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { pattern, maxCh := convertS3MPackedPattern(pkt, getPatternLen(patNum)) if pattern == nil { continue } - if lastEnabledChannel < maxCh { - lastEnabledChannel = maxCh + if maxPatternChannel < maxCh { + maxPatternChannel = maxCh } - song.Patterns[patNum] = *pattern + s.Patterns[patNum] = pattern } sharedMem := channel.SharedMemory{ - VolSlideEveryFrame: st300volSlides, + VolSlideEveryTick: st300volSlides, + ST300Portas: st300portas, LowPassFilterEnable: sbFilterEnable, ResetMemoryAtStartOfOrder0: true, ST2Vibrato: st2Vibrato, @@ -324,25 +347,30 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, ModCompatibility: wasModFile, } - channels := make([]layout.ChannelSetting, 0) + channels := make([]layout.ChannelSetting, 0, maxPatternChannel+1) + lastEnabledChannel := 3 for chNum, ch := range f.ChannelSettings { chn := ch.GetChannel() cs := layout.ChannelSetting{ Enabled: ch.IsEnabled(), + Muted: !ch.IsEnabled(), Category: chn.GetChannelCategory(), OutputChannelNum: int(ch.GetChannel() & 0x07), - InitialVolume: s3mVolume.DefaultVolume, + InitialVolume: s3mVolume.Volume(s3mfile.DefaultVolume), + PanEnabled: stereoMode, InitialPanning: s3mPanning.DefaultPanning, Memory: channel.Memory{ Shared: &sharedMem, }, } - cs.Memory.ResetOscillators() + if sbFilterEnable { + cs.DefaultFilter.Name = "amigalpf" + } pf := f.Panning[chNum] if pf.IsValid() { - cs.InitialPanning = s3mPanning.PanningFromS3M(pf.Value()) + cs.InitialPanning = s3mPanning.Panning(pf.Value()) } else { switch cs.Category { case s3mfile.ChannelCategoryPCMLeft: @@ -360,12 +388,30 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } } - song.ChannelSettings = channels[:lastEnabledChannel+1] + s.NumChannels = lastEnabledChannel + 1 + s.ChannelSettings = channels[:maxPatternChannel+1] + + var channelOrders [4][]index.Channel + for i, cs := range s.ChannelSettings { + switch cs.Category { + case s3mfile.ChannelCategoryPCMLeft: + channelOrders[0] = append(channelOrders[0], index.Channel(i)) + case s3mfile.ChannelCategoryPCMRight: + channelOrders[1] = append(channelOrders[1], index.Channel(i)) + case s3mfile.ChannelCategoryOPL2Melody: + channelOrders[2] = append(channelOrders[2], index.Channel(i)) + case s3mfile.ChannelCategoryOPL2Drums: + channelOrders[3] = append(channelOrders[3], index.Channel(i)) + } + } + for _, co := range channelOrders { + s.ChannelOrders = append(s.ChannelOrders, co...) + } - return &song, nil + return &s, nil } -func readS3M(r io.Reader, features []feature.Feature) (*layout.Song, error) { +func readS3M(r io.Reader, features []feature.Feature) (song.Data, error) { f, err := s3mfile.Read(r) if err != nil { return nil, err diff --git a/format/s3m/oscillator/factory.go b/format/s3m/oscillator/factory.go new file mode 100644 index 0000000..e2e7ce9 --- /dev/null +++ b/format/s3m/oscillator/factory.go @@ -0,0 +1,35 @@ +package oscillator + +import ( + "fmt" + + oscillatorImpl "github.com/gotracker/playback/oscillator" + "github.com/gotracker/playback/voice/oscillator" +) + +func VibratoFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil +} + +func TremoloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil +} + +func PanbrelloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil +} + +func OscillatorFactory(name string) (oscillator.Oscillator, error) { + switch name { + case "": + return nil, nil + case "vibrato": + return VibratoFactory() + case "tremolo": + return TremoloFactory() + case "panbrello": + return PanbrelloFactory() + default: + return nil, fmt.Errorf("unsupported oscillator: %q", name) + } +} diff --git a/format/s3m/panning/panning.go b/format/s3m/panning/panning.go index c4b89aa..156d6b7 100644 --- a/format/s3m/panning/panning.go +++ b/format/s3m/panning/panning.go @@ -1,17 +1,55 @@ package panning -import "github.com/gotracker/gomixing/panning" +import ( + "math" + + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/playback/voice/types" +) var ( // DefaultPanningLeft is the default panning value for left channels - DefaultPanningLeft = PanningFromS3M(0x03) + DefaultPanningLeft = Panning(0x03) // DefaultPanning is the default panning value for unconfigured channels - DefaultPanning = PanningFromS3M(0x08) + DefaultPanning = Panning(0x08) // DefaultPanningRight is the default panning value for right channels - DefaultPanningRight = PanningFromS3M(0x0C) + DefaultPanningRight = Panning(0x0C) + + MaxPanning = Panning(0x0F) +) + +type Panning uint8 + +var ( + _ types.PanningInformationer[Panning] = Panning(0) + _ types.PanningDeltaer[Panning] = Panning(0) ) +func (p Panning) IsInvalid() bool { + return p > 0x0F +} + +func (p Panning) ToPosition() panning.Position { + return panning.MakeStereoPosition(float32(p), 0, 0x0F) +} + +func (Panning) GetDefault() Panning { + return DefaultPanning +} + +func (Panning) GetMax() Panning { + return MaxPanning +} + +func (p Panning) FMA(multiplier, add float32) Panning { + return Panning(min(max(math.FMA(float64(p), float64(multiplier), float64(add)), 0), 0x0F)) +} + +func (p Panning) AddDelta(d types.PanDelta) Panning { + return Panning(min(max(int16(p)+int16(d), 0), int16(MaxPanning))) +} + // PanningFromS3M returns a radian panning position from an S3M panning value func PanningFromS3M(pos uint8) panning.Position { - return panning.MakeStereoPosition(float32(pos), 0, 0x0F) + return Panning(pos).ToPosition() } diff --git a/format/s3m/pattern/pattern.go b/format/s3m/pattern/pattern.go deleted file mode 100644 index cb99b75..0000000 --- a/format/s3m/pattern/pattern.go +++ /dev/null @@ -1,338 +0,0 @@ -package pattern - -import ( - "errors" - - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" - formatutil "github.com/gotracker/playback/util" - "github.com/heucuva/optional" -) - -// State is the current pattern state -type State struct { - currentOrder index.Order - currentRow index.Row - ticks int - tempo int - patternDelay optional.Value[int] - finePatternDelay int - resetPatternLoops bool - - SongLoop feature.SongLoop - PlayUntilOrderAndRow feature.PlayUntilOrderAndRow - loopDetect formatutil.LoopDetect // when SongLoopEnabled is false, this is used to detect song loops - loopCount int - - Patterns []pattern.Pattern[channel.Data] - Orders []index.Pattern -} - -// GetTempo returns the tempo of the current state -func (state *State) GetTempo() int { - return state.tempo -} - -// GetSpeed returns the row speed of the current state -func (state *State) GetSpeed() int { - return state.ticks -} - -// GetTicksThisRow returns the number of ticks in the current row -func (state *State) GetTicksThisRow() int { - rowLoops := 1 - if patternDelay, ok := state.patternDelay.Get(); ok { - rowLoops = patternDelay - } - extraTicks := state.finePatternDelay - - ticksThisRow := state.ticks*rowLoops + extraTicks - return ticksThisRow -} - -// GetPatNum returns the current pattern number -func (state *State) GetPatNum() index.Pattern { - if int(state.currentOrder) >= len(state.Orders) { - return index.InvalidPattern - } - return state.Orders[state.currentOrder] -} - -// GetNumRows returns the number of rows in the current pattern -func (state *State) GetNumRows() (int, error) { - rows, err := state.GetRows() - if err != nil { - return 0, err - } - if rows != nil { - return rows.NumRows(), nil - } - return 0, nil -} - -// WantsStop returns true when the current pattern wants to end the song -func (state *State) WantsStop() bool { - return state.GetPatNum() == index.InvalidPattern -} - -// setCurrentOrder sets the current order index -func (state *State) setCurrentOrder(order index.Order) { - state.currentOrder = order - state.resetPatternLoops = true -} - -func (state *State) advanceOrder() { - state.setCurrentOrder(state.currentOrder + 1) -} - -// GetCurrentOrder returns the current order -func (state *State) GetCurrentOrder() index.Order { - return state.currentOrder -} - -// GetNumOrders returns the number of orders in the song -func (state *State) GetNumOrders() int { - return len(state.Orders) -} - -// GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { - ordLen := len(state.Orders) - - if ordLen == 0 { - // nothing to play, don't even try - return 0, song.ErrStopSong - } - - for loopCount := 0; loopCount < ordLen; loopCount++ { - ordIdx := int(state.GetCurrentOrder()) - if ordIdx >= ordLen { - if !(state.SongLoop.Count < 0 || state.loopCount < state.SongLoop.Count) { - return 0, song.ErrStopSong - } - state.setCurrentOrder(0) - continue - } - - patIdx := state.Orders[ordIdx] - if patIdx == index.NextPattern { - if err := state.nextOrder(true); err != nil { - return 0, err - } - continue - } - - if patIdx == index.InvalidPattern { - if err := state.nextOrder(true); err != nil { - return 0, err - } - continue // this is supposed to be a song break - } - - return patIdx, nil - } - return 0, errors.New("infinite loop detected in order list") -} - -// GetCurrentRow returns the current row -func (state *State) GetCurrentRow() index.Row { - return state.currentRow -} - -// setCurrentRow sets the current row -func (state *State) setCurrentRow(row index.Row) error { - state.currentRow = row - rows, err := state.GetNumRows() - if err != nil { - return err - } - if int(state.GetCurrentRow()) >= rows { - if err := state.nextOrder(true); err != nil { - return err - } - } - return nil -} - -// Observe will attempt to detect a song loop -func (state *State) Observe() error { - if state.SongLoop.Count >= 0 { - if state.loopDetect.Observe(state.currentOrder, state.currentRow) { - if state.SongLoop.Count == 0 || (state.SongLoop.Count > 0 && state.loopCount >= state.SongLoop.Count) { - return song.ErrStopSong - } - state.loopCount += 1 - state.loopDetect.Reset() - } - } - if state.currentOrder == index.Order(state.PlayUntilOrderAndRow.Order) && state.currentRow == index.Row(state.PlayUntilOrderAndRow.Row) { - if state.SongLoop.Count >= 0 && state.loopCount >= state.SongLoop.Count { - return song.ErrStopSong - } - } - return nil -} - -// nextOrder travels to the next pattern in the order list -func (state *State) nextOrder(resetRow ...bool) error { - state.advanceOrder() - state.patternDelay.Reset() - state.finePatternDelay = 0 - // called only to clean up order position info - if _, err := state.GetCurrentPatternIdx(); err != nil { - return err - } - if len(resetRow) > 0 && resetRow[0] { - state.currentRow = 0 - } - return nil -} - -// Reset resets a pattern state back to zeroes -func (state *State) Reset() { - *state = State{ - SongLoop: feature.SongLoop{ - Count: 0, - }, - PlayUntilOrderAndRow: feature.PlayUntilOrderAndRow{ - Order: -1, - Row: -1, - }, - } -} - -// nextRow travels to the next row in the pattern -// or the next order in the order list if the last row has been exhausted -func (state *State) nextRow() error { - state.patternDelay.Reset() - state.finePatternDelay = 0 - - var patNum = state.GetPatNum() - if patNum == index.InvalidPattern { - return nil - } - - if patNum == index.NextPattern { - if err := state.nextOrder(true); err != nil { - return err - } - return nil - } - - rows, err := state.GetNumRows() - if err != nil { - return err - } - if state.currentRow.Increment(rows) { - if err := state.nextOrder(true); err != nil { - return err - } - } - return nil -} - -// GetRows returns all the rows in the pattern -func (state *State) GetRows() (song.Rows[channel.Data], error) { -nextRow: - for loops := 0; loops < len(state.Patterns); loops++ { - var patNum = state.GetPatNum() - switch patNum { - case index.InvalidPattern: - return nil, nil - case index.NextPattern: - if err := state.nextRow(); err != nil { - return nil, err - } - continue nextRow - default: - if int(patNum) >= len(state.Patterns) { - return nil, nil - } - pattern := state.Patterns[patNum] - return pattern.GetRows(), nil - } - } - return nil, nil -} - -// NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State) NeedResetPatternLoops() bool { - rpl := state.resetPatternLoops - state.resetPatternLoops = false - return rpl -} - -// commitTransaction will update the order and row indexes at once, idempotently, from a row update transaction. -func (state *State) commitTransaction(txn *pattern.RowUpdateTransaction) error { - tempo, tempoSet := txn.Tempo.Get() - tempoDelta, tempoDeltaSet := txn.TempoDelta.Get() - if tempoSet || tempoDeltaSet { - newTempo := state.tempo - if tempoSet { - newTempo = tempo - } - if tempoDeltaSet { - newTempo += tempoDelta - } - state.tempo = newTempo - } - - if ticks, ok := txn.Ticks.Get(); ok { - state.ticks = ticks - } - - if finePatternDelay, ok := txn.FinePatternDelay.Get(); ok { - state.finePatternDelay = finePatternDelay - } - - if !state.patternDelay.IsSet() { - if patternDelay, ok := txn.GetPatternDelay(); ok { - state.patternDelay.Set(patternDelay) - } - } - - orderIdx, orderIdxSet := txn.GetOrderIdx() - rowIdx, rowIdxSet := txn.GetRowIdx() - - if orderIdxSet || rowIdxSet { - if orderIdxSet { - state.setCurrentOrder(orderIdx) - if !rowIdxSet { - if err := state.setCurrentRow(0); err != nil { - return err - } - } - } - if rowIdxSet { - if !orderIdxSet && !txn.RowIdxAllowBacktrack { // && state.currentRow > rowIdx // QUIRK[S3M/MOD] - if err := state.nextOrder(); err != nil { - return err - } - } - if err := state.setCurrentRow(rowIdx); err != nil { - return err - } - } - } else if txn.BreakOrder { // QUIRK[S3M/MOD] - if err := state.nextOrder(true); err != nil { - return err - } - } else if txn.AdvanceRow { - if err := state.nextRow(); err != nil { - return err - } - } - return nil -} - -// StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn -} diff --git a/format/s3m/period/amiga.go b/format/s3m/period/amiga.go deleted file mode 100644 index c2c185a..0000000 --- a/format/s3m/period/amiga.go +++ /dev/null @@ -1,93 +0,0 @@ -package period - -import ( - "fmt" - "math" - - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - - "github.com/gotracker/playback/period" -) - -// Amiga defines a sampler period that follows the Amiga-style approach of note -// definition. Useful in calculating resampling. -type Amiga period.AmigaPeriod - -// AddInteger truncates the current period to an integer and adds the delta integer in -// then returns the resulting period -func (p Amiga) AddInteger(delta int) Amiga { - ret := Amiga(int(p) + delta) - // clamp to 64 as minimum - if ret < 64 { - ret = 64 - } - return ret -} - -// Add adds the current period to a delta value then returns the resulting period -func (p Amiga) AddDelta(delta period.Delta) period.Period { - ret := p - d := period.ToPeriodDelta(delta) - ret += Amiga(d) - // clamp to 64 as minimum - if ret < 64 { - ret = 64 - } - return ret -} - -// Compare returns: -// -1 if the current period is higher frequency than the `rhs` period -// 0 if the current period is equal in frequency to the `rhs` period -// 1 if the current period is lower frequency than the `rhs` period -func (p Amiga) Compare(rhs period.Period) comparison.Spaceship { - lf := p.GetFrequency() - rf := rhs.GetFrequency() - - switch { - case lf < rf: - return comparison.SpaceshipRightGreater - case lf > rf: - return comparison.SpaceshipLeftGreater - default: - return comparison.SpaceshipEqual - } -} - -// Lerp linear-interpolates the current period with the `rhs` period -func (p Amiga) Lerp(t float64, rhs period.Period) period.Period { - right := Amiga(0) - if r, ok := rhs.(Amiga); ok { - right = r - } - - ret := Amiga(period.AmigaPeriod(p).Lerp(t, period.AmigaPeriod(right))) - return ret -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { - return float64(period.AmigaPeriod(p).GetFrequency(period.Frequency(samplerSpeed))) -} - -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - return period.AmigaPeriod(p).GetFrequency(period.Frequency(S3MBaseClock)) -} - -func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%f }", float32(p)) -} - -// ToAmigaPeriod calculates an amiga period for a linear finetune period -func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { - if finetunes < 0 { - finetunes = 0 - } - pow := math.Pow(2, float64(finetunes)/semitonesPerOctave) - linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - - period := Amiga(float64(semitonePeriodTable[0]) / linFreq) - return period -} diff --git a/format/s3m/period/amigaconverter.go b/format/s3m/period/amigaconverter.go new file mode 100644 index 0000000..8a0e787 --- /dev/null +++ b/format/s3m/period/amigaconverter.go @@ -0,0 +1,22 @@ +package period + +import ( + "github.com/gotracker/playback/format/s3m/system" + "github.com/gotracker/playback/period" +) + +var S3MAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + System: system.S3MSystem, + MinPeriod: 64, + MaxPeriod: 32767, + SlideTo0Allowed: true, // this will allow sliding unclamped to 0, which in S3M stops the playback +} + +//MinMOD15Period = 452 +//MaxMOD15Period = 3424 + +var MODAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + System: system.S3MSystem, + MinPeriod: 56, + MaxPeriod: 13696, +} diff --git a/format/s3m/period/util.go b/format/s3m/period/util.go index 88e12ee..9ded5d8 100644 --- a/format/s3m/period/util.go +++ b/format/s3m/period/util.go @@ -1,67 +1,49 @@ package period import ( - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" -) - -const ( - floatDefaultC2Spd = float32(DefaultC2Spd) - c2Period = 1712 - - // DefaultC2Spd is the default C2SPD for S3M samples - DefaultC2Spd = period.Frequency(s3mfile.DefaultC2Spd) - - // S3MBaseClock is the base clock speed of S3M files - S3MBaseClock period.Frequency = DefaultC2Spd * c2Period - - notesPerOctave = 12 - semitonesPerNote = 64 - semitonesPerOctave = notesPerOctave * semitonesPerNote + "github.com/gotracker/playback/format/s3m/system" + "github.com/gotracker/playback/frequency" ) +var DefaultC4SampleRate = system.DefaultC4SampleRate var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} -// CalcSemitonePeriod calculates the semitone period for it notes -func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequency) period.Period { - if semi == note.UnchangedSemitone { - panic("how?") - } - - key := int(semi.Key()) - octave := uint32(semi.Octave()) - - if key >= len(semitonePeriodTable) { - return nil - } - - if c2spd == 0 { - c2spd = period.Frequency(DefaultC2Spd) +// CalcFinetuneC4SampleRate calculates a new frequency after a finetune adjustment +func CalcFinetuneC4SampleRate(finetune uint8) frequency.Frequency { + switch finetune { + case 0x0: + return 7895 + case 0x1: + return 7941 + case 0x2: + return 7985 + case 0x3: + return 8046 + case 0x4: + return 8107 + case 0x5: + return 8169 + case 0x6: + return 8232 + case 0x7: + return 8280 + case 0x8: + return 8363 + case 0x9: + return 8413 + case 0xA: + return 8463 + case 0xB: + return 8529 + case 0xC: + return 8581 + case 0xD: + return 8651 + case 0xE: + return 8723 + case 0xF: + return 8757 + default: + panic("unhandled") } - - if ft != 0 { - c2spd = CalcFinetuneC2Spd(c2spd, ft) - } - - p := (Amiga(floatDefaultC2Spd*semitonePeriodTable[key]) / Amiga(uint32(c2spd)< 0 { - m.ensureOPL2() - } - return nil -} diff --git a/format/s3m/playback/playback_command.go b/format/s3m/playback/playback_command.go deleted file mode 100644 index 62abe93..0000000 --- a/format/s3m/playback/playback_command.go +++ /dev/null @@ -1,184 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback" - "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/format/s3m/channel" - s3mPeriod "github.com/gotracker/playback/format/s3m/period" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/player/state" -) - -type doNoteCalc struct { - Semitone note.Semitone - UpdateFunc state.PeriodUpdateFunc -} - -func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data]) error { - if o.UpdateFunc == nil { - return nil - } - - if inst := cs.GetTargetInst(); inst != nil { - cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) - period := s3mPeriod.CalcSemitonePeriod(cs.Semitone, inst.GetFinetune(), inst.GetC2Spd()) - o.UpdateFunc(period) - } - return nil -} - -func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitPreTick(m, cs, currentTick, lastTick, cs.SemitoneSetterFactory); err != nil { - return err - } - if err := txn.CommitTick(m, cs, currentTick, lastTick, cs.SemitoneSetterFactory); err != nil { - return err - } - if err := txn.CommitPostTick(m, cs, currentTick, lastTick, cs.SemitoneSetterFactory); err != nil { - return err - } - } - - if err := m.processRowNote(ch, cs, currentTick, lastTick); err != nil { - return err - } - - if err := m.processVoiceUpdates(ch, cs, currentTick, lastTick); err != nil { - return err - } - - return nil -} - -func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { - triggerTick, noteAction := cs.WillTriggerOn(currentTick) - if !triggerTick { - return nil - } - var n note.Note = note.EmptyNote{} - if cs.GetData() != nil { - n = cs.GetData().GetNote() - } - keyOn := false - keyOff := false - stop := false - - if targetInst := cs.GetTargetInst(); targetInst != nil { - cs.SetInstrument(targetInst) - keyOn = true - } else { - cs.SetInstrument(nil) - } - - if cs.UseTargetPeriod { - if nc := cs.GetVoice(); nc != nil { - nc.Release() - nc.Fadeout() - } - targetPeriod := cs.GetTargetPeriod() - cs.SetPeriod(targetPeriod) - cs.SetPortaTargetPeriod(targetPeriod) - } - cs.SetPos(cs.GetTargetPos()) - - if inst := cs.GetInstrument(); inst != nil { - keyOff = inst.IsReleaseNote(n) - stop = inst.IsStopNote(n) - } - - if nc := cs.GetVoice(); nc != nil { - if keyOn && noteAction == note.ActionRetrigger { - // S3M is weird and only sets the global volume on the channel when a KeyOn happens - cs.SetGlobalVolume(m.GetGlobalVolume()) - nc.Attack() - mem := cs.GetMemory() - mem.Retrigger() - } else if keyOff { - nc.Release() - nc.Fadeout() - cs.SetPeriod(nil) - } else if stop { - cs.SetInstrument(nil) - cs.SetPeriod(nil) - } - } - return nil -} - -func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { - if cs.UsePeriodOverride { - cs.UsePeriodOverride = false - arpeggioPeriod := cs.GetPeriodOverride() - cs.SetPeriod(arpeggioPeriod) - } - return nil -} - -// SetFilterEnable activates or deactivates the amiga low-pass filter on the instruments -func (m *Manager) SetFilterEnable(on bool) { - for i := range m.song.ChannelSettings { - c := m.GetChannel(i) - if o := c.GetRenderChannel(); o != nil { - if on { - if o.Filter == nil { - o.Filter = filter.NewAmigaLPF(s3mPeriod.DefaultC2Spd, m.GetSampleRate()) - } - } else { - o.Filter = nil - } - } - } -} - -// SetTicks sets the number of ticks the row expects to play for -func (m *Manager) SetTicks(ticks int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.Ticks.Set(ticks) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.Ticks.Set(ticks) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// AddRowTicks increases the number of ticks the row expects to play for -func (m *Manager) AddRowTicks(ticks int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.FinePatternDelay.Set(ticks) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.FinePatternDelay.Set(ticks) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetPatternDelay sets the repeat number for the row to `rept` -// NOTE: this may be set 1 time (first in wins) and will be reset only by the next row being read in -func (m *Manager) SetPatternDelay(rept int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.SetPatternDelay(rept) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetPatternDelay(rept) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go deleted file mode 100644 index 2f5f09e..0000000 --- a/format/s3m/playback/playback_pattern.go +++ /dev/null @@ -1,163 +0,0 @@ -package playback - -import ( - "errors" - "time" - - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/player/state" - "github.com/gotracker/playback/song" -) - -const ( - tickBaseDuration = time.Duration(2500) * time.Millisecond -) - -func (m *Manager) processPatternRow() error { - patIdx, err := m.pattern.GetCurrentPatternIdx() - if err != nil { - return err - } - - if m.pattern.NeedResetPatternLoops() { - for _, cs := range m.channels { - mem := cs.GetMemory() - pl := mem.GetPatternLoop() - pl.Count = 0 - pl.Enabled = false - } - } - - pat := m.song.GetPattern(patIdx) - if pat == nil { - return song.ErrStopSong - } - - withinPatternLoop := false - for _, cs := range m.channels { - mem := cs.GetMemory() - pl := mem.GetPatternLoop() - if pl.Enabled { - withinPatternLoop = true - break - } - } - - if !withinPatternLoop { - if err := m.pattern.Observe(); err != nil { - return err - } - } - - rows := pat.GetRows() - - myCurrentRow := m.pattern.GetCurrentRow() - - row := rows.GetRow(myCurrentRow) - - preMixRowTxn := m.pattern.StartTransaction() - defer func() { - preMixRowTxn.Cancel() - m.preMixRowTxn = nil - }() - m.preMixRowTxn = preMixRowTxn - - s := m.GetSampler() - if s == nil { - return errors.New("sampler not configured") - } - - if m.rowRenderState == nil { - panmixer := s.GetPanMixer() - - m.rowRenderState = &rowRenderState{ - RenderDetails: state.RenderDetails{ - Mix: s.Mixer(), - SamplerSpeed: s.GetSamplerSpeed(), - Panmixer: panmixer, - }, - } - } - - var resetMemory bool - if myCurrentRow == 0 { - if myCurrentOrder := m.pattern.GetCurrentOrder(); myCurrentOrder == 0 { - resetMemory = true - } - } - - for ch := range m.channels { - cs := &m.channels[ch] - cs.AdvanceRow(&channelDataTransaction{}) - if resetMemory { - mem := cs.GetMemory() - mem.StartOrder() - } - } - - // generate effects and run prestart - channels := row.GetChannels() - for channelNum := range channels { - if channelNum >= m.GetNumChannels() { - continue - } - - cdata := &channels[channelNum] - - cs := &m.channels[channelNum] - if err := cs.SetData(cdata); err != nil { - return err - } - } - - for ch := range m.channels { - cs := &m.channels[ch] - - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitPreRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - } - } - - if err := preMixRowTxn.Commit(); err != nil { - return err - } - - tickDuration := tickBaseDuration / time.Duration(m.pattern.GetTempo()) - - m.rowRenderState.Duration = tickDuration - m.rowRenderState.Samples = int(tickDuration.Seconds() * float64(s.SampleRate)) - m.rowRenderState.ticksThisRow = m.pattern.GetTicksThisRow() - m.rowRenderState.currentTick = 0 - - for _, order := range m.chOrder { - for _, cs := range order { - if cs == nil { - continue - } - - if err := m.processRowForChannel(cs); err != nil { - return err - } - } - } - - return nil -} - -func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory, channel.Data]) error { - mem := cs.GetMemory() - mem.TremorMem().Reset() - - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - - if err := txn.CommitPostRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - } - return nil -} diff --git a/format/s3m/playback/playback_render.go b/format/s3m/playback/playback_render.go deleted file mode 100644 index b946c1e..0000000 --- a/format/s3m/playback/playback_render.go +++ /dev/null @@ -1,113 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state" -) - -// OnTick runs the S3M tick processing -func (m *Manager) OnTick() error { - m.premix = nil - premix, err := m.renderTick() - if err != nil { - return err - } - - m.premix = premix - return nil -} - -// GetPremixData gets the current premix data from the manager -func (m *Manager) GetPremixData() (*output.PremixData, error) { - return m.premix, nil -} - -// RenderOneRow renders the next single row from the song pattern data into a RowRender object -func (m *Manager) renderTick() (*output.PremixData, error) { - postMixRowTxn := m.pattern.StartTransaction() - defer func() { - postMixRowTxn.Cancel() - m.postMixRowTxn = nil - }() - m.postMixRowTxn = postMixRowTxn - - if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { - if err := m.processPatternRow(); err != nil { - return nil, err - } - } - - var finalData render.RowRender - premix := &output.PremixData{ - Userdata: &finalData, - SamplesLen: m.rowRenderState.Samples, - } - - if err := m.soundRenderTick(premix); err != nil { - return nil, err - } - - finalData.Order = int(m.pattern.GetCurrentOrder()) - finalData.Row = int(m.pattern.GetCurrentRow()) - finalData.Tick = m.rowRenderState.currentTick - if m.rowRenderState.currentTick == 0 { - finalData.RowText = m.getRowText() - } - - m.rowRenderState.currentTick++ - if m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { - postMixRowTxn.AdvanceRow = true - } - - if err := postMixRowTxn.Commit(); err != nil { - return nil, err - } - return premix, nil -} - -type rowRenderState struct { - state.RenderDetails - - ticksThisRow int - currentTick int -} - -func (m *Manager) soundRenderTick(premix *output.PremixData) error { - tick := m.rowRenderState.currentTick - var lastTick = (tick+1 == m.rowRenderState.ticksThisRow) - - for ch := range m.channels { - cs := &m.channels[ch] - if m.song.IsChannelEnabled(ch) { - - if err := m.processEffect(ch, cs, tick, lastTick); err != nil { - return err - } - - rr, err := cs.RenderRowTick(m.rowRenderState.RenderDetails, nil) - if err != nil { - return err - } - if rr != nil { - premix.Data = append(premix.Data, rr) - } - } - } - - premix.MixerVolume = m.GetMixerVolume() - return nil -} - -func (m *Manager) ensureOPL2() { - if opl2 := m.GetOPL2Chip(); opl2 == nil { - if s := m.GetSampler(); s != nil { - opl2 = render.NewOPL2Chip(uint32(s.SampleRate)) - opl2.WriteReg(0x01, 0x20) // enable all waveforms - opl2.WriteReg(0x04, 0x00) // clear timer flags - opl2.WriteReg(0x08, 0x40) // clear CSW and set NOTE-SEL - opl2.WriteReg(0xBD, 0x00) // set default notes - m.SetOPL2Chip(opl2) - } - } -} diff --git a/format/s3m/playback/playback_textoutput.go b/format/s3m/playback/playback_textoutput.go deleted file mode 100644 index 086b0ff..0000000 --- a/format/s3m/playback/playback_textoutput.go +++ /dev/null @@ -1,27 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/player/render" -) - -func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { - nCh := 0 - for ch := range m.channels { - if !m.song.IsChannelEnabled(ch) { - continue - } - nCh++ - } - rowText := render.NewRowText[channel.Data](nCh, true) - for ch, cs := range m.channels { - if !m.song.IsChannelEnabled(ch) { - continue - } - - if cd := cs.GetData(); cd != nil { - rowText.Channels[ch] = *cd - } - } - return &rowText -} diff --git a/format/s3m/s3m.go b/format/s3m/s3m.go index d752c54..b7fad94 100644 --- a/format/s3m/s3m.go +++ b/format/s3m/s3m.go @@ -4,13 +4,18 @@ package s3m import ( "io" - "github.com/gotracker/playback" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/load" + s3mSettings "github.com/gotracker/playback/format/s3m/settings" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/song" "github.com/gotracker/playback/util" ) -type format struct{} +type format struct { + common.Format +} var ( // S3M is the exported interface to the S3M file loader @@ -18,7 +23,7 @@ var ( ) // Load loads an S3M file into a playback system -func (f format) Load(filename string, features []feature.Feature) (playback.Playback, error) { +func (f format) Load(filename string, features []feature.Feature) (song.Data, error) { r, err := util.ReadFile(filename) if err != nil { return nil, err @@ -28,6 +33,10 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play } // Load loads an S3M file on a reader into a playback system -func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { +func (format) LoadFromReader(r io.Reader, features []feature.Feature) (song.Data, error) { return load.S3M(r, features) } + +func init() { + machine.RegisterMachine(s3mSettings.GetMachineSettings(true)) +} diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go new file mode 100644 index 0000000..b61cc9b --- /dev/null +++ b/format/s3m/settings/machine.go @@ -0,0 +1,48 @@ +package settings + +import ( + s3mFilter "github.com/gotracker/playback/format/s3m/filter" + s3mOscillator "github.com/gotracker/playback/format/s3m/oscillator" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" +) + +func GetMachineSettings(modLimits bool) *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + if modLimits { + return &amigaMOD31Settings + } + return &amigaS3MSettings +} + +var ( + s3mQuirks = settings.MachineQuirks{ + PreviousPeriodUsesModifiedPeriod: true, + PortaToNoteUsesModifiedPeriod: true, + DoNotProcessEffectsOnMutedChannels: true, + } + + amigaMOD31Settings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + PeriodConverter: s3mPeriod.S3MAmigaConverter, + GetFilterFactory: s3mFilter.Factory, + GetVibratoFactory: s3mOscillator.VibratoFactory, + GetTremoloFactory: s3mOscillator.TremoloFactory, + GetPanbrelloFactory: s3mOscillator.PanbrelloFactory, + VoiceFactory: amigaVoiceFactory, + OPL2Enabled: true, + Quirks: s3mQuirks, + } + + amigaS3MSettings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + PeriodConverter: s3mPeriod.S3MAmigaConverter, + GetFilterFactory: s3mFilter.Factory, + GetVibratoFactory: s3mOscillator.VibratoFactory, + GetTremoloFactory: s3mOscillator.TremoloFactory, + GetPanbrelloFactory: s3mOscillator.PanbrelloFactory, + VoiceFactory: amigaVoiceFactory, + OPL2Enabled: true, + Quirks: s3mQuirks, + } +) diff --git a/format/s3m/settings/voicefactory.go b/format/s3m/settings/voicefactory.go new file mode 100644 index 0000000..580075e --- /dev/null +++ b/format/s3m/settings/voicefactory.go @@ -0,0 +1,19 @@ +package settings + +import ( + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVoice "github.com/gotracker/playback/format/s3m/voice" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" +) + +type voiceFactory struct{} + +func (voiceFactory) NewVoice(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + return s3mVoice.New(config) +} + +var ( + amigaVoiceFactory voiceFactory +) diff --git a/format/s3m/system/system.go b/format/s3m/system/system.go new file mode 100644 index 0000000..4fea21e --- /dev/null +++ b/format/s3m/system/system.go @@ -0,0 +1,42 @@ +package system + +import ( + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/system" +) + +const ( + // DefaultC4SampleRate is the default c4 sample rate for S3M samples + DefaultC4SampleRate = frequency.Frequency(s3mfile.DefaultC2Spd) + // C4Period is the sampler (Amiga-style) period of the C-4 note + C4Period = 1712 + + C4Octave = 4 + C4Note = C4Octave * NotesPerOctave + + // S3MBaseClock is the base clock speed of S3M files + S3MBaseClock frequency.Frequency = DefaultC4SampleRate * C4Period + + NotesPerOctave = 12 + SlideFinesPerSemitone = 4 + SemitonesPerNote = 16 + SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote + SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave + C4SlideFines = C4Note * SlideFinesPerNote +) + +var semitonePeriodTable = [...]uint16{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} + +var S3MSystem system.ClockableSystem = system.ClockedSystem{ + MaxPastNotesPerChannel: 0, + BaseClock: S3MBaseClock, + BaseFinetunes: C4SlideFines, + FinetunesPerOctave: SlideFinesPerOctave, + FinetunesPerNote: SlideFinesPerNote, + CommonPeriod: C4Period, + CommonRate: DefaultC4SampleRate, + SemitonePeriods: semitonePeriodTable, + OctaveShift: 1, +} diff --git a/format/s3m/voice/sampler.go b/format/s3m/voice/sampler.go new file mode 100644 index 0000000..d72bd4b --- /dev/null +++ b/format/s3m/voice/sampler.go @@ -0,0 +1,51 @@ +package voice + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" +) + +type voicerPos interface { + GetPos() sampling.Pos + SetPos(pos sampling.Pos) +} + +type voicerSampler interface { + GetSample(pos sampling.Pos) volume.Matrix +} + +func (v *s3mVoice) GetPos() (sampling.Pos, error) { + if vp, ok := v.voicer.(voicerPos); ok { + return vp.GetPos(), nil + } + return sampling.Pos{}, nil +} + +func (v *s3mVoice) SetPos(pos sampling.Pos) error { + if vp, ok := v.voicer.(voicerPos); ok { + vp.SetPos(pos) + } + return nil +} + +func (v *s3mVoice) GetSample(pos sampling.Pos) volume.Matrix { + var dry volume.Matrix + if sampler, ok := v.voicer.(voicerSampler); ok { + dry = sampler.GetSample(pos) + if dry.Channels == 0 { + dry.Channels = v.voicer.GetNumChannels() + } + } + + vol := v.GetFinalVolume() + wet := dry.Apply(vol) + if v.voiceFilter != nil { + wet = v.voiceFilter.Filter(wet) + } + return wet +} + +func (v s3mVoice) GetSampleRate() frequency.Frequency { + return v.inst.SampleRate +} diff --git a/format/s3m/voice/tracing.go b/format/s3m/voice/tracing.go new file mode 100644 index 0000000..a1ee0bf --- /dev/null +++ b/format/s3m/voice/tracing.go @@ -0,0 +1,25 @@ +package voice + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" +) + +func (v s3mVoice) DumpState(ch index.Channel, t tracing.Tracer) { + if t == nil { + return + } + + v.KeyModulator.DumpState(ch, t, "s3mVoice.KeyModulator") + if v.voicer != nil { + v.voicer.DumpState(ch, t, "s3mVoice.voicer") + } else { + t.TraceChannelWithComment(ch, "nil", "s3mVoice.voicer") + } + v.AmpModulator.DumpState(ch, t, "s3mVoice.amp") + v.FreqModulator.DumpState(ch, t, "s3mVoice.freq") + v.PanModulator.DumpState(ch, t, "s3mVoice.pan") + v.vol0Opt.DumpState(ch, t, "s3mVoice.vol0Opt") + //voiceFilter + //pluginFilter +} diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go new file mode 100644 index 0000000..f4803bc --- /dev/null +++ b/format/s3m/voice/voice.go @@ -0,0 +1,252 @@ +package voice + +import ( + "errors" + "fmt" + + "github.com/gotracker/opl2" + + "github.com/gotracker/playback/filter" + s3mFilter "github.com/gotracker/playback/format/s3m/filter" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" + s3mSystem "github.com/gotracker/playback/format/s3m/system" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/component" +) + +type s3mVoice struct { + inst *instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + opl2Chip *opl2.Chip + opl2Channel index.OPLChannel + + component.KeyModulator + + stopped bool + voicer component.Voicer[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] + component.AmpModulator[s3mVolume.FineVolume, s3mVolume.Volume] + component.FreqModulator[period.Amiga] + component.PanModulator[s3mPanning.Panning] + opl2 component.OPL2Registers + vol0Opt component.Vol0Optimization + voiceFilter filter.Filter +} + +var ( + _ voice.Sampler = (*s3mVoice)(nil) + _ voice.AmpModulator[s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume] = (*s3mVoice)(nil) + _ voice.FreqModulator[period.Amiga] = (*s3mVoice)(nil) + _ voice.PanModulator[s3mPanning.Panning] = (*s3mVoice)(nil) +) + +func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + v := &s3mVoice{ + opl2Chip: config.OPLChip, + opl2Channel: config.OPLChannel, + } + + v.KeyModulator.Setup(component.KeyModulatorSettings{ + Attack: v.doAttack, + Release: v.doRelease, + Fadeout: v.doFadeout, + DeferredAttack: v.doDeferredAttack, + DeferredRelease: v.doDeferredRelease, + }) + + v.AmpModulator.Setup(component.AmpModulatorSettings[s3mVolume.FineVolume, s3mVolume.Volume]{ + Active: true, + DefaultMixingVolume: config.InitialMixing, + DefaultVolume: config.InitialVolume, + }) + + v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{ + PC: config.PC, + }) + + v.PanModulator.Setup(component.PanModulatorSettings[s3mPanning.Panning]{ + Enabled: config.PanEnabled, + InitialPan: config.InitialPan, + }) + + v.vol0Opt.Setup(config.Vol0Optimization) + + return v +} + +func (v *s3mVoice) SetOPL2Chip(chip *opl2.Chip) { + v.opl2Chip = chip +} + +func (v *s3mVoice) doAttack() { + v.vol0Opt.Reset() + + if v.voicer != nil { + v.voicer.Attack() + } +} + +func (v *s3mVoice) doRelease() { + if v.voicer != nil { + v.voicer.Release() + } +} + +func (v *s3mVoice) doFadeout() { +} + +func (v *s3mVoice) doDeferredAttack() { + if v.voicer != nil { + v.voicer.DeferredAttack() + } +} + +func (v *s3mVoice) doDeferredRelease() { + if v.voicer != nil { + v.voicer.DeferredRelease() + } +} + +func (v *s3mVoice) SetPlaybackRate(outputRate frequency.Frequency) error { + if v.voiceFilter != nil { + v.voiceFilter.SetPlaybackRate(outputRate) + } + return nil +} + +func (v *s3mVoice) SetPeriod(p period.Amiga) error { + if p.IsInvalid() { + v.Stop() + return nil + } + return v.FreqModulator.SetPeriod(p) +} + +func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { + v.inst = inst + + switch d := inst.GetData().(type) { + case *instrument.PCM[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]: + if err := v.AmpModulator.SetMixingVolumeOverride(d.MixingVolume); err != nil { + return err + } + + var s component.Sampler[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] + s.Setup(component.SamplerSettings[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume]{ + Sample: d.Sample, + DefaultVolume: inst.GetDefaultVolume(), + MixVolume: s3mVolume.MaxFineVolume, + WholeLoop: d.Loop, + SustainLoop: d.SustainLoop, + }) + v.voicer = &s + + case *instrument.OPL2: + if err := v.KeyModulator.SetAttackTriggersRelease(true); err != nil { + return err + } + + var o component.OPL2[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] + o.Setup(v.opl2Chip, int(v.opl2Channel), v.opl2, s3mPeriod.S3MAmigaConverter, s3mSystem.DefaultC4SampleRate, inst.GetDefaultVolume()) + v.voicer = &o + + default: + return fmt.Errorf("unhandled instrument type: %T", d) + } + if inst == nil { + return errors.New("instrument is nil") + } + + info := inst.GetVoiceFilterInfo() + f, err := s3mFilter.Factory(info.Name, inst.SampleRate, info.Params) + if err != nil { + return fmt.Errorf("filter factory(%q) error: %w", info.Name, err) + } + v.voiceFilter = f + + v.Reset() + return nil +} + +func (v *s3mVoice) Reset() error { + v.stopped = false + return errors.Join( + v.AmpModulator.Reset(), + v.FreqModulator.Reset(), + v.PanModulator.Reset(), + v.vol0Opt.Reset(), + ) +} + +func (v *s3mVoice) Stop() { + v.stopped = true +} + +func (v s3mVoice) IsMuted() bool { + return v.AmpModulator.IsMuted() +} + +func (v s3mVoice) IsDone() bool { + if v.voicer == nil || v.stopped { + return true + } + + return v.vol0Opt.IsDone() +} + +func (v *s3mVoice) Tick() error { + // has to be after the mod/env updates + v.KeyModulator.DeferredUpdate() + + if o, ok := v.voicer.(*component.OPL2[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume]); ok { + fp, err := v.GetFinalPeriod() + if err != nil { + return err + } + o.Advance(v.GetFinalVolume(), fp) + } + + v.KeyModulator.Advance() + return nil +} + +func (v *s3mVoice) RowEnd() error { + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + return nil +} + +func (v *s3mVoice) Clone(bool) voice.Voice { + vv := s3mVoice{ + inst: v.inst, + opl2Chip: v.opl2Chip, + opl2Channel: v.opl2Channel, + stopped: v.stopped, + AmpModulator: v.AmpModulator.Clone(), + FreqModulator: v.FreqModulator.Clone(), + PanModulator: v.PanModulator.Clone(), + opl2: v.opl2.Clone(), + vol0Opt: v.vol0Opt.Clone(), + } + + vv.KeyModulator = v.KeyModulator.Clone(component.KeyModulatorSettings{ + Attack: vv.doAttack, + Release: vv.doRelease, + Fadeout: vv.doFadeout, + DeferredAttack: vv.doDeferredAttack, + DeferredRelease: vv.doDeferredRelease, + }) + + if v.voicer != nil { + vv.voicer = v.voicer.Clone() + } + + if v.voiceFilter != nil { + vv.voiceFilter = v.voiceFilter.Clone() + } + + return &vv +} diff --git a/format/s3m/volume/finevolume.go b/format/s3m/volume/finevolume.go new file mode 100644 index 0000000..ba15d50 --- /dev/null +++ b/format/s3m/volume/finevolume.go @@ -0,0 +1,53 @@ +package volume + +import ( + "math" + + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/voice/types" +) + +const ( + MaxFineVolume = FineVolume(0x7f) +) + +type FineVolume s3mfile.Volume + +var ( + _ types.VolumeMaxer[FineVolume] = FineVolume(0) + _ types.VolumeDeltaer[FineVolume] = FineVolume(0) +) + +const finevolCoeff = volume.Volume(1) / volume.Volume(MaxFineVolume) + +func (v FineVolume) ToVolume() volume.Volume { + if v != FineVolume(s3mfile.EmptyVolume) { + return volume.Volume(min(v, MaxFineVolume)) * finevolCoeff + } + return volume.VolumeUseInstVol +} + +func (v FineVolume) IsInvalid() bool { + return v > MaxFineVolume && v != FineVolume(s3mfile.EmptyVolume) +} + +func (v FineVolume) IsUseInstrumentVol() bool { + return v == FineVolume(s3mfile.EmptyVolume) +} + +func (FineVolume) GetMax() FineVolume { + return MaxFineVolume +} + +func (v FineVolume) FMA(multiplier, add float32) FineVolume { + if v == FineVolume(s3mfile.EmptyVolume) { + return v + } + + return min(FineVolume(max(math.FMA(float64(v), float64(multiplier), float64(add)), 0)), MaxFineVolume) +} + +func (v FineVolume) AddDelta(d types.VolumeDelta) FineVolume { + return FineVolume(min(max(int16(v)+int16(d), 0), int16(MaxFineVolume))) +} diff --git a/format/s3m/volume/volume.go b/format/s3m/volume/volume.go index 44a05e7..f2d0ba5 100644 --- a/format/s3m/volume/volume.go +++ b/format/s3m/volume/volume.go @@ -1,25 +1,74 @@ package volume import ( + "math" + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/voice/types" +) + +const ( + MaxVolume = Volume(0x40) ) var ( + DefaultS3MVolume = Volume(s3mfile.DefaultVolume) + // DefaultVolume is the default volume value for most everything in S3M format - DefaultVolume = VolumeFromS3M(s3mfile.DefaultVolume) + DefaultVolume = VolumeFromS3M(DefaultS3MVolume) +) + +type Volume s3mfile.Volume + +var ( + _ types.VolumeMaxer[Volume] = Volume(0) + _ types.VolumeDeltaer[Volume] = Volume(0) ) +const volCoeff = volume.Volume(1) / volume.Volume(MaxVolume) + +func (v Volume) ToVolume() volume.Volume { + if v != Volume(s3mfile.EmptyVolume) { + return volume.Volume(min(v, 0x3f)) * volCoeff + } + return volume.VolumeUseInstVol +} + +func (v Volume) IsInvalid() bool { + return v > MaxVolume && v != Volume(s3mfile.EmptyVolume) +} + +func (v Volume) IsUseInstrumentVol() bool { + return v == Volume(s3mfile.EmptyVolume) +} + +func (Volume) GetMax() Volume { + return MaxVolume +} + +func (v Volume) FMA(multiplier, add float32) Volume { + if v == Volume(s3mfile.EmptyVolume) { + return v + } + + return Volume(min(max(math.FMA(float64(v), float64(multiplier), float64(add)), 0), float64(MaxVolume))) +} + +func (v Volume) AddDelta(d types.VolumeDelta) Volume { + return Volume(min(max(int16(v)+int16(d), 0), int16(MaxVolume))) +} + // VolumeFromS3M converts an S3M volume to a player volume -func VolumeFromS3M(vol s3mfile.Volume) volume.Volume { +func VolumeFromS3M(vol Volume) volume.Volume { var v volume.Volume switch { - case vol == s3mfile.EmptyVolume: + case vol == Volume(s3mfile.EmptyVolume): v = volume.VolumeUseInstVol case vol >= 63: - v = volume.Volume(63.0) / 64.0 + v = volume.Volume(63.0) / volume.Volume(MaxVolume) case vol < 63: - v = volume.Volume(vol) / 64.0 + v = volume.Volume(vol) / volume.Volume(MaxVolume) default: v = 0.0 } @@ -27,12 +76,12 @@ func VolumeFromS3M(vol s3mfile.Volume) volume.Volume { } // VolumeToS3M converts a player volume to an S3M volume -func VolumeToS3M(v volume.Volume) s3mfile.Volume { +func VolumeToS3M(v volume.Volume) Volume { switch { case v == volume.VolumeUseInstVol: - return s3mfile.EmptyVolume + return Volume(s3mfile.EmptyVolume) default: - return s3mfile.Volume(v * 64.0) + return Volume(v * volume.Volume(MaxVolume)) } } diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index 0d3a0e5..9263aeb 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -7,10 +7,16 @@ import ( xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback" xmNote "github.com/gotracker/playback/format/xm/note" + xmPanning "github.com/gotracker/playback/format/xm/panning" xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/instruction" + "github.com/gotracker/playback/song" ) type Command uint8 @@ -30,7 +36,7 @@ func (c Command) ToRune() rune { type DataEffect uint8 // Data is the data for the channel -type Data struct { +type Data[TPeriod period.Period] struct { What xmfile.ChannelFlags Note uint8 Instrument uint8 @@ -40,37 +46,27 @@ type Data struct { } // HasNote returns true if there exists a note on the channel -func (d Data) HasNote() bool { +func (d Data[TPeriod]) HasNote() bool { return d.What.HasNote() } // GetNote returns the note for the channel -func (d Data) GetNote() note.Note { +func (d Data[TPeriod]) GetNote() note.Note { return xmNote.FromXmNote(d.Note) } // HasInstrument returns true if there exists an instrument on the channel -func (d Data) HasInstrument() bool { +func (d Data[TPeriod]) HasInstrument() bool { return d.What.HasInstrument() } // GetInstrument returns the instrument for the channel -func (d Data) GetInstrument(stmem note.Semitone) instrument.ID { - st := stmem - if d.HasNote() { - n := d.GetNote() - if nn, ok := n.(note.Normal); ok { - st = note.Semitone(nn) - } - } - return SampleID{ - InstID: d.Instrument, - Semitone: st, - } +func (d Data[TPeriod]) GetInstrument() int { + return int(d.Instrument) } // HasVolume returns true if there exists a volume on the channel -func (d Data) HasVolume() bool { +func (d Data[TPeriod]) HasVolume() bool { if !d.What.HasVolume() { return false } @@ -79,12 +75,16 @@ func (d Data) HasVolume() bool { } // GetVolume returns the volume for the channel -func (d Data) GetVolume() volume.Volume { +func (d Data[TPeriod]) GetVolumeGeneric() volume.Volume { return d.Volume.Volume() } +func (d Data[TPeriod]) GetVolume() xmVolume.XmVolume { + return xmVolume.XmVolume(d.Volume.Volume() * 64) +} + // HasCommand returns true if there exists a command on the channel -func (d Data) HasCommand() bool { +func (d Data[TPeriod]) HasCommand() bool { if d.What.HasEffect() || d.What.HasEffectParameter() { return true } @@ -97,11 +97,18 @@ func (d Data) HasCommand() bool { } // Channel returns the channel ID for the channel -func (d Data) Channel() uint8 { +func (d Data[TPeriod]) Channel() uint8 { return 0 } -func (Data) getNoteString(n note.Note) string { +func (d Data[TPeriod]) GetEffects(mem *Memory, periodType period.Period) []playback.Effect { + if e := EffectFactory[TPeriod](mem, d); e != nil { + return []playback.Effect{e} + } + return nil +} + +func (Data[TPeriod]) getNoteString(n note.Note) string { switch note.Type(n) { case note.SpecialTypeRelease: return "== " @@ -112,7 +119,7 @@ func (Data) getNoteString(n note.Note) string { } } -func (d Data) String() string { +func (d Data[TPeriod]) String() string { pieces := []string{ "...", // note " ", // inst @@ -135,9 +142,24 @@ func (d Data) String() string { return strings.Join(pieces, " ") } -func (d Data) ShortString() string { +func (d Data[TPeriod]) ShortString() string { if d.HasNote() { return d.getNoteString(d.GetNote()) } return "..." } + +func (d Data[TPeriod]) ToInstructions(m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], ch index.Channel, songData song.Data) ([]instruction.Instruction, error) { + var instructions []instruction.Instruction + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return nil, err + } + + if e := EffectFactory[TPeriod](mem, d); e != nil { + instructions = append(instructions, e) + } + + return instructions, nil +} diff --git a/format/xm/channel/effect_arpeggio.go b/format/xm/channel/effect_arpeggio.go new file mode 100644 index 0000000..4c7451b --- /dev/null +++ b/format/xm/channel/effect_arpeggio.go @@ -0,0 +1,32 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Arpeggio defines an arpeggio effect +type Arpeggio[TPeriod period.Period] DataEffect // '0' + +func (e Arpeggio[TPeriod]) String() string { + return fmt.Sprintf("0%0.2x", DataEffect(e)) +} + +func (e Arpeggio[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + xy := DataEffect(e) + if xy == 0 { + return nil + } + + x, y := int8(xy>>4), int8(xy&0x0f) + return doArpeggio[TPeriod](ch, m, tick, x, y) +} + +func (e Arpeggio[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_extrafineportadown.go b/format/xm/channel/effect_extrafineportadown.go new file mode 100644 index 0000000..a6d1d2f --- /dev/null +++ b/format/xm/channel/effect_extrafineportadown.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ExtraFinePortaDown defines an extra-fine portamento down effect +type ExtraFinePortaDown[TPeriod period.Period] DataEffect // 'X2x' + +func (e ExtraFinePortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e ExtraFinePortaDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := mem.ExtraFinePortaDown(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(y)*1) +} + +func (e ExtraFinePortaDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_extrafineportaup.go b/format/xm/channel/effect_extrafineportaup.go new file mode 100644 index 0000000..99cab71 --- /dev/null +++ b/format/xm/channel/effect_extrafineportaup.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// ExtraFinePortaUp defines an extra-fine portamento up effect +type ExtraFinePortaUp[TPeriod period.Period] DataEffect // 'X1x' + +func (e ExtraFinePortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e ExtraFinePortaUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := mem.ExtraFinePortaUp(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(y)*1) +} + +func (e ExtraFinePortaUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_fineportadown.go b/format/xm/channel/effect_fineportadown.go new file mode 100644 index 0000000..8b9f3c5 --- /dev/null +++ b/format/xm/channel/effect_fineportadown.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePortaDown defines an fine portamento down effect +type FinePortaDown[TPeriod period.Period] DataEffect // 'E2x' + +func (e FinePortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e FinePortaDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := mem.FinePortaDown(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(y)*4) +} + +func (e FinePortaDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_fineportaup.go b/format/xm/channel/effect_fineportaup.go new file mode 100644 index 0000000..b1e380c --- /dev/null +++ b/format/xm/channel/effect_fineportaup.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FinePortaUp defines an fine portamento up effect +type FinePortaUp[TPeriod period.Period] DataEffect // 'E1x' + +func (e FinePortaUp[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e FinePortaUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := mem.FinePortaUp(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(y)*4) +} + +func (e FinePortaUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_finevolslidedown.go b/format/xm/channel/effect_finevolslidedown.go new file mode 100644 index 0000000..d354a60 --- /dev/null +++ b/format/xm/channel/effect_finevolslidedown.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVolumeSlideDown defines a volume slide effect +type FineVolumeSlideDown[TPeriod period.Period] DataEffect // 'EAx' + +func (e FineVolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e FineVolumeSlideDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := mem.FineVolumeSlideDown(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.SlideChannelVolume(ch, 1.0, -float32(y)) +} + +func (e FineVolumeSlideDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_finevolslideup.go b/format/xm/channel/effect_finevolslideup.go new file mode 100644 index 0000000..a802503 --- /dev/null +++ b/format/xm/channel/effect_finevolslideup.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// FineVolumeSlideUp defines a volume slide effect +type FineVolumeSlideUp[TPeriod period.Period] DataEffect // 'EAx' + +func (e FineVolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e FineVolumeSlideUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + y := mem.FineVolumeSlideUp(DataEffect(e)) & 0x0F + + if tick != 0 { + return nil + } + + return m.SlideChannelVolume(ch, 1.0, float32(y)) +} + +func (e FineVolumeSlideUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_globalvolumeslide.go b/format/xm/channel/effect_globalvolumeslide.go new file mode 100644 index 0000000..3c4125e --- /dev/null +++ b/format/xm/channel/effect_globalvolumeslide.go @@ -0,0 +1,42 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// GlobalVolumeSlide defines a global volume slide effect +type GlobalVolumeSlide[TPeriod period.Period] DataEffect // 'H' + +func (e GlobalVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("H%0.2x", DataEffect(e)) +} + +func (e GlobalVolumeSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.GlobalVolumeSlide(DataEffect(e)) + + if tick == 0 { + return nil + } + + if x == 0 { + return m.SlideGlobalVolume(1, -float32(y)) + } else if y == 0 { + return m.SlideGlobalVolume(1, float32(y)) + } + return nil +} + +func (e GlobalVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_notecut.go b/format/xm/channel/effect_notecut.go new file mode 100644 index 0000000..a1c86f6 --- /dev/null +++ b/format/xm/channel/effect_notecut.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NoteCut defines a note cut effect +type NoteCut[TPeriod period.Period] DataEffect // 'ECx' + +func (e NoteCut[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e NoteCut[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + x := DataEffect(e) & 0x0F + return m.SetChannelNoteAction(ch, note.ActionCut, int(x)) +} + +func (e NoteCut[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_notedelay.go b/format/xm/channel/effect_notedelay.go new file mode 100644 index 0000000..19d8fa7 --- /dev/null +++ b/format/xm/channel/effect_notedelay.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// NoteDelay defines a note delay effect +type NoteDelay[TPeriod period.Period] DataEffect // 'EDx' + +func (e NoteDelay[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e NoteDelay[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + x := DataEffect(e) & 0x0F + return m.SetChannelNoteAction(ch, note.ActionRetrigger, int(x)) +} + +func (e NoteDelay[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_orderjump.go b/format/xm/channel/effect_orderjump.go new file mode 100644 index 0000000..9a622c0 --- /dev/null +++ b/format/xm/channel/effect_orderjump.go @@ -0,0 +1,26 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// OrderJump defines an order jump effect +type OrderJump[TPeriod period.Period] DataEffect // 'B' + +func (e OrderJump[TPeriod]) String() string { + return fmt.Sprintf("B%0.2x", DataEffect(e)) +} + +func (e OrderJump[TPeriod]) RowEnd(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + return m.SetOrder(index.Order(e)) +} + +func (e OrderJump[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_panslide.go b/format/xm/channel/effect_panslide.go new file mode 100644 index 0000000..34e08c8 --- /dev/null +++ b/format/xm/channel/effect_panslide.go @@ -0,0 +1,40 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PanSlide defines a pan slide effect +type PanSlide[TPeriod period.Period] DataEffect // 'Pxx' + +func (e PanSlide[TPeriod]) String() string { + return fmt.Sprintf("P%0.2x", DataEffect(e)) +} + +func (e PanSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + xx := DataEffect(e) + x, y := xx>>4, xx&0x0F + + if x == 0 { + // slide left y units + if err := m.SlideChannelPan(ch, 1, -float32(y)); err != nil { + return err + } + } else if y == 0 { + // slide right x units + if err := m.SlideChannelPan(ch, 1, float32(x)); err != nil { + return err + } + } + return nil +} + +func (e PanSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_patterndelay.go b/format/xm/channel/effect_patterndelay.go new file mode 100644 index 0000000..0dec3c3 --- /dev/null +++ b/format/xm/channel/effect_patterndelay.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PatternDelay defines a pattern delay effect +type PatternDelay[TPeriod period.Period] DataEffect // 'SEx' + +func (e PatternDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} + +func (e PatternDelay[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + times := int(DataEffect(e) & 0x0F) + return m.RowRepeat(times) +} + +func (e PatternDelay[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_patternloop.go b/format/xm/channel/effect_patternloop.go new file mode 100644 index 0000000..bdff5f4 --- /dev/null +++ b/format/xm/channel/effect_patternloop.go @@ -0,0 +1,34 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PatternLoop defines a pattern loop effect +type PatternLoop[TPeriod period.Period] DataEffect // 'E6x' + +func (e PatternLoop[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e PatternLoop[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + x := DataEffect(e) & 0x0F + + if x == 0 { + // set loop start + return m.SetPatternLoopStart(ch) + } else { + // set loop end + count + return m.SetPatternLoops(ch, int(x)) + } +} + +func (e PatternLoop[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_portadown.go b/format/xm/channel/effect_portadown.go new file mode 100644 index 0000000..6e0e58b --- /dev/null +++ b/format/xm/channel/effect_portadown.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaDown defines a portamento down effect +type PortaDown[TPeriod period.Period] DataEffect // '2' + +func (e PortaDown[TPeriod]) String() string { + return fmt.Sprintf("2%0.2x", DataEffect(e)) +} + +func (e PortaDown[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.PortaDown(DataEffect(e)) + + if tick == 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) +} + +func (e PortaDown[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_portatonote.go b/format/xm/channel/effect_portatonote.go new file mode 100644 index 0000000..c840190 --- /dev/null +++ b/format/xm/channel/effect_portatonote.go @@ -0,0 +1,40 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaToNote defines a portamento-to-note effect +type PortaToNote[TPeriod period.Period] DataEffect // '3' + +func (e PortaToNote[TPeriod]) String() string { + return fmt.Sprintf("3%0.2x", DataEffect(e)) +} + +func (e PortaToNote[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + return m.StartChannelPortaToNote(ch) +} + +func (e PortaToNote[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + if tick == 0 { + return nil + } + + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.PortaToNote(DataEffect(e)) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) +} + +func (e PortaToNote[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_portaup.go b/format/xm/channel/effect_portaup.go new file mode 100644 index 0000000..ad88406 --- /dev/null +++ b/format/xm/channel/effect_portaup.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// PortaUp defines a portamento up effect +type PortaUp[TPeriod period.Period] DataEffect // '1' + +func (e PortaUp[TPeriod]) String() string { + return fmt.Sprintf("1%0.2x", DataEffect(e)) +} + +func (e PortaUp[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.PortaUp(DataEffect(e)) + + if tick == 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) +} + +func (e PortaUp[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_portavolslide.go b/format/xm/channel/effect_portavolslide.go new file mode 100644 index 0000000..2cae5f8 --- /dev/null +++ b/format/xm/channel/effect_portavolslide.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" +) + +// PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect +type PortaVolumeSlide[TPeriod period.Period] struct { // '5' + playback.CombinedEffect[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning, *Memory, Data[TPeriod]] +} + +// NewPortaVolumeSlide creates a new PortaVolumeSlide object +func NewPortaVolumeSlide[TPeriod period.Period](val DataEffect) PortaVolumeSlide[TPeriod] { + pvs := PortaVolumeSlide[TPeriod]{} + pvs.Effects = append(pvs.Effects, VolumeSlide[TPeriod](val), PortaToNote[TPeriod](0x00)) + return pvs +} + +func (e PortaVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("5%0.2x", DataEffect(any(e.Effects[0]).(VolumeSlide[TPeriod]))) +} + +func (e PortaVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_retriggernote.go b/format/xm/channel/effect_retriggernote.go new file mode 100644 index 0000000..129ee79 --- /dev/null +++ b/format/xm/channel/effect_retriggernote.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RetriggerNote defines a retriggering effect +type RetriggerNote[TPeriod period.Period] DataEffect // 'E9x' + +func (e RetriggerNote[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e RetriggerNote[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + y := DataEffect(e) & 0x0F + return m.SetChannelNoteAction(ch, note.ActionRetrigger, int(y)) +} + +func (e RetriggerNote[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_retrigvolslide.go b/format/xm/channel/effect_retrigvolslide.go new file mode 100644 index 0000000..ebf85fb --- /dev/null +++ b/format/xm/channel/effect_retrigvolslide.go @@ -0,0 +1,73 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RetrigVolumeSlide defines a retriggering volume slide effect +type RetrigVolumeSlide[TPeriod period.Period] DataEffect // 'R' + +func (e RetrigVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("R%0.2x", DataEffect(e)) +} + +func (e RetrigVolumeSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + x := DataEffect(e) >> 4 // vol slide instruction + y := DataEffect(e) & 0x0F // number of ticks between retriggers + + if (tick % int(y+1)) != 0 { + return nil + } + + if err := m.SetChannelNoteAction(ch, note.ActionRetrigger, tick); err != nil { + return err + } + + switch x { + case 0: // nothing + fallthrough + default: + + case 1: // -1 + return m.SlideChannelVolume(ch, 1, -1) + case 2: // -2 + return m.SlideChannelVolume(ch, 1, -2) + case 3: // -4 + return m.SlideChannelVolume(ch, 1, -4) + case 4: // -8 + return m.SlideChannelVolume(ch, 1, -8) + case 5: // -16 + return m.SlideChannelVolume(ch, 1, -16) + case 6: // * 2/3 + return m.SlideChannelVolume(ch, 2.0/3.0, 0) + case 7: // * 1/2 + return m.SlideChannelVolume(ch, 1.0/2.0, 0) + case 8: // ? + case 9: // +1 + return m.SlideChannelVolume(ch, 1, 1) + case 10: // +2 + return m.SlideChannelVolume(ch, 1, 2) + case 11: // +4 + return m.SlideChannelVolume(ch, 1, 4) + case 12: // +8 + return m.SlideChannelVolume(ch, 1, 8) + case 13: // +16 + return m.SlideChannelVolume(ch, 1, 16) + case 14: // * 3/2 + return m.SlideChannelVolume(ch, 3.0/2.0, 0) + case 15: // * 2 + return m.SlideChannelVolume(ch, 2, 0) + } + return nil +} + +func (e RetrigVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_rowjump.go b/format/xm/channel/effect_rowjump.go new file mode 100644 index 0000000..da086b6 --- /dev/null +++ b/format/xm/channel/effect_rowjump.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// RowJump defines a row jump effect +type RowJump[TPeriod period.Period] DataEffect // 'D' + +func (e RowJump[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) +} + +func (e RowJump[TPeriod]) RowEnd(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + xy := DataEffect(e) + x, y := xy>>4, xy&0x0f + row := index.Row(x*10 + y) + + return m.SetRow(row, true) +} + +func (e RowJump[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_sampleoffset.go b/format/xm/channel/effect_sampleoffset.go new file mode 100644 index 0000000..5b9f2bd --- /dev/null +++ b/format/xm/channel/effect_sampleoffset.go @@ -0,0 +1,34 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/gomixing/sampling" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SampleOffset defines a sample offset effect +type SampleOffset[TPeriod period.Period] DataEffect // '9' + +func (e SampleOffset[TPeriod]) String() string { + return fmt.Sprintf("9%0.2x", DataEffect(e)) +} + +func (e SampleOffset[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + xx := mem.SampleOffset(DataEffect(e)) + return m.SetChannelPos(ch, sampling.Pos{Pos: int(xx) * 0x100}) +} + +func (e SampleOffset[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setcoarsepanposition.go b/format/xm/channel/effect_setcoarsepanposition.go new file mode 100644 index 0000000..fbecf34 --- /dev/null +++ b/format/xm/channel/effect_setcoarsepanposition.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetCoarsePanPosition defines a set pan position effect +type SetCoarsePanPosition[TPeriod period.Period] DataEffect // 'E8x' + +func (e SetCoarsePanPosition[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e SetCoarsePanPosition[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + pan := xmPanning.Panning((e & 0x0F) << 4) + return m.SetChannelPan(ch, pan) +} + +func (e SetCoarsePanPosition[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setenvelopeposition.go b/format/xm/channel/effect_setenvelopeposition.go new file mode 100644 index 0000000..c1f811a --- /dev/null +++ b/format/xm/channel/effect_setenvelopeposition.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetEnvelopePosition defines a set envelope position effect +type SetEnvelopePosition[TPeriod period.Period] DataEffect // 'Lxx' + +func (e SetEnvelopePosition[TPeriod]) String() string { + return fmt.Sprintf("L%0.2x", DataEffect(e)) +} + +func (e SetEnvelopePosition[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + return m.SetChannelEnvelopePositions(ch, int(e)) +} + +func (e SetEnvelopePosition[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setfinetune.go b/format/xm/channel/effect_setfinetune.go new file mode 100644 index 0000000..dc94044 --- /dev/null +++ b/format/xm/channel/effect_setfinetune.go @@ -0,0 +1,36 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetFinetune defines a mod-style set finetune effect +type SetFinetune[TPeriod period.Period] DataEffect // 'E5x' + +func (e SetFinetune[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e SetFinetune[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err + } + + if inst != nil { + ft := (note.Finetune(e&0x0F) - 8) * 4 + inst.SetFinetune(ft) + } + return nil +} + +func (e SetFinetune[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setglobalvolume.go b/format/xm/channel/effect_setglobalvolume.go new file mode 100644 index 0000000..c3effba --- /dev/null +++ b/format/xm/channel/effect_setglobalvolume.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetGlobalVolume defines a set global volume effect +type SetGlobalVolume[TPeriod period.Period] DataEffect // 'G' + +func (e SetGlobalVolume[TPeriod]) String() string { + return fmt.Sprintf("G%0.2x", DataEffect(e)) +} + +func (e SetGlobalVolume[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + v := xmVolume.XmVolume(DataEffect(max(e, 0x40))) + return m.SetGlobalVolume(v) +} + +func (e SetGlobalVolume[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setpanposition.go b/format/xm/channel/effect_setpanposition.go new file mode 100644 index 0000000..d6e7802 --- /dev/null +++ b/format/xm/channel/effect_setpanposition.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetPanPosition defines a set pan position effect +type SetPanPosition[TPeriod period.Period] DataEffect // '8xx' + +func (e SetPanPosition[TPeriod]) String() string { + return fmt.Sprintf("8%0.2x", DataEffect(e)) +} + +func (e SetPanPosition[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + if tick != 0 { + return nil + } + + xx := uint8(e) + return m.SetChannelPan(ch, xmPanning.Panning(xx)) +} + +func (e SetPanPosition[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setspeed.go b/format/xm/channel/effect_setspeed.go new file mode 100644 index 0000000..f679b9b --- /dev/null +++ b/format/xm/channel/effect_setspeed.go @@ -0,0 +1,29 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetSpeed defines a set speed effect +type SetSpeed[TPeriod period.Period] DataEffect // 'F' + +func (e SetSpeed[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e SetSpeed[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + if e == 0 { + return nil + } + return m.SetTempo(int(e)) +} + +func (e SetSpeed[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_settempo.go b/format/xm/channel/effect_settempo.go new file mode 100644 index 0000000..556aad4 --- /dev/null +++ b/format/xm/channel/effect_settempo.go @@ -0,0 +1,29 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetTempo defines a set tempo effect +type SetTempo[TPeriod period.Period] DataEffect // 'F' + +func (e SetTempo[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} + +func (e SetTempo[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + if e < 0x20 { + return nil + } + return m.SetBPM(int(e)) +} + +func (e SetTempo[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_settremolowaveform.go b/format/xm/channel/effect_settremolowaveform.go new file mode 100644 index 0000000..60c9d5d --- /dev/null +++ b/format/xm/channel/effect_settremolowaveform.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetTremoloWaveform defines a set tremolo waveform effect +type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'E7x' + +func (e SetTremoloWaveform[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e SetTremoloWaveform[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorTremolo, oscillator.WaveTableSelect(e&0x0F)) +} + +func (e SetTremoloWaveform[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setvibratowaveform.go b/format/xm/channel/effect_setvibratowaveform.go new file mode 100644 index 0000000..5bb0ddc --- /dev/null +++ b/format/xm/channel/effect_setvibratowaveform.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/oscillator" +) + +// SetVibratoWaveform defines a set vibrato waveform effect +type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'E4x' + +func (e SetVibratoWaveform[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} + +func (e SetVibratoWaveform[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + return m.SetChannelOscillatorWaveform(ch, machine.OscillatorVibrato, oscillator.WaveTableSelect(e&0xf)) +} + +func (e SetVibratoWaveform[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_setvolume.go b/format/xm/channel/effect_setvolume.go new file mode 100644 index 0000000..e36fcab --- /dev/null +++ b/format/xm/channel/effect_setvolume.go @@ -0,0 +1,27 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// SetVolume defines a volume slide effect +type SetVolume[TPeriod period.Period] DataEffect // 'C' + +func (e SetVolume[TPeriod]) String() string { + return fmt.Sprintf("C%0.2x", DataEffect(e)) +} + +func (e SetVolume[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + xx := xmVolume.XmVolume(e) + return m.SetChannelVolume(ch, xx) +} + +func (e SetVolume[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_tremolo.go b/format/xm/channel/effect_tremolo.go new file mode 100644 index 0000000..fe1ee92 --- /dev/null +++ b/format/xm/channel/effect_tremolo.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/types" +) + +// Tremolo defines a tremolo effect +type Tremolo[TPeriod period.Period] DataEffect // '7' + +func (e Tremolo[TPeriod]) String() string { + return fmt.Sprintf("7%0.2x", DataEffect(e)) +} + +func (e Tremolo[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Tremolo(DataEffect(e)) + // NOTE: JBC - XM updates on tick 0, but MOD does not. + // Just have to eat this incompatibility, I guess... + return withOscillatorDo[TPeriod](ch, m, int(x), float32(y)*4, machine.OscillatorTremolo, func(value float32) error { + return m.SetChannelVolumeDelta(ch, types.VolumeDelta(value)) + }) +} + +func (e Tremolo[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_tremor.go b/format/xm/channel/effect_tremor.go new file mode 100644 index 0000000..c8478dc --- /dev/null +++ b/format/xm/channel/effect_tremor.go @@ -0,0 +1,32 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Tremor defines a tremor effect +type Tremor[TPeriod period.Period] DataEffect // 'T' + +func (e Tremor[TPeriod]) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) +} + +func (e Tremor[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Tremor(DataEffect(e)) + return doTremor(ch, m, int(x)+1, int(y)+1) +} + +func (e Tremor[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_vibrato.go b/format/xm/channel/effect_vibrato.go new file mode 100644 index 0000000..918a96c --- /dev/null +++ b/format/xm/channel/effect_vibrato.go @@ -0,0 +1,37 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// Vibrato defines a vibrato effect +type Vibrato[TPeriod period.Period] DataEffect // '4' + +func (e Vibrato[TPeriod]) String() string { + return fmt.Sprintf("4%0.2x", DataEffect(e)) +} + +func (e Vibrato[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.Vibrato(DataEffect(e)) + + // NOTE: JBC - XM updates on tick 0, but MOD does not. + // Just have to eat this incompatibility, I guess... + return withOscillatorDo[TPeriod](ch, m, int(x), float32(y)*4, machine.OscillatorVibrato, func(value float32) error { + return m.SetChannelPeriodDelta(ch, period.Delta(value)) + }) +} + +func (e Vibrato[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_vibratovolslide.go b/format/xm/channel/effect_vibratovolslide.go new file mode 100644 index 0000000..24afcf5 --- /dev/null +++ b/format/xm/channel/effect_vibratovolslide.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" +) + +// VibratoVolumeSlide defines a combination vibrato and volume slide effect +type VibratoVolumeSlide[TPeriod period.Period] struct { // '6' + playback.CombinedEffect[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning, *Memory, Data[TPeriod]] +} + +// NewVibratoVolumeSlide creates a new VibratoVolumeSlide object +func NewVibratoVolumeSlide[TPeriod period.Period](val DataEffect) VibratoVolumeSlide[TPeriod] { + vvs := VibratoVolumeSlide[TPeriod]{} + vvs.Effects = append(vvs.Effects, VolumeSlide[TPeriod](val), Vibrato[TPeriod](0x00)) + return vvs +} + +func (e VibratoVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("6%0.2x", DataEffect(any(e.Effects[0]).(VolumeSlide[TPeriod]))) +} + +func (e VibratoVolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effect_volslide.go b/format/xm/channel/effect_volslide.go new file mode 100644 index 0000000..23fc81d --- /dev/null +++ b/format/xm/channel/effect_volslide.go @@ -0,0 +1,45 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// VolumeSlide defines a volume slide effect +type VolumeSlide[TPeriod period.Period] DataEffect // 'A' + +func (e VolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("A%0.2x", DataEffect(e)) +} + +func (e VolumeSlide[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + x, y := mem.VolumeSlide(DataEffect(e)) + + if tick == 0 { + return nil + } + + if x == 0 { + // vol slide down + return m.SlideChannelVolume(ch, 1, -float32(y)) + } else if y == 0 { + // vol slide up + return m.SlideChannelVolume(ch, 1, float32(x)) + } + + return nil +} + +func (e VolumeSlide[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/effectfactory.go b/format/xm/channel/effectfactory.go new file mode 100644 index 0000000..e8854d7 --- /dev/null +++ b/format/xm/channel/effectfactory.go @@ -0,0 +1,72 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" +) + +type EffectXM = playback.Effect + +// VolEff is a combined effect that includes a volume effect and a standard effect +type VolEff[TPeriod period.Period] struct { + playback.CombinedEffect[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning, *Memory, Data[TPeriod]] + eff EffectXM +} + +func (e VolEff[TPeriod]) String() string { + if e.eff == nil { + return "..." + } + return fmt.Sprint(e.eff) +} + +func (e VolEff[TPeriod]) Names() []string { + names := e.CombinedEffect.Names() + if e.eff != nil { + names = append(names, playback.GetEffectNames(e.eff)...) + } + return names +} + +func (e VolEff[TPeriod]) TraceData() string { + return e.String() +} + +// Factory produces an effect for the provided channel pattern data +func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData[xmVolume.XmVolume]) EffectXM { + if data == nil { + return nil + } + + d, _ := data.(Data[TPeriod]) + if !d.HasCommand() { + return nil + } + + var eff VolEff[TPeriod] + if d.What.HasVolume() { + ve := volumeEffectFactory[TPeriod](mem, d.Volume) + if ve != nil { + eff.Effects = append(eff.Effects, ve) + } + } + + if e := standardEffectFactory[TPeriod](mem, d); e != nil { + eff.Effects = append(eff.Effects, e) + eff.eff = e + } + + switch len(eff.Effects) { + case 0: + return nil + case 1: + return eff.Effects[0] + default: + return &eff + } +} diff --git a/format/xm/channel/effectfactory_standard.go b/format/xm/channel/effectfactory_standard.go new file mode 100644 index 0000000..111aebd --- /dev/null +++ b/format/xm/channel/effectfactory_standard.go @@ -0,0 +1,115 @@ +package channel + +import ( + "github.com/gotracker/playback/period" +) + +func standardEffectFactory[TPeriod period.Period](mem *Memory, cd Data[TPeriod]) EffectXM { + if !cd.What.HasEffect() && !cd.What.HasEffectParameter() { + return nil + } + + switch cd.Effect { + case 0x00: // Arpeggio + return Arpeggio[TPeriod](cd.EffectParameter) + case 0x01: // Porta up + return PortaUp[TPeriod](cd.EffectParameter) + case 0x02: // Porta down + return PortaDown[TPeriod](cd.EffectParameter) + case 0x03: // Tone porta + return PortaToNote[TPeriod](cd.EffectParameter) + case 0x04: // Vibrato + return Vibrato[TPeriod](cd.EffectParameter) + case 0x05: // Tone porta + Volume slide + return NewPortaVolumeSlide[TPeriod](cd.EffectParameter) + case 0x06: // Vibrato + Volume slide + return NewVibratoVolumeSlide[TPeriod](cd.EffectParameter) + case 0x07: // Tremolo + return Tremolo[TPeriod](cd.EffectParameter) + case 0x08: // Set (fine) panning + return SetPanPosition[TPeriod](cd.EffectParameter) + case 0x09: // Sample offset + return SampleOffset[TPeriod](cd.EffectParameter) + case 0x0A: // Volume slide + return VolumeSlide[TPeriod](cd.EffectParameter) + case 0x0B: // Position jump + return OrderJump[TPeriod](cd.EffectParameter) + case 0x0C: // Set volume + return SetVolume[TPeriod](cd.EffectParameter) + case 0x0D: // Pattern break + return RowJump[TPeriod](cd.EffectParameter) + case 0x0E: // extra... + return specialEffectFactory[TPeriod](mem, cd.Effect, cd.EffectParameter) + case 0x0F: // Set tempo/BPM + if cd.EffectParameter < 0x20 { + return SetSpeed[TPeriod](cd.EffectParameter) + } + return SetTempo[TPeriod](cd.EffectParameter) + case 0x10: // Set global volume + return SetGlobalVolume[TPeriod](cd.EffectParameter) + case 0x11: // Global volume slide + return GlobalVolumeSlide[TPeriod](cd.EffectParameter) + + case 0x15: // Set envelope position + return SetEnvelopePosition[TPeriod](cd.EffectParameter) + + case 0x19: // Panning slide + return PanSlide[TPeriod](cd.EffectParameter) + + case 0x1B: // Multi retrig note + return RetrigVolumeSlide[TPeriod](cd.EffectParameter) + + case 0x1D: // Tremor + return Tremor[TPeriod](cd.EffectParameter) + + case 0x21: // Extra fine porta commands + return extraFinePortaEffectFactory[TPeriod](mem, cd.Effect, cd.EffectParameter) + } + return UnhandledCommand[TPeriod]{Command: cd.Effect, Info: cd.EffectParameter} +} + +func extraFinePortaEffectFactory[TPeriod period.Period](mem *Memory, ce Command, cp DataEffect) EffectXM { + switch cp >> 4 { + case 0x0: // none + return nil + case 0x1: // Extra fine porta up + return ExtraFinePortaUp[TPeriod](cp) + case 0x2: // Extra fine porta down + return ExtraFinePortaDown[TPeriod](cp) + } + return UnhandledCommand[TPeriod]{Command: ce, Info: cp} +} + +func specialEffectFactory[TPeriod period.Period](mem *Memory, ce Command, cp DataEffect) EffectXM { + switch cp >> 4 { + case 0x1: // Fine porta up + return FinePortaUp[TPeriod](cp) + case 0x2: // Fine porta down + return FinePortaDown[TPeriod](cp) + //case 0x3: // Set glissando control + // TODO: add glissando functionality + case 0x4: // Set vibrato control + return SetVibratoWaveform[TPeriod](cp) + case 0x5: // Set finetune + return SetFinetune[TPeriod](cp) + case 0x6: // Set loop begin/loop + return PatternLoop[TPeriod](cp) + case 0x7: // Set tremolo control + return SetTremoloWaveform[TPeriod](cp) + case 0x8: // Set coarse panning + return SetCoarsePanPosition[TPeriod](cp) + case 0x9: // Retrig note + return RetriggerNote[TPeriod](cp) + case 0xA: // Fine volume slide up + return FineVolumeSlideUp[TPeriod](cp) + case 0xB: // Fine volume slide down + return FineVolumeSlideDown[TPeriod](cp) + case 0xC: // Note cut + return NoteCut[TPeriod](cp) + case 0xD: // Note delay + return NoteDelay[TPeriod](cp) + case 0xE: // Pattern delay + return PatternDelay[TPeriod](cp) + } + return UnhandledCommand[TPeriod]{Command: ce, Info: cp} +} diff --git a/format/xm/effect/effectfactory_volume.go b/format/xm/channel/effectfactory_volume.go similarity index 55% rename from format/xm/effect/effectfactory_volume.go rename to format/xm/channel/effectfactory_volume.go index 4a57234..e49db15 100644 --- a/format/xm/effect/effectfactory_volume.go +++ b/format/xm/channel/effectfactory_volume.go @@ -1,11 +1,11 @@ -package effect +package channel import ( - "github.com/gotracker/playback/format/xm/channel" xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" ) -func volumeEffectFactory(mem *channel.Memory, v xmVolume.VolEffect) EffectXM { +func volumeEffectFactory[TPeriod period.Period](mem *Memory, v xmVolume.VolEffect) EffectXM { switch { case v <= 0x0f: // nothing return nil @@ -13,27 +13,27 @@ func volumeEffectFactory(mem *channel.Memory, v xmVolume.VolEffect) EffectXM { // really should be v >= 0x10 && v <= 0x50 return nil case v >= 0x60 && v <= 0x6f: // vol slide down - return VolumeSlide(v & 0x0f) + return VolumeSlide[TPeriod](v & 0x0f) case v >= 0x70 && v <= 0x7f: // vol slide up - return VolumeSlide((v & 0x0f) << 4) + return VolumeSlide[TPeriod]((v & 0x0f) << 4) case v >= 0x80 && v <= 0x8f: // fine volume slide down - return FineVolumeSlideDown(v & 0x0f) + return FineVolumeSlideDown[TPeriod](v & 0x0f) case v >= 0x90 && v <= 0x9f: // fine volume slide up - return FineVolumeSlideUp(v & 0x0f) + return FineVolumeSlideUp[TPeriod](v & 0x0f) case v >= 0xA0 && v <= 0xAf: // set vibrato speed - mem.VibratoSpeed(channel.DataEffect(v) & 0x0f) + mem.VibratoSpeed(DataEffect(v) & 0x0f) return nil case v >= 0xB0 && v <= 0xBf: // vibrato vs := mem.VibratoSpeed(0x00) - return Vibrato(vs<<4 | (channel.DataEffect(v) & 0x0f)) + return Vibrato[TPeriod](vs<<4 | (DataEffect(v) & 0x0f)) case v >= 0xC0 && v <= 0xCf: // set panning - return SetCoarsePanPosition(v & 0x0f) + return SetCoarsePanPosition[TPeriod](v & 0x0f) case v >= 0xD0 && v <= 0xDf: // panning slide left - return PanSlide(v & 0x0f) + return PanSlide[TPeriod](v & 0x0f) case v >= 0xE0 && v <= 0xEf: // panning slide right - return PanSlide((v & 0x0f) << 4) + return PanSlide[TPeriod]((v & 0x0f) << 4) case v >= 0xF0 && v <= 0xFf: // tone portamento - return PortaToNote(v & 0x0f) + return PortaToNote[TPeriod](v & 0x0f) } - return UnhandledVolCommand{Vol: v} + return UnhandledVolCommand[TPeriod]{Vol: v} } diff --git a/format/xm/channel/machine.go b/format/xm/channel/machine.go new file mode 100644 index 0000000..0962117 --- /dev/null +++ b/format/xm/channel/machine.go @@ -0,0 +1,51 @@ +package channel + +import ( + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +func withOscillatorDo[TPeriod period.Period](ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], speed int, depth float32, osc machine.Oscillator, fn func(value float32) error) error { + value, err := m.GetNextChannelWavetableValue(ch, speed, depth, machine.OscillatorVibrato) + if err != nil { + return err + } + + return fn(value) +} + +func doArpeggio[TPeriod period.Period](ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], tick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { + switch tick % 3 { + case 0: + fallthrough + default: + return m.DoChannelArpeggio(ch, 0) + case 1: + return m.DoChannelArpeggio(ch, arpSemitoneADelta) + case 2: + return m.DoChannelArpeggio(ch, arpSemitoneBDelta) + } +} + +func doTremor[TPeriod period.Period](ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], onTicks int, offTicks int) error { + mem, err := machine.GetChannelMemory[*Memory](m, ch) + if err != nil { + return err + } + + tremor := mem.TremorMem() + if tremor.IsActive() { + if tremor.Advance() >= onTicks { + tremor.ToggleAndReset() + } + } else { + if tremor.Advance() >= offTicks { + tremor.ToggleAndReset() + } + } + + return m.SetChannelVolumeActive(ch, tremor.IsActive()) +} diff --git a/format/xm/channel/memory.go b/format/xm/channel/memory.go index a37de68..3b05b62 100644 --- a/format/xm/channel/memory.go +++ b/format/xm/channel/memory.go @@ -1,12 +1,8 @@ package channel import ( - "github.com/gotracker/playback/voice/oscillator" - "github.com/gotracker/playback/memory" - oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/tremor" - formatutil "github.com/gotracker/playback/util" ) // Memory is the storage object for custom effect/effect values @@ -30,20 +26,11 @@ type Memory struct { extraFinePortaUp memory.Value[DataEffect] extraFinePortaDown memory.Value[DataEffect] - tremorMem tremor.Tremor - vibratoOscillator oscillator.Oscillator - tremoloOscillator oscillator.Oscillator - patternLoop formatutil.PatternLoop + tremorMem tremor.Tremor Shared *SharedMemory } -// ResetOscillators resets the oscillators to defaults -func (m *Memory) ResetOscillators() { - m.vibratoOscillator = oscillatorImpl.NewProtrackerOscillator() - m.tremoloOscillator = oscillatorImpl.NewProtrackerOscillator() -} - // PortaToNote gets or sets the most recent non-zero value (or input) for Portamento-to-note func (m *Memory) PortaToNote(input DataEffect) DataEffect { return m.portaToNote.Coalesce(input) @@ -139,30 +126,11 @@ func (m *Memory) TremorMem() *tremor.Tremor { return &m.tremorMem } -// VibratoOscillator returns the Vibrato oscillator object -func (m *Memory) VibratoOscillator() oscillator.Oscillator { - return m.vibratoOscillator -} - -// TremoloOscillator returns the Tremolo oscillator object -func (m *Memory) TremoloOscillator() oscillator.Oscillator { - return m.tremoloOscillator -} - -// Retrigger runs certain operations when a note is retriggered func (m *Memory) Retrigger() { - for _, osc := range []oscillator.Oscillator{m.VibratoOscillator(), m.TremoloOscillator()} { - osc.Reset() - } -} - -// GetPatternLoop returns the pattern loop object from the memory -func (m *Memory) GetPatternLoop() *formatutil.PatternLoop { - return &m.patternLoop } // StartOrder is called when the first order's row at tick 0 is started -func (m *Memory) StartOrder() { +func (m *Memory) StartOrder0() { if m.Shared.ResetMemoryAtStartOfOrder0 { m.portaToNote.Reset() m.vibrato.Reset() diff --git a/format/xm/channel/sampid.go b/format/xm/channel/sampid.go index b529a24..7f6512c 100644 --- a/format/xm/channel/sampid.go +++ b/format/xm/channel/sampid.go @@ -2,14 +2,12 @@ package channel import ( "fmt" - - "github.com/gotracker/playback/note" ) // SampleID is an InstrumentID that is a combination of InstID and SampID type SampleID struct { - InstID uint8 - Semitone note.Semitone + InstID uint8 + SampID uint8 } // IsEmpty returns true if the sample ID is empty @@ -17,6 +15,10 @@ func (s SampleID) IsEmpty() bool { return s.InstID == 0 } +func (s SampleID) GetIndexAndSample() (int, int) { + return int(s.InstID) - 1, int(s.SampID) +} + func (s SampleID) String() string { - return fmt.Sprint(s.InstID) + return fmt.Sprintf("%d(%d)", s.InstID, s.SampID) } diff --git a/format/xm/channel/unhandled.go b/format/xm/channel/unhandled.go new file mode 100644 index 0000000..35c0fe2 --- /dev/null +++ b/format/xm/channel/unhandled.go @@ -0,0 +1,66 @@ +package channel + +import ( + "fmt" + + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" +) + +// UnhandledCommand is an unhandled command +type UnhandledCommand[TPeriod period.Period] struct { + Command Command + Info DataEffect +} + +func (e UnhandledCommand[TPeriod]) String() string { + return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) +} + +func (e UnhandledCommand[TPeriod]) Names() []string { + return []string{ + fmt.Sprintf("UnhandledCommand(%s)", e.String()), + } +} + +func (e UnhandledCommand[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + if !m.IgnoreUnknownEffect() { + panic("unhandled command") + } + return nil +} + +func (e UnhandledCommand[TPeriod]) TraceData() string { + return e.String() +} + +//////// + +// UnhandledVolCommand is an unhandled volume command +type UnhandledVolCommand[TPeriod period.Period] struct { + Vol xmVolume.VolEffect +} + +func (e UnhandledVolCommand[TPeriod]) String() string { + return fmt.Sprintf("v%0.2x", e.Vol) +} + +func (e UnhandledVolCommand[TPeriod]) Names() []string { + return []string{ + fmt.Sprintf("UnhandledVolCommand(%s)", e.String()), + } +} + +func (e UnhandledVolCommand[TPeriod]) RowStart(ch index.Channel, m machine.Machine[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + if !m.IgnoreUnknownEffect() { + panic("unhandled command") + } + return nil +} + +func (e UnhandledVolCommand[TPeriod]) TraceData() string { + return e.String() +} diff --git a/format/xm/channel/util.go b/format/xm/channel/util.go new file mode 100644 index 0000000..449b10b --- /dev/null +++ b/format/xm/channel/util.go @@ -0,0 +1,14 @@ +package channel + +import ( + xmVolume "github.com/gotracker/playback/format/xm/volume" +) + +var ( + volSlideTwoThirdsTable = [...]xmVolume.XmVolume{ + 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9, + 10, 10, 11, 11, 12, 13, 13, 14, 15, 15, 16, 16, 17, 18, 18, 19, + 20, 20, 21, 21, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29, + 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, + } +) diff --git a/format/xm/effect/effect_arpeggio.go b/format/xm/effect/effect_arpeggio.go deleted file mode 100644 index dbc4503..0000000 --- a/format/xm/effect/effect_arpeggio.go +++ /dev/null @@ -1,35 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// Arpeggio defines an arpeggio effect -type Arpeggio channel.DataEffect // '0' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - cs.SetPos(cs.GetTargetPos()) - return nil -} - -// Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - xy := channel.DataEffect(e) - if xy == 0 { - return nil - } - - x := int8(xy >> 4) - y := int8(xy & 0x0f) - return doArpeggio(cs, currentTick, x, y) -} - -func (e Arpeggio) String() string { - return fmt.Sprintf("0%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_extrafineportadown.go b/format/xm/effect/effect_extrafineportadown.go deleted file mode 100644 index 4f1ced8..0000000 --- a/format/xm/effect/effect_extrafineportadown.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// ExtraFinePortaDown defines an extra-fine portamento down effect -type ExtraFinePortaDown channel.DataEffect // 'X2x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - xx := mem.ExtraFinePortaDown(channel.DataEffect(e)) - y := xx & 0x0F - - return doPortaDown(cs, float32(y), 1, mem.Shared.LinearFreqSlides) -} - -func (e ExtraFinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_extrafineportaup.go b/format/xm/effect/effect_extrafineportaup.go deleted file mode 100644 index 9281ad5..0000000 --- a/format/xm/effect/effect_extrafineportaup.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// ExtraFinePortaUp defines an extra-fine portamento up effect -type ExtraFinePortaUp channel.DataEffect // 'X1x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - xx := mem.ExtraFinePortaUp(channel.DataEffect(e)) - y := xx & 0x0F - - return doPortaUp(cs, float32(y), 1, mem.Shared.LinearFreqSlides) -} - -func (e ExtraFinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_fineportadown.go b/format/xm/effect/effect_fineportadown.go deleted file mode 100644 index 551da99..0000000 --- a/format/xm/effect/effect_fineportadown.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// FinePortaDown defines an fine portamento down effect -type FinePortaDown channel.DataEffect // 'E2x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - xy := mem.FinePortaDown(channel.DataEffect(e)) - y := xy & 0x0F - - return doPortaDown(cs, float32(y), 4, mem.Shared.LinearFreqSlides) -} - -func (e FinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_fineportaup.go b/format/xm/effect/effect_fineportaup.go deleted file mode 100644 index 657a8ca..0000000 --- a/format/xm/effect/effect_fineportaup.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// FinePortaUp defines an fine portamento up effect -type FinePortaUp channel.DataEffect // 'E1x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - - mem := cs.GetMemory() - xy := mem.FinePortaUp(channel.DataEffect(e)) - y := xy & 0x0F - - return doPortaUp(cs, float32(y), 4, mem.Shared.LinearFreqSlides) -} - -func (e FinePortaUp) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_finevolslidedown.go b/format/xm/effect/effect_finevolslidedown.go deleted file mode 100644 index 27b843c..0000000 --- a/format/xm/effect/effect_finevolslidedown.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// FineVolumeSlideDown defines a volume slide effect -type FineVolumeSlideDown channel.DataEffect // 'EAx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - mem := cs.GetMemory() - xy := mem.FineVolumeSlideDown(channel.DataEffect(e)) - y := channel.DataEffect(xy & 0x0F) - - return doVolSlide(cs, -float32(y), 1.0) -} - -func (e FineVolumeSlideDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_finevolslideup.go b/format/xm/effect/effect_finevolslideup.go deleted file mode 100644 index 0c10ed6..0000000 --- a/format/xm/effect/effect_finevolslideup.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// FineVolumeSlideUp defines a volume slide effect -type FineVolumeSlideUp channel.DataEffect // 'EAx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - mem := cs.GetMemory() - xy := mem.FineVolumeSlideUp(channel.DataEffect(e)) - y := channel.DataEffect(xy & 0x0F) - - return doVolSlide(cs, float32(y), 1.0) -} - -func (e FineVolumeSlideUp) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_globalvolumeslide.go b/format/xm/effect/effect_globalvolumeslide.go deleted file mode 100644 index 23c10ac..0000000 --- a/format/xm/effect/effect_globalvolumeslide.go +++ /dev/null @@ -1,43 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" -) - -// GlobalVolumeSlide defines a global volume slide effect -type GlobalVolumeSlide channel.DataEffect // 'H' - -// Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e GlobalVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) - - if currentTick == 0 { - return nil - } - - m := p.(effectIntf.XM) - - if x == 0 { - // global vol slide down - return doGlobalVolSlide(m, -float32(y), 1.0) - } else if y == 0 { - // global vol slide up - return doGlobalVolSlide(m, float32(y), 1.0) - } - return nil -} - -func (e GlobalVolumeSlide) String() string { - return fmt.Sprintf("H%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_notecut.go b/format/xm/effect/effect_notecut.go deleted file mode 100644 index a343c3e..0000000 --- a/format/xm/effect/effect_notecut.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// NoteCut defines a note cut effect -type NoteCut channel.DataEffect // 'ECx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e NoteCut) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.NoteCut() - } - return nil -} - -func (e NoteCut) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_notedelay.go b/format/xm/effect/effect_notedelay.go deleted file mode 100644 index 559e884..0000000 --- a/format/xm/effect/effect_notedelay.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/note" -) - -// NoteDelay defines a note delay effect -type NoteDelay channel.DataEffect // 'EDx' - -// PreStart triggers when the effect enters onto the channel state -func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(channel.DataEffect(e)&0x0F)) - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e NoteDelay) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_orderjump.go b/format/xm/effect/effect_orderjump.go deleted file mode 100644 index fa62293..0000000 --- a/format/xm/effect/effect_orderjump.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/index" -) - -// OrderJump defines an order jump effect -type OrderJump channel.DataEffect // 'B' - -// Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e OrderJump) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - return p.SetNextOrder(index.Order(e)) -} - -func (e OrderJump) String() string { - return fmt.Sprintf("B%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_panslide.go b/format/xm/effect/effect_panslide.go deleted file mode 100644 index fc10d2b..0000000 --- a/format/xm/effect/effect_panslide.go +++ /dev/null @@ -1,42 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - xmPanning "github.com/gotracker/playback/format/xm/panning" -) - -// PanSlide defines a pan slide effect -type PanSlide channel.DataEffect // 'Pxx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PanSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - xx := channel.DataEffect(e) - x := xx >> 4 - y := xx & 0x0F - - xp := channel.DataEffect(xmPanning.PanningToXm(cs.GetPan())) - if x == 0 { - // slide left y units - if xp < y { - xp = 0 - } else { - xp -= y - } - } else if y == 0 { - // slide right x units - if xp > 0xFF-x { - xp = 0xFF - } else { - xp += x - } - } - cs.SetPan(xmPanning.PanningFromXm(uint8(xp))) - return nil -} - -func (e PanSlide) String() string { - return fmt.Sprintf("P%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_patterndelay.go b/format/xm/effect/effect_patterndelay.go deleted file mode 100644 index f98c46c..0000000 --- a/format/xm/effect/effect_patterndelay.go +++ /dev/null @@ -1,28 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" -) - -// PatternDelay defines a pattern delay effect -type PatternDelay channel.DataEffect // 'SEx' - -// PreStart triggers when the effect enters onto the channel state -func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - m := p.(effectIntf.XM) - return m.SetPatternDelay(int(channel.DataEffect(e) & 0x0F)) -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e PatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_patternloop.go b/format/xm/effect/effect_patternloop.go deleted file mode 100644 index 1be3284..0000000 --- a/format/xm/effect/effect_patternloop.go +++ /dev/null @@ -1,40 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// PatternLoop defines a pattern loop effect -type PatternLoop channel.DataEffect // 'E6x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xF - - mem := cs.GetMemory() - pl := mem.GetPatternLoop() - if x == 0 { - // set loop - pl.Start = p.GetCurrentRow() - } else { - if !pl.Enabled { - pl.Enabled = true - pl.Total = uint8(x) - pl.End = p.GetCurrentRow() - pl.Count = 0 - } - if row, ok := pl.ContinueLoop(p.GetCurrentRow()); ok { - return p.SetNextRowWithBacktrack(row, true) - } - } - return nil -} - -func (e PatternLoop) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_portadown.go b/format/xm/effect/effect_portadown.go deleted file mode 100644 index b379bb4..0000000 --- a/format/xm/effect/effect_portadown.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// PortaDown defines a portamento down effect -type PortaDown channel.DataEffect // '2' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaDown) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaDown(channel.DataEffect(e)) - - if currentTick == 0 { - return nil - } - - return doPortaDown(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) -} - -func (e PortaDown) String() string { - return fmt.Sprintf("2%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_portatonote.go b/format/xm/effect/effect_portatonote.go deleted file mode 100644 index 143419e..0000000 --- a/format/xm/effect/effect_portatonote.go +++ /dev/null @@ -1,47 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/heucuva/comparison" -) - -// PortaToNote defines a portamento-to-note effect -type PortaToNote channel.DataEffect // '3' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) - cs.SetNotePlayTick(false, note.ActionContinue, 0) - } - return nil -} - -// Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - if currentTick == 0 { - return nil - } - - mem := cs.GetMemory() - xx := mem.PortaToNote(channel.DataEffect(e)) - - current := cs.GetPeriod() - target := cs.GetPortaTargetPeriod() - if period.ComparePeriods(current, target) == comparison.SpaceshipRightGreater { - return doPortaUpToNote(cs, float32(xx), 4, target, mem.Shared.LinearFreqSlides) // subtracts - } else { - return doPortaDownToNote(cs, float32(xx), 4, target, mem.Shared.LinearFreqSlides) // adds - } -} - -func (e PortaToNote) String() string { - return fmt.Sprintf("3%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_portaup.go b/format/xm/effect/effect_portaup.go deleted file mode 100644 index 3f65ea3..0000000 --- a/format/xm/effect/effect_portaup.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// PortaUp defines a portamento up effect -type PortaUp channel.DataEffect // '1' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaUp) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaUp(channel.DataEffect(e)) - - if currentTick == 0 { - return nil - } - - return doPortaUp(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) -} - -func (e PortaUp) String() string { - return fmt.Sprintf("1%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_portavolslide.go b/format/xm/effect/effect_portavolslide.go deleted file mode 100644 index 9e62e9c..0000000 --- a/format/xm/effect/effect_portavolslide.go +++ /dev/null @@ -1,24 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect -type PortaVolumeSlide struct { // '5' - playback.CombinedEffect[channel.Memory, channel.Data] -} - -// NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide(val channel.DataEffect) PortaVolumeSlide { - pvs := PortaVolumeSlide{} - pvs.Effects = append(pvs.Effects, VolumeSlide(val), PortaToNote(0x00)) - return pvs -} - -func (e PortaVolumeSlide) String() string { - return fmt.Sprintf("5%0.2x", channel.DataEffect(e.Effects[0].(VolumeSlide))) -} diff --git a/format/xm/effect/effect_retriggernote.go b/format/xm/effect/effect_retriggernote.go deleted file mode 100644 index 127d1f0..0000000 --- a/format/xm/effect/effect_retriggernote.go +++ /dev/null @@ -1,39 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// RetriggerNote defines a retriggering effect -type RetriggerNote channel.DataEffect // 'E9x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RetriggerNote) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e RetriggerNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - y := channel.DataEffect(e) & 0x0F - if y == 0 { - return nil - } - - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= y { - cs.SetPos(sampling.Pos{}) - cs.ResetRetriggerCount() - } - return nil -} - -func (e RetriggerNote) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_retrigvolslide.go b/format/xm/effect/effect_retrigvolslide.go deleted file mode 100644 index 04ef253..0000000 --- a/format/xm/effect/effect_retrigvolslide.go +++ /dev/null @@ -1,71 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide channel.DataEffect // 'R' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) >> 4 - y := channel.DataEffect(e) & 0x0F - if y == 0 { - return nil - } - - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= x { - cs.SetPos(sampling.Pos{}) - cs.ResetRetriggerCount() - switch x { - case 1: - return doVolSlide(cs, -1, 1) - case 2: - return doVolSlide(cs, -2, 1) - case 3: - return doVolSlide(cs, -4, 1) - case 4: - return doVolSlide(cs, -8, 1) - case 5: - return doVolSlide(cs, -6, 1) - case 6: - return doVolSlideTwoThirds(cs) - case 7: - return doVolSlide(cs, 0, float32(0.5)) - case 8: // ? - case 9: - return doVolSlide(cs, 1, 1) - case 10: - return doVolSlide(cs, 2, 1) - case 11: - return doVolSlide(cs, 4, 1) - case 12: - return doVolSlide(cs, 8, 1) - case 13: - return doVolSlide(cs, 16, 1) - case 14: - return doVolSlide(cs, 0, float32(1.5)) - case 15: - return doVolSlide(cs, 0, 2) - } - } - return nil -} - -func (e RetrigVolumeSlide) String() string { - return fmt.Sprintf("R%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_rowjump.go b/format/xm/effect/effect_rowjump.go deleted file mode 100644 index f9b37ee..0000000 --- a/format/xm/effect/effect_rowjump.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/index" -) - -// RowJump defines a row jump effect -type RowJump channel.DataEffect // 'D' - -// Start triggers on the first tick, but before the Tick() function is called -func (e RowJump) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e RowJump) Stop(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, lastTick int) error { - xy := channel.DataEffect(e) - x := xy >> 4 - y := xy & 0x0f - row := index.Row(x*10 + y) - if err := p.BreakOrder(); err != nil { - return err - } - return p.SetNextRow(row) -} - -func (e RowJump) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_sampleoffset.go b/format/xm/effect/effect_sampleoffset.go deleted file mode 100644 index b8b1256..0000000 --- a/format/xm/effect/effect_sampleoffset.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// SampleOffset defines a sample offset effect -type SampleOffset channel.DataEffect // '9' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() - xx := mem.SampleOffset(channel.DataEffect(e)) - cs.SetTargetPos(sampling.Pos{Pos: int(xx) * 0x100}) - return nil -} - -func (e SampleOffset) String() string { - return fmt.Sprintf("9%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setcoarsepanposition.go b/format/xm/effect/effect_setcoarsepanposition.go deleted file mode 100644 index 925a283..0000000 --- a/format/xm/effect/effect_setcoarsepanposition.go +++ /dev/null @@ -1,27 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - xmPanning "github.com/gotracker/playback/format/xm/panning" -) - -// SetCoarsePanPosition defines a set pan position effect -type SetCoarsePanPosition channel.DataEffect // 'E8x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xy := channel.DataEffect(e) - y := xy & 0x0F - - cs.SetPan(xmPanning.PanningFromXm(uint8(y) << 4)) - return nil -} - -func (e SetCoarsePanPosition) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setenvelopeposition.go b/format/xm/effect/effect_setenvelopeposition.go deleted file mode 100644 index 7bd2a17..0000000 --- a/format/xm/effect/effect_setenvelopeposition.go +++ /dev/null @@ -1,25 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// SetEnvelopePosition defines a set envelope position effect -type SetEnvelopePosition channel.DataEffect // 'Lxx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetEnvelopePosition) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xx := channel.DataEffect(e) - - cs.SetEnvelopePosition(int(xx)) - return nil -} - -func (e SetEnvelopePosition) String() string { - return fmt.Sprintf("L%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setfinetune.go b/format/xm/effect/effect_setfinetune.go deleted file mode 100644 index a0f1fbc..0000000 --- a/format/xm/effect/effect_setfinetune.go +++ /dev/null @@ -1,34 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/note" -) - -// SetFinetune defines a mod-style set finetune effect -type SetFinetune channel.DataEffect // 'E5x' - -// PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - x := channel.DataEffect(e) & 0xf - - inst := cs.GetTargetInst() - if inst != nil { - ft := (note.Finetune(x) - 8) * 4 - inst.SetFinetune(ft) - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetFinetune) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setglobalvolume.go b/format/xm/effect/effect_setglobalvolume.go deleted file mode 100644 index f024f3c..0000000 --- a/format/xm/effect/effect_setglobalvolume.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - xmVolume "github.com/gotracker/playback/format/xm/volume" -) - -// SetGlobalVolume defines a set global volume effect -type SetGlobalVolume channel.DataEffect // 'G' - -// PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - v := xmVolume.XmVolume(e) - p.SetGlobalVolume(v.Volume()) - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetGlobalVolume) String() string { - return fmt.Sprintf("G%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setpanposition.go b/format/xm/effect/effect_setpanposition.go deleted file mode 100644 index 8c36a20..0000000 --- a/format/xm/effect/effect_setpanposition.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - xmPanning "github.com/gotracker/playback/format/xm/panning" -) - -// SetPanPosition defines a set pan position effect -type SetPanPosition channel.DataEffect // '8xx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xx := uint8(e) - - cs.SetPan(xmPanning.PanningFromXm(xx)) - return nil -} - -func (e SetPanPosition) String() string { - return fmt.Sprintf("8%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setspeed.go b/format/xm/effect/effect_setspeed.go deleted file mode 100644 index 652b394..0000000 --- a/format/xm/effect/effect_setspeed.go +++ /dev/null @@ -1,33 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" -) - -// SetSpeed defines a set speed effect -type SetSpeed channel.DataEffect // 'F' - -// PreStart triggers when the effect enters onto the channel state -func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - if e != 0 { - m := p.(effectIntf.XM) - if err := m.SetTicks(int(e)); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetSpeed) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_settempo.go b/format/xm/effect/effect_settempo.go deleted file mode 100644 index d84c144..0000000 --- a/format/xm/effect/effect_settempo.go +++ /dev/null @@ -1,39 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" -) - -// SetTempo defines a set tempo effect -type SetTempo channel.DataEffect // 'F' - -// PreStart triggers when the effect enters onto the channel state -func (e SetTempo) PreStart(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - if e > 0x20 { - m := p.(effectIntf.XM) - if err := m.SetTempo(int(e)); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e SetTempo) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - m := p.(effectIntf.XM) - return m.SetTempo(int(e)) -} - -func (e SetTempo) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_settremolowaveform.go b/format/xm/effect/effect_settremolowaveform.go deleted file mode 100644 index fd29336..0000000 --- a/format/xm/effect/effect_settremolowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// SetTremoloWaveform defines a set tremolo waveform effect -type SetTremoloWaveform channel.DataEffect // 'E7x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - trem := mem.TremoloOscillator() - trem.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetTremoloWaveform) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setvibratowaveform.go b/format/xm/effect/effect_setvibratowaveform.go deleted file mode 100644 index 474085d..0000000 --- a/format/xm/effect/effect_setvibratowaveform.go +++ /dev/null @@ -1,29 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// SetVibratoWaveform defines a set vibrato waveform effect -type SetVibratoWaveform channel.DataEffect // 'E4x' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := channel.DataEffect(e) & 0xf - - mem := cs.GetMemory() - vib := mem.VibratoOscillator() - vib.SetWaveform(oscillator.WaveTableSelect(x)) - return nil -} - -func (e SetVibratoWaveform) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_setvolume.go b/format/xm/effect/effect_setvolume.go deleted file mode 100644 index d98f85f..0000000 --- a/format/xm/effect/effect_setvolume.go +++ /dev/null @@ -1,26 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - xmVolume "github.com/gotracker/playback/format/xm/volume" -) - -// SetVolume defines a volume slide effect -type SetVolume channel.DataEffect // 'C' - -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVolume) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xx := xmVolume.XmVolume(e) - - cs.SetActiveVolume(xx.Volume()) - return nil -} - -func (e SetVolume) String() string { - return fmt.Sprintf("C%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_tremolo.go b/format/xm/effect/effect_tremolo.go deleted file mode 100644 index 13bd2fa..0000000 --- a/format/xm/effect/effect_tremolo.go +++ /dev/null @@ -1,30 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// Tremolo defines a tremolo effect -type Tremolo channel.DataEffect // '7' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremolo) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Tremolo(channel.DataEffect(e)) - // NOTE: JBC - XM updates on tick 0, but MOD does not. - // Just have to eat this incompatibility, I guess... - return doTremolo(cs, currentTick, x, y, 4) -} - -func (e Tremolo) String() string { - return fmt.Sprintf("7%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_tremor.go b/format/xm/effect/effect_tremor.go deleted file mode 100644 index dd6ffc9..0000000 --- a/format/xm/effect/effect_tremor.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// Tremor defines a tremor effect -type Tremor channel.DataEffect // 'T' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremor) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - if currentTick != 0 { - mem := cs.GetMemory() - x, y := mem.Tremor(channel.DataEffect(e)) - return doTremor(cs, currentTick, int(x)+1, int(y)+1) - } - return nil -} - -func (e Tremor) String() string { - return fmt.Sprintf("T%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_vibrato.go b/format/xm/effect/effect_vibrato.go deleted file mode 100644 index 583caaf..0000000 --- a/format/xm/effect/effect_vibrato.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// Vibrato defines a vibrato effect -type Vibrato channel.DataEffect // '4' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e Vibrato) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) - // NOTE: JBC - XM updates on tick 0, but MOD does not. - // Just have to eat this incompatibility, I guess... - return doVibrato(cs, currentTick, x, y, 4) -} - -func (e Vibrato) String() string { - return fmt.Sprintf("4%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effect_vibratovolslide.go b/format/xm/effect/effect_vibratovolslide.go deleted file mode 100644 index caf9f06..0000000 --- a/format/xm/effect/effect_vibratovolslide.go +++ /dev/null @@ -1,24 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// VibratoVolumeSlide defines a combination vibrato and volume slide effect -type VibratoVolumeSlide struct { // '6' - playback.CombinedEffect[channel.Memory, channel.Data] -} - -// NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide(val channel.DataEffect) VibratoVolumeSlide { - vvs := VibratoVolumeSlide{} - vvs.Effects = append(vvs.Effects, VolumeSlide(val), Vibrato(0x00)) - return vvs -} - -func (e VibratoVolumeSlide) String() string { - return fmt.Sprintf("6%0.2x", channel.DataEffect(e.Effects[0].(VolumeSlide))) -} diff --git a/format/xm/effect/effect_volslide.go b/format/xm/effect/effect_volslide.go deleted file mode 100644 index 54cd068..0000000 --- a/format/xm/effect/effect_volslide.go +++ /dev/null @@ -1,40 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -// VolumeSlide defines a volume slide effect -type VolumeSlide channel.DataEffect // 'A' - -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e VolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.VolumeSlide(channel.DataEffect(e)) - - if currentTick == 0 { - return nil - } - - if x == 0 { - // vol slide down - return doVolSlide(cs, -float32(y), 1.0) - } else if y == 0 { - // vol slide up - return doVolSlide(cs, float32(y), 1.0) - } - return nil -} - -func (e VolumeSlide) String() string { - return fmt.Sprintf("A%0.2x", channel.DataEffect(e)) -} diff --git a/format/xm/effect/effectfactory.go b/format/xm/effect/effectfactory.go deleted file mode 100644 index 9fdddf8..0000000 --- a/format/xm/effect/effectfactory.go +++ /dev/null @@ -1,58 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" -) - -type EffectXM interface { - playback.Effect -} - -// VolEff is a combined effect that includes a volume effect and a standard effect -type VolEff struct { - playback.CombinedEffect[channel.Memory, channel.Data] - eff EffectXM -} - -func (e VolEff) String() string { - if e.eff == nil { - return "..." - } - return fmt.Sprint(e.eff) -} - -// Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data *channel.Data) EffectXM { - if data == nil { - return nil - } - - if !data.HasCommand() { - return nil - } - - eff := VolEff{} - if data.What.HasVolume() { - ve := volumeEffectFactory(mem, data.Volume) - if ve != nil { - eff.Effects = append(eff.Effects, ve) - } - } - - if e := standardEffectFactory(mem, data); e != nil { - eff.Effects = append(eff.Effects, e) - eff.eff = e - } - - switch len(eff.Effects) { - case 0: - return nil - case 1: - return eff.Effects[0] - default: - return &eff - } -} diff --git a/format/xm/effect/effectfactory_standard.go b/format/xm/effect/effectfactory_standard.go deleted file mode 100644 index a32b533..0000000 --- a/format/xm/effect/effectfactory_standard.go +++ /dev/null @@ -1,115 +0,0 @@ -package effect - -import ( - "github.com/gotracker/playback/format/xm/channel" -) - -func standardEffectFactory(mem *channel.Memory, cd *channel.Data) EffectXM { - if !cd.What.HasEffect() && !cd.What.HasEffectParameter() { - return nil - } - - switch cd.Effect { - case 0x00: // Arpeggio - return Arpeggio(cd.EffectParameter) - case 0x01: // Porta up - return PortaUp(cd.EffectParameter) - case 0x02: // Porta down - return PortaDown(cd.EffectParameter) - case 0x03: // Tone porta - return PortaToNote(cd.EffectParameter) - case 0x04: // Vibrato - return Vibrato(cd.EffectParameter) - case 0x05: // Tone porta + Volume slide - return NewPortaVolumeSlide(cd.EffectParameter) - case 0x06: // Vibrato + Volume slide - return NewVibratoVolumeSlide(cd.EffectParameter) - case 0x07: // Tremolo - return Tremolo(cd.EffectParameter) - case 0x08: // Set (fine) panning - return SetPanPosition(cd.EffectParameter) - case 0x09: // Sample offset - return SampleOffset(cd.EffectParameter) - case 0x0A: // Volume slide - return VolumeSlide(cd.EffectParameter) - case 0x0B: // Position jump - return OrderJump(cd.EffectParameter) - case 0x0C: // Set volume - return SetVolume(cd.EffectParameter) - case 0x0D: // Pattern break - return RowJump(cd.EffectParameter) - case 0x0E: // extra... - return specialEffectFactory(mem, cd.Effect, cd.EffectParameter) - case 0x0F: // Set tempo/BPM - if cd.EffectParameter < 0x20 { - return SetSpeed(cd.EffectParameter) - } - return SetTempo(cd.EffectParameter) - case 0x10: // Set global volume - return SetGlobalVolume(cd.EffectParameter) - case 0x11: // Global volume slide - return GlobalVolumeSlide(cd.EffectParameter) - - case 0x15: // Set envelope position - return SetEnvelopePosition(cd.EffectParameter) - - case 0x19: // Panning slide - return PanSlide(cd.EffectParameter) - - case 0x1B: // Multi retrig note - return RetrigVolumeSlide(cd.EffectParameter) - - case 0x1D: // Tremor - return Tremor(cd.EffectParameter) - - case 0x21: // Extra fine porta commands - return extraFinePortaEffectFactory(mem, cd.Effect, cd.EffectParameter) - } - return UnhandledCommand{Command: cd.Effect, Info: cd.EffectParameter} -} - -func extraFinePortaEffectFactory(mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { - switch cp >> 4 { - case 0x0: // none - return nil - case 0x1: // Extra fine porta up - return ExtraFinePortaUp(cp) - case 0x2: // Extra fine porta down - return ExtraFinePortaDown(cp) - } - return UnhandledCommand{Command: ce, Info: cp} -} - -func specialEffectFactory(mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { - switch cp >> 4 { - case 0x1: // Fine porta up - return FinePortaUp(cp) - case 0x2: // Fine porta down - return FinePortaDown(cp) - //case 0x3: // Set glissando control - - case 0x4: // Set vibrato control - return SetVibratoWaveform(cp) - case 0x5: // Set finetune - return SetFinetune(cp) - case 0x6: // Set loop begin/loop - return PatternLoop(cp) - case 0x7: // Set tremolo control - return SetTremoloWaveform(cp) - case 0x8: // Set coarse panning - return SetCoarsePanPosition(cp) - case 0x9: // Retrig note - return RetriggerNote(cp) - case 0xA: // Fine volume slide up - return FineVolumeSlideUp(cp) - case 0xB: // Fine volume slide down - return FineVolumeSlideDown(cp) - case 0xC: // Note cut - return NoteCut(cp) - case 0xD: // Note delay - return NoteDelay(cp) - case 0xE: // Pattern delay - return PatternDelay(cp) - } - return UnhandledCommand{Command: ce, Info: cp} -} diff --git a/format/xm/effect/intf/intf.go b/format/xm/effect/intf/intf.go deleted file mode 100644 index 7935606..0000000 --- a/format/xm/effect/intf/intf.go +++ /dev/null @@ -1,22 +0,0 @@ -package intf - -import ( - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/index" -) - -// XM is an interface to XM effect operations -type XM interface { - SetNextOrder(index.Order) error // Bxx - BreakOrder() error // Dxx - SetNextRow(index.Row) error // Dxx - SetNextRowWithBacktrack(index.Row, bool) error // E6x - GetCurrentRow() index.Row // E6x - SetPatternDelay(int) error // EEx - SetTicks(int) error // Fxx - SetTempo(int) error // Fxx - SetGlobalVolume(volume.Volume) // Gxx - GetGlobalVolume() volume.Volume // Hxx - SetEnvelopePosition(int) // Lxx - IgnoreUnknownEffect() bool // Unhandled -} diff --git a/format/xm/effect/unhandled.go b/format/xm/effect/unhandled.go deleted file mode 100644 index 14ca039..0000000 --- a/format/xm/effect/unhandled.go +++ /dev/null @@ -1,45 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" - xmVolume "github.com/gotracker/playback/format/xm/volume" -) - -// UnhandledCommand is an unhandled command -type UnhandledCommand struct { - Command channel.Command - Info channel.DataEffect -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory, channel.Data], m effectIntf.XM) error { - if !m.IgnoreUnknownEffect() { - panic("unhandled command") - } - return nil -} - -func (e UnhandledCommand) String() string { - return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) -} - -// UnhandledVolCommand is an unhandled volume command -type UnhandledVolCommand struct { - Vol xmVolume.VolEffect -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand) PreStart(cs playback.Channel[channel.Memory, channel.Data], m effectIntf.XM) error { - if !m.IgnoreUnknownEffect() { - panic("unhandled command") - } - return nil -} - -func (e UnhandledVolCommand) String() string { - return fmt.Sprintf("v%0.2x", e.Vol) -} diff --git a/format/xm/effect/util.go b/format/xm/effect/util.go deleted file mode 100644 index 98a7a64..0000000 --- a/format/xm/effect/util.go +++ /dev/null @@ -1,179 +0,0 @@ -package effect - -import ( - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" - xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" -) - -func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32, multiplier float32) error { - av := cs.GetActiveVolume() - v := xmVolume.ToVolumeXM(av) - vol := int16((float32(v) + delta) * multiplier) - if vol >= 0x40 { - vol = 0x40 - } - if vol < 0x00 { - vol = 0x00 - } - v = xmVolume.XmVolume(channel.DataEffect(vol)) - cs.SetActiveVolume(v.Volume()) - return nil -} - -func doGlobalVolSlide(m effectIntf.XM, delta float32, multiplier float32) error { - gv := m.GetGlobalVolume() - v := xmVolume.ToVolumeXM(gv) - vol := int16((float32(v) + delta) * multiplier) - if vol >= 0x40 { - vol = 0x40 - } - if vol < 0x00 { - vol = 0x00 - } - v = xmVolume.XmVolume(channel.DataEffect(vol)) - m.SetGlobalVolume(v.Volume()) - return nil -} - -func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory, channel.Data], delta int) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) - cs.SetPeriod(cur) - return nil -} - -func doPortaByDeltaLinear(cs playback.Channel[channel.Memory, channel.Data], delta int) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - finetune := period.PeriodDelta(delta) - cur = cur.AddDelta(finetune) - cs.SetPeriod(cur) - return nil -} - -func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { - delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, delta) - } - return doPortaByDeltaAmiga(cs, -delta) -} - -func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaUp(cs, amount, multiplier, linearFreqSlides); err != nil { - return err - } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { - cs.SetPeriod(target) - } - return nil -} - -func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { - delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, -delta) - } - return doPortaByDeltaAmiga(cs, delta) -} - -func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaDown(cs, amount, multiplier, linearFreqSlides); err != nil { - return err - } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { - cs.SetPeriod(target) - } - return nil -} - -func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { - mem := cs.GetMemory() - vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) - delta := period.PeriodDelta(vib) - cs.SetPeriodDelta(delta) - return nil -} - -func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int, onTicks int, offTicks int) error { - mem := cs.GetMemory() - tremor := mem.TremorMem() - if tremor.IsActive() { - if tremor.Advance() >= onTicks { - tremor.ToggleAndReset() - } - } else { - if tremor.Advance() >= offTicks { - tremor.ToggleAndReset() - } - } - cs.SetVolumeActive(tremor.IsActive()) - return nil -} - -func doArpeggio(cs playback.Channel[channel.Memory, channel.Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { - ns := cs.GetNoteSemitone() - var arpSemitoneTarget note.Semitone - switch currentTick % 3 { - case 0: - arpSemitoneTarget = ns - case 1: - arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneADelta) - case 2: - arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneBDelta) - } - cs.SetOverrideSemitone(arpSemitoneTarget) - cs.SetTargetPos(cs.GetPos()) - return nil -} - -var ( - volSlideTwoThirdsTable = [...]xmVolume.XmVolume{ - 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9, - 10, 10, 11, 11, 12, 13, 13, 14, 15, 15, 16, 16, 17, 18, 18, 19, - 20, 20, 21, 21, 22, 23, 23, 24, 25, 25, 26, 26, 27, 28, 28, 29, - 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, - } -) - -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) error { - vol := xmVolume.ToVolumeXM(cs.GetActiveVolume()) - if vol >= 64 { - vol = 63 - } - - v := volSlideTwoThirdsTable[vol] - if v >= 0x40 { - v = 0x40 - } - - cs.SetActiveVolume(v.Volume()) - return nil -} - -func doTremolo(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return doVolSlide(cs, delta, 1.0) -} - -func calculateWaveTable(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { - delta := o.GetWave(float32(depth) * multiplier) - o.Advance(int(speed)) - return delta -} diff --git a/format/xm/filter/factory.go b/format/xm/filter/factory.go new file mode 100644 index 0000000..5d3f852 --- /dev/null +++ b/format/xm/filter/factory.go @@ -0,0 +1,24 @@ +package filter + +import ( + "fmt" + + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" +) + +func Factory(name string, instrumentRate frequency.Frequency, params any) (filter.Filter, error) { + var f filter.Filter + switch name { + case "": + // nothing + + case "amigalpf": + f = filter.NewAmigaLPF(instrumentRate) + + default: + return nil, fmt.Errorf("unsupported filter name: %q", name) + } + + return f, nil +} diff --git a/format/xm/layout/channelsetting.go b/format/xm/layout/channelsetting.go index c0f8f96..c15f79a 100644 --- a/format/xm/layout/channelsetting.go +++ b/format/xm/layout/channelsetting.go @@ -1,16 +1,74 @@ package layout import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/format/xm/channel" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/vol0optimization" ) // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled bool + Muted bool OutputChannelNum int - InitialVolume volume.Volume - InitialPanning panning.Position + InitialVolume xmVolume.XmVolume + InitialPanning xmPanning.Panning Memory channel.Memory } + +var _ song.ChannelSettings = (*ChannelSetting)(nil) + +func (c ChannelSetting) IsEnabled() bool { + return c.Enabled +} + +func (c ChannelSetting) IsMuted() bool { + return c.Muted +} + +func (c ChannelSetting) GetOutputChannelNum() int { + return c.OutputChannelNum +} + +func (c ChannelSetting) GetInitialVolume() xmVolume.XmVolume { + return c.InitialVolume +} + +func (c ChannelSetting) GetMixingVolume() xmVolume.XmVolume { + return xmVolume.DefaultXmVolume +} + +func (c ChannelSetting) GetInitialPanning() xmPanning.Panning { + return c.InitialPanning +} + +func (c ChannelSetting) GetMemory() song.ChannelMemory { + return &c.Memory +} + +func (c ChannelSetting) IsPanEnabled() bool { + return true +} + +func (c ChannelSetting) GetDefaultFilterInfo() filter.Info { + return filter.Info{} +} + +func (c ChannelSetting) IsDefaultFilterEnabled() bool { + return false +} + +func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { + return vol0optimization.Vol0OptimizationSettings{ + Enabled: true, + MaxRowsAt0: 3, + } +} + +func (ChannelSetting) GetOPLChannel() index.OPLChannel { + return index.InvalidOPLChannel +} diff --git a/format/xm/layout/header.go b/format/xm/layout/header.go index 12fa550..cd646d7 100644 --- a/format/xm/layout/header.go +++ b/format/xm/layout/header.go @@ -1,12 +1,17 @@ package layout -import "github.com/gotracker/gomixing/volume" +import ( + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" +) // Header is a mildly-decoded XM header definition type Header struct { - Name string - InitialSpeed int - InitialTempo int - GlobalVolume volume.Volume - MixingVolume volume.Volume + Name string + InitialSpeed int + InitialTempo int + GlobalVolume xmVolume.XmVolume + MixingVolume xmVolume.XmVolume + LinearFreqSlides bool + InitialOrder index.Order } diff --git a/format/xm/layout/row.go b/format/xm/layout/row.go new file mode 100644 index 0000000..cf7931c --- /dev/null +++ b/format/xm/layout/row.go @@ -0,0 +1,28 @@ +package layout + +import ( + "github.com/gotracker/playback/format/xm/channel" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" +) + +type Row[TPeriod period.Period] []channel.Data[TPeriod] + +func (r Row[TPeriod]) Len() int { + return len(r) +} + +func (r Row[TPeriod]) ForEach(fn func(ch index.Channel, cd song.ChannelData[xmVolume.XmVolume]) (bool, error)) error { + for i, c := range r { + cont, err := fn(index.Channel(i), c) + if err != nil { + return err + } + if !cont { + break + } + } + return nil +} diff --git a/format/xm/layout/song.go b/format/xm/layout/song.go index 5b13e8e..da4fd9c 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -1,82 +1,86 @@ package layout import ( + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/channel" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" ) -// Song is the full definition of the song data of an Song file -type Song struct { - Head Header - Instruments map[uint8]*instrument.Instrument - InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument - Patterns []pattern.Pattern[channel.Data] +// Song is the full definition of the song data of an XM file +type Song[TPeriod period.Period] struct { + common.BaseSong[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + + InstrumentNoteMap map[uint8]SemitoneSamples ChannelSettings []ChannelSetting - OrderList []index.Pattern } -// GetOrderList returns the list of all pattern orders for the song -func (s Song) GetOrderList() []index.Pattern { - return s.OrderList -} +type SemitoneSamples [96]int // semitone -> instrument index -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { - if int(patNum) >= len(s.Patterns) { - return nil - } - return &s.Patterns[patNum] +// GetNumChannels returns the number of channels the song has +func (s Song[TPeriod]) GetNumChannels() int { + return len(s.ChannelSettings) } -// IsChannelEnabled returns true if the channel at index `channelNum` is enabled -func (s Song) IsChannelEnabled(channelNum int) bool { - return s.ChannelSettings[channelNum].Enabled +// GetChannelSettings returns the channel settings at index `channelNum` +func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + return s.ChannelSettings[channelNum] } -// GetRenderChannel returns the output channel for the channel at index `channelNum` -func (s Song) GetRenderChannel(channelNum int) int { - return s.ChannelSettings[channelNum].OutputChannelNum -} +// GetInstrument returns the instrument interface indexed by `instNum` (0-based) +func (s Song[TPeriod]) GetInstrument(instID int, st note.Semitone) (instrument.InstrumentIntf, note.Semitone) { + if instID == 0 { + return nil, note.UnchangedSemitone + } -// NumInstruments returns the number of instruments in the song -func (s Song) NumInstruments() int { - return len(s.Instruments) -} + idx := instID - 1 -// IsValidInstrumentID returns true if the instrument exists -func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { - if instNum.IsEmpty() { - return false + i := idx + if inm, ok := s.InstrumentNoteMap[uint8(idx)]; ok { + i = inm[st] } - switch id := instNum.(type) { - case channel.SampleID: - _, ok := s.Instruments[id.InstID] - return ok + + if i < 0 || i >= len(s.Instruments) { + return nil, st } - return false + + return s.Instruments[i], st } -// GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instNum instrument.ID) (*instrument.Instrument, note.Semitone) { - if instNum.IsEmpty() { - return nil, note.UnchangedSemitone - } - switch id := instNum.(type) { - case channel.SampleID: - if nm, ok1 := s.InstrumentNoteMap[id.InstID]; ok1 { - if sm, ok2 := nm[id.Semitone]; ok2 { - return sm, note.UnchangedSemitone - } +func (s Song[TPeriod]) GetRowRenderStringer(row song.Row, channels int, longFormat bool) render.RowStringer { + rt := render.NewRowText[channel.Data[TPeriod]](channels, longFormat) + rowData := make([]channel.Data[TPeriod], channels) + song.ForEachRowChannel(row, func(ch index.Channel, d song.ChannelData[xmVolume.XmVolume]) (bool, error) { + if int(ch) >= channels || !s.ChannelSettings[ch].Enabled || s.ChannelSettings[ch].Muted { + return true, nil } - } - return nil, note.UnchangedSemitone + rowData[ch] = d.(channel.Data[TPeriod]) + return true, nil + }) + rt.Channels = rowData + return rt } -// GetName returns the name of the song -func (s Song) GetName() string { - return s.Head.Name +func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { + for i, cs := range s.ChannelSettings { + if enabledOnly { + if !cs.Enabled || (cs.Muted && s.MS.Quirks.DoNotProcessEffectsOnMutedChannels) { + continue + } + } + cont, err := fn(index.Channel(i)) + if err != nil { + return err + } + if !cont { + break + } + } + return nil } diff --git a/format/xm/layout/stringrow.go b/format/xm/layout/stringrow.go new file mode 100644 index 0000000..9210947 --- /dev/null +++ b/format/xm/layout/stringrow.go @@ -0,0 +1,139 @@ +package layout + +import ( + "fmt" + "regexp" + "slices" + "strconv" + "strings" + + xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" + "github.com/gotracker/playback/format/xm/channel" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" +) + +type StringRow[TPeriod period.Period] string + +func (r StringRow[TPeriod]) Len() int { + return len(strings.SplitAfter(string(r), "|")) - 1 +} + +func (r StringRow[TPeriod]) ForEach(fn func(ch index.Channel, d song.ChannelData[xmVolume.XmVolume]) (bool, error)) error { + cstrPieces := strings.SplitAfter(string(r), "|") + cstrPieces = slices.DeleteFunc(cstrPieces, func(s string) bool { + return len(s) == 0 || s == "|" + }) + + row := make(Row[TPeriod], len(cstrPieces)) + for ch, cstr := range cstrPieces { + d, err := r.decodeChannel(strings.TrimSuffix(cstr, "|")) + if err != nil { + return err + } + row[ch] = d + } + + return row.ForEach(fn) +} + +var channelRegex = regexp.MustCompile(`^(...) +(..) +(..) +(...)$`) + +func (StringRow[TPeriod]) decodeChannel(cstr string) (channel.Data[TPeriod], error) { + var d channel.Data[TPeriod] + + pieces := channelRegex.FindStringSubmatch(cstr) + if len(pieces) != 5 { + return d, fmt.Errorf("could not parse channel: %q", cstr) + } + note, instrument, vol, cmd := pieces[1], pieces[2], pieces[3], pieces[4] + + d.Note = 0 + + // note + if note == "===" || note == "== " { + d.What |= xmfile.ChannelFlagHasNote + d.Note = 97 + } else if note != "..." { + key := note[0:2] + oct, err := strconv.Atoi(note[2:]) + if err != nil { + return d, err + } + + switch key { + case "C-": + d.Note = uint8(oct*12 + 1) + case "C#": + d.Note = uint8(oct*12 + 2) + case "D-": + d.Note = uint8(oct*12 + 3) + case "D#": + d.Note = uint8(oct*12 + 4) + case "E-": + d.Note = uint8(oct*12 + 5) + case "F-": + d.Note = uint8(oct*12 + 6) + case "F#": + d.Note = uint8(oct*12 + 7) + case "G-": + d.Note = uint8(oct*12 + 8) + case "G#": + d.Note = uint8(oct*12 + 9) + case "A-": + d.Note = uint8(oct*12 + 10) + case "A#": + d.Note = uint8(oct*12 + 11) + case "B-": + d.Note = uint8(oct*12 + 12) + default: + return d, fmt.Errorf("invalid key in note: %q", note) + } + d.What |= xmfile.ChannelFlagHasNote + } + + // instrument + if instrument != " " { + i, err := strconv.ParseUint(strings.TrimSpace(instrument), 16, 8) + if err != nil { + return d, err + } + + if i > 0 { + d.What |= xmfile.ChannelFlagHasInstrument + d.Instrument = uint8(i) + } + } + + // vol + if vol != ".." { + v, err := strconv.ParseUint(vol, 16, 8) + if err != nil { + return d, err + } + + d.What |= xmfile.ChannelFlagHasVolume + d.Volume = xmVolume.VolEffect(v) + } + + // cmd + if cmd != "..." { + c := cmd[0] + i, err := strconv.ParseUint(cmd[1:], 16, 8) + if err != nil { + return d, err + } + + d.What |= xmfile.ChannelFlagHasEffect | xmfile.ChannelFlagHasEffectParameter + if e := c - '0'; e <= 9 { + d.Effect = channel.Command(e) + } else if e := c - 'A' + 10; e < 36 { + d.Effect = channel.Command(e) + } + d.EffectParameter = channel.DataEffect(i) + } + + return d, nil +} diff --git a/format/xm/load/load.go b/format/xm/load/load.go index 5c2f475..64840df 100644 --- a/format/xm/load/load.go +++ b/format/xm/load/load.go @@ -3,13 +3,12 @@ package load import ( "io" - "github.com/gotracker/playback" "github.com/gotracker/playback/format/common" - xmPlayback "github.com/gotracker/playback/format/xm/playback" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" ) // XM loads an XM file and upgrades it into an XM file internally -func XM(r io.Reader, features []feature.Feature) (playback.Playback, error) { - return common.Load(r, readXM, xmPlayback.NewManager, features) +func XM(r io.Reader, features []feature.Feature) (song.Data, error) { + return common.Load(r, readXM, features) } diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index bfe318d..7cbb85f 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -6,38 +6,44 @@ import ( "math" xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" - "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/loop" - "github.com/gotracker/playback/voice/pcm" + "github.com/heucuva/optional" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/format/xm/layout" + "github.com/gotracker/playback/format/common" + xmChannel "github.com/gotracker/playback/format/xm/channel" + xmLayout "github.com/gotracker/playback/format/xm/layout" xmPanning "github.com/gotracker/playback/format/xm/panning" xmPeriod "github.com/gotracker/playback/format/xm/period" + xmSettings "github.com/gotracker/playback/format/xm/settings" + xmSystem "github.com/gotracker/playback/format/xm/system" xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/oscillator" - "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" ) -func moduleHeaderToHeader(fh *xmfile.ModuleHeader) (*layout.Header, error) { +func moduleHeaderToHeader(fh *xmfile.ModuleHeader) (*xmLayout.Header, error) { if fh == nil { return nil, errors.New("file header is nil") } - head := layout.Header{ - Name: fh.GetName(), - InitialSpeed: int(fh.DefaultSpeed), - InitialTempo: int(fh.DefaultTempo), - GlobalVolume: xmVolume.DefaultVolume, - MixingVolume: xmVolume.DefaultMixingVolume, + head := xmLayout.Header{ + Name: fh.GetName(), + InitialSpeed: int(fh.DefaultSpeed), + InitialTempo: int(fh.DefaultTempo), + GlobalVolume: xmVolume.DefaultXmVolume, + MixingVolume: xmVolume.DefaultXmMixingVolume, + LinearFreqSlides: fh.Flags.IsLinearSlides(), + InitialOrder: 0, } return &head, nil } @@ -59,32 +65,30 @@ func xmAutoVibratoWSToProtrackerWS(vibtype uint8) uint8 { } } -func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument, map[int][]note.Semitone, error) { +func xmInstrumentToInstrument[TPeriod period.Period](inst *xmfile.InstrumentHeader, pc period.PeriodConverter[TPeriod], linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], map[int][]note.Semitone, error) { noteMap := make(map[int][]note.Semitone) - var instruments []*instrument.Instrument + var instruments []*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] for _, si := range inst.Samples { - v := xmVolume.XmVolume(si.Volume) - if v >= 0x40 { - v = 0x40 - } - sample := instrument.Instrument{ - Static: instrument.StaticValues{ + v := min(xmVolume.XmVolume(si.Volume), 0x40) + sample := instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + Static: instrument.StaticValues[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + PC: pc, Filename: si.GetName(), Name: inst.GetName(), - Volume: v.Volume(), + Volume: v, RelativeNoteNumber: si.RelativeNoteNumber, - AutoVibrato: voice.AutoVibrato{ + AutoVibrato: autovibrato.AutoVibratoConfig[TPeriod]{ Enabled: (inst.VibratoDepth != 0 && inst.VibratoRate != 0), Sweep: int(inst.VibratoSweep), WaveformSelection: xmAutoVibratoWSToProtrackerWS(inst.VibratoType), Depth: float32(inst.VibratoDepth), Rate: int(inst.VibratoRate), - Factory: oscillator.NewProtrackerOscillator, + FactoryName: "vibrato", }, }, - C2Spd: period.Frequency(0), // uses si.Finetune, below + SampleRate: frequency.Frequency(0), // uses si.Finetune, below } if !linearFrequencySlides { @@ -123,18 +127,17 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid End: int(inst.PanSustainPoint), } - ii := instrument.PCM{ - Loop: &loop.Disabled{}, - MixingVolume: volume.Volume(1), + ii := instrument.PCM[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + Loop: &loop.Disabled{}, FadeOut: fadeout.Settings{ Mode: fadeout.ModeOnlyIfVolEnvActive, Amount: volume.Volume(inst.VolumeFadeout) / 65536, }, - Panning: xmPanning.PanningFromXm(si.Panning), - VolEnv: envelope.Envelope[volume.Volume]{ + Panning: optional.NewValue[xmPanning.Panning](xmPanning.Panning(si.Panning)), + VolEnv: envelope.Envelope[xmVolume.XmVolume]{ Enabled: (inst.VolFlags & xmfile.EnvelopeFlagEnabled) != 0, }, - PanEnv: envelope.Envelope[panning.Position]{ + PanEnv: envelope.Envelope[xmPanning.Panning]{ Enabled: (inst.PanFlags & xmfile.EnvelopeFlagEnabled) != 0, }, } @@ -147,7 +150,7 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid volEnvSustainMode = loop.ModeNormal } - ii.VolEnv.Values = make([]envelope.EnvPoint[volume.Volume], int(inst.VolPoints)) + ii.VolEnv.Values = make([]envelope.Point[xmVolume.XmVolume], int(inst.VolPoints)) for i := range ii.VolEnv.Values { x1 := int(inst.VolEnv[i].X) y1 := uint8(inst.VolEnv[i].Y) @@ -155,9 +158,13 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid if i+1 < len(ii.VolEnv.Values) { x2 = int(inst.VolEnv[i+1].X) } else { + ii.VolEnv.Length = x1 x2 = math.MaxInt64 } - ii.VolEnv.Values[i].Init(x2-x1, xmVolume.XmVolume(y1).Volume()) + v := &ii.VolEnv.Values[i] + v.Length = x2 - x1 + v.Pos = x1 + v.Y = xmVolume.XmVolume(y1) } } @@ -169,7 +176,7 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid panEnvSustainMode = loop.ModeNormal } - ii.PanEnv.Values = make([]envelope.EnvPoint[panning.Position], int(inst.VolPoints)) + ii.PanEnv.Values = make([]envelope.Point[xmPanning.Panning], int(inst.VolPoints)) for i := range ii.PanEnv.Values { x1 := int(inst.PanEnv[i].X) // XM stores pan envelope values in 0..64 @@ -181,17 +188,17 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid x2 = int(inst.PanEnv[i+1].X) } else { x2 = math.MaxInt64 + ii.PanEnv.Length = x1 } - ii.PanEnv.Values[i].Init(x2-x1, xmPanning.PanningFromXm(y1)) + v := &ii.PanEnv.Values[i] + v.Length = x2 - x1 + v.Pos = x1 + v.Y = xmPanning.Panning(y1) } } - if si.Finetune != 0 { - sample.C2Spd = xmPeriod.CalcFinetuneC2Spd(xmPeriod.DefaultC2Spd, note.Finetune(si.Finetune), linearFrequencySlides) - } - if sample.C2Spd == 0 { - sample.C2Spd = period.Frequency(xmPeriod.DefaultC2Spd) - } + n := note.Semitone(xmSystem.C4Note + si.RelativeNoteNumber) + sample.SampleRate = xmPeriod.CalcFinetuneC4SampleRate(xmSystem.DefaultC4SampleRate, n, note.Finetune(si.Finetune)) if si.Flags.IsStereo() { numChannels = 2 } @@ -243,34 +250,32 @@ func xmLoopModeToLoopMode(mode xmfile.SampleLoopMode) loop.Mode { } } -func convertXMInstrumentToInstrument(ih *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument, map[int][]note.Semitone, error) { +func convertXMInstrumentToInstrument[TPeriod period.Period](ih *xmfile.InstrumentHeader, pc period.PeriodConverter[TPeriod], linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], map[int][]note.Semitone, error) { if ih == nil { return nil, nil, errors.New("instrument is nil") } - return xmInstrumentToInstrument(ih, linearFrequencySlides, features) + return xmInstrumentToInstrument(ih, pc, linearFrequencySlides, features) } -func convertXmPattern(pkt xmfile.Pattern) (*pattern.Pattern[channel.Data], int) { - pat := &pattern.Pattern[channel.Data]{ - Orig: pkt, - } +func convertXmPattern[TPeriod period.Period](pkt xmfile.Pattern) (song.Pattern, int) { + pat := make(song.Pattern, len(pkt.Data)) maxCh := uint8(0) for rowNum, drow := range pkt.Data { - pat.Rows = append(pat.Rows, pattern.RowData[channel.Data]{}) - row := &pat.Rows[rowNum] - row.Channels = make([]channel.Data, len(drow)) + row := make(xmLayout.Row[TPeriod], len(drow)) + pat[rowNum] = row + for channelNum, chn := range drow { - cd := channel.Data{ + cd := xmChannel.Data[TPeriod]{ What: chn.Flags, Note: chn.Note, Instrument: chn.Instrument, Volume: xmVolume.VolEffect(chn.Volume), - Effect: channel.Command(chn.Effect), - EffectParameter: channel.DataEffect(chn.EffectParameter), + Effect: xmChannel.Command(chn.Effect), + EffectParameter: xmChannel.DataEffect(chn.EffectParameter), } - row.Channels[channelNum] = cd + row[channelNum] = cd if maxCh < uint8(channelNum) { maxCh = uint8(channelNum) } @@ -280,7 +285,15 @@ func convertXmPattern(pkt xmfile.Pattern) (*pattern.Pattern[channel.Data], int) return pat, int(maxCh) } -func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.Song, error) { +func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (song.Data, error) { + if f.Head.Flags.IsLinearSlides() { + return convertXmFileToTypedSong[period.Linear](f, features) + } else { + return convertXmFileToTypedSong[period.Amiga](f, features) + } +} + +func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features []feature.Feature) (*xmLayout.Song[TPeriod], error) { h, err := moduleHeaderToHeader(&f.Head) if err != nil { return nil, err @@ -288,20 +301,31 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So linearFrequencySlides := f.Head.Flags.IsLinearSlides() - song := layout.Song{ - Head: *h, - Instruments: make(map[uint8]*instrument.Instrument), - InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument), - Patterns: make([]pattern.Pattern[channel.Data], len(f.Patterns)), - OrderList: make([]index.Pattern, int(f.Head.SongLength)), + ms := xmSettings.GetMachineSettings[TPeriod]() + + s := xmLayout.Song[TPeriod]{ + BaseSong: common.BaseSong[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + System: xmSystem.XMSystem, + MS: ms, + Name: h.Name, + InitialBPM: h.InitialTempo, + InitialTempo: h.InitialSpeed, + GlobalVolume: h.GlobalVolume, + MixingVolume: h.MixingVolume, + InitialOrder: h.InitialOrder, + Instruments: make([]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], len(f.Instruments)), + Patterns: make([]song.Pattern, len(f.Patterns)), + OrderList: make([]index.Pattern, f.Head.SongLength), + }, + InstrumentNoteMap: make(map[uint8]xmLayout.SemitoneSamples), } for i := 0; i < int(f.Head.SongLength); i++ { - song.OrderList[i] = index.Pattern(f.Head.OrderTable[i]) + s.OrderList[i] = index.Pattern(f.Head.OrderTable[i]) } for instNum, ih := range f.Instruments { - samples, noteMap, err := convertXMInstrumentToInstrument(&ih, linearFrequencySlides, features) + samples, noteMap, err := convertXMInstrumentToInstrument(&ih, ms.PeriodConverter, linearFrequencySlides, features) if err != nil { return nil, err } @@ -309,69 +333,69 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So if sample == nil { continue } - id := channel.SampleID{ + id := xmChannel.SampleID{ InstID: uint8(instNum + 1), } sample.Static.ID = id - song.Instruments[id.InstID] = sample + s.Instruments[instNum] = sample } for i, sts := range noteMap { sample := samples[i] - id, ok := sample.Static.ID.(channel.SampleID) - if !ok { - continue - } - inm, ok := song.InstrumentNoteMap[id.InstID] - if !ok { - inm = make(map[note.Semitone]*instrument.Instrument) - song.InstrumentNoteMap[id.InstID] = inm - } + inm, _ := s.InstrumentNoteMap[uint8(instNum)] + idx, _ := sample.Static.ID.GetIndexAndSample() + + var remapped bool for _, st := range sts { - inm[st] = samples[i] + inm[st] = idx + if idx != instNum { + remapped = true + } + } + // only add it back to the map if there's a remapping + if remapped { + s.InstrumentNoteMap[uint8(instNum)] = inm } } } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern[channel.Data], len(f.Patterns)) for patNum, pkt := range f.Patterns { - pattern, maxCh := convertXmPattern(pkt) - if pattern == nil { + pat, maxCh := convertXmPattern[TPeriod](pkt) + if pat == nil { continue } if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - song.Patterns[patNum] = *pattern + s.Patterns[patNum] = pat } - sharedMem := channel.SharedMemory{ + sharedMem := xmChannel.SharedMemory{ LinearFreqSlides: linearFrequencySlides, ResetMemoryAtStartOfOrder0: true, } - channels := make([]layout.ChannelSetting, lastEnabledChannel+1) + channels := make([]xmLayout.ChannelSetting, lastEnabledChannel+1) for chNum := range channels { - cs := layout.ChannelSetting{ + cs := xmLayout.ChannelSetting{ Enabled: true, - InitialVolume: xmVolume.DefaultVolume, + Muted: false, + InitialVolume: xmVolume.DefaultXmVolume, InitialPanning: xmPanning.DefaultPanning, - Memory: channel.Memory{ + Memory: xmChannel.Memory{ Shared: &sharedMem, }, } - cs.Memory.ResetOscillators() - channels[chNum] = cs } - song.ChannelSettings = channels + s.ChannelSettings = channels - return &song, nil + return &s, nil } -func readXM(r io.Reader, features []feature.Feature) (*layout.Song, error) { +func readXM(r io.Reader, features []feature.Feature) (song.Data, error) { f, err := xmfile.Read(r) if err != nil { return nil, err diff --git a/format/xm/oscillator/factory.go b/format/xm/oscillator/factory.go new file mode 100644 index 0000000..c7f0557 --- /dev/null +++ b/format/xm/oscillator/factory.go @@ -0,0 +1,35 @@ +package oscillator + +import ( + "fmt" + + oscillatorImpl "github.com/gotracker/playback/oscillator" + "github.com/gotracker/playback/voice/oscillator" +) + +func VibratoFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil +} + +func TremoloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil +} + +func PanbrelloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil +} + +func Factory(name string) (oscillator.Oscillator, error) { + switch name { + case "": + return nil, nil + case "vibrato": + return VibratoFactory() + case "tremolo": + return TremoloFactory() + case "panbrello": + return PanbrelloFactory() + default: + return nil, fmt.Errorf("unsupported oscillator: %q", name) + } +} diff --git a/format/xm/panning/panning.go b/format/xm/panning/panning.go index ee562c1..4481f08 100644 --- a/format/xm/panning/panning.go +++ b/format/xm/panning/panning.go @@ -1,21 +1,63 @@ package panning import ( + "math" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/playback/voice/types" +) + +const ( + DefaultPanningLeft = Panning(0x30) + DefaultPanning = Panning(0x80) + DefaultPanningRight = Panning(0xC0) + + MaxPanning = Panning(0xFF) +) + +var ( + // DefaultPanningLeftPosition is the default panning value for left channels + DefaultPanningLeftPosition = PanningFromXm(DefaultPanningLeft) + // DefaultPanningPosition is the default panning value for unconfigured channels + DefaultPanningPosition = PanningFromXm(DefaultPanning) + // DefaultPanningRightPosition is the default panning value for right channels + DefaultPanningRightPosition = PanningFromXm(DefaultPanningRight) ) +type Panning uint8 + var ( - // DefaultPanningLeft is the default panning value for left channels - DefaultPanningLeft = PanningFromXm(0x30) - // DefaultPanning is the default panning value for unconfigured channels - DefaultPanning = PanningFromXm(0x80) - // DefaultPanningRight is the default panning value for right channels - DefaultPanningRight = PanningFromXm(0xC0) + _ types.PanningInformationer[Panning] = Panning(0) + _ types.PanningDeltaer[Panning] = Panning(0) ) +func (p Panning) IsInvalid() bool { + return false +} + +func (p Panning) ToPosition() panning.Position { + return panning.MakeStereoPosition(float32(p), 0, 0xFF) +} + +func (Panning) GetDefault() Panning { + return DefaultPanning +} + +func (Panning) GetMax() Panning { + return MaxPanning +} + +func (p Panning) FMA(multiplier, add float32) Panning { + return Panning(min(max(math.FMA(float64(p), float64(multiplier), float64(add)), 0), 0xFF)) +} + +func (p Panning) AddDelta(d types.PanDelta) Panning { + return Panning(min(max(int16(p)+int16(d), 0), int16(MaxPanning))) +} + // PanningFromXm returns a radian panning position from an xm panning value -func PanningFromXm(pos uint8) panning.Position { - return panning.MakeStereoPosition(float32(pos), 0, 0xFF) +func PanningFromXm(pos Panning) panning.Position { + return pos.ToPosition() } // PanningToXm returns the xm panning value for a radian panning position diff --git a/format/xm/pattern/pattern.go b/format/xm/pattern/pattern.go deleted file mode 100644 index 5c19be6..0000000 --- a/format/xm/pattern/pattern.go +++ /dev/null @@ -1,339 +0,0 @@ -package pattern - -import ( - "errors" - - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" - formatutil "github.com/gotracker/playback/util" - "github.com/heucuva/optional" -) - -// State is the current pattern state -type State struct { - currentOrder index.Order - currentRow index.Row - ticks int - tempo int - patternDelay optional.Value[int] - finePatternDelay int - resetPatternLoops bool - - SongLoop feature.SongLoop - PlayUntilOrderAndRow feature.PlayUntilOrderAndRow - loopDetect formatutil.LoopDetect // when SongLoopEnabled is false, this is used to detect song loops - loopCount int - - Patterns []pattern.Pattern[channel.Data] - Orders []index.Pattern -} - -// GetTempo returns the tempo of the current state -func (state *State) GetTempo() int { - return state.tempo -} - -// GetSpeed returns the row speed of the current state -func (state *State) GetSpeed() int { - return state.ticks -} - -// GetTicksThisRow returns the number of ticks in the current row -func (state *State) GetTicksThisRow() int { - rowLoops := 1 - if patternDelay, ok := state.patternDelay.Get(); ok { - rowLoops = patternDelay - } - extraTicks := state.finePatternDelay - - ticksThisRow := state.ticks*rowLoops + extraTicks - return ticksThisRow -} - -// GetPatNum returns the current pattern number -func (state *State) GetPatNum() index.Pattern { - if int(state.currentOrder) >= len(state.Orders) { - return index.InvalidPattern - } - return state.Orders[state.currentOrder] -} - -// GetNumRows returns the number of rows in the current pattern -func (state *State) GetNumRows() (int, error) { - rows, err := state.GetRows() - if err != nil { - return 0, err - } - if rows != nil { - return rows.NumRows(), nil - } - return 0, nil -} - -// WantsStop returns true when the current pattern wants to end the song -func (state *State) WantsStop() bool { - return state.GetPatNum() == index.InvalidPattern -} - -// setCurrentOrder sets the current order index -func (state *State) setCurrentOrder(order index.Order) { - state.currentOrder = order -} - -func (state *State) advanceOrder() { - state.setCurrentOrder(state.currentOrder + 1) -} - -// GetCurrentOrder returns the current order -func (state *State) GetCurrentOrder() index.Order { - return state.currentOrder -} - -// GetNumOrders returns the number of orders in the song -func (state *State) GetNumOrders() int { - return len(state.Orders) -} - -// GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { - ordLen := len(state.Orders) - - if ordLen == 0 { - // nothing to play, don't even try - return 0, song.ErrStopSong - } - - for loopCount := 0; loopCount < ordLen; loopCount++ { - ordIdx := int(state.GetCurrentOrder()) - if ordIdx >= ordLen { - if !(state.SongLoop.Count < 0 || state.loopCount < state.SongLoop.Count) { - return 0, song.ErrStopSong - } - state.setCurrentOrder(0) - continue - } - - patIdx := state.Orders[ordIdx] - if patIdx == index.NextPattern { - if err := state.nextOrder(true); err != nil { - return 0, err - } - continue - } - - if patIdx == index.InvalidPattern { - if err := state.nextOrder(true); err != nil { - return 0, err - } - continue // this is supposed to be a song break - } - - return patIdx, nil - } - return 0, errors.New("infinite loop detected in order list") -} - -// GetCurrentRow returns the current row -func (state *State) GetCurrentRow() index.Row { - return state.currentRow -} - -// setCurrentRow sets the current row -func (state *State) setCurrentRow(row index.Row) error { - state.currentRow = row - rows, err := state.GetNumRows() - if err != nil { - return err - } - if int(state.GetCurrentRow()) >= rows { - if err := state.nextOrder(true); err != nil { - return err - } - } - return nil -} - -// Observe will attempt to detect a song loop -func (state *State) Observe() error { - if state.SongLoop.Count >= 0 { - if state.loopDetect.Observe(state.currentOrder, state.currentRow) { - if state.SongLoop.Count == 0 || (state.SongLoop.Count > 0 && state.loopCount >= state.SongLoop.Count) { - return song.ErrStopSong - } - state.loopCount += 1 - state.loopDetect.Reset() - } - } - if state.currentOrder == index.Order(state.PlayUntilOrderAndRow.Order) && state.currentRow == index.Row(state.PlayUntilOrderAndRow.Row) { - if state.SongLoop.Count >= 0 && state.loopCount >= state.SongLoop.Count { - return song.ErrStopSong - } - } - return nil -} - -// nextOrder travels to the next pattern in the order list -func (state *State) nextOrder(resetRow ...bool) error { - state.advanceOrder() - state.patternDelay.Reset() - state.finePatternDelay = 0 - // called only to clean up order position info - if _, err := state.GetCurrentPatternIdx(); err != nil { - return err - } - if len(resetRow) > 0 && resetRow[0] { - state.currentRow = 0 - } - return nil -} - -// Reset resets a pattern state back to zeroes -func (state *State) Reset() { - *state = State{ - SongLoop: feature.SongLoop{ - Count: 0, - }, - PlayUntilOrderAndRow: feature.PlayUntilOrderAndRow{ - Order: -1, - Row: -1, - }, - } -} - -// nextRow travels to the next row in the pattern -// or the next order in the order list if the last row has been exhausted -func (state *State) nextRow() error { - state.patternDelay.Reset() - state.finePatternDelay = 0 - - var patNum = state.GetPatNum() - if patNum == index.InvalidPattern { - return nil - } - - if patNum == index.NextPattern { - if err := state.nextOrder(true); err != nil { - return err - } - return nil - } - - rows, err := state.GetNumRows() - if err != nil { - return err - } - if state.currentRow.Increment(rows) { - if err := state.nextOrder(true); err != nil { - return err - } - } - return nil -} - -// GetRows returns all the rows in the pattern -func (state *State) GetRows() (song.Rows[channel.Data], error) { -nextRow: - for loops := 0; loops < len(state.Patterns); loops++ { - var patNum = state.GetPatNum() - switch patNum { - case index.InvalidPattern: - return nil, nil - case index.NextPattern: - if err := state.nextRow(); err != nil { - return nil, err - } - continue nextRow - default: - if int(patNum) >= len(state.Patterns) { - return nil, nil - } - pattern := state.Patterns[patNum] - return pattern.GetRows(), nil - } - } - return nil, nil -} - -// NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State) NeedResetPatternLoops() bool { - rpl := state.resetPatternLoops - state.resetPatternLoops = false - return rpl -} - -// commitTransaction will update the order and row indexes at once, idempotently, from a row update transaction. -func (state *State) commitTransaction(txn *pattern.RowUpdateTransaction) error { - tempo, tempoSet := txn.Tempo.Get() - tempoDelta, tempoDeltaSet := txn.TempoDelta.Get() - if tempoSet || tempoDeltaSet { - newTempo := state.tempo - if tempoSet { - newTempo = tempo - } - if tempoDeltaSet { - newTempo += tempoDelta - } - state.tempo = newTempo - } - - if ticks, ok := txn.Ticks.Get(); ok { - state.ticks = ticks - } - - if finePatternDelay, ok := txn.FinePatternDelay.Get(); ok { - state.finePatternDelay = finePatternDelay - } - - if !state.patternDelay.IsSet() { - if patternDelay, ok := txn.GetPatternDelay(); ok { - state.patternDelay.Set(patternDelay) - } - } - - if txn.BreakOrder { - if err := state.nextOrder(true); err != nil { - return err - } - } - - orderIdx, orderIdxSet := txn.GetOrderIdx() - rowIdx, rowIdxSet := txn.GetRowIdx() - - if orderIdxSet || rowIdxSet { - if orderIdxSet { - state.setCurrentOrder(orderIdx) - if !rowIdxSet { - if err := state.setCurrentRow(0); err != nil { - return err - } - } - } - if rowIdxSet { - if !orderIdxSet && !txn.RowIdxAllowBacktrack && state.currentRow > rowIdx { - if err := state.nextOrder(); err != nil { - return err - } - } - if err := state.setCurrentRow(rowIdx); err != nil { - return err - } - } - } else if txn.AdvanceRow { - if err := state.nextRow(); err != nil { - return err - } - } - return nil -} - -// StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn -} diff --git a/format/xm/period/amiga.go b/format/xm/period/amiga.go deleted file mode 100644 index a2a004c..0000000 --- a/format/xm/period/amiga.go +++ /dev/null @@ -1,84 +0,0 @@ -package period - -import ( - "fmt" - "math" - - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - - "github.com/gotracker/playback/period" -) - -// Amiga defines a sampler period that follows the Amiga-style approach of note -// definition. Useful in calculating resampling. -type Amiga period.AmigaPeriod - -// AddInteger truncates the current period to an integer and adds the delta integer in -// then returns the resulting period -func (p Amiga) AddInteger(delta int) Amiga { - p = Amiga(int(p) + delta) - return p -} - -// Add adds the current period to a delta value then returns the resulting period -func (p Amiga) AddDelta(delta period.Delta) period.Period { - d := period.ToPeriodDelta(delta) - p += Amiga(d) - return p -} - -// Compare returns: -// -1 if the current period is higher frequency than the `rhs` period -// 0 if the current period is equal in frequency to the `rhs` period -// 1 if the current period is lower frequency than the `rhs` period -func (p Amiga) Compare(rhs period.Period) comparison.Spaceship { - lf := p.GetFrequency() - rf := rhs.GetFrequency() - - switch { - case lf < rf: - return comparison.SpaceshipRightGreater - case lf > rf: - return comparison.SpaceshipLeftGreater - default: - return comparison.SpaceshipEqual - } -} - -// Lerp linear-interpolates the current period with the `rhs` period -func (p Amiga) Lerp(t float64, rhs period.Period) period.Period { - right := Amiga(0) - if r, ok := rhs.(Amiga); ok { - right = r - } - - p = Amiga(period.AmigaPeriod(p).Lerp(t, period.AmigaPeriod(right))) - return p -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { - return float64(period.AmigaPeriod(p).GetFrequency(period.Frequency(samplerSpeed))) -} - -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - return period.AmigaPeriod(p).GetFrequency(XMBaseClock) -} - -func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%f }", float32(p)) -} - -// ToAmigaPeriod calculates an amiga period for a linear finetune period -func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { - if finetunes < 0 { - finetunes = 0 - } - pow := math.Pow(2, float64(finetunes)/semitonesPerOctave) - linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - - period := Amiga(float64(semitonePeriodTable[0]) / linFreq) - return period -} diff --git a/format/xm/period/amigaconverter.go b/format/xm/period/amigaconverter.go new file mode 100644 index 0000000..abae9a5 --- /dev/null +++ b/format/xm/period/amigaconverter.go @@ -0,0 +1,12 @@ +package period + +import ( + "github.com/gotracker/playback/format/xm/system" + "github.com/gotracker/playback/period" +) + +var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + System: system.XMSystem, + MinPeriod: 1, + MaxPeriod: 31999, +} diff --git a/format/xm/period/linear.go b/format/xm/period/linear.go deleted file mode 100644 index a578ed5..0000000 --- a/format/xm/period/linear.go +++ /dev/null @@ -1,97 +0,0 @@ -package period - -import ( - "fmt" - "math" - - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - - "github.com/gotracker/playback/period" -) - -// Linear is a linear period, based on semitone and finetune values -type Linear struct { - Finetune note.Finetune - C2Spd period.Frequency -} - -// Add adds the current period to a delta value then returns the resulting period -func (p Linear) AddDelta(delta period.Delta) period.Period { - // 0 means "not playing", so keep it that way - if p.Finetune > 0 { - d := period.ToPeriodDelta(delta) - p.Finetune += note.Finetune(d) - if p.Finetune < 1 { - p.Finetune = 1 - } - } - return p -} - -// Compare returns: -// -1 if the current period is higher frequency than the `rhs` period -// 0 if the current period is equal in frequency to the `rhs` period -// 1 if the current period is lower frequency than the `rhs` period -func (p Linear) Compare(rhs period.Period) comparison.Spaceship { - lf := p.GetFrequency() - rf := rhs.GetFrequency() - - switch { - case lf < rf: - return comparison.SpaceshipRightGreater - case lf > rf: - return comparison.SpaceshipLeftGreater - default: - return comparison.SpaceshipEqual - } -} - -// Lerp linear-interpolates the current period with the `rhs` period -func (p Linear) Lerp(t float64, rhs period.Period) period.Period { - right := ToLinearPeriod(rhs) - - lnft := float64(p.Finetune) - rnft := float64(right.Finetune) - - delta := period.PeriodDelta(t * (rnft - lnft)) - return p.AddDelta(delta) -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Linear) GetSamplerAdd(samplerSpeed float64) float64 { - period := float64(ToAmigaPeriod(p.Finetune, DefaultC2Spd)) - if period == 0 { - return 0 - } - return samplerSpeed / period -} - -// GetFrequency returns the frequency defined by the period -func (p Linear) GetFrequency() period.Frequency { - am := ToAmigaPeriod(p.Finetune, DefaultC2Spd) - return am.GetFrequency() -} - -func (p Linear) String() string { - return fmt.Sprintf("Linear{ Finetune:%v C2Spd:%v }", p.Finetune, p.C2Spd) -} - -// ToLinearPeriod returns the linear frequency period for a given period -func ToLinearPeriod(p period.Period) Linear { - switch pp := p.(type) { - case Linear: - return pp - case Amiga: - linFreq := float64(semitonePeriodTable[0]) / float64(pp) - - fts := note.Finetune(semitonesPerOctave * math.Log2(linFreq)) - - lp := Linear{ - Finetune: fts, - C2Spd: DefaultC2Spd, - } - return lp - } - return Linear{} -} diff --git a/format/xm/period/linearconverter.go b/format/xm/period/linearconverter.go new file mode 100644 index 0000000..90623f1 --- /dev/null +++ b/format/xm/period/linearconverter.go @@ -0,0 +1,10 @@ +package period + +import ( + "github.com/gotracker/playback/format/xm/system" + "github.com/gotracker/playback/period" +) + +var LinearConverter period.PeriodConverter[period.Linear] = period.LinearConverter{ + System: system.XMSystem, +} diff --git a/format/xm/period/util.go b/format/xm/period/util.go index db0455f..d17bbb6 100644 --- a/format/xm/period/util.go +++ b/format/xm/period/util.go @@ -1,73 +1,24 @@ package period import ( - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" -) - -const ( - // DefaultC2Spd is the default C2SPD for XM samples - DefaultC2Spd = 8363 - c2Period = 1712 - - floatDefaultC2Spd = float32(DefaultC2Spd) - - // XMBaseClock is the base clock speed of xm files - XMBaseClock period.Frequency = DefaultC2Spd * c2Period + "math" - notesPerOctave = 12 - semitonesPerNote = 64 - semitonesPerOctave = notesPerOctave * semitonesPerNote + xmSystem "github.com/gotracker/playback/format/xm/system" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/note" ) -var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} - -// CalcSemitonePeriod calculates the semitone period for it notes -func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequency, linearFreqSlides bool) period.Period { - if semi == note.UnchangedSemitone { - panic("how?") - } - if linearFreqSlides { - nft := int(semi)*64 + int(ft) - return Linear{ - Finetune: note.Finetune(nft), - C2Spd: c2spd, - } +// CalcFinetuneC4SampleRate calculates a new c4 sample rate after a finetune adjustment +func CalcFinetuneC4SampleRate(c4SampleRate frequency.Frequency, st note.Semitone, finetune note.Finetune) frequency.Frequency { + if finetune == 0 && st == xmSystem.C4Note { + return c4SampleRate } - key := int(semi.Key()) - octave := uint32(semi.Octave()) - - if key >= len(semitonePeriodTable) { - return nil - } + per := max(float64(st)*xmSystem.SlideFinesPerNote+float64(finetune)/2, 0) + exp := per / xmSystem.SlideFinesPerOctave + pow := math.Pow(2.0, exp-xmSystem.C4Octave) - if c2spd == 0 { - c2spd = period.Frequency(DefaultC2Spd) - } - - if ft != 0 { - c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) - } - - period := (Amiga(floatDefaultC2Spd*semitonePeriodTable[key]) / Amiga(uint32(c2spd)<= m.GetNumChannels() { - continue - } - - cdata := &channels[channelNum] - - cs := &m.channels[channelNum] - if err := cs.SetData(cdata); err != nil { - return err - } - } - - for ch := range m.channels { - cs := &m.channels[ch] - - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitPreRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - } - } - - if err := preMixRowTxn.Commit(); err != nil { - return err - } - - tickDuration := tickBaseDuration / time.Duration(m.pattern.GetTempo()) - - m.rowRenderState.Duration = tickDuration - m.rowRenderState.Samples = int(tickDuration.Seconds() * float64(s.SampleRate)) - m.rowRenderState.ticksThisRow = m.pattern.GetTicksThisRow() - m.rowRenderState.currentTick = 0 - - // run row processing, now that prestart has completed - for channelNum := range row.GetChannels() { - if channelNum >= m.GetNumChannels() { - continue - } - - cs := &m.channels[channelNum] - - if err := m.processRowForChannel(cs); err != nil { - return err - } - } - - return nil -} - -func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory, channel.Data]) error { - mem := cs.GetMemory() - mem.TremorMem().Reset() - - if txn := cs.GetTxn(); txn != nil { - if err := txn.CommitRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - - if err := txn.CommitPostRow(m, cs, cs.SemitoneSetterFactory); err != nil { - return err - } - } - return nil -} diff --git a/format/xm/playback/playback_render.go b/format/xm/playback/playback_render.go deleted file mode 100644 index 466b40a..0000000 --- a/format/xm/playback/playback_render.go +++ /dev/null @@ -1,115 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state" -) - -// OnTick runs the XM tick processing -func (m *Manager) OnTick() error { - m.premix = nil - premix, err := m.renderTick() - if err != nil { - return err - } - - m.premix = premix - return nil -} - -// GetPremixData gets the current premix data from the manager -func (m *Manager) GetPremixData() (*output.PremixData, error) { - return m.premix, nil -} - -// RenderOneRow renders the next single row from the song pattern data into a RowRender object -func (m *Manager) renderTick() (*output.PremixData, error) { - postMixRowTxn := m.pattern.StartTransaction() - defer func() { - postMixRowTxn.Cancel() - m.postMixRowTxn = nil - }() - m.postMixRowTxn = postMixRowTxn - - if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { - if err := m.processPatternRow(); err != nil { - return nil, err - } - } - - var finalData render.RowRender - premix := &output.PremixData{ - Userdata: &finalData, - SamplesLen: m.rowRenderState.Samples, - } - - if err := m.soundRenderTick(premix); err != nil { - return nil, err - } - - finalData.Order = int(m.pattern.GetCurrentOrder()) - finalData.Row = int(m.pattern.GetCurrentRow()) - finalData.Tick = m.rowRenderState.currentTick - if m.rowRenderState.currentTick == 0 { - finalData.RowText = m.getRowText() - } - - m.rowRenderState.currentTick++ - if m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { - postMixRowTxn.AdvanceRow = true - } - - if err := postMixRowTxn.Commit(); err != nil { - return nil, err - } - return premix, nil -} - -type rowRenderState struct { - state.RenderDetails - - ticksThisRow int - currentTick int -} - -func (m *Manager) soundRenderTick(premix *output.PremixData) error { - tick := m.rowRenderState.currentTick - var lastTick = (tick+1 == m.rowRenderState.ticksThisRow) - - for ch := range m.channels { - cs := &m.channels[ch] - if m.song.IsChannelEnabled(ch) { - - if err := m.processEffect(ch, cs, tick, lastTick); err != nil { - return err - } - - rr, err := cs.RenderRowTick(m.rowRenderState.RenderDetails, nil) - if err != nil { - return err - } - if rr != nil { - premix.Data = append(premix.Data, rr) - } - } - } - - premix.MixerVolume = m.GetMixerVolume() - return nil -} - -/** unused in XM, for now -func (m *Manager) ensureOPL2() { - if opl2 := m.GetOPL2Chip(); opl2 == nil { - if s := m.GetSampler(); s != nil { - opl2 = render.NewOPL2Chip(uint32(s.SampleRate)) - opl2.WriteReg(0x01, 0x20) // enable all waveforms - opl2.WriteReg(0x04, 0x00) // clear timer flags - opl2.WriteReg(0x08, 0x40) // clear CSW and set NOTE-SEL - opl2.WriteReg(0xBD, 0x00) // set default notes - m.SetOPL2Chip(opl2) - } - } -} -*/ diff --git a/format/xm/playback/playback_textoutput.go b/format/xm/playback/playback_textoutput.go deleted file mode 100644 index ee5cdc9..0000000 --- a/format/xm/playback/playback_textoutput.go +++ /dev/null @@ -1,27 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/player/render" -) - -func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { - nCh := 0 - for ch := range m.channels { - if !m.song.IsChannelEnabled(ch) { - continue - } - nCh++ - } - rowText := render.NewRowText[channel.Data](nCh, true) - for ch, cs := range m.channels { - if !m.song.IsChannelEnabled(ch) { - continue - } - - if cd := cs.GetData(); cd != nil { - rowText.Channels[ch] = *cd - } - } - return &rowText -} diff --git a/format/xm/settings/machine.go b/format/xm/settings/machine.go new file mode 100644 index 0000000..e14f873 --- /dev/null +++ b/format/xm/settings/machine.go @@ -0,0 +1,45 @@ +package settings + +import ( + xmFilter "github.com/gotracker/playback/format/xm/filter" + xmOscillator "github.com/gotracker/playback/format/xm/oscillator" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmPeriod "github.com/gotracker/playback/format/xm/period" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" +) + +func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { + var p TPeriod + switch any(p).(type) { + case period.Amiga: + return any(&amigaMachine).(*settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) + case period.Linear: + return any(&linearMachine).(*settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) + default: + panic("unsupported machine type") + } +} + +var ( + amigaMachine = settings.MachineSettings[period.Amiga, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + PeriodConverter: xmPeriod.AmigaConverter, + GetFilterFactory: xmFilter.Factory, + GetVibratoFactory: xmOscillator.VibratoFactory, + GetTremoloFactory: xmOscillator.TremoloFactory, + GetPanbrelloFactory: xmOscillator.PanbrelloFactory, + VoiceFactory: amigaVoiceFactory, + OPL2Enabled: false, + } + + linearMachine = settings.MachineSettings[period.Linear, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + PeriodConverter: xmPeriod.LinearConverter, + GetFilterFactory: xmFilter.Factory, + GetVibratoFactory: xmOscillator.VibratoFactory, + GetTremoloFactory: xmOscillator.TremoloFactory, + GetPanbrelloFactory: xmOscillator.PanbrelloFactory, + VoiceFactory: linearVoiceFactory, + OPL2Enabled: false, + } +) diff --git a/format/xm/settings/voicefactory.go b/format/xm/settings/voicefactory.go new file mode 100644 index 0000000..7401f69 --- /dev/null +++ b/format/xm/settings/voicefactory.go @@ -0,0 +1,20 @@ +package settings + +import ( + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVoice "github.com/gotracker/playback/format/xm/voice" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" +) + +type voiceFactory[TPeriod period.Period] struct{} + +func (voiceFactory[TPeriod]) NewVoice(config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { + return xmVoice.New(config) +} + +var ( + amigaVoiceFactory voiceFactory[period.Amiga] + linearVoiceFactory voiceFactory[period.Linear] +) diff --git a/format/xm/system/system.go b/format/xm/system/system.go new file mode 100644 index 0000000..28a33f8 --- /dev/null +++ b/format/xm/system/system.go @@ -0,0 +1,41 @@ +package system + +import ( + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/system" +) + +const ( + // DefaultC4SampleRate is the default c4 sample rate for XM samples + DefaultC4SampleRate = 8363 + // C4Period is the sampler (Amiga-style) period of the C-4 note + C4Period = 856 + + C4Octave = 4 + C4Note = C4Octave * NotesPerOctave + + floatDefaultC4SampleRate = float32(DefaultC4SampleRate) + + // XMBaseClock is the base clock speed of XM files + XMBaseClock frequency.Frequency = DefaultC4SampleRate * C4Period + + NotesPerOctave = 12 + SlideFinesPerSemitone = 4 + SemitonesPerNote = 16 + SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote + SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave + C4SlideFines = C4Note * SlideFinesPerNote +) + +var semitonePeriodTable = [...]uint16{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} + +var XMSystem system.ClockableSystem = system.ClockedSystem{ + MaxPastNotesPerChannel: 0, + BaseClock: XMBaseClock, + BaseFinetunes: C4SlideFines, + FinetunesPerOctave: SlideFinesPerOctave, + FinetunesPerNote: SlideFinesPerNote, + CommonPeriod: C4Period, + CommonRate: DefaultC4SampleRate, + SemitonePeriods: semitonePeriodTable, +} diff --git a/format/xm/voice/enveloper_filter.go b/format/xm/voice/enveloper_filter.go new file mode 100644 index 0000000..aa8f610 --- /dev/null +++ b/format/xm/voice/enveloper_filter.go @@ -0,0 +1,21 @@ +package voice + +// == FilterEnveloper == + +func (v *xmVoice[TPeriod]) EnableFilterEnvelope(enabled bool) { +} + +func (v xmVoice[TPeriod]) IsFilterEnvelopeEnabled() bool { + return false +} + +func (v xmVoice[TPeriod]) GetCurrentFilterEnvelope() uint8 { + return 0 +} + +func (v *xmVoice[TPeriod]) SetFilterEnvelopePosition(pos int) { +} + +func (v xmVoice[TPeriod]) GetFilterEnvelopePosition() int { + return 0 +} diff --git a/format/xm/voice/enveloper_pan.go b/format/xm/voice/enveloper_pan.go new file mode 100644 index 0000000..10851e8 --- /dev/null +++ b/format/xm/voice/enveloper_pan.go @@ -0,0 +1,37 @@ +package voice + +import ( + xmPanning "github.com/gotracker/playback/format/xm/panning" +) + +// == PanEnveloper == + +func (v *xmVoice[TPeriod]) EnablePanEnvelope(enabled bool) error { + return v.panEnv.SetEnabled(enabled) +} + +func (v xmVoice[TPeriod]) IsPanEnvelopeEnabled() bool { + return v.panEnv.IsEnabled() +} + +func (v xmVoice[TPeriod]) GetCurrentPanEnvelope() xmPanning.Panning { + if v.panEnv.IsEnabled() { + return v.panEnv.GetCurrentValue() + } + return xmPanning.DefaultPanning +} + +func (v *xmVoice[TPeriod]) SetPanEnvelopePosition(pos int) error { + doneCB, err := v.panEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { + doneCB(v) + } + return nil +} + +func (v xmVoice[TPeriod]) GetPanEnvelopePosition() int { + return v.panEnv.GetEnvelopePosition() +} diff --git a/format/xm/voice/enveloper_pitch.go b/format/xm/voice/enveloper_pitch.go new file mode 100644 index 0000000..465ea87 --- /dev/null +++ b/format/xm/voice/enveloper_pitch.go @@ -0,0 +1,25 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == PitchEnveloper == + +func (v *xmVoice[TPeriod]) EnablePitchEnvelope(enabled bool) { +} + +func (v xmVoice[TPeriod]) IsPitchEnvelopeEnabled() bool { + return false +} + +func (v xmVoice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { + return 0 +} + +func (v *xmVoice[TPeriod]) SetPitchEnvelopePosition(pos int) { +} + +func (v xmVoice[TPeriod]) GetPitchEnvelopePosition() int { + return 0 +} diff --git a/format/xm/voice/enveloper_volume.go b/format/xm/voice/enveloper_volume.go new file mode 100644 index 0000000..c45e9d8 --- /dev/null +++ b/format/xm/voice/enveloper_volume.go @@ -0,0 +1,37 @@ +package voice + +import ( + xmVolume "github.com/gotracker/playback/format/xm/volume" +) + +// == VolumeEnveloper == + +func (v *xmVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) error { + return v.volEnv.SetEnabled(enabled) +} + +func (v xmVoice[TPeriod]) IsVolumeEnvelopeEnabled() bool { + return v.volEnv.IsEnabled() +} + +func (v xmVoice[TPeriod]) GetCurrentVolumeEnvelope() xmVolume.XmVolume { + if v.volEnv.IsEnabled() { + return v.volEnv.GetCurrentValue() + } + return xmVolume.DefaultXmVolume +} + +func (v *xmVoice[TPeriod]) SetVolumeEnvelopePosition(pos int) error { + doneCB, err := v.volEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { + doneCB(v) + } + return nil +} + +func (v xmVoice[TPeriod]) GetVolumeEnvelopePosition() int { + return v.volEnv.GetEnvelopePosition() +} diff --git a/format/xm/voice/modulator_amp.go b/format/xm/voice/modulator_amp.go new file mode 100644 index 0000000..bb7be8c --- /dev/null +++ b/format/xm/voice/modulator_amp.go @@ -0,0 +1,60 @@ +package voice + +import ( + "github.com/gotracker/gomixing/volume" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/voice/types" +) + +// == AmpModulator == + +func (v *xmVoice[TPeriod]) SetActive(on bool) error { + return v.amp.SetActive(on) +} + +func (v xmVoice[TPeriod]) IsActive() bool { + return v.amp.IsActive() +} + +func (v *xmVoice[TPeriod]) SetMixingVolume(vol xmVolume.XmVolume) error { + return v.amp.SetMixingVolume(vol) +} + +func (v xmVoice[TPeriod]) GetMixingVolume() xmVolume.XmVolume { + return v.amp.GetMixingVolume() +} + +func (v *xmVoice[TPeriod]) SetVolume(vol xmVolume.XmVolume) error { + if vol.IsUseInstrumentVol() { + vol = v.voicer.GetDefaultVolume() + } + return v.amp.SetVolume(vol) +} + +func (v xmVoice[TPeriod]) GetVolume() xmVolume.XmVolume { + return v.amp.GetVolume() +} + +func (v *xmVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) error { + return v.amp.SetVolumeDelta(d) +} + +func (v xmVoice[TPeriod]) GetVolumeDelta() types.VolumeDelta { + return v.amp.GetVolumeDelta() +} + +func (v xmVoice[TPeriod]) IsFadeout() bool { + return v.fadeout.IsActive() +} + +func (v xmVoice[TPeriod]) GetFadeoutVolume() volume.Volume { + return v.fadeout.GetVolume() +} + +func (v xmVoice[TPeriod]) GetFinalVolume() volume.Volume { + vol := v.amp.GetFinalVolume() + if v.IsVolumeEnvelopeEnabled() { + vol *= v.GetCurrentVolumeEnvelope().ToVolume() + } + return vol * v.fadeout.GetFinalVolume() +} diff --git a/format/xm/voice/modulator_freq.go b/format/xm/voice/modulator_freq.go new file mode 100644 index 0000000..a840c4e --- /dev/null +++ b/format/xm/voice/modulator_freq.go @@ -0,0 +1,35 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == FreqModulator == + +func (v *xmVoice[TPeriod]) SetPeriod(period TPeriod) error { + return v.freq.SetPeriod(period) +} + +func (v *xmVoice[TPeriod]) GetPeriod() TPeriod { + return v.freq.GetPeriod() +} + +func (v *xmVoice[TPeriod]) SetPeriodDelta(delta period.Delta) error { + return v.freq.SetPeriodDelta(delta) +} + +func (v *xmVoice[TPeriod]) GetPeriodDelta() period.Delta { + return v.freq.GetPeriodDelta() +} + +func (v *xmVoice[TPeriod]) GetFinalPeriod() (TPeriod, error) { + p, err := v.freq.GetFinalPeriod() + if err != nil { + return p, err + } + if v.IsPitchEnvelopeEnabled() { + delta := v.GetCurrentPitchEnvelope() + p, err = v.inst.Static.PC.AddDelta(p, delta) + } + return p, err +} diff --git a/format/xm/voice/modulator_pan.go b/format/xm/voice/modulator_pan.go new file mode 100644 index 0000000..b33ce3a --- /dev/null +++ b/format/xm/voice/modulator_pan.go @@ -0,0 +1,34 @@ +package voice + +import ( + "github.com/gotracker/gomixing/panning" + xmPanning "github.com/gotracker/playback/format/xm/panning" + "github.com/gotracker/playback/voice/types" +) + +// == PanModulator == + +func (v *xmVoice[TPeriod]) SetPan(pan xmPanning.Panning) error { + return v.pan.SetPan(pan) +} + +func (v xmVoice[TPeriod]) GetPan() xmPanning.Panning { + return v.pan.GetPan() +} + +func (v *xmVoice[TPeriod]) SetPanDelta(d types.PanDelta) error { + return v.pan.SetPanDelta(d) +} + +func (v xmVoice[TPeriod]) GetPanDelta() types.PanDelta { + return v.pan.GetPanDelta() +} + +func (v xmVoice[TPeriod]) GetFinalPan() panning.Position { + if !v.IsPanEnvelopeEnabled() { + return v.pan.GetFinalPan() + } + + envPan := v.panEnv.GetCurrentValue() + return envPan.ToPosition() +} diff --git a/format/xm/voice/sampler.go b/format/xm/voice/sampler.go new file mode 100644 index 0000000..d2475af --- /dev/null +++ b/format/xm/voice/sampler.go @@ -0,0 +1,51 @@ +package voice + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" +) + +type voicerPos interface { + GetPos() sampling.Pos + SetPos(pos sampling.Pos) +} + +type voicerSampler interface { + GetSample(pos sampling.Pos) volume.Matrix +} + +func (v *xmVoice[TPeriod]) GetPos() (sampling.Pos, error) { + if vp, ok := v.voicer.(voicerPos); ok { + return vp.GetPos(), nil + } + return sampling.Pos{}, nil +} + +func (v *xmVoice[TPeriod]) SetPos(pos sampling.Pos) error { + if vp, ok := v.voicer.(voicerPos); ok { + vp.SetPos(pos) + } + return nil +} + +func (v *xmVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { + var dry volume.Matrix + if sampler, ok := v.voicer.(voicerSampler); ok { + dry = sampler.GetSample(pos) + if dry.Channels == 0 { + dry.Channels = v.voicer.GetNumChannels() + } + } + + vol := v.GetFinalVolume() + wet := dry.Apply(vol) + if v.voiceFilter != nil { + wet = v.voiceFilter.Filter(wet) + } + return wet +} + +func (v xmVoice[TPeriod]) GetSampleRate() frequency.Frequency { + return v.inst.SampleRate +} diff --git a/format/xm/voice/tracing.go b/format/xm/voice/tracing.go new file mode 100644 index 0000000..1d48d71 --- /dev/null +++ b/format/xm/voice/tracing.go @@ -0,0 +1,27 @@ +package voice + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" +) + +func (v xmVoice[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer) { + if t == nil { + return + } + + v.KeyModulator.DumpState(ch, t, "xmVoice.KeyModulator") + if v.voicer != nil { + v.voicer.DumpState(ch, t, "xmVoice.voicer") + } else { + t.TraceChannelWithComment(ch, "nil", "xmVoice.voicer") + } + v.amp.DumpState(ch, t, "xmVoice.amp") + v.freq.DumpState(ch, t, "xmVoice.freq") + v.pan.DumpState(ch, t, "xmVoice.pan") + v.volEnv.DumpState(ch, t, "xmVoice.volEnv") + v.panEnv.DumpState(ch, t, "xmVoice.panEnv") + v.vol0Opt.DumpState(ch, t, "xmVoice.vol0Opt") + //voiceFilter + //pluginFilter +} diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go new file mode 100644 index 0000000..9c48da2 --- /dev/null +++ b/format/xm/voice/voice.go @@ -0,0 +1,304 @@ +package voice + +import ( + "errors" + "fmt" + + "github.com/gotracker/playback/filter" + xmFilter "github.com/gotracker/playback/format/xm/filter" + xmOscillator "github.com/gotracker/playback/format/xm/oscillator" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/voice/fadeout" +) + +type Period interface { + period.Period +} + +type xmVoice[TPeriod Period] struct { + inst *instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + + fadeoutMode fadeout.Mode + + component.KeyModulator + + stopped bool + voicer component.Voicer[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] + amp component.AmpModulator[xmVolume.XmVolume, xmVolume.XmVolume] + fadeout component.FadeoutModulator + freq component.FreqModulator[TPeriod] + autoVibrato component.AutoVibratoModulator[TPeriod] + pan component.PanModulator[xmPanning.Panning] + volEnv component.VolumeEnvelope[xmVolume.XmVolume] + panEnv component.PanEnvelope[xmPanning.Panning] + vol0Opt component.Vol0Optimization + voiceFilter filter.Filter +} + +var ( + _ voice.Sampler = (*xmVoice[period.Linear])(nil) + _ voice.AmpModulator[xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume] = (*xmVoice[period.Linear])(nil) + _ voice.FadeoutModulator = (*xmVoice[period.Linear])(nil) + _ voice.FreqModulator[period.Linear] = (*xmVoice[period.Linear])(nil) + _ voice.PanModulator[xmPanning.Panning] = (*xmVoice[period.Linear])(nil) + _ voice.VolumeEnvelope[xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume] = (*xmVoice[period.Linear])(nil) + _ voice.PanEnvelope[xmPanning.Panning] = (*xmVoice[period.Linear])(nil) +) + +func New[TPeriod Period](config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { + v := &xmVoice[TPeriod]{} + + v.KeyModulator.Setup(component.KeyModulatorSettings{ + Attack: v.doAttack, + Release: v.doRelease, + Fadeout: v.doFadeout, + DeferredAttack: v.doDeferredAttack, + DeferredRelease: v.doDeferredRelease, + }) + + v.amp.Setup(component.AmpModulatorSettings[xmVolume.XmVolume, xmVolume.XmVolume]{ + Active: true, + DefaultMixingVolume: config.InitialMixing, + DefaultVolume: config.InitialVolume, + }) + + v.freq.Setup(component.FreqModulatorSettings[TPeriod]{ + PC: config.PC, + }) + + v.pan.Setup(component.PanModulatorSettings[xmPanning.Panning]{ + Enabled: config.PanEnabled, + InitialPan: config.InitialPan, + }) + + v.vol0Opt.Setup(config.Vol0Optimization) + + return v +} + +func (v *xmVoice[TPeriod]) doAttack() { + v.vol0Opt.Reset() + v.autoVibrato.ResetAutoVibrato() + + v.SetVolumeEnvelopePosition(0) + v.SetPitchEnvelopePosition(0) + v.SetPanEnvelopePosition(0) + v.SetFilterEnvelopePosition(0) + + v.fadeout.Reset() + v.volEnv.Attack() + v.panEnv.Attack() + if v.voicer != nil { + v.voicer.Attack() + } +} + +func (v *xmVoice[TPeriod]) doRelease() { + v.volEnv.Release() + v.panEnv.Release() + if v.voicer != nil { + v.voicer.Release() + } +} + +func (v *xmVoice[TPeriod]) doFadeout() { + if v.voicer != nil { + v.voicer.Fadeout() + } +} + +func (v *xmVoice[TPeriod]) doDeferredAttack() { + if v.voicer != nil { + v.voicer.DeferredAttack() + } +} + +func (v *xmVoice[TPeriod]) doDeferredRelease() { + if v.voicer != nil { + v.voicer.DeferredRelease() + } +} + +func (v xmVoice[TPeriod]) getFadeoutEnabled() bool { + return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) +} + +func (v *xmVoice[TPeriod]) SetPlaybackRate(outputRate frequency.Frequency) error { + if v.voiceFilter != nil { + v.voiceFilter.SetPlaybackRate(outputRate) + } + return nil +} + +func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { + v.inst = inst + + switch d := inst.GetData().(type) { + case *instrument.PCM[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]: + v.fadeoutMode = d.FadeOut.Mode + + v.fadeout.Setup(component.FadeoutModulatorSettings{ + Enabled: d.FadeOut.Mode != fadeout.ModeDisabled, + GetActive: v.getFadeoutEnabled, + Amount: d.FadeOut.Amount, + }) + + volEnvSettings := component.EnvelopeSettings[xmVolume.XmVolume, xmVolume.XmVolume]{ + Envelope: d.VolEnv, + } + if d.VolEnvFinishFadesOut { + volEnvSettings.OnFinished = func(v voice.Voice) { + v.Fadeout() + } + } + v.volEnv.Setup(volEnvSettings) + + v.panEnv.Setup(component.EnvelopeSettings[xmPanning.Panning, xmPanning.Panning]{ + Envelope: d.PanEnv, + }) + + if err := v.amp.SetMixingVolumeOverride(d.MixingVolume); err != nil { + return err + } + + var s component.Sampler[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] + s.Setup(component.SamplerSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume]{ + Sample: d.Sample, + DefaultVolume: inst.GetDefaultVolume(), + MixVolume: xmVolume.DefaultXmMixingVolume, + WholeLoop: d.Loop, + SustainLoop: d.SustainLoop, + }) + v.voicer = &s + + default: + return fmt.Errorf("unhandled instrument type: %T", d) + } + if inst == nil { + return errors.New("instrument is nil") + } + + v.autoVibrato.Setup(autovibrato.AutoVibratoSettings[TPeriod]{ + AutoVibratoConfig: inst.Static.AutoVibrato, + Factory: xmOscillator.Factory, + }) + + info := inst.GetVoiceFilterInfo() + f, err := xmFilter.Factory(info.Name, inst.SampleRate, info.Params) + if err != nil { + return fmt.Errorf("filter factory(%q) error: %w", info.Name, err) + } + v.voiceFilter = f + + v.Reset() + return nil +} + +func (v *xmVoice[TPeriod]) Reset() error { + v.KeyModulator.Release() + v.stopped = false + return errors.Join( + v.amp.Reset(), + v.fadeout.Reset(), + v.freq.Reset(), + v.autoVibrato.Reset(), + v.pan.Reset(), + v.volEnv.Reset(), + v.panEnv.Reset(), + v.vol0Opt.Reset(), + ) +} + +func (v *xmVoice[TPeriod]) Stop() { + v.stopped = true +} + +func (v xmVoice[TPeriod]) IsDone() bool { + if v.voicer == nil || v.stopped { + return true + } + + if v.fadeout.IsActive() { + return v.fadeout.GetVolume() <= 0 + } + + return v.vol0Opt.IsDone() +} + +func (v *xmVoice[TPeriod]) SetMuted(muted bool) error { + return v.amp.SetMuted(muted) +} + +func (v xmVoice[TPeriod]) IsMuted() bool { + return v.amp.IsMuted() +} + +func (v *xmVoice[TPeriod]) Tick() error { + v.fadeout.Advance() + v.autoVibrato.Advance() + if v.IsVolumeEnvelopeEnabled() { + if doneCB := v.volEnv.Advance(); doneCB != nil { + doneCB(v) + } + } + if v.IsPanEnvelopeEnabled() { + if doneCB := v.panEnv.Advance(); doneCB != nil { + doneCB(v) + } + } + + // has to be after the mod/env updates + v.KeyModulator.DeferredUpdate() + + v.KeyModulator.Advance() + return nil +} + +func (v *xmVoice[TPeriod]) RowEnd() error { + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + return nil +} + +func (v *xmVoice[TPeriod]) Clone(bool) voice.Voice { + vv := xmVoice[TPeriod]{ + inst: v.inst, + fadeoutMode: v.fadeoutMode, + stopped: v.stopped, + amp: v.amp.Clone(), + fadeout: v.fadeout.Clone(), + freq: v.freq.Clone(), + autoVibrato: v.autoVibrato.Clone(), + pan: v.pan.Clone(), + panEnv: v.panEnv.Clone(nil), + vol0Opt: v.vol0Opt.Clone(), + } + + vv.volEnv = v.volEnv.Clone(func(v voice.Voice) { + vv.Fadeout() + }) + + vv.KeyModulator = v.KeyModulator.Clone(component.KeyModulatorSettings{ + Attack: vv.doAttack, + Release: vv.doRelease, + Fadeout: vv.doFadeout, + DeferredAttack: vv.doDeferredAttack, + DeferredRelease: vv.doDeferredRelease, + }) + + if v.voicer != nil { + vv.voicer = v.voicer.Clone() + } + + if v.voiceFilter != nil { + vv.voiceFilter = v.voiceFilter.Clone() + } + + return &vv +} diff --git a/format/xm/volume/voleffect.go b/format/xm/volume/voleffect.go index e193e69..ca26f38 100644 --- a/format/xm/volume/voleffect.go +++ b/format/xm/volume/voleffect.go @@ -15,7 +15,7 @@ func (v VolEffect) Volume() volume.Volume { if v == 0x00 { return volume.VolumeUseInstVol } - return XmVolume(v - 0x10).Volume() + return XmVolume(v - 0x10).ToVolume() } // ToVolume converts an xm volume to a player volume diff --git a/format/xm/volume/volume.go b/format/xm/volume/volume.go index dd5b8e5..53e06d8 100644 --- a/format/xm/volume/volume.go +++ b/format/xm/volume/volume.go @@ -1,12 +1,20 @@ package volume import ( + "math" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/voice/types" +) + +const ( + DefaultXmVolume = XmVolume(0x40) + DefaultXmMixingVolume = XmVolume(0x18) ) var ( // DefaultVolume is the default volume value for most everything in xm format - DefaultVolume = ToVolume(0x10 + 0x40) + DefaultVolume = ToVolume(0x10 + VolEffect(DefaultXmVolume)) // DefaultMixingVolume is the default mixing volume DefaultMixingVolume = volume.Volume(0x30) / 0x80 @@ -15,14 +23,49 @@ var ( // XmVolume is a helpful converter from the XM range of 0-64 into a volume type XmVolume uint8 +var ( + _ types.VolumeMaxer[XmVolume] = XmVolume(0) + _ types.VolumeDeltaer[XmVolume] = XmVolume(0) +) + const cVolumeXMCoeff = volume.Volume(1) / 0x40 // Volume returns the volume from the internal format -func (v XmVolume) Volume() volume.Volume { - return volume.Volume(v) * cVolumeXMCoeff +func (v XmVolume) ToVolume() volume.Volume { + if v != 0xff { + return volume.Volume(v) * cVolumeXMCoeff + } + return volume.VolumeUseInstVol +} + +func (v XmVolume) IsInvalid() bool { + return v > 0x40 && v != 0xff +} + +func (v XmVolume) IsUseInstrumentVol() bool { + return v == 0xff +} + +func (XmVolume) GetMax() XmVolume { + return 0x40 +} + +func (v XmVolume) FMA(multiplier, add float32) XmVolume { + if v == XmVolume(0xff) { + return v + } + + return XmVolume(min(max(math.FMA(float64(v), float64(multiplier), float64(add)), 0), 64)) +} + +func (v XmVolume) AddDelta(d types.VolumeDelta) XmVolume { + return XmVolume(min(max(int16(v)+int16(d), 0), 0x40)) } // ToVolumeXM returns the VolumeXM representation of a volume func ToVolumeXM(v volume.Volume) XmVolume { - return XmVolume(v * 0x40) + if v != volume.VolumeUseInstVol { + return XmVolume(v * 0x40) + } + return XmVolume(0xff) } diff --git a/format/xm/xm.go b/format/xm/xm.go index e27de1d..19f16ce 100644 --- a/format/xm/xm.go +++ b/format/xm/xm.go @@ -4,13 +4,19 @@ package xm import ( "io" - "github.com/gotracker/playback" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/load" + xmSettings "github.com/gotracker/playback/format/xm/settings" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/song" "github.com/gotracker/playback/util" ) -type format struct{} +type format struct { + common.Format +} var ( // XM is the exported interface to the XM file loader @@ -18,7 +24,7 @@ var ( ) // Load loads an XM file into a playback system -func (f format) Load(filename string, features []feature.Feature) (playback.Playback, error) { +func (f format) Load(filename string, features []feature.Feature) (song.Data, error) { r, err := util.ReadFile(filename) if err != nil { return nil, err @@ -28,6 +34,11 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play } // LoadFromReader loads an XM file on a reader into a playback system -func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { +func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (song.Data, error) { return load.XM(r, features) } + +func init() { + machine.RegisterMachine(xmSettings.GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(xmSettings.GetMachineSettings[period.Linear]()) +} diff --git a/frequency/frequency.go b/frequency/frequency.go new file mode 100644 index 0000000..402b444 --- /dev/null +++ b/frequency/frequency.go @@ -0,0 +1,25 @@ +package frequency + +import ( + "fmt" +) + +// Frequency is a frequency value, in Hertz (Hz) +type Frequency float64 + +func (f Frequency) GoString() string { + switch { + case f < 1_000: + return fmt.Sprintf("%fHz", f) + case f < 1_000_000: + return fmt.Sprintf("%3.fkHz", f) + case f < 1_000_000_000: + return fmt.Sprintf("%3.fMHz", f) + case f < 1_000_000_000_000: + return fmt.Sprintf("%3.fGHz", f) + case f < 1_000_000_000_000_000: + return fmt.Sprintf("%3.fTHz", f) + default: + return fmt.Sprintf("%fHz", f) + } +} diff --git a/go.mod b/go.mod index 2e3b336..d0ea5ad 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,12 @@ module github.com/gotracker/playback -go 1.18 +go 1.21 require ( - github.com/gotracker/goaudiofile v1.0.14 - github.com/gotracker/gomixing v1.3.0 + github.com/gotracker/goaudiofile v1.0.16 + github.com/gotracker/gomixing v1.3.1 github.com/gotracker/opl2 v1.0.1 github.com/heucuva/comparison v1.0.0 github.com/heucuva/optional v0.0.1 - golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a ) - -require github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index c5aa1a4..ed098c8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/gotracker/goaudiofile v1.0.14 h1:gbut0vivYxV0EWH/rzvqukmx6GjbtvMylLHQpZ//7d0= -github.com/gotracker/goaudiofile v1.0.14/go.mod h1:+biBmTEKcFRF4hCR1flCtqOA76p6OReIAwqqV+07N8Y= -github.com/gotracker/gomixing v1.3.0 h1:L0pOTsjIppAbSoo+yYRVghrfF2dcAywUTsk6Ig2Z/IM= -github.com/gotracker/gomixing v1.3.0/go.mod h1:KSwLWBk4HMKTVZH+zq4Db7nlDVcRegIL4uStkat0ASg= +github.com/gotracker/goaudiofile v1.0.16 h1:+QlrDbZluWs01NZdg3JOuM+Zm98o1NNFVbtts2Fkw2M= +github.com/gotracker/goaudiofile v1.0.16/go.mod h1:mX/CjpkoClUFrGQ8MU6x2hm4ma/ClQTh83wwHhLC7RY= +github.com/gotracker/gomixing v1.3.1 h1:u2AbhZoFtqXRgY0wxQXWDWbnZpIG+rLfSEd91YKIqnI= +github.com/gotracker/gomixing v1.3.1/go.mod h1:y0lfvWy49qzwfQiATw+kBjAlNTK75Ix9NpxPh+8QgPs= github.com/gotracker/opl2 v1.0.1 h1:1PVNs0dXqEAQxdws7fz2WEE3nSKkMb1osTTT7KgEi5g= github.com/gotracker/opl2 v1.0.1/go.mod h1:lW1WbZlh7svEMpurp9LLYWSyf1WPAb750cQ7xGIhCnY= github.com/heucuva/comparison v1.0.0 h1:xxXNKS9GKHetQavOz35FitlAXWvmvM3U6M5IRIw7kN8= @@ -10,6 +10,7 @@ github.com/heucuva/optional v0.0.1 h1:tLbVBMQBKzQVfe43bHQFSxjhTzYcRK8frnTBG6FLks github.com/heucuva/optional v0.0.1/go.mod h1:2AtE/X9279wzrHLkCNvKl0xP7AiEIj3RijGKwbO8R3M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 h1:x03zeu7B2B11ySp+daztnwM5oBJ/8wGUSqrwcw9L0RA= -golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/index/channel.go b/index/channel.go new file mode 100644 index 0000000..b3826cc --- /dev/null +++ b/index/channel.go @@ -0,0 +1,18 @@ +package index + +const ( + InvalidChannel = Channel(0xFFFF) + InvalidOPLChannel = OPLChannel(0xFF) +) + +type Channel uint16 + +func (c Channel) IsValid() bool { + return c != InvalidChannel +} + +type OPLChannel uint8 + +func (c OPLChannel) IsValid() bool { + return c != InvalidOPLChannel +} diff --git a/index/pattern.go b/index/pattern.go index ee8ba52..179e351 100644 --- a/index/pattern.go +++ b/index/pattern.go @@ -1,5 +1,11 @@ package index +import "errors" + +var ( + ErrNextPattern = errors.New("go to next pattern") +) + // Pattern is an index into the pattern list type Pattern uint8 diff --git a/instrument/instrument.go b/instrument/instrument.go index efeae86..0e2775f 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -3,69 +3,97 @@ package instrument import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" + "github.com/heucuva/optional" "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" - "github.com/heucuva/optional" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/types" ) +type InstrumentIntf interface { + IsInvalid() bool + GetSampleRate() frequency.Frequency + SetSampleRate(sampleRate frequency.Frequency) + GetLength() sampling.Pos + SetFinetune(ft note.Finetune) + GetFinetune() note.Finetune + GetID() ID + GetSemitoneShift() int8 + GetNewNoteAction() note.Action + GetData() Data + GetVoiceFilterInfo() filter.Info + GetPluginFilterInfo() filter.Info + IsReleaseNote(n note.Note) bool + IsStopNote(n note.Note) bool + GetDefaultVolumeGeneric() volume.Volume +} + // StaticValues are the static values associated with an instrument -type StaticValues struct { +type StaticValues[TPeriod types.Period, TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { + PC period.PeriodConverter[TPeriod] Filename string Name string ID ID - Volume volume.Volume + Volume TVolume + MixingVolume optional.Value[TMixingVolume] + Panning optional.Value[TPanning] RelativeNoteNumber int8 - AutoVibrato voice.AutoVibrato + AutoVibrato autovibrato.AutoVibratoConfig[TPeriod] NewNoteAction note.Action Finetune note.Finetune - FilterFactory filter.Factory - PluginFilter filter.Factory + VoiceFilter filter.Info + PluginFilter filter.Info } // Instrument is the mildly-decoded instrument/sample header -type Instrument struct { - Static StaticValues - Inst Data - C2Spd period.Frequency - Finetune optional.Value[note.Finetune] +type Instrument[TPeriod types.Period, TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { + Static StaticValues[TPeriod, TMixingVolume, TVolume, TPanning] + Inst Data + SampleRate frequency.Frequency + Finetune optional.Value[note.Finetune] } // IsInvalid always returns false (valid) -func (inst Instrument) IsInvalid() bool { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) IsInvalid() bool { return false } -// GetC2Spd returns the C2SPD value for the instrument +// GetSampleRate returns the central-note sample rate value for the instrument // This may get mutated if a finetune effect is processed -func (inst Instrument) GetC2Spd() period.Frequency { - return inst.C2Spd +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetSampleRate() frequency.Frequency { + return inst.SampleRate } -// SetC2Spd sets the C2SPD value for the instrument -func (inst *Instrument) SetC2Spd(c2spd period.Frequency) { - inst.C2Spd = c2spd +// SetSampleRate sets the central-note sample rate value for the instrument +func (inst *Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) SetSampleRate(sampleRate frequency.Frequency) { + inst.SampleRate = sampleRate } // GetDefaultVolume returns the default volume value for the instrument -func (inst Instrument) GetDefaultVolume() volume.Volume { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetDefaultVolume() TVolume { return inst.Static.Volume } +// GetDefaultVolumeGeneric returns the default volume value for the instrument +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetDefaultVolumeGeneric() volume.Volume { + return inst.Static.Volume.ToVolume() +} + // GetLength returns the length of the instrument -func (inst Instrument) GetLength() sampling.Pos { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetLength() sampling.Pos { return inst.Inst.GetLength() } // SetFinetune sets the finetune value on the instrument -func (inst *Instrument) SetFinetune(ft note.Finetune) { +func (inst *Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) SetFinetune(ft note.Finetune) { inst.Finetune.Set(ft) } // GetFinetune returns the finetune value on the instrument -func (inst Instrument) GetFinetune() note.Finetune { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetFinetune() note.Finetune { if ft, ok := inst.Finetune.Get(); ok { return ft } @@ -73,59 +101,55 @@ func (inst Instrument) GetFinetune() note.Finetune { } // GetID returns the instrument number (1-based) -func (inst Instrument) GetID() ID { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetID() ID { return inst.Static.ID } // GetSemitoneShift returns the amount of semitones worth of shift to play the instrument at -func (inst Instrument) GetSemitoneShift() int8 { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetSemitoneShift() int8 { return inst.Static.RelativeNoteNumber } -// GetKind returns the kind of the instrument -func (inst Instrument) GetKind() Kind { - return inst.Inst.GetKind() -} - // GetNewNoteAction returns the NewNoteAction associated to the instrument -func (inst Instrument) GetNewNoteAction() note.Action { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetNewNoteAction() note.Action { return inst.Static.NewNoteAction } // GetData returns the instrument-specific data interface -func (inst Instrument) GetData() Data { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetData() Data { return inst.Inst } -// GetFilterFactory returns the factory for the channel filter -func (inst Instrument) GetFilterFactory() filter.Factory { - return inst.Static.FilterFactory +// GetVoiceFilterInfo returns the factory for the channel filter +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetVoiceFilterInfo() filter.Info { + return inst.Static.VoiceFilter } -// GetPluginFilterFactory returns the factory for the channel plugin filter -func (inst Instrument) GetPluginFilterFactory() filter.Factory { +// GetPluginFilterInfo returns the factory for the channel plugin filter +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetPluginFilterInfo() filter.Info { return inst.Static.PluginFilter } -// GetAutoVibrato returns the settings for the autovibrato system -func (inst Instrument) GetAutoVibrato() voice.AutoVibrato { - return inst.Static.AutoVibrato -} - // IsReleaseNote returns true if the note is a release (Note-Off) -func (inst Instrument) IsReleaseNote(n note.Note) bool { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) IsReleaseNote(n note.Note) bool { switch n.Type() { case note.SpecialTypeStopOrRelease: - return inst.GetKind() == KindOPL2 + switch inst.GetData().(type) { + case *OPL2: + return true + } } return note.IsRelease(n) } // IsStopNote returns true if the note is a stop (Note-Cut) -func (inst Instrument) IsStopNote(n note.Note) bool { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) IsStopNote(n note.Note) bool { switch n.Type() { case note.SpecialTypeStopOrRelease: - return inst.GetKind() == KindPCM + switch inst.GetData().(type) { + case *OPL2: + return true + } } return note.IsRelease(n) } diff --git a/instrument/opl2.go b/instrument/opl2.go index 5c2ddac..4441a00 100644 --- a/instrument/opl2.go +++ b/instrument/opl2.go @@ -76,10 +76,6 @@ type OPL2 struct { AdditiveSynthesis bool } -func (OPL2) GetKind() Kind { - return KindOPL2 -} - func (OPL2) GetLength() sampling.Pos { return sampling.Pos{Pos: math.MaxInt64} } diff --git a/instrument/pcm.go b/instrument/pcm.go index a9d43af..49bcf1f 100644 --- a/instrument/pcm.go +++ b/instrument/pcm.go @@ -1,33 +1,32 @@ package instrument import ( - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/voice/envelope" "github.com/gotracker/playback/voice/fadeout" "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/pcm" + "github.com/gotracker/playback/voice/pitchpan" + "github.com/gotracker/playback/voice/types" + "github.com/heucuva/optional" ) // PCM is a PCM-data instrument -type PCM struct { - Sample pcm.Sample - Loop loop.Loop - SustainLoop loop.Loop - Panning panning.Position - MixingVolume volume.Volume - FadeOut fadeout.Settings - VolEnv envelope.Envelope[volume.Volume] - PanEnv envelope.Envelope[panning.Position] - PitchFiltMode bool // true = filter, false = pitch - PitchFiltEnv envelope.Envelope[int8] // this is either pitch or filter +type PCM[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { + Sample pcm.Sample + Loop loop.Loop + SustainLoop loop.Loop + Panning optional.Value[TPanning] + MixingVolume optional.Value[TMixingVolume] + FadeOut fadeout.Settings + PitchPan pitchpan.PitchPan + VolEnv envelope.Envelope[TVolume] + VolEnvFinishFadesOut bool + PanEnv envelope.Envelope[TPanning] + PitchFiltMode bool // true = filter, false = pitch + PitchFiltEnv envelope.Envelope[types.PitchFiltValue] // this is either pitch or filter } -func (PCM) GetKind() Kind { - return KindPCM -} - -func (p PCM) GetLength() sampling.Pos { +func (p PCM[TMixingVolume, TVolume, TPanning]) GetLength() sampling.Pos { return sampling.Pos{Pos: p.Sample.Length()} } diff --git a/instrument/util.go b/instrument/util.go index bd03d4b..15baab9 100644 --- a/instrument/util.go +++ b/instrument/util.go @@ -9,21 +9,11 @@ import ( // ID is an identifier for an instrument/sample that means something to the format type ID interface { IsEmpty() bool + GetIndexAndSample() (int, int) fmt.Stringer } // Data is the interface to implementation-specific functions on an instrument type Data interface { - GetKind() Kind GetLength() sampling.Pos } - -// Kind defines the kind of instrument -type Kind int - -const ( - // KindPCM defines a PCM instrument - KindPCM = Kind(iota) - // KindOPL2 defines an OPL2 instrument - KindOPL2 -) diff --git a/internal/examples/bufferload/example.go b/internal/examples/bufferload/example.go index 1372da9..9673a2e 100644 --- a/internal/examples/bufferload/example.go +++ b/internal/examples/bufferload/example.go @@ -10,6 +10,9 @@ import ( "github.com/gotracker/playback/format" "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/song" ) @@ -50,28 +53,26 @@ func ExamplePlayBufferToStdout() { // the song one time through. features = append(features, feature.SongLoop{Count: 0}) - // There's an automagical loader utility which divines the file type and presents a player that can - // effectively play it. See `ExamplePlayFileToStdout` (./internal/examples/fileload) for an example - // of the file loader version of this call. In this example, we know the format, so we can pass in - // the specific format loader we want to use. - player, _, err := format.LoadFromReader("mod", bytes.NewReader(modfile), features) + // There's an automagical loader utility which divines the file type and presents a song that can + // effectively represent it. See `ExamplePlayFileToStdout` (./internal/examples/fileload) for an + // example of the file loader version of this call. In this example, we know the format, so we can + // pass in the specific format loader we want to use. + songData, songFormat, err := format.LoadFromReader("mod", bytes.NewReader(modfile), features) if err != nil { panic(err) } - // Now that we have a player allocated for the format, we need to tell it the minimal configuration - // for the stream of data we are wanting to produce - namely, the sampling rate and the number of - // channels. These two parameters are fundamental to a huge number of operations, so they must be - // set outside of the configuration process you will see below. - if err := player.SetupSampler(sampleRate, channels); err != nil { + // Here is where we get a final chance to submit any overrides or configurations we want to + // supply - we can send it the configuration we already have built up, since it will know how to + // pull the settings it wants, so no need to worry about filtering or splitting out the settings. + var userSettings settings.UserSettings + if err := songFormat.ConvertFeaturesToSettings(&userSettings, features); err != nil { panic(err) } - // The player's nearly ready to start playing! Here is where we get a final chance to submit any - // overrides or configurations we want to supply - we can send it the configuration we already - // have built up, since it will know how to pull the settings it wants, so no need to worry about - // filtering or splitting out the settings. - if err := player.Configure(features); err != nil { + // Next, create a player machine to operate over the song configured by the settings. + player, err := machine.NewMachine(songData, userSettings) + if err != nil { panic(err) } @@ -89,6 +90,21 @@ func ExamplePlayBufferToStdout() { // to end. defer close(premixDataChannel) + // Now that we have a player allocated for the format, we need to tell it the minimal configuration + // for the stream of data we are wanting to produce - namely, the sampling rate and the number of + // channels. These first two parameters are fundamental to a huge number of operations, so they must + // be set outside of the configuration process you will see below. The third parameter provides a + // way for the calling application (our example) to get the generated output data in the form of + // pre-mixed packets. These packets can be further mixed into audio streams for use with sound + // devices and files. + out := sampler.NewSampler(sampleRate, channels, func(premix *output.PremixData) { + // put our premixed data into the premixDataChannel we built earlier. + premixDataChannel <- premix + }) + if out == nil { + panic(errors.New("could not create sampler")) + } + // Our desire is to output a specific format of PCM audio data to the standard output device, so // we need to mix and convert the pre-mix data into that format. This mixer will be able to do // just that. @@ -117,9 +133,10 @@ func ExamplePlayBufferToStdout() { playerUpdateLoop: for { // Now we need to tell the player to update its internal state - this will generate a single - // row tick's worth of pre-mix data and add it to the `premixDataChannel`. - // normally, we would want to set up a goroutine for this call to run in and - if err := player.Update(0, premixDataChannel); err != nil { + // row tick's worth of pre-mix data and call our callback function specified in the Sampler + // stage we specified earlier. Normally, we would want to set up a goroutine for this call to + // run in, but in this example, we're fine to do a simple loop. + if err := player.Tick(out); err != nil { // In the event we finish our song, we will receive a specific error message informing us // we can quit. if errors.Is(err, song.ErrStopSong) { diff --git a/internal/examples/fileload/example.go b/internal/examples/fileload/example.go index 1bcf569..6cfb539 100644 --- a/internal/examples/fileload/example.go +++ b/internal/examples/fileload/example.go @@ -9,6 +9,9 @@ import ( "github.com/gotracker/playback/format" "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/song" ) @@ -53,24 +56,22 @@ func ExamplePlayFileToStdout() { // There's an automagical loader utility which divines the file type and presents a player that can // effectively play it. See `ExamplePlayBufferToStdout` (./internal/examples/bufferload) for an // example of the io.Reader version of this call. - player, _, err := format.Load(filename, features) + songData, songFormat, err := format.Load(filename, features) if err != nil { panic(err) } - // Now that we have a player allocated for the format, we need to tell it the minimal configuration - // for the stream of data we are wanting to produce - namely, the sampling rate and the number of - // channels. These two parameters are fundamental to a huge number of operations, so they must be - // set outside of the configuration process you will see below. - if err := player.SetupSampler(sampleRate, channels); err != nil { + // Here is where we get a final chance to submit any overrides or configurations we want to + // supply - we can send it the configuration we already have built up, since it will know how to + // pull the settings it wants, so no need to worry about filtering or splitting out the settings. + var userSettings settings.UserSettings + if err := songFormat.ConvertFeaturesToSettings(&userSettings, features); err != nil { panic(err) } - // The player's nearly ready to start playing! Here is where we get a final chance to submit any - // overrides or configurations we want to supply - we can send it the configuration we already - // have built up, since it will know how to pull the settings it wants, so no need to worry about - // filtering or splitting out the settings. - if err := player.Configure(features); err != nil { + // Next, create a player machine to operate over the song configured by the settings. + player, err := machine.NewMachine(songData, userSettings) + if err != nil { panic(err) } @@ -88,6 +89,21 @@ func ExamplePlayFileToStdout() { // to end. defer close(premixDataChannel) + // Now that we have a player allocated for the format, we need to tell it the minimal configuration + // for the stream of data we are wanting to produce - namely, the sampling rate and the number of + // channels. These first two parameters are fundamental to a huge number of operations, so they must + // be set outside of the configuration process you will see below. The third parameter provides a + // way for the calling application (our example) to get the generated output data in the form of + // pre-mixed packets. These packets can be further mixed into audio streams for use with sound + // devices and files. + out := sampler.NewSampler(sampleRate, channels, func(premix *output.PremixData) { + // put our premixed data into the premixDataChannel we built earlier. + premixDataChannel <- premix + }) + if out == nil { + panic(errors.New("could not create sampler")) + } + // Our desire is to output a specific format of PCM audio data to the standard output device, so // we need to mix and convert the pre-mix data into that format. This mixer will be able to do // just that. @@ -116,9 +132,10 @@ func ExamplePlayFileToStdout() { playerUpdateLoop: for { // Now we need to tell the player to update its internal state - this will generate a single - // row tick's worth of pre-mix data and add it to the `premixDataChannel`. - // normally, we would want to set up a goroutine for this call to run in and - if err := player.Update(0, premixDataChannel); err != nil { + // row tick's worth of pre-mix data and call our callback function specified in the Sampler + // stage we specified earlier. Normally, we would want to set up a goroutine for this call to + // run in, but in this example, we're fine to do a simple loop. + if err := player.Tick(out); err != nil { // In the event we finish our song, we will receive a specific error message informing us // we can quit. if errors.Is(err, song.ErrStopSong) { diff --git a/note/action.go b/note/action.go index 00452b1..073e0d2 100644 --- a/note/action.go +++ b/note/action.go @@ -1,5 +1,7 @@ package note +import "fmt" + // Action is the action to take on a note type Action uint8 @@ -17,3 +19,20 @@ const ( // ActionRetrigger will perform a key-on for the note/instrument playback immediately ActionRetrigger ) + +func (a Action) String() string { + switch a { + case ActionCut: + return "ActionCut" + case ActionContinue: + return "ActionContinue" + case ActionRelease: + return "ActionRelease" + case ActionFadeout: + return "ActionFadeout" + case ActionRetrigger: + return "ActionRetrigger" + default: + return fmt.Sprintf("Unknown[%d]", int(a)) + } +} diff --git a/note/keyoct.go b/note/keyoct.go index e8a41e0..da07317 100644 --- a/note/keyoct.go +++ b/note/keyoct.go @@ -40,6 +40,8 @@ const ( KeyInvalid4 ) +const NumKeys = int(KeyB + 1) + // IsInvalid returns true if the key is invalid func (k Key) IsInvalid() bool { switch k { diff --git a/note/note.go b/note/note.go index 8b82d6b..fe951a6 100644 --- a/note/note.go +++ b/note/note.go @@ -10,6 +10,7 @@ const ( SpecialTypeStop SpecialTypeNormal SpecialTypeStopOrRelease + SpecialTypeFadeout SpecialTypeInvalid ) @@ -57,6 +58,16 @@ func (n StopNote) Type() SpecialType { return SpecialTypeStop } +type FadeoutNote baseNote + +func (n FadeoutNote) String() string { + return "vvv" +} + +func (n FadeoutNote) Type() SpecialType { + return SpecialTypeFadeout +} + // Normal is a standard note, which is a combination of key and octave type Normal Semitone diff --git a/note/note_test.go b/note/note_test.go index 731a4ac..6a21fcd 100644 --- a/note/note_test.go +++ b/note/note_test.go @@ -1,7 +1,6 @@ package note_test import ( - "fmt" "testing" "github.com/heucuva/comparison" @@ -11,74 +10,12 @@ import ( // testPeriod defines a sampler period that follows the Amiga-style approach of note // definition. Useful in calculating resampling. -type testPeriod float32 +type testPeriod = period.Amiga -// AddInteger truncates the current period to an integer and adds the delta integer in -// then returns the resulting period -func (p testPeriod) AddInteger(delta int) testPeriod { - period := testPeriod(int(p) + delta) - return period -} - -// Add adds the current period to a delta value then returns the resulting period -func (p testPeriod) AddDelta(delta period.Delta) period.Period { - d := period.ToPeriodDelta(delta) - p += testPeriod(d) - return p -} - -// Compare returns: -// -1 if the current period is higher frequency than the `rhs` period -// 0 if the current period is equal in frequency to the `rhs` period -// 1 if the current period is lower frequency than the `rhs` period -func (p testPeriod) Compare(rhs period.Period) comparison.Spaceship { - lf := p.GetFrequency() - rf := rhs.GetFrequency() - - switch { - case lf < rf: - return comparison.SpaceshipRightGreater - case lf > rf: - return comparison.SpaceshipLeftGreater - default: - return comparison.SpaceshipEqual - } -} - -// Lerp linear-interpolates the current period with the `rhs` period -func (p testPeriod) Lerp(t float64, rhs period.Period) period.Period { - right := testPeriod(0) - if r, ok := rhs.(*testPeriod); ok { - right = *r - } - - delta := period.PeriodDelta(t * (float64(right) - float64(p))) - p.AddDelta(delta) - return p -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p testPeriod) GetSamplerAdd(samplerSpeed float64) float64 { - period := float64(p) - if period == 0 { - return 0 - } - return samplerSpeed / period -} - -// GetFrequency returns the frequency defined by the period -func (p testPeriod) GetFrequency() period.Frequency { - return period.Frequency(p.GetSamplerAdd(float64(8363 * 1712))) -} - -func (p *testPeriod) String() string { - return fmt.Sprintf("%f", *p) -} - -func periodCompareTest(t *testing.T, lhs period.Period, rhs period.Period, expected comparison.Spaceship) { +func periodCompareTest(t *testing.T, lhs, rhs testPeriod, expected comparison.Spaceship) { t.Helper() - if period.ComparePeriods(lhs, rhs) != expected { + if lhs.Compare(rhs) != expected { t.Fatalf("%v <=> %v was not %v", lhs, rhs, expected) } } @@ -86,13 +23,13 @@ func periodCompareTest(t *testing.T, lhs period.Period, rhs period.Period, expec func TestPeriodCompare(t *testing.T) { lhs1 := testPeriod(1) rhs1 := testPeriod(1) - periodCompareTest(t, &lhs1, &rhs1, comparison.SpaceshipEqual) + periodCompareTest(t, lhs1, rhs1, comparison.SpaceshipEqual) lhs2 := testPeriod(1) rhs2 := testPeriod(2) - periodCompareTest(t, &lhs2, &rhs2, comparison.SpaceshipLeftGreater) + periodCompareTest(t, lhs2, rhs2, comparison.SpaceshipLeftGreater) lhs3 := testPeriod(2) rhs3 := testPeriod(1) - periodCompareTest(t, &lhs3, &rhs3, comparison.SpaceshipRightGreater) + periodCompareTest(t, lhs3, rhs3, comparison.SpaceshipRightGreater) } diff --git a/oscillator/impulsetracker.go b/oscillator/impulsetracker.go index 7c942b6..d9609ff 100644 --- a/oscillator/impulsetracker.go +++ b/oscillator/impulsetracker.go @@ -72,6 +72,14 @@ func NewImpulseTrackerOscillator(mul uint8) oscillator.Oscillator { } } +func (o impulseOscillator) Clone() oscillator.Oscillator { + return &impulseOscillator{ + Table: o.Table, + Pos: 0, + Mul: o.Mul, + } +} + // GetWave returns the wave amplitude for the current position func (o *impulseOscillator) GetWave(depth float32) float32 { var vib float32 @@ -106,14 +114,22 @@ func (o *impulseOscillator) SetWaveform(table oscillator.WaveTableSelect) { o.Table = table } +func (o *impulseOscillator) GetWaveform() oscillator.WaveTableSelect { + return o.Table +} + // Reset resets the position of the oscillator -func (o *impulseOscillator) Reset(hard ...bool) { - hardReset := false - if len(hard) > 0 { - hardReset = hard[0] - } +func (o *impulseOscillator) Reset() { + o.reset(false) +} + +// HardReset resets the position of the oscillator regardless of how the wavetable operates +func (o *impulseOscillator) HardReset() { + o.reset(true) +} - doReset := hardReset +func (o *impulseOscillator) reset(hard bool) { + doReset := hard switch o.Table { case WaveTableSelectSineRetrigger, WaveTableSelectSawtoothRetrigger, WaveTableSelectSquareRetrigger, WaveTableSelectRandomRetrigger: doReset = true diff --git a/oscillator/protracker.go b/oscillator/protracker.go index fb7bfc5..526c298 100644 --- a/oscillator/protracker.go +++ b/oscillator/protracker.go @@ -34,8 +34,15 @@ func NewProtrackerOscillator() oscillator.Oscillator { return &protrackerOscillator{} } +func (o protrackerOscillator) Clone() oscillator.Oscillator { + return &protrackerOscillator{ + Table: o.Table, + Pos: 0, + } +} + // GetWave returns the wave amplitude for the current position -func (o *protrackerOscillator) GetWave(depth float32) float32 { +func (o protrackerOscillator) GetWave(depth float32) float32 { var vib float32 switch o.Table { case WaveTableSelectSineRetrigger, WaveTableSelectSineContinue: @@ -74,14 +81,22 @@ func (o *protrackerOscillator) SetWaveform(table oscillator.WaveTableSelect) { o.Table = table } +func (o protrackerOscillator) GetWaveform() oscillator.WaveTableSelect { + return o.Table +} + // Reset resets the position of the oscillator -func (o *protrackerOscillator) Reset(hard ...bool) { - hardReset := false - if len(hard) > 0 { - hardReset = hard[0] - } +func (o *protrackerOscillator) Reset() { + o.reset(false) +} + +// HardReset resets the position of the oscillator regardless of how the wavetable operates +func (o *protrackerOscillator) HardReset() { + o.reset(true) +} - doReset := hardReset +func (o *protrackerOscillator) reset(hard bool) { + doReset := hard switch o.Table { case WaveTableSelectSineRetrigger, WaveTableSelectSawtoothRetrigger, WaveTableSelectSquareRetrigger, WaveTableSelectRandomRetrigger: doReset = true diff --git a/pan/util.go b/pan/util.go deleted file mode 100644 index 80389a7..0000000 --- a/pan/util.go +++ /dev/null @@ -1,55 +0,0 @@ -package pan - -import ( - "math" - - "github.com/gotracker/gomixing/panning" -) - -const ( - pi2 = math.Pi / 2 - pi4 = math.Pi / 4 - pi8 = math.Pi / 8 - - twopi = math.Pi * 2 -) - -// CalculateCombinedPanning calculates a panning value where `p1` modifies -// panning value `p0` such that `p0` is primary component and `p1` is secondary -// TODO: JBC - move this calculation function into gomixing lib -func CalculateCombinedPanning(p0, p1 panning.Position) panning.Position { - p0a := float64(p0.Angle) - pi4 - p1a := float64(p1.Angle) - pi4 - - fa := p0a + (p1a-pi8)*(pi4-math.Abs(p0a-pi4))/pi8 - if fa > pi2 { - fa = pi2 - } else if fa < -pi2 { - fa = -pi2 - } - - fd := math.Sqrt(float64(p0.Distance * p1.Distance)) - - return panning.Position{ - Angle: float32(fa + pi4), - Distance: float32(fd), - } -} - -// GetPanningDifference calculates the difference of `p0` - `p1` -func GetPanningDifference(p0, p1 panning.Position) panning.Position { - ia := float64(panning.CenterAhead.Angle) - p0a := float64(p0.Angle) - p1a := float64(p1.Angle) - - fa := math.Mod(ia+p1a-p0a, twopi) - for fa < 0 { - fa += twopi - } - fd := panning.CenterAhead.Distance + p1.Distance - p0.Distance - - return panning.Position{ - Angle: float32(fa), - Distance: float32(fd), - } -} diff --git a/pattern/pattern.go b/pattern/pattern.go deleted file mode 100644 index 13d9b8f..0000000 --- a/pattern/pattern.go +++ /dev/null @@ -1,45 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/song" -) - -// RowData is the data for each row -type RowData[TChannelData any] struct { - Channels []TChannelData -} - -// GetChannels returns an interface to all the channels in the row -func (r RowData[TChannelData]) GetChannels() []TChannelData { - return r.Channels -} - -// Rows is a list of row data (channels and whatnot) -type Rows[TChannelData any] []RowData[TChannelData] - -// GetRow returns the row at the specified row index from the list of rows -func (r Rows[TChannelData]) GetRow(idx index.Row) song.Row[TChannelData] { - return &r[int(idx)] -} - -// NumRows returns the number of rows in this list of rows -func (r Rows[TChannelData]) NumRows() int { - return len(r) -} - -// Pattern is the data for each pattern -type Pattern[TChannelData any] struct { - Rows Rows[TChannelData] - Orig any -} - -// GetRow returns the interface to the row at index `row` -func (p Pattern[TChannelData]) GetRow(row index.Row) song.Row[TChannelData] { - return &p.Rows[row] -} - -// GetRows returns the interfaces to all the rows in the pattern -func (p Pattern[TChannelData]) GetRows() song.Rows[TChannelData] { - return p.Rows -} diff --git a/pattern/transaction.go b/pattern/transaction.go deleted file mode 100644 index b2abbeb..0000000 --- a/pattern/transaction.go +++ /dev/null @@ -1,112 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/index" - "github.com/heucuva/optional" -) - -type WhoJumpedFirst uint8 - -const ( - WhoJumpedFirstNone = WhoJumpedFirst(iota) - WhoJumpedFirstOrder - WhoJumpedFirstRow -) - -// RowUpdateTransaction is a transactional operation for row/order updates -type RowUpdateTransaction struct { - committed bool - CommitTransaction func(*RowUpdateTransaction) error - - orderIdx optional.Value[index.Order] - rowIdx optional.Value[index.Row] - patternDelay optional.Value[int] - FinePatternDelay optional.Value[int] - Tempo optional.Value[int] - Ticks optional.Value[int] - TempoDelta optional.Value[int] - - RowIdxAllowBacktrack bool - WhoJumpedFirst WhoJumpedFirst - AdvanceRow bool - BreakOrder bool -} - -// Cancel will mark a transaction as void/spent, i.e.: cancelled -func (txn *RowUpdateTransaction) Cancel() { - txn.committed = true -} - -// Commit will update the order and row indexes at once, idempotently. -func (txn *RowUpdateTransaction) Commit() error { - if txn.committed { - return nil - } - if txn.CommitTransaction == nil { - panic("cannot commit transaction using unset commit function") - } - txn.committed = true - return txn.CommitTransaction(txn) -} - -// SetNextOrder will set the next order index -func (txn *RowUpdateTransaction) SetNextOrder(ordIdx index.Order) { - if !txn.orderIdx.IsSet() { - txn.orderIdx.Set(ordIdx) - if txn.WhoJumpedFirst == WhoJumpedFirstNone { - txn.WhoJumpedFirst = WhoJumpedFirstOrder - } - } -} - -// GetOrderIdx gets the order index and a flag for if it is valid/set -func (txn *RowUpdateTransaction) GetOrderIdx() (index.Order, bool) { - return txn.orderIdx.Get() -} - -// SetNextRow will set the next row index -func (txn *RowUpdateTransaction) SetNextRow(rowIdx index.Row) { - if !txn.rowIdx.IsSet() { - txn.rowIdx.Set(rowIdx) - if txn.WhoJumpedFirst == WhoJumpedFirstNone { - txn.WhoJumpedFirst = WhoJumpedFirstRow - } - } -} - -// SetNextRowWithBacktrack will set the next row index and backtracing allowance -func (txn *RowUpdateTransaction) SetNextRowWithBacktrack(rowIdx index.Row, allowBacktrack bool) { - if !txn.rowIdx.IsSet() { - txn.rowIdx.Set(rowIdx) - if txn.WhoJumpedFirst == WhoJumpedFirstNone { - txn.WhoJumpedFirst = WhoJumpedFirstRow - } - txn.RowIdxAllowBacktrack = allowBacktrack - } -} - -// GetOrderIdx gets the row index and a flag for if it is valid/set -func (txn *RowUpdateTransaction) GetRowIdx() (index.Row, bool) { - return txn.rowIdx.Get() -} - -// SetPatternDelay sets the row pattern delay -func (txn *RowUpdateTransaction) SetPatternDelay(patternDelay int) { - if !txn.patternDelay.IsSet() { - txn.patternDelay.Set(patternDelay) - } -} - -// GetPatternDelay gets the row pattern delay and a flag for if it is valid/set -func (txn *RowUpdateTransaction) GetPatternDelay() (int, bool) { - return txn.patternDelay.Get() -} - -// AccTempoDelta accumulates the amount of tempo delta -func (txn *RowUpdateTransaction) AccTempoDelta(delta int) { - tempoDelta := delta - if d, ok := txn.TempoDelta.Get(); ok { - tempoDelta += d - } - txn.TempoDelta.Set(tempoDelta) -} diff --git a/period/amiga.go b/period/amiga.go new file mode 100644 index 0000000..b3e6714 --- /dev/null +++ b/period/amiga.go @@ -0,0 +1,93 @@ +package period + +import ( + "fmt" + + "github.com/gotracker/playback/util" + "github.com/heucuva/comparison" +) + +type Amiga uint16 + +// Add adds the current period to a delta value then returns the resulting period +func (p Amiga) Add(d Delta, minPeriod, maxPeriod Amiga, canSlideTo0 bool) Amiga { + if d == 0 { + return p + } + a := int(p) + if a == 0 { + // 0 means "not playing", so keep it that way + return p + } + + a -= int(d) + if a == 0 && canSlideTo0 { + return 0 + } + // can't use Clamp() here because we need to clamp negatives + c := min(Amiga(max(a, int(minPeriod))), maxPeriod) + if c < 64 { + _ = c + } + return c +} + +func (p Amiga) Clamp(minPeriod, maxPeriod Amiga) Amiga { + if p == 0 { + return 0 + } + return min(max(p, minPeriod), maxPeriod) +} + +func (p Amiga) PortaDown(amount Delta, minPeriod, maxPeriod Amiga, canSlideTo0 bool) Amiga { + return p.Add(-amount, minPeriod, maxPeriod, canSlideTo0) +} + +func (p Amiga) PortaUp(amount Delta, minPeriod, maxPeriod Amiga, canSlideTo0 bool) Amiga { + return p.Add(amount, minPeriod, maxPeriod, canSlideTo0) +} + +func (p Amiga) PortaTo(amount Delta, target, minPeriod, maxPeriod Amiga) Amiga { + switch p.Compare(target) { + case comparison.SpaceshipLeftGreater: + // porta down to target + p = p.PortaDown(amount, minPeriod, target, false) + case comparison.SpaceshipRightGreater: + // porta up to target + p = p.PortaUp(amount, target, maxPeriod, false) + } + return p +} + +func (p Amiga) IsInvalid() bool { + return p == 0 +} + +// Compare returns: +// +// -1 if the current period is higher frequency than the `rhs` period +// 0 if the current period is equal in frequency to the `rhs` period +// 1 if the current period is lower frequency than the `rhs` period +func (p Amiga) Compare(rhs Amiga) comparison.Spaceship { + switch { + case p < rhs: + return comparison.SpaceshipLeftGreater + case p > rhs: + return comparison.SpaceshipRightGreater + default: + return comparison.SpaceshipEqual + } +} + +// Lerp linear-interpolates the current period with the `rhs` period +func (p Amiga) Lerp(t float64, rhs Amiga) Amiga { + p = util.Lerp(t, p, rhs) + return p +} + +func (p Amiga) String() string { + if p == 0 { + return "Amiga{ nil }" + } + return fmt.Sprintf("Amiga{ Period:%d }", p) +} diff --git a/period/amigaconverter.go b/period/amigaconverter.go new file mode 100644 index 0000000..2d5c312 --- /dev/null +++ b/period/amigaconverter.go @@ -0,0 +1,84 @@ +package period + +import ( + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/system" +) + +// AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note +// definition. Useful in calculating resampling. +type AmigaConverter struct { + System system.ClockableSystem + MinPeriod Amiga + MaxPeriod Amiga + SlideTo0Allowed bool +} + +var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) + +func (c AmigaConverter) GetSystem() system.System { + return c.System +} + +// GetFrequency returns the frequency defined by the period +func (c AmigaConverter) GetFrequency(p Amiga) frequency.Frequency { + if p.IsInvalid() { + return 0 + } + return c.System.GetBaseClock() / frequency.Frequency(p<> octave + + return p.Clamp(c.MinPeriod, c.MaxPeriod) + case note.SpecialTypeInvalid: + fallthrough + default: + panic("unsupported note type") + } +} + +func (c AmigaConverter) PortaToNote(p Amiga, delta Delta, target Amiga) (Amiga, error) { + return p.PortaTo(delta, target, c.MinPeriod, c.MaxPeriod), nil +} + +func (c AmigaConverter) PortaDown(p Amiga, delta Delta) (Amiga, error) { + return p.PortaDown(delta, c.MinPeriod, c.MaxPeriod, c.SlideTo0Allowed), nil +} + +func (c AmigaConverter) PortaUp(p Amiga, delta Delta) (Amiga, error) { + return p.PortaUp(delta, c.MinPeriod, c.MaxPeriod, c.SlideTo0Allowed), nil +} + +func (c AmigaConverter) AddDelta(p Amiga, delta Delta) (Amiga, error) { + return p.Add(delta, c.MinPeriod, c.MaxPeriod, c.SlideTo0Allowed), nil +} + +func (c AmigaConverter) clamp(p Amiga) Amiga { + return min(max(p, c.MinPeriod), c.MaxPeriod) +} diff --git a/period/delta.go b/period/delta.go index d0e91ac..8278668 100755 --- a/period/delta.go +++ b/period/delta.go @@ -1,4 +1,4 @@ package period // Delta is an amount of delta understood by the Period -type Delta interface{} +type Delta int16 diff --git a/period/frequency.go b/period/frequency.go deleted file mode 100755 index 25537b1..0000000 --- a/period/frequency.go +++ /dev/null @@ -1,4 +0,0 @@ -package period - -// Frequency is a frequency value, in Hertz (Hz) -type Frequency float64 diff --git a/period/linear.go b/period/linear.go new file mode 100644 index 0000000..036a9ee --- /dev/null +++ b/period/linear.go @@ -0,0 +1,89 @@ +package period + +import ( + "fmt" + + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/util" + "github.com/heucuva/comparison" +) + +// Linear is a linear period, based on semitone and finetune values +type Linear struct { + Finetune note.Finetune +} + +// Add adds the current period to a delta value then returns the resulting period +func (p Linear) Add(d Delta) Linear { + a := int(p.Finetune) + if a == 0 { + // 0 means "not playing", so keep it that way + return p + } + + a += int(d) + if a < 1 { + a = 1 + } + p.Finetune = note.Finetune(a) + return p +} + +func (p Linear) PortaDown(amount int) Linear { + return p.Add(Delta(-amount)) +} + +func (p Linear) PortaUp(amount int) Linear { + return p.Add(Delta(amount)) +} + +func (p Linear) PortaTo(amount int, target Linear) Linear { + switch p.Compare(target) { + case comparison.SpaceshipLeftGreater: + // porta down to target + p = p.PortaDown(amount) + if p.Compare(target) == comparison.SpaceshipRightGreater { + return target + } + case comparison.SpaceshipRightGreater: + // porta up to target + p = p.PortaUp(amount) + if p.Compare(target) == comparison.SpaceshipLeftGreater { + return target + } + } + return p +} + +func (p Linear) IsInvalid() bool { + return p.Finetune == 0 +} + +// Compare returns: +// +// -1 if the current period is higher frequency than the `rhs` period +// 0 if the current period is equal in frequency to the `rhs` period +// 1 if the current period is lower frequency than the `rhs` period +func (p Linear) Compare(rhs Linear) comparison.Spaceship { + switch { + case p.Finetune < rhs.Finetune: + return comparison.SpaceshipRightGreater + case p.Finetune > rhs.Finetune: + return comparison.SpaceshipLeftGreater + default: + return comparison.SpaceshipEqual + } +} + +// Lerp linear-interpolates the current period with the `rhs` period +func (p Linear) Lerp(t float64, rhs Linear) Period { + p.Finetune = util.Lerp(t, p.Finetune, rhs.Finetune) + return p +} + +func (p Linear) String() string { + if p.Finetune == 0 { + return "Linear{ nil }" + } + return fmt.Sprintf("Linear{ Finetune:%v }", p.Finetune) +} diff --git a/period/linearconverter.go b/period/linearconverter.go new file mode 100644 index 0000000..3ad77c6 --- /dev/null +++ b/period/linearconverter.go @@ -0,0 +1,68 @@ +package period + +import ( + "math" + + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/system" +) + +// LinearConverter defines a sampler period that follows the Linear-style approach of note +// definition. Useful in calculating resampling. +type LinearConverter struct { + System system.ClockableSystem +} + +var _ PeriodConverter[Linear] = (*LinearConverter)(nil) + +func (c LinearConverter) GetSystem() system.System { + return c.System +} + +// GetSamplerAdd returns the number of samples to advance an instrument by given the period +func (c LinearConverter) GetSamplerAdd(p Linear, instrumentRate, outputRate frequency.Frequency) float64 { + return float64(c.GetFrequency(p) * instrumentRate / outputRate) +} + +// GetFrequency returns the frequency defined by the period +func (c LinearConverter) GetFrequency(p Linear) frequency.Frequency { + if p.Finetune == 0 { + return 0 + } + pft := float64(p.Finetune-c.System.GetBaseFinetunes()) / float64(c.System.GetFinetunesPerOctave()) + f := frequency.Frequency(math.Pow(2.0, pft)) + return f +} + +func (c LinearConverter) GetPeriod(n note.Note) Linear { + switch n.Type() { + case note.SpecialTypeEmpty, note.SpecialTypeRelease, note.SpecialTypeStop, note.SpecialTypeStopOrRelease: + return Linear{} + case note.SpecialTypeNormal: + st := note.Semitone(n.(note.Normal)) + return Linear{ + Finetune: note.Finetune(st) * c.System.GetFinetunesPerSemitone(), + } + case note.SpecialTypeInvalid: + fallthrough + default: + panic("unsupported note type") + } +} + +func (c LinearConverter) PortaToNote(p Linear, delta Delta, target Linear) (Linear, error) { + return p.PortaTo(int(delta), target), nil +} + +func (c LinearConverter) PortaDown(p Linear, delta Delta) (Linear, error) { + return p.PortaDown(int(delta)), nil +} + +func (c LinearConverter) PortaUp(p Linear, delta Delta) (Linear, error) { + return p.PortaUp(int(delta)), nil +} + +func (c LinearConverter) AddDelta(p Linear, delta Delta) (Linear, error) { + return p.Add(delta), nil +} diff --git a/period/period.go b/period/period.go index 58239f4..cf24dd7 100644 --- a/period/period.go +++ b/period/period.go @@ -1,45 +1,6 @@ package period -import ( - "github.com/heucuva/comparison" -) - // Period is an interface that defines a sampler period type Period interface { - AddDelta(Delta) Period - Compare(Period) comparison.Spaceship // <=> - Lerp(float64, Period) Period - GetSamplerAdd(float64) float64 - GetFrequency() Frequency -} - -// PeriodDelta is an amount of delta specific to the period type it modifies -// it's intended to be non-specific unit type, so it's up to the implementer -// to keep track of the expected unit type. -type PeriodDelta float64 - -// ToPeriodDelta works as a conversion system for different types of 'delta' values to a single common one -func ToPeriodDelta(delta Delta) PeriodDelta { - switch d := delta.(type) { - case PeriodDelta: - return d - case float32: - return PeriodDelta(d) - default: - panic("unknown type conversion for Delta") - } -} - -// ComparePeriods compares two periods, taking nil into account -func ComparePeriods(lhs Period, rhs Period) comparison.Spaceship { - if lhs == nil { - if rhs == nil { - return comparison.SpaceshipEqual - } - return comparison.SpaceshipRightGreater - } else if rhs == nil { - return comparison.SpaceshipLeftGreater - } - - return lhs.Compare(rhs) + IsInvalid() bool } diff --git a/period/periodconverter.go b/period/periodconverter.go new file mode 100644 index 0000000..7d825b3 --- /dev/null +++ b/period/periodconverter.go @@ -0,0 +1,20 @@ +package period + +import ( + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/system" +) + +type PeriodConverter[TPeriod Period] interface { + GetSystem() system.System + + GetPeriod(note.Note) TPeriod + PortaToNote(TPeriod, Delta, TPeriod) (TPeriod, error) + PortaDown(TPeriod, Delta) (TPeriod, error) + PortaUp(TPeriod, Delta) (TPeriod, error) + AddDelta(TPeriod, Delta) (TPeriod, error) + + GetSamplerAdd(TPeriod, frequency.Frequency, frequency.Frequency) float64 + GetFrequency(TPeriod) frequency.Frequency +} diff --git a/period/protracker.go b/period/protracker.go deleted file mode 100644 index eec8216..0000000 --- a/period/protracker.go +++ /dev/null @@ -1,18 +0,0 @@ -package period - -import ( - "github.com/gotracker/playback/util" -) - -type AmigaPeriod float64 - -func (p AmigaPeriod) Lerp(t float64, rhs AmigaPeriod) AmigaPeriod { - return AmigaPeriod(util.LerpFloat64(t, float64(p), float64(rhs))) -} - -func (p AmigaPeriod) GetFrequency(baseClockRate Frequency) Frequency { - if p == 0 { - return 0 - } - return baseClockRate / Frequency(p) -} diff --git a/playback.go b/playback.go index 05bd9b2..3709baf 100644 --- a/playback.go +++ b/playback.go @@ -1,45 +1,20 @@ package playback import ( - "time" - - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" - "github.com/gotracker/playback/voice/render" + "github.com/gotracker/playback/player/sampler" ) // Playback is an interface for rendering a song to output data type Playback interface { - SetupSampler(samplesPerSecond int, channels int) error - GetSampleRate() period.Frequency - GetOPL2Chip() render.OPL2Chip - GetGlobalVolume() volume.Volume - SetGlobalVolume(volume.Volume) - - Update(time.Duration, chan<- *output.PremixData) error - Generate(time.Duration) (*output.PremixData, error) + Configure([]feature.Feature) error - GetSongData() song.Data + // runs a single tick + // if the onGenerate function was provided to the SetupSampler call, + // then the generated output will be provided through it + Tick(s *sampler.Sampler) error - GetNumChannels() int GetNumOrders() int - SetNextOrder(index.Order) error - SetNextRow(index.Row) error - SetNextRowWithBacktrack(index.Row, bool) error - GetCurrentRow() index.Row - Configure([]feature.Feature) error - GetName() string CanOrderLoop() bool - BreakOrder() error - SetOnEffect(func(Effect)) - GetOnEffect() func(Effect) - IgnoreUnknownEffect() bool - - StartPatternTransaction() *pattern.RowUpdateTransaction + GetName() string } diff --git a/player/feature/enabletracing.go b/player/feature/enabletracing.go deleted file mode 100644 index 516ea05..0000000 --- a/player/feature/enabletracing.go +++ /dev/null @@ -1,5 +0,0 @@ -package feature - -type EnableTracing struct { - Filename string -} diff --git a/player/feature/startorderandrow.go b/player/feature/startorderandrow.go new file mode 100644 index 0000000..5f99ff6 --- /dev/null +++ b/player/feature/startorderandrow.go @@ -0,0 +1,8 @@ +package feature + +import "github.com/heucuva/optional" + +type StartOrderAndRow struct { + Order optional.Value[int] + Row optional.Value[int] +} diff --git a/player/machine/channel.go b/player/machine/channel.go new file mode 100644 index 0000000..87c098e --- /dev/null +++ b/player/machine/channel.go @@ -0,0 +1,64 @@ +package machine + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/memory" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/player/machine/instruction" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/oscillator" + "github.com/heucuva/optional" +) + +type channel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + enabled bool + memory song.ChannelMemory + osc [NumOscillators]oscillator.Oscillator + patternLoop struct { + Start index.Row + End index.Row + Total int + Count int + } + + prev struct { + Period TPeriod + Inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] + Semitone memory.Value[note.Semitone] + } + target struct { + PortaPeriod TPeriod + Inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] + Pos optional.Value[sampling.Pos] + ActionTick optional.Value[ActionTick] + TriggerNNA bool + } + newNote NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning] + + surround bool + filter filter.Filter + filterEnabled bool + nna note.Action + + cv voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + pastNotes []index.Channel + + instructions []instruction.Instruction +} + +type channelInfo[TPeriod Period, TMixingVolume, TVolume Volume, TPanning Panning] struct { + Period TPeriod + Inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] +} + +type channelTargets[TPeriod Period, TMixingVolume, TVolume Volume, TPanning Panning] struct { + channelInfo[TPeriod, TMixingVolume, TVolume, TPanning] + + Pos optional.Value[sampling.Pos] + Action note.Action + ActionTick int +} diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go new file mode 100644 index 0000000..5cf2159 --- /dev/null +++ b/player/machine/channel_noteaction.go @@ -0,0 +1,127 @@ +package machine + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/voice" +) + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + na, set := c.target.ActionTick.Get() + if !set { + // assume continue + return nil + } + + if na.Tick != m.ticker.current.Tick { + // not time yet + return nil + } + + // consume the action + traceChannelOptionalValueResetWithComment(m, ch, "target.ActionTick", c.target.ActionTick, "doNoteAction") + c.target.ActionTick.Reset() + + // perform new note action + if c.target.TriggerNNA && m.canPastNote() { + c.target.TriggerNNA = false + var pn voice.Voice + switch c.nna { + case note.ActionCut: + c.cv.Stop() + case note.ActionRelease: + pn = c.cv.Clone(true) + pn.Release() + case note.ActionFadeout: + pn = c.cv.Clone(true) + pn.Release() + pn.Fadeout() + case note.ActionRetrigger: + pn = c.cv.Clone(true) + pn.Release() + pn.Attack() + + case note.ActionContinue: + fallthrough + default: + // nothing + } + + if pn != nil { + c.addPastNote(m, pn.(voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning])) + } + } + + switch na.Action { + case note.ActionCut: + c.cv.Stop() + return nil + + case note.ActionRelease: + c.cv.Release() + + case note.ActionFadeout: + c.cv.Fadeout() + + case note.ActionRetrigger: + c.cv.Release() + + if err := c.doSetupInstrument(ch, m); err != nil { + return err + } + + c.memory.Retrigger() + + for _, o := range c.osc { + o.Reset() + } + + c.cv.Reset() + + c.cv.Attack() + + case note.ActionContinue: + fallthrough + default: + // nothing + } + + if pitchPanMod, ok := c.cv.(voice.PitchPanModulator[TPanning]); ok { + pitchPanMod.SetPitchPanNote(c.prev.Semitone.Coalesce(0)) + } + + if pos, set := c.target.Pos.Get(); set { + if samp, ok := c.cv.(voice.Sampler); ok { + prevPos, _ := samp.GetPos() + traceChannelValueChangeWithComment(m, ch, "pos", prevPos, pos, "DoNoteAction") + samp.SetPos(pos) + } + traceChannelOptionalValueResetWithComment(m, ch, "target.Pos", c.target.Pos, "DoNoteAction") + c.target.Pos.Reset() + } + + return nil +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupInstrument(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + inst := c.target.Inst + prevInst := c.prev.Inst + if inst != nil { + if prevInst != inst { + rc := &m.actualOutputs[ch] + info := inst.GetPluginFilterInfo() + var err error + rc.PluginFilter, err = m.ms.GetFilterFactory(info.Name, inst.SampleRate, info.Params) + if err != nil { + return err + } + + if err := c.cv.Setup(inst); err != nil { + return err + } + } + } else { + c.cv.Stop() + } + return nil +} diff --git a/player/machine/channel_notedecode.go b/player/machine/channel_notedecode.go new file mode 100644 index 0000000..6b79060 --- /dev/null +++ b/player/machine/channel_notedecode.go @@ -0,0 +1,135 @@ +package machine + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/song" +) + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) decodeNote(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], d song.ChannelDataIntf) error { + var changeNote NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning] + + var n note.Note + if d.HasNote() { + if dn := d.GetNote(); dn.Type() != note.SpecialTypeEmpty { + n = dn + } + } + + var ( + st note.Semitone + na note.Action = note.ActionContinue + needNoteInstIdent bool + wantInstrumentDefaults bool + wantTriggerNNA bool + ) + if n != nil { + switch n.Type() { + case note.SpecialTypeNormal: + st = note.Semitone(n.(note.Normal)) + na = note.ActionRetrigger + + case note.SpecialTypeStop: + na = note.ActionCut + + case note.SpecialTypeRelease: + na = note.ActionRelease + + case note.SpecialTypeStopOrRelease: + // assume cut + na = note.ActionCut + needNoteInstIdent = true + } + } + + var inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] + if d.HasInstrument() { + // retrigger (new?) instrument with period specified by `st` (0 = previous semitone) + i := d.GetInstrument() + + var ii instrument.InstrumentIntf + ii, st = m.songData.GetInstrument(i, c.prev.Semitone.Coalesce(st)) + inst, _ = ii.(*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) + wantInstrumentDefaults = inst != nil + changeNote.Period.Set(c.prev.Period) + changeNote.Inst.Set(inst) + wantTriggerNNA = true + } else if st != 0 && c.target.Inst != nil { + // retrigger same instrument + i, _ := c.target.Inst.GetID().GetIndexAndSample() + var ii instrument.InstrumentIntf + ii, st = m.songData.GetInstrument(i, st) + inst, _ = ii.(*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) + wantInstrumentDefaults = true + wantTriggerNNA = true + } + + if inst != nil && n != nil { + if needNoteInstIdent { + if inst.IsReleaseNote(n) { + na = note.ActionRelease + } else if inst.IsStopNote(n) { + na = note.ActionCut + } + } + } + + if n != nil { + switch n.(type) { + case note.Normal: + // perform remap + n = note.Normal(st) + } + if p := m.ConvertToPeriod(n); !p.IsInvalid() { + changeNote.Period.Set(p) + } + } + + if na != note.ActionContinue { + changeNote.ActionTick.Set(ActionTick{Action: na, Tick: 0}) + if na == note.ActionRetrigger { + changeNote.Pos.Set(sampling.Pos{}) + } + } + + if wantInstrumentDefaults { + if err := c.decodeInstrument(m, &changeNote, inst); err != nil { + return err + } + } + + if d.HasVolume() { + if dd, ok := d.(song.ChannelData[TVolume]); ok { + if v := dd.GetVolume(); !v.IsInvalid() { + if !v.IsUseInstrumentVol() { + changeNote.Vol.Set(v) + } + } + } + } + + c.newNote = changeNote + c.target.TriggerNNA = wantTriggerNNA + return nil +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) decodeInstrument(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], changeNote *NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) error { + if inst == nil { + return nil + } + + changeNote.Vol.Set(inst.GetDefaultVolume()) + changeNote.NewNoteAction.Set(inst.GetNewNoteAction()) + + switch d := inst.GetData().(type) { + case *instrument.PCM[TMixingVolume, TVolume, TPanning]: + if pan, set := d.Panning.Get(); set { + changeNote.Pan.Set(pan) + } + + case *instrument.OPL2: + // TODO - is there anything to actually do here? + } + return nil +} diff --git a/player/machine/channel_pastnote.go b/player/machine/channel_pastnote.go new file mode 100644 index 0000000..9caeb4b --- /dev/null +++ b/player/machine/channel_pastnote.go @@ -0,0 +1,116 @@ +package machine + +import ( + "slices" + "sort" + + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/voice" +) + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], pn voice.Voice) { + // try to find an empty spot in output channels + type pnVolChan struct { + vol volume.Volume + ch index.Channel + } + pnvc := make([]pnVolChan, len(m.virtualOutputs)) + for i := range m.virtualOutputs { + ch := index.Channel(i) + rc := &m.virtualOutputs[ch] + + v := rc.GetVoice() + if v == nil { + rc.StartVoice(pn, func() { + c.removePastNote(m, ch) + }) + c.pastNotes = append(c.pastNotes, ch) + return + } + + vc := pnVolChan{ + vol: volume.Volume(1), + ch: ch, + } + if ampMod, ok := v.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + vc.vol = ampMod.GetFinalVolume() + } + + pnvc = append(pnvc, vc) + } + + // we failed to find a spot? + if len(pnvc) == 0 { + // no room at all? strange + pn.Stop() + return + } + + // look for lowest volume + sort.SliceStable(pnvc, func(i, j int) bool { + return pnvc[i].vol < pnvc[j].vol + }) + + lowest := pnvc[0].ch + rc := &m.virtualOutputs[lowest] + + rc.StartVoice(pn, func() { + c.removePastNote(m, lowest) + }) + c.pastNotes = append(c.pastNotes, lowest) +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) removePastNote(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel) { + c.pastNotes = slices.DeleteFunc(c.pastNotes, func(i index.Channel) bool { + return i == ch + }) +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPastNoteAction(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], na note.Action) { + for _, ch := range c.pastNotes { + rc := &m.virtualOutputs[ch] + v := rc.GetVoice() + if v == nil { + continue + } + + switch na { + case note.ActionCut: + rc.StopVoice() + case note.ActionRelease: + v.Release() + case note.ActionFadeout: + v.Release() + v.Fadeout() + case note.ActionRetrigger: + v.Release() + v.Attack() + + case note.ActionContinue: + fallthrough + default: + // nothing + } + } +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) updatePastNotes(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { + var updated []index.Channel + for _, ch := range c.pastNotes { + rc := &m.virtualOutputs[ch] + v := rc.GetVoice() + if v == nil { + continue + } + + if v.IsDone() { + rc.StopVoice() + continue + } + + updated = append(updated, ch) + } + c.pastNotes = updated +} diff --git a/player/machine/channel_patternloop.go b/player/machine/channel_patternloop.go new file mode 100644 index 0000000..fd70b77 --- /dev/null +++ b/player/machine/channel_patternloop.go @@ -0,0 +1,36 @@ +package machine + +import "github.com/gotracker/playback/index" + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) resetPatternLoop() { + c.patternLoop.Total = 0 + c.patternLoop.Count = 0 +} + +// ContinueLoop returns the next expected row if a loop occurs +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPatternLoop(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if c.patternLoop.Total == 0 { + return nil + } + + if m.ticker.current.Row != c.patternLoop.End { + return nil + } + + newCount := c.patternLoop.Count + 1 + doLoop := newCount <= c.patternLoop.Total + if !doLoop { + newCount = 0 + } + + traceChannelValueChangeWithComment(m, ch, "patternLoopCount", c.patternLoop.Count, newCount, "doPatternLoop") + c.patternLoop.Count = newCount + + if doLoop { + return m.SetRow(c.patternLoop.Start, false) + } + + traceChannelValueChangeWithComment(m, ch, "patternLoopTotal", c.patternLoop.Total, 0, "doPatternLoop") + c.patternLoop.Total = 0 + return nil +} diff --git a/player/machine/channel_tick.go b/player/machine/channel_tick.go new file mode 100644 index 0000000..0aaaa2b --- /dev/null +++ b/player/machine/channel_tick.go @@ -0,0 +1,175 @@ +package machine + +import ( + "fmt" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" +) + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) OrderStart(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if m.ticker.current.Order == 0 { + c.memory.StartOrder0() + } + c.resetPatternLoop() + return nil +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowStart(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelOptionalValueResetWithComment(m, ch, "target.ActionTick", c.target.ActionTick, "channel.RowStart") + c.target.ActionTick.Reset() + + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + pd := freqMod.GetPeriodDelta() + var reset period.Delta + traceChannelValueChangeWithComment(m, ch, "pd", pd, reset, "channel.RowStart") + freqMod.SetPeriodDelta(reset) + } + + for _, i := range c.instructions { + if err := m.DoInstructionRowStart(ch, i); err != nil { + return err + } + } + + info := c.newNote + c.newNote.Reset() + + if tp, set := info.Period.Get(); set { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + orig := freqMod.GetPeriod() + traceChannelValueChangeWithComment(m, ch, "target.Period", orig, tp, "channel.RowStart") + freqMod.SetPeriod(tp) + } + } + + if inst, set := info.Inst.Get(); set { + var prev, next instrument.ID + if c.target.Inst != nil { + prev = c.target.Inst.GetID() + } + if inst != nil { + next = inst.GetID() + } + + traceChannelValueChangeWithComment(m, ch, "target.Inst", prev, next, "channel.RowStart") + c.target.Inst = inst + } + + if tpos, set := info.Pos.Get(); set { + traceChannelOptionalValueChangeWithComment(m, ch, "target.Pos", c.target.Pos, tpos, "channel.RowStart") + c.target.Pos.Set(tpos) + } + + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + if mv, set := info.MixVol.Get(); set { + if mv.IsInvalid() { + return fmt.Errorf("channel[%d] mixing volume out of range: %v", ch, mv) + } + + orig := ampMod.GetMixingVolume() + traceChannelValueChangeWithComment(m, ch, "mv", orig, mv, "channel.RowStart") + ampMod.SetMixingVolume(mv) + } + + if vol, set := info.Vol.Get(); set { + if vol.IsInvalid() { + return fmt.Errorf("channel[%d] volume out of range: %v", ch, vol) + } + + orig := ampMod.GetVolume() + traceChannelValueChangeWithComment(m, ch, "vol", orig, vol, "channel.RowStart") + ampMod.SetVolume(vol) + } + } + + if pan, set := info.Pan.Get(); set { + if pan.IsInvalid() { + return fmt.Errorf("channel[%d] channel pan out of range: %v", ch, pan) + } + + if panMod, ok := c.cv.(voice.PanModulator[TPanning]); ok { + orig := panMod.GetPan() + traceChannelValueChangeWithComment(m, ch, "pan", orig, pan, "channel.RowStart") + panMod.SetPan(pan) + } + } + + if nna, set := info.NewNoteAction.Get(); set { + traceChannelValueChangeWithComment(m, ch, "nna", c.nna, nna, "channel.RowStart") + c.nna = nna + } + + if na, set := info.ActionTick.Get(); set { + traceChannelOptionalValueChangeWithComment(m, ch, "target.ActionTick", c.target.ActionTick, na, "channel.RowStart") + c.target.ActionTick.Set(na) + } + + if c.target.Pos.IsSet() && !c.target.ActionTick.IsSet() { + c.target.ActionTick.Set(ActionTick{ + Action: note.ActionRetrigger, + Tick: 0, + }) + } + + return nil +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + for _, i := range c.instructions { + if err := m.DoInstructionTick(ch, i); err != nil { + return err + } + } + + return nil +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowEnd(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + for _, i := range c.instructions { + if err := m.DoInstructionRowEnd(ch, i); err != nil { + return err + } + } + + var ( + prevID instrument.ID + curID instrument.ID + ) + if c.prev.Inst != nil { + prevID = c.prev.Inst.GetID() + } + if c.target.Inst != nil { + curID = c.target.Inst.GetID() + } + traceChannelValueChangeWithComment(m, ch, "prev.Inst", prevID, curID, "channel.RowEnd") + c.prev.Inst = c.target.Inst + + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + var p TPeriod + if m.ms.Quirks.PreviousPeriodUsesModifiedPeriod { + var err error + p, err = freqMod.GetFinalPeriod() + if err != nil { + return err + } + } else { + p = freqMod.GetPeriod() + } + traceChannelValueChangeWithComment(m, ch, "prev.Period", c.prev.Period, p, "channel.RowEnd") + c.prev.Period = p + } + + if err := c.doPatternLoop(ch, m); err != nil { + return nil + } + + return nil +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) OrderEnd(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + return nil +} diff --git a/player/machine/instruction/instruction.go b/player/machine/instruction/instruction.go new file mode 100644 index 0000000..c693a59 --- /dev/null +++ b/player/machine/instruction/instruction.go @@ -0,0 +1,5 @@ +package instruction + +type Instruction interface { + TraceData() string +} diff --git a/player/machine/instruction/value.go b/player/machine/instruction/value.go new file mode 100644 index 0000000..fbf330c --- /dev/null +++ b/player/machine/instruction/value.go @@ -0,0 +1,5 @@ +package instruction + +type Value[T any] struct { + Value T +} diff --git a/player/machine/machine.go b/player/machine/machine.go new file mode 100644 index 0000000..2094eea --- /dev/null +++ b/player/machine/machine.go @@ -0,0 +1,270 @@ +package machine + +import ( + "errors" + "fmt" + + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/opl2" + + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/instruction" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/sampler" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/types" +) + +type ( + Period = settings.Period + Volume = settings.Volume + Panning = settings.Panning +) + +type VolumeFMA[TVolume Volume] interface { + FMA(multiplier, add float32) TVolume +} + +type PanningFMA[TPanning Panning] interface { + FMA(multiplier, add float32) TPanning +} + +type MachineInfo interface { + GetNumOrders() int + CanOrderLoop() bool + GetName() string +} + +type MachineTicker interface { + MachineInfo + + Tick(s *sampler.Sampler) error +} + +type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + MachineTicker + + ConvertToPeriod(n note.Note) TPeriod + IgnoreUnknownEffect() bool + GetQuirks() *settings.MachineQuirks + + // Globals + SetTempo(tempo int) error + SetBPM(bpm int) error + SlideBPM(add int) error + SetGlobalVolume(v TGlobalVolume) error + SlideGlobalVolume(multiplier, add float32) error + SetMixingVolume(v volume.Volume) error + SetSynthVolume(v volume.Volume) error + SetSampleVolume(v volume.Volume) error + SetOrder(o index.Order) error + SetRow(r index.Row, breakOrder bool) error + SetFilterOnAllChannelsByFilterName(name string, enabled bool, params any) error + GetPosition() Position + + // Single Row + AddExtraTicks(ticks int) error + RowRepeat(times int) error + + // Channel + GetChannelMemory(ch index.Channel) (song.ChannelMemory, error) + IsChannelMuted(ch index.Channel) (bool, error) + SetChannelMute(ch index.Channel, muted bool) error + SetChannelMixingVolume(ch index.Channel, v TMixingVolume) error + GetChannelPeriod(ch index.Channel) (TPeriod, error) + SetChannelPeriod(ch index.Channel, p TPeriod) error + SetChannelPeriodDelta(ch index.Channel, d period.Delta) error + GetChannelInstrument(ch index.Channel) (*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning], error) + SetChannelInstrumentByID(ch index.Channel, i int) error + GetChannelVolume(ch index.Channel) (TVolume, error) + SetChannelVolume(ch index.Channel, v TVolume) error + SetChannelVolumeDelta(ch index.Channel, d types.VolumeDelta) error + GetChannelPan(ch index.Channel) (TPanning, error) + SetChannelPan(ch index.Channel, pan TPanning) error + SetChannelPanningDelta(ch index.Channel, d types.PanDelta) error + SetChannelSurround(ch index.Channel, enabled bool) error + SetChannelFilter(ch index.Channel, f filter.Filter) error + ChannelStopOrRelease(ch index.Channel) error + ChannelStop(ch index.Channel) error + ChannelRelease(ch index.Channel) error + ChannelFadeout(ch index.Channel) error + GetNextChannelWavetableValue(ch index.Channel, speed int, depth float32, oscSelect Oscillator) (float32, error) + SetChannelNoteAction(ch index.Channel, na note.Action, tick int) error + SetPatternLoopStart(ch index.Channel) error + SetPatternLoops(ch index.Channel, count int) error + StartChannelPortaToNote(ch index.Channel) error + DoChannelPortaToNote(ch index.Channel, delta period.Delta) error + DoChannelPortaDown(ch index.Channel, delta period.Delta) error + DoChannelPortaUp(ch index.Channel, delta period.Delta) error + DoChannelArpeggio(ch index.Channel, delta int8) error + SlideChannelVolume(ch index.Channel, multiplier, add float32) error + SlideChannelMixingVolume(ch index.Channel, multiplier, add float32) error + SetChannelPos(ch index.Channel, pos sampling.Pos) error + SetChannelEnvelopePositions(ch index.Channel, pos int) error + SlideChannelPan(ch index.Channel, multiplier, add float32) error + SetChannelVolumeActive(ch index.Channel, on bool) error + SetChannelOscillatorWaveform(ch index.Channel, osc Oscillator, wave oscillator.WaveTableSelect) error + DoChannelPastNoteEffect(ch index.Channel, na note.Action) error + SetChannelNewNoteAction(ch index.Channel, na note.Action) error + SetChannelVolumeEnvelopeEnable(ch index.Channel, enabled bool) error + SetChannelPanningEnvelopeEnable(ch index.Channel, enabled bool) error + SetChannelPitchEnvelopeEnable(ch index.Channel, enabled bool) error + + // Instructions + DoInstructionOrderStart(ch index.Channel, i instruction.Instruction) error + DoInstructionRowStart(ch index.Channel, i instruction.Instruction) error + DoInstructionTick(ch index.Channel, i instruction.Instruction) error + DoInstructionRowEnd(ch index.Channel, i instruction.Instruction) error + DoInstructionOrderEnd(ch index.Channel, i instruction.Instruction) error +} + +type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + globals[TGlobalVolume] + singleRow + channels []channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + pastNotes []pastNote[TPeriod] + + ticker ticker + age int + + songData song.Data + ms *settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + us settings.UserSettings + opl2 *opl2.Chip + opl2Enabled bool + + rowStringer render.RowStringer + // 1:1 with channels + actualOutputs []render.Channel[TPeriod] + // extra channels for pastNotes playback + virtualOutputs []render.Channel[TPeriod] +} + +func NewMachine(songData song.Data, us settings.UserSettings) (MachineTicker, error) { + if songData == nil { + return nil, errors.New("songData is nil") + } + + tl := typeLookup{ + p: songData.GetPeriodType(), + gv: songData.GetGlobalVolumeType(), + cmv: songData.GetChannelMixingVolumeType(), + cv: songData.GetChannelVolumeType(), + cp: songData.GetChannelPanningType(), + } + + factory, found := factoryRegistry[tl] + str := tl.String() + _ = str + if !found { + return nil, errors.New("could not identify machine type from song parameters") + } + + return factory(songData, us) +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetPosition() Position { + return m.ticker.current +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetQuirks() *settings.MachineQuirks { + return &m.ms.Quirks +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ConvertToPeriod(n note.Note) TPeriod { + return m.ms.PeriodConverter.GetPeriod(n) +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IgnoreUnknownEffect() bool { + return m.us.IgnoreUnknownEffect +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetNumOrders() int { + return len(m.songData.GetOrderList()) +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) CanOrderLoop() bool { + return m.us.SongLoopCount != 0 +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetName() string { + return m.songData.GetName() +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) getChannel(ch index.Channel) (*channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], error) { + if int(ch) >= len(m.channels) { + return nil, fmt.Errorf("invalid channel index: %d", ch) + } + + return &m.channels[ch], nil +} + +type dataInstructionGenerator[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + ToInstructions(m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, songData song.Data) ([]instruction.Instruction, error) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) getRowData() (song.Row, error) { + pat, err := m.songData.GetPatternByOrder(m.ticker.current.Order) + if err != nil { + return nil, err + } + if pat == nil || int(m.ticker.current.Row) >= pat.NumRows() { + return nil, song.ErrStopSong + } + + row := pat.GetRow(m.ticker.current.Row) + if row == nil { + return nil, song.ErrStopSong + } + + return row, nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) updateInstructions(rowData song.Row) error { + for i := range m.channels { + m.channels[i].instructions = nil + } + + numRowChannels := song.GetRowNumChannels[TVolume](rowData) + rowChannels := min(m.songData.GetNumChannels(), numRowChannels) + return song.ForEachRowChannel(rowData, func(ch index.Channel, d song.ChannelData[TVolume]) (bool, error) { + if int(ch) >= rowChannels { + return true, nil + } + + c := &m.channels[ch] + if !c.enabled || d == nil || (m.ms.Quirks.DoNotProcessEffectsOnMutedChannels && c.cv.IsMuted()) { + return true, nil + } + + if err := c.decodeNote(m, d); err != nil { + return false, fmt.Errorf("channel[%d] decode error: %w", ch, err) + } + + if gen, ok := d.(dataInstructionGenerator[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { + insts, err := gen.ToInstructions(m, ch, m.songData) + if err != nil { + return false, fmt.Errorf("channel[%d] instruction error: %w", ch, err) + } + + c.instructions = insts + } + return true, nil + }) +} + +func GetPeriodCalculator[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) song.PeriodCalculator[TPeriod] { + mach, _ := m.(*machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if mach == nil { + return nil + } + + return mach.ms.PeriodConverter +} diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go new file mode 100644 index 0000000..7c4fa94 --- /dev/null +++ b/player/machine/machine_channel.go @@ -0,0 +1,586 @@ +package machine + +import ( + "errors" + "fmt" + + "github.com/gotracker/gomixing/sampling" + + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/types" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canPastNote() bool { + return m.us.EnableNewNoteActions && len(m.virtualOutputs) > 0 +} + +func withChannel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, fn func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error) error { + c, err := m.getChannel(ch) + if err != nil { + return err + } + + return fn(c) +} + +func withChannelReturningValue[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning, T any](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, fn func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (T, error)) (T, error) { + c, err := m.getChannel(ch) + if err != nil { + var empty T + return empty, err + } + + return fn(c) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelMemory(ch index.Channel) (song.ChannelMemory, error) { + return withChannelReturningValue(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (song.ChannelMemory, error) { + return c.memory, nil + }) +} + +func GetChannelMemory[TMemory song.ChannelMemory, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel) (TMemory, error) { + chMem, err := m.GetChannelMemory(ch) + if err != nil { + var empty TMemory + return empty, err + } + + mem, ok := chMem.(TMemory) + if !ok { + var empty TMemory + return empty, errors.New("could not convert channel memory type") + } + + return mem, nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IsChannelMuted(ch index.Channel) (bool, error) { + return withChannelReturningValue(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (bool, error) { + return c.cv.IsMuted(), nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelMute(ch index.Channel, muted bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + prevMute := c.cv.IsMuted() + traceChannelValueChangeWithComment(m, ch, "mute", prevMute, muted, "SetChannelMute") + return c.cv.SetMuted(muted) + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelMixingVolume(ch index.Channel, v TMixingVolume) error { + if v.IsInvalid() { + return fmt.Errorf("channel[%d] mixing volume out of range: %v", ch, v) + } + + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + mv := ampMod.GetMixingVolume() + traceChannelValueChangeWithComment(m, ch, "mv", mv, v, "SetChannelMixingVolume") + ampMod.SetMixingVolume(v) + } + return nil + }) +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelPeriod(ch index.Channel) (TPeriod, error) { + return withChannelReturningValue(&m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (TPeriod, error) { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + return freqMod.GetPeriod(), nil + } + var empty TPeriod + return empty, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPeriod(ch index.Channel, p TPeriod) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + tp := freqMod.GetPeriod() + traceChannelValueChangeWithComment(m, ch, "target.Period", tp, p, "SetChannelPeriod") + freqMod.SetPeriod(p) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPeriodDelta(ch index.Channel, d period.Delta) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + pd := freqMod.GetPeriodDelta() + traceChannelValueChangeWithComment(m, ch, "pd", pd, d, "SetChannelPeriodDelta") + freqMod.SetPeriodDelta(d) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelInstrument(ch index.Channel) (*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning], error) { + return withChannelReturningValue(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning], error) { + return c.target.Inst, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelInstrumentByID(ch index.Channel, i int) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + var oldID int + if c.target.Inst != nil { + oldID, _ = c.target.Inst.GetID().GetIndexAndSample() + } + + traceChannelValueChangeWithComment(m, ch, "target.Inst", oldID, i, "SetChannelInstrumentByID") + inst, _ := m.songData.GetInstrument(i, c.prev.Semitone.Coalesce(0)) + var ok bool + c.target.Inst, ok = inst.(*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) + if !ok { + return errors.New("could not convert instrument to pointer type") + } + + return nil + }) +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelVolume(ch index.Channel) (TVolume, error) { + return withChannelReturningValue(&m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (TVolume, error) { + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + return ampMod.GetVolume(), nil + } + var empty TVolume + return empty, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelVolume(ch index.Channel, v TVolume) error { + if v.IsInvalid() { + return fmt.Errorf("channel[%d] volume out of range: %v", ch, v) + } + + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelOptionalValueChangeWithComment(m, ch, "newNote.Vol", c.newNote.Vol, v, "SetChannelVolume") + c.newNote.Vol.Set(v) + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelVolumeDelta(ch index.Channel, d types.VolumeDelta) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + vd := ampMod.GetVolumeDelta() + traceChannelValueChangeWithComment(m, ch, "vd", vd, d, "SetChannelVolumeDelta") + ampMod.SetVolumeDelta(d) + } + return nil + }) +} + +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetChannelPan(ch index.Channel) (TPanning, error) { + return withChannelReturningValue(&m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (TPanning, error) { + if panMod, ok := c.cv.(voice.PanModulator[TPanning]); ok { + return panMod.GetPan(), nil + } + return types.GetPanDefault[TPanning](), nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPan(ch index.Channel, pan TPanning) error { + if pan.IsInvalid() { + return fmt.Errorf("channel[%d] channel pan out of range: %v", ch, pan) + } + + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if panMod, ok := c.cv.(voice.PanModulator[TPanning]); ok { + orig := panMod.GetPan() + traceChannelValueChangeWithComment(m, ch, "pan", orig, pan, "SetChannelPan") + panMod.SetPan(pan) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPanningDelta(ch index.Channel, d types.PanDelta) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if panMod, ok := c.cv.(voice.PanModulator[TPanning]); ok { + orig := panMod.GetPanDelta() + traceChannelValueChangeWithComment(m, ch, "delta", orig, d, "SetChannelPanningDelta") + panMod.SetPanDelta(d) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelSurround(ch index.Channel, enabled bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelValueChangeWithComment(m, ch, "surround", c.surround, enabled, "SetChannelSurround") + c.surround = enabled + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelFilter(ch index.Channel, f filter.Filter) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelValueChangeWithComment(m, ch, "filter", c.filter, f, "SetChannelFilter") + c.filter = f + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ChannelStopOrRelease(ch index.Channel) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannel(m, ch, "ChannelStopOrRelease") + var n note.StopOrReleaseNote + if c.target.Inst != nil && c.target.Inst.IsReleaseNote(n) { + c.cv.Release() + return nil + } + + c.cv.Stop() + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ChannelStop(ch index.Channel) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannel(m, ch, "ChannelStop") + c.cv.Stop() + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ChannelRelease(ch index.Channel) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannel(m, ch, "ChannelRelease") + c.cv.Release() + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ChannelFadeout(ch index.Channel) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannel(m, ch, "ChannelFadeout") + c.cv.Fadeout() + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetNextChannelWavetableValue(ch index.Channel, speed int, depth float32, oscSelect Oscillator) (float32, error) { + if int(oscSelect) >= NumOscillators { + return 0, fmt.Errorf("oscillator select out of range: %v", oscSelect) + } + + return withChannelReturningValue(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (float32, error) { + o := c.osc[oscSelect] + out := o.GetWave(depth) + o.Advance(speed) + return out, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelNoteAction(ch index.Channel, na note.Action, tick int) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + at := ActionTick{Action: na, Tick: tick} + traceChannelOptionalValueChangeWithComment(m, ch, "target.ActionTick", c.target.ActionTick, at, "SetChannelNoteAction") + c.target.ActionTick.Set(at) + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetPatternLoopStart(ch index.Channel) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelValueChangeWithComment(m, ch, "patternLoopStart", c.patternLoop.Start, m.ticker.current.Row, "SetPatternLoopStart") + c.patternLoop.Start = m.ticker.current.Row + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetPatternLoops(ch index.Channel, count int) error { + if count <= 0 { + return fmt.Errorf("loop count out of range: %d", count) + } + + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + pl := &c.patternLoop + + traceChannelValueChangeWithComment(m, ch, "patternLoopEnd", pl.End, m.ticker.current.Row, "SetPatternLoops") + pl.End = m.ticker.current.Row + + traceChannelValueChangeWithComment(m, ch, "patternLoopTotal", pl.Total, count, "SetPatternLoops") + pl.Total = count + + return nil + }) + +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) StartChannelPortaToNote(ch index.Channel) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelOptionalValueResetWithComment(m, ch, "newNote.ActionTick", c.newNote.ActionTick, "StartChannelPortaToNote") + c.newNote.ActionTick.Reset() + + if p, set := c.newNote.Period.Get(); set { + traceChannelOptionalValueResetWithComment(m, ch, "newNote.Period", c.newNote.Period, "StartChannelPortaToNote") + c.newNote.Period.Reset() + traceChannelValueChangeWithComment(m, ch, "target.PortaPeriod", c.target.PortaPeriod, p, "StartChannelPortaToNote") + c.target.PortaPeriod = p + } + + traceChannelOptionalValueResetWithComment(m, ch, "newNote.Pos", c.newNote.Pos, "StartChannelPortaToNote") + c.newNote.Pos.Reset() + c.target.TriggerNNA = false + + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPortaToNote(ch index.Channel, delta period.Delta) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + var p TPeriod + if m.ms.Quirks.PortaToNoteUsesModifiedPeriod { + var err error + p, err = freqMod.GetFinalPeriod() + if err != nil { + return err + } + } else { + p = freqMod.GetPeriod() + } + tp, err := m.ms.PeriodConverter.PortaToNote(p, delta, c.target.PortaPeriod) + if err != nil { + return err + } + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaToNote (%d)", delta) + freqMod.SetPeriod(tp) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPortaDown(ch index.Channel, delta period.Delta) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + p := freqMod.GetPeriod() + tp, err := m.ms.PeriodConverter.PortaDown(p, delta) + if err != nil { + return err + } + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaDown (%d)", delta) + freqMod.SetPeriod(tp) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPortaUp(ch index.Channel, delta period.Delta) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + p := freqMod.GetPeriod() + tp, err := m.ms.PeriodConverter.PortaUp(p, delta) + if err != nil { + return err + } + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaUp (%d)", delta) + freqMod.SetPeriod(tp) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelArpeggio(ch index.Channel, delta int8) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + p := freqMod.GetPeriod() + + st := c.prev.Semitone.Coalesce(0) + + tp := m.ConvertToPeriod(note.Normal(note.Semitone(int8(st) + delta))) + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelArpeggio") + freqMod.SetPeriod(tp) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SlideChannelVolume(ch index.Channel, multiplier, add float32) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + vol := ampMod.GetVolume() + fma, ok := any(vol).(VolumeFMA[TVolume]) + if !ok { + return errors.New("could not determine FMA interface for channel volume") + } + v := fma.FMA(multiplier, add) + + if v.IsInvalid() { + return fmt.Errorf("channel volume out of range: %v", v) + } + + traceChannelValueChangeWithComment[TVolume](m, ch, "vol", vol, v, "SlideChannelVolume") + ampMod.SetVolume(v) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SlideChannelMixingVolume(ch index.Channel, multiplier, add float32) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + mv := ampMod.GetMixingVolume() + fma, ok := any(mv).(VolumeFMA[TMixingVolume]) + if !ok { + return errors.New("could not determine FMA interface for channel mixing volume") + } + v := fma.FMA(multiplier, add) + + if v.IsInvalid() { + return fmt.Errorf("channel mixing volume out of range: %v", v) + } + + traceChannelValueChangeWithComment[TMixingVolume](m, ch, "mv", mv, v, "SlideChannelMixingVolume") + ampMod.SetMixingVolume(v) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPos(ch index.Channel, pos sampling.Pos) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelOptionalValueChangeWithComment(m, ch, "newNote.Pos", c.newNote.Pos, pos, "SetChannelPos") + c.newNote.Pos.Set(pos) + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelEnvelopePositions(ch index.Channel, pos int) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if volEnv, ok := c.cv.(voice.VolumeEnvelope[TGlobalVolume, TMixingVolume, TVolume]); ok { + volPos := volEnv.GetVolumeEnvelopePosition() + traceChannelValueChangeWithComment(m, ch, "volEnv.Pos", volPos, pos, "SetChannelEnvelopePositions") + volEnv.SetVolumeEnvelopePosition(pos) + } + + if pitchEnv, ok := c.cv.(voice.PitchEnvelope[TPeriod]); ok { + pitchPos := pitchEnv.GetPitchEnvelopePosition() + traceChannelValueChangeWithComment(m, ch, "pitchEnv.Pos", pitchPos, pos, "SetChannelEnvelopePositions") + pitchEnv.SetPitchEnvelopePosition(pos) + } + + if panEnv, ok := c.cv.(voice.PanEnvelope[TPanning]); ok { + panPos := panEnv.GetPanEnvelopePosition() + traceChannelValueChangeWithComment(m, ch, "panEnv.Pos", panPos, pos, "SetChannelEnvelopePositions") + panEnv.SetPanEnvelopePosition(pos) + } + + if filterEnv, ok := c.cv.(voice.FilterEnvelope); ok { + filtPos := filterEnv.GetFilterEnvelopePosition() + traceChannelValueChangeWithComment(m, ch, "filterEnv.Pos", filtPos, pos, "SetChannelEnvelopePositions") + filterEnv.SetFilterEnvelopePosition(pos) + } + + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SlideChannelPan(ch index.Channel, multiplier, add float32) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if panMod, ok := c.cv.(voice.PanModulator[TPanning]); ok { + p := panMod.GetPan() + fma, ok := any(p).(PanningFMA[TPanning]) + if !ok { + return errors.New("could not determine FMA interface for channel panning") + } + v := fma.FMA(multiplier, add) + + if v.IsInvalid() { + return fmt.Errorf("channel panning out of range: %v", v) + } + + traceChannelValueChangeWithComment[TPanning](m, ch, "pan", p, v, "SlideChannelPan") + panMod.SetPan(v) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelVolumeActive(ch index.Channel, on bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { + active := ampMod.IsActive() + traceChannelValueChangeWithComment(m, ch, "active", active, on, "SetChannelVolumeActive") + ampMod.SetActive(on) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelOscillatorWaveform(ch index.Channel, osc Oscillator, wave oscillator.WaveTableSelect) error { + if int(osc) >= NumOscillators { + return fmt.Errorf("oscillator select out of range: %v", osc) + } + + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + wf := c.osc[osc].GetWaveform() + traceChannelValueChangeWithComment(m, ch, fmt.Sprintf("osc[%d].wave", osc), wf, wave, "SetChannelOscillatorWaveform") + c.osc[osc].SetWaveform(wave) + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPastNoteEffect(ch index.Channel, na note.Action) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + c.doPastNoteAction(m, na) + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelNewNoteAction(ch index.Channel, na note.Action) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelValueChangeWithComment(m, ch, "nna", c.nna, na, "SetChannelNewNoteAction") + c.nna = na + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelVolumeEnvelopeEnable(ch index.Channel, enabled bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if volEnv, ok := c.cv.(voice.VolumeEnvelope[TGlobalVolume, TMixingVolume, TVolume]); ok { + on := volEnv.IsVolumeEnvelopeEnabled() + traceChannelValueChangeWithComment(m, ch, "volEnv.enabled", on, enabled, "SetChannelVolumeEnvelopeEnable") + volEnv.EnableVolumeEnvelope(enabled) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPanningEnvelopeEnable(ch index.Channel, enabled bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if panEnv, ok := c.cv.(voice.PanEnvelope[TPanning]); ok { + on := panEnv.IsPanEnvelopeEnabled() + traceChannelValueChangeWithComment(m, ch, "panEnv.enabled", on, enabled, "SetChannelPanningEnvelopeEnable") + panEnv.EnablePanEnvelope(enabled) + } + return nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelPitchEnvelopeEnable(ch index.Channel, enabled bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if pitchEnv, ok := c.cv.(voice.PitchEnvelope[TPeriod]); ok { + on := pitchEnv.IsPitchEnvelopeEnabled() + traceChannelValueChangeWithComment(m, ch, "pitchEnv.enabled", on, enabled, "SetChannelPitchEnvelopeEnable") + pitchEnv.EnablePitchEnvelope(enabled) + } + return nil + }) +} diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go new file mode 100644 index 0000000..f71d6bb --- /dev/null +++ b/player/machine/machine_factory.go @@ -0,0 +1,216 @@ +package machine + +import ( + "fmt" + "reflect" + + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/opl2" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice" +) + +type typeLookup struct { + p reflect.Type + gv reflect.Type + cmv reflect.Type + cv reflect.Type + cp reflect.Type +} + +func (t typeLookup) String() string { + return fmt.Sprintf("[%v, %v, %v, %v, %v]", t.p, t.gv, t.cmv, t.cv, t.cp) +} + +type factory func(songData song.Data, us settings.UserSettings) (MachineTicker, error) + +var factoryRegistry = make(map[typeLookup]factory) + +func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](ms *settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { + var ( + p TPeriod + gv TGlobalVolume + cmv TMixingVolume + cv TVolume + cp TPanning + ) + tl := typeLookup{ + p: reflect.TypeOf(p), + gv: reflect.TypeOf(gv), + cmv: reflect.TypeOf(cmv), + cv: reflect.TypeOf(cv), + cp: reflect.TypeOf(cp), + } + + if _, exists := factoryRegistry[tl]; exists { + panic(fmt.Sprintf("attempted to re-register factory for %s", tl.String())) + } + + factoryRegistry[tl] = func(songData song.Data, us settings.UserSettings) (MachineTicker, error) { + var m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + // we have to use the songData's machine settings + sms := songData.GetMachineSettings() + + var ok bool + m.ms, ok = sms.(*settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil, fmt.Errorf("invalid machine settings from songdata: %T - expected %s", sms, tl.String()) + } + + m.songData = songData + m.us = us + + order := songData.GetInitialOrder() + if o, set := us.Start.Order.Get(); set { + order = index.Order(o) + } + + var row index.Row + if r, set := us.Start.Row.Get(); set { + row = index.Row(r) + } + + sys := songData.GetSystem() + + bpm := songData.GetInitialBPM() + if us.Start.BPM != 0 { + bpm = us.Start.BPM + } + + tempo := songData.GetInitialTempo() + if us.Start.Tempo != 0 { + tempo = us.Start.Tempo + } + + if err := m.SetBPM(bpm); err != nil { + return nil, err + } + if err := m.SetTempo(tempo); err != nil { + return nil, err + } + gv, err := song.GetGlobalVolume[TGlobalVolume](songData) + if err != nil { + return nil, err + } + if err := m.SetGlobalVolume(gv); err != nil { + return nil, err + } + mv, err := song.GetMixingVolume[TMixingVolume](songData) + if err != nil { + return nil, err + } + if err := m.SetMixingVolume(mv.ToVolume()); err != nil { + return nil, err + } + + channels := songData.GetNumChannels() + + m.channels = make([]channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], channels) + m.actualOutputs = make([]render.Channel[TPeriod], channels) + + m.opl2Enabled = songData.IsOPL2Enabled() + + mpnpc := sys.GetMaxPastNotesPerChannel() + if mpnpc > 0 { + m.virtualOutputs = make([]render.Channel[TPeriod], channels*mpnpc) + } + + for i := 0; i < channels; i++ { + ch := index.Channel(i) + cs := songData.GetChannelSettings(ch) + + rc := &m.actualOutputs[ch] + rc.OutputFilter = nil + + if cs.IsDefaultFilterEnabled() { + info := cs.GetDefaultFilterInfo() + filt, err := ms.GetFilterFactory(info.Name, sys.GetCommonRate(), info.Params) + if err != nil { + return nil, err + } + + rc.OutputFilter = filt + } + rc.GetOPL2Chip = func() *opl2.Chip { + return m.opl2 + } + + initialVolume, err := song.GetChannelInitialVolume[TVolume](cs) + if err != nil { + return nil, err + } + + initialMixing, err := song.GetChannelMixingVolume[TMixingVolume](cs) + if err != nil { + return nil, err + } + + initialPan, err := song.GetChannelInitialPanning[TPanning](cs) + if err != nil { + return nil, err + } + + rc.GlobalVolume = volume.Volume(1) + + c := &m.channels[ch] + c.enabled = cs.IsEnabled() + c.cv = m.ms.VoiceFactory.NewVoice(voice.VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ + PC: ms.PeriodConverter, + OPLChannel: cs.GetOPLChannel(), + InitialVolume: initialVolume, + InitialMixing: initialMixing, + PanEnabled: cs.IsPanEnabled(), + InitialPan: initialPan, + Vol0Optimization: cs.GetVol0OptimizationSettings(), + }) + c.memory = cs.GetMemory() + rc.StartVoice(c.cv, func() {}) // can't remove this channel, as it's hard-wired into actual + c.target.ActionTick.Reset() + + c.nna = note.ActionCut + if c.osc[OscillatorVibrato], err = ms.GetVibratoFactory(); err != nil { + return nil, err + } + if c.osc[OscillatorTremolo], err = ms.GetTremoloFactory(); err != nil { + return nil, err + } + if c.osc[OscillatorPanbrello], err = ms.GetPanbrelloFactory(); err != nil { + return nil, err + } + cmv, err := song.GetChannelMixingVolume[TMixingVolume](cs) + if err != nil { + return nil, fmt.Errorf("channel[%d]: %w", i, err) + } + m.SetChannelMute(ch, cs.IsMuted()) + m.SetChannelMixingVolume(ch, cmv) + cv, err := song.GetChannelInitialVolume[TVolume](cs) + if err != nil { + return nil, fmt.Errorf("channel[%d]: %w", i, err) + } + m.SetChannelVolume(ch, cv) + cp, err := song.GetChannelInitialPanning[TPanning](cs) + if err != nil { + return nil, fmt.Errorf("channel[%d]: %w", i, err) + } + m.SetChannelPan(ch, cp) + } + + if err := initTick(&m.ticker, &m, tickerSettings{ + InitialOrder: order, + InitialRow: row, + SongLoopStartingOrder: 0, + SongLoopCount: us.SongLoopCount, + PlayUntilOrder: us.PlayUntil.Order, + PlayUntilRow: us.PlayUntil.Row, + }); err != nil { + return nil, err + } + + return &m, nil + } +} diff --git a/player/machine/machine_globals.go b/player/machine/machine_globals.go new file mode 100644 index 0000000..33107d6 --- /dev/null +++ b/player/machine/machine_globals.go @@ -0,0 +1,163 @@ +package machine + +import ( + "errors" + "fmt" + + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" +) + +type globals[TGlobalVolume Volume] struct { + bpm int + tempo int + + gv TGlobalVolume // global volume + mv volume.Volume // mixing volume + synv volume.Volume // synth volume + sampv volume.Volume // sample volume + + patternLoopStart index.Row + patternLoopCount int +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetTempo(tempo int) error { + if tempo == 0 { + return errors.New("tempo cannot be 0") + } + + traceValueChangeWithComment(m, "tempo", m.tempo, tempo, "SetTempo") + m.tempo = tempo + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetBPM(bpm int) error { + if bpm == 0 { + return errors.New("bpm cannot be 0") + } + + traceValueChangeWithComment(m, "bpm", m.bpm, bpm, "SetBPM") + m.bpm = bpm + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SlideBPM(add int) error { + if add == 0 { + return nil + } + + bpm := m.bpm + add + if bpm <= 0 || bpm > 255 { + return fmt.Errorf("resulting bpm would be invalid: %d", bpm) + } + + traceValueChangeWithComment(m, "bpm", m.bpm, bpm, "SlideBPM") + m.bpm = bpm + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetGlobalVolume(v TGlobalVolume) error { + if v.IsInvalid() { + return fmt.Errorf("global volume out of range: %v", v) + } + + traceValueChangeWithComment(m, "gv", m.gv, v, "SetGlobalVolume") + m.gv = v + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SlideGlobalVolume(multiplier, add float32) error { + fma, ok := any(m.gv).(VolumeFMA[TGlobalVolume]) + if !ok { + return errors.New("could not determine FMA interface for global volume") + } + v := fma.FMA(multiplier, add) + + if v.IsInvalid() { + return fmt.Errorf("global volume out of range: %v", v) + } + + traceValueChangeWithComment(m, "gv", m.gv, v, "SlideGlobalVolume") + m.gv = v + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetMixingVolume(v volume.Volume) error { + if v < 0 || v > 1 { + return fmt.Errorf("mixing volume out of range: %v", v) + } + + traceValueChangeWithComment(m, "mv", m.mv, v, "SetMixingVolume") + m.mv = v + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetSynthVolume(v volume.Volume) error { + if v < 0 || v > 1 { + return fmt.Errorf("synth volume out of range: %v", v) + } + + traceValueChangeWithComment(m, "synv", m.synv, v, "SetSynthVolume") + m.synv = v + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetSampleVolume(v volume.Volume) error { + if v < 0 || v > 1 { + return fmt.Errorf("sample volume out of range: %v", v) + } + + traceValueChangeWithComment(m, "sampv", m.sampv, v, "SetSampleVolume") + m.sampv = v + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetOrder(o index.Order) error { + if int(o) >= len(m.songData.GetOrderList()) { + return fmt.Errorf("order index out of range: %d", o) + } + + traceOptionalValueChangeWithComment(m, "nextOrder", m.ticker.next.order, o, "SetOrder") + m.ticker.next.order.Set(o) + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetRow(r index.Row, breakOrder bool) error { + rb := tickerRowBreak{ + row: r, + breakOrder: breakOrder, + } + traceOptionalValueChangeWithComment(m, "nextRow", m.ticker.next.row, rb, "SetRow") + m.ticker.next.row.Set(rb) + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetFilterOnAllChannelsByFilterName(name string, enabled bool, params any) error { + cr := m.songData.GetSystem().GetCommonRate() + return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if enabled { + filt, err := m.ms.GetFilterFactory(name, cr, params) + if err != nil { + return false, err + } + + if err != nil { + return false, err + } + c.filter = filt + } else { + c.filter = nil + } + return true, nil + }) +} diff --git a/player/machine/machine_instruction.go b/player/machine/machine_instruction.go new file mode 100644 index 0000000..0c63ca7 --- /dev/null +++ b/player/machine/machine_instruction.go @@ -0,0 +1,71 @@ +package machine + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/player/machine/instruction" +) + +type instructionOrderStart[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + OrderStart(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionOrderStart(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionOrderStart[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.OrderStart(ch, m) +} + +type instructionRowStart[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + RowStart(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionRowStart(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionRowStart[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.RowStart(ch, m) +} + +type instructionTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + Tick(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionTick(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionTick[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.Tick(ch, m, m.ticker.current.Tick) +} + +type instructionRowEnd[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + RowEnd(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionRowEnd(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionRowEnd[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.RowEnd(ch, m) +} + +type instructionOrderEnd[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + OrderEnd(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionOrderEnd(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionOrderEnd[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.OrderEnd(ch, m) +} diff --git a/player/machine/machine_opl2.go b/player/machine/machine_opl2.go new file mode 100644 index 0000000..de3e668 --- /dev/null +++ b/player/machine/machine_opl2.go @@ -0,0 +1,64 @@ +package machine + +import ( + "errors" + + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/opl2" + "github.com/gotracker/playback/player/sampler" + "github.com/gotracker/playback/voice" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setupOPL2(s *sampler.Sampler) error { + if s == nil { + return errors.New("sampler is nil") + } + + o := opl2.NewChip(uint32(s.SampleRate), false) + o.WriteReg(0x01, 0x20) // enable all waveforms + o.WriteReg(0x04, 0x00) // clear timer flags + o.WriteReg(0x08, 0x40) // clear CSW and set NOTE-SEL + o.WriteReg(0xBD, 0x00) // set default notes + m.opl2 = o + + for i := range m.actualOutputs { + rc := &m.actualOutputs[i] + if v, _ := rc.GetVoice().(voice.VoiceOPL2er); v != nil { + v.SetOPL2Chip(m.opl2) + } + } + + for i := range m.virtualOutputs { + rc := &m.virtualOutputs[i] + if v, _ := rc.GetVoice().(voice.VoiceOPL2er); v != nil { + v.SetOPL2Chip(m.opl2) + } + } + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) renderOPL2Tick(mixerData *mixing.Data, mix *mixing.Mixer, tickSamples int) error { + // make a stand-alone data buffer for this channel for this tick + data := mix.NewMixBuffer(tickSamples) + + opl2data := make([]int32, tickSamples) + + if opl2 := m.opl2; opl2 != nil { + opl2.GenerateBlock2(uint(tickSamples), opl2data) + } + + for i, s := range opl2data { + sv := volume.Volume(s) / 32768.0 + data[i].Assign(1, []volume.Volume{sv}) + } + *mixerData = mixing.Data{ + Data: data, + Pan: panning.CenterAhead, + Volume: m.gv.ToVolume(), + SamplesLen: tickSamples, + } + return nil +} diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go new file mode 100644 index 0000000..596e12b --- /dev/null +++ b/player/machine/machine_render.go @@ -0,0 +1,139 @@ +package machine + +import ( + "fmt" + + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/output" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/sampler" + "github.com/gotracker/playback/voice/mixer" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) render(s *sampler.Sampler) (*output.PremixData, error) { + tickDuration := m.songData.GetTickDuration(m.bpm) + if tickDuration <= 0 { + return nil, fmt.Errorf("unexpected tick duration: %v", tickDuration) + } + + renderRow := render.RowRender{ + Order: int(m.ticker.current.Order), + Row: int(m.ticker.current.Row), + Tick: m.ticker.current.Tick, + } + + premix := output.PremixData{ + SamplesLen: int(float64(s.SampleRate) * tickDuration.Seconds()), + MixerVolume: m.gv.ToVolume() * m.mv, + Userdata: &renderRow, + } + + if m.ticker.current.Tick == 0 { + // make a copy so it doesn't get stomped + renderRow.RowText = m.rowStringer + } + + details := mixer.Details{ + Mix: s.Mixer(), + Panmixer: s.GetPanMixer(), + SampleRate: frequency.Frequency(s.SampleRate), + Samples: premix.SamplesLen, + Duration: tickDuration, + } + + centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) + + var mixData []mixing.Data + for i := range m.actualOutputs { + rc := &m.actualOutputs[i] + + rc.GlobalVolume = m.gv.ToVolume() + + rc.GetVoice().DumpState(index.Channel(i), m.us.Tracer) + data, err := rc.RenderAndTick(m.ms.PeriodConverter, centerAheadPan, details) + if err != nil { + return nil, err + } + + if data != nil { + mixData = append(mixData, *data) + } else { + mixData = append(mixData, mixing.Data{ + Data: details.Mix.NewMixBuffer(details.Samples), + Pan: panning.CenterAhead, + Volume: volume.Volume(0), + Pos: 0, + SamplesLen: details.Samples, + }) + } + } + + for i := range m.virtualOutputs { + rc := &m.virtualOutputs[i] + + var data *mixing.Data + if rc.GetVoice() != nil { + rc.GlobalVolume = m.gv.ToVolume() + + //rc.GetVoice().DumpState(index.Channel(i), m.us.Tracer) + var err error + data, err = rc.RenderAndTick(m.ms.PeriodConverter, centerAheadPan, details) + if err != nil { + return nil, err + } + } + + if data != nil { + mixData = append(mixData, *data) + } else { + mixData = append(mixData, mixing.Data{ + Data: details.Mix.NewMixBuffer(details.Samples), + Pan: panning.CenterAhead, + Volume: volume.Volume(0), + Pos: 0, + SamplesLen: details.Samples, + }) + } + } + + if len(mixData) > 0 { + premix.Data = append(premix.Data, mixData) + } + + if m.opl2 != nil { + rr := [1]mixing.Data{} + if err := m.renderOPL2Tick(&rr[0], s.Mixer(), premix.SamplesLen); err != nil { + return nil, err + } + premix.Data = append(premix.Data, rr[:]) + + // make room in the mixer for the OPL2 data + // effectively, we can do this by calculating the new number (+1) of channels from the mixer volume (channels = reciprocal of mixer volume): + // numChannels = (1/mv) + 1 + // then by taking the reciprocal of it: + // 1 / numChannels + // but that ends up being simplified to: + // mv / (mv + 1) + // and we get protection from div/0 in the process - provided, of course, that the mixerVolume is not exactly -1... + mv := premix.MixerVolume + premix.MixerVolume /= (mv + 1) + } + + if len(premix.Data) == 0 { + premix.Data = append(premix.Data, mixing.ChannelData{ + mixing.Data{ + Data: details.Mix.NewMixBuffer(details.Samples), + Pan: panning.CenterAhead, + Volume: volume.Volume(0), + Pos: 0, + SamplesLen: details.Samples, + }, + }) + } + + return &premix, nil +} diff --git a/player/machine/machine_singlerow.go b/player/machine/machine_singlerow.go new file mode 100644 index 0000000..51b001b --- /dev/null +++ b/player/machine/machine_singlerow.go @@ -0,0 +1,49 @@ +package machine + +import "fmt" + +type singleRow struct { + extraTicks int + repeats int +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) singleRowRowStart() error { + var reset singleRow + traceValueChangeWithComment(m, "extraTicks", m.extraTicks, reset.extraTicks, "RowStart") + m.extraTicks = reset.extraTicks + traceValueChangeWithComment(m, "repeats", m.repeats, reset.repeats, "RowStart") + m.repeats = reset.repeats + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) consumeRepeat() bool { + if m.repeats <= 0 { + return false + } + + r := m.repeats - 1 + traceValueChangeWithComment(m, "repeats", m.repeats, r, "consumeRepeat") + m.repeats = r + return true +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) AddExtraTicks(ticks int) error { + if ticks < 0 { + return fmt.Errorf("invalid number of ticks to add: %d", ticks) + } + + t := m.extraTicks + ticks + traceValueChangeWithComment(m, "extraTicks", m.extraTicks, t, "AddExtraTicks") + m.extraTicks = t + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowRepeat(times int) error { + if times < 0 { + return fmt.Errorf("invalid number of repeat times: %d", times) + } + + traceValueChangeWithComment(m, "repeats", m.repeats, times, "RowRepeat") + m.repeats = times + return nil +} diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go new file mode 100644 index 0000000..e5fdef3 --- /dev/null +++ b/player/machine/machine_tick.go @@ -0,0 +1,131 @@ +package machine + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/player/sampler" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(s *sampler.Sampler) error { + if s != nil { + if m.opl2Enabled && m.opl2 == nil && m.ms.OPL2Enabled { + if err := m.setupOPL2(s); err != nil { + return err + } + } + } + + if err := m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.DoNoteAction(ch, m); err != nil { + return false, err + } + return true, nil + }); err != nil { + return err + } + + err := runTick(&m.ticker, m) + if err != nil { + return err + } + + m.age++ + + if s != nil { + premix, err := m.render(s) + if err != nil { + return err + } + if s.OnGenerate != nil { + s.OnGenerate(premix) + } + } + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTick() error { + return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.Tick(ch, m); err != nil { + return false, err + } + + c.updatePastNotes(m) + return true, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onOrderStart() error { + return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.OrderStart(ch, m); err != nil { + return false, err + } + return true, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowStart() error { + rowData, err := m.getRowData() + if err != nil { + return err + } + + m.rowStringer = m.songData.GetRowRenderStringer(rowData, len(m.channels), m.us.LongChannelOutput) + + trace(m, m.rowStringer.String()) + + if err := m.singleRowRowStart(); err != nil { + return err + } + + if err := m.updateInstructions(rowData); err != nil { + return err + } + + return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.RowStart(ch, m); err != nil { + return false, err + } + return true, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowEnd() error { + if err := m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.RowEnd(ch, m); err != nil { + return false, err + } + return true, nil + }); err != nil { + return err + } + + for i := range m.actualOutputs { + rc := &m.actualOutputs[i] + if v := rc.GetVoice(); v != nil { + v.RowEnd() + } + } + + for i := range m.virtualOutputs { + rc := &m.virtualOutputs[i] + if v := rc.GetVoice(); v != nil { + v.RowEnd() + } + } + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onOrderEnd() error { + return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.OrderEnd(ch, m); err != nil { + return false, err + } + return true, nil + }) +} diff --git a/player/machine/machine_tracing.go b/player/machine/machine_tracing.go new file mode 100644 index 0000000..4bc4148 --- /dev/null +++ b/player/machine/machine_tracing.go @@ -0,0 +1,115 @@ +package machine + +import ( + "reflect" + + "github.com/gotracker/playback/index" + "github.com/heucuva/optional" +) + +func trace[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string) { + m.us.Trace(name) +} + +func traceOptionalValueClear[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before optional.Value[T]) { + if v, set := before.Get(); set { + m.us.TraceValueChange(name, v, nil) + } +} + +func traceOptionalValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before optional.Value[T], after T) { + if v, set := before.Get(); set { + traceValueChange(m, name, v, after) + return + } + + m.us.TraceValueChange(name, nil, after) +} + +func traceWithComment[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name, commentFmt string, commentParams ...any) { + m.us.TraceWithComment(name, commentFmt, commentParams...) +} + +func traceValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before T, after T) { + if reflect.DeepEqual(before, after) { + return + } + m.us.TraceValueChange(name, before, after) +} + +func traceOptionalValueResetWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before optional.Value[T], commentFmt string, commentParams ...any) { + if v, set := before.Get(); set { + m.us.TraceValueChangeWithComment(name, v, nil, commentFmt, commentParams...) + } +} + +func traceOptionalValueChangeWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before optional.Value[T], after T, commentFmt string, commentParams ...any) { + if v, set := before.Get(); set { + traceValueChangeWithComment(m, name, v, after, commentFmt, commentParams...) + return + } + + m.us.TraceValueChangeWithComment(name, nil, after, commentFmt, commentParams...) +} + +func traceValueChangeWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before, after T, commentFmt string, commentParams ...any) { + if reflect.DeepEqual(before, after) { + return + } + + m.us.TraceValueChangeWithComment(name, before, after, commentFmt, commentParams...) +} + +func traceChannel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string) { + m.us.TraceChannel(ch, name) +} + +func traceChannelWithComment[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name, commentFmt string, commentParams ...any) { + m.us.TraceChannelWithComment(ch, name, commentFmt, commentParams...) +} + +func traceChannelOptionalValueReset[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string, before optional.Value[T]) { + if v, set := before.Get(); set { + m.us.TraceChannelValueChange(ch, name, v, nil) + } +} + +func traceChannelOptionalValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string, before optional.Value[T], after T) { + if v, set := before.Get(); set { + traceChannelValueChange(m, ch, name, v, after) + return + } + + m.us.TraceChannelValueChange(ch, name, nil, after) +} + +func traceChannelValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string, before, after T) { + if reflect.DeepEqual(before, after) { + return + } + + m.us.TraceChannelValueChange(ch, name, before, after) +} + +func traceChannelOptionalValueResetWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string, before optional.Value[T], commentFmt string, commentParams ...any) { + if v, set := before.Get(); set { + m.us.TraceChannelValueChangeWithComment(ch, name, v, nil, commentFmt, commentParams...) + } +} + +func traceChannelOptionalValueChangeWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string, before optional.Value[T], after T, commentFmt string, commentParams ...any) { + if v, set := before.Get(); set { + traceChannelValueChangeWithComment(m, ch, name, v, after, commentFmt, commentParams...) + return + } + + m.us.TraceChannelValueChangeWithComment(ch, name, nil, after, commentFmt, commentParams...) +} + +func traceChannelValueChangeWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], ch index.Channel, name string, before, after T, commentFmt string, commentParams ...any) { + if reflect.DeepEqual(before, after) { + return + } + + m.us.TraceChannelValueChangeWithComment(ch, name, before, after, commentFmt, commentParams...) +} diff --git a/player/machine/newnoteinfo.go b/player/machine/newnoteinfo.go new file mode 100644 index 0000000..35d4fab --- /dev/null +++ b/player/machine/newnoteinfo.go @@ -0,0 +1,46 @@ +package machine + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/heucuva/optional" +) + +type ActionTick struct { + Action note.Action + Tick int +} + +type NewNoteInfo[TPeriod Period, TMixingVolume, TVolume Volume, TPanning Panning] struct { + Period optional.Value[TPeriod] + Inst optional.Value[*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]] + Pos optional.Value[sampling.Pos] + MixVol optional.Value[TMixingVolume] + Vol optional.Value[TVolume] + Pan optional.Value[TPanning] + ActionTick optional.Value[ActionTick] + NewNoteAction optional.Value[note.Action] +} + +func (n *NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning]) Reset() { + n.Period.Reset() + n.Inst.Reset() + n.Pos.Reset() + n.MixVol.Reset() + n.Vol.Reset() + n.Pan.Reset() + n.ActionTick.Reset() + n.NewNoteAction.Reset() +} + +func (n NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning]) IsSet() bool { + return n.Period.IsSet() || + n.Inst.IsSet() || + n.Pos.IsSet() || + n.MixVol.IsSet() || + n.Vol.IsSet() || + n.Pan.IsSet() || + n.ActionTick.IsSet() || + n.NewNoteAction.IsSet() +} diff --git a/player/machine/oscillator.go b/player/machine/oscillator.go new file mode 100644 index 0000000..0ba65f1 --- /dev/null +++ b/player/machine/oscillator.go @@ -0,0 +1,14 @@ +package machine + +type Oscillator int + +const ( + OscillatorVibrato = Oscillator(iota) + OscillatorTremolo + OscillatorPanbrello + + //==== + cNumOscillators +) + +const NumOscillators = int(cNumOscillators) diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go new file mode 100644 index 0000000..94552d7 --- /dev/null +++ b/player/machine/pastnote.go @@ -0,0 +1,17 @@ +package machine + +import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/voice/mixer" +) + +type pastNote[TPeriod Period] struct { + rc *render.Channel[TPeriod] +} + +func (p pastNote[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { + return p.rc.RenderAndTick(pc, centerAheadPan, details) +} diff --git a/player/machine/position.go b/player/machine/position.go new file mode 100644 index 0000000..8fb74ac --- /dev/null +++ b/player/machine/position.go @@ -0,0 +1,9 @@ +package machine + +import "github.com/gotracker/playback/index" + +type Position struct { + Order index.Order + Row index.Row + Tick int +} diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go new file mode 100644 index 0000000..ae55119 --- /dev/null +++ b/player/machine/settings/machinesettings.go @@ -0,0 +1,34 @@ +package settings + +import ( + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/oscillator" +) + +type ( + Period = voice.Period + Volume = voice.Volume + Panning = voice.Panning +) + +type FilterFactoryFunc func(instrument frequency.Frequency) (filter.Filter, error) + +type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + PeriodConverter song.PeriodCalculator[TPeriod] + GetFilterFactory func(name string, instrumentRate frequency.Frequency, params any) (filter.Filter, error) + GetVibratoFactory func() (oscillator.Oscillator, error) + GetTremoloFactory func() (oscillator.Oscillator, error) + GetPanbrelloFactory func() (oscillator.Oscillator, error) + VoiceFactory voice.VoiceFactory[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + OPL2Enabled bool + Quirks MachineQuirks +} + +type MachineQuirks struct { + PreviousPeriodUsesModifiedPeriod bool + PortaToNoteUsesModifiedPeriod bool + DoNotProcessEffectsOnMutedChannels bool +} diff --git a/player/machine/settings/usersettings.go b/player/machine/settings/usersettings.go new file mode 100644 index 0000000..86f7bdd --- /dev/null +++ b/player/machine/settings/usersettings.go @@ -0,0 +1,133 @@ +package settings + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" + "github.com/heucuva/optional" +) + +type UserSettings struct { + Tracer tracing.TracerWithClose + SongLoopCount int + Start struct { + Order optional.Value[index.Order] // default: based on song + Row optional.Value[index.Row] // default: 0 + Tempo int // 0 = based on song + BPM int // 0 = based on song + } + PlayUntil struct { + Order optional.Value[index.Order] // default: based on song + Row optional.Value[index.Row] // default: based on song + } + LongChannelOutput bool + IgnoreUnknownEffect bool + EnableNewNoteActions bool +} + +// Reset applies the defaults +// +// NOTE: does not reset the Tracer value +func (s *UserSettings) Reset() { + // don't touch the Tracer here + s.SongLoopCount = 0 + s.Start.Order.Reset() + s.Start.Row.Reset() + s.Start.Tempo = 0 + s.Start.BPM = 0 + s.PlayUntil.Order.Reset() + s.PlayUntil.Row.Reset() + s.LongChannelOutput = true + s.IgnoreUnknownEffect = false + s.EnableNewNoteActions = true +} + +func (s *UserSettings) SetupTracingWithFilename(filename string) error { + var err error + s.Tracer, err = tracing.NewFromFilename(filename) + return err +} + +func (s *UserSettings) OutputTraces() { + if s.Tracer != nil { + s.Tracer.OutputTraces() + } +} + +func (s *UserSettings) CloseTracing() error { + if s.Tracer != nil { + return s.Tracer.Close() + } + return nil +} + +func (s UserSettings) SetTracingTick(order index.Order, row index.Row, tick int) { + if s.Tracer == nil { + return + } + + s.Tracer.SetTracingTick(order, row, tick) +} + +func (s UserSettings) Trace(op string) { + if s.Tracer == nil { + return + } + + s.Tracer.Trace(op) +} + +func (s UserSettings) TraceWithComment(op, commentFmt string, commentParams ...any) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceWithComment(op, commentFmt, commentParams...) +} + +func (s UserSettings) TraceValueChange(op string, prev, new any) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceValueChange(op, prev, new) +} + +func (s UserSettings) TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceValueChangeWithComment(op, prev, new, commentFmt, commentParams...) +} + +func (s UserSettings) TraceChannel(ch index.Channel, op string) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceChannel(ch, op) +} + +func (s UserSettings) TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceChannelWithComment(ch, op, commentFmt, commentParams...) +} + +func (s UserSettings) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceChannelValueChange(ch, op, prev, new) +} + +func (s UserSettings) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceChannelValueChangeWithComment(ch, op, prev, new, commentFmt, commentParams...) +} diff --git a/player/machine/ticker.go b/player/machine/ticker.go new file mode 100644 index 0000000..39b35eb --- /dev/null +++ b/player/machine/ticker.go @@ -0,0 +1,263 @@ +package machine + +import ( + "errors" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" + "github.com/heucuva/optional" +) + +type ticker struct { + settings tickerSettings + current Position + next struct { + row optional.Value[tickerRowBreak] + order optional.Value[index.Order] + } + songLoop struct { + current int + detect map[index.Order]struct{} + } +} + +type tickerRowBreak struct { + row index.Row + breakOrder bool +} + +type tickerSettings struct { + InitialOrder index.Order + InitialRow index.Row + SongLoopStartingOrder index.Order + SongLoopCount int + PlayUntilOrder optional.Value[index.Order] + PlayUntilRow optional.Value[index.Row] +} + +func initTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](t *ticker, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], settings tickerSettings) error { + t.settings = settings + t.current.Tick = 0 + t.current.Row = 0 + t.current.Order = 0 + t.next.row.Set(tickerRowBreak{ + row: settings.InitialRow, + breakOrder: false, + }) + t.next.order.Set(settings.InitialOrder) + + nextRow, nextOrder, err := advanceRowOrder(t, m) + if err != nil { + return err + } + + if row, set := nextRow.Get(); set { + t.current.Row = row + } + if order, set := nextOrder.Get(); set { + t.current.Order = order + } + + if t.songLoop.detect == nil { + t.songLoop.detect = make(map[index.Order]struct{}) + } + + t.songLoop.detect[t.current.Order] = struct{}{} + + m.us.SetTracingTick(t.current.Order, t.current.Row, t.current.Tick) + + if err := m.onOrderStart(); err != nil { + return err + } + + return m.onRowStart() +} + +func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](t *ticker, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + if err := m.onTick(); err != nil { + return err + } + + tick := t.current.Tick + 1 + row := t.current.Row + order := t.current.Order + rowAdvanced := false + orderAdvanced := false + done := false + if tick >= m.tempo { + tick = 0 + rowAdvanced = true + } + + if rowAdvanced { + if err := m.onRowEnd(); err != nil { + return err + } + + nextRow, nextOrder, err := advanceRowOrder(t, m) + if err != nil { + if !errors.Is(err, song.ErrStopSong) { + return err + } + + done = true + } + + if r, set := nextRow.Get(); set { + row = r + } + + if o, set := nextOrder.Get(); set { + order = o + orderAdvanced = true + + if err := m.onOrderEnd(); err != nil { + return err + } + } + } + + traceValueChangeWithComment(m, "tick", t.current.Tick, tick, "runTick") + traceValueChangeWithComment(m, "row", t.current.Row, row, "runTick") + traceValueChangeWithComment(m, "order", t.current.Order, order, "runTick") + t.current.Tick = tick + t.current.Row = row + t.current.Order = order + m.us.SetTracingTick(t.current.Order, t.current.Row, t.current.Tick) + + if !done { + o, oset := t.settings.PlayUntilOrder.Get() + r, rset := t.settings.PlayUntilRow.Get() + if oset || rset { + orderMatch := true + if oset { + orderMatch = (o == t.current.Order) + } + + rowMatch := true + if rset { + rowMatch = (r == t.current.Row) + } + + done = orderMatch && rowMatch + } + } + + if done { + return song.ErrStopSong + } + + if orderAdvanced { + if err := m.onOrderStart(); err != nil { + return err + } + } + if rowAdvanced { + if err := m.onRowStart(); err != nil { + return err + } + } + return nil +} + +func advanceRowOrder[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](t *ticker, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (optional.Value[index.Row], optional.Value[index.Order], error) { + row := int(t.current.Row) + rowUpdated := false + order := int(t.current.Order) + orderUpdated := false + + desiredRow, rowSet := t.next.row.Get() + desiredOrder, orderSet := t.next.order.Get() + + if rowSet && orderSet { + row = int(desiredRow.row) + rowUpdated = true + order = int(desiredOrder) + orderUpdated = true + } else if rowSet { + row = int(desiredRow.row) + rowUpdated = true + if desiredRow.breakOrder { + order++ + orderUpdated = true + } + } else if orderSet { + order = int(desiredOrder) + orderUpdated = true + row = 0 + rowUpdated = true + } else { + row++ + rowUpdated = true + } + + t.next.row.Reset() + t.next.order.Reset() + + orderScanMax := len(m.songData.GetOrderList()) + orderScanIter := 0 + forceLoopDetect := false +orderScan: + if orderScanIter >= orderScanMax { + order = int(t.settings.SongLoopStartingOrder) + orderUpdated = true + forceLoopDetect = true + } + + pat, err := m.songData.GetPatternByOrder(index.Order(order)) + if err != nil { + if errors.Is(err, index.ErrNextPattern) { + order++ + orderUpdated = true + orderScanIter++ + // don't update row here + goto orderScan + } + var ( + emptyRow optional.Value[index.Row] + emptyOrder optional.Value[index.Order] + ) + return emptyRow, emptyOrder, err + } + + if row >= pat.NumRows() { + order++ + orderUpdated = true + orderScanIter++ + row = 0 + rowUpdated = true + goto orderScan + } + + if orderUpdated && (forceLoopDetect || order != int(t.current.Order)) && t.settings.SongLoopCount >= 0 { + if _, found := t.songLoop.detect[index.Order(order)]; found { + t.songLoop.current++ + if t.settings.SongLoopCount >= 0 && t.songLoop.current >= t.settings.SongLoopCount { + var ( + emptyRow optional.Value[index.Row] + emptyOrder optional.Value[index.Order] + ) + return emptyRow, emptyOrder, song.ErrStopSong + } + + // allow and clear + t.songLoop.detect = nil + } + if t.songLoop.detect == nil { + t.songLoop.detect = make(map[index.Order]struct{}) + } + t.songLoop.detect[index.Order(order)] = struct{}{} + } + + var outRow optional.Value[index.Row] + if rowUpdated { + outRow.Set(index.Row(row)) + } + + var outOrder optional.Value[index.Order] + if orderUpdated { + outOrder.Set(index.Order(order)) + } + + return outRow, outOrder, nil +} diff --git a/player/render/channel.go b/player/render/channel.go index eb862df..21c30ed 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -1,45 +1,91 @@ package render import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/opl2" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/render" - - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/mixer" ) +type ChannelIntf interface { + ApplyFilter(dry volume.Matrix) volume.Matrix + GetPremixVolume() volume.Volume +} + // Channel is the important bits to make output to a particular downmixing channel work -type Channel struct { - ChannelNum int - Filter filter.Filter - GetSampleRate func() period.Frequency - SetGlobalVolume func(volume.Volume) - GetOPL2Chip func() render.OPL2Chip - ChannelVolume volume.Volume - LastGlobalVolume volume.Volume // this is the channel's version of the GlobalVolume +type Channel[TPeriod period.Period] struct { + PluginFilter filter.Filter + OutputFilter filter.Filter + GetOPL2Chip func() *opl2.Chip + GlobalVolume volume.Volume // this is the channel's version of the GlobalVolume + + v voice.Voice + vrem func() // function to call when voice is stopped/removed } -// ApplyFilter will apply the channel filter, if there is one. -func (oc *Channel) ApplyFilter(dry volume.Matrix) volume.Matrix { - if dry.Channels == 0 { - return volume.Matrix{} +func (c *Channel[TPeriod]) RenderAndTick(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { + if filt := c.PluginFilter; filt != nil { + filt.SetPlaybackRate(details.SampleRate) } - premix := oc.GetPremixVolume() - wet := dry.Apply(premix) - if oc.Filter != nil { - return oc.Filter.Filter(wet) + + if filt := c.OutputFilter; filt != nil { + filt.SetPlaybackRate(details.SampleRate) } - return wet + + data, err := voice.RenderAndTick(c.v, pc, centerAheadPan, details, c) + if err != nil { + return nil, err + } + + if data == nil { + return nil, nil + } + return data, nil } -// GetPremixVolume returns the premix volume of the output channel -func (oc *Channel) GetPremixVolume() volume.Volume { - return oc.LastGlobalVolume * oc.ChannelVolume +func (c Channel[TPeriod]) GetVoice() voice.Voice { + return c.v } -// SetFilterEnvelopeValue updates the filter on the channel with the new envelope value -func (oc *Channel) SetFilterEnvelopeValue(envVal int8) { - if oc.Filter != nil { - oc.Filter.UpdateEnv(envVal) +func (c *Channel[TPeriod]) StartVoice(v voice.Voice, vrem func()) { + c.StopVoice() + c.v = v + c.vrem = vrem +} + +func (c *Channel[TPeriod]) StopVoice() { + if c.v == nil { + return + } + + var fn func() + fn, c.vrem = c.vrem, nil + c.v.Stop() + + if fn != nil { + fn() } } + +// ApplyFilter will apply the channel filter, if there is one. +func (c *Channel[TPeriod]) ApplyFilter(dry volume.Matrix) volume.Matrix { + if dry.Channels == 0 { + return dry + } + wet := dry + if c.PluginFilter != nil { + wet = c.PluginFilter.Filter(wet) + } + wet = wet.Apply(c.GlobalVolume) + if c.OutputFilter != nil { + wet = c.OutputFilter.Filter(wet) + } + return wet +} + +func (Channel[TPeriod]) SetFilterEnvelopeValue(envVal uint8) { +} diff --git a/player/render/opl2intf.go b/player/render/opl2intf.go deleted file mode 100644 index a960bd2..0000000 --- a/player/render/opl2intf.go +++ /dev/null @@ -1,11 +0,0 @@ -package render - -import ( - "github.com/gotracker/opl2" - "github.com/gotracker/playback/voice/render" -) - -// NewOPL2Chip generates a new OPL2 chip -func NewOPL2Chip(rate uint32) render.OPL2Chip { - return opl2.NewChip(rate, false) -} diff --git a/player/render/render.go b/player/render/render.go index 4d3b29a..012504c 100644 --- a/player/render/render.go +++ b/player/render/render.go @@ -7,13 +7,13 @@ import ( ) // RowDisplay is an array of ChannelDisplays -type RowDisplay[TChannelData song.ChannelData] struct { +type RowDisplay[TChannelData song.ChannelDataIntf] struct { Channels []TChannelData longFormat bool } // NewRowText creates an array of ChannelDisplay information -func NewRowText[TChannelData song.ChannelData](channels int, longFormat bool) RowDisplay[TChannelData] { +func NewRowText[TChannelData song.ChannelDataIntf](channels int, longFormat bool) RowDisplay[TChannelData] { rd := RowDisplay[TChannelData]{ Channels: make([]TChannelData, channels), longFormat: longFormat, @@ -40,11 +40,9 @@ func (rt RowDisplay[TChannelData]) String(options ...any) string { return "|" + strings.Join(items, "|") + "|" } -type RowStringer interface { - String(options ...any) string -} +type RowStringer = song.RowStringer -//RowRender is the final output of a single row's data +// RowRender is the final output of a single row's data type RowRender struct { Order int Row int diff --git a/player/sampler/sampler.go b/player/sampler/sampler.go index 89aa3ed..6cedefc 100644 --- a/player/sampler/sampler.go +++ b/player/sampler/sampler.go @@ -2,22 +2,24 @@ package sampler import ( "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/output" ) // Sampler is a container of sampler/mixer settings type Sampler struct { SampleRate int - BaseClockRate period.Frequency + BaseClockRate frequency.Frequency + OnGenerate func(premix *output.PremixData) mixer mixing.Mixer } // NewSampler returns a new sampler object based on the input settings -func NewSampler(samplesPerSec, channels int, baseClockRate period.Frequency) *Sampler { +func NewSampler(samplesPerSec, channels int, onGenerate func(premix *output.PremixData)) *Sampler { s := Sampler{ - SampleRate: samplesPerSec, - BaseClockRate: baseClockRate, + SampleRate: samplesPerSec, + OnGenerate: onGenerate, mixer: mixing.Mixer{ Channels: channels, }, @@ -25,13 +27,6 @@ func NewSampler(samplesPerSec, channels int, baseClockRate period.Frequency) *Sa return &s } -// GetSamplerSpeed returns the current sampler speed -// which is a product of the base sampler clock rate and the inverse -// of the output render rate (the sample rate) -func (s *Sampler) GetSamplerSpeed() float32 { - return float32(s.BaseClockRate) / float32(s.SampleRate) -} - // Mixer returns a pointer to the current mixer object func (s *Sampler) Mixer() *mixing.Mixer { return &s.mixer diff --git a/player/state/active.go b/player/state/active.go deleted file mode 100644 index 0894192..0000000 --- a/player/state/active.go +++ /dev/null @@ -1,147 +0,0 @@ -package state - -import ( - "time" - - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" -) - -// Active is the active state of a channel -type Active struct { - Playback - Voice voice.Voice - PeriodDelta period.PeriodDelta -} - -// Reset sets the active state to defaults -func (a *Active) Reset() { - if v := a.Voice; v != nil { - v.Release() - a.Voice = nil - } - a.PeriodDelta = 0 - a.Playback.Reset() -} - -// Clone clones the active state so that various interfaces do not collide -func (a *Active) Clone() *Active { - var c Active = *a - if a.Voice != nil { - c.Voice = a.Voice.Clone() - } - - return &c -} - -// Transitions the active state so that various interfaces do not collide -func (a *Active) Transition() *Active { - var c *Active - if a.Voice != nil && !a.Voice.IsDone() { - c = &Active{ - Playback: a.Playback, - Voice: a.Voice, - PeriodDelta: a.PeriodDelta, - } - } - - a.Reset() - a.Voice = nil - - return c -} - -type RenderDetails struct { - Mix *mixing.Mixer - Panmixer mixing.PanMixer - SamplerSpeed float32 - Samples int - Duration time.Duration -} - -// RenderStatesTogether renders a channel's series of sample data for a the provided number of samples -func RenderStatesTogether(activeState *Active, pastNotes []*Active, details RenderDetails) []mixing.Data { - var mixData []mixing.Data - - centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) - - if activeState != nil { - if data := activeState.renderState(centerAheadPan, details); data != nil { - mixData = append(mixData, *data) - } - } - - for _, pn := range pastNotes { - if pn != nil { - if data := pn.renderState(centerAheadPan, details); data != nil { - mixData = append(mixData, *data) - } - } - } - - return mixData -} - -func (a *Active) renderState(centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { - if a.Period == nil || a.Volume == 0 { - return nil - } - - ncv := a.Voice - if ncv == nil || ncv.IsDone() { - return nil - } - - // Commit the playback settings to the note-control - voice.SetPeriod(ncv, a.Period) - voice.SetVolume(ncv, a.Volume) - voice.SetPos(ncv, a.Pos) - voice.SetPan(ncv, a.Pan) - - voice.SetPeriodDelta(ncv, a.PeriodDelta) - - // the period might be updated by the auto-vibrato system, here - ncv.Advance(details.Duration) - - if !ncv.IsActive() { - return nil - } - - sampler := ncv.GetSampler(details.SamplerSpeed) - - if sampler == nil { - return nil - } - - // ... so grab the new value now. - period := voice.GetFinalPeriod(ncv) - pan := voice.GetFinalPan(ncv) - - // make a stand-alone data buffer for this channel for this tick - sampleData := mixing.SampleMixIn{ - Sample: sampler, - StaticVol: volume.Volume(1.0), - VolMatrix: centerAheadPan, - MixPos: 0, - MixLen: details.Samples, - } - - mixBuffer := details.Mix.NewMixBuffer(details.Samples) - mixBuffer.MixInSample(sampleData) - data := &mixing.Data{ - Data: mixBuffer, - Pan: pan, - Volume: volume.Volume(1.0), - Pos: 0, - SamplesLen: details.Samples, - } - - a.Pos = voice.GetPos(ncv) - samplerAdd := float32(period.GetSamplerAdd(float64(details.SamplerSpeed))) - a.Pos.Add(samplerAdd * float32(details.Samples)) - - return data -} diff --git a/player/state/channel.go b/player/state/channel.go deleted file mode 100644 index 416056e..0000000 --- a/player/state/channel.go +++ /dev/null @@ -1,473 +0,0 @@ -package state - -import ( - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/player/render" - voiceImpl "github.com/gotracker/playback/player/voice" - "github.com/gotracker/playback/song" - "github.com/heucuva/optional" -) - -type NoteTrigger struct { - NoteAction note.Action - Tick int -} - -type VolOp[TMemory, TChannelData any] interface { - Process(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error -} - -type NoteOp[TMemory, TChannelData any] interface { - Process(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error -} - -type PeriodUpdateFunc func(period.Period) - -type SemitoneSetterFactory[TMemory, TChannelData any] func(note.Semitone, PeriodUpdateFunc) NoteOp[TMemory, TChannelData] - -// ChannelState is the state of a single channel -type ChannelState[TMemory, TChannelData any] struct { - activeState Active - targetState Playback - prevState Active - - ActiveEffect playback.Effect - - s song.Data - txn ChannelDataTransaction[TMemory, TChannelData] - prevTxn ChannelDataTransaction[TMemory, TChannelData] - - SemitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData] - - StoredSemitone note.Semitone // from pattern, unmodified, current note - PortaTargetPeriod optional.Value[period.Period] - Trigger optional.Value[NoteTrigger] - RetriggerCount uint8 - Memory *TMemory - freezePlayback bool - Semitone note.Semitone // from TargetSemitone, modified further, used in period calculations - UseTargetPeriod bool - periodOverride period.Period - UsePeriodOverride bool - volumeActive bool - PanEnabled bool - NewNoteAction note.Action - - PastNotes *PastNotesProcessor - RenderChannel *render.Channel -} - -// WillTriggerOn returns true if a note will trigger on the tick specified -func (cs *ChannelState[TMemory, TChannelData]) WillTriggerOn(tick int) (bool, note.Action) { - if trigger, ok := cs.Trigger.Get(); ok { - return trigger.Tick == tick, trigger.NoteAction - } - - return false, note.ActionContinue -} - -// AdvanceRow will update the current state to make room for the next row's state data -func (cs *ChannelState[TMemory, TChannelData]) AdvanceRow(txn ChannelDataTransaction[TMemory, TChannelData]) { - cs.prevState = cs.activeState - cs.targetState = cs.activeState.Playback - cs.Trigger.Reset() - cs.RetriggerCount = 0 - cs.activeState.PeriodDelta = 0 - - cs.UseTargetPeriod = false - cs.prevTxn = cs.txn - cs.txn = txn -} - -// RenderRowTick renders a channel's row data for a single tick -func (cs *ChannelState[TMemory, TChannelData]) RenderRowTick(details RenderDetails, pastNotes []*Active) ([]mixing.Data, error) { - if cs.PlaybackFrozen() { - return nil, nil - } - - mixData := RenderStatesTogether(&cs.activeState, pastNotes, details) - - return mixData, nil -} - -// ResetStates resets the channel's internal states -func (cs *ChannelState[TMemory, TChannelData]) ResetStates() { - cs.activeState.Reset() - cs.targetState.Reset() - cs.prevState.Reset() -} - -func (cs *ChannelState[TMemory, TChannelData]) GetActiveEffect() playback.Effect { - return cs.ActiveEffect -} - -func (cs *ChannelState[TMemory, TChannelData]) SetActiveEffect(e playback.Effect) { - cs.ActiveEffect = e -} - -// FreezePlayback suspends mixer progression on the channel -func (cs *ChannelState[TMemory, TChannelData]) FreezePlayback() { - cs.freezePlayback = true -} - -// UnfreezePlayback resumes mixer progression on the channel -func (cs *ChannelState[TMemory, TChannelData]) UnfreezePlayback() { - cs.freezePlayback = false -} - -// PlaybackFrozen returns true if the mixer progression for the channel is suspended -func (cs ChannelState[TMemory, TChannelData]) PlaybackFrozen() bool { - return cs.freezePlayback -} - -// ResetRetriggerCount sets the retrigger count to 0 -func (cs *ChannelState[TMemory, TChannelData]) ResetRetriggerCount() { - cs.RetriggerCount = 0 -} - -// GetMemory returns the interface to the custom effect memory module -func (cs *ChannelState[TMemory, TChannelData]) GetMemory() *TMemory { - return cs.Memory -} - -// SetMemory sets the custom effect memory interface -func (cs *ChannelState[TMemory, TChannelData]) SetMemory(mem *TMemory) { - cs.Memory = mem -} - -// GetActiveVolume returns the current active volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) GetActiveVolume() volume.Volume { - return cs.activeState.Volume -} - -// SetActiveVolume sets the active volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) SetActiveVolume(vol volume.Volume) { - if vol != volume.VolumeUseInstVol { - cs.activeState.Volume = vol - } -} - -func (cs *ChannelState[TMemory, TChannelData]) SetSongDataInterface(s song.Data) { - cs.s = s -} - -// GetData returns the interface to the current channel song pattern data -func (cs *ChannelState[TMemory, TChannelData]) GetData() *TChannelData { - if cs.txn == nil { - return nil - } - - return cs.txn.GetData() -} - -func (cs *ChannelState[TMemory, TChannelData]) SetData(cdata *TChannelData) error { - if cs.txn == nil { - return nil - } - - return cs.txn.SetData(cdata, cs.s, cs) -} - -func (cs *ChannelState[TMemory, TChannelData]) GetTxn() ChannelDataTransaction[TMemory, TChannelData] { - return cs.txn -} - -// GetPortaTargetPeriod returns the current target portamento (to note) sampler period -func (cs *ChannelState[TMemory, TChannelData]) GetPortaTargetPeriod() period.Period { - if p, ok := cs.PortaTargetPeriod.Get(); ok { - return p - } - return nil -} - -// SetPortaTargetPeriod sets the current target portamento (to note) sampler period -func (cs *ChannelState[TMemory, TChannelData]) SetPortaTargetPeriod(period period.Period) { - if period != nil { - cs.PortaTargetPeriod.Set(period) - } else { - cs.PortaTargetPeriod.Reset() - } -} - -// GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) GetTargetPeriod() period.Period { - return cs.targetState.Period -} - -// SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) SetTargetPeriod(period period.Period) { - cs.targetState.Period = period -} - -// GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) GetPeriodOverride() period.Period { - return cs.periodOverride -} - -// SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) SetPeriodOverride(period period.Period) { - cs.periodOverride = period - cs.UsePeriodOverride = true -} - -// SetPeriodDelta sets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TMemory, TChannelData]) SetPeriodDelta(delta period.PeriodDelta) { - cs.activeState.PeriodDelta = delta -} - -// GetPeriodDelta gets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TMemory, TChannelData]) GetPeriodDelta() period.PeriodDelta { - return cs.activeState.PeriodDelta -} - -// SetVolumeActive enables or disables the sample of the instrument -func (cs *ChannelState[TMemory, TChannelData]) SetVolumeActive(on bool) { - cs.volumeActive = on -} - -// GetInstrument returns the interface to the active instrument -func (cs *ChannelState[TMemory, TChannelData]) GetInstrument() *instrument.Instrument { - return cs.activeState.Instrument -} - -// SetInstrument sets the interface to the active instrument -func (cs *ChannelState[TMemory, TChannelData]) SetInstrument(inst *instrument.Instrument) { - cs.activeState.Instrument = inst - if inst != nil { - if inst == cs.prevState.Instrument { - cs.activeState.Voice = cs.prevState.Voice - } else { - cs.activeState.Voice = voiceImpl.New(inst, cs.RenderChannel) - } - } -} - -// GetVoice returns the active voice interface -func (cs *ChannelState[TMemory, TChannelData]) GetVoice() voice.Voice { - return cs.activeState.Voice -} - -// GetTargetInst returns the interface to the soon-to-be-committed active instrument (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) GetTargetInst() *instrument.Instrument { - return cs.targetState.Instrument -} - -// SetTargetInst sets the soon-to-be-committed active instrument (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) SetTargetInst(inst *instrument.Instrument) { - cs.targetState.Instrument = inst -} - -// GetPrevInst returns the interface to the last row's active instrument -func (cs *ChannelState[TMemory, TChannelData]) GetPrevInst() *instrument.Instrument { - return cs.prevState.Instrument -} - -// GetPrevVoice returns the interface to the last row's active voice -func (cs *ChannelState[TMemory, TChannelData]) GetPrevVoice() voice.Voice { - return cs.prevState.Voice -} - -// GetNoteSemitone returns the note semitone for the channel -func (cs *ChannelState[TMemory, TChannelData]) GetNoteSemitone() note.Semitone { - return cs.StoredSemitone -} - -// GetTargetPos returns the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TMemory, TChannelData]) GetTargetPos() sampling.Pos { - return cs.targetState.Pos -} - -// SetTargetPos sets the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TMemory, TChannelData]) SetTargetPos(pos sampling.Pos) { - cs.targetState.Pos = pos -} - -// GetPeriod returns the current sampler period of the active instrument -func (cs *ChannelState[TMemory, TChannelData]) GetPeriod() period.Period { - return cs.activeState.Period -} - -// SetPeriod sets the current sampler period of the active instrument -func (cs *ChannelState[TMemory, TChannelData]) SetPeriod(period period.Period) { - cs.activeState.Period = period -} - -// GetPos returns the sample position of the active instrument -func (cs *ChannelState[TMemory, TChannelData]) GetPos() sampling.Pos { - return cs.activeState.Pos -} - -// SetPos sets the sample position of the active instrument -func (cs *ChannelState[TMemory, TChannelData]) SetPos(pos sampling.Pos) { - cs.activeState.Pos = pos -} - -// SetNotePlayTick sets the tick on which the note will retrigger -func (cs *ChannelState[TMemory, TChannelData]) SetNotePlayTick(enabled bool, action note.Action, tick int) { - if enabled { - cs.Trigger.Set(NoteTrigger{ - NoteAction: action, - Tick: tick, - }) - } else { - cs.Trigger.Reset() - } -} - -// GetRetriggerCount returns the current count of the retrigger counter -func (cs *ChannelState[TMemory, TChannelData]) GetRetriggerCount() uint8 { - return cs.RetriggerCount -} - -// SetRetriggerCount sets the current count of the retrigger counter -func (cs *ChannelState[TMemory, TChannelData]) SetRetriggerCount(cnt uint8) { - cs.RetriggerCount = cnt -} - -// SetPanEnabled activates or deactivates the panning. If enabled, then pan updates work (see SetPan) -func (cs *ChannelState[TMemory, TChannelData]) SetPanEnabled(on bool) { - cs.PanEnabled = on -} - -// SetPan sets the active panning value of the channel -func (cs *ChannelState[TMemory, TChannelData]) SetPan(pan panning.Position) { - if cs.PanEnabled { - cs.activeState.Pan = pan - } -} - -// GetPan gets the active panning value of the channel -func (cs *ChannelState[TMemory, TChannelData]) GetPan() panning.Position { - return cs.activeState.Pan -} - -// SetTargetSemitone sets the target semitone for the channel -func (cs *ChannelState[TMemory, TChannelData]) SetTargetSemitone(st note.Semitone) { - if cs.txn != nil { - cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, cs.SetTargetPeriod)) - } -} - -func (cs *ChannelState[TMemory, TChannelData]) SetOverrideSemitone(st note.Semitone) { - if cs.txn != nil { - cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, cs.SetPeriodOverride)) - } -} - -// SetStoredSemitone sets the stored semitone for the channel -func (cs *ChannelState[TMemory, TChannelData]) SetStoredSemitone(st note.Semitone) { - cs.StoredSemitone = st -} - -// SetRenderChannel sets the output channel for the channel -func (cs *ChannelState[TMemory, TChannelData]) SetRenderChannel(outputCh *render.Channel) { - cs.RenderChannel = outputCh -} - -// GetRenderChannel returns the output channel for the channel -func (cs *ChannelState[TMemory, TChannelData]) GetRenderChannel() *render.Channel { - return cs.RenderChannel -} - -// SetGlobalVolume sets the last-known global volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) SetGlobalVolume(gv volume.Volume) { - cs.RenderChannel.LastGlobalVolume = gv - cs.RenderChannel.SetGlobalVolume(gv) -} - -// SetChannelVolume sets the channel volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) SetChannelVolume(cv volume.Volume) { - cs.RenderChannel.ChannelVolume = cv -} - -// GetChannelVolume gets the channel volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) GetChannelVolume() volume.Volume { - return cs.RenderChannel.ChannelVolume -} - -// SetEnvelopePosition sets the envelope position for the active instrument -func (cs *ChannelState[TMemory, TChannelData]) SetEnvelopePosition(ticks int) { - if nc := cs.GetVoice(); nc != nil { - voice.SetVolumeEnvelopePosition(nc, ticks) - voice.SetPanEnvelopePosition(nc, ticks) - voice.SetPitchEnvelopePosition(nc, ticks) - voice.SetFilterEnvelopePosition(nc, ticks) - } -} - -// TransitionActiveToPastState will transition the current active state to the 'past' state -// and will activate the specified New-Note Action on it -func (cs *ChannelState[TMemory, TChannelData]) TransitionActiveToPastState() { - if cs.PastNotes != nil { - switch cs.NewNoteAction { - case note.ActionCut: - // reset at end - - case note.ActionContinue: - // nothing - pn := cs.activeState.Clone() - if nc := pn.Voice; nc != nil { - cs.PastNotes.Add(cs.RenderChannel.ChannelNum, pn) - } - - case note.ActionRelease: - pn := cs.activeState.Clone() - if nc := pn.Voice; nc != nil { - nc.Release() - cs.PastNotes.Add(cs.RenderChannel.ChannelNum, pn) - } - - case note.ActionFadeout: - pn := cs.activeState.Clone() - if nc := pn.Voice; nc != nil { - nc.Release() - nc.Fadeout() - cs.PastNotes.Add(cs.RenderChannel.ChannelNum, pn) - } - } - } - cs.activeState.Reset() -} - -// DoPastNoteEffect performs an action on all past-note playbacks associated with the channel -func (cs *ChannelState[TMemory, TChannelData]) DoPastNoteEffect(action note.Action) { - cs.PastNotes.Do(cs.RenderChannel.ChannelNum, action) -} - -// SetNewNoteAction sets the New-Note Action on the channel -func (cs *ChannelState[TMemory, TChannelData]) SetNewNoteAction(nna note.Action) { - cs.NewNoteAction = nna -} - -// GetNewNoteAction gets the New-Note Action on the channel -func (cs *ChannelState[TMemory, TChannelData]) GetNewNoteAction() note.Action { - return cs.NewNoteAction -} - -// SetVolumeEnvelopeEnable sets the enable flag on the active volume envelope -func (cs *ChannelState[TMemory, TChannelData]) SetVolumeEnvelopeEnable(enabled bool) { - voice.EnableVolumeEnvelope(cs.activeState.Voice, enabled) -} - -// SetPanningEnvelopeEnable sets the enable flag on the active panning envelope -func (cs *ChannelState[TMemory, TChannelData]) SetPanningEnvelopeEnable(enabled bool) { - voice.EnablePanEnvelope(cs.activeState.Voice, enabled) -} - -// SetPitchEnvelopeEnable sets the enable flag on the active pitch/filter envelope -func (cs *ChannelState[TMemory, TChannelData]) SetPitchEnvelopeEnable(enabled bool) { - voice.EnablePitchEnvelope(cs.activeState.Voice, enabled) -} - -func (cs *ChannelState[TMemory, TChannelData]) NoteCut() { - cs.activeState.Period = nil -} diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go deleted file mode 100644 index a539fca..0000000 --- a/player/state/channel_transaction.go +++ /dev/null @@ -1,144 +0,0 @@ -package state - -import ( - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/song" - "github.com/heucuva/optional" -) - -type ChannelDataTransaction[TMemory, TChannelData any] interface { - GetData() *TChannelData - SetData(data *TChannelData, s song.Data, cs *ChannelState[TMemory, TChannelData]) error - - CommitPreRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error - CommitRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error - CommitPostRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error - - CommitPreTick(p playback.Playback, cs *ChannelState[TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error - CommitTick(p playback.Playback, cs *ChannelState[TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error - CommitPostTick(p playback.Playback, cs *ChannelState[TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error - - AddVolOp(op VolOp[TMemory, TChannelData]) - AddNoteOp(op NoteOp[TMemory, TChannelData]) -} - -type ChannelDataActions struct { - NoteAction optional.Value[note.Action] - NoteCalcST optional.Value[note.Semitone] - - TargetPos optional.Value[sampling.Pos] - TargetInst optional.Value[*instrument.Instrument] - TargetPeriod optional.Value[period.Period] - TargetStoredSemitone optional.Value[note.Semitone] - TargetNewNoteAction optional.Value[note.Action] - TargetVolume optional.Value[volume.Volume] -} - -type ChannelDataConverter[TMemory, TChannelData any] interface { - Process(out *ChannelDataActions, data *TChannelData, s song.Data, cs *ChannelState[TMemory, TChannelData]) error -} - -type ChannelDataTxnHelper[TMemory, TChannelData any, TChannelDataConverter ChannelDataConverter[TMemory, TChannelData]] struct { - Data *TChannelData - - ChannelDataActions - - VolOps []VolOp[TMemory, TChannelData] - NoteOps []NoteOp[TMemory, TChannelData] -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) GetData() *TChannelData { - return d.Data -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) SetData(cd *TChannelData, s song.Data, cs *ChannelState[TMemory, TChannelData]) error { - d.Data = cd - - var converter TChannelDataConverter - return converter.Process(&d.ChannelDataActions, cd, s, cs) -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitPreRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitPostRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitPreTick(p playback.Playback, cs *ChannelState[TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { - // pre-effect - if err := d.ProcessVolOps(p, cs); err != nil { - return err - } - if err := d.ProcessNoteOps(p, cs); err != nil { - return err - } - - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitTick(p playback.Playback, cs *ChannelState[TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { - if err := playback.DoEffect[TMemory, TChannelData](cs.ActiveEffect, cs, p, currentTick, lastTick); err != nil { - return err - } - - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitPostTick(p playback.Playback, cs *ChannelState[TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { - // post-effect - if err := d.ProcessVolOps(p, cs); err != nil { - return err - } - if err := d.ProcessNoteOps(p, cs); err != nil { - return err - } - - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) AddVolOp(op VolOp[TMemory, TChannelData]) { - d.VolOps = append(d.VolOps, op) -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) ProcessVolOps(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error { - for _, op := range d.VolOps { - if op == nil { - continue - } - if err := op.Process(p, cs); err != nil { - return err - } - } - d.VolOps = nil - - return nil -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) AddNoteOp(op NoteOp[TMemory, TChannelData]) { - d.NoteOps = append(d.NoteOps, op) -} - -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error { - for _, op := range d.NoteOps { - if op == nil { - continue - } - if err := op.Process(p, cs); err != nil { - return err - } - } - d.NoteOps = nil - - return nil -} diff --git a/player/state/pastnotes.go b/player/state/pastnotes.go deleted file mode 100644 index ddcf971..0000000 --- a/player/state/pastnotes.go +++ /dev/null @@ -1,126 +0,0 @@ -package state - -import ( - "github.com/gotracker/playback/note" - "github.com/heucuva/optional" -) - -type pastNote struct { - ch int - activeState *Active -} - -func (pn *pastNote) IsValid() bool { - return pn.activeState.Voice != nil && !pn.activeState.Voice.IsDone() -} - -type PastNotesProcessor struct { - order []pastNote - max optional.Value[int] - maxPerCh optional.Value[int] -} - -func (p *PastNotesProcessor) Add(ch int, data *Active) { - if data == nil { - return - } - - if max, ok := p.max.Get(); ok { - if c := len(p.order) - max; c > 0 { - o := p.order[0:c] - p.order = p.order[c:] - - for _, pn := range o { - pn.activeState.Reset() - } - } - } - - cl := pastNote{ - ch: ch, - activeState: data, - } - - p.order = append(p.order, cl) -} - -func (p *PastNotesProcessor) Do(ch int, action note.Action) { - if action == note.ActionContinue { - return - } - - for _, pn := range p.order { - if pn.ch != ch { - continue - } - - if !pn.IsValid() { - continue - } - - switch action { - case note.ActionCut: - pn.activeState.Reset() - case note.ActionRelease: - pn.activeState.Voice.Release() - case note.ActionFadeout: - pn.activeState.Voice.Release() - pn.activeState.Voice.Fadeout() - } - } -} - -func (p *PastNotesProcessor) Update() { - var nl []pastNote - for _, o := range p.order { - if !o.IsValid() { - o.activeState.Reset() - continue - } - - nl = append(nl, o) - } - p.order = nl -} - -func (p *PastNotesProcessor) GetNotesForChannel(ch int) []*Active { - var pastNotes []*Active - for _, pn := range p.order { - if pn.ch != ch { - continue - } - - if !pn.IsValid() { - continue - } - - pastNotes = append(pastNotes, pn.activeState) - if max, ok := p.maxPerCh.Get(); ok { - if c := len(pastNotes) - max; c > 0 { - o := pastNotes[0:c] - pastNotes = pastNotes[c:] - - for _, pn := range o { - pn.Reset() - } - } - } - } - return pastNotes -} - -func (p *PastNotesProcessor) SetMax(max int) { - p.max.Set(max) -} - -func (p *PastNotesProcessor) ClearMax() { - p.max.Reset() -} - -func (p *PastNotesProcessor) SetMaxPerChannel(max int) { - p.maxPerCh.Set(max) -} - -func (p *PastNotesProcessor) ClearMaxPerChannel() { - p.maxPerCh.Reset() -} diff --git a/player/state/playback.go b/player/state/playback.go deleted file mode 100644 index b1ec8de..0000000 --- a/player/state/playback.go +++ /dev/null @@ -1,27 +0,0 @@ -package state - -import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/period" -) - -// Playback is the information needed to make an instrument play -type Playback struct { - Instrument *instrument.Instrument - Period period.Period - Volume volume.Volume - Pos sampling.Pos - Pan panning.Position -} - -// Reset sets the render state to defaults -func (p *Playback) Reset() { - p.Instrument = nil - p.Period = nil - p.Pos = sampling.Pos{} - p.Pan = panning.CenterAhead -} diff --git a/player/tick.go b/player/tick.go deleted file mode 100644 index 452d11d..0000000 --- a/player/tick.go +++ /dev/null @@ -1,14 +0,0 @@ -package player - -// Tickable is an interface which exposes the OnTick call -type Tickable interface { - OnTick() error -} - -// DoTick calls the OnTick() function on the interface, if possible -func DoTick(t Tickable) error { - if t != nil { - return t.OnTick() - } - return nil -} diff --git a/player/tracker.go b/player/tracker.go deleted file mode 100644 index a336c84..0000000 --- a/player/tracker.go +++ /dev/null @@ -1,231 +0,0 @@ -package player - -import ( - "errors" - "os" - "time" - - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/sampler" - voiceRender "github.com/gotracker/playback/voice/render" -) - -// Premixable is an interface to getting the premix data from the tracker -type Premixable interface { - GetPremixData() (*output.PremixData, error) -} - -// Tracker is an extensible music tracker -type Tracker struct { - BaseClockRate period.Frequency - Tickable Tickable - Premixable Premixable - Traceable Traceable - - s *sampler.Sampler - opl2 voiceRender.OPL2Chip - - globalVolume volume.Volume - mixerVolume volume.Volume - - ignoreUnknownEffect feature.IgnoreUnknownEffect - tracingFile *os.File - tracingState tracingState - outputChannels map[int]*render.Channel -} - -func (t *Tracker) Close() { - if t.tracingState.c != nil { - close(t.tracingState.c) - } - if t.tracingFile != nil { - t.tracingFile.Close() - } - t.tracingState.wg.Wait() -} - -// Update runs processing on the tracker, producing premixed sound data -func (t *Tracker) Update(deltaTime time.Duration, out chan<- *output.PremixData) error { - premix, err := t.Generate(deltaTime) - if err != nil { - return err - } - - t.OutputTraces() - - if premix != nil && premix.Data != nil && len(premix.Data) != 0 { - out <- premix - } - - return nil -} - -// Generate runs processing on the tracker, then returns the premixed sound data (if possible) -func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) { - premix, err := t.renderTick() - if err != nil { - return nil, err - } - - if premix != nil { - if len(premix.Data) == 0 { - cd := mixing.ChannelData{ - mixing.Data{ - Data: nil, - Pan: panning.CenterAhead, - Volume: volume.Volume(0), - SamplesLen: premix.SamplesLen, - }, - } - premix.Data = append(premix.Data, cd) - } - return premix, nil - } - - return nil, nil -} - -// GetRenderChannel returns the output channel for the provided index `ch` -func (t *Tracker) GetRenderChannel(ch int, init func(ch int) *render.Channel) *render.Channel { - if t.outputChannels == nil { - t.outputChannels = make(map[int]*render.Channel) - } - - if oc, ok := t.outputChannels[ch]; ok { - return oc - } - oc := init(ch) - t.outputChannels[ch] = oc - return oc -} - -// GetSampleRate returns the sample rate of the sampler -func (t *Tracker) GetSampleRate() period.Frequency { - return period.Frequency(t.GetSampler().SampleRate) -} - -func (t *Tracker) renderTick() (*output.PremixData, error) { - if err := DoTick(t.Tickable); err != nil { - return nil, err - } - - premix, err := t.Premixable.GetPremixData() - if err != nil { - return nil, err - } - - if t.opl2 != nil { - rr := [1]mixing.Data{} - t.renderOPL2Tick(&rr[0], - t.s.Mixer(), - premix.SamplesLen) - premix.Data = append(premix.Data, rr[:]) - - // make room in the mixer for the OPL2 data - // effectively, we can do this by calculating the new number (+1) of channels from the mixer volume (channels = reciprocal of mixer volume): - // numChannels = (1/mv) + 1 - // then by taking the reciprocal of it: - // 1 / numChannels - // but that ends up being simplified to: - // mv / (mv + 1) - // and we get protection from div/0 in the process - provided, of course, that the mixerVolume is not exactly -1... - mv := premix.MixerVolume - premix.MixerVolume /= (mv + 1) - } - return premix, nil -} - -func (t *Tracker) renderOPL2Tick(mixerData *mixing.Data, mix *mixing.Mixer, tickSamples int) { - // make a stand-alone data buffer for this channel for this tick - data := mix.NewMixBuffer(tickSamples) - - opl2data := make([]int32, tickSamples) - - if opl2 := t.opl2; opl2 != nil { - opl2.GenerateBlock2(uint(tickSamples), opl2data) - } - - for i, s := range opl2data { - sv := volume.Volume(s) / 32768.0 - data[i].Assign(1, []volume.Volume{sv}) - } - *mixerData = mixing.Data{ - Data: data, - Pan: panning.CenterAhead, - Volume: t.globalVolume, - SamplesLen: tickSamples, - } -} - -// GetOPL2Chip returns the current song's OPL2 chip, if it's needed -func (t *Tracker) GetOPL2Chip() voiceRender.OPL2Chip { - return t.opl2 -} - -// SetOPL2Chip sets the current song's OPL2 chip -func (t *Tracker) SetOPL2Chip(opl2 voiceRender.OPL2Chip) { - t.opl2 = opl2 -} - -// SetupSampler configures the internal sampler -func (t *Tracker) SetupSampler(samplesPerSecond int, channels int) error { - t.s = sampler.NewSampler(samplesPerSecond, channels, t.BaseClockRate) - if t.s == nil { - return errors.New("NewSampler() returned nil") - } - - return nil -} - -// GetSampler returns the current sampler -func (t *Tracker) GetSampler() *sampler.Sampler { - return t.s -} - -// GetGlobalVolume returns the global volume value -func (t *Tracker) GetGlobalVolume() volume.Volume { - return t.globalVolume -} - -// SetGlobalVolume sets the global volume to the specified `vol` value -func (t *Tracker) SetGlobalVolume(vol volume.Volume) { - t.globalVolume = vol -} - -// GetMixerVolume returns the mixer volume value -func (t *Tracker) GetMixerVolume() volume.Volume { - return t.mixerVolume -} - -// SetMixerVolume sets the mixer volume to the specified `vol` value -func (t *Tracker) SetMixerVolume(vol volume.Volume) { - t.mixerVolume = vol -} - -// IgnoreUnknownEffect returns true if the tracker wants unknown effects to be ignored -func (t *Tracker) IgnoreUnknownEffect() bool { - return t.ignoreUnknownEffect.Enabled -} - -// Configure sets specified features -func (t *Tracker) Configure(features []feature.Feature) error { - for _, feat := range features { - switch f := feat.(type) { - case feature.IgnoreUnknownEffect: - t.ignoreUnknownEffect = f - case feature.EnableTracing: - var err error - t.tracingFile, err = os.Create(f.Filename) - if err != nil { - return err - } - } - } - return nil -} diff --git a/player/tracker_tracing.go b/player/tracker_tracing.go deleted file mode 100644 index 8f1e869..0000000 --- a/player/tracker_tracing.go +++ /dev/null @@ -1,133 +0,0 @@ -package player - -import ( - "errors" - "fmt" - "io" - "strings" - "sync" -) - -type tracingMsgFunc func() string - -type tracingState struct { - chMap map[int]*tracingChannelState - traces []tracingMsgFunc - c chan func(w io.Writer) - wg sync.WaitGroup -} - -type tracingChannelState struct { - traces []tracingMsgFunc -} - -func (t *Tracker) TraceChannel(ch int, msgFunc tracingMsgFunc) { - if t.tracingFile == nil { - return - } - - tc := t.tracingState.chMap[ch] - if tc == nil { - tc = &tracingChannelState{} - t.tracingState.chMap[ch] = tc - } - - tc.traces = append(tc.traces, msgFunc) -} - -func (t *Tracker) TraceTick(msgFunc tracingMsgFunc) { - if t.tracingFile == nil { - return - } - - t.tracingState.traces = append(t.tracingState.traces, msgFunc) -} - -type tracingColumn struct { - heading string - rows []string -} - -type TracingTable struct { - cols []*tracingColumn - name string - maxRows int -} - -func NewTracingTable(name string, headers ...string) TracingTable { - tt := TracingTable{ - name: name, - } - for _, h := range headers { - tt.cols = append(tt.cols, &tracingColumn{ - heading: h, - }) - } - return tt -} - -func (tt *TracingTable) AddRow(cols ...any) { - for i, col := range cols { - c := tt.cols[i] - c.rows = append(c.rows, fmt.Sprint(col)) - } - tt.maxRows++ -} - -func (tt TracingTable) Fprintln(w io.Writer, colSep string, withRowNums bool) error { - head := []string{tt.name} - for _, c := range tt.cols { - head = append(head, c.heading) - } - if _, err := fmt.Fprintln(w, strings.Join(head, colSep)); err != nil { - return err - } - - for r := 0; r < tt.maxRows; r++ { - numCols := len(tt.cols) - colStart := 0 - if withRowNums { - numCols++ - colStart++ - } - cols := []string{""} - if withRowNums { - cols[0] = fmt.Sprintf("[%d]", r+1) - } - for _, col := range tt.cols { - if r >= len(col.rows) { - return errors.New("not enough rows to satisfy TracingTable writer") - } - cols = append(cols, col.rows[r]) - } - if _, err := fmt.Fprintln(w, strings.Join(cols, colSep)); err != nil { - return err - } - } - - return nil -} - -type Traceable interface { - OutputTraces(out chan<- func(w io.Writer)) -} - -func (t *Tracker) OutputTraces() { - if t.tracingFile != nil && t.Traceable != nil { - if t.tracingState.c == nil { - t.tracingState.c = make(chan func(w io.Writer), 1000*1000) - go func() { - defer close(t.tracingState.c) - defer t.tracingFile.Close() - - t.tracingState.wg.Add(1) - defer t.tracingState.wg.Done() - - for tr := range t.tracingState.c { - tr(t.tracingFile) - } - }() - } - t.Traceable.OutputTraces(t.tracingState.c) - } -} diff --git a/player/voice/opl2.go b/player/voice/opl2.go deleted file mode 100644 index 434783b..0000000 --- a/player/voice/opl2.go +++ /dev/null @@ -1,299 +0,0 @@ -package voice - -import ( - "time" - - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/component" - "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/render" - - "github.com/gotracker/playback/instrument" -) - -// OPL2 is an OPL2 voice interface -type OPL2 interface { - voice.Voice - voice.FreqModulator - voice.AmpModulator - voice.VolumeEnveloper - voice.PitchEnveloper -} - -// OPL2Registers is a set of OPL operator configurations -type OPL2Registers component.OPL2Registers - -// OPLConfiguration is the information needed to configure an OPL2 voice -type OPLConfiguration struct { - Chip render.OPL2Chip - Channel int - C2SPD period.Frequency - InitialVolume volume.Volume - InitialPeriod period.Period - AutoVibrato voice.AutoVibrato - Data instrument.Data -} - -// == the actual opl2 voice == - -type opl2Voice struct { - c2spd period.Frequency - initialVolume volume.Volume - - active bool - keyOn bool - prevKeyOn bool - - fadeoutMode fadeout.Mode - - o component.OPL2 - amp component.AmpModulator - freq component.FreqModulator - volEnv component.VolumeEnvelope - pitchEnv component.PitchEnvelope -} - -// NewOPL2 creates a new OPL2 voice -func NewOPL2(config OPLConfiguration) voice.Voice { - v := opl2Voice{ - c2spd: config.C2SPD, - initialVolume: config.InitialVolume, - fadeoutMode: fadeout.ModeDisabled, - active: true, - } - - var regs component.OPL2Registers - - switch d := config.Data.(type) { - case *instrument.OPL2: - v.amp.Setup(1) - v.amp.ResetFadeoutValue(0) - v.volEnv.SetEnabled(false) - v.volEnv.Reset(nil) - v.pitchEnv.SetEnabled(false) - v.pitchEnv.Reset(nil) - regs.Mod.Reg20 = d.Modulator.GetReg20() - regs.Mod.Reg40 = d.Modulator.GetReg40() - regs.Mod.Reg60 = d.Modulator.GetReg60() - regs.Mod.Reg80 = d.Modulator.GetReg80() - regs.Mod.RegE0 = d.Modulator.GetRegE0() - regs.Car.Reg20 = d.Carrier.GetReg20() - regs.Car.Reg40 = d.Carrier.GetReg40() - regs.Car.Reg60 = d.Carrier.GetReg60() - regs.Car.Reg80 = d.Carrier.GetReg80() - regs.Car.RegE0 = d.Carrier.GetRegE0() - regs.RegC0 = d.GetRegC0() - default: - _ = d - } - - v.o.Setup(config.Chip, config.Channel, regs, config.C2SPD) - v.amp.SetVolume(config.InitialVolume) - v.freq.SetPeriod(config.InitialPeriod) - v.freq.SetAutoVibratoEnabled(config.AutoVibrato.Enabled) - if config.AutoVibrato.Enabled { - v.freq.ConfigureAutoVibrato(config.AutoVibrato) - v.freq.ResetAutoVibrato(config.AutoVibrato.Sweep) - } - - var o OPL2 = &v - return o -} - -// == Controller == - -func (v *opl2Voice) Attack() { - v.keyOn = true - v.amp.Attack() - v.freq.ResetAutoVibrato() - v.SetVolumeEnvelopePosition(0) - v.SetPitchEnvelopePosition(0) - -} - -func (v *opl2Voice) Release() { - v.keyOn = false - v.amp.Release() - v.o.Release() -} - -func (v *opl2Voice) Fadeout() { - switch v.fadeoutMode { - case fadeout.ModeAlwaysActive: - v.amp.Fadeout() - case fadeout.ModeOnlyIfVolEnvActive: - if v.IsVolumeEnvelopeEnabled() { - v.amp.Fadeout() - } - } -} - -func (v *opl2Voice) IsKeyOn() bool { - return v.keyOn -} - -func (v *opl2Voice) IsFadeout() bool { - return v.amp.IsFadeoutEnabled() -} - -func (v *opl2Voice) IsDone() bool { - if !v.amp.IsFadeoutEnabled() { - return false - } - return v.amp.GetFadeoutVolume() <= 0 -} - -// == FreqModulator == - -func (v *opl2Voice) SetPeriod(period period.Period) { - v.freq.SetPeriod(period) -} - -func (v *opl2Voice) GetPeriod() period.Period { - return v.freq.GetPeriod() -} - -func (v *opl2Voice) SetPeriodDelta(delta period.Delta) { - v.freq.SetDelta(delta) -} - -func (v *opl2Voice) GetPeriodDelta() period.Delta { - return v.freq.GetDelta() -} - -func (v *opl2Voice) GetFinalPeriod() period.Period { - p := v.freq.GetFinalPeriod() - if v.IsPitchEnvelopeEnabled() { - p = p.AddDelta(v.GetCurrentPitchEnvelope()) - } - return p -} - -// == AmpModulator == - -func (v *opl2Voice) SetVolume(vol volume.Volume) { - if vol == volume.VolumeUseInstVol { - vol = v.initialVolume - } - v.amp.SetVolume(vol) -} - -func (v *opl2Voice) GetVolume() volume.Volume { - return v.amp.GetVolume() -} - -func (v *opl2Voice) GetFinalVolume() volume.Volume { - vol := v.amp.GetFinalVolume() - if v.IsVolumeEnvelopeEnabled() { - vol *= v.GetCurrentVolumeEnvelope() - } - return vol -} - -// == VolumeEnveloper == - -func (v *opl2Voice) EnableVolumeEnvelope(enabled bool) { - v.volEnv.SetEnabled(enabled) -} - -func (v *opl2Voice) IsVolumeEnvelopeEnabled() bool { - return v.volEnv.IsEnabled() -} - -func (v *opl2Voice) GetCurrentVolumeEnvelope() volume.Volume { - if v.volEnv.IsEnabled() { - return v.volEnv.GetCurrentValue() - } - return 1 -} - -func (v *opl2Voice) SetVolumeEnvelopePosition(pos int) { - if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == PitchEnveloper == - -func (v *opl2Voice) EnablePitchEnvelope(enabled bool) { - v.pitchEnv.SetEnabled(enabled) -} - -func (v *opl2Voice) IsPitchEnvelopeEnabled() bool { - return v.pitchEnv.IsEnabled() -} - -func (v *opl2Voice) GetCurrentPitchEnvelope() period.Delta { - if v.pitchEnv.IsEnabled() { - return v.pitchEnv.GetCurrentValue() - } - return 0 -} - -func (v *opl2Voice) SetPitchEnvelopePosition(pos int) { - if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == required function interfaces == - -func (v *opl2Voice) Advance(tickDuration time.Duration) { - defer func() { - v.prevKeyOn = v.keyOn - }() - v.amp.Advance() - v.freq.Advance() - if v.IsVolumeEnvelopeEnabled() { - if doneCB := v.volEnv.Advance(v.keyOn, v.prevKeyOn); doneCB != nil { - doneCB(v) - } - } - if v.IsPitchEnvelopeEnabled() { - if doneCB := v.pitchEnv.Advance(v.keyOn, v.prevKeyOn); doneCB != nil { - doneCB(v) - } - } - - // has to be after the mod/env updates - if v.keyOn != v.prevKeyOn { - if v.keyOn { - v.o.Attack() - } else { - v.o.Release() - } - } - - v.o.Advance(v.GetFinalVolume(), v.GetFinalPeriod()) -} - -func (v *opl2Voice) GetSample(pos sampling.Pos) volume.Matrix { - return volume.Matrix{} -} - -func (v *opl2Voice) GetSampler(samplerRate float32) sampling.Sampler { - return nil -} - -func (v *opl2Voice) Clone() voice.Voice { - o := *v - return &o -} - -func (v *opl2Voice) StartTransaction() voice.Transaction { - t := txn{ - Voice: v, - } - return &t -} - -func (v *opl2Voice) SetActive(active bool) { - v.active = active -} - -func (v *opl2Voice) IsActive() bool { - return v.active -} diff --git a/player/voice/pcm.go b/player/voice/pcm.go deleted file mode 100644 index 9aa290f..0000000 --- a/player/voice/pcm.go +++ /dev/null @@ -1,448 +0,0 @@ -package voice - -import ( - "time" - - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/component" - "github.com/gotracker/playback/voice/fadeout" - - "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/pan" -) - -// PCM is a PCM voice interface -type PCM interface { - voice.Voice - voice.Positioner - voice.FreqModulator - voice.AmpModulator - voice.PanModulator - voice.VolumeEnveloper - voice.PitchEnveloper - voice.PanEnveloper - voice.FilterEnveloper -} - -// PCMConfiguration is the information needed to configure an PCM2 voice -type PCMConfiguration struct { - C2SPD period.Frequency - InitialVolume volume.Volume - InitialPeriod period.Period - AutoVibrato voice.AutoVibrato - Data instrument.Data - OutputFilter voice.FilterApplier - VoiceFilter filter.Filter - PluginFilter filter.Filter -} - -// == the actual pcm voice == - -type pcmVoice struct { - c2spd period.Frequency - initialVolume volume.Volume - outputFilter voice.FilterApplier - voiceFilter filter.Filter - pluginFilter filter.Filter - fadeoutMode fadeout.Mode - channels int - - active bool - keyOn bool - prevKeyOn bool - - pitchAndFilterEnvShared bool - filterEnvActive bool // if pitchAndFilterEnvShared is true, this dictates which is active initially - true=filter, false=pitch - - sampler component.Sampler - amp component.AmpModulator - freq component.FreqModulator - pan component.PanModulator - volEnv component.VolumeEnvelope - pitchEnv component.PitchEnvelope - panEnv component.PanEnvelope - filterEnv component.FilterEnvelope - vol0ticks int - done bool -} - -// NewPCM creates a new PCM voice -func NewPCM(config PCMConfiguration) voice.Voice { - v := pcmVoice{ - c2spd: config.C2SPD, - initialVolume: config.InitialVolume, - outputFilter: config.OutputFilter, - voiceFilter: config.VoiceFilter, - pluginFilter: config.PluginFilter, - active: true, - } - - switch d := config.Data.(type) { - case *instrument.PCM: - v.pitchAndFilterEnvShared = true - v.filterEnvActive = d.PitchFiltMode - v.sampler.Setup(d.Sample, d.Loop, d.SustainLoop) - //v.sampler.SetPos(d.InitialPos) - v.amp.Setup(d.MixingVolume) - v.amp.ResetFadeoutValue(d.FadeOut.Amount) - v.pan.SetPan(d.Panning) - v.volEnv.SetEnabled(d.VolEnv.Enabled) - v.volEnv.Reset(&d.VolEnv) - v.pitchEnv.SetEnabled(d.PitchFiltEnv.Enabled) - v.pitchEnv.Reset(&d.PitchFiltEnv) - v.panEnv.SetEnabled(d.PanEnv.Enabled) - v.panEnv.Reset(&d.PanEnv) - v.filterEnv.SetEnabled(d.PitchFiltEnv.Enabled) - v.filterEnv.Reset(&d.PitchFiltEnv) - v.channels = d.Sample.Channels() - } - - v.amp.SetVolume(config.InitialVolume) - v.freq.SetPeriod(config.InitialPeriod) - v.freq.SetAutoVibratoEnabled(config.AutoVibrato.Enabled) - if config.AutoVibrato.Enabled { - v.freq.ConfigureAutoVibrato(config.AutoVibrato) - v.freq.ResetAutoVibrato(config.AutoVibrato.Sweep) - } - - var o PCM = &v - return o -} - -// == Controller == - -func (v *pcmVoice) Attack() { - v.keyOn = true - v.vol0ticks = 0 - v.done = false - v.amp.Attack() - v.freq.ResetAutoVibrato() - v.sampler.Attack() - v.SetVolumeEnvelopePosition(0) - v.SetPitchEnvelopePosition(0) - v.SetPanEnvelopePosition(0) - v.SetFilterEnvelopePosition(0) -} - -func (v *pcmVoice) Release() { - v.keyOn = false - v.amp.Release() - v.sampler.Release() -} - -func (v *pcmVoice) Fadeout() { - switch v.fadeoutMode { - case fadeout.ModeAlwaysActive: - v.amp.Fadeout() - case fadeout.ModeOnlyIfVolEnvActive: - if v.IsVolumeEnvelopeEnabled() { - v.amp.Fadeout() - } - } - - v.sampler.Fadeout() -} - -func (v *pcmVoice) IsKeyOn() bool { - return v.keyOn -} - -func (v *pcmVoice) IsFadeout() bool { - return v.amp.IsFadeoutEnabled() -} - -func (v *pcmVoice) IsDone() bool { - if v.done { - return true - } - - if v.amp.IsFadeoutEnabled() { - return v.amp.GetFadeoutVolume() <= 0 - } - - return v.vol0ticks >= 3 -} - -// == SampleStream == - -func (v *pcmVoice) GetSample(pos sampling.Pos) volume.Matrix { - samp := v.sampler.GetSample(pos) - if samp.Channels == 0 { - v.done = true - samp.Channels = v.channels - } - vol := v.GetFinalVolume() - wet := samp.Apply(vol) - if v.voiceFilter != nil { - wet = v.voiceFilter.Filter(wet) - } - if v.pluginFilter != nil { - wet = v.pluginFilter.Filter(wet) - } - return wet -} - -// == Positioner == - -func (v *pcmVoice) SetPos(pos sampling.Pos) { - v.sampler.SetPos(pos) -} - -func (v *pcmVoice) GetPos() sampling.Pos { - return v.sampler.GetPos() -} - -// == FreqModulator == - -func (v *pcmVoice) SetPeriod(period period.Period) { - v.freq.SetPeriod(period) -} - -func (v *pcmVoice) GetPeriod() period.Period { - return v.freq.GetPeriod() -} - -func (v *pcmVoice) SetPeriodDelta(delta period.Delta) { - v.freq.SetDelta(delta) -} - -func (v *pcmVoice) GetPeriodDelta() period.Delta { - return v.freq.GetDelta() -} - -func (v *pcmVoice) GetFinalPeriod() period.Period { - p := v.freq.GetFinalPeriod() - if v.IsPitchEnvelopeEnabled() { - delta := v.GetCurrentPitchEnvelope() - p = p.AddDelta(delta) - } - return p -} - -// == AmpModulator == - -func (v *pcmVoice) SetVolume(vol volume.Volume) { - if vol == volume.VolumeUseInstVol { - vol = v.initialVolume - } - v.amp.SetVolume(vol) -} - -func (v *pcmVoice) GetVolume() volume.Volume { - return v.amp.GetVolume() -} - -func (v *pcmVoice) GetFinalVolume() volume.Volume { - vol := v.amp.GetFinalVolume() - if v.IsVolumeEnvelopeEnabled() { - vol *= v.GetCurrentVolumeEnvelope() - } - return vol -} - -// == PanModulator == - -func (v *pcmVoice) SetPan(pan panning.Position) { - v.pan.SetPan(pan) -} - -func (v *pcmVoice) GetPan() panning.Position { - return v.pan.GetPan() -} - -func (v *pcmVoice) GetFinalPan() panning.Position { - p := v.pan.GetFinalPan() - if v.IsPanEnvelopeEnabled() { - p = pan.CalculateCombinedPanning(p, v.panEnv.GetCurrentValue()) - } - return p -} - -// == VolumeEnveloper == - -func (v *pcmVoice) EnableVolumeEnvelope(enabled bool) { - v.volEnv.SetEnabled(enabled) -} - -func (v *pcmVoice) IsVolumeEnvelopeEnabled() bool { - return v.volEnv.IsEnabled() -} - -func (v *pcmVoice) GetCurrentVolumeEnvelope() volume.Volume { - if v.volEnv.IsEnabled() { - return v.volEnv.GetCurrentValue() - } - return 1 -} - -func (v *pcmVoice) SetVolumeEnvelopePosition(pos int) { - if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == PitchEnveloper == - -func (v *pcmVoice) EnablePitchEnvelope(enabled bool) { - v.pitchEnv.SetEnabled(enabled) -} - -func (v *pcmVoice) IsPitchEnvelopeEnabled() bool { - if v.pitchAndFilterEnvShared && v.filterEnvActive { - return false - } - return v.pitchEnv.IsEnabled() -} - -func (v *pcmVoice) GetCurrentPitchEnvelope() period.Delta { - if v.pitchEnv.IsEnabled() { - return v.pitchEnv.GetCurrentValue() - } - return 0 -} - -func (v *pcmVoice) SetPitchEnvelopePosition(pos int) { - if !v.pitchAndFilterEnvShared || !v.filterEnvActive { - if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } - } -} - -// == FilterEnveloper == - -func (v *pcmVoice) EnableFilterEnvelope(enabled bool) { - if !v.pitchAndFilterEnvShared { - v.filterEnv.SetEnabled(enabled) - return - } - - // shared filter/pitch envelope - if !v.filterEnvActive { - return - } - - v.filterEnv.SetEnabled(enabled) -} - -func (v *pcmVoice) IsFilterEnvelopeEnabled() bool { - if v.pitchAndFilterEnvShared && !v.filterEnvActive { - return false - } - return v.filterEnv.IsEnabled() -} - -func (v *pcmVoice) GetCurrentFilterEnvelope() int8 { - return v.filterEnv.GetCurrentValue() -} - -func (v *pcmVoice) SetFilterEnvelopePosition(pos int) { - if !v.pitchAndFilterEnvShared || v.filterEnvActive { - if doneCB := v.filterEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } - } -} - -// == PanEnveloper == - -func (v *pcmVoice) EnablePanEnvelope(enabled bool) { - v.panEnv.SetEnabled(enabled) -} - -func (v *pcmVoice) IsPanEnvelopeEnabled() bool { - return v.panEnv.IsEnabled() -} - -func (v *pcmVoice) GetCurrentPanEnvelope() panning.Position { - return v.panEnv.GetCurrentValue() -} - -func (v *pcmVoice) SetPanEnvelopePosition(pos int) { - if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == required function interfaces == - -func (v *pcmVoice) Advance(tickDuration time.Duration) { - defer func() { - v.prevKeyOn = v.keyOn - }() - v.amp.Advance() - v.freq.Advance() - v.pan.Advance() - if v.IsVolumeEnvelopeEnabled() { - if doneCB := v.volEnv.Advance(v.keyOn, v.prevKeyOn); doneCB != nil { - doneCB(v) - } - } - if v.IsPanEnvelopeEnabled() { - if doneCB := v.panEnv.Advance(v.keyOn, v.prevKeyOn); doneCB != nil { - doneCB(v) - } - } - if v.IsPitchEnvelopeEnabled() { - if doneCB := v.pitchEnv.Advance(v.keyOn, v.prevKeyOn); doneCB != nil { - doneCB(v) - } - } - if v.IsFilterEnvelopeEnabled() { - if doneCB := v.filterEnv.Advance(v.keyOn, v.prevKeyOn); doneCB != nil { - doneCB(v) - } - } - - if v.voiceFilter != nil && v.IsFilterEnvelopeEnabled() { - fval := v.GetCurrentFilterEnvelope() - v.voiceFilter.UpdateEnv(fval) - } - - if vol := v.GetFinalVolume(); vol <= 0 { - v.vol0ticks++ - } else { - v.vol0ticks = 0 - } -} - -func (v *pcmVoice) GetSampler(samplerRate float32) sampling.Sampler { - period := v.GetFinalPeriod() - samplerAdd := float32(period.GetSamplerAdd(float64(samplerRate))) - o := component.OutputFilter{ - Input: v, - Output: v.outputFilter, - } - return sampling.NewSampler(&o, v.GetPos(), samplerAdd) -} - -func (v *pcmVoice) Clone() voice.Voice { - p := *v - if p.voiceFilter != nil { - p.voiceFilter = p.voiceFilter.Clone() - } - if p.pluginFilter != nil { - p.pluginFilter = p.pluginFilter.Clone() - } - return &p -} - -func (v *pcmVoice) StartTransaction() voice.Transaction { - t := txn{ - Voice: v, - } - return &t -} - -func (v *pcmVoice) SetActive(active bool) { - v.active = active -} - -func (v *pcmVoice) IsActive() bool { - return v.active -} diff --git a/player/voice/transaction.go b/player/voice/transaction.go deleted file mode 100644 index 154b9df..0000000 --- a/player/voice/transaction.go +++ /dev/null @@ -1,286 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" - - "github.com/heucuva/optional" -) - -type envSettings struct { - enabled optional.Value[bool] - pos optional.Value[int] -} - -type playingMode uint8 - -const ( - playingModeAttack = playingMode(iota) - playingModeRelease -) - -type txn struct { - cancelled bool - Voice voice.Voice - - active optional.Value[bool] - playing optional.Value[playingMode] - fadeout optional.Value[struct{}] - period optional.Value[period.Period] - periodDelta optional.Value[period.Delta] - vol optional.Value[volume.Volume] - pos optional.Value[sampling.Pos] - pan optional.Value[panning.Position] - volEnv envSettings - pitchEnv envSettings - panEnv envSettings - filterEnv envSettings -} - -func (t *txn) SetActive(active bool) { - t.active.Set(active) -} - -func (t *txn) IsPendingActive() (bool, bool) { - return t.active.Get() -} - -func (t *txn) IsCurrentlyActive() bool { - return t.Voice.IsActive() -} - -// Attack sets the playing mode to Attack -func (t *txn) Attack() { - t.playing.Set(playingModeAttack) -} - -// Release sets the playing mode to Release -func (t *txn) Release() { - t.playing.Set(playingModeRelease) -} - -// Fadeout activates the voice's fade-out function -func (t *txn) Fadeout() { - t.fadeout.Set(struct{}{}) -} - -// SetPeriod sets the period -func (t *txn) SetPeriod(period period.Period) { - t.period.Set(period) -} - -func (t *txn) GetPendingPeriod() (period.Period, bool) { - return t.period.Get() -} - -func (t *txn) GetCurrentPeriod() period.Period { - return voice.GetPeriod(t.Voice) -} - -// SetPeriodDelta sets the period delta -func (t *txn) SetPeriodDelta(delta period.Delta) { - t.periodDelta.Set(delta) -} - -func (t *txn) GetPendingPeriodDelta() (period.Delta, bool) { - return t.periodDelta.Get() -} - -func (t *txn) GetCurrentPeriodDelta() period.Delta { - return voice.GetPeriodDelta(t.Voice) -} - -// SetVolume sets the volume -func (t *txn) SetVolume(vol volume.Volume) { - t.vol.Set(vol) -} - -func (t *txn) GetPendingVolume() (volume.Volume, bool) { - return t.vol.Get() -} - -func (t *txn) GetCurrentVolume() volume.Volume { - return voice.GetVolume(t.Voice) -} - -// SetPos sets the position -func (t *txn) SetPos(pos sampling.Pos) { - t.pos.Set(pos) -} - -func (t *txn) GetPendingPos() (sampling.Pos, bool) { - return t.pos.Get() -} - -func (t *txn) GetCurrentPos() sampling.Pos { - return voice.GetPos(t.Voice) -} - -// SetPan sets the panning position -func (t *txn) SetPan(pan panning.Position) { - t.pan.Set(pan) -} - -func (t *txn) GetPendingPan() (panning.Position, bool) { - return t.pan.Get() -} - -func (t *txn) GetCurrentPan() panning.Position { - return voice.GetPan(t.Voice) -} - -// SetVolumeEnvelopePosition sets the volume envelope position -func (t *txn) SetVolumeEnvelopePosition(pos int) { - t.volEnv.pos.Set(pos) -} - -// EnableVolumeEnvelope sets the volume envelope enable flag -func (t *txn) EnableVolumeEnvelope(enabled bool) { - t.volEnv.enabled.Set(enabled) -} - -func (t *txn) IsPendingVolumeEnvelopeEnabled() (bool, bool) { - return t.volEnv.enabled.Get() -} - -func (t *txn) IsCurrentVolumeEnvelopeEnabled() bool { - return voice.IsVolumeEnvelopeEnabled(t.Voice) -} - -// SetPitchEnvelopePosition sets the pitch envelope position -func (t *txn) SetPitchEnvelopePosition(pos int) { - t.pitchEnv.pos.Set(pos) -} - -// EnablePitchEnvelope sets the pitch envelope enable flag -func (t *txn) EnablePitchEnvelope(enabled bool) { - t.pitchEnv.enabled.Set(enabled) -} - -// SetPanEnvelopePosition sets the panning envelope position -func (t *txn) SetPanEnvelopePosition(pos int) { - t.panEnv.pos.Set(pos) -} - -// EnablePanEnvelope sets the pan envelope enable flag -func (t *txn) EnablePanEnvelope(enabled bool) { - t.panEnv.enabled.Set(enabled) -} - -// SetFilterEnvelopePosition sets the pitch envelope position -func (t *txn) SetFilterEnvelopePosition(pos int) { - t.filterEnv.pos.Set(pos) -} - -// EnableFilterEnvelope sets the filter envelope enable flag -func (t *txn) EnableFilterEnvelope(enabled bool) { - t.filterEnv.enabled.Set(enabled) -} - -// SetAllEnvelopePositions sets all the envelope positions to the same value -func (t *txn) SetAllEnvelopePositions(pos int) { - t.volEnv.pos.Set(pos) - t.pitchEnv.pos.Set(pos) - t.panEnv.pos.Set(pos) - t.filterEnv.pos.Set(pos) -} - -// ====== - -// Cancel cancels a pending transaction -func (t *txn) Cancel() { - t.cancelled = true -} - -// Commit commits the transaction by applying pending updates -func (t *txn) Commit() { - if t.cancelled { - return - } - t.cancelled = true - - if t.Voice == nil { - panic("voice not initialized") - } - - if active, ok := t.active.Get(); ok { - t.Voice.SetActive(active) - } - - if p, ok := t.period.Get(); ok { - voice.SetPeriod(t.Voice, p) - } - - if delta, ok := t.periodDelta.Get(); ok { - voice.SetPeriodDelta(t.Voice, delta) - } - - if vol, ok := t.vol.Get(); ok { - voice.SetVolume(t.Voice, vol) - } - - if pos, ok := t.pos.Get(); ok { - voice.SetPos(t.Voice, pos) - } - - if pan, ok := t.pan.Get(); ok { - voice.SetPan(t.Voice, pan) - } - - if pos, ok := t.volEnv.pos.Get(); ok { - voice.SetVolumeEnvelopePosition(t.Voice, pos) - } - - if enabled, ok := t.volEnv.enabled.Get(); ok { - voice.EnableVolumeEnvelope(t.Voice, enabled) - } - - if pos, ok := t.pitchEnv.pos.Get(); ok { - voice.SetPitchEnvelopePosition(t.Voice, pos) - } - - if enabled, ok := t.pitchEnv.enabled.Get(); ok { - voice.EnablePitchEnvelope(t.Voice, enabled) - } - - if pos, ok := t.panEnv.pos.Get(); ok { - voice.SetPanEnvelopePosition(t.Voice, pos) - } - - if enabled, ok := t.panEnv.enabled.Get(); ok { - voice.EnablePanEnvelope(t.Voice, enabled) - } - - if pos, ok := t.filterEnv.pos.Get(); ok { - voice.SetFilterEnvelopePosition(t.Voice, pos) - } - - if enabled, ok := t.filterEnv.enabled.Get(); ok { - voice.EnableFilterEnvelope(t.Voice, enabled) - } - - if mode, ok := t.playing.Get(); ok { - switch mode { - case playingModeAttack: - t.Voice.Attack() - case playingModeRelease: - t.Voice.Release() - } - } - - if _, ok := t.fadeout.Get(); ok { - t.Voice.Fadeout() - } -} - -func (t *txn) GetVoice() voice.Voice { - return t.Voice -} - -func (t *txn) Clone() voice.Transaction { - c := *t - return &c -} diff --git a/player/voice/voice.go b/player/voice/voice.go deleted file mode 100644 index 5f72d52..0000000 --- a/player/voice/voice.go +++ /dev/null @@ -1,45 +0,0 @@ -package voice - -import ( - "github.com/gotracker/playback/voice" - - "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/player/render" -) - -// New returns a new Voice from the instrument and output channel provided -func New(inst *instrument.Instrument, output *render.Channel) voice.Voice { - switch data := inst.GetData().(type) { - case *instrument.PCM: - var ( - voiceFilter filter.Filter - pluginFilter filter.Filter - ) - if factory := inst.GetFilterFactory(); factory != nil { - voiceFilter = factory(inst.C2Spd, output.GetSampleRate()) - } - if factory := inst.GetPluginFilterFactory(); factory != nil { - pluginFilter = factory(inst.C2Spd, output.GetSampleRate()) - } - return NewPCM(PCMConfiguration{ - C2SPD: inst.GetC2Spd(), - InitialVolume: inst.GetDefaultVolume(), - AutoVibrato: inst.GetAutoVibrato(), - Data: data, - OutputFilter: output, - VoiceFilter: voiceFilter, - PluginFilter: pluginFilter, - }) - case *instrument.OPL2: - return NewOPL2(OPLConfiguration{ - Chip: output.GetOPL2Chip(), - Channel: output.ChannelNum, - C2SPD: inst.GetC2Spd(), - InitialVolume: inst.GetDefaultVolume(), - AutoVibrato: inst.GetAutoVibrato(), - Data: data, - }) - } - return nil -} diff --git a/song/channel.go b/song/channeldata.go similarity index 64% rename from song/channel.go rename to song/channeldata.go index b43bd9e..8009ca3 100644 --- a/song/channel.go +++ b/song/channeldata.go @@ -4,21 +4,19 @@ import ( "fmt" "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" ) // ChannelData is an interface for channel data -type ChannelData interface { +type ChannelDataIntf interface { HasNote() bool GetNote() note.Note HasInstrument() bool - GetInstrument(note.Semitone) instrument.ID + GetInstrument() int HasVolume() bool - GetVolume() volume.Volume + GetVolumeGeneric() volume.Volume HasCommand() bool @@ -27,3 +25,9 @@ type ChannelData interface { fmt.Stringer ShortString() string } + +type ChannelData[TVolume Volume] interface { + ChannelDataIntf + + GetVolume() TVolume +} diff --git a/song/channelmemory.go b/song/channelmemory.go new file mode 100644 index 0000000..02bcc01 --- /dev/null +++ b/song/channelmemory.go @@ -0,0 +1,6 @@ +package song + +type ChannelMemory interface { + Retrigger() + StartOrder0() +} diff --git a/song/channelsettings.go b/song/channelsettings.go new file mode 100644 index 0000000..064a523 --- /dev/null +++ b/song/channelsettings.go @@ -0,0 +1,59 @@ +package song + +import ( + "errors" + + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/voice/vol0optimization" +) + +type ChannelSettings interface { + IsEnabled() bool + IsMuted() bool + GetOutputChannelNum() int + GetMemory() ChannelMemory + IsPanEnabled() bool + GetDefaultFilterInfo() filter.Info + IsDefaultFilterEnabled() bool + GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings + GetOPLChannel() index.OPLChannel +} + +type channelInitialVolumeGetter[TVolume Volume] interface { + GetInitialVolume() TVolume +} + +func GetChannelInitialVolume[TVolume Volume](c ChannelSettings) (TVolume, error) { + gicv, ok := c.(channelInitialVolumeGetter[TVolume]) + if !ok { + var empty TVolume + return empty, errors.New("could not identify channel initial volume interface") + } + + return gicv.GetInitialVolume(), nil +} + +func GetChannelMixingVolume[TMixingVolume Volume](c ChannelSettings) (TMixingVolume, error) { + gcmv, ok := c.(mixingVolumeGetter[TMixingVolume]) + if !ok { + var empty TMixingVolume + return empty, errors.New("could not identify channel volume interface") + } + + return gcmv.GetMixingVolume(), nil +} + +type channelInitialPanningGetter[TPanning Panning] interface { + GetInitialPanning() TPanning +} + +func GetChannelInitialPanning[TPanning Panning](c ChannelSettings) (TPanning, error) { + gicp, ok := c.(channelInitialPanningGetter[TPanning]) + if !ok { + var empty TPanning + return empty, errors.New("could not identify channel initial panning interface") + } + + return gicp.GetInitialPanning(), nil +} diff --git a/song/pattern.go b/song/pattern.go index b922cc9..6fa5d41 100644 --- a/song/pattern.go +++ b/song/pattern.go @@ -11,17 +11,23 @@ var ( ErrStopSong = errors.New("stop song") ) -// Pattern is an interface for pattern data -type Pattern[TChannelData any] interface { - GetRow(index.Row) Row[TChannelData] - GetRows() Rows[TChannelData] +type PatternIntf interface { + GetRow(row index.Row) Row + NumRows() int } -// Patterns is an array of pattern interfaces -type Patterns[TChannelData any] []Pattern[TChannelData] +// Pattern is structure containing the pattern data +type Pattern []Row -// Rows is an interface to obtain row data -type Rows[TChannelData any] interface { - GetRow(index.Row) Row[TChannelData] - NumRows() int +// GetRow returns a single row of channel data +func (p Pattern) GetRow(row index.Row) Row { + return p[row] } + +// NumRows returns the number of rows contained within the pattern +func (p Pattern) NumRows() int { + return len(p) +} + +// Patterns is an array of pattern interfaces +type Patterns[TChannelData ChannelData[TVolume], TVolume Volume] []Pattern diff --git a/song/periodcalculator.go b/song/periodcalculator.go new file mode 100644 index 0000000..cb75dd4 --- /dev/null +++ b/song/periodcalculator.go @@ -0,0 +1,25 @@ +package song + +import ( + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/system" +) + +type PeriodCalculatorIntf interface { + GetSystem() system.System +} + +type PeriodCalculator[TPeriod period.Period] interface { + PeriodCalculatorIntf + + GetPeriod(note.Note) TPeriod + PortaToNote(TPeriod, period.Delta, TPeriod) (TPeriod, error) + PortaDown(TPeriod, period.Delta) (TPeriod, error) + PortaUp(TPeriod, period.Delta) (TPeriod, error) + AddDelta(TPeriod, period.Delta) (TPeriod, error) + + GetSamplerAdd(TPeriod, frequency.Frequency, frequency.Frequency) float64 + GetFrequency(TPeriod) frequency.Frequency +} diff --git a/song/row.go b/song/row.go index 487fd38..ab39fac 100644 --- a/song/row.go +++ b/song/row.go @@ -1,6 +1,31 @@ package song -// Row is an interface to a row -type Row[TChannelData any] interface { - GetChannels() []TChannelData +import "github.com/gotracker/playback/index" + +type rowIntf[TVolume Volume] interface { + Len() int + ForEach(fn func(ch index.Channel, d ChannelData[TVolume]) (bool, error)) error +} + +// Row is a structure containing a single row +type Row any + +func GetRowNumChannels[TVolume Volume](r Row) int { + if row, ok := r.(rowIntf[TVolume]); ok { + return row.Len() + } + return 0 +} + +func ForEachRowChannel[TVolume Volume](r Row, fn func(ch index.Channel, d ChannelData[TVolume]) (bool, error)) error { + row, ok := r.(rowIntf[TVolume]) + if !ok { + return nil + } + + return row.ForEach(fn) +} + +type RowStringer interface { + String(options ...any) string } diff --git a/song/song.go b/song/song.go index 922a584..92888d3 100644 --- a/song/song.go +++ b/song/song.go @@ -1,22 +1,76 @@ package song import ( + "errors" + "reflect" + "time" + + "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/system" + "github.com/gotracker/playback/voice/types" ) // Data is an interface to the song data type Data interface { + GetPeriodType() reflect.Type + GetGlobalVolumeType() reflect.Type + GetChannelMixingVolumeType() reflect.Type + GetChannelVolumeType() reflect.Type + GetChannelPanningType() reflect.Type + + GetInitialBPM() int + GetInitialTempo() int + GetMixingVolumeGeneric() volume.Volume + GetTickDuration(bpm int) time.Duration GetOrderList() []index.Pattern - IsChannelEnabled(int) bool - GetRenderChannel(int) int + GetNumChannels() int + GetChannelSettings(index.Channel) ChannelSettings NumInstruments() int - IsValidInstrumentID(instrument.ID) bool - GetInstrument(instrument.ID) (*instrument.Instrument, note.Semitone) + GetInstrument(int, note.Semitone) (instrument.InstrumentIntf, note.Semitone) GetName() string + GetPatternByOrder(index.Order) (Pattern, error) + GetPattern(index.Pattern) (Pattern, error) + GetPeriodCalculator() PeriodCalculatorIntf + GetInitialOrder() index.Order + GetRowRenderStringer(Row, int, bool) RowStringer + GetSystem() system.System + GetMachineSettings() any + ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error + IsOPL2Enabled() bool +} + +type ( + Volume = types.Volume + Panning = types.Panning +) + +type globalVolumeGetter[TGlobalVolume Volume] interface { + GetGlobalVolume() TGlobalVolume } -type PatternData[TChannelData any] interface { - GetPattern(index.Pattern) Pattern[TChannelData] +func GetGlobalVolume[TGlobalVolume Volume](s Data) (TGlobalVolume, error) { + ggv, ok := s.(globalVolumeGetter[TGlobalVolume]) + if !ok { + var empty TGlobalVolume + return empty, errors.New("could not identify global volume interface") + } + + return ggv.GetGlobalVolume(), nil +} + +type mixingVolumeGetter[TMixingVolume Volume] interface { + GetMixingVolume() TMixingVolume +} + +func GetMixingVolume[TMixingVolume Volume](s Data) (TMixingVolume, error) { + gmv, ok := s.(mixingVolumeGetter[TMixingVolume]) + if !ok { + var empty TMixingVolume + return empty, errors.New("could not identify mixing volume interface") + } + + return gmv.GetMixingVolume(), nil } diff --git a/system/clockedsystem.go b/system/clockedsystem.go new file mode 100644 index 0000000..68d1ad6 --- /dev/null +++ b/system/clockedsystem.go @@ -0,0 +1,71 @@ +package system + +import ( + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/note" +) + +type ClockableSystem interface { + System + GetBaseClock() frequency.Frequency + GetCommonPeriod() uint16 + GetBaseFinetunes() note.Finetune + GetFinetunesPerOctave() note.Finetune + GetFinetunesPerSemitone() note.Finetune + GetSemitonePeriod(note.Key) (uint16, bool) + GetOctaveShift() uint16 +} + +type ClockedSystem struct { + MaxPastNotesPerChannel int + + BaseClock frequency.Frequency + BaseFinetunes note.Finetune + FinetunesPerOctave note.Finetune + FinetunesPerNote note.Finetune + CommonPeriod uint16 + CommonRate frequency.Frequency + SemitonePeriods [note.NumKeys]uint16 + OctaveShift uint16 +} + +var _ ClockableSystem = (*ClockedSystem)(nil) + +func (s ClockedSystem) GetMaxPastNotesPerChannel() int { + return s.MaxPastNotesPerChannel +} + +func (s ClockedSystem) GetBaseClock() frequency.Frequency { + return s.BaseClock +} + +func (s ClockedSystem) GetBaseFinetunes() note.Finetune { + return s.BaseFinetunes +} + +func (s ClockedSystem) GetFinetunesPerOctave() note.Finetune { + return s.FinetunesPerOctave +} + +func (s ClockedSystem) GetFinetunesPerSemitone() note.Finetune { + return s.FinetunesPerNote +} + +func (s ClockedSystem) GetCommonPeriod() uint16 { + return s.CommonPeriod +} + +func (s ClockedSystem) GetCommonRate() frequency.Frequency { + return s.CommonRate +} + +func (s ClockedSystem) GetSemitonePeriod(k note.Key) (uint16, bool) { + if int(k) < note.NumKeys { + return s.SemitonePeriods[int(k)] >> s.OctaveShift, true + } + return 0, false +} + +func (s ClockedSystem) GetOctaveShift() uint16 { + return s.OctaveShift +} diff --git a/system/system.go b/system/system.go new file mode 100644 index 0000000..6d5a2c4 --- /dev/null +++ b/system/system.go @@ -0,0 +1,8 @@ +package system + +import "github.com/gotracker/playback/frequency" + +type System interface { + GetMaxPastNotesPerChannel() int + GetCommonRate() frequency.Frequency +} diff --git a/tracing/entry.go b/tracing/entry.go new file mode 100644 index 0000000..0f34eda --- /dev/null +++ b/tracing/entry.go @@ -0,0 +1,68 @@ +package tracing + +import ( + "fmt" + "strings" +) + +type entry[TPrefix Ticker, TPayload fmt.Stringer] struct { + prefix TPrefix + operation string + comment string + payload TPayload +} + +func (e entry[TPrefix, TPayload]) String() string { + var chunks []string + if len(e.operation) > 0 { + chunks = append(chunks, e.operation) + } + if line := fmt.Sprint(e.payload); len(line) > 0 { + chunks = append(chunks, line) + } + if len(e.comment) > 0 { + chunks = append(chunks, "//", e.comment) + } + return strings.Join(chunks, " ") +} + +func (e entry[TPrefix, TPayload]) GetTick() Tick { + return e.prefix.GetTick() +} + +func (e entry[TPrefix, TPayload]) Prefix() string { + return e.prefix.String() +} + +/////////////////////////////////////////////////////////// + +func (t *tracerFile) trace(tick Tick, op string) { + t.traceWithComment(tick, op, "") +} + +type emptyPayload struct{} + +func (emptyPayload) String() string { + return "" +} + +var empty emptyPayload + +func (t *tracerFile) traceWithComment(tick Tick, op, comment string) { + if t.file == nil { + return + } + traceWithPayload(t, tick, op, comment, empty) +} + +func traceWithPayload[TPrefix Ticker, TPayload fmt.Stringer](t *tracerFile, prefix TPrefix, op, comment string, payload TPayload) { + e := entry[TPrefix, TPayload]{ + prefix: prefix, + operation: op, + comment: comment, + payload: payload, + } + t.mu.Lock() + defer t.mu.Unlock() + t.updates = append(t.updates, e) +} diff --git a/tracing/tick.go b/tracing/tick.go new file mode 100644 index 0000000..30c3327 --- /dev/null +++ b/tracing/tick.go @@ -0,0 +1,34 @@ +package tracing + +import ( + "fmt" + + "github.com/gotracker/playback/index" +) + +type Tick struct { + Order index.Order + Row index.Row + Tick int +} + +func (t Tick) Equals(rhs Tick) bool { + return t.Tick == rhs.Tick && t.Row == rhs.Row && t.Order == rhs.Order +} + +func (t Tick) String() string { + ts := fmt.Sprint(t.Tick) + if len(ts) < 2 { + ts = " " + ts + } + return fmt.Sprintf("%03d:%03d ", t.Order, t.Row) + ts +} + +func (t Tick) GetTick() Tick { + return t +} + +type Ticker interface { + fmt.Stringer + GetTick() Tick +} diff --git a/tracing/tickchannel.go b/tracing/tickchannel.go new file mode 100644 index 0000000..7453d02 --- /dev/null +++ b/tracing/tickchannel.go @@ -0,0 +1,37 @@ +package tracing + +import ( + "fmt" + + "github.com/gotracker/playback/index" +) + +type tickChannel struct { + tick Tick + ch index.Channel +} + +func (e tickChannel) String() string { + return fmt.Sprintf("%v %03d", e.tick, e.ch+1) +} + +func (e tickChannel) GetTick() Tick { + return e.tick +} + +/////////////////////////////////////////////////////////// + +func (t *tracerFile) traceChannel(tick Tick, ch index.Channel, op string) { + t.traceChannelWithComment(tick, ch, op, "") +} + +func (t *tracerFile) traceChannelWithComment(tick Tick, ch index.Channel, op string, comment string) { + if t.file == nil { + return + } + tc := tickChannel{ + tick: tick, + ch: ch, + } + traceWithPayload(t, tc, op, comment, empty) +} diff --git a/tracing/tracechannel.go b/tracing/tracechannel.go new file mode 100644 index 0000000..38e3c19 --- /dev/null +++ b/tracing/tracechannel.go @@ -0,0 +1,31 @@ +package tracing + +import "github.com/gotracker/playback/index" + +type TraceChannel interface { + Trace(op string) + TraceWithComment(op, comment string) + TraceValueChange(op string, prev, new any) + TraceValueChangeWithComment(op string, prev, new any, comment string) +} + +type channelTracer struct { + t Tracer + channel index.Channel +} + +func (c channelTracer) Trace(op string) { + c.t.TraceChannel(c.channel, op) +} + +func (c channelTracer) TraceWithComment(op, comment string) { + c.t.TraceChannelWithComment(c.channel, op, comment) +} + +func (c channelTracer) TraceValueChange(op string, prev, new any) { + c.t.TraceChannelValueChange(c.channel, op, prev, new) +} + +func (c channelTracer) TraceValueChangeWithComment(op string, prev, new any, comment string) { + c.t.TraceChannelValueChangeWithComment(c.channel, op, prev, new, comment) +} diff --git a/tracing/tracer.go b/tracing/tracer.go new file mode 100644 index 0000000..4133a67 --- /dev/null +++ b/tracing/tracer.go @@ -0,0 +1,38 @@ +package tracing + +import ( + "io" + "os" + + "github.com/gotracker/playback/index" +) + +type Tracer interface { + OutputTraces() + SetTracingTick(order index.Order, row index.Row, tick int) + Trace(op string) + TraceWithComment(op, commentFmt string, commentParams ...any) + TraceValueChange(op string, prev, new any) + TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) + TraceChannel(ch index.Channel, op string) + TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) + TraceChannelValueChange(ch index.Channel, op string, prev, new any) + TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) +} + +type TracerWithClose interface { + Tracer + io.Closer +} + +func NewFromFilename(filename string) (TracerWithClose, error) { + f, err := os.Create(filename) + if err != nil { + return nil, err + } + + tf := tracerFile{ + file: f, + } + return &tf, nil +} diff --git a/tracing/tracerfile.go b/tracing/tracerfile.go new file mode 100644 index 0000000..c9c4d63 --- /dev/null +++ b/tracing/tracerfile.go @@ -0,0 +1,136 @@ +package tracing + +import ( + "fmt" + "io" + "log" + "os" + "sync" + + "github.com/gotracker/playback/index" +) + +type tracerFile struct { + file *os.File + chMap map[int]*tracingChannelState + traces []tracingMsgFunc + c chan func(w io.Writer) + wg sync.WaitGroup + + tick Tick + updates []entryIntf + prevTick Tick + mu sync.RWMutex +} + +type entryIntf interface { + GetTick() Tick + Prefix() string + fmt.Stringer +} + +type tracingMsgFunc func() string + +type tracingChannelState struct { + traces []tracingMsgFunc +} + +func (t *tracerFile) Close() error { + if t.c != nil { + close(t.c) + } + if t.file != nil { + if err := t.file.Close(); err != nil { + return err + } + } + t.wg.Wait() + return nil +} + +func (t *tracerFile) OutputTraces() { + if t.file == nil { + return + } + + t.mu.Lock() + defer t.mu.Unlock() + + var updates []entryIntf + updates, t.updates = t.updates, nil + + go func() { + logger := log.New(t.file, "", 0) + for _, u := range updates { + if tick := u.GetTick(); !tick.Equals(t.prevTick) { + fmt.Fprintln(t.file) + t.prevTick = tick + } + + logger.Println("[" + u.Prefix() + "] " + u.String()) + } + }() +} + +func (t *tracerFile) SetTracingTick(order index.Order, row index.Row, tick int) { + t.mu.Lock() + t.tick = Tick{ + Order: order, + Row: row, + Tick: tick, + } + t.mu.Unlock() +} + +func (t *tracerFile) GetTracingTick() Tick { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.tick +} + +func (t *tracerFile) Trace(op string) { + t.TraceWithComment(op, "") +} + +func (t *tracerFile) TraceWithComment(op, commentFmt string, commentParams ...any) { + traceWithPayload(t, t.GetTracingTick(), op, fmt.Sprintf(commentFmt, commentParams...), empty) +} + +func (t *tracerFile) TraceValueChange(op string, prev, new any) { + t.TraceValueChangeWithComment(op, prev, new, "") +} + +func (t *tracerFile) TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) { + traceWithPayload(t, t.GetTracingTick(), op, fmt.Sprintf(commentFmt, commentParams...), valueUpdate{ + old: prev, + new: new, + }) +} + +func (t *tracerFile) TraceChannel(ch index.Channel, op string) { + t.TraceChannelWithComment(ch, op, "") +} + +func (t *tracerFile) TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) { + tc := tickChannel{ + tick: t.GetTracingTick(), + ch: ch, + } + traceWithPayload(t, tc, op, fmt.Sprintf(commentFmt, commentParams...), empty) +} + +func (t *tracerFile) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { + t.TraceChannelValueChangeWithComment(ch, op, prev, new, "") +} + +func (t *tracerFile) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) { + tc := tickChannel{ + tick: t.GetTracingTick(), + ch: ch, + } + traceWithPayload(t, tc, op, fmt.Sprintf(commentFmt, commentParams...), valueUpdate{ + old: prev, + new: new, + }) +} diff --git a/tracing/valueupdate.go b/tracing/valueupdate.go new file mode 100644 index 0000000..a029e2c --- /dev/null +++ b/tracing/valueupdate.go @@ -0,0 +1,58 @@ +package tracing + +import ( + "fmt" + "reflect" + + "github.com/gotracker/playback/index" +) + +type valueUpdate struct { + old any + new any +} + +func (e valueUpdate) String() string { + return fmt.Sprintf("%v <- %v", e.new, e.old) +} + +/////////////////////////////////////////////////////////// + +func (t *tracerFile) traceValueChange(tick Tick, op string, prev, new any) { + t.traceValueChangeWithComment(tick, op, prev, new, "") +} + +func (t *tracerFile) traceValueChangeWithComment(tick Tick, op string, prev, new any, comment string) { + if t.file == nil { + return + } + if reflect.DeepEqual(prev, new) { + return + } + traceWithPayload(t, tick, op, comment, valueUpdate{ + old: prev, + new: new, + }) +} + +func (t *tracerFile) traceChannelValueChange(tick Tick, ch index.Channel, op string, prev, new any) { + t.traceChannelValueChangeWithComment(tick, ch, op, prev, new, "") +} + +func (t *tracerFile) traceChannelValueChangeWithComment(tick Tick, ch index.Channel, op string, prev, new any, comment string) { + if t.file == nil { + return + } + if reflect.DeepEqual(prev, new) { + return + } + + tc := tickChannel{ + tick: tick, + ch: ch, + } + traceWithPayload(t, tc, op, comment, valueUpdate{ + old: prev, + new: new, + }) +} diff --git a/util/lerp.go b/util/lerp.go index 2b62893..bca938f 100644 --- a/util/lerp.go +++ b/util/lerp.go @@ -1,22 +1,16 @@ package util -func LerpFloat32(t float64, a, b float32) float32 { - return float32(LerpFloat64(t, float64(a), float64(b))) +import "golang.org/x/exp/constraints" + +type Lerpable interface { + constraints.Integer | constraints.Float } -func LerpFloat64(t float64, a, b float64) float64 { +func Lerp[T Lerpable](t float64, a, b T) T { if t <= 0 { return a } else if t >= 1 { return b } - return a + (t * (b - a)) -} - -func LerpInt(t float64, a, b int) int { - return int(LerpFloat64(t, float64(a), float64(b))) -} - -func LerpUint(t float64, a, b uint) uint { - return uint(LerpFloat64(t, float64(a), float64(b))) + return a + T(t*(float64(b)-(float64(a)))) } diff --git a/util/loopdetect.go b/util/loopdetect.go deleted file mode 100644 index b85daf1..0000000 --- a/util/loopdetect.go +++ /dev/null @@ -1,46 +0,0 @@ -package util - -import "github.com/gotracker/playback/index" - -type loopDetectNode map[index.Row]struct{} - -// LoopDetect is a poorly-optimized, but simple loop detection system for tracked music -type LoopDetect struct { - orders map[index.Order]*loopDetectNode -} - -// Observe determines if a particular order+row combination has been observed before and returns true if it has -// it will also add the combination to the detection tree if it has not been observed before. -func (ld *LoopDetect) Observe(ord index.Order, row index.Row) bool { - n := ld.findOrAddOrder(ord) - - if *n == nil { - *n = make(loopDetectNode) - } - - if _, found := (*n)[row]; found { - return true - } - - (*n)[row] = struct{}{} - return false -} - -func (ld *LoopDetect) Reset() { - ld.orders = nil -} - -func (ld *LoopDetect) findOrAddOrder(ord index.Order) *loopDetectNode { - if ld.orders == nil { - ld.orders = make(map[index.Order]*loopDetectNode) - } - - if n, ok := ld.orders[ord]; ok && n != nil { - return n - } - - n := loopDetectNode{} - ld.orders[ord] = &n - - return &n -} diff --git a/util/patternloop.go b/util/patternloop.go deleted file mode 100644 index fe97015..0000000 --- a/util/patternloop.go +++ /dev/null @@ -1,27 +0,0 @@ -package util - -import "github.com/gotracker/playback/index" - -// PatternLoop is a state machine for pattern loops -type PatternLoop struct { - Enabled bool - Start index.Row - End index.Row - Total uint8 - - Count uint8 -} - -// ContinueLoop returns the next expected row if a loop occurs -func (pl *PatternLoop) ContinueLoop(currentRow index.Row) (index.Row, bool) { - if pl.Enabled { - if currentRow == pl.End { - pl.Count++ - if pl.Count <= pl.Total { - return pl.Start, true - } - pl.Enabled = false - } - } - return 0, false -} diff --git a/voice/ampmodulator.go b/voice/ampmodulator.go deleted file mode 100755 index d29ff16..0000000 --- a/voice/ampmodulator.go +++ /dev/null @@ -1,12 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/volume" -) - -// AmpModulator is the instrument volume (amplitude) control interface -type AmpModulator interface { - SetVolume(vol volume.Volume) - GetVolume() volume.Volume - GetFinalVolume() volume.Volume -} diff --git a/voice/autovibrato.go b/voice/autovibrato.go deleted file mode 100755 index 08770ad..0000000 --- a/voice/autovibrato.go +++ /dev/null @@ -1,25 +0,0 @@ -package voice - -import ( - "github.com/gotracker/playback/voice/oscillator" -) - -// AutoVibrato is the setting and memory for the auto-vibrato system -type AutoVibrato struct { - Enabled bool - Sweep int - WaveformSelection uint8 - Depth float32 - Rate int - Factory func() oscillator.Oscillator -} - -// Generate creates an AutoVibrato waveform oscillator and configures it with the inital values -func (a *AutoVibrato) Generate() oscillator.Oscillator { - if a.Factory == nil { - return nil - } - o := a.Factory() - o.SetWaveform(oscillator.WaveTableSelect(a.WaveformSelection)) - return o -} diff --git a/voice/autovibrato/config.go b/voice/autovibrato/config.go new file mode 100644 index 0000000..12f7512 --- /dev/null +++ b/voice/autovibrato/config.go @@ -0,0 +1,33 @@ +package autovibrato + +import ( + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/types" +) + +// AutoVibratoConfig is the setting and memory for the auto-vibrato system +type AutoVibratoConfig[TPeriod types.Period] struct { + PC period.PeriodConverter[TPeriod] + Enabled bool + Sweep int + WaveformSelection uint8 + Depth float32 + Rate int + FactoryName string +} + +// Generate creates an AutoVibrato waveform oscillator and configures it with the inital values +func (a AutoVibratoConfig[TPeriod]) Generate(factory func(string) (oscillator.Oscillator, error)) (oscillator.Oscillator, error) { + if factory == nil { + return nil, nil + } + + o, err := factory(a.FactoryName) + if err != nil { + return nil, err + } + + o.SetWaveform(oscillator.WaveTableSelect(a.WaveformSelection)) + return o, nil +} diff --git a/voice/autovibrato/settings.go b/voice/autovibrato/settings.go new file mode 100644 index 0000000..ca470d7 --- /dev/null +++ b/voice/autovibrato/settings.go @@ -0,0 +1,11 @@ +package autovibrato + +import ( + "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/types" +) + +type AutoVibratoSettings[TPeriod types.Period] struct { + AutoVibratoConfig[TPeriod] + Factory func(string) (oscillator.Oscillator, error) +} diff --git a/voice/component/envelope.go b/voice/component/envelope.go index a4060b3..c3ff266 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -1,9 +1,233 @@ package component +import ( + "fmt" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/loop" +) + // Envelope is an envelope component interface type Envelope interface { - //Reset(env *envelope.Envelope) + //Init(env *envelope.Envelope) SetEnabled(enabled bool) IsEnabled() bool - Advance(keyOn bool, prevKeyOn bool) + Reset() + Advance() +} + +// baseEnvelope is a basic modulation envelope +type baseEnvelope[TIn, TOut any] struct { + settings EnvelopeSettings[TIn, TOut] + updater func(TIn, TIn, float64) TOut + unkeyed struct{} + keyed struct { + active bool + pos int + done bool + } + value TOut + + slimKeyModulator +} + +type EnvelopeSettings[TIn, TOut any] struct { + envelope.Envelope[TIn] + OnFinished voice.Callback +} + +func (e *baseEnvelope[TIn, TOut]) Setup(settings EnvelopeSettings[TIn, TOut], update func(TIn, TIn, float64) TOut) { + e.settings = settings + e.updater = update + e.Reset() +} + +func (e baseEnvelope[TIn, TOut]) Clone(update func(TIn, TIn, float64) TOut, onFinished voice.Callback) baseEnvelope[TIn, TOut] { + m := e + m.settings.OnFinished = onFinished + m.updater = update + return m +} + +// Reset resets the state to defaults based on the envelope provided +func (e *baseEnvelope[TIn, TOut]) Reset() error { + e.keyed.active = e.settings.Enabled + return e.stateReset() +} + +func (e baseEnvelope[TIn, TOut]) CanLoop() bool { + return e.settings.Loop != nil && e.settings.Loop.Enabled() +} + +// SetEnabled sets the enabled flag for the envelope +func (e *baseEnvelope[TIn, TOut]) SetEnabled(enabled bool) error { + e.keyed.active = enabled + return nil +} + +// IsEnabled returns the enabled flag for the envelope +func (e baseEnvelope[TIn, TOut]) IsEnabled() bool { + return e.keyed.active +} + +func (e baseEnvelope[TIn, TOut]) IsDone() bool { + return e.keyed.done +} + +// GetCurrentValue returns the current cached envelope value +func (e baseEnvelope[TIn, TOut]) GetCurrentValue() TOut { + return e.value +} + +// SetEnvelopePosition sets the current position in the envelope +func (e *baseEnvelope[TIn, TOut]) SetEnvelopePosition(pos int) (voice.Callback, error) { + prev := e.keyed.active + e.keyed.active = true + e.keyed.done = false + e.stateReset() + // XXX: this is gross, but currently the most optimal way to find the correct position + for i := 0; i < pos; i++ { + if doneCB := e.Advance(); doneCB != nil { + return doneCB, nil + } + } + e.keyed.active = prev + return nil, nil +} + +func (e baseEnvelope[TIn, TOut]) GetEnvelopePosition() int { + return e.keyed.pos +} + +// Advance advances the envelope state 1 tick and calculates the current envelope value +func (e *baseEnvelope[TIn, TOut]) Advance() voice.Callback { + var doneCB voice.Callback + if done := e.stateAdvance(e.keyOn); done { + doneCB = e.settings.OnFinished + } + return doneCB +} + +func (e baseEnvelope[TIn, TOut]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} pos{%v} stopped{%v} value{%v}", + e.keyed.active, + e.keyed.pos, + e.keyed.done, + e.value, + ), comment) +} + +func (e *baseEnvelope[TIn, TOut]) stateReset() error { + if !e.settings.Envelope.Enabled { + e.keyed.done = true + return nil + } + + e.keyed.pos = 0 + e.keyed.done = false + return e.updateValue() +} + +func (e *baseEnvelope[TIn, TOut]) updateValue() error { + if !e.keyed.active || e.keyed.done { + return nil + } + + nPoints := len(e.settings.Envelope.Values) + + if nPoints == 0 { + return nil + } + + curTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.keyed.pos, e.settings.Envelope.Length, e.prevKeyOn) + nextTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, curTick+1, e.settings.Envelope.Length, e.keyOn) + + curPoint := -1 + for i, it := range e.settings.Envelope.Values { + if it.Pos > curTick { + curPoint = i - 1 + break + } + } + var cur envelope.Point[TIn] + if curPoint >= 0 && curPoint < nPoints { + cur = e.settings.Values[curPoint] + } else { + cur = e.settings.Values[nPoints-1] + } + + nextPoint := -1 + for i, it := range e.settings.Envelope.Values { + if it.Pos > nextTick { + nextPoint = i + break + } + } + + if nextPoint < 0 || nextPoint >= nPoints { + e.value = e.updater(cur.Y, cur.Y, 0) + return nil + } + + next := e.settings.Values[nextPoint] + + t := float64(0) + if cur.Length > 0 { + if tl := curTick - cur.Pos; tl > 0 { + t = max(min((float64(tl)/float64(cur.Length)), 1), 0) + } + } + + e.value = e.updater(cur.Y, next.Y, t) + return nil +} + +func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { + if e.keyed.done { + return false + } + + if e.settings.Envelope.Sustain.Enabled() && keyOn { + if e.settings.Envelope.Sustain.Length() == 0 { + return false + } + } else if e.settings.Envelope.Loop.Enabled() { + if e.settings.Envelope.Loop.Length() == 0 { + return false + } + } + + nPoints := len(e.settings.Envelope.Values) + + if nPoints == 0 { + e.keyed.done = true + return true + } + + e.keyed.pos++ + curTick, looped := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.keyed.pos, e.settings.Envelope.Length, keyOn) + + found := false + for _, i := range e.settings.Envelope.Values { + if i.Pos >= curTick { + found = true + break + } + } + + if !found { + e.keyed.done = true + return true + } + + if !keyOn && !looped && curTick >= e.settings.Length { + e.keyed.done = false + return true + } + + e.updateValue() + return false } diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 03b6cf5..dc65142 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -1,81 +1,27 @@ package component import ( + "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/types" ) // FilterEnvelope is a filter frequency cutoff modulation envelope type FilterEnvelope struct { - enabled bool - state envelope.State[int8] - value int8 - keyOn bool - prevKeyOn bool + baseEnvelope[types.PitchFiltValue, uint8] } -// Reset resets the state to defaults based on the envelope provided -func (e *FilterEnvelope) Reset(env *envelope.Envelope[int8]) { - e.state.Reset(env) - e.keyOn = false - e.prevKeyOn = false - e.update() +func (e *FilterEnvelope) Setup(settings EnvelopeSettings[types.PitchFiltValue, uint8]) { + e.baseEnvelope.Setup(settings, e.calc) } -// SetEnabled sets the enabled flag for the envelope -func (e *FilterEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled +func (e FilterEnvelope) Clone(onFinished voice.Callback) FilterEnvelope { + var m FilterEnvelope + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) + return m } -// IsEnabled returns the enabled flag for the envelope -func (e *FilterEnvelope) IsEnabled() bool { - return e.enabled -} - -// GetCurrentValue returns the current cached envelope value -func (e *FilterEnvelope) GetCurrentValue() int8 { - return e.value -} - -// SetEnvelopePosition sets the current position in the envelope -func (e *FilterEnvelope) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) - // TODO: this is gross, but currently the most optimal way to find the correct position - for i := 0; i < pos; i++ { - if doneCB := e.Advance(keyOn, prevKeyOn); doneCB != nil { - return doneCB - } - } - return nil -} - -// Advance advances the envelope state 1 tick and calculates the current envelope value -func (e *FilterEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { - e.keyOn = keyOn - e.prevKeyOn = prevKeyOn - var doneCB voice.Callback - if done := e.state.Advance(e.keyOn, e.prevKeyOn); done { - doneCB = e.state.Envelope().OnFinished - } - e.update() - return doneCB -} - -func (e *FilterEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) - - var y0 float32 - if cur != nil { - y0 = float32(cur.Value()) - } - - var y1 float32 - if next != nil { - y1 = float32(next.Value()) - } - - e.value = int8(y0 + t*(y1-y0)) +func (e *FilterEnvelope) calc(y0, y1 types.PitchFiltValue, t float64) uint8 { + v := util.Lerp(t, y0, y1) + return uint8(32 + v) } diff --git a/voice/component/envelope_pan.go b/voice/component/envelope_pan.go index a956980..ff2d8c5 100755 --- a/voice/component/envelope_pan.go +++ b/voice/component/envelope_pan.go @@ -1,85 +1,26 @@ package component import ( - "github.com/gotracker/gomixing/panning" - + "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/types" ) // PanEnvelope is a spatial modulation envelope -type PanEnvelope struct { - enabled bool - state envelope.State[panning.Position] - pan panning.Position - keyOn bool - prevKeyOn bool -} - -// Reset resets the state to defaults based on the envelope provided -func (e *PanEnvelope) Reset(env *envelope.Envelope[panning.Position]) { - e.state.Reset(env) - e.keyOn = false - e.prevKeyOn = false - e.update() -} - -// SetEnabled sets the enabled flag for the envelope -func (e *PanEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled -} - -// IsEnabled returns the enabled flag for the envelope -func (e *PanEnvelope) IsEnabled() bool { - return e.enabled -} - -// GetCurrentValue returns the current cached envelope value -func (e *PanEnvelope) GetCurrentValue() panning.Position { - return e.pan +type PanEnvelope[TPanning types.Panning] struct { + baseEnvelope[TPanning, TPanning] } -// SetEnvelopePosition sets the current position in the envelope -func (e *PanEnvelope) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) - // TODO: this is gross, but currently the most optimal way to find the correct position - for i := 0; i < pos; i++ { - if doneCB := e.Advance(keyOn, prevKeyOn); doneCB != nil { - return doneCB - } - } - return nil +func (e *PanEnvelope[TPanning]) Setup(settings EnvelopeSettings[TPanning, TPanning]) { + e.baseEnvelope.Setup(settings, e.calc) } -// Advance advances the envelope state 1 tick and calculates the current envelope value -func (e *PanEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { - e.keyOn = keyOn - e.prevKeyOn = prevKeyOn - var doneCB voice.Callback - if done := e.state.Advance(e.keyOn, e.prevKeyOn); done { - doneCB = e.state.Envelope().OnFinished - } - e.update() - return doneCB +func (e PanEnvelope[TPanning]) Clone(onFinished voice.Callback) PanEnvelope[TPanning] { + var m PanEnvelope[TPanning] + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) + return m } -func (e *PanEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) - - y0 := panning.CenterAhead - if cur != nil { - y0 = cur.Value() - } - - y1 := panning.CenterAhead - if next != nil { - y1 = next.Value() - } - - // TODO: perform an angular interpolation instead of a linear one. - e.pan.Angle = y0.Angle + t*(y1.Angle-y0.Angle) - e.pan.Distance = y0.Distance + t*(y1.Distance-y0.Distance) +func (e *PanEnvelope[TPanning]) calc(y0, y1 TPanning, t float64) TPanning { + return util.Lerp(t, y0, y1) } diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index e229fb3..c29c02e 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -2,82 +2,26 @@ package component import ( "github.com/gotracker/playback/period" + "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/types" ) // PitchEnvelope is an frequency modulation envelope type PitchEnvelope struct { - enabled bool - state envelope.State[int8] - delta period.Delta - keyOn bool - prevKeyOn bool + baseEnvelope[types.PitchFiltValue, period.Delta] } -// Reset resets the state to defaults based on the envelope provided -func (e *PitchEnvelope) Reset(env *envelope.Envelope[int8]) { - e.state.Reset(env) - e.keyOn = false - e.prevKeyOn = false - e.update() +func (e *PitchEnvelope) Setup(settings EnvelopeSettings[types.PitchFiltValue, period.Delta]) { + e.baseEnvelope.Setup(settings, e.calc) } -// SetEnabled sets the enabled flag for the envelope -func (e *PitchEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled +func (e PitchEnvelope) Clone(onFinished voice.Callback) PitchEnvelope { + var m PitchEnvelope + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) + return m } -// IsEnabled returns the enabled flag for the envelope -func (e *PitchEnvelope) IsEnabled() bool { - return e.enabled -} - -// GetCurrentValue returns the current cached envelope value -func (e *PitchEnvelope) GetCurrentValue() period.Delta { - return e.delta -} - -// SetEnvelopePosition sets the current position in the envelope -func (e *PitchEnvelope) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) - // TODO: this is gross, but currently the most optimal way to find the correct position - for i := 0; i < pos; i++ { - if doneCB := e.Advance(keyOn, prevKeyOn); doneCB != nil { - return doneCB - } - } - return nil -} - -// Advance advances the envelope state 1 tick and calculates the current envelope value -func (e *PitchEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { - e.keyOn = keyOn - e.prevKeyOn = prevKeyOn - var doneCB voice.Callback - if done := e.state.Advance(e.keyOn, e.prevKeyOn); done { - doneCB = e.state.Envelope().OnFinished - } - e.update() - return doneCB -} - -func (e *PitchEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) - - var y0 float32 - if cur != nil { - y0 = float32(cur.Value()) - } - - var y1 float32 - if next != nil { - y1 = float32(next.Value()) - } - - val := y0 + t*(y1-y0) - e.delta = period.Delta(-val) +func (e *PitchEnvelope) calc(y0, y1 types.PitchFiltValue, t float64) period.Delta { + return -period.Delta(util.Lerp(t, y0, y1)) } diff --git a/voice/component/envelope_volume.go b/voice/component/envelope_volume.go index 4ef157e..fcb3859 100755 --- a/voice/component/envelope_volume.go +++ b/voice/component/envelope_volume.go @@ -1,83 +1,26 @@ package component import ( - "github.com/gotracker/gomixing/volume" - + "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/types" ) // VolumeEnvelope is an amplitude modulation envelope -type VolumeEnvelope struct { - enabled bool - state envelope.State[volume.Volume] - vol volume.Volume - keyOn bool - prevKeyOn bool -} - -// Reset resets the state to defaults based on the envelope provided -func (e *VolumeEnvelope) Reset(env *envelope.Envelope[volume.Volume]) { - e.state.Reset(env) - e.keyOn = false - e.prevKeyOn = false - e.update() -} - -// SetEnabled sets the enabled flag for the envelope -func (e *VolumeEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled -} - -// IsEnabled returns the enabled flag for the envelope -func (e *VolumeEnvelope) IsEnabled() bool { - return e.enabled -} - -// GetCurrentValue returns the current cached envelope value -func (e *VolumeEnvelope) GetCurrentValue() volume.Volume { - return e.vol +type VolumeEnvelope[TVolume types.Volume] struct { + baseEnvelope[TVolume, TVolume] } -// SetEnvelopePosition sets the current position in the envelope -func (e *VolumeEnvelope) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) - // TODO: this is gross, but currently the most optimal way to find the correct position - for i := 0; i < pos; i++ { - if doneCB := e.Advance(keyOn, prevKeyOn); doneCB != nil { - return doneCB - } - } - return nil +func (e *VolumeEnvelope[TVolume]) Setup(settings EnvelopeSettings[TVolume, TVolume]) { + e.baseEnvelope.Setup(settings, e.calc) } -// Advance advances the envelope state 1 tick and calculates the current envelope value -func (e *VolumeEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { - e.keyOn = keyOn - e.prevKeyOn = prevKeyOn - var doneCB voice.Callback - if done := e.state.Advance(e.keyOn, e.prevKeyOn); done { - doneCB = e.state.Envelope().OnFinished - } - e.update() - return doneCB +func (e VolumeEnvelope[TVolume]) Clone(onFinished voice.Callback) VolumeEnvelope[TVolume] { + var m VolumeEnvelope[TVolume] + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) + return m } -func (e *VolumeEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) - - var y0 volume.Volume - if cur != nil { - y0 = cur.Value() - } - - var y1 volume.Volume - if next != nil { - y1 = next.Value() - } - - e.vol = y0 + volume.Volume(t)*(y1-y0) +func (e *VolumeEnvelope[TVolume]) calc(y0, y1 TVolume, t float64) TVolume { + return util.Lerp(t, y0, y1) } diff --git a/voice/component/modulator_amp.go b/voice/component/modulator_amp.go index 7d5de20..687ab14 100755 --- a/voice/component/modulator_amp.go +++ b/voice/component/modulator_amp.go @@ -1,102 +1,154 @@ package component import ( + "fmt" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/types" + "github.com/heucuva/optional" ) // AmpModulator is an amplitude (volume) modulator -type AmpModulator struct { - vol volume.Volume - mixing volume.Volume - fadeoutEnabled bool - fadeoutVol volume.Volume - fadeoutAmt volume.Volume - final volume.Volume // = [fadeoutVol *] mixing * vol +type AmpModulator[TMixingVolume, TVolume types.Volume] struct { + settings AmpModulatorSettings[TMixingVolume, TVolume] + + unkeyed struct { + active bool + muted bool + vol TVolume + mixing TMixingVolume + } + keyed struct { + delta types.VolumeDelta + mixingOverride optional.Value[TMixingVolume] + } + final volume.Volume // = active? * mixing * vol } -// Setup configures the initial settings of the modulator -func (a *AmpModulator) Setup(mixing volume.Volume) { - a.mixing = mixing - a.updateFinal() +type AmpModulatorSettings[TMixingVolume, TVolume types.Volume] struct { + Active bool + Muted bool + DefaultMixingVolume TMixingVolume + DefaultVolume TVolume } -// Attack disables the fadeout and resets its volume -func (a *AmpModulator) Attack() { - a.fadeoutEnabled = false - a.fadeoutVol = volume.Volume(1) - a.updateFinal() +func (a *AmpModulator[TMixingVolume, TVolume]) Setup(settings AmpModulatorSettings[TMixingVolume, TVolume]) { + a.settings = settings + a.unkeyed.active = settings.Active + a.unkeyed.muted = settings.Muted + a.unkeyed.vol = settings.DefaultVolume + a.unkeyed.mixing = settings.DefaultMixingVolume + a.Reset() } -// Release currently does nothing -func (a *AmpModulator) Release() { +func (a AmpModulator[TMixingVolume, TVolume]) Clone() AmpModulator[TMixingVolume, TVolume] { + m := a + return m } -// Fadeout activates the fadeout -func (a *AmpModulator) Fadeout() { - a.fadeoutEnabled = true - a.updateFinal() +func (a *AmpModulator[TMixingVolume, TVolume]) Reset() error { + a.keyed.delta = 0 + a.keyed.mixingOverride.Reset() + return a.updateFinal() } -// SetVolume sets the current volume (before fadeout calculation) -func (a *AmpModulator) SetVolume(vol volume.Volume) { - a.vol = vol - a.updateFinal() +func (a *AmpModulator[TMixingVolume, TVolume]) SetActive(active bool) error { + a.unkeyed.active = active + return a.updateFinal() } -// GetVolume returns the current volume (before fadeout calculation) -func (a *AmpModulator) GetVolume() volume.Volume { - return a.vol +func (a AmpModulator[TMixingVolume, TVolume]) IsActive() bool { + return a.unkeyed.active +} + +func (a *AmpModulator[TMixingVolume, TVolume]) SetMuted(muted bool) error { + a.unkeyed.muted = muted + return nil +} + +func (a AmpModulator[TMixingVolume, TVolume]) IsMuted() bool { + return a.unkeyed.muted +} + +// SetMixingVolume configures the mixing volume of the modulator +func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolume(mixing TMixingVolume) error { + if mixing.IsUseInstrumentVol() { + return nil + } + + a.unkeyed.mixing = mixing + return a.updateFinal() +} + +// GetMixingVolume returns the current mixing volume of the modulator +func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolume() TMixingVolume { + return a.unkeyed.mixing } -// SetFadeoutEnabled sets the status of the fadeout enablement flag -func (a *AmpModulator) SetFadeoutEnabled(enabled bool) { - a.fadeoutEnabled = enabled - a.updateFinal() +func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolumeOverride(mvo optional.Value[TMixingVolume]) error { + a.keyed.mixingOverride = mvo + return nil } -// ResetFadeoutValue resets the current fadeout value and optionally configures the amount of fadeout -func (a *AmpModulator) ResetFadeoutValue(amount ...volume.Volume) { - a.fadeoutVol = volume.Volume(1) - if len(amount) > 0 { - a.fadeoutAmt = amount[0] +func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolumeOverride() optional.Value[TMixingVolume] { + return a.keyed.mixingOverride +} + +// SetVolume sets the current volume (before fadeout calculation) +func (a *AmpModulator[TMixingVolume, TVolume]) SetVolume(vol TVolume) error { + if vol.IsUseInstrumentVol() { + vol = a.settings.DefaultVolume } - a.updateFinal() + a.unkeyed.vol = vol + return a.updateFinal() } -// IsFadeoutEnabled returns the status of the fadeout enablement flag -func (a *AmpModulator) IsFadeoutEnabled() bool { - return a.fadeoutEnabled +// GetVolume returns the current volume (before fadeout calculation) +func (a AmpModulator[TMixingVolume, TVolume]) GetVolume() TVolume { + return a.unkeyed.vol +} + +func (a *AmpModulator[TMixingVolume, TVolume]) SetVolumeDelta(d types.VolumeDelta) error { + a.keyed.delta = d + return a.updateFinal() } -// GetFadeoutVolume returns the value of the fadeout volume -func (a *AmpModulator) GetFadeoutVolume() volume.Volume { - return a.fadeoutVol +func (a AmpModulator[TMixingVolume, TVolume]) GetVolumeDelta() types.VolumeDelta { + return a.keyed.delta } // GetFinalVolume returns the current volume (after fadeout calculation) -func (a *AmpModulator) GetFinalVolume() volume.Volume { +func (a AmpModulator[TMixingVolume, TVolume]) GetFinalVolume() volume.Volume { return a.final } -// Advance advances the fadeout value by 1 tick -func (a *AmpModulator) Advance() { - if a.fadeoutEnabled || a.fadeoutVol <= 0 { - return +func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() error { + if !a.unkeyed.active { + a.final = 0 + return nil } - a.fadeoutVol -= a.fadeoutAmt - switch { - case a.fadeoutVol < 0: - a.fadeoutVol = 0 - case a.fadeoutVol > 1: - a.fadeoutVol = 1 + v := types.AddVolumeDelta(a.unkeyed.vol, a.keyed.delta) + + mv := a.unkeyed.mixing + if mvo, set := a.keyed.mixingOverride.Get(); set { + mv = mvo } - a.updateFinal() + + a.final = mv.ToVolume() * v.ToVolume() + return nil } -func (a *AmpModulator) updateFinal() { - a.final = a.mixing * a.vol - if a.fadeoutEnabled { - a.final *= a.fadeoutVol - } +func (a AmpModulator[TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} muted{%v} vol{%v} mixing{%v} mixingOverride{%v} delta{%v} final{%v}", + a.unkeyed.active, + a.unkeyed.muted, + a.unkeyed.vol, + a.unkeyed.mixing, + a.keyed.mixingOverride, + a.keyed.delta, + a.final, + ), comment) } diff --git a/voice/component/modulator_autovibrato.go b/voice/component/modulator_autovibrato.go new file mode 100644 index 0000000..bbfdff8 --- /dev/null +++ b/voice/component/modulator_autovibrato.go @@ -0,0 +1,100 @@ +package component + +import ( + "fmt" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/types" +) + +// AutoVibratoModulator is a frequency (pitch) modulator +type AutoVibratoModulator[TPeriod types.Period] struct { + settings autovibrato.AutoVibratoSettings[TPeriod] + unkeyed struct { + enabled bool + } + keyed struct { + age int // current age of oscillator (in ticks) + } + autoVibrato oscillator.Oscillator +} + +func (f *AutoVibratoModulator[TPeriod]) Setup(settings autovibrato.AutoVibratoSettings[TPeriod]) { + f.settings = settings + f.unkeyed.enabled = f.settings.Enabled + f.Reset() +} + +func (f AutoVibratoModulator[TPeriod]) Clone() AutoVibratoModulator[TPeriod] { + m := f + if f.autoVibrato != nil { + m.autoVibrato = f.autoVibrato.Clone() + } + return m +} + +func (f *AutoVibratoModulator[TPeriod]) Reset() error { + f.keyed.age = 0 + var err error + f.autoVibrato, err = f.settings.Generate(f.settings.Factory) + if err != nil { + return err + } + return f.ResetAutoVibrato() +} + +// SetEnabled sets the status of the AutoVibrato enablement flag +func (f *AutoVibratoModulator[TPeriod]) SetEnabled(enabled bool) { + f.unkeyed.enabled = enabled +} + +// ResetAutoVibrato resets the current AutoVibrato +func (f *AutoVibratoModulator[TPeriod]) ResetAutoVibrato() error { + if f.autoVibrato != nil { + f.autoVibrato.HardReset() + } + + f.keyed.age = 0 + return nil +} + +// IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag +func (f *AutoVibratoModulator[TPeriod]) IsAutoVibratoEnabled() bool { + return f.unkeyed.enabled +} + +// GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) +func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) (TPeriod, error) { + if !f.unkeyed.enabled { + return in, nil + } + + depth := f.settings.Depth + if f.settings.Sweep > f.keyed.age { + depth *= float32(f.keyed.age) / float32(f.settings.Sweep) + } + avDelta := f.autoVibrato.GetWave(depth) + d := period.Delta(avDelta) + return f.settings.PC.AddDelta(in, d) +} + +// Advance advances the autoVibrato value by 1 tick +func (f *AutoVibratoModulator[TPeriod]) Advance() { + if !f.unkeyed.enabled { + return + } + + f.autoVibrato.Advance(f.settings.Rate) + f.keyed.age++ +} + +func (f AutoVibratoModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} age{%v}", + f.unkeyed.enabled, + f.keyed.age, + ), comment) +} diff --git a/voice/component/modulator_fadeout.go b/voice/component/modulator_fadeout.go new file mode 100644 index 0000000..aebc7b3 --- /dev/null +++ b/voice/component/modulator_fadeout.go @@ -0,0 +1,84 @@ +package component + +import ( + "fmt" + + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" +) + +// FadeoutModulator is an amplitude (volume) modulator +type FadeoutModulator struct { + settings FadeoutModulatorSettings + unkeyed struct { + enabled bool + } + keyed struct { + vol volume.Volume + } +} + +type FadeoutModulatorSettings struct { + Enabled bool + GetActive func() bool + Amount volume.Volume +} + +func (a *FadeoutModulator) Setup(settings FadeoutModulatorSettings) { + a.settings = settings + a.unkeyed.enabled = settings.Enabled + a.Reset() +} + +func (a FadeoutModulator) Clone() FadeoutModulator { + m := a + return m +} + +// Reset disables the fadeout and resets its volume +func (a *FadeoutModulator) Reset() error { + a.keyed.vol = volume.Volume(1) + return nil +} + +// SetEnabled sets the status of the fadeout enable flag +func (a *FadeoutModulator) SetEnabled(enabled bool) error { + a.unkeyed.enabled = enabled + return nil +} + +// IsEnabled returns the status of the fadeout enablement flag +func (a FadeoutModulator) IsActive() bool { + if !a.unkeyed.enabled || a.settings.GetActive == nil { + return false + } + + return a.settings.GetActive() +} + +// GetVolume returns the value of the fadeout volume +func (a FadeoutModulator) GetVolume() volume.Volume { + return a.keyed.vol +} + +func (a FadeoutModulator) GetFinalVolume() volume.Volume { + if !a.IsActive() { + return volume.Volume(1) + } + return a.keyed.vol +} + +// Advance advances the fadeout value by 1 tick +func (a *FadeoutModulator) Advance() { + if a.IsActive() && a.keyed.vol > 0 { + a.keyed.vol = min(max(a.keyed.vol-a.settings.Amount, 0), 1) + } +} + +func (a FadeoutModulator) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} vol{%v}", + a.unkeyed.enabled, + a.keyed.vol, + ), comment) +} diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index e26c5bb..693b81a 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -1,94 +1,88 @@ package component import ( + "fmt" + + "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/tracing" ) // FreqModulator is a frequency (pitch) modulator -type FreqModulator struct { - period period.Period - delta period.Delta - autoVibratoEnabled bool - autoVibrato oscillator.Oscillator - autoVibratoDepth float32 - autoVibratoRate int - autoVibratoSweep int // maximum age when oscillator is at max depth (in ticks) - autoVibratoAge int // current age of oscillator (in ticks) +type FreqModulator[TPeriod period.Period] struct { + settings FreqModulatorSettings[TPeriod] + unkeyed struct { + period TPeriod + } + keyed struct { + delta period.Delta + } + final TPeriod } -// SetPeriod sets the current period (before AutoVibrato and Delta calculation) -func (a *FreqModulator) SetPeriod(period period.Period) { - a.period = period +type FreqModulatorSettings[TPeriod period.Period] struct { + PC period.PeriodConverter[TPeriod] } -// GetPeriod returns the current period (before AutoVibrato and Delta calculation) -func (a *FreqModulator) GetPeriod() period.Period { - return a.period +func (f *FreqModulator[TPeriod]) Setup(settings FreqModulatorSettings[TPeriod]) error { + f.settings = settings + var empty TPeriod + f.unkeyed.period = empty + return f.Reset() } -// SetDelta sets the current period delta (before AutoVibrato calculation) -func (a *FreqModulator) SetDelta(delta period.Delta) { - a.delta = delta +func (f *FreqModulator[TPeriod]) Reset() error { + f.keyed.delta = 0 + return f.updateFinal() } -// GetDelta returns the current period delta (before AutoVibrato calculation) -func (a *FreqModulator) GetDelta() period.Delta { - return a.delta +func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { + m := f + return m } -// SetAutoVibratoEnabled sets the status of the AutoVibrato enablement flag -func (a *FreqModulator) SetAutoVibratoEnabled(enabled bool) { - a.autoVibratoEnabled = enabled -} +// SetPeriod sets the current period (before AutoVibrato and Delta calculation) +func (f *FreqModulator[TPeriod]) SetPeriod(period TPeriod) error { + if period.IsInvalid() { + // ignore it for now + return nil + } -// ConfigureAutoVibrato sets the AutoVibrato oscillator settings -func (a *FreqModulator) ConfigureAutoVibrato(av voice.AutoVibrato) { - a.autoVibrato = av.Generate() - a.autoVibratoRate = int(av.Rate) - a.autoVibratoDepth = av.Depth + f.unkeyed.period = period + return f.updateFinal() } -// ResetAutoVibrato resets the current AutoVibrato -func (a *FreqModulator) ResetAutoVibrato(sweep ...int) { - if a.autoVibrato != nil { - a.autoVibrato.Reset(true) - } - - a.autoVibratoAge = 0 +// GetPeriod returns the current period (before AutoVibrato and Delta calculation) +func (f *FreqModulator[TPeriod]) GetPeriod() TPeriod { + return f.unkeyed.period +} - if sweep != nil { - a.autoVibratoSweep = sweep[0] - } +// SetPeriodDelta sets the current period delta (before AutoVibrato calculation) +func (f *FreqModulator[TPeriod]) SetPeriodDelta(delta period.Delta) error { + f.keyed.delta = delta + return f.updateFinal() } -// IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag -func (a *FreqModulator) IsAutoVibratoEnabled() bool { - return a.autoVibratoEnabled +// GetDelta returns the current period delta (before AutoVibrato calculation) +func (f *FreqModulator[TPeriod]) GetPeriodDelta() period.Delta { + return f.keyed.delta } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) -func (a *FreqModulator) GetFinalPeriod() period.Period { - p := a.period.AddDelta(a.delta) - if a.autoVibratoEnabled { - depth := a.autoVibratoDepth - if a.autoVibratoSweep > a.autoVibratoAge { - depth *= float32(a.autoVibratoAge) / float32(a.autoVibratoSweep) - } - avDelta := a.autoVibrato.GetWave(depth) - p = p.AddDelta(period.Delta(avDelta)) - } - - return p +func (f *FreqModulator[TPeriod]) GetFinalPeriod() (TPeriod, error) { + return f.final, nil } -// Advance advances the autoVibrato value by 1 tick -func (a *FreqModulator) Advance() { - if !a.autoVibratoEnabled { - return - } +func (f FreqModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("period{%v} delta{%v} final{%v}", + f.unkeyed.period, + f.keyed.delta, + f.final, + ), comment) +} - a.autoVibrato.Advance(a.autoVibratoRate) - a.autoVibratoAge++ +func (f *FreqModulator[TPeriod]) updateFinal() error { + var err error + f.final, err = f.settings.PC.AddDelta(f.unkeyed.period, f.keyed.delta) + return err } diff --git a/voice/component/modulator_key.go b/voice/component/modulator_key.go new file mode 100644 index 0000000..468273e --- /dev/null +++ b/voice/component/modulator_key.go @@ -0,0 +1,121 @@ +package component + +import ( + "fmt" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" +) + +type KeyModulator struct { + settings KeyModulatorSettings + slimKeyModulator + attackTriggersRelease bool + fadeout bool + deferredUpdates []bool +} + +type KeyModulatorSettings struct { + Attack func() + Release func() + Fadeout func() + DeferredAttack func() + DeferredRelease func() +} + +func (k *KeyModulator) Setup(settings KeyModulatorSettings) { + k.settings = settings +} + +func (k KeyModulator) GetAttackTriggersRelease() bool { + return k.attackTriggersRelease +} + +func (k *KeyModulator) SetAttackTriggersRelease(enabled bool) error { + k.attackTriggersRelease = enabled + return nil +} + +func (k *KeyModulator) DeferredUpdate() { + var deferredUpdates []bool + deferredUpdates, k.deferredUpdates = k.deferredUpdates, nil + for _, keyOn := range deferredUpdates { + if keyOn { + if k.settings.DeferredAttack != nil { + k.settings.DeferredAttack() + } + } else { + if k.settings.DeferredRelease != nil { + k.settings.DeferredRelease() + } + } + } +} + +func (k *KeyModulator) Attack() { + if k.attackTriggersRelease && k.prevKeyOn && k.keyOn { + k.Release() + } + + k.slimKeyModulator.Attack() + k.fadeout = false + + if k.settings.DeferredAttack != nil { + k.deferredUpdates = append(k.deferredUpdates, true) + } + + if k.settings.Attack != nil { + k.settings.Attack() + } +} + +func (k *KeyModulator) Release() { + k.slimKeyModulator.Release() + + if k.settings.DeferredRelease != nil { + k.deferredUpdates = append(k.deferredUpdates, false) + } + + if k.settings.Release != nil { + k.settings.Release() + } +} + +func (k KeyModulator) IsKeyFadeout() bool { + return k.fadeout +} + +func (k *KeyModulator) Fadeout() { + k.fadeout = true + if k.settings.Fadeout != nil { + k.settings.Fadeout() + } +} + +func (k *KeyModulator) Advance() { + k.prevKeyOn = k.keyOn +} + +func (k KeyModulator) Clone(settings KeyModulatorSettings) KeyModulator { + m := k + m.settings = settings + return m +} + +func (k KeyModulator) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("keyOn{%v} prevKeyOn{%v}", + k.keyOn, + k.prevKeyOn, + ), comment) +} + +func (k KeyModulator) String() string { + switch { + case k.fadeout: + return "Fadeout" + case k.keyOn: + return "Attack" + default: + return "Release" + } +} diff --git a/voice/component/modulator_pan.go b/voice/component/modulator_pan.go index eb95c7c..04ed488 100755 --- a/voice/component/modulator_pan.go +++ b/voice/component/modulator_pan.go @@ -1,29 +1,90 @@ package component import ( + "fmt" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/types" ) // PanModulator is an pan (spatial) modulator -type PanModulator struct { - pan panning.Position +type PanModulator[TPanning types.Panning] struct { + settings PanModulatorSettings[TPanning] + unkeyed struct { + pan TPanning + } + keyed struct { + delta types.PanDelta + } + final panning.Position +} + +type PanModulatorSettings[TPanning types.Panning] struct { + Enabled bool + InitialPan TPanning +} + +func (p *PanModulator[TPanning]) Setup(settings PanModulatorSettings[TPanning]) { + p.settings = settings + p.unkeyed.pan = settings.InitialPan + p.Reset() +} + +func (p *PanModulator[TPanning]) Reset() error { + p.keyed.delta = 0 + return p.updateFinal() +} + +func (p PanModulator[TPanning]) Clone() PanModulator[TPanning] { + m := p + return m } // SetPan sets the current panning -func (p *PanModulator) SetPan(vol panning.Position) { - p.pan = vol +func (p *PanModulator[TPanning]) SetPan(pan TPanning) error { + if !p.settings.Enabled { + return nil + } + + p.unkeyed.pan = pan + return p.updateFinal() } // GetPan returns the current panning -func (p *PanModulator) GetPan() panning.Position { - return p.pan +func (p PanModulator[TPanning]) GetPan() TPanning { + return p.unkeyed.pan +} + +// SetPanDelta sets the current panning delta +func (p *PanModulator[TPanning]) SetPanDelta(d types.PanDelta) error { + if !p.settings.Enabled { + return nil + } + + p.keyed.delta = d + return p.updateFinal() +} + +// GetPanDelta returns the current panning delta +func (p PanModulator[TPanning]) GetPanDelta() types.PanDelta { + return p.keyed.delta } // GetFinalPan returns the current panning -func (p *PanModulator) GetFinalPan() panning.Position { - return p.pan +func (p PanModulator[TPanning]) GetFinalPan() panning.Position { + return p.final +} + +func (p PanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("pan{%v} delta{%v}", + p.unkeyed.pan, + p.keyed.delta, + ), comment) } -// Advance advances the fadeout value by 1 tick -func (p *PanModulator) Advance() { +func (p *PanModulator[TPanning]) updateFinal() error { + p.final = types.AddPanningDelta(p.unkeyed.pan, p.keyed.delta).ToPosition() + return nil } diff --git a/voice/component/modulator_pitchpan.go b/voice/component/modulator_pitchpan.go new file mode 100644 index 0000000..b983f17 --- /dev/null +++ b/voice/component/modulator_pitchpan.go @@ -0,0 +1,96 @@ +package component + +import ( + "fmt" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/types" +) + +// PitchPanModulator is an pan (spatial) modulator +type PitchPanModulator[TPanning types.Panning] struct { + settings PitchPanModulatorSettings[TPanning] + unkeyed struct { + enabled bool + pitch note.Semitone + } + keyed struct{} + panSep float32 +} + +type PitchPanModulatorSettings[TPanning types.Panning] struct { + PitchPanEnable bool + PitchPanCenter note.Semitone + PitchPanSeparation float32 +} + +func (p *PitchPanModulator[TPanning]) Setup(settings PitchPanModulatorSettings[TPanning]) { + p.settings = settings + p.unkeyed.enabled = settings.PitchPanEnable + p.unkeyed.pitch = settings.PitchPanCenter + p.Reset() +} + +func (p PitchPanModulator[TPanning]) Clone() PitchPanModulator[TPanning] { + m := p + return m +} + +func (p *PitchPanModulator[TPanning]) Reset() error { + return p.updatePitchPan() +} + +// SetPitch updates the pan separation modulated by the provided pitch +func (p *PitchPanModulator[TPanning]) SetPitch(st note.Semitone) error { + p.unkeyed.pitch = st + return p.updatePitchPan() +} + +// IsPitchPanEnabled returns the enablement of the pitch-pan separation function +func (p PitchPanModulator[TPanning]) IsPitchPanEnabled() bool { + return p.unkeyed.enabled +} + +// EnablePitchPan enables the pitch-pan separation function +func (p *PitchPanModulator[TPanning]) EnablePitchPan(enabled bool) error { + p.unkeyed.enabled = enabled + return p.updatePitchPan() +} + +// SetPanSeparation gets the current pan separation +func (p PitchPanModulator[TPanning]) GetPanSeparation() float32 { + return p.panSep +} + +func (p PitchPanModulator[TPanning]) GetSeparatedPan(pan TPanning) TPanning { + if !p.unkeyed.enabled || p.panSep == 0 { + return pan + } + + updatedPan := float32(pan) + p.panSep + sepPan := TPanning(min(max(updatedPan, 0), float32(types.GetPanMax[TPanning]()))) + return sepPan +} + +// Advance advances the fadeout value by 1 tick +func (p *PitchPanModulator[TPanning]) Advance() { +} + +func (p *PitchPanModulator[TPanning]) updatePitchPan() error { + if !p.unkeyed.enabled { + return nil + } + + p.panSep = (float32(p.unkeyed.pitch) - float32(p.settings.PitchPanCenter)) * p.settings.PitchPanSeparation + return nil +} + +func (p PitchPanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} pitch{%v} panSep{%v}", + p.unkeyed.enabled, + p.unkeyed.pitch, + p.panSep, + ), comment) +} diff --git a/voice/component/opl2.go b/voice/component/opl2.go index 5ba3427..250bb05 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -4,8 +4,11 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/opl2" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/render" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/types" ) // OPL2Operator is a block of values specific to configuring an OPL operator (modulator or carrier) @@ -24,28 +27,48 @@ type OPL2Registers struct { RegC0 uint8 } +func (o OPL2Registers) Clone() OPL2Registers { + m := o + return m +} + // OPL2 is an OPL2 component -type OPL2 struct { - chip render.OPL2Chip - channel int - reg OPL2Registers - baseFreq period.Frequency - keyOn bool +type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { + chip *opl2.Chip + channel int + reg OPL2Registers + baseFreq frequency.Frequency + periodConverter period.PeriodConverter[TPeriod] + defaultVolume TVolume + keyOn bool } // Setup sets up the OPL2 component -func (o *OPL2) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Setup(chip *opl2.Chip, channel int, reg OPL2Registers, pc period.PeriodConverter[TPeriod], baseFreq frequency.Frequency, defaultVolume TVolume) { o.chip = chip o.channel = channel o.reg = reg o.baseFreq = baseFreq + o.periodConverter = pc + o.defaultVolume = defaultVolume o.keyOn = false } -// Attack activates the key-on bit -func (o *OPL2) Attack() { - o.keyOn = true +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Attack() { + // does nothing +} +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Release() { + // does nothing +} + +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Fadeout() { + // does nothing +} + +// DeferredAttack activates the key-on bit +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) DeferredAttack() { + o.keyOn = true // calculate the register addressing information index := uint32(o.channel) mod := o.getChannelIndex(o.channel) @@ -68,8 +91,8 @@ func (o *OPL2) Attack() { ch.WriteReg(0xC0|index, o.reg.RegC0) } -// Release deactivates the key-on bit -func (o *OPL2) Release() { +// DeferredRelease deactivates the key-on bit +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) DeferredRelease() { o.keyOn = false // calculate the register addressing information @@ -81,7 +104,7 @@ func (o *OPL2) Release() { } // Advance advances the playback -func (o *OPL2) Advance(carVol volume.Volume, period period.Period) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Advance(carVol volume.Volume, period TPeriod) { // calculate the register addressing information index := uint32(o.channel) mod := o.getChannelIndex(o.channel) @@ -111,17 +134,30 @@ func (o *OPL2) Advance(carVol volume.Volume, period period.Period) { ch.WriteReg(0xB0|index, regB0) } +func (o OPL2[TPeriod, TMixingVolume, TVolume]) Clone() Voicer[TPeriod, TMixingVolume, TVolume] { + m := o + return &m +} + +func (o OPL2[TPeriod, TMixingVolume, TVolume]) GetDefaultVolume() TVolume { + return o.defaultVolume +} + +func (o OPL2[TPeriod, TMixingVolume, TVolume]) GetNumChannels() int { + return 1 +} + // twoOperatorMelodic var twoOperatorMelodic = [...]uint32{ 0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12, 0x100, 0x101, 0x102, 0x108, 0x109, 0x10A, 0x110, 0x111, 0x112, } -func (o *OPL2) getChannelIndex(channelIdx int) uint32 { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) getChannelIndex(channelIdx int) uint32 { return twoOperatorMelodic[channelIdx%18] } -func (o *OPL2) calc40(reg40 uint8, vol volume.Volume) uint8 { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) calc40(reg40 uint8, vol volume.Volume) uint8 { oVol := volume.Volume(63-uint16(reg40&0x3f)) / 63 totalVol := oVol * vol * 63 if totalVol > 63 { @@ -134,21 +170,21 @@ func (o *OPL2) calc40(reg40 uint8, vol volume.Volume) uint8 { return result } -func (o *OPL2) periodToFreqBlock(period period.Period, baseFreq period.Frequency) (uint16, uint8) { - modFreq := period.GetFrequency() +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) periodToFreqBlock(p TPeriod, baseFreq frequency.Frequency) (uint16, uint8) { + modFreq := o.periodConverter.GetFrequency(p) freq := float64(baseFreq) * float64(modFreq) / 261625 return o.freqToFnumBlock(freq) } -func (o *OPL2) freqBlockToRegA0B0(freq uint16, block uint8) (uint8, uint8) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) freqBlockToRegA0B0(freq uint16, block uint8) (uint8, uint8) { regA0 := uint8(freq) regB0 := uint8(uint16(freq)>>8) & 0x03 regB0 |= (block & 0x07) << 3 return regA0, regB0 } -func (o *OPL2) freqToFnumBlock(freq float64) (uint16, uint8) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) freqToFnumBlock(freq float64) (uint16, uint8) { if freq > 6208.431 { return 0, 0 } @@ -175,3 +211,7 @@ func (o *OPL2) freqToFnumBlock(freq float64) (uint16, uint8) { return fnum, block } + +func (o OPL2[TPeriod, TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + // TODO - add state dumper +} diff --git a/voice/component/output.go b/voice/component/output.go deleted file mode 100644 index 3ead058..0000000 --- a/voice/component/output.go +++ /dev/null @@ -1,20 +0,0 @@ -package component - -import ( - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/voice" -) - -// OutputFilter applies a filter to a sample stream -type OutputFilter struct { - Input sampling.SampleStream - Output voice.FilterApplier -} - -// GetSample operates the filter -func (o *OutputFilter) GetSample(pos sampling.Pos) volume.Matrix { - dry := o.Input.GetSample(pos) - return o.Output.ApplyFilter(dry) -} diff --git a/voice/component/sampler.go b/voice/component/sampler.go index 45f8390..d4f360b 100644 --- a/voice/component/sampler.go +++ b/voice/component/sampler.go @@ -1,58 +1,105 @@ package component import ( + "fmt" + "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/pcm" + "github.com/gotracker/playback/voice/types" ) // Sampler is a sampler component -type Sampler struct { - sample pcm.Sample - pos sampling.Pos - keyOn bool - loopsEnabled bool - wholeLoop loop.Loop - sustainLoop loop.Loop +type Sampler[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { + settings SamplerSettings[TPeriod, TMixingVolume, TVolume] + + unkeyed struct { + pos sampling.Pos + mixVol volume.Volume + } + keyed struct { + loopsEnabled bool + } + + slimKeyModulator +} + +type SamplerSettings[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { + Sample pcm.Sample + DefaultVolume TVolume + MixVolume TMixingVolume + WholeLoop loop.Loop + SustainLoop loop.Loop } // Setup sets up the sampler -func (s *Sampler) Setup(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop) { - s.sample = sample - s.wholeLoop = wholeLoop - s.sustainLoop = sustainLoop +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Setup(settings SamplerSettings[TPeriod, TMixingVolume, TVolume]) { + s.settings = settings + s.unkeyed.pos = sampling.Pos{} + s.unkeyed.mixVol = settings.MixVolume.ToVolume() + s.Reset() +} + +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Reset() { + s.keyed.loopsEnabled = false +} + +func (s Sampler[TPeriod, TMixingVolume, TVolume]) Clone() Voicer[TPeriod, TMixingVolume, TVolume] { + m := s + return &m } // SetPos sets the current position of the sampler in the pcm data (and loops) -func (s *Sampler) SetPos(pos sampling.Pos) { - s.pos = pos +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) SetPos(pos sampling.Pos) { + s.unkeyed.pos = pos } // GetPos returns the current position of the sampler in the pcm data (and loops) -func (s *Sampler) GetPos() sampling.Pos { - return s.pos +func (s Sampler[TPeriod, TMixingVolume, TVolume]) GetPos() sampling.Pos { + return s.unkeyed.pos } // Attack sets the key-on value (for loop processing) -func (s *Sampler) Attack() { - s.keyOn = true - s.loopsEnabled = true +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Attack() { + s.slimKeyModulator.Attack() + s.keyed.loopsEnabled = true } // Release releases the key-on value (for loop processing) -func (s *Sampler) Release() { - s.keyOn = false +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Release() { + s.slimKeyModulator.Release() } // Fadeout disables the loops (for loop processing) -func (s *Sampler) Fadeout() { - s.loopsEnabled = false +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Fadeout() { + s.keyed.loopsEnabled = false +} + +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) DeferredAttack() { + // does nothing +} + +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) DeferredRelease() { + // does nothing +} + +func (s Sampler[TPeriod, TMixingVolume, TVolume]) GetDefaultVolume() TVolume { + return s.settings.DefaultVolume +} + +func (s Sampler[TPeriod, TMixingVolume, TVolume]) GetNumChannels() int { + if s.settings.Sample == nil { + return 0 + } + return s.settings.Sample.Channels() } // GetSample returns a multi-channel sample at the specified position -func (s *Sampler) GetSample(pos sampling.Pos) volume.Matrix { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) GetSample(pos sampling.Pos) volume.Matrix { v0 := s.getConvertedSample(pos.Pos) if v0.Channels == 0 { if s.canLoop() { @@ -63,43 +110,60 @@ func (s *Sampler) GetSample(pos sampling.Pos) volume.Matrix { } if pos.Frac == 0 { - return v0 + return v0.Apply(s.unkeyed.mixVol) } v1 := s.getConvertedSample(pos.Pos + 1) - return v0.Lerp(v1, pos.Frac) + lerped := v0.Lerp(v1, pos.Frac) + return lerped.Apply(s.unkeyed.mixVol) } -func (s *Sampler) canLoop() bool { - switch { - case !s.loopsEnabled: - return false - case s.keyOn && s.sustainLoop.Enabled(): - return true - case s.wholeLoop.Enabled(): - return true +func (s Sampler[TPeriod, TMixingVolume, TVolume]) canLoop() bool { + if s.keyed.loopsEnabled { + return (s.keyOn && s.settings.SustainLoop.Enabled()) || s.settings.WholeLoop.Enabled() } return false } -func (s *Sampler) getConvertedSample(pos int) volume.Matrix { - if s.sample == nil { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) getConvertedSample(pos int) volume.Matrix { + if s.settings.Sample == nil { return volume.Matrix{} } - sl := s.sample.Length() - if pos >= sl && !s.canLoop() { - return volume.Matrix{} + sl := s.settings.Sample.Length() + fadeout := false + fadeoutLen := 0 + if pos >= sl { + if s.canLoop() { + pos, _ = loop.CalcLoopPos(s.settings.WholeLoop, s.settings.SustainLoop, pos, sl, s.keyOn) + } else { + fadeoutLen = pos - sl + pos = sl - 1 + fadeout = true + } } - opos := pos - pos, _ = loop.CalcLoopPos(s.wholeLoop, s.sustainLoop, pos, sl, s.keyOn) - _ = opos if pos < 0 || pos >= sl { return volume.Matrix{} } - s.sample.Seek(pos) - data, err := s.sample.Read() + s.settings.Sample.Seek(pos) + data, err := s.settings.Sample.Read() if err != nil { return volume.Matrix{} } - return data + + if !fadeout { + return data + } + if fadeoutLen >= 32 { + return data.Apply(0) + } + + atten := volume.Volume(1) / volume.Volume(int(1<= c.settings.MaxRowsAt0 +} + +func (c Vol0Optimization) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} rowsAt0{%v}", + c.unkeyed.enabled, + c.keyed.rowsAt0, + ), comment) +} diff --git a/voice/config.go b/voice/config.go new file mode 100644 index 0000000..82ed08a --- /dev/null +++ b/voice/config.go @@ -0,0 +1,27 @@ +package voice + +import ( + "github.com/gotracker/opl2" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/types" + "github.com/gotracker/playback/voice/vol0optimization" +) + +type ( + Period = types.Period + Volume = types.Volume + Panning = types.Panning +) + +type VoiceConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + PC period.PeriodConverter[TPeriod] + OPLChip *opl2.Chip + OPLChannel index.OPLChannel + InitialVolume TVolume + InitialMixing TMixingVolume + PanEnabled bool + InitialPan TPanning + Vol0Optimization vol0optimization.Vol0OptimizationSettings +} diff --git a/voice/envelope/envelope.go b/voice/envelope/envelope.go index 2bc0cf0..6b3d596 100644 --- a/voice/envelope/envelope.go +++ b/voice/envelope/envelope.go @@ -4,130 +4,11 @@ import ( "github.com/gotracker/playback/voice/loop" ) -// State is the state information about an envelope -type State[T any] struct { - position int - length int - stopped bool - env *Envelope[T] -} - -// Stopped returns true if the envelope state is stopped -func (e *State[T]) Stopped() bool { - return e.stopped -} - -// Stop stops the envelope state -func (e *State[T]) Stop() { - e.stopped = true -} - -// Envelope returns the envelope that the state is based on -func (e *State[T]) Envelope() *Envelope[T] { - return e.env -} - -// Reset resets the envelope -func (e *State[T]) Reset(env *Envelope[T]) { - e.env = env - if e.env == nil || !e.env.Enabled { - e.stopped = true - return - } - - e.position = 0 - pos, _, _ := e.calcLoopedPos(true) - if pos < len(e.env.Values) { - e.length = e.env.Values[pos].Length() - } -} - -func (e *State[T]) calcLoopedPos(keyOn bool) (int, int, bool) { - nPoints := len(e.env.Values) - var looped bool - cur, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.position, nPoints, keyOn) - next, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.position+1, nPoints, keyOn) - if (keyOn && e.env.Sustain.Enabled()) || e.env.Loop.Enabled() { - looped = true - } - return cur, next, looped -} - -// GetCurrentValue returns the current value -func (e *State[T]) GetCurrentValue(keyOn bool) (*EnvPoint[T], *EnvPoint[T], float32) { - if e.stopped { - return nil, nil, 0 - } - - pos, npos, looped := e.calcLoopedPos(keyOn) - if pos >= len(e.env.Values) { - return nil, nil, 0 - } - - if npos >= len(e.env.Values) { - npos = pos - } - - cur := e.env.Values[pos] - next := e.env.Values[npos] - t := float32(0) - tl := cur.Length() - if tl > 0 { - l := float32(e.length) - if looped { - if e.env.Sustain.Enabled() && keyOn && e.env.Sustain.Length() == 0 { - l = 0 - } else { - l = float32(e.length) - } - } - t = 1 - (l / float32(tl)) - } - switch { - case t < 0: - t = 0 - case t > 1: - t = 1 - } - return &cur, &next, t -} - -// Advance advances the state by 1 tick -func (e *State[T]) Advance(keyOn bool, prevKeyOn bool) bool { - if e.stopped { - return false - } - - if e.env.Sustain.Enabled() && keyOn { - if e.env.Sustain.Length() == 0 { - return false - } - } else if e.env.Loop.Enabled() { - if e.env.Loop.Length() == 0 { - return false - } - } - -loopAdvance: - e.length-- - if e.length > 0 { - return false - } - if keyOn != prevKeyOn && prevKeyOn { - p, _, _ := e.calcLoopedPos(prevKeyOn) - e.position = p - } - - e.position++ - pos, _, _ := e.calcLoopedPos(keyOn) - if pos >= len(e.env.Values) { - e.stopped = true - return true - } - - e.length = e.env.Values[pos].Length() - if e.length <= 0 { - goto loopAdvance - } - return false +// Envelope is an envelope for instruments +type Envelope[T any] struct { + Enabled bool + Loop loop.Loop + Sustain loop.Loop + Length int + Values []Point[T] } diff --git a/voice/envelope/instrumentenv.go b/voice/envelope/instrumentenv.go deleted file mode 100755 index 89104b4..0000000 --- a/voice/envelope/instrumentenv.go +++ /dev/null @@ -1,34 +0,0 @@ -package envelope - -import ( - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/loop" -) - -// Envelope is an envelope for instruments -type Envelope[T any] struct { - Enabled bool - Loop loop.Loop - Sustain loop.Loop - Values []EnvPoint[T] - OnFinished voice.Callback -} - -// EnvPoint is a point for the envelope -type EnvPoint[T any] struct { - Ticks int - Y T -} - -func (p EnvPoint[T]) Length() int { - return p.Ticks -} - -func (p EnvPoint[T]) Value() T { - return p.Y -} - -func (p *EnvPoint[T]) Init(ticks int, value T) { - p.Ticks = ticks - p.Y = value -} diff --git a/voice/envelope/point.go b/voice/envelope/point.go new file mode 100644 index 0000000..2d93de7 --- /dev/null +++ b/voice/envelope/point.go @@ -0,0 +1,8 @@ +package envelope + +// Point is a point for the envelope +type Point[T any] struct { + Pos int + Length int + Y T +} diff --git a/voice/fadeout/fadeout.go b/voice/fadeout/fadeout.go index af4b61d..d29fb65 100755 --- a/voice/fadeout/fadeout.go +++ b/voice/fadeout/fadeout.go @@ -21,3 +21,16 @@ type Settings struct { Mode Mode Amount volume.Volume } + +func (m Mode) IsFadeoutActive(forceFadeout, volEnvEnabled, volEnvDone bool) bool { + switch m { + case ModeDisabled: + return false + case ModeAlwaysActive: + return forceFadeout || !volEnvEnabled || volEnvDone + case ModeOnlyIfVolEnvActive: + return forceFadeout || volEnvEnabled + default: + return false + } +} diff --git a/voice/filter/filterapplier.go b/voice/filter/filterapplier.go new file mode 100644 index 0000000..e91e197 --- /dev/null +++ b/voice/filter/filterapplier.go @@ -0,0 +1,11 @@ +package filter + +import ( + "github.com/gotracker/gomixing/volume" +) + +// Applier is an interface for applying a filter to a sample stream +type Applier interface { + ApplyFilter(dry volume.Matrix) volume.Matrix + SetFilterEnvelopeValue(envVal uint8) +} diff --git a/voice/filterapplier.go b/voice/filterapplier.go deleted file mode 100755 index c2b7858..0000000 --- a/voice/filterapplier.go +++ /dev/null @@ -1,11 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/volume" -) - -// FilterApplier is an interface for applying a filter to a sample stream -type FilterApplier interface { - ApplyFilter(dry volume.Matrix) volume.Matrix - SetFilterEnvelopeValue(envVal int8) -} diff --git a/voice/filterenveloper.go b/voice/filterenveloper.go deleted file mode 100755 index 59a3c7d..0000000 --- a/voice/filterenveloper.go +++ /dev/null @@ -1,9 +0,0 @@ -package voice - -// FilterEnveloper is a filter envelope interface -type FilterEnveloper interface { - EnableFilterEnvelope(enabled bool) - IsFilterEnvelopeEnabled() bool - GetCurrentFilterEnvelope() int8 - SetFilterEnvelopePosition(pos int) -} diff --git a/voice/freqmodulator.go b/voice/freqmodulator.go deleted file mode 100755 index c46ffb7..0000000 --- a/voice/freqmodulator.go +++ /dev/null @@ -1,14 +0,0 @@ -package voice - -import ( - "github.com/gotracker/playback/period" -) - -// FreqModulator is the instrument frequency control interface -type FreqModulator interface { - SetPeriod(period period.Period) - GetPeriod() period.Period - SetPeriodDelta(delta period.Delta) - GetPeriodDelta() period.Delta - GetFinalPeriod() period.Period -} diff --git a/voice/loop/disabled.go b/voice/loop/disabled.go index 6e0b789..c8369ee 100644 --- a/voice/loop/disabled.go +++ b/voice/loop/disabled.go @@ -16,12 +16,5 @@ func (l *Disabled) Length() int { // CalcPos calculates the position based on the loop details func (l *Disabled) CalcPos(pos int, length int) (int, bool) { - switch { - case pos < 0: - return 0, false - case pos < length: - return pos, false - default: - return length, false - } + return min(max(pos, 0), length), false } diff --git a/voice/mixer/details.go b/voice/mixer/details.go new file mode 100644 index 0000000..3b3ce39 --- /dev/null +++ b/voice/mixer/details.go @@ -0,0 +1,16 @@ +package mixer + +import ( + "time" + + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/playback/frequency" +) + +type Details struct { + Mix *mixing.Mixer + Panmixer mixing.PanMixer + SampleRate frequency.Frequency + Samples int + Duration time.Duration +} diff --git a/voice/mixer/output.go b/voice/mixer/output.go new file mode 100644 index 0000000..d47ae9b --- /dev/null +++ b/voice/mixer/output.go @@ -0,0 +1,23 @@ +package mixer + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" +) + +// Output applies a filter to a sample stream +type Output struct { + Input sampling.SampleStream + Output ApplyFilter +} + +// GetSample operates the filter +// must be pointer receiver +func (o *Output) GetSample(pos sampling.Pos) volume.Matrix { + dry := o.Input.GetSample(pos) + return o.Output.ApplyFilter(dry) +} + +type ApplyFilter interface { + ApplyFilter(dry volume.Matrix) volume.Matrix +} diff --git a/voice/oscillator/oscillator.go b/voice/oscillator/oscillator.go index 317c2db..19fe5d4 100755 --- a/voice/oscillator/oscillator.go +++ b/voice/oscillator/oscillator.go @@ -5,8 +5,11 @@ type WaveTableSelect uint8 // Oscillator is an oscillator type Oscillator interface { + Clone() Oscillator GetWave(depth float32) float32 Advance(speed int) SetWaveform(table WaveTableSelect) - Reset(hard ...bool) + GetWaveform() WaveTableSelect + HardReset() + Reset() } diff --git a/voice/panenveloper.go b/voice/panenveloper.go deleted file mode 100755 index 00ffb01..0000000 --- a/voice/panenveloper.go +++ /dev/null @@ -1,13 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/panning" -) - -// PanEnveloper is a pan envelope interface -type PanEnveloper interface { - EnablePanEnvelope(enabled bool) - IsPanEnvelopeEnabled() bool - GetCurrentPanEnvelope() panning.Position - SetPanEnvelopePosition(pos int) -} diff --git a/voice/panmodulator.go b/voice/panmodulator.go deleted file mode 100755 index 9738b19..0000000 --- a/voice/panmodulator.go +++ /dev/null @@ -1,12 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/panning" -) - -// PanModulator is the instrument pan (spatial) control interface -type PanModulator interface { - SetPan(vol panning.Position) - GetPan() panning.Position - GetFinalPan() panning.Position -} diff --git a/voice/pcm/bit16.go b/voice/pcm/bit16.go index fe2dde0..6920a35 100755 --- a/voice/pcm/bit16.go +++ b/voice/pcm/bit16.go @@ -15,12 +15,12 @@ const ( type Sample16BitSigned struct{} // Volume returns the volume value for the sample -func (s Sample16BitSigned) volume(v int16) volume.Volume { +func (Sample16BitSigned) volume(v int16) volume.Volume { return volume.Volume(v) * cSample16BitVolumeCoeff } // Size returns the size of the sample in bytes -func (s Sample16BitSigned) Size() int { +func (Sample16BitSigned) Size() int { return cSample16BitBytes } @@ -37,16 +37,24 @@ func (s Sample16BitSigned) ReadAt(d *SampleData, ofs int64) (volume.Volume, erro return s.volume(v), nil } +func (Sample16BitSigned) Format(d *SampleData) SampleDataFormat { + if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { + return SampleDataFormat16BitBESigned + } else { + return SampleDataFormat16BitLESigned + } +} + // Sample16BitUnsigned is an unsigned 16-bit sample type Sample16BitUnsigned struct{} // Volume returns the volume value for the sample -func (s Sample16BitUnsigned) volume(v uint16) volume.Volume { +func (Sample16BitUnsigned) volume(v uint16) volume.Volume { return volume.Volume(int16(v-0x8000)) * cSample16BitVolumeCoeff } // Size returns the size of the sample in bytes -func (s Sample16BitUnsigned) Size() int { +func (Sample16BitUnsigned) Size() int { return cSample16BitBytes } @@ -62,3 +70,11 @@ func (s Sample16BitUnsigned) ReadAt(d *SampleData, ofs int64) (volume.Volume, er v := uint16(d.byteOrder.Uint16(d.data[ofs:])) return s.volume(v), nil } + +func (Sample16BitUnsigned) Format(d *SampleData) SampleDataFormat { + if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { + return SampleDataFormat16BitBEUnsigned + } else { + return SampleDataFormat16BitLEUnsigned + } +} diff --git a/voice/pcm/bit32float.go b/voice/pcm/bit32float.go index 0cd88ac..d92b755 100755 --- a/voice/pcm/bit32float.go +++ b/voice/pcm/bit32float.go @@ -16,7 +16,7 @@ const ( type Sample32BitFloat struct{} // Size returns the size of the sample in bytes -func (s Sample32BitFloat) Size() int { +func (Sample32BitFloat) Size() int { return cSample32BitFloatBytes } @@ -32,3 +32,11 @@ func (s Sample32BitFloat) ReadAt(d *SampleData, ofs int64) (volume.Volume, error v := math.Float32frombits(d.byteOrder.Uint32(d.data[ofs:])) return volume.Volume(v), nil } + +func (Sample32BitFloat) Format(d *SampleData) SampleDataFormat { + if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { + return SampleDataFormat32BitBEFloat + } else { + return SampleDataFormat32BitLEFloat + } +} diff --git a/voice/pcm/bit64float.go b/voice/pcm/bit64float.go index 4e0c6fb..7f7d298 100755 --- a/voice/pcm/bit64float.go +++ b/voice/pcm/bit64float.go @@ -16,7 +16,7 @@ const ( type Sample64BitFloat struct{} // Size returns the size of the sample in bytes -func (s Sample64BitFloat) Size() int { +func (Sample64BitFloat) Size() int { return cSample64BitFloatBytes } @@ -32,3 +32,11 @@ func (s Sample64BitFloat) ReadAt(d *SampleData, ofs int64) (volume.Volume, error f := math.Float64frombits(d.byteOrder.Uint64(d.data[ofs:])) return volume.Volume(f), nil } + +func (Sample64BitFloat) Format(d *SampleData) SampleDataFormat { + if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { + return SampleDataFormat64BitBEFloat + } else { + return SampleDataFormat64BitLEFloat + } +} diff --git a/voice/pcm/bit8.go b/voice/pcm/bit8.go index a74ad21..b27ed4e 100755 --- a/voice/pcm/bit8.go +++ b/voice/pcm/bit8.go @@ -15,12 +15,12 @@ const ( type Sample8BitSigned struct{} // Volume returns the volume value for the sample -func (s Sample8BitSigned) volume(v int8) volume.Volume { +func (Sample8BitSigned) volume(v int8) volume.Volume { return volume.Volume(v) * cSample8BitVolumeCoeff } // Size returns the size of the sample in bytes -func (s Sample8BitSigned) Size() int { +func (Sample8BitSigned) Size() int { return cSample8BitBytes } @@ -37,16 +37,20 @@ func (s Sample8BitSigned) ReadAt(d *SampleData, ofs int64) (volume.Volume, error return s.volume(v), nil } +func (Sample8BitSigned) Format(d *SampleData) SampleDataFormat { + return SampleDataFormat8BitSigned +} + // Sample8BitUnsigned is an unsigned 8-bit sample type Sample8BitUnsigned struct{} // Volume returns the volume value for the sample -func (s Sample8BitUnsigned) volume(v uint8) volume.Volume { +func (Sample8BitUnsigned) volume(v uint8) volume.Volume { return volume.Volume(int8(v-0x80)) * cSample8BitVolumeCoeff } // Size returns the size of the sample in bytes -func (s Sample8BitUnsigned) Size() int { +func (Sample8BitUnsigned) Size() int { return cSample8BitBytes } @@ -62,3 +66,7 @@ func (s Sample8BitUnsigned) ReadAt(d *SampleData, ofs int64) (volume.Volume, err v := uint8(d.data[ofs]) return s.volume(v), nil } + +func (Sample8BitUnsigned) Format(d *SampleData) SampleDataFormat { + return SampleDataFormat8BitUnsigned +} diff --git a/voice/pcm/converter.go b/voice/pcm/converter.go index 9584bfa..6c080f5 100755 --- a/voice/pcm/converter.go +++ b/voice/pcm/converter.go @@ -6,4 +6,5 @@ import "github.com/gotracker/gomixing/volume" type SampleConverter interface { Size() int ReadAt(s *SampleData, ofs int64) (volume.Volume, error) + Format(s *SampleData) SampleDataFormat } diff --git a/voice/pcm/format.go b/voice/pcm/format.go index cc7dd53..d6e7622 100755 --- a/voice/pcm/format.go +++ b/voice/pcm/format.go @@ -29,3 +29,28 @@ const ( ) const SampleDataFormatNative = math.MaxUint8 + +func getSampleBytes(sdf SampleDataFormat) int { + switch sdf { + case SampleDataFormat8BitUnsigned: + return Sample8BitUnsigned{}.Size() + + case SampleDataFormat8BitSigned: + return Sample8BitSigned{}.Size() + + case SampleDataFormat16BitLEUnsigned, SampleDataFormat16BitBEUnsigned: + return Sample16BitUnsigned{}.Size() + + case SampleDataFormat16BitLESigned, SampleDataFormat16BitBESigned: + return Sample16BitSigned{}.Size() + + case SampleDataFormat32BitLEFloat, SampleDataFormat32BitBEFloat: + return Sample32BitFloat{}.Size() + + case SampleDataFormat64BitLEFloat, SampleDataFormat64BitBEFloat: + return Sample64BitFloat{}.Size() + + default: + return 1 + } +} diff --git a/voice/pcm/sample_native.go b/voice/pcm/sample_native.go index 732ebb0..245a4ca 100755 --- a/voice/pcm/sample_native.go +++ b/voice/pcm/sample_native.go @@ -1,7 +1,11 @@ package pcm import ( + "bytes" + "encoding/base64" + "encoding/binary" "errors" + "math" "github.com/gotracker/gomixing/volume" ) @@ -60,6 +64,21 @@ func (s *NativeSampleData) readData() (volume.Matrix, error) { return samp, nil } +func (s NativeSampleData) Format() SampleDataFormat { + return SampleDataFormat64BitLEFloat +} + +func (s NativeSampleData) Base64() string { + var src bytes.Buffer + for _, d := range s.data { + for c := 0; c < s.channels; c++ { + binary.Write(&src, binary.LittleEndian, math.Float64bits(float64(d.StaticMatrix[c]))) + } + } + + return base64.StdEncoding.EncodeToString(src.Bytes()) +} + func NewSampleNative(data []volume.Matrix, length int, channels int) Sample { return &SampleReaderNative{ NativeSampleData: NativeSampleData{ diff --git a/voice/pcm/sampledata.go b/voice/pcm/sampledata.go index ffaadd5..eec5e5d 100755 --- a/voice/pcm/sampledata.go +++ b/voice/pcm/sampledata.go @@ -2,6 +2,7 @@ package pcm import ( "bytes" + "encoding/base64" "encoding/binary" "errors" "math" @@ -13,9 +14,11 @@ import ( type Sample interface { SampleReader Channels() int + Format() SampleDataFormat Length() int Seek(pos int) Tell() int + Base64() string } // SampleData is the presentation of the core data of the sample @@ -51,12 +54,17 @@ func (s *SampleData) Tell() int { return s.pos } +func (s SampleData) Base64() string { + return base64.StdEncoding.EncodeToString(s.data) +} + // NewSample constructs a sampler that can handle the requested sampler format func NewSample(data []byte, length int, channels int, format SampleDataFormat) Sample { base := baseSampleData{ length: length, channels: channels, } + switch format { case SampleDataFormat8BitSigned: return &PCMReader[Sample8BitSigned]{ @@ -213,3 +221,15 @@ func ConvertTo(from Sample, format SampleDataFormat) (Sample, error) { to := NewSample(cvt.Bytes(), length, channels, format) return to, nil } + +func NewSampleFromBase64(channels int, format SampleDataFormat, data string) Sample { + d, err := base64.StdEncoding.DecodeString(data) + if err != nil { + panic(err) + } + + sb := getSampleBytes(format) + length := len(d) / (sb * channels) + + return NewSample(d, length, channels, format) +} diff --git a/voice/pcm/samplereader.go b/voice/pcm/samplereader.go index cb4f2e8..1c7971b 100644 --- a/voice/pcm/samplereader.go +++ b/voice/pcm/samplereader.go @@ -11,6 +11,10 @@ type PCMReader[TConverter SampleConverter] struct { cnv TConverter } -func (s *PCMReader[T]) Read() (volume.Matrix, error) { +func (s *PCMReader[TConverter]) Read() (volume.Matrix, error) { return s.readData(s.cnv) } + +func (s PCMReader[TConverter]) Format() SampleDataFormat { + return s.cnv.Format(&s.SampleData) +} diff --git a/voice/pitchenveloper.go b/voice/pitchenveloper.go deleted file mode 100755 index 08d6739..0000000 --- a/voice/pitchenveloper.go +++ /dev/null @@ -1,13 +0,0 @@ -package voice - -import ( - "github.com/gotracker/playback/period" -) - -// PitchEnveloper is a pitch envelope interface -type PitchEnveloper interface { - EnablePitchEnvelope(enabled bool) - IsPitchEnvelopeEnabled() bool - GetCurrentPitchEnvelope() period.Delta - SetPitchEnvelopePosition(pos int) -} diff --git a/voice/pitchpan/pitchpan.go b/voice/pitchpan/pitchpan.go new file mode 100644 index 0000000..1b902c8 --- /dev/null +++ b/voice/pitchpan/pitchpan.go @@ -0,0 +1,9 @@ +package pitchpan + +import "github.com/gotracker/playback/note" + +type PitchPan struct { + Enabled bool + Center note.Semitone + Separation float32 +} diff --git a/voice/positioner.go b/voice/positioner.go deleted file mode 100644 index 4307473..0000000 --- a/voice/positioner.go +++ /dev/null @@ -1,11 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/sampling" -) - -// Positioner is the instrument position (timeline) control interface -type Positioner interface { - SetPos(pos sampling.Pos) - GetPos() sampling.Pos -} diff --git a/voice/render.go b/voice/render.go new file mode 100644 index 0000000..c7f1b02 --- /dev/null +++ b/voice/render.go @@ -0,0 +1,78 @@ +package voice + +import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/mixer" +) + +func RenderAndTick[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details, out mixer.ApplyFilter) (*mixing.Data, error) { + if in.IsDone() { + return nil, nil + } + + defer in.Tick() + + rs, ok := in.(RenderSampler[TPeriod]) + if !ok { + return nil, nil + } + + if !rs.IsActive() { + return nil, nil + } + + pos, err := rs.GetPos() + if err != nil { + return nil, err + } + + p, err := rs.GetFinalPeriod() + if err != nil { + return nil, err + } + + if err := in.SetPlaybackRate(details.SampleRate); err != nil { + return nil, err + } + + samplerAdd := float32(pc.GetSamplerAdd(p, rs.GetSampleRate(), details.SampleRate)) + + o := mixer.Output{ + Input: rs, + Output: out, + } + + sampler := sampling.NewSampler(&o, pos, samplerAdd) + + // ... so grab the new value now. + pan := rs.GetFinalPan() + + // make a stand-alone data buffer for this channel for this tick + sampleData := mixing.SampleMixIn{ + Sample: sampler, + StaticVol: volume.Volume(1.0), + VolMatrix: centerAheadPan, + MixPos: 0, + MixLen: details.Samples, + } + + mixBuffer := details.Mix.NewMixBuffer(details.Samples) + mixBuffer.MixInSample(sampleData) + data := &mixing.Data{ + Data: mixBuffer, + Pan: pan, + Volume: volume.Volume(1.0), + Pos: 0, + SamplesLen: details.Samples, + } + + // reflect the sampling position back to the voice + if err := rs.SetPos(sampler.GetPosition()); err != nil { + return nil, err + } + + return data, nil +} diff --git a/voice/render/opl2chip.go b/voice/render/opl2chip.go deleted file mode 100755 index feb5369..0000000 --- a/voice/render/opl2chip.go +++ /dev/null @@ -1,12 +0,0 @@ -package render - -// OPL2Chip sets up a contract that the chip definition will contain these interfaces -type OPL2Chip interface { - WriteReg(uint32, uint8) - GenerateBlock2(uint, []int32) -} - -// OPL2Intf is an interface to get the active OPL2 chip -type OPL2Intf interface { - GetOPL2Chip() OPL2Chip -} diff --git a/voice/transaction.go b/voice/transaction.go deleted file mode 100755 index e04a295..0000000 --- a/voice/transaction.go +++ /dev/null @@ -1,51 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/period" -) - -// Transaction is an interface for updating Voice settings -type Transaction interface { - Cancel() - Commit() - GetVoice() Voice - Clone() Transaction - - SetActive(active bool) - IsPendingActive() (bool, bool) - IsCurrentlyActive() bool - - Attack() - Release() - Fadeout() - SetPeriod(period period.Period) - GetPendingPeriod() (period.Period, bool) - GetCurrentPeriod() period.Period - SetPeriodDelta(delta period.Delta) - GetPendingPeriodDelta() (period.Delta, bool) - GetCurrentPeriodDelta() period.Delta - SetVolume(vol volume.Volume) - GetPendingVolume() (volume.Volume, bool) - GetCurrentVolume() volume.Volume - SetPos(pos sampling.Pos) - GetPendingPos() (sampling.Pos, bool) - GetCurrentPos() sampling.Pos - SetPan(pan panning.Position) - GetPendingPan() (panning.Position, bool) - GetCurrentPan() panning.Position - SetVolumeEnvelopePosition(pos int) - EnableVolumeEnvelope(enabled bool) - IsPendingVolumeEnvelopeEnabled() (bool, bool) - IsCurrentVolumeEnvelopeEnabled() bool - SetPitchEnvelopePosition(pos int) - EnablePitchEnvelope(enabled bool) - SetPanEnvelopePosition(pos int) - EnablePanEnvelope(enabled bool) - SetFilterEnvelopePosition(pos int) - EnableFilterEnvelope(enabled bool) - SetAllEnvelopePositions(pos int) -} diff --git a/voice/types/pandelta.go b/voice/types/pandelta.go new file mode 100644 index 0000000..1d9824e --- /dev/null +++ b/voice/types/pandelta.go @@ -0,0 +1,3 @@ +package types + +type PanDelta float32 diff --git a/voice/types/panning.go b/voice/types/panning.go new file mode 100644 index 0000000..6c6ea2b --- /dev/null +++ b/voice/types/panning.go @@ -0,0 +1,34 @@ +package types + +import "github.com/gotracker/gomixing/panning" + +type Panning interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 + IsInvalid() bool + ToPosition() panning.Position +} + +type PanningInformationer[TPanning Panning] interface { + GetDefault() TPanning + GetMax() TPanning +} + +func GetPanDefault[TPanning Panning]() TPanning { + var pd TPanning + return any(pd).(PanningInformationer[TPanning]).GetDefault() +} + +func GetPanMax[TPanning Panning]() TPanning { + var pd TPanning + return any(pd).(PanningInformationer[TPanning]).GetMax() +} + +type PanningDeltaer[TPanning Panning] interface { + AddDelta(d PanDelta) TPanning +} + +func AddPanningDelta[TPanning Panning](v TPanning, d PanDelta) TPanning { + return any(v).(PanningDeltaer[TPanning]).AddDelta(d) +} diff --git a/voice/types/period.go b/voice/types/period.go new file mode 100644 index 0000000..40fc25b --- /dev/null +++ b/voice/types/period.go @@ -0,0 +1,9 @@ +package types + +import ( + "github.com/gotracker/playback/period" +) + +type Period interface { + period.Period +} diff --git a/voice/types/pitchfiltervalue.go b/voice/types/pitchfiltervalue.go new file mode 100644 index 0000000..e14734c --- /dev/null +++ b/voice/types/pitchfiltervalue.go @@ -0,0 +1,3 @@ +package types + +type PitchFiltValue = int8 diff --git a/voice/types/volume.go b/voice/types/volume.go new file mode 100644 index 0000000..caedbe3 --- /dev/null +++ b/voice/types/volume.go @@ -0,0 +1,29 @@ +package types + +import "github.com/gotracker/gomixing/volume" + +type Volume interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 + IsInvalid() bool + IsUseInstrumentVol() bool + ToVolume() volume.Volume +} + +type VolumeMaxer[TVolume Volume] interface { + GetMax() TVolume +} + +func GetMaxVolume[TVolume Volume]() TVolume { + var vm TVolume + return any(vm).(VolumeMaxer[TVolume]).GetMax() +} + +type VolumeDeltaer[TVolume Volume] interface { + AddDelta(d VolumeDelta) TVolume +} + +func AddVolumeDelta[TVolume Volume](v TVolume, d VolumeDelta) TVolume { + return any(v).(VolumeDeltaer[TVolume]).AddDelta(d) +} diff --git a/voice/types/volumedelta.go b/voice/types/volumedelta.go new file mode 100644 index 0000000..0197859 --- /dev/null +++ b/voice/types/volumedelta.go @@ -0,0 +1,3 @@ +package types + +type VolumeDelta float32 diff --git a/voice/voice.go b/voice/voice.go index c170375..14e00f5 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -1,241 +1,152 @@ package voice import ( - "time" - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/opl2" + "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/types" ) -// Voice is a voice interface type Voice interface { - Controller - sampling.SampleStream - // == optional control interfaces == - //Positioner - //FreqModulator - //AmpModulator - //PanModulator - //VolumeEnveloper - //PanEnveloper - //PitchEnveloper - //FilterEnveloper - - // == required function interfaces == - Advance(tickDuration time.Duration) - GetSampler(samplerRate float32) sampling.Sampler - Clone() Voice - StartTransaction() Transaction -} + Clone(background bool) Voice + DumpState(ch index.Channel, t tracing.Tracer) -// Controller is the instrument actuation control interface -type Controller interface { + // Configuration + Reset() error + SetPlaybackRate(outputRate frequency.Frequency) error + + // Actions Attack() Release() Fadeout() - IsKeyOn() bool - IsFadeout() bool - IsDone() bool - SetActive(active bool) - IsActive() bool -} + Stop() -// == Positioner == - -// SetPos sets the position within the positioner, if the interface for it exists on the voice -func SetPos(v Voice, pos sampling.Pos) { - if p, ok := v.(Positioner); ok { - p.SetPos(pos) - } -} - -// GetPos gets the position from the positioner, if the interface for it exists on the voice -func GetPos(v Voice) sampling.Pos { - if p, ok := v.(Positioner); ok { - return p.GetPos() - } - return sampling.Pos{} -} - -// == FreqModulator == - -// SetPeriod sets the period into the frequency modulator, if the interface for it exists on the voice -func SetPeriod(v Voice, period period.Period) { - if fm, ok := v.(FreqModulator); ok { - fm.SetPeriod(period) - } -} - -// GetPeriod gets the period from the frequency modulator, if the interface for it exists on the voice -func GetPeriod(v Voice) period.Period { - if fm, ok := v.(FreqModulator); ok { - return fm.GetPeriod() - } - return nil -} - -// SetPeriodDelta sets the period delta into the frequency modulator, if the interface for it exists on the voice -func SetPeriodDelta(v Voice, delta period.Delta) { - if fm, ok := v.(FreqModulator); ok { - fm.SetPeriodDelta(delta) - } -} + // State Machine Update + Tick() error + RowEnd() error -// GetPeriodDelta returns the period delta from the frequency modulator, if the interface for it exists on the voice -func GetPeriodDelta(v Voice) period.Delta { - if fm, ok := v.(FreqModulator); ok { - return fm.GetPeriodDelta() - } - return period.Delta(0) -} - -// GetFinalPeriod returns the final period from the frequency modulator, if the interface for it exists on the voice -func GetFinalPeriod(v Voice) period.Period { - if fm, ok := v.(FreqModulator); ok { - return fm.GetFinalPeriod() - } - return nil -} - -// == AmpModulator == - -// SetVolume sets the volume into the amplitude modulator, if the interface for it exists on the voice -func SetVolume(v Voice, vol volume.Volume) { - if am, ok := v.(AmpModulator); ok { - am.SetVolume(vol) - } -} - -// GetVolume gets the volume from the amplitude modulator, if the interface for it exists on the voice -func GetVolume(v Voice) volume.Volume { - if am, ok := v.(AmpModulator); ok { - return am.GetVolume() - } - return volume.Volume(1) + // General Parameters + IsDone() bool + SetMuted(muted bool) error + IsMuted() bool + GetSampleRate() frequency.Frequency } -// GetFinalVolume returns the final volume from the amplitude modulator, if the interface for it exists on the voice -func GetFinalVolume(v Voice) volume.Volume { - if am, ok := v.(AmpModulator); ok { - return am.GetFinalVolume() - } - return volume.Volume(1) +type VoiceOPL2er interface { + SetOPL2Chip(chip *opl2.Chip) } -// == PanModulator == +type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + Voice -// SetPan sets the period into the pan modulator, if the interface for it exists on the voice -func SetPan(v Voice, pan panning.Position) { - if pm, ok := v.(PanModulator); ok { - pm.SetPan(pan) - } + // Configuration + SetPlaybackRate(outputRate frequency.Frequency) error + Setup(inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) error } -// GetPan gets the period from the pan modulator, if the interface for it exists on the voice -func GetPan(v Voice) panning.Position { - if pm, ok := v.(PanModulator); ok { - return pm.GetPan() - } - return panning.CenterAhead +type AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { + // Amp/Volume Parameters + IsActive() bool + SetActive(active bool) error + GetMixingVolume() TMixingVolume + SetMixingVolume(v TMixingVolume) error + GetVolume() TVolume + SetVolume(v TVolume) error + GetVolumeDelta() types.VolumeDelta + SetVolumeDelta(d types.VolumeDelta) error + GetFinalVolume() volume.Volume } -// GetFinalPan returns the final panning position from the pan modulator, if the interface for it exists on the voice -func GetFinalPan(v Voice) panning.Position { - if pm, ok := v.(PanModulator); ok { - return pm.GetFinalPan() - } - return panning.CenterAhead +type FadeoutModulator interface { + IsFadeout() bool + GetFadeoutVolume() volume.Volume } -// == VolumeEnveloper == - -// EnableVolumeEnvelope sets the volume envelope enable flag, if the interface for it exists on the voice -func EnableVolumeEnvelope(v Voice, enabled bool) { - if ve, ok := v.(VolumeEnveloper); ok { - ve.EnableVolumeEnvelope(enabled) - } +type FreqModulator[TPeriod Period] interface { + // Frequency/Pitch Parameters + GetPeriod() TPeriod + SetPeriod(p TPeriod) error + GetPeriodDelta() period.Delta + SetPeriodDelta(delta period.Delta) error + GetFinalPeriod() (TPeriod, error) } -// IsVolumeEnvelopeEnabled returns true if the volume envelope is enabled and the interface for it exists on the voice -func IsVolumeEnvelopeEnabled(v Voice) bool { - if ve, ok := v.(VolumeEnveloper); ok { - return ve.IsVolumeEnvelopeEnabled() - } - return false +type Sampler interface { + // Sampler Parameters + SetPos(pos sampling.Pos) error + GetPos() (sampling.Pos, error) } -// SetVolumeEnvelopePosition sets the volume envelope position, if the interface for it exists on the voice -func SetVolumeEnvelopePosition(v Voice, pos int) { - if ve, ok := v.(VolumeEnveloper); ok { - ve.SetVolumeEnvelopePosition(pos) - } -} +type RenderSampler[TPeriod Period] interface { + Voice + sampling.SampleStream -// == PanEnveloper == + IsActive() bool -// EnablePanEnvelope sets the pan envelope enable flag, if the interface for it exists on the voice -func EnablePanEnvelope(v Voice, enabled bool) { - if pe, ok := v.(PanEnveloper); ok { - pe.EnablePanEnvelope(enabled) - } -} + SetPos(pos sampling.Pos) error + GetPos() (sampling.Pos, error) -// SetPanEnvelopePosition sets the pan envelope position, if the interface for it exists on the voice -func SetPanEnvelopePosition(v Voice, pos int) { - if pe, ok := v.(PanEnveloper); ok { - pe.SetPanEnvelopePosition(pos) - } + GetFinalPeriod() (TPeriod, error) + GetFinalVolume() volume.Volume + GetFinalPan() panning.Position } -// == PitchEnveloper == - -// EnablePitchEnvelope sets the pitch envelope enable flag, if the interface for it exists on the voice -func EnablePitchEnvelope(v Voice, enabled bool) { - if pe, ok := v.(PitchEnveloper); ok { - pe.EnablePitchEnvelope(enabled) - } +type PanModulator[TPanning Panning] interface { + // Pan Parameters + GetPan() TPanning + SetPan(pan TPanning) error + GetPanDelta() types.PanDelta + SetPanDelta(d types.PanDelta) error + GetFinalPan() panning.Position } -// SetPitchEnvelopePosition sets the pitch envelope position, if the interface for it exists on the voice -func SetPitchEnvelopePosition(v Voice, pos int) { - if pe, ok := v.(PitchEnveloper); ok { - pe.SetPitchEnvelopePosition(pos) - } +type PitchPanModulator[TPanning Panning] interface { + SetPitchPanNote(st note.Semitone) error + IsPitchPanEnabled() bool + EnablePitchPan(enabled bool) error + GetPanSeparation() float32 } -// == FilterEnveloper == - -// EnableFilterEnvelope sets the filter envelope enable flag, if the interface for it exists on the voice -func EnableFilterEnvelope(v Voice, enabled bool) { - if pe, ok := v.(FilterEnveloper); ok { - pe.EnableFilterEnvelope(enabled) - } +type VolumeEnvelope[TGlobalVolume, TMixingVolume, TVolume Volume] interface { + // Amp/Volume Envelope Parameters + IsVolumeEnvelopeEnabled() bool + EnableVolumeEnvelope(enabled bool) error + GetVolumeEnvelopePosition() int + SetVolumeEnvelopePosition(pos int) error + GetCurrentVolumeEnvelope() TVolume } -// SetFilterEnvelopePosition sets the filter envelope position, if the interface for it exists on the voice -func SetFilterEnvelopePosition(v Voice, pos int) { - if pe, ok := v.(FilterEnveloper); ok { - pe.SetFilterEnvelopePosition(pos) - } +type PitchEnvelope[TPeriod Period] interface { + // Frequency/Pitch Envelope Parameters + IsPitchEnvelopeEnabled() bool + EnablePitchEnvelope(enabled bool) error + GetPitchEnvelopePosition() int + SetPitchEnvelopePosition(pos int) error + GetCurrentPitchEnvelope() period.Delta } -// GetCurrentFilterEnvelope returns the filter envelope's current value, if the interface for it exists on the voice -func GetCurrentFilterEnvelope(v Voice) int8 { - if pe, ok := v.(FilterEnveloper); ok { - return pe.GetCurrentFilterEnvelope() - } - return 1 +type PanEnvelope[TPanning Panning] interface { + // Pan Envelope Parameters + IsPanEnvelopeEnabled() bool + EnablePanEnvelope(enabled bool) error + GetPanEnvelopePosition() int + SetPanEnvelopePosition(pos int) error + GetCurrentPanEnvelope() TPanning } -// == Envelopes == - -// SetEnvelopePosition sets the envelope position(s) on the voice -func SetAllEnvelopePositions(v Voice, pos int) { - SetVolumeEnvelopePosition(v, pos) - SetPanEnvelopePosition(v, pos) - SetPitchEnvelopePosition(v, pos) - SetFilterEnvelopePosition(v, pos) +type FilterEnvelope interface { + // Filter Envelope Parameters + IsFilterEnvelopeEnabled() bool + EnableFilterEnvelope(enabled bool) error + GetFilterEnvelopePosition() int + SetFilterEnvelopePosition(pos int) error + GetCurrentFilterEnvelope() uint8 } diff --git a/voice/voicefactory.go b/voice/voicefactory.go new file mode 100644 index 0000000..a300fde --- /dev/null +++ b/voice/voicefactory.go @@ -0,0 +1,5 @@ +package voice + +type VoiceFactory[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + NewVoice(settings VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] +} diff --git a/voice/vol0optimization/settings.go b/voice/vol0optimization/settings.go new file mode 100644 index 0000000..5c06271 --- /dev/null +++ b/voice/vol0optimization/settings.go @@ -0,0 +1,6 @@ +package vol0optimization + +type Vol0OptimizationSettings struct { + Enabled bool + MaxRowsAt0 int +} diff --git a/voice/volumeenveloper.go b/voice/volumeenveloper.go deleted file mode 100755 index 4392ac3..0000000 --- a/voice/volumeenveloper.go +++ /dev/null @@ -1,13 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/volume" -) - -// VolumeEnveloper is a volume envelope interface -type VolumeEnveloper interface { - EnableVolumeEnvelope(enabled bool) - IsVolumeEnvelopeEnabled() bool - GetCurrentVolumeEnvelope() volume.Volume - SetVolumeEnvelopePosition(pos int) -}