From 5b14bf9ab0eb9d5fb0f0c7d69307640c4d82918e Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 16 Dec 2023 14:02:35 -0800 Subject: [PATCH 01/63] Update to go 1.21 - update dependencies - some clean up use of generics --- channel.go | 5 +- effect.go | 46 +-- format/it/effect/effect_arpeggio.go | 4 +- format/it/effect/effect_channelvolumeslide.go | 4 +- format/it/effect/effect_extrafineportadown.go | 2 +- format/it/effect/effect_extrafineportaup.go | 2 +- format/it/effect/effect_finepatterndelay.go | 2 +- format/it/effect/effect_fineportadown.go | 2 +- format/it/effect/effect_fineportaup.go | 2 +- format/it/effect/effect_finevibrato.go | 4 +- format/it/effect/effect_finevolslidedown.go | 6 +- format/it/effect/effect_finevolslideup.go | 6 +- format/it/effect/effect_globalvolumeslide.go | 4 +- format/it/effect/effect_highoffset.go | 2 +- .../effect_newnoteactionnotecontinue.go | 2 +- .../it/effect/effect_newnoteactionnotecut.go | 2 +- .../it/effect/effect_newnoteactionnotefade.go | 2 +- .../it/effect/effect_newnoteactionnoteoff.go | 2 +- format/it/effect/effect_notecut.go | 4 +- format/it/effect/effect_notedelay.go | 4 +- format/it/effect/effect_orderjump.go | 4 +- format/it/effect/effect_panningenvelopeoff.go | 2 +- format/it/effect/effect_panningenvelopeon.go | 2 +- format/it/effect/effect_pastnotecut.go | 2 +- format/it/effect/effect_pastnotefadeout.go | 2 +- format/it/effect/effect_pastnoteoff.go | 2 +- format/it/effect/effect_patterndelay.go | 4 +- format/it/effect/effect_patternloop.go | 4 +- format/it/effect/effect_pitchenvelopeoff.go | 2 +- format/it/effect/effect_pitchenvelopeon.go | 2 +- format/it/effect/effect_portadown.go | 4 +- format/it/effect/effect_portatonote.go | 4 +- format/it/effect/effect_portaup.go | 4 +- format/it/effect/effect_portavolslide.go | 2 +- format/it/effect/effect_retrigvolslide.go | 4 +- format/it/effect/effect_rowjump.go | 4 +- format/it/effect/effect_sampleoffset.go | 2 +- format/it/effect/effect_setchannelvolume.go | 2 +- .../it/effect/effect_setcoarsepanposition.go | 2 +- format/it/effect/effect_setfinetune.go | 4 +- format/it/effect/effect_setglobalvolume.go | 4 +- .../it/effect/effect_setpanbrellowaveform.go | 2 +- format/it/effect/effect_setpanposition.go | 2 +- format/it/effect/effect_setspeed.go | 4 +- format/it/effect/effect_settempo.go | 6 +- format/it/effect/effect_settremolowaveform.go | 2 +- format/it/effect/effect_setvibratowaveform.go | 2 +- format/it/effect/effect_tremolo.go | 4 +- format/it/effect/effect_tremor.go | 4 +- format/it/effect/effect_vibrato.go | 4 +- format/it/effect/effect_vibratovolslide.go | 2 +- format/it/effect/effect_volslidedown.go | 6 +- format/it/effect/effect_volslideup.go | 6 +- format/it/effect/effect_volumeenvelopeoff.go | 2 +- format/it/effect/effect_volumeenvelopeon.go | 2 +- format/it/effect/effectfactory.go | 16 +- format/it/effect/unhandled.go | 4 +- format/it/effect/util.go | 26 +- format/it/layout/song.go | 8 +- format/it/load/itformat.go | 21 +- format/it/pattern/pattern.go | 333 +---------------- format/it/pattern/row.go | 16 + format/it/pattern/state.go | 338 ++++++++++++++++++ format/it/playback/channeldata_transaction.go | 10 +- format/it/playback/playback.go | 8 +- format/it/playback/playback_command.go | 8 +- format/it/playback/playback_pattern.go | 4 +- format/it/playback/playback_textoutput.go | 2 +- format/s3m/effect/effect_arpeggio.go | 4 +- format/s3m/effect/effect_enablefilter.go | 2 +- .../s3m/effect/effect_extrafineportadown.go | 2 +- format/s3m/effect/effect_extrafineportaup.go | 2 +- format/s3m/effect/effect_finepatterndelay.go | 2 +- format/s3m/effect/effect_fineportadown.go | 2 +- format/s3m/effect/effect_fineportaup.go | 2 +- format/s3m/effect/effect_finevibrato.go | 4 +- format/s3m/effect/effect_finevolslidedown.go | 4 +- format/s3m/effect/effect_finevolslideup.go | 4 +- format/s3m/effect/effect_notecut.go | 4 +- format/s3m/effect/effect_notedelay.go | 4 +- format/s3m/effect/effect_orderjump.go | 4 +- format/s3m/effect/effect_patterndelay.go | 4 +- format/s3m/effect/effect_patternloop.go | 4 +- format/s3m/effect/effect_portadown.go | 4 +- format/s3m/effect/effect_portatonote.go | 4 +- format/s3m/effect/effect_portaup.go | 4 +- format/s3m/effect/effect_portavolslide.go | 2 +- format/s3m/effect/effect_retrigvolslide.go | 4 +- format/s3m/effect/effect_rowjump.go | 4 +- format/s3m/effect/effect_sampleoffset.go | 2 +- format/s3m/effect/effect_setfinetune.go | 4 +- format/s3m/effect/effect_setglobalvolume.go | 4 +- format/s3m/effect/effect_setpanposition.go | 2 +- format/s3m/effect/effect_setspeed.go | 4 +- format/s3m/effect/effect_settempo.go | 6 +- .../s3m/effect/effect_settremolowaveform.go | 2 +- .../s3m/effect/effect_setvibratowaveform.go | 2 +- format/s3m/effect/effect_stereocontrol.go | 2 +- format/s3m/effect/effect_tremolo.go | 4 +- format/s3m/effect/effect_tremor.go | 4 +- format/s3m/effect/effect_vibrato.go | 4 +- format/s3m/effect/effect_vibratovolslide.go | 2 +- format/s3m/effect/effect_volslidedown.go | 4 +- format/s3m/effect/effect_volslideup.go | 4 +- format/s3m/effect/effectfactory.go | 56 +-- format/s3m/effect/unhandled.go | 2 +- format/s3m/effect/util.go | 22 +- format/s3m/layout/song.go | 6 +- format/s3m/load/s3mformat.go | 39 +- format/s3m/pattern/pattern.go | 332 +---------------- format/s3m/pattern/row.go | 16 + format/s3m/pattern/state.go | 337 +++++++++++++++++ .../s3m/playback/channeldata_transaction.go | 10 +- format/s3m/playback/playback.go | 10 +- format/s3m/playback/playback_command.go | 8 +- format/s3m/playback/playback_pattern.go | 4 +- format/s3m/playback/playback_textoutput.go | 2 +- format/xm/effect/effect_arpeggio.go | 4 +- format/xm/effect/effect_extrafineportadown.go | 2 +- format/xm/effect/effect_extrafineportaup.go | 2 +- format/xm/effect/effect_fineportadown.go | 2 +- format/xm/effect/effect_fineportaup.go | 2 +- format/xm/effect/effect_finevolslidedown.go | 2 +- format/xm/effect/effect_finevolslideup.go | 2 +- format/xm/effect/effect_globalvolumeslide.go | 4 +- format/xm/effect/effect_notecut.go | 4 +- format/xm/effect/effect_notedelay.go | 4 +- format/xm/effect/effect_orderjump.go | 4 +- format/xm/effect/effect_panslide.go | 2 +- format/xm/effect/effect_patterndelay.go | 4 +- format/xm/effect/effect_patternloop.go | 2 +- format/xm/effect/effect_portadown.go | 4 +- format/xm/effect/effect_portatonote.go | 4 +- format/xm/effect/effect_portaup.go | 4 +- format/xm/effect/effect_portavolslide.go | 2 +- format/xm/effect/effect_retriggernote.go | 4 +- format/xm/effect/effect_retrigvolslide.go | 4 +- format/xm/effect/effect_rowjump.go | 4 +- format/xm/effect/effect_sampleoffset.go | 2 +- .../xm/effect/effect_setcoarsepanposition.go | 2 +- .../xm/effect/effect_setenvelopeposition.go | 2 +- format/xm/effect/effect_setfinetune.go | 4 +- format/xm/effect/effect_setglobalvolume.go | 4 +- format/xm/effect/effect_setpanposition.go | 2 +- format/xm/effect/effect_setspeed.go | 4 +- format/xm/effect/effect_settempo.go | 6 +- format/xm/effect/effect_settremolowaveform.go | 2 +- format/xm/effect/effect_setvibratowaveform.go | 2 +- format/xm/effect/effect_setvolume.go | 2 +- format/xm/effect/effect_tremolo.go | 4 +- format/xm/effect/effect_tremor.go | 4 +- format/xm/effect/effect_vibrato.go | 4 +- format/xm/effect/effect_vibratovolslide.go | 2 +- format/xm/effect/effect_volslide.go | 4 +- format/xm/effect/effectfactory.go | 16 +- format/xm/effect/unhandled.go | 4 +- format/xm/effect/util.go | 26 +- format/xm/layout/song.go | 6 +- format/xm/load/xmformat.go | 36 +- format/xm/pattern/pattern.go | 333 +---------------- format/xm/pattern/row.go | 16 + format/xm/pattern/state.go | 338 ++++++++++++++++++ format/xm/playback/channeldata_transaction.go | 10 +- format/xm/playback/playback.go | 8 +- format/xm/playback/playback_command.go | 8 +- format/xm/playback/playback_pattern.go | 4 +- format/xm/playback/playback_textoutput.go | 2 +- go.mod | 4 +- go.sum | 5 +- pattern/pattern.go | 45 --- player/state/channel.go | 146 ++++---- player/state/channel_transaction.go | 60 ++-- song/pattern.go | 12 +- song/row.go | 4 +- song/song.go | 4 +- 175 files changed, 1619 insertions(+), 1559 deletions(-) create mode 100644 format/it/pattern/row.go create mode 100644 format/it/pattern/state.go create mode 100644 format/s3m/pattern/row.go create mode 100644 format/s3m/pattern/state.go create mode 100644 format/xm/pattern/row.go create mode 100644 format/xm/pattern/state.go delete mode 100644 pattern/pattern.go diff --git a/channel.go b/channel.go index 0f6e866..3fd8b8c 100644 --- a/channel.go +++ b/channel.go @@ -6,6 +6,7 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/instrument" @@ -13,7 +14,7 @@ import ( ) // Channel is an interface for channel state -type Channel[TMemory, TChannelData any] interface { +type Channel[TMemory any] interface { ResetRetriggerCount() SetMemory(*TMemory) GetMemory() *TMemory @@ -21,7 +22,7 @@ type Channel[TMemory, TChannelData any] interface { SetActiveVolume(volume.Volume) FreezePlayback() UnfreezePlayback() - GetData() *TChannelData + GetData() song.ChannelData GetPortaTargetPeriod() period.Period SetPortaTargetPeriod(period.Period) GetTargetPeriod() period.Period diff --git a/effect.go b/effect.go index 81c686e..da42d71 100644 --- a/effect.go +++ b/effect.go @@ -7,13 +7,13 @@ type Effect interface { //fmt.Stringer } -type effectPreStartIntf[TMemory, TChannelData any] interface { - PreStart(Channel[TMemory, TChannelData], Playback) error +type effectPreStartIntf[TMemory any] interface { + PreStart(Channel[TMemory], Playback) error } // 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 { +func EffectPreStart[TMemory any](e Effect, cs Channel[TMemory], p Playback) error { + if eff, ok := e.(effectPreStartIntf[TMemory]); ok { if err := eff.PreStart(cs, p); err != nil { return err } @@ -21,13 +21,13 @@ func EffectPreStart[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TCh return nil } -type effectStartIntf[TMemory, TChannelData any] interface { - Start(Channel[TMemory, TChannelData], Playback) error +type effectStartIntf[TMemory any] interface { + Start(Channel[TMemory], Playback) error } // 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 { +func EffectStart[TMemory any](e Effect, cs Channel[TMemory], p Playback) error { + if eff, ok := e.(effectStartIntf[TMemory]); ok { if err := eff.Start(cs, p); err != nil { return err } @@ -35,13 +35,13 @@ func EffectStart[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChann return nil } -type effectTickIntf[TMemory, TChannelData any] interface { - Tick(Channel[TMemory, TChannelData], Playback, int) error +type effectTickIntf[TMemory any] interface { + Tick(Channel[TMemory], Playback, int) error } // 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 { +func EffectTick[TMemory any](e Effect, cs Channel[TMemory], p Playback, currentTick int) error { + if eff, ok := e.(effectTickIntf[TMemory]); ok { if err := eff.Tick(cs, p, currentTick); err != nil { return err } @@ -49,13 +49,13 @@ func EffectTick[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChanne return nil } -type effectStopIntf[TMemory, TChannelData any] interface { - Stop(Channel[TMemory, TChannelData], Playback, int) error +type effectStopIntf[TMemory any] interface { + Stop(Channel[TMemory], Playback, int) error } // 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 { +func EffectStop[TMemory any](e Effect, cs Channel[TMemory], p Playback, lastTick int) error { + if eff, ok := e.(effectStopIntf[TMemory]); ok { if err := eff.Stop(cs, p, lastTick); err != nil { return err } @@ -64,12 +64,12 @@ func EffectStop[TMemory, TChannelData any](e Effect, cs Channel[TMemory, TChanne } // CombinedEffect specifies multiple simultaneous effects into one -type CombinedEffect[TMemory, TChannelData any] struct { +type CombinedEffect[TMemory 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[TMemory]) PreStart(cs Channel[TMemory], p Playback) error { for _, effect := range e.Effects { if err := EffectPreStart(effect, cs, p); err != nil { return err @@ -79,7 +79,7 @@ func (e CombinedEffect[TMemory, TChannelData]) PreStart(cs Channel[TMemory, TCha } // 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[TMemory]) Start(cs Channel[TMemory], p Playback) error { for _, effect := range e.Effects { if err := EffectStart(effect, cs, p); err != nil { return err @@ -89,7 +89,7 @@ func (e CombinedEffect[TMemory, TChannelData]) Start(cs Channel[TMemory, TChanne } // Tick is called on every tick -func (e CombinedEffect[TMemory, TChannelData]) Tick(cs Channel[TMemory, TChannelData], p Playback, currentTick int) error { +func (e CombinedEffect[TMemory]) Tick(cs Channel[TMemory], p Playback, currentTick int) error { for _, effect := range e.Effects { if err := EffectTick(effect, cs, p, currentTick); err != nil { return err @@ -99,7 +99,7 @@ func (e CombinedEffect[TMemory, TChannelData]) Tick(cs Channel[TMemory, TChannel } // 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[TMemory]) Stop(cs Channel[TMemory], p Playback, lastTick int) error { for _, effect := range e.Effects { if err := EffectStop(effect, cs, p, lastTick); err != nil { return err @@ -109,7 +109,7 @@ func (e CombinedEffect[TMemory, TChannelData]) Stop(cs Channel[TMemory, TChannel } // String returns the string for the effect list -func (e CombinedEffect[TMemory, TChannelData]) String() string { +func (e CombinedEffect[TMemory]) String() string { for _, eff := range e.Effects { s := fmt.Sprint(eff) if s != "" { @@ -120,7 +120,7 @@ func (e CombinedEffect[TMemory, TChannelData]) String() string { } // 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 { +func DoEffect[TMemory any](e Effect, cs Channel[TMemory], p Playback, currentTick int, lastTick bool) error { if e == nil { return nil } diff --git a/format/it/effect/effect_arpeggio.go b/format/it/effect/effect_arpeggio.go index b877d45..bf73de9 100644 --- a/format/it/effect/effect_arpeggio.go +++ b/format/it/effect/effect_arpeggio.go @@ -11,7 +11,7 @@ import ( 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 { +func (e Arpeggio) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -19,7 +19,7 @@ func (e Arpeggio) Start(cs playback.Channel[channel.Memory, channel.Data], p pla } // Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e Arpeggio) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Arpeggio(channel.DataEffect(e)) return doArpeggio(cs, currentTick, int8(x), int8(y)) diff --git a/format/it/effect/effect_channelvolumeslide.go b/format/it/effect/effect_channelvolumeslide.go index f3a815a..f573b64 100644 --- a/format/it/effect/effect_channelvolumeslide.go +++ b/format/it/effect/effect_channelvolumeslide.go @@ -13,7 +13,7 @@ import ( 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 { +func (e ChannelVolumeSlide) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() @@ -39,7 +39,7 @@ func (e ChannelVolumeSlide) Start(cs playback.Channel[channel.Memory, channel.Da } // Tick is called on every tick -func (e ChannelVolumeSlide) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e ChannelVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.ChannelVolumeSlide(channel.DataEffect(e)) diff --git a/format/it/effect/effect_extrafineportadown.go b/format/it/effect/effect_extrafineportadown.go index 99d6582..76ab002 100644 --- a/format/it/effect/effect_extrafineportadown.go +++ b/format/it/effect/effect_extrafineportadown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e ExtraFinePortaDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/effect/effect_extrafineportaup.go b/format/it/effect/effect_extrafineportaup.go index ff36996..357ac1b 100644 --- a/format/it/effect/effect_extrafineportaup.go +++ b/format/it/effect/effect_extrafineportaup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e ExtraFinePortaUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/effect/effect_finepatterndelay.go b/format/it/effect/effect_finepatterndelay.go index da66c76..3e23d82 100644 --- a/format/it/effect/effect_finepatterndelay.go +++ b/format/it/effect/effect_finepatterndelay.go @@ -12,7 +12,7 @@ import ( 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 { +func (e FinePatternDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/it/effect/effect_fineportadown.go b/format/it/effect/effect_fineportadown.go index 2ac702d..b5799bb 100644 --- a/format/it/effect/effect_fineportadown.go +++ b/format/it/effect/effect_fineportadown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FinePortaDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/effect/effect_fineportaup.go b/format/it/effect/effect_fineportaup.go index 73df0a2..fa2bf9b 100644 --- a/format/it/effect/effect_fineportaup.go +++ b/format/it/effect/effect_fineportaup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FinePortaUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/effect/effect_finevibrato.go b/format/it/effect/effect_finevibrato.go index 7673b4b..03e58df 100644 --- a/format/it/effect/effect_finevibrato.go +++ b/format/it/effect/effect_finevibrato.go @@ -11,14 +11,14 @@ import ( 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 { +func (e FineVibrato) Start(cs playback.Channel[channel.Memory], 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 { +func (e FineVibrato) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(channel.DataEffect(e)) if currentTick != 0 { diff --git a/format/it/effect/effect_finevolslidedown.go b/format/it/effect/effect_finevolslidedown.go index dff0e00..6f52009 100644 --- a/format/it/effect/effect_finevolslidedown.go +++ b/format/it/effect/effect_finevolslidedown.go @@ -11,13 +11,13 @@ import ( 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 { +func (e FineVolumeSlideDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e FineVolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() _, y := mem.VolumeSlide(channel.DataEffect(e)) @@ -37,7 +37,7 @@ func (e FineVolumeSlideDown) String() string { 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 { +func (e VolChanFineVolumeSlideDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { mem := cs.GetMemory() y := mem.VolChanVolumeSlide(channel.DataEffect(e)) diff --git a/format/it/effect/effect_finevolslideup.go b/format/it/effect/effect_finevolslideup.go index f18c795..b153e65 100644 --- a/format/it/effect/effect_finevolslideup.go +++ b/format/it/effect/effect_finevolslideup.go @@ -11,13 +11,13 @@ import ( 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 { +func (e FineVolumeSlideUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e FineVolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, _ := mem.VolumeSlide(channel.DataEffect(e)) @@ -37,7 +37,7 @@ func (e FineVolumeSlideUp) String() string { 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 { +func (e VolChanFineVolumeSlideUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { mem := cs.GetMemory() x := mem.VolChanVolumeSlide(channel.DataEffect(e)) diff --git a/format/it/effect/effect_globalvolumeslide.go b/format/it/effect/effect_globalvolumeslide.go index b63bdf8..32687f3 100644 --- a/format/it/effect/effect_globalvolumeslide.go +++ b/format/it/effect/effect_globalvolumeslide.go @@ -12,13 +12,13 @@ import ( 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 { +func (e GlobalVolumeSlide) Start(cs playback.Channel[channel.Memory], 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 { +func (e GlobalVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) diff --git a/format/it/effect/effect_highoffset.go b/format/it/effect/effect_highoffset.go index 912a23c..d92af4c 100644 --- a/format/it/effect/effect_highoffset.go +++ b/format/it/effect/effect_highoffset.go @@ -11,7 +11,7 @@ import ( 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 { +func (e HighOffset) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() diff --git a/format/it/effect/effect_newnoteactionnotecontinue.go b/format/it/effect/effect_newnoteactionnotecontinue.go index aa84905..9897a3d 100644 --- a/format/it/effect/effect_newnoteactionnotecontinue.go +++ b/format/it/effect/effect_newnoteactionnotecontinue.go @@ -12,7 +12,7 @@ import ( 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 { +func (e NewNoteActionNoteContinue) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionContinue) return nil } diff --git a/format/it/effect/effect_newnoteactionnotecut.go b/format/it/effect/effect_newnoteactionnotecut.go index 03e39f7..8001490 100644 --- a/format/it/effect/effect_newnoteactionnotecut.go +++ b/format/it/effect/effect_newnoteactionnotecut.go @@ -12,7 +12,7 @@ import ( 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 { +func (e NewNoteActionNoteCut) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionCut) return nil } diff --git a/format/it/effect/effect_newnoteactionnotefade.go b/format/it/effect/effect_newnoteactionnotefade.go index d6cff6a..e9054e1 100644 --- a/format/it/effect/effect_newnoteactionnotefade.go +++ b/format/it/effect/effect_newnoteactionnotefade.go @@ -12,7 +12,7 @@ import ( 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 { +func (e NewNoteActionNoteFade) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionFadeout) return nil } diff --git a/format/it/effect/effect_newnoteactionnoteoff.go b/format/it/effect/effect_newnoteactionnoteoff.go index 9899615..e49e5d1 100644 --- a/format/it/effect/effect_newnoteactionnoteoff.go +++ b/format/it/effect/effect_newnoteactionnoteoff.go @@ -12,7 +12,7 @@ import ( 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 { +func (e NewNoteActionNoteOff) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionRelease) return nil } diff --git a/format/it/effect/effect_notecut.go b/format/it/effect/effect_notecut.go index 5e02c82..3fac390 100644 --- a/format/it/effect/effect_notecut.go +++ b/format/it/effect/effect_notecut.go @@ -11,13 +11,13 @@ import ( 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 { +func (e NoteCut) Start(cs playback.Channel[channel.Memory], 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 { +func (e NoteCut) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { diff --git a/format/it/effect/effect_notedelay.go b/format/it/effect/effect_notedelay.go index 34aae9e..b8b97ee 100644 --- a/format/it/effect/effect_notedelay.go +++ b/format/it/effect/effect_notedelay.go @@ -12,13 +12,13 @@ import ( 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 { +func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e NoteDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/effect/effect_orderjump.go b/format/it/effect/effect_orderjump.go index 6188862..292b373 100644 --- a/format/it/effect/effect_orderjump.go +++ b/format/it/effect/effect_orderjump.go @@ -12,13 +12,13 @@ import ( 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 { +func (e OrderJump) Start(cs playback.Channel[channel.Memory], 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 { +func (e OrderJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } diff --git a/format/it/effect/effect_panningenvelopeoff.go b/format/it/effect/effect_panningenvelopeoff.go index f43b14b..1b54720 100644 --- a/format/it/effect/effect_panningenvelopeoff.go +++ b/format/it/effect/effect_panningenvelopeoff.go @@ -11,7 +11,7 @@ import ( 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 { +func (e PanningEnvelopeOff) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(false) diff --git a/format/it/effect/effect_panningenvelopeon.go b/format/it/effect/effect_panningenvelopeon.go index 99dc7a6..da0fc09 100644 --- a/format/it/effect/effect_panningenvelopeon.go +++ b/format/it/effect/effect_panningenvelopeon.go @@ -11,7 +11,7 @@ import ( 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 { +func (e PanningEnvelopeOn) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(true) diff --git a/format/it/effect/effect_pastnotecut.go b/format/it/effect/effect_pastnotecut.go index 7f1202f..9e71b1e 100644 --- a/format/it/effect/effect_pastnotecut.go +++ b/format/it/effect/effect_pastnotecut.go @@ -12,7 +12,7 @@ import ( 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 { +func (e PastNoteCut) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionCut) return nil } diff --git a/format/it/effect/effect_pastnotefadeout.go b/format/it/effect/effect_pastnotefadeout.go index 4272248..794bce2 100644 --- a/format/it/effect/effect_pastnotefadeout.go +++ b/format/it/effect/effect_pastnotefadeout.go @@ -12,7 +12,7 @@ import ( 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 { +func (e PastNoteFade) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionFadeout) return nil } diff --git a/format/it/effect/effect_pastnoteoff.go b/format/it/effect/effect_pastnoteoff.go index da0bdb1..545af35 100644 --- a/format/it/effect/effect_pastnoteoff.go +++ b/format/it/effect/effect_pastnoteoff.go @@ -12,7 +12,7 @@ import ( 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 { +func (e PastNoteOff) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionRelease) return nil } diff --git a/format/it/effect/effect_patterndelay.go b/format/it/effect/effect_patterndelay.go index 491cb47..dd861b6 100644 --- a/format/it/effect/effect_patterndelay.go +++ b/format/it/effect/effect_patterndelay.go @@ -12,13 +12,13 @@ import ( 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 { +func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e PatternDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/effect/effect_patternloop.go b/format/it/effect/effect_patternloop.go index dd5d1a4..46923aa 100644 --- a/format/it/effect/effect_patternloop.go +++ b/format/it/effect/effect_patternloop.go @@ -11,13 +11,13 @@ import ( 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 { +func (e PatternLoop) Start(cs playback.Channel[channel.Memory], 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 { +func (e PatternLoop) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { x := uint8(e) & 0xF mem := cs.GetMemory() diff --git a/format/it/effect/effect_pitchenvelopeoff.go b/format/it/effect/effect_pitchenvelopeoff.go index 0a269a7..17427c9 100644 --- a/format/it/effect/effect_pitchenvelopeoff.go +++ b/format/it/effect/effect_pitchenvelopeoff.go @@ -11,7 +11,7 @@ import ( 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 { +func (e PitchEnvelopeOff) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(false) diff --git a/format/it/effect/effect_pitchenvelopeon.go b/format/it/effect/effect_pitchenvelopeon.go index 0131a82..79befb1 100644 --- a/format/it/effect/effect_pitchenvelopeon.go +++ b/format/it/effect/effect_pitchenvelopeon.go @@ -11,7 +11,7 @@ import ( 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 { +func (e PitchEnvelopeOn) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(true) diff --git a/format/it/effect/effect_portadown.go b/format/it/effect/effect_portadown.go index 10bdf2b..df55836 100644 --- a/format/it/effect/effect_portadown.go +++ b/format/it/effect/effect_portadown.go @@ -11,14 +11,14 @@ import ( 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 { +func (e PortaDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e PortaDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaDown(channel.DataEffect(e)) diff --git a/format/it/effect/effect_portatonote.go b/format/it/effect/effect_portatonote.go index 9d990fd..e490ea1 100644 --- a/format/it/effect/effect_portatonote.go +++ b/format/it/effect/effect_portatonote.go @@ -14,7 +14,7 @@ import ( 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 { +func (e PortaToNote) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,7 +25,7 @@ func (e PortaToNote) Start(cs playback.Channel[channel.Memory, channel.Data], p } // Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaToNote(channel.DataEffect(e)) diff --git a/format/it/effect/effect_portaup.go b/format/it/effect/effect_portaup.go index 7a1fbdf..d3491c7 100644 --- a/format/it/effect/effect_portaup.go +++ b/format/it/effect/effect_portaup.go @@ -11,14 +11,14 @@ import ( 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 { +func (e PortaUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e PortaUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaUp(channel.DataEffect(e)) diff --git a/format/it/effect/effect_portavolslide.go b/format/it/effect/effect_portavolslide.go index e9d0cda..30f6da3 100644 --- a/format/it/effect/effect_portavolslide.go +++ b/format/it/effect/effect_portavolslide.go @@ -9,7 +9,7 @@ import ( // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/it/effect/effect_retrigvolslide.go b/format/it/effect/effect_retrigvolslide.go index ac82f73..495edb9 100644 --- a/format/it/effect/effect_retrigvolslide.go +++ b/format/it/effect/effect_retrigvolslide.go @@ -13,13 +13,13 @@ import ( 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 { +func (e RetrigVolumeSlide) Start(cs playback.Channel[channel.Memory], 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 { +func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.RetrigVolumeSlide(channel.DataEffect(e)) if y == 0 { diff --git a/format/it/effect/effect_rowjump.go b/format/it/effect/effect_rowjump.go index eb0cfc6..eb02a07 100644 --- a/format/it/effect/effect_rowjump.go +++ b/format/it/effect/effect_rowjump.go @@ -12,13 +12,13 @@ import ( 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 { +func (e RowJump) Start(cs playback.Channel[channel.Memory], 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 { +func (e RowJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { r := channel.DataEffect(e) rowIdx := index.Row(r) return p.SetNextRow(rowIdx) diff --git a/format/it/effect/effect_sampleoffset.go b/format/it/effect/effect_sampleoffset.go index ae5733f..895f61c 100644 --- a/format/it/effect/effect_sampleoffset.go +++ b/format/it/effect/effect_sampleoffset.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SampleOffset) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(channel.DataEffect(e)) diff --git a/format/it/effect/effect_setchannelvolume.go b/format/it/effect/effect_setchannelvolume.go index 1427c23..0f12150 100644 --- a/format/it/effect/effect_setchannelvolume.go +++ b/format/it/effect/effect_setchannelvolume.go @@ -14,7 +14,7 @@ import ( 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 { +func (e SetChannelVolume) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := channel.DataEffect(e) diff --git a/format/it/effect/effect_setcoarsepanposition.go b/format/it/effect/effect_setcoarsepanposition.go index f283ff2..972131f 100644 --- a/format/it/effect/effect_setcoarsepanposition.go +++ b/format/it/effect/effect_setcoarsepanposition.go @@ -14,7 +14,7 @@ import ( 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 { +func (e SetCoarsePanPosition) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/it/effect/effect_setfinetune.go b/format/it/effect/effect_setfinetune.go index 3ce3790..df3c827 100644 --- a/format/it/effect/effect_setfinetune.go +++ b/format/it/effect/effect_setfinetune.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { x := channel.DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,7 +24,7 @@ func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory, channel.Data], } // 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 { +func (e SetFinetune) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/effect/effect_setglobalvolume.go b/format/it/effect/effect_setglobalvolume.go index 81f413d..f5a5cfb 100644 --- a/format/it/effect/effect_setglobalvolume.go +++ b/format/it/effect/effect_setglobalvolume.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { v := volume.Volume(channel.DataEffect(e)) / 0x80 if v > 1 { v = 1 @@ -23,7 +23,7 @@ func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory, channel.Da } // 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 { +func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/effect/effect_setpanbrellowaveform.go b/format/it/effect/effect_setpanbrellowaveform.go index 77bed8b..20ae48c 100644 --- a/format/it/effect/effect_setpanbrellowaveform.go +++ b/format/it/effect/effect_setpanbrellowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetPanbrelloWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/it/effect/effect_setpanposition.go b/format/it/effect/effect_setpanposition.go index db60a4d..dc8b0fc 100644 --- a/format/it/effect/effect_setpanposition.go +++ b/format/it/effect/effect_setpanposition.go @@ -14,7 +14,7 @@ import ( 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 { +func (e SetPanPosition) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) diff --git a/format/it/effect/effect_setspeed.go b/format/it/effect/effect_setspeed.go index 7c8258c..387ba87 100644 --- a/format/it/effect/effect_setspeed.go +++ b/format/it/effect/effect_setspeed.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { if e != 0 { m := p.(effectIntf.IT) if err := m.SetTicks(int(e)); err != nil { @@ -23,7 +23,7 @@ func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory, channel.Data], p } // 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 { +func (e SetSpeed) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/effect/effect_settempo.go b/format/it/effect/effect_settempo.go index ff82142..a1cd884 100644 --- a/format/it/effect/effect_settempo.go +++ b/format/it/effect/effect_settempo.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { if e > 0x20 { m := p.(effectIntf.IT) if err := m.SetTempo(int(e)); err != nil { @@ -23,13 +23,13 @@ func (e SetTempo) PreStart(cs playback.Channel[channel.Memory, channel.Data], p } // 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 { +func (e SetTempo) Start(cs playback.Channel[channel.Memory], 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 { +func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { m := p.(effectIntf.IT) switch channel.DataEffect(e >> 4) { case 0: // decrease tempo diff --git a/format/it/effect/effect_settremolowaveform.go b/format/it/effect/effect_settremolowaveform.go index 7449d70..1fefedc 100644 --- a/format/it/effect/effect_settremolowaveform.go +++ b/format/it/effect/effect_settremolowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/it/effect/effect_setvibratowaveform.go b/format/it/effect/effect_setvibratowaveform.go index 718c799..6fdb06e 100644 --- a/format/it/effect/effect_setvibratowaveform.go +++ b/format/it/effect/effect_setvibratowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/it/effect/effect_tremolo.go b/format/it/effect/effect_tremolo.go index 56abe48..5bf3173 100644 --- a/format/it/effect/effect_tremolo.go +++ b/format/it/effect/effect_tremolo.go @@ -11,13 +11,13 @@ import ( 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 { +func (e Tremolo) Start(cs playback.Channel[channel.Memory], 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 { +func (e Tremolo) Tick(cs playback.Channel[channel.Memory], 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. diff --git a/format/it/effect/effect_tremor.go b/format/it/effect/effect_tremor.go index 3972b2a..30ea4d8 100644 --- a/format/it/effect/effect_tremor.go +++ b/format/it/effect/effect_tremor.go @@ -11,13 +11,13 @@ import ( 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 { +func (e Tremor) Start(cs playback.Channel[channel.Memory], 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 { +func (e Tremor) Tick(cs playback.Channel[channel.Memory], 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) diff --git a/format/it/effect/effect_vibrato.go b/format/it/effect/effect_vibrato.go index ef775c1..1d0a0e4 100644 --- a/format/it/effect/effect_vibrato.go +++ b/format/it/effect/effect_vibrato.go @@ -11,14 +11,14 @@ import ( 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 { +func (e Vibrato) Start(cs playback.Channel[channel.Memory], 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 { +func (e Vibrato) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(channel.DataEffect(e)) if mem.Shared.OldEffectMode { diff --git a/format/it/effect/effect_vibratovolslide.go b/format/it/effect/effect_vibratovolslide.go index 20ac2d9..3fc3e34 100644 --- a/format/it/effect/effect_vibratovolslide.go +++ b/format/it/effect/effect_vibratovolslide.go @@ -9,7 +9,7 @@ import ( // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/it/effect/effect_volslidedown.go b/format/it/effect/effect_volslidedown.go index c461fb5..90bd080 100644 --- a/format/it/effect/effect_volslidedown.go +++ b/format/it/effect/effect_volslidedown.go @@ -11,13 +11,13 @@ import ( 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 { +func (e VolumeSlideDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e VolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() _, y := mem.VolumeSlide(channel.DataEffect(e)) @@ -34,7 +34,7 @@ func (e VolumeSlideDown) String() string { 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 { +func (e VolChanVolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() y := mem.VolChanVolumeSlide(channel.DataEffect(e)) diff --git a/format/it/effect/effect_volslideup.go b/format/it/effect/effect_volslideup.go index 343ce4e..42c972d 100644 --- a/format/it/effect/effect_volslideup.go +++ b/format/it/effect/effect_volslideup.go @@ -11,13 +11,13 @@ import ( 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 { +func (e VolumeSlideUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e VolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, _ := mem.VolumeSlide(channel.DataEffect(e)) @@ -34,7 +34,7 @@ func (e VolumeSlideUp) String() string { 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 { +func (e VolChanVolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x := mem.VolChanVolumeSlide(channel.DataEffect(e)) diff --git a/format/it/effect/effect_volumeenvelopeoff.go b/format/it/effect/effect_volumeenvelopeoff.go index aab597b..e398cbd 100644 --- a/format/it/effect/effect_volumeenvelopeoff.go +++ b/format/it/effect/effect_volumeenvelopeoff.go @@ -11,7 +11,7 @@ import ( 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 { +func (e VolumeEnvelopeOff) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(false) diff --git a/format/it/effect/effect_volumeenvelopeon.go b/format/it/effect/effect_volumeenvelopeon.go index 4262709..53b4c67 100644 --- a/format/it/effect/effect_volumeenvelopeon.go +++ b/format/it/effect/effect_volumeenvelopeon.go @@ -11,7 +11,7 @@ import ( 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 { +func (e VolumeEnvelopeOn) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(true) diff --git a/format/it/effect/effectfactory.go b/format/it/effect/effectfactory.go index 39a64e0..20eb706 100644 --- a/format/it/effect/effectfactory.go +++ b/format/it/effect/effectfactory.go @@ -5,6 +5,7 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/song" ) type EffectIT interface { @@ -13,7 +14,7 @@ type EffectIT interface { // VolEff is a combined effect that includes a volume effect and a standard effect type VolEff struct { - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] eff EffectIT } @@ -25,24 +26,25 @@ func (e VolEff) String() string { } // Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data *channel.Data) EffectIT { - if data == nil { +func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { + d, _ := data.(*channel.Data) + if d == nil { return nil } - if !data.What.HasCommand() && !data.What.HasVolPan() { + if !d.What.HasCommand() && !d.What.HasVolPan() { return nil } eff := VolEff{} - if data.What.HasVolPan() { - ve := volPanEffectFactory(mem, data.VolPan) + if d.What.HasVolPan() { + ve := volPanEffectFactory(mem, d.VolPan) if ve != nil { eff.Effects = append(eff.Effects, ve) } } - if e := standardEffectFactory(mem, data); e != nil { + if e := standardEffectFactory(mem, d); e != nil { eff.Effects = append(eff.Effects, e) eff.eff = e } diff --git a/format/it/effect/unhandled.go b/format/it/effect/unhandled.go index 493af35..40cdc40 100644 --- a/format/it/effect/unhandled.go +++ b/format/it/effect/unhandled.go @@ -15,7 +15,7 @@ type UnhandledCommand struct { } // 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 { +func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: ce:%0.2X cp:%0.2X", e.Command, e.Info)) } @@ -32,7 +32,7 @@ type UnhandledVolCommand struct { } // 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 { +func (e UnhandledVolCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: volCmd:%0.2X", e.Vol)) } diff --git a/format/it/effect/util.go b/format/it/effect/util.go index e542375..f4d04ba 100644 --- a/format/it/effect/util.go +++ b/format/it/effect/util.go @@ -13,7 +13,7 @@ import ( "github.com/heucuva/comparison" ) -func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32, multiplier float32) error { +func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := itVolume.ToItVolume(av) vol := int16((float32(v) + delta) * multiplier) @@ -45,7 +45,7 @@ func doGlobalVolSlide(m effectIntf.IT, delta float32, multiplier float32) error return nil } -func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory, channel.Data], delta int) error { +func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -57,7 +57,7 @@ func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory, channel.Data], delt return nil } -func doPortaByDeltaLinear(cs playback.Channel[channel.Memory, channel.Data], delta int) error { +func doPortaByDeltaLinear(cs playback.Channel[channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -69,7 +69,7 @@ func doPortaByDeltaLinear(cs playback.Channel[channel.Memory, channel.Data], del return nil } -func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { delta := int(amount * multiplier) if linearFreqSlides { return doPortaByDeltaLinear(cs, delta) @@ -77,7 +77,7 @@ func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32 return doPortaByDeltaAmiga(cs, -delta) } -func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { +func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { if err := doPortaUp(cs, amount, multiplier, linearFreqSlides); err != nil { return err } @@ -87,7 +87,7 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount f return nil } -func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { delta := int(amount * multiplier) if linearFreqSlides { return doPortaByDeltaLinear(cs, -delta) @@ -95,7 +95,7 @@ func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float return doPortaByDeltaAmiga(cs, delta) } -func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { +func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { if err := doPortaDown(cs, amount, multiplier, linearFreqSlides); err != nil { return err } @@ -105,7 +105,7 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount return nil } -func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato(cs playback.Channel[channel.Memory], 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) @@ -113,7 +113,7 @@ func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick in return nil } -func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int, onTicks int, offTicks int) error { +func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -129,7 +129,7 @@ func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int return nil } -func doArpeggio(cs playback.Channel[channel.Memory, channel.Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio(cs playback.Channel[channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -154,7 +154,7 @@ var ( } ) -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) error { +func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { vol := itVolume.ToItVolume(cs.GetActiveVolume()) if vol >= 0x10 && vol <= 0x50 { vol -= 0x10 @@ -173,13 +173,13 @@ func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) erro return nil } -func doTremolo(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo(cs playback.Channel[channel.Memory], 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 { +func calculateWaveTable(cs playback.Channel[channel.Memory], 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/layout/song.go b/format/it/layout/song.go index 5574863..8d43228 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -3,10 +3,10 @@ package layout import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/format/it/pattern" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" "github.com/gotracker/playback/song" ) @@ -15,7 +15,7 @@ type Song struct { Head Header Instruments map[uint8]*instrument.Instrument InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument - Patterns []pattern.Pattern[channel.Data] + Patterns []pattern.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern FilterPlugins map[int]filter.Factory @@ -27,11 +27,11 @@ func (s Song) GetOrderList() []index.Pattern { } // GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { +func (s Song) GetPattern(patNum index.Pattern) song.Pattern { if int(patNum) >= len(s.Patterns) { return nil } - return &s.Patterns[patNum] + return s.Patterns[patNum] } // IsChannelEnabled returns true if the channel at index `channelNum` is enabled diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index 8f2f995..ed5143c 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -16,10 +16,10 @@ import ( "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/pattern" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" "github.com/gotracker/playback/player/feature" ) @@ -42,18 +42,15 @@ func moduleHeaderToHeader(fh *itfile.ModuleHeader) (*layout.Header, error) { 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(pkt itfile.PackedPattern, channels int) (pattern.Pattern, int, error) { + pat := make(pattern.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([]channel.Data, channels) + pat[rowNum] = row channelLoop: for { sz, chn, err := pkt.ReadChannelData(pos, channelMem) @@ -76,7 +73,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) } @@ -100,7 +97,7 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So 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)), + Patterns: make([]pattern.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.OrderCount)), FilterPlugins: make(map[int]filter.Factory), } @@ -152,7 +149,7 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern[channel.Data], len(f.Patterns)) + song.Patterns = make([]pattern.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { pattern, maxCh, err := convertItPattern(pkt, len(f.Head.ChannelVol)) if err != nil { @@ -164,7 +161,7 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - song.Patterns[patNum] = *pattern + song.Patterns[patNum] = pattern } sharedMem := channel.SharedMemory{ diff --git a/format/it/pattern/pattern.go b/format/it/pattern/pattern.go index 2269251..92a7b4f 100644 --- a/format/it/pattern/pattern.go +++ b/format/it/pattern/pattern.go @@ -1,339 +1,20 @@ 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 - } +type Pattern []Row - 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 +func (p Pattern) GetRow(row index.Row) song.Row { + return p[row] } -// 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 +func (p Pattern) NumRows() int { + return len(p) } -// StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn +func (p Pattern) GetRows() song.Rows { + return p } diff --git a/format/it/pattern/row.go b/format/it/pattern/row.go new file mode 100644 index 0000000..d376e3a --- /dev/null +++ b/format/it/pattern/row.go @@ -0,0 +1,16 @@ +package pattern + +import ( + "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/song" +) + +type Row []channel.Data + +func (r Row) GetChannels() []song.ChannelData { + c := make([]song.ChannelData, len(r)) + for i := range r { + c[i] = &r[i] + } + return c +} diff --git a/format/it/pattern/state.go b/format/it/pattern/state.go new file mode 100644 index 0000000..75eaa01 --- /dev/null +++ b/format/it/pattern/state.go @@ -0,0 +1,338 @@ +package pattern + +import ( + "errors" + + "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 + 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, 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/playback/channeldata_transaction.go b/format/it/playback/channeldata_transaction.go index d1e44a1..781e65f 100644 --- a/format/it/playback/channeldata_transaction.go +++ b/format/it/playback/channeldata_transaction.go @@ -13,7 +13,7 @@ import ( type channelDataConverter struct{} -func (c channelDataConverter) Process(out *state.ChannelDataActions, data *channel.Data, s song.Data, cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (c channelDataConverter) Process(out *state.ChannelDataActions, data song.ChannelData, s song.Data, cs *state.ChannelState[channel.Memory]) error { if data == nil { return nil } @@ -89,17 +89,17 @@ func (c channelDataConverter) Process(out *state.ChannelDataActions, data *chann } type channelDataTransaction struct { - state.ChannelDataTxnHelper[channel.Memory, channel.Data, channelDataConverter] + state.ChannelDataTxnHelper[channel.Memory, channelDataConverter] } -func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory, channel.Data]) error { +func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { e := effect.Factory(cs.GetMemory(), d.Data) cs.SetActiveEffect(e) if e != nil { if onEff := p.GetOnEffect(); onEff != nil { onEff(e) } - if err := playback.EffectPreStart[channel.Memory, channel.Data](e, cs, p); err != nil { + if err := playback.EffectPreStart[channel.Memory](e, cs, p); err != nil { return err } } @@ -107,7 +107,7 @@ func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.Cha return nil } -func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory, channel.Data]) error { +func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { if pos, ok := d.TargetPos.Get(); ok { cs.SetTargetPos(pos) } diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index 3265f2f..da31d5f 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -26,7 +26,7 @@ type Manager struct { song *layout.Song - channels []state.ChannelState[channel.Memory, channel.Data] + channels []state.ChannelState[channel.Memory] PastNotes state.PastNotesProcessor pattern pattern.State @@ -100,7 +100,7 @@ func (m *Manager) GetNumChannels() int { return len(m.channels) } -func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc) state.NoteOp[channel.Memory, channel.Data] { +func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc) state.NoteOp[channel.Memory] { return doNoteCalc{ Semitone: st, UpdateFunc: fn, @@ -109,7 +109,7 @@ func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateF // SetNumChannels updates the song to have the specified number of channels and resets their states func (m *Manager) SetNumChannels(num int) { - m.channels = make([]state.ChannelState[channel.Memory, channel.Data], num) + m.channels = make([]state.ChannelState[channel.Memory], num) m.PastNotes.SetMax(channel.MaxTotalChannels - num) for ch := range m.channels { @@ -311,7 +311,7 @@ func (m *Manager) GetSongData() song.Data { } // GetChannel returns the channel interface for the specified channel number -func (m *Manager) GetChannel(ch int) *state.ChannelState[channel.Memory, channel.Data] { +func (m *Manager) GetChannel(ch int) *state.ChannelState[channel.Memory] { return &m.channels[ch] } diff --git a/format/it/playback/playback_command.go b/format/it/playback/playback_command.go index 311ef82..8af157e 100644 --- a/format/it/playback/playback_command.go +++ b/format/it/playback/playback_command.go @@ -15,7 +15,7 @@ type doNoteCalc struct { UpdateFunc state.PeriodUpdateFunc } -func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory]) error { if o.UpdateFunc == nil { return nil } @@ -29,7 +29,7 @@ func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel. return nil } -func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory], 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 @@ -54,7 +54,7 @@ func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, c return nil } -func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory], currentTick int, lastTick bool) error { targetTick, noteAction := cs.WillTriggerOn(currentTick) if !targetTick { return nil @@ -106,7 +106,7 @@ func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, return nil } -func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory], currentTick int, lastTick bool) error { if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index 0257f26..690710a 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -102,7 +102,7 @@ func (m *Manager) processPatternRow() error { continue } - cdata := &channels[channelNum] + cdata := channels[channelNum] cs := &m.channels[channelNum] if err := cs.SetData(cdata); err != nil { @@ -147,7 +147,7 @@ func (m *Manager) processPatternRow() error { return nil } -func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory]) error { mem := cs.GetMemory() mem.TremorMem().Reset() diff --git a/format/it/playback/playback_textoutput.go b/format/it/playback/playback_textoutput.go index 5a22f57..84ee63f 100644 --- a/format/it/playback/playback_textoutput.go +++ b/format/it/playback/playback_textoutput.go @@ -19,7 +19,7 @@ func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if cd := cs.GetData(); cd != nil { + if cd := cs.GetData().(*channel.Data); cd != nil { rowText.Channels[ch] = *cd } } diff --git a/format/s3m/effect/effect_arpeggio.go b/format/s3m/effect/effect_arpeggio.go index f70136c..45d5e80 100644 --- a/format/s3m/effect/effect_arpeggio.go +++ b/format/s3m/effect/effect_arpeggio.go @@ -11,7 +11,7 @@ import ( 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 { +func (e Arpeggio) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -19,7 +19,7 @@ func (e Arpeggio) Start(cs playback.Channel[channel.Memory, channel.Data], p pla } // Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e Arpeggio) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.LastNonZeroXY(channel.DataEffect(e)) return doArpeggio(cs, currentTick, int8(x), int8(y)) diff --git a/format/s3m/effect/effect_enablefilter.go b/format/s3m/effect/effect_enablefilter.go index 5933722..d059acf 100644 --- a/format/s3m/effect/effect_enablefilter.go +++ b/format/s3m/effect/effect_enablefilter.go @@ -12,7 +12,7 @@ import ( 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 { +func (e EnableFilter) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/s3m/effect/effect_extrafineportadown.go b/format/s3m/effect/effect_extrafineportadown.go index f83af89..b2ee951 100644 --- a/format/s3m/effect/effect_extrafineportadown.go +++ b/format/s3m/effect/effect_extrafineportadown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e ExtraFinePortaDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/s3m/effect/effect_extrafineportaup.go b/format/s3m/effect/effect_extrafineportaup.go index d42a3b7..7819cd9 100644 --- a/format/s3m/effect/effect_extrafineportaup.go +++ b/format/s3m/effect/effect_extrafineportaup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e ExtraFinePortaUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/s3m/effect/effect_finepatterndelay.go b/format/s3m/effect/effect_finepatterndelay.go index ad80702..584da41 100644 --- a/format/s3m/effect/effect_finepatterndelay.go +++ b/format/s3m/effect/effect_finepatterndelay.go @@ -12,7 +12,7 @@ import ( 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 { +func (e FinePatternDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/s3m/effect/effect_fineportadown.go b/format/s3m/effect/effect_fineportadown.go index 00e965f..0cbaadf 100644 --- a/format/s3m/effect/effect_fineportadown.go +++ b/format/s3m/effect/effect_fineportadown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FinePortaDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/s3m/effect/effect_fineportaup.go b/format/s3m/effect/effect_fineportaup.go index 1735c4b..1eb7469 100644 --- a/format/s3m/effect/effect_fineportaup.go +++ b/format/s3m/effect/effect_fineportaup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FinePortaUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/s3m/effect/effect_finevibrato.go b/format/s3m/effect/effect_finevibrato.go index 2f0223d..50b5970 100644 --- a/format/s3m/effect/effect_finevibrato.go +++ b/format/s3m/effect/effect_finevibrato.go @@ -11,14 +11,14 @@ import ( 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 { +func (e FineVibrato) Start(cs playback.Channel[channel.Memory], 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 { +func (e FineVibrato) Tick(cs playback.Channel[channel.Memory], 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. diff --git a/format/s3m/effect/effect_finevolslidedown.go b/format/s3m/effect/effect_finevolslidedown.go index e72e582..f30bdae 100644 --- a/format/s3m/effect/effect_finevolslidedown.go +++ b/format/s3m/effect/effect_finevolslidedown.go @@ -11,13 +11,13 @@ import ( 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 { +func (e FineVolumeSlideDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e FineVolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { y := channel.DataEffect(e) & 0x0F if y != 0x0F && currentTick == 0 { diff --git a/format/s3m/effect/effect_finevolslideup.go b/format/s3m/effect/effect_finevolslideup.go index db37718..7b6dc99 100644 --- a/format/s3m/effect/effect_finevolslideup.go +++ b/format/s3m/effect/effect_finevolslideup.go @@ -11,13 +11,13 @@ import ( 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 { +func (e FineVolumeSlideUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e FineVolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) >> 4 if x != 0x0F && currentTick == 0 { diff --git a/format/s3m/effect/effect_notecut.go b/format/s3m/effect/effect_notecut.go index 0ab9c05..b44fc5c 100644 --- a/format/s3m/effect/effect_notecut.go +++ b/format/s3m/effect/effect_notecut.go @@ -11,13 +11,13 @@ import ( 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 { +func (e NoteCut) Start(cs playback.Channel[channel.Memory], 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 { +func (e NoteCut) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { diff --git a/format/s3m/effect/effect_notedelay.go b/format/s3m/effect/effect_notedelay.go index 4b595a7..9a5ec27 100644 --- a/format/s3m/effect/effect_notedelay.go +++ b/format/s3m/effect/effect_notedelay.go @@ -12,13 +12,13 @@ import ( 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 { +func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e NoteDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_orderjump.go b/format/s3m/effect/effect_orderjump.go index 58168f2..01de809 100644 --- a/format/s3m/effect/effect_orderjump.go +++ b/format/s3m/effect/effect_orderjump.go @@ -12,13 +12,13 @@ import ( 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 { +func (e OrderJump) Start(cs playback.Channel[channel.Memory], 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 { +func (e OrderJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } diff --git a/format/s3m/effect/effect_patterndelay.go b/format/s3m/effect/effect_patterndelay.go index 3ebf8da..801bfc9 100644 --- a/format/s3m/effect/effect_patterndelay.go +++ b/format/s3m/effect/effect_patterndelay.go @@ -12,13 +12,13 @@ import ( 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 { +func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e PatternDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_patternloop.go b/format/s3m/effect/effect_patternloop.go index e0c5ade..f105195 100644 --- a/format/s3m/effect/effect_patternloop.go +++ b/format/s3m/effect/effect_patternloop.go @@ -11,13 +11,13 @@ import ( 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 { +func (e PatternLoop) Start(cs playback.Channel[channel.Memory], 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 { +func (e PatternLoop) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { x := channel.DataEffect(e) & 0xF mem := cs.GetMemory() diff --git a/format/s3m/effect/effect_portadown.go b/format/s3m/effect/effect_portadown.go index da0b32a..ecd7328 100644 --- a/format/s3m/effect/effect_portadown.go +++ b/format/s3m/effect/effect_portadown.go @@ -11,14 +11,14 @@ import ( 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 { +func (e PortaDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e PortaDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.LastNonZero(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_portatonote.go b/format/s3m/effect/effect_portatonote.go index 37061b5..4cade4c 100644 --- a/format/s3m/effect/effect_portatonote.go +++ b/format/s3m/effect/effect_portatonote.go @@ -14,7 +14,7 @@ import ( 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 { +func (e PortaToNote) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,7 +25,7 @@ func (e PortaToNote) Start(cs playback.Channel[channel.Memory, channel.Data], p } // Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaToNote(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_portaup.go b/format/s3m/effect/effect_portaup.go index b214bd4..b3fe2dc 100644 --- a/format/s3m/effect/effect_portaup.go +++ b/format/s3m/effect/effect_portaup.go @@ -11,14 +11,14 @@ import ( 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 { +func (e PortaUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e PortaUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.LastNonZero(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_portavolslide.go b/format/s3m/effect/effect_portavolslide.go index f7dff38..1c1d1cb 100644 --- a/format/s3m/effect/effect_portavolslide.go +++ b/format/s3m/effect/effect_portavolslide.go @@ -9,7 +9,7 @@ import ( // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/s3m/effect/effect_retrigvolslide.go b/format/s3m/effect/effect_retrigvolslide.go index dea8389..fc7c4d9 100644 --- a/format/s3m/effect/effect_retrigvolslide.go +++ b/format/s3m/effect/effect_retrigvolslide.go @@ -13,13 +13,13 @@ import ( 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 { +func (e RetrigVolumeSlide) Start(cs playback.Channel[channel.Memory], 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 { +func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) >> 4 y := channel.DataEffect(e) & 0x0F if y == 0 { diff --git a/format/s3m/effect/effect_rowjump.go b/format/s3m/effect/effect_rowjump.go index 5e5669d..cc3a86f 100644 --- a/format/s3m/effect/effect_rowjump.go +++ b/format/s3m/effect/effect_rowjump.go @@ -12,13 +12,13 @@ import ( 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 { +func (e RowJump) Start(cs playback.Channel[channel.Memory], 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 { +func (e RowJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { r := channel.DataEffect(e) rowIdx := index.Row((r >> 4) * 10) rowIdx += index.Row(r & 0xf) diff --git a/format/s3m/effect/effect_sampleoffset.go b/format/s3m/effect/effect_sampleoffset.go index 66e745a..0b629d6 100644 --- a/format/s3m/effect/effect_sampleoffset.go +++ b/format/s3m/effect/effect_sampleoffset.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SampleOffset) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_setfinetune.go b/format/s3m/effect/effect_setfinetune.go index 01c85ce..7f7ecb5 100644 --- a/format/s3m/effect/effect_setfinetune.go +++ b/format/s3m/effect/effect_setfinetune.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { x := channel.DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,7 +24,7 @@ func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory, channel.Data], } // 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 { +func (e SetFinetune) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_setglobalvolume.go b/format/s3m/effect/effect_setglobalvolume.go index ec7f4d4..28d978d 100644 --- a/format/s3m/effect/effect_setglobalvolume.go +++ b/format/s3m/effect/effect_setglobalvolume.go @@ -14,13 +14,13 @@ import ( 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 { +func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_setpanposition.go b/format/s3m/effect/effect_setpanposition.go index e276d9d..8fcc366 100644 --- a/format/s3m/effect/effect_setpanposition.go +++ b/format/s3m/effect/effect_setpanposition.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetPanPosition) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := uint8(e) & 0xf diff --git a/format/s3m/effect/effect_setspeed.go b/format/s3m/effect/effect_setspeed.go index cc43bf5..ea942df 100644 --- a/format/s3m/effect/effect_setspeed.go +++ b/format/s3m/effect/effect_setspeed.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { if e != 0 { m := p.(effectIntf.S3M) if err := m.SetTicks(int(e)); err != nil { @@ -23,7 +23,7 @@ func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory, channel.Data], p } // 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 { +func (e SetSpeed) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_settempo.go b/format/s3m/effect/effect_settempo.go index 9567759..a343cf5 100644 --- a/format/s3m/effect/effect_settempo.go +++ b/format/s3m/effect/effect_settempo.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { if e > 0x20 { m := p.(effectIntf.S3M) if err := m.SetTempo(int(e)); err != nil { @@ -23,13 +23,13 @@ func (e SetTempo) PreStart(cs playback.Channel[channel.Memory, channel.Data], p } // 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 { +func (e SetTempo) Start(cs playback.Channel[channel.Memory], 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 { +func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { m := p.(effectIntf.S3M) switch channel.DataEffect(e >> 4) { case 0: // decrease tempo diff --git a/format/s3m/effect/effect_settremolowaveform.go b/format/s3m/effect/effect_settremolowaveform.go index 383ec80..f6997bb 100644 --- a/format/s3m/effect/effect_settremolowaveform.go +++ b/format/s3m/effect/effect_settremolowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/s3m/effect/effect_setvibratowaveform.go b/format/s3m/effect/effect_setvibratowaveform.go index 113f523..5b53d6c 100644 --- a/format/s3m/effect/effect_setvibratowaveform.go +++ b/format/s3m/effect/effect_setvibratowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/s3m/effect/effect_stereocontrol.go b/format/s3m/effect/effect_stereocontrol.go index e59f5fa..186ccd5 100644 --- a/format/s3m/effect/effect_stereocontrol.go +++ b/format/s3m/effect/effect_stereocontrol.go @@ -12,7 +12,7 @@ import ( 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 { +func (e StereoControl) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := uint8(e) & 0xf diff --git a/format/s3m/effect/effect_tremolo.go b/format/s3m/effect/effect_tremolo.go index 866e4d9..38bfea2 100644 --- a/format/s3m/effect/effect_tremolo.go +++ b/format/s3m/effect/effect_tremolo.go @@ -11,13 +11,13 @@ import ( 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 { +func (e Tremolo) Start(cs playback.Channel[channel.Memory], 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 { +func (e Tremolo) Tick(cs playback.Channel[channel.Memory], 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. diff --git a/format/s3m/effect/effect_tremor.go b/format/s3m/effect/effect_tremor.go index 51f0bc8..8a1b703 100644 --- a/format/s3m/effect/effect_tremor.go +++ b/format/s3m/effect/effect_tremor.go @@ -11,13 +11,13 @@ import ( 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 { +func (e Tremor) Start(cs playback.Channel[channel.Memory], 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 { +func (e Tremor) Tick(cs playback.Channel[channel.Memory], 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) diff --git a/format/s3m/effect/effect_vibrato.go b/format/s3m/effect/effect_vibrato.go index ace0713..cf75611 100644 --- a/format/s3m/effect/effect_vibrato.go +++ b/format/s3m/effect/effect_vibrato.go @@ -11,14 +11,14 @@ import ( 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 { +func (e Vibrato) Start(cs playback.Channel[channel.Memory], 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 { +func (e Vibrato) Tick(cs playback.Channel[channel.Memory], 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. diff --git a/format/s3m/effect/effect_vibratovolslide.go b/format/s3m/effect/effect_vibratovolslide.go index 6a40643..6b2bffb 100644 --- a/format/s3m/effect/effect_vibratovolslide.go +++ b/format/s3m/effect/effect_vibratovolslide.go @@ -9,7 +9,7 @@ import ( // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/s3m/effect/effect_volslidedown.go b/format/s3m/effect/effect_volslidedown.go index 453cf28..7007c29 100644 --- a/format/s3m/effect/effect_volslidedown.go +++ b/format/s3m/effect/effect_volslidedown.go @@ -11,13 +11,13 @@ import ( 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 { +func (e VolumeSlideDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e VolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() y := channel.DataEffect(e) & 0x0F diff --git a/format/s3m/effect/effect_volslideup.go b/format/s3m/effect/effect_volslideup.go index 7cdcef6..930ba7c 100644 --- a/format/s3m/effect/effect_volslideup.go +++ b/format/s3m/effect/effect_volslideup.go @@ -11,13 +11,13 @@ import ( 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 { +func (e VolumeSlideUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e VolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x := channel.DataEffect(e) >> 4 diff --git a/format/s3m/effect/effectfactory.go b/format/s3m/effect/effectfactory.go index fb93093..e64d248 100644 --- a/format/s3m/effect/effectfactory.go +++ b/format/s3m/effect/effectfactory.go @@ -3,6 +3,7 @@ package effect import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/song" ) type EffectS3M interface { @@ -12,80 +13,81 @@ type EffectS3M interface { type ChannelCommand channel.DataEffect // Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data *channel.Data) EffectS3M { - if data == nil { +func Factory(mem *channel.Memory, data song.ChannelData) EffectS3M { + d, _ := data.(*channel.Data) + if d == nil { return nil } - if !data.What.HasCommand() { + if !d.What.HasCommand() { return nil } - mem.LastNonZero(data.Info) - switch data.Command + '@' { + 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.LastNonZero(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.LastNonZero(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 { diff --git a/format/s3m/effect/unhandled.go b/format/s3m/effect/unhandled.go index 2909bdd..2157eaa 100644 --- a/format/s3m/effect/unhandled.go +++ b/format/s3m/effect/unhandled.go @@ -15,7 +15,7 @@ type UnhandledCommand struct { } // 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 { +func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.S3M) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } diff --git a/format/s3m/effect/util.go b/format/s3m/effect/util.go index 6a7becc..c0e0ea0 100644 --- a/format/s3m/effect/util.go +++ b/format/s3m/effect/util.go @@ -12,7 +12,7 @@ import ( "github.com/heucuva/comparison" ) -func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32, multiplier float32) error { +func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := s3mVolume.VolumeToS3M(av) vol := int16((float32(v) + delta) * multiplier) @@ -28,7 +28,7 @@ func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32 return nil } -func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32) error { +func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -41,7 +41,7 @@ func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32 return nil } -func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period) error { +func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -57,7 +57,7 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount f return nil } -func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32) error { +func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -70,7 +70,7 @@ func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float return nil } -func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period) error { +func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -86,14 +86,14 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount return nil } -func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato(cs playback.Channel[channel.Memory], 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 { +func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -109,7 +109,7 @@ func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int return nil } -func doArpeggio(cs playback.Channel[channel.Memory, channel.Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio(cs playback.Channel[channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -134,7 +134,7 @@ var ( } ) -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) error { +func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { vol := s3mVolume.VolumeToS3M(cs.GetActiveVolume()) if vol >= 64 { vol = 63 @@ -143,13 +143,13 @@ func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) erro return nil } -func doTremolo(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo(cs playback.Channel[channel.Memory], 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 { +func calculateWaveTable(cs playback.Channel[channel.Memory], 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/layout/song.go b/format/s3m/layout/song.go index 11b60e3..fa3afc6 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -2,10 +2,10 @@ package layout import ( "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/format/s3m/pattern" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" "github.com/gotracker/playback/song" ) @@ -13,7 +13,7 @@ import ( type Song struct { Head Header Instruments []*instrument.Instrument - Patterns []pattern.Pattern[channel.Data] + Patterns []pattern.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern } @@ -24,7 +24,7 @@ func (s Song) GetOrderList() []index.Pattern { } // GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { +func (s Song) GetPattern(patNum index.Pattern) song.Pattern { if int(patNum) >= len(s.Patterns) { return nil } diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index d45faf8..98305b5 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -9,19 +9,19 @@ import ( 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/s3m/channel" "github.com/gotracker/playback/format/s3m/layout" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + "github.com/gotracker/playback/format/s3m/pattern" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "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/voice/fadeout" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" ) func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { @@ -185,18 +185,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) (pattern.Pattern, int) { + pat := make(pattern.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(pattern.Row, 0) + channelLoop: for { var what s3mfile.PatternFlags if err := binary.Read(buffer, binary.LittleEndian, &what); err != nil { @@ -204,15 +201,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 } @@ -248,6 +244,7 @@ func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (*pattern } } } + pat[rowNum] = row } return pat, int(maxCh) @@ -300,7 +297,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern[channel.Data], len(f.Patterns)) + song.Patterns = make([]pattern.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { pattern, maxCh := convertS3MPackedPattern(pkt, getPatternLen(patNum)) if pattern == nil { @@ -309,7 +306,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - song.Patterns[patNum] = *pattern + song.Patterns[patNum] = pattern } sharedMem := channel.SharedMemory{ diff --git a/format/s3m/pattern/pattern.go b/format/s3m/pattern/pattern.go index cb99b75..92a7b4f 100644 --- a/format/s3m/pattern/pattern.go +++ b/format/s3m/pattern/pattern.go @@ -1,338 +1,20 @@ 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) -} +type Pattern []Row -// GetCurrentOrder returns the current order -func (state *State) GetCurrentOrder() index.Order { - return state.currentOrder +func (p Pattern) GetRow(row index.Row) song.Row { + return p[row] } -// 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") +func (p Pattern) NumRows() int { + return len(p) } -// 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 +func (p Pattern) GetRows() song.Rows { + return p } diff --git a/format/s3m/pattern/row.go b/format/s3m/pattern/row.go new file mode 100644 index 0000000..7f7215f --- /dev/null +++ b/format/s3m/pattern/row.go @@ -0,0 +1,16 @@ +package pattern + +import ( + "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/song" +) + +type Row []channel.Data + +func (r Row) GetChannels() []song.ChannelData { + c := make([]song.ChannelData, len(r)) + for i := range r { + c[i] = &r[i] + } + return c +} diff --git a/format/s3m/pattern/state.go b/format/s3m/pattern/state.go new file mode 100644 index 0000000..ff7de04 --- /dev/null +++ b/format/s3m/pattern/state.go @@ -0,0 +1,337 @@ +package pattern + +import ( + "errors" + + "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 + 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, 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/playback/channeldata_transaction.go b/format/s3m/playback/channeldata_transaction.go index 283bb23..2121799 100644 --- a/format/s3m/playback/channeldata_transaction.go +++ b/format/s3m/playback/channeldata_transaction.go @@ -14,7 +14,7 @@ import ( type channelDataConverter struct{} -func (c channelDataConverter) Process(out *state.ChannelDataActions, data *channel.Data, s song.Data, cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (c channelDataConverter) Process(out *state.ChannelDataActions, data song.ChannelData, s song.Data, cs *state.ChannelState[channel.Memory]) error { if data == nil { return nil } @@ -82,17 +82,17 @@ func (c channelDataConverter) Process(out *state.ChannelDataActions, data *chann } type channelDataTransaction struct { - state.ChannelDataTxnHelper[channel.Memory, channel.Data, channelDataConverter] + state.ChannelDataTxnHelper[channel.Memory, channelDataConverter] } -func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory, channel.Data]) error { +func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { e := effect.Factory(cs.GetMemory(), d.Data) cs.SetActiveEffect(e) if e != nil { if onEff := p.GetOnEffect(); onEff != nil { onEff(e) } - if err := playback.EffectPreStart[channel.Memory, channel.Data](e, cs, p); err != nil { + if err := playback.EffectPreStart[channel.Memory](e, cs, p); err != nil { return err } } @@ -100,7 +100,7 @@ func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.Cha return nil } -func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory, channel.Data]) error { +func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { if pos, ok := d.TargetPos.Get(); ok { cs.SetTargetPos(pos) } diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index 019ef24..0d33dd3 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -27,7 +27,7 @@ type Manager struct { song *layout.Song - channels []state.ChannelState[channel.Memory, channel.Data] + channels []state.ChannelState[channel.Memory] pattern pattern.State preMixRowTxn *playpattern.RowUpdateTransaction @@ -37,7 +37,7 @@ type Manager struct { rowRenderState *rowRenderState OnEffect func(playback.Effect) - chOrder [4][]*state.ChannelState[channel.Memory, channel.Data] + chOrder [4][]*state.ChannelState[channel.Memory] } // NewManager creates a new manager for an S3M song @@ -132,7 +132,7 @@ func (m *Manager) GetNumChannels() int { return len(m.channels) } -func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc) state.NoteOp[channel.Memory, channel.Data] { +func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc) state.NoteOp[channel.Memory] { return doNoteCalc{ Semitone: st, UpdateFunc: fn, @@ -141,7 +141,7 @@ func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateF // SetNumChannels updates the song to have the specified number of channels and resets their states func (m *Manager) SetNumChannels(num int) { - m.channels = make([]state.ChannelState[channel.Memory, channel.Data], num) + m.channels = make([]state.ChannelState[channel.Memory], num) for ch := range m.channels { cs := &m.channels[ch] @@ -315,7 +315,7 @@ func (m *Manager) GetSongData() song.Data { } // GetChannel returns the channel interface for the specified channel number -func (m *Manager) GetChannel(ch int) *state.ChannelState[channel.Memory, channel.Data] { +func (m *Manager) GetChannel(ch int) *state.ChannelState[channel.Memory] { return &m.channels[ch] } diff --git a/format/s3m/playback/playback_command.go b/format/s3m/playback/playback_command.go index 62abe93..ae269ef 100644 --- a/format/s3m/playback/playback_command.go +++ b/format/s3m/playback/playback_command.go @@ -14,7 +14,7 @@ type doNoteCalc struct { UpdateFunc state.PeriodUpdateFunc } -func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory]) error { if o.UpdateFunc == nil { return nil } @@ -27,7 +27,7 @@ func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel. return nil } -func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory], 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 @@ -51,7 +51,7 @@ func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, c return nil } -func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory], currentTick int, lastTick bool) error { triggerTick, noteAction := cs.WillTriggerOn(currentTick) if !triggerTick { return nil @@ -106,7 +106,7 @@ func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, return nil } -func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory], currentTick int, lastTick bool) error { if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index 2f5f09e..aeca8c0 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -102,7 +102,7 @@ func (m *Manager) processPatternRow() error { continue } - cdata := &channels[channelNum] + cdata := channels[channelNum] cs := &m.channels[channelNum] if err := cs.SetData(cdata); err != nil { @@ -146,7 +146,7 @@ func (m *Manager) processPatternRow() error { return nil } -func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory]) error { mem := cs.GetMemory() mem.TremorMem().Reset() diff --git a/format/s3m/playback/playback_textoutput.go b/format/s3m/playback/playback_textoutput.go index 086b0ff..ba38f64 100644 --- a/format/s3m/playback/playback_textoutput.go +++ b/format/s3m/playback/playback_textoutput.go @@ -19,7 +19,7 @@ func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if cd := cs.GetData(); cd != nil { + if cd, _ := cs.GetData().(*channel.Data); cd != nil { rowText.Channels[ch] = *cd } } diff --git a/format/xm/effect/effect_arpeggio.go b/format/xm/effect/effect_arpeggio.go index dbc4503..817dbd4 100644 --- a/format/xm/effect/effect_arpeggio.go +++ b/format/xm/effect/effect_arpeggio.go @@ -11,7 +11,7 @@ import ( 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 { +func (e Arpeggio) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -19,7 +19,7 @@ func (e Arpeggio) Start(cs playback.Channel[channel.Memory, channel.Data], p pla } // Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e Arpeggio) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { xy := channel.DataEffect(e) if xy == 0 { return nil diff --git a/format/xm/effect/effect_extrafineportadown.go b/format/xm/effect/effect_extrafineportadown.go index 4f1ced8..aeb2e28 100644 --- a/format/xm/effect/effect_extrafineportadown.go +++ b/format/xm/effect/effect_extrafineportadown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e ExtraFinePortaDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/effect/effect_extrafineportaup.go b/format/xm/effect/effect_extrafineportaup.go index 9281ad5..fbaf09d 100644 --- a/format/xm/effect/effect_extrafineportaup.go +++ b/format/xm/effect/effect_extrafineportaup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e ExtraFinePortaUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/effect/effect_fineportadown.go b/format/xm/effect/effect_fineportadown.go index 551da99..25fe828 100644 --- a/format/xm/effect/effect_fineportadown.go +++ b/format/xm/effect/effect_fineportadown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FinePortaDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/effect/effect_fineportaup.go b/format/xm/effect/effect_fineportaup.go index 657a8ca..d6fe939 100644 --- a/format/xm/effect/effect_fineportaup.go +++ b/format/xm/effect/effect_fineportaup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FinePortaUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/effect/effect_finevolslidedown.go b/format/xm/effect/effect_finevolslidedown.go index 27b843c..56eaf60 100644 --- a/format/xm/effect/effect_finevolslidedown.go +++ b/format/xm/effect/effect_finevolslidedown.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FineVolumeSlideDown) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() diff --git a/format/xm/effect/effect_finevolslideup.go b/format/xm/effect/effect_finevolslideup.go index 0c10ed6..3b4f07a 100644 --- a/format/xm/effect/effect_finevolslideup.go +++ b/format/xm/effect/effect_finevolslideup.go @@ -11,7 +11,7 @@ import ( 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 { +func (e FineVolumeSlideUp) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() diff --git a/format/xm/effect/effect_globalvolumeslide.go b/format/xm/effect/effect_globalvolumeslide.go index 23c10ac..35d2532 100644 --- a/format/xm/effect/effect_globalvolumeslide.go +++ b/format/xm/effect/effect_globalvolumeslide.go @@ -12,13 +12,13 @@ import ( 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 { +func (e GlobalVolumeSlide) Start(cs playback.Channel[channel.Memory], 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 { +func (e GlobalVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) diff --git a/format/xm/effect/effect_notecut.go b/format/xm/effect/effect_notecut.go index a343c3e..ff53efc 100644 --- a/format/xm/effect/effect_notecut.go +++ b/format/xm/effect/effect_notecut.go @@ -11,13 +11,13 @@ import ( 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 { +func (e NoteCut) Start(cs playback.Channel[channel.Memory], 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 { +func (e NoteCut) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { diff --git a/format/xm/effect/effect_notedelay.go b/format/xm/effect/effect_notedelay.go index 559e884..77e0766 100644 --- a/format/xm/effect/effect_notedelay.go +++ b/format/xm/effect/effect_notedelay.go @@ -12,13 +12,13 @@ import ( 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 { +func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e NoteDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/effect/effect_orderjump.go b/format/xm/effect/effect_orderjump.go index fa62293..1663765 100644 --- a/format/xm/effect/effect_orderjump.go +++ b/format/xm/effect/effect_orderjump.go @@ -12,13 +12,13 @@ import ( 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 { +func (e OrderJump) Start(cs playback.Channel[channel.Memory], 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 { +func (e OrderJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } diff --git a/format/xm/effect/effect_panslide.go b/format/xm/effect/effect_panslide.go index fc10d2b..424be9c 100644 --- a/format/xm/effect/effect_panslide.go +++ b/format/xm/effect/effect_panslide.go @@ -12,7 +12,7 @@ import ( 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 { +func (e PanSlide) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { xx := channel.DataEffect(e) x := xx >> 4 y := xx & 0x0F diff --git a/format/xm/effect/effect_patterndelay.go b/format/xm/effect/effect_patterndelay.go index f98c46c..ad3561e 100644 --- a/format/xm/effect/effect_patterndelay.go +++ b/format/xm/effect/effect_patterndelay.go @@ -12,13 +12,13 @@ import ( 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 { +func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e PatternDelay) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/effect/effect_patternloop.go b/format/xm/effect/effect_patternloop.go index 1be3284..97c6456 100644 --- a/format/xm/effect/effect_patternloop.go +++ b/format/xm/effect/effect_patternloop.go @@ -11,7 +11,7 @@ import ( 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 { +func (e PatternLoop) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xF diff --git a/format/xm/effect/effect_portadown.go b/format/xm/effect/effect_portadown.go index b379bb4..223a560 100644 --- a/format/xm/effect/effect_portadown.go +++ b/format/xm/effect/effect_portadown.go @@ -11,14 +11,14 @@ import ( 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 { +func (e PortaDown) Start(cs playback.Channel[channel.Memory], 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 { +func (e PortaDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaDown(channel.DataEffect(e)) diff --git a/format/xm/effect/effect_portatonote.go b/format/xm/effect/effect_portatonote.go index 143419e..6a19524 100644 --- a/format/xm/effect/effect_portatonote.go +++ b/format/xm/effect/effect_portatonote.go @@ -14,7 +14,7 @@ import ( 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 { +func (e PortaToNote) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,7 +25,7 @@ func (e PortaToNote) Start(cs playback.Channel[channel.Memory, channel.Data], p } // Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory, channel.Data], p playback.Playback, currentTick int) error { +func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { if currentTick == 0 { return nil } diff --git a/format/xm/effect/effect_portaup.go b/format/xm/effect/effect_portaup.go index 3f65ea3..be9231e 100644 --- a/format/xm/effect/effect_portaup.go +++ b/format/xm/effect/effect_portaup.go @@ -11,14 +11,14 @@ import ( 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 { +func (e PortaUp) Start(cs playback.Channel[channel.Memory], 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 { +func (e PortaUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaUp(channel.DataEffect(e)) diff --git a/format/xm/effect/effect_portavolslide.go b/format/xm/effect/effect_portavolslide.go index 9e62e9c..953daf9 100644 --- a/format/xm/effect/effect_portavolslide.go +++ b/format/xm/effect/effect_portavolslide.go @@ -9,7 +9,7 @@ import ( // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide struct { // '5' - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/xm/effect/effect_retriggernote.go b/format/xm/effect/effect_retriggernote.go index 127d1f0..6708e46 100644 --- a/format/xm/effect/effect_retriggernote.go +++ b/format/xm/effect/effect_retriggernote.go @@ -13,13 +13,13 @@ import ( 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 { +func (e RetriggerNote) Start(cs playback.Channel[channel.Memory], 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 { +func (e RetriggerNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { y := channel.DataEffect(e) & 0x0F if y == 0 { return nil diff --git a/format/xm/effect/effect_retrigvolslide.go b/format/xm/effect/effect_retrigvolslide.go index 04ef253..dcdfdf1 100644 --- a/format/xm/effect/effect_retrigvolslide.go +++ b/format/xm/effect/effect_retrigvolslide.go @@ -13,13 +13,13 @@ import ( 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 { +func (e RetrigVolumeSlide) Start(cs playback.Channel[channel.Memory], 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 { +func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) >> 4 y := channel.DataEffect(e) & 0x0F if y == 0 { diff --git a/format/xm/effect/effect_rowjump.go b/format/xm/effect/effect_rowjump.go index f9b37ee..0570903 100644 --- a/format/xm/effect/effect_rowjump.go +++ b/format/xm/effect/effect_rowjump.go @@ -12,13 +12,13 @@ import ( 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 { +func (e RowJump) Start(cs playback.Channel[channel.Memory], 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 { +func (e RowJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, lastTick int) error { xy := channel.DataEffect(e) x := xy >> 4 y := xy & 0x0f diff --git a/format/xm/effect/effect_sampleoffset.go b/format/xm/effect/effect_sampleoffset.go index b8b1256..f9e1aad 100644 --- a/format/xm/effect/effect_sampleoffset.go +++ b/format/xm/effect/effect_sampleoffset.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SampleOffset) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(channel.DataEffect(e)) diff --git a/format/xm/effect/effect_setcoarsepanposition.go b/format/xm/effect/effect_setcoarsepanposition.go index 925a283..a4c38b8 100644 --- a/format/xm/effect/effect_setcoarsepanposition.go +++ b/format/xm/effect/effect_setcoarsepanposition.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetCoarsePanPosition) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xy := channel.DataEffect(e) diff --git a/format/xm/effect/effect_setenvelopeposition.go b/format/xm/effect/effect_setenvelopeposition.go index 7bd2a17..87b6b25 100644 --- a/format/xm/effect/effect_setenvelopeposition.go +++ b/format/xm/effect/effect_setenvelopeposition.go @@ -11,7 +11,7 @@ import ( 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 { +func (e SetEnvelopePosition) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := channel.DataEffect(e) diff --git a/format/xm/effect/effect_setfinetune.go b/format/xm/effect/effect_setfinetune.go index a0f1fbc..290f918 100644 --- a/format/xm/effect/effect_setfinetune.go +++ b/format/xm/effect/effect_setfinetune.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { x := channel.DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,7 +24,7 @@ func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory, channel.Data], } // 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 { +func (e SetFinetune) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/effect/effect_setglobalvolume.go b/format/xm/effect/effect_setglobalvolume.go index f024f3c..458a36b 100644 --- a/format/xm/effect/effect_setglobalvolume.go +++ b/format/xm/effect/effect_setglobalvolume.go @@ -12,14 +12,14 @@ import ( 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 { +func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], 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 { +func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/effect/effect_setpanposition.go b/format/xm/effect/effect_setpanposition.go index 8c36a20..f10f62b 100644 --- a/format/xm/effect/effect_setpanposition.go +++ b/format/xm/effect/effect_setpanposition.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetPanPosition) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := uint8(e) diff --git a/format/xm/effect/effect_setspeed.go b/format/xm/effect/effect_setspeed.go index 652b394..75fef6b 100644 --- a/format/xm/effect/effect_setspeed.go +++ b/format/xm/effect/effect_setspeed.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { if e != 0 { m := p.(effectIntf.XM) if err := m.SetTicks(int(e)); err != nil { @@ -23,7 +23,7 @@ func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory, channel.Data], p } // 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 { +func (e SetSpeed) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/effect/effect_settempo.go b/format/xm/effect/effect_settempo.go index d84c144..4c83cb2 100644 --- a/format/xm/effect/effect_settempo.go +++ b/format/xm/effect/effect_settempo.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { if e > 0x20 { m := p.(effectIntf.XM) if err := m.SetTempo(int(e)); err != nil { @@ -23,13 +23,13 @@ func (e SetTempo) PreStart(cs playback.Channel[channel.Memory, channel.Data], p } // 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 { +func (e SetTempo) Start(cs playback.Channel[channel.Memory], 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 { +func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { m := p.(effectIntf.XM) return m.SetTempo(int(e)) } diff --git a/format/xm/effect/effect_settremolowaveform.go b/format/xm/effect/effect_settremolowaveform.go index fd29336..63e4cd6 100644 --- a/format/xm/effect/effect_settremolowaveform.go +++ b/format/xm/effect/effect_settremolowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/xm/effect/effect_setvibratowaveform.go b/format/xm/effect/effect_setvibratowaveform.go index 474085d..3729af9 100644 --- a/format/xm/effect/effect_setvibratowaveform.go +++ b/format/xm/effect/effect_setvibratowaveform.go @@ -13,7 +13,7 @@ import ( 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 { +func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/xm/effect/effect_setvolume.go b/format/xm/effect/effect_setvolume.go index d98f85f..40ee961 100644 --- a/format/xm/effect/effect_setvolume.go +++ b/format/xm/effect/effect_setvolume.go @@ -12,7 +12,7 @@ import ( 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 { +func (e SetVolume) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := xmVolume.XmVolume(e) diff --git a/format/xm/effect/effect_tremolo.go b/format/xm/effect/effect_tremolo.go index 13bd2fa..21b8c22 100644 --- a/format/xm/effect/effect_tremolo.go +++ b/format/xm/effect/effect_tremolo.go @@ -11,13 +11,13 @@ import ( 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 { +func (e Tremolo) Start(cs playback.Channel[channel.Memory], 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 { +func (e Tremolo) Tick(cs playback.Channel[channel.Memory], 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. diff --git a/format/xm/effect/effect_tremor.go b/format/xm/effect/effect_tremor.go index dd6ffc9..06adc03 100644 --- a/format/xm/effect/effect_tremor.go +++ b/format/xm/effect/effect_tremor.go @@ -11,13 +11,13 @@ import ( 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 { +func (e Tremor) Start(cs playback.Channel[channel.Memory], 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 { +func (e Tremor) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { if currentTick != 0 { mem := cs.GetMemory() x, y := mem.Tremor(channel.DataEffect(e)) diff --git a/format/xm/effect/effect_vibrato.go b/format/xm/effect/effect_vibrato.go index 583caaf..bdad63c 100644 --- a/format/xm/effect/effect_vibrato.go +++ b/format/xm/effect/effect_vibrato.go @@ -11,14 +11,14 @@ import ( 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 { +func (e Vibrato) Start(cs playback.Channel[channel.Memory], 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 { +func (e Vibrato) Tick(cs playback.Channel[channel.Memory], 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. diff --git a/format/xm/effect/effect_vibratovolslide.go b/format/xm/effect/effect_vibratovolslide.go index caf9f06..6dcbfda 100644 --- a/format/xm/effect/effect_vibratovolslide.go +++ b/format/xm/effect/effect_vibratovolslide.go @@ -9,7 +9,7 @@ import ( // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide struct { // '6' - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/xm/effect/effect_volslide.go b/format/xm/effect/effect_volslide.go index 54cd068..38b4ef4 100644 --- a/format/xm/effect/effect_volslide.go +++ b/format/xm/effect/effect_volslide.go @@ -11,13 +11,13 @@ import ( 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 { +func (e VolumeSlide) Start(cs playback.Channel[channel.Memory], 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 { +func (e VolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.VolumeSlide(channel.DataEffect(e)) diff --git a/format/xm/effect/effectfactory.go b/format/xm/effect/effectfactory.go index 9fdddf8..7a3ccb4 100644 --- a/format/xm/effect/effectfactory.go +++ b/format/xm/effect/effectfactory.go @@ -5,6 +5,7 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/song" ) type EffectXM interface { @@ -13,7 +14,7 @@ type EffectXM interface { // VolEff is a combined effect that includes a volume effect and a standard effect type VolEff struct { - playback.CombinedEffect[channel.Memory, channel.Data] + playback.CombinedEffect[channel.Memory] eff EffectXM } @@ -25,24 +26,25 @@ func (e VolEff) String() string { } // Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data *channel.Data) EffectXM { - if data == nil { +func Factory(mem *channel.Memory, data song.ChannelData) EffectXM { + d, _ := data.(*channel.Data) + if d == nil { return nil } - if !data.HasCommand() { + if !d.HasCommand() { return nil } eff := VolEff{} - if data.What.HasVolume() { - ve := volumeEffectFactory(mem, data.Volume) + if d.What.HasVolume() { + ve := volumeEffectFactory(mem, d.Volume) if ve != nil { eff.Effects = append(eff.Effects, ve) } } - if e := standardEffectFactory(mem, data); e != nil { + if e := standardEffectFactory(mem, d); e != nil { eff.Effects = append(eff.Effects, e) eff.eff = e } diff --git a/format/xm/effect/unhandled.go b/format/xm/effect/unhandled.go index 14ca039..6edbef5 100644 --- a/format/xm/effect/unhandled.go +++ b/format/xm/effect/unhandled.go @@ -16,7 +16,7 @@ type UnhandledCommand struct { } // 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 { +func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } @@ -33,7 +33,7 @@ type UnhandledVolCommand struct { } // 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 { +func (e UnhandledVolCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } diff --git a/format/xm/effect/util.go b/format/xm/effect/util.go index 98a7a64..a97844f 100644 --- a/format/xm/effect/util.go +++ b/format/xm/effect/util.go @@ -12,7 +12,7 @@ import ( "github.com/heucuva/comparison" ) -func doVolSlide(cs playback.Channel[channel.Memory, channel.Data], delta float32, multiplier float32) error { +func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := xmVolume.ToVolumeXM(av) vol := int16((float32(v) + delta) * multiplier) @@ -42,7 +42,7 @@ func doGlobalVolSlide(m effectIntf.XM, delta float32, multiplier float32) error return nil } -func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory, channel.Data], delta int) error { +func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -54,7 +54,7 @@ func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory, channel.Data], delt return nil } -func doPortaByDeltaLinear(cs playback.Channel[channel.Memory, channel.Data], delta int) error { +func doPortaByDeltaLinear(cs playback.Channel[channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -66,7 +66,7 @@ func doPortaByDeltaLinear(cs playback.Channel[channel.Memory, channel.Data], del return nil } -func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { delta := int(amount * multiplier) if linearFreqSlides { return doPortaByDeltaLinear(cs, delta) @@ -74,7 +74,7 @@ func doPortaUp(cs playback.Channel[channel.Memory, channel.Data], amount float32 return doPortaByDeltaAmiga(cs, -delta) } -func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { +func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { if err := doPortaUp(cs, amount, multiplier, linearFreqSlides); err != nil { return err } @@ -84,7 +84,7 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory, channel.Data], amount f return nil } -func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { delta := int(amount * multiplier) if linearFreqSlides { return doPortaByDeltaLinear(cs, -delta) @@ -92,7 +92,7 @@ func doPortaDown(cs playback.Channel[channel.Memory, channel.Data], amount float return doPortaByDeltaAmiga(cs, delta) } -func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { +func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { if err := doPortaDown(cs, amount, multiplier, linearFreqSlides); err != nil { return err } @@ -102,7 +102,7 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory, channel.Data], amount return nil } -func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato(cs playback.Channel[channel.Memory], 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) @@ -110,7 +110,7 @@ func doVibrato(cs playback.Channel[channel.Memory, channel.Data], currentTick in return nil } -func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int, onTicks int, offTicks int) error { +func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -126,7 +126,7 @@ func doTremor(cs playback.Channel[channel.Memory, channel.Data], currentTick int return nil } -func doArpeggio(cs playback.Channel[channel.Memory, channel.Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio(cs playback.Channel[channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -151,7 +151,7 @@ var ( } ) -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) error { +func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { vol := xmVolume.ToVolumeXM(cs.GetActiveVolume()) if vol >= 64 { vol = 63 @@ -166,13 +166,13 @@ func doVolSlideTwoThirds(cs playback.Channel[channel.Memory, channel.Data]) erro return nil } -func doTremolo(cs playback.Channel[channel.Memory, channel.Data], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo(cs playback.Channel[channel.Memory], 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 { +func calculateWaveTable(cs playback.Channel[channel.Memory], 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/layout/song.go b/format/xm/layout/song.go index 5b13e8e..020c240 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -2,10 +2,10 @@ package layout import ( "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/format/xm/pattern" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/pattern" "github.com/gotracker/playback/song" ) @@ -14,7 +14,7 @@ type Song struct { Head Header Instruments map[uint8]*instrument.Instrument InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument - Patterns []pattern.Pattern[channel.Data] + Patterns []pattern.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern } @@ -25,7 +25,7 @@ func (s Song) GetOrderList() []index.Pattern { } // GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { +func (s Song) GetPattern(patNum index.Pattern) song.Pattern { if int(patNum) >= len(s.Patterns) { return nil } diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index bfe318d..fb466b3 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -8,24 +8,24 @@ import ( 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/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/format/xm/layout" xmPanning "github.com/gotracker/playback/format/xm/panning" + "github.com/gotracker/playback/format/xm/pattern" xmPeriod "github.com/gotracker/playback/format/xm/period" 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/oscillator" - "github.com/gotracker/playback/pattern" + "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" ) func moduleHeaderToHeader(fh *xmfile.ModuleHeader) (*layout.Header, error) { @@ -251,16 +251,14 @@ func convertXMInstrumentToInstrument(ih *xmfile.InstrumentHeader, linearFrequenc return xmInstrumentToInstrument(ih, linearFrequencySlides, features) } -func convertXmPattern(pkt xmfile.Pattern) (*pattern.Pattern[channel.Data], int) { - pat := &pattern.Pattern[channel.Data]{ - Orig: pkt, - } +func convertXmPattern(pkt xmfile.Pattern) (pattern.Pattern, int) { + pat := make(pattern.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(pattern.Row, len(drow)) + pat[rowNum] = row + for channelNum, chn := range drow { cd := channel.Data{ What: chn.Flags, @@ -270,7 +268,7 @@ func convertXmPattern(pkt xmfile.Pattern) (*pattern.Pattern[channel.Data], int) Effect: channel.Command(chn.Effect), EffectParameter: channel.DataEffect(chn.EffectParameter), } - row.Channels[channelNum] = cd + row[channelNum] = cd if maxCh < uint8(channelNum) { maxCh = uint8(channelNum) } @@ -292,7 +290,7 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So 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)), + Patterns: make([]pattern.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.SongLength)), } @@ -333,7 +331,7 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern[channel.Data], len(f.Patterns)) + song.Patterns = make([]pattern.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { pattern, maxCh := convertXmPattern(pkt) if pattern == nil { @@ -342,7 +340,7 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - song.Patterns[patNum] = *pattern + song.Patterns[patNum] = pattern } sharedMem := channel.SharedMemory{ diff --git a/format/xm/pattern/pattern.go b/format/xm/pattern/pattern.go index 5c19be6..92a7b4f 100644 --- a/format/xm/pattern/pattern.go +++ b/format/xm/pattern/pattern.go @@ -1,339 +1,20 @@ 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 - } +type Pattern []Row - 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 +func (p Pattern) GetRow(row index.Row) song.Row { + return p[row] } -// 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 +func (p Pattern) NumRows() int { + return len(p) } -// StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn +func (p Pattern) GetRows() song.Rows { + return p } diff --git a/format/xm/pattern/row.go b/format/xm/pattern/row.go new file mode 100644 index 0000000..8a810fe --- /dev/null +++ b/format/xm/pattern/row.go @@ -0,0 +1,16 @@ +package pattern + +import ( + "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/song" +) + +type Row []channel.Data + +func (r Row) GetChannels() []song.ChannelData { + c := make([]song.ChannelData, len(r)) + for i := range r { + c[i] = &r[i] + } + return c +} diff --git a/format/xm/pattern/state.go b/format/xm/pattern/state.go new file mode 100644 index 0000000..75eaa01 --- /dev/null +++ b/format/xm/pattern/state.go @@ -0,0 +1,338 @@ +package pattern + +import ( + "errors" + + "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 + 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, 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/playback/channeldata_transaction.go b/format/xm/playback/channeldata_transaction.go index fe7467d..499e50d 100755 --- a/format/xm/playback/channeldata_transaction.go +++ b/format/xm/playback/channeldata_transaction.go @@ -14,7 +14,7 @@ import ( type channelDataConverter struct{} -func (c channelDataConverter) Process(out *state.ChannelDataActions, data *channel.Data, s song.Data, cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (c channelDataConverter) Process(out *state.ChannelDataActions, data song.ChannelData, s song.Data, cs *state.ChannelState[channel.Memory]) error { if data == nil { return nil } @@ -82,17 +82,17 @@ func (c channelDataConverter) Process(out *state.ChannelDataActions, data *chann } type channelDataTransaction struct { - state.ChannelDataTxnHelper[channel.Memory, channel.Data, channelDataConverter] + state.ChannelDataTxnHelper[channel.Memory, channelDataConverter] } -func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory, channel.Data]) error { +func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { e := effect.Factory(cs.GetMemory(), d.Data) cs.SetActiveEffect(e) if e != nil { if onEff := p.GetOnEffect(); onEff != nil { onEff(e) } - if err := playback.EffectPreStart[channel.Memory, channel.Data](e, cs, p); err != nil { + if err := playback.EffectPreStart[channel.Memory](e, cs, p); err != nil { return err } } @@ -100,7 +100,7 @@ func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.Cha return nil } -func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory, channel.Data]) error { +func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { if pos, ok := d.TargetPos.Get(); ok { cs.SetTargetPos(pos) } diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index c9b608c..59aa4d0 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -25,7 +25,7 @@ type Manager struct { song *layout.Song - channels []state.ChannelState[channel.Memory, channel.Data] + channels []state.ChannelState[channel.Memory] pattern pattern.State preMixRowTxn *playpattern.RowUpdateTransaction @@ -103,7 +103,7 @@ func (m *Manager) GetNumChannels() int { return len(m.channels) } -func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc) state.NoteOp[channel.Memory, channel.Data] { +func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc) state.NoteOp[channel.Memory] { return doNoteCalc{ Semitone: st, UpdateFunc: fn, @@ -112,7 +112,7 @@ func (m *Manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateF // SetNumChannels updates the song to have the specified number of channels and resets their states func (m *Manager) SetNumChannels(num int) { - m.channels = make([]state.ChannelState[channel.Memory, channel.Data], num) + m.channels = make([]state.ChannelState[channel.Memory], num) for ch := range m.channels { cs := &m.channels[ch] @@ -286,7 +286,7 @@ func (m *Manager) GetSongData() song.Data { } // GetChannel returns the channel interface for the specified channel number -func (m *Manager) GetChannel(ch int) *state.ChannelState[channel.Memory, channel.Data] { +func (m *Manager) GetChannel(ch int) *state.ChannelState[channel.Memory] { return &m.channels[ch] } diff --git a/format/xm/playback/playback_command.go b/format/xm/playback/playback_command.go index b93e2de..7d965ec 100644 --- a/format/xm/playback/playback_command.go +++ b/format/xm/playback/playback_command.go @@ -17,7 +17,7 @@ type doNoteCalc struct { UpdateFunc state.PeriodUpdateFunc } -func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel.Memory]) error { if o.UpdateFunc == nil { return nil } @@ -31,7 +31,7 @@ func (o doNoteCalc) Process(p playback.Playback, cs *state.ChannelState[channel. return nil } -func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory], 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 @@ -55,7 +55,7 @@ func (m *Manager) processEffect(ch int, cs *state.ChannelState[channel.Memory, c return nil } -func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory], currentTick int, lastTick bool) error { var n note.Note = note.EmptyNote{} if cs.GetData() != nil { n = cs.GetData().GetNote() @@ -113,7 +113,7 @@ func (m *Manager) processRowNote(ch int, cs *state.ChannelState[channel.Memory, return nil } -func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory, channel.Data], currentTick int, lastTick bool) error { +func (m *Manager) processVoiceUpdates(ch int, cs *state.ChannelState[channel.Memory], currentTick int, lastTick bool) error { if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index 163af39..a41d7ac 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -102,7 +102,7 @@ func (m *Manager) processPatternRow() error { continue } - cdata := &channels[channelNum] + cdata := channels[channelNum] cs := &m.channels[channelNum] if err := cs.SetData(cdata); err != nil { @@ -147,7 +147,7 @@ func (m *Manager) processPatternRow() error { return nil } -func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory, channel.Data]) error { +func (m *Manager) processRowForChannel(cs *state.ChannelState[channel.Memory]) error { mem := cs.GetMemory() mem.TremorMem().Reset() diff --git a/format/xm/playback/playback_textoutput.go b/format/xm/playback/playback_textoutput.go index ee5cdc9..df9a5cd 100644 --- a/format/xm/playback/playback_textoutput.go +++ b/format/xm/playback/playback_textoutput.go @@ -19,7 +19,7 @@ func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if cd := cs.GetData(); cd != nil { + if cd := cs.GetData().(*channel.Data); cd != nil { rowText.Channels[ch] = *cd } } diff --git a/go.mod b/go.mod index 2e3b336..0dafad9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gotracker/playback -go 1.18 +go 1.21 require ( github.com/gotracker/goaudiofile v1.0.14 @@ -8,7 +8,7 @@ require ( 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-20231214170342-aacd6d4b4611 ) require github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index c5aa1a4..542d993 100644 --- a/go.sum +++ b/go.sum @@ -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-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 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/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/player/state/channel.go b/player/state/channel.go index 416056e..46b37f9 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -22,20 +22,20 @@ type NoteTrigger struct { Tick int } -type VolOp[TMemory, TChannelData any] interface { - Process(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error +type VolOp[TMemory any] interface { + Process(p playback.Playback, cs *ChannelState[TMemory]) error } -type NoteOp[TMemory, TChannelData any] interface { - Process(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error +type NoteOp[TMemory any] interface { + Process(p playback.Playback, cs *ChannelState[TMemory]) error } type PeriodUpdateFunc func(period.Period) -type SemitoneSetterFactory[TMemory, TChannelData any] func(note.Semitone, PeriodUpdateFunc) NoteOp[TMemory, TChannelData] +type SemitoneSetterFactory[TMemory any] func(note.Semitone, PeriodUpdateFunc) NoteOp[TMemory] // ChannelState is the state of a single channel -type ChannelState[TMemory, TChannelData any] struct { +type ChannelState[TMemory any] struct { activeState Active targetState Playback prevState Active @@ -43,10 +43,10 @@ type ChannelState[TMemory, TChannelData any] struct { ActiveEffect playback.Effect s song.Data - txn ChannelDataTransaction[TMemory, TChannelData] - prevTxn ChannelDataTransaction[TMemory, TChannelData] + txn ChannelDataTransaction[TMemory] + prevTxn ChannelDataTransaction[TMemory] - SemitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData] + SemitoneSetterFactory SemitoneSetterFactory[TMemory] StoredSemitone note.Semitone // from pattern, unmodified, current note PortaTargetPeriod optional.Value[period.Period] @@ -67,7 +67,7 @@ type ChannelState[TMemory, TChannelData any] struct { } // WillTriggerOn returns true if a note will trigger on the tick specified -func (cs *ChannelState[TMemory, TChannelData]) WillTriggerOn(tick int) (bool, note.Action) { +func (cs *ChannelState[TMemory]) WillTriggerOn(tick int) (bool, note.Action) { if trigger, ok := cs.Trigger.Get(); ok { return trigger.Tick == tick, trigger.NoteAction } @@ -76,7 +76,7 @@ func (cs *ChannelState[TMemory, TChannelData]) WillTriggerOn(tick int) (bool, no } // 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]) { +func (cs *ChannelState[TMemory]) AdvanceRow(txn ChannelDataTransaction[TMemory]) { cs.prevState = cs.activeState cs.targetState = cs.activeState.Playback cs.Trigger.Reset() @@ -89,7 +89,7 @@ func (cs *ChannelState[TMemory, TChannelData]) AdvanceRow(txn ChannelDataTransac } // RenderRowTick renders a channel's row data for a single tick -func (cs *ChannelState[TMemory, TChannelData]) RenderRowTick(details RenderDetails, pastNotes []*Active) ([]mixing.Data, error) { +func (cs *ChannelState[TMemory]) RenderRowTick(details RenderDetails, pastNotes []*Active) ([]mixing.Data, error) { if cs.PlaybackFrozen() { return nil, nil } @@ -100,68 +100,68 @@ func (cs *ChannelState[TMemory, TChannelData]) RenderRowTick(details RenderDetai } // ResetStates resets the channel's internal states -func (cs *ChannelState[TMemory, TChannelData]) ResetStates() { +func (cs *ChannelState[TMemory]) ResetStates() { cs.activeState.Reset() cs.targetState.Reset() cs.prevState.Reset() } -func (cs *ChannelState[TMemory, TChannelData]) GetActiveEffect() playback.Effect { +func (cs *ChannelState[TMemory]) GetActiveEffect() playback.Effect { return cs.ActiveEffect } -func (cs *ChannelState[TMemory, TChannelData]) SetActiveEffect(e playback.Effect) { +func (cs *ChannelState[TMemory]) SetActiveEffect(e playback.Effect) { cs.ActiveEffect = e } // FreezePlayback suspends mixer progression on the channel -func (cs *ChannelState[TMemory, TChannelData]) FreezePlayback() { +func (cs *ChannelState[TMemory]) FreezePlayback() { cs.freezePlayback = true } // UnfreezePlayback resumes mixer progression on the channel -func (cs *ChannelState[TMemory, TChannelData]) UnfreezePlayback() { +func (cs *ChannelState[TMemory]) UnfreezePlayback() { cs.freezePlayback = false } // PlaybackFrozen returns true if the mixer progression for the channel is suspended -func (cs ChannelState[TMemory, TChannelData]) PlaybackFrozen() bool { +func (cs ChannelState[TMemory]) PlaybackFrozen() bool { return cs.freezePlayback } // ResetRetriggerCount sets the retrigger count to 0 -func (cs *ChannelState[TMemory, TChannelData]) ResetRetriggerCount() { +func (cs *ChannelState[TMemory]) ResetRetriggerCount() { cs.RetriggerCount = 0 } // GetMemory returns the interface to the custom effect memory module -func (cs *ChannelState[TMemory, TChannelData]) GetMemory() *TMemory { +func (cs *ChannelState[TMemory]) GetMemory() *TMemory { return cs.Memory } // SetMemory sets the custom effect memory interface -func (cs *ChannelState[TMemory, TChannelData]) SetMemory(mem *TMemory) { +func (cs *ChannelState[TMemory]) SetMemory(mem *TMemory) { cs.Memory = mem } // GetActiveVolume returns the current active volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) GetActiveVolume() volume.Volume { +func (cs *ChannelState[TMemory]) GetActiveVolume() volume.Volume { return cs.activeState.Volume } // SetActiveVolume sets the active volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) SetActiveVolume(vol volume.Volume) { +func (cs *ChannelState[TMemory]) SetActiveVolume(vol volume.Volume) { if vol != volume.VolumeUseInstVol { cs.activeState.Volume = vol } } -func (cs *ChannelState[TMemory, TChannelData]) SetSongDataInterface(s song.Data) { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) GetData() song.ChannelData { if cs.txn == nil { return nil } @@ -169,7 +169,7 @@ func (cs *ChannelState[TMemory, TChannelData]) GetData() *TChannelData { return cs.txn.GetData() } -func (cs *ChannelState[TMemory, TChannelData]) SetData(cdata *TChannelData) error { +func (cs *ChannelState[TMemory]) SetData(cdata song.ChannelData) error { if cs.txn == nil { return nil } @@ -177,12 +177,12 @@ func (cs *ChannelState[TMemory, TChannelData]) SetData(cdata *TChannelData) erro return cs.txn.SetData(cdata, cs.s, cs) } -func (cs *ChannelState[TMemory, TChannelData]) GetTxn() ChannelDataTransaction[TMemory, TChannelData] { +func (cs *ChannelState[TMemory]) GetTxn() ChannelDataTransaction[TMemory] { return cs.txn } // GetPortaTargetPeriod returns the current target portamento (to note) sampler period -func (cs *ChannelState[TMemory, TChannelData]) GetPortaTargetPeriod() period.Period { +func (cs *ChannelState[TMemory]) GetPortaTargetPeriod() period.Period { if p, ok := cs.PortaTargetPeriod.Get(); ok { return p } @@ -190,7 +190,7 @@ func (cs *ChannelState[TMemory, TChannelData]) GetPortaTargetPeriod() period.Per } // SetPortaTargetPeriod sets the current target portamento (to note) sampler period -func (cs *ChannelState[TMemory, TChannelData]) SetPortaTargetPeriod(period period.Period) { +func (cs *ChannelState[TMemory]) SetPortaTargetPeriod(period period.Period) { if period != nil { cs.PortaTargetPeriod.Set(period) } else { @@ -199,48 +199,48 @@ func (cs *ChannelState[TMemory, TChannelData]) SetPortaTargetPeriod(period perio } // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory, TChannelData]) GetTargetPeriod() period.Period { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetPeriodDelta(delta period.PeriodDelta) { cs.activeState.PeriodDelta = delta } // GetPeriodDelta gets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TMemory, TChannelData]) GetPeriodDelta() period.PeriodDelta { +func (cs *ChannelState[TMemory]) GetPeriodDelta() period.PeriodDelta { return cs.activeState.PeriodDelta } // SetVolumeActive enables or disables the sample of the instrument -func (cs *ChannelState[TMemory, TChannelData]) SetVolumeActive(on bool) { +func (cs *ChannelState[TMemory]) SetVolumeActive(on bool) { cs.volumeActive = on } // GetInstrument returns the interface to the active instrument -func (cs *ChannelState[TMemory, TChannelData]) GetInstrument() *instrument.Instrument { +func (cs *ChannelState[TMemory]) GetInstrument() *instrument.Instrument { return cs.activeState.Instrument } // SetInstrument sets the interface to the active instrument -func (cs *ChannelState[TMemory, TChannelData]) SetInstrument(inst *instrument.Instrument) { +func (cs *ChannelState[TMemory]) SetInstrument(inst *instrument.Instrument) { cs.activeState.Instrument = inst if inst != nil { if inst == cs.prevState.Instrument { @@ -252,67 +252,67 @@ func (cs *ChannelState[TMemory, TChannelData]) SetInstrument(inst *instrument.In } // GetVoice returns the active voice interface -func (cs *ChannelState[TMemory, TChannelData]) GetVoice() voice.Voice { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) GetPrevVoice() voice.Voice { return cs.prevState.Voice } // GetNoteSemitone returns the note semitone for the channel -func (cs *ChannelState[TMemory, TChannelData]) GetNoteSemitone() note.Semitone { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetNotePlayTick(enabled bool, action note.Action, tick int) { if enabled { cs.Trigger.Set(NoteTrigger{ NoteAction: action, @@ -324,78 +324,78 @@ func (cs *ChannelState[TMemory, TChannelData]) SetNotePlayTick(enabled bool, act } // GetRetriggerCount returns the current count of the retrigger counter -func (cs *ChannelState[TMemory, TChannelData]) GetRetriggerCount() uint8 { +func (cs *ChannelState[TMemory]) GetRetriggerCount() uint8 { return cs.RetriggerCount } // SetRetriggerCount sets the current count of the retrigger counter -func (cs *ChannelState[TMemory, TChannelData]) SetRetriggerCount(cnt uint8) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetPanEnabled(on bool) { cs.PanEnabled = on } // SetPan sets the active panning value of the channel -func (cs *ChannelState[TMemory, TChannelData]) SetPan(pan panning.Position) { +func (cs *ChannelState[TMemory]) 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 { +func (cs *ChannelState[TMemory]) GetPan() panning.Position { return cs.activeState.Pan } // SetTargetSemitone sets the target semitone for the channel -func (cs *ChannelState[TMemory, TChannelData]) SetTargetSemitone(st note.Semitone) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetStoredSemitone(st note.Semitone) { cs.StoredSemitone = st } // SetRenderChannel sets the output channel for the channel -func (cs *ChannelState[TMemory, TChannelData]) SetRenderChannel(outputCh *render.Channel) { +func (cs *ChannelState[TMemory]) SetRenderChannel(outputCh *render.Channel) { cs.RenderChannel = outputCh } // GetRenderChannel returns the output channel for the channel -func (cs *ChannelState[TMemory, TChannelData]) GetRenderChannel() *render.Channel { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetChannelVolume(cv volume.Volume) { cs.RenderChannel.ChannelVolume = cv } // GetChannelVolume gets the channel volume on the channel -func (cs *ChannelState[TMemory, TChannelData]) GetChannelVolume() volume.Volume { +func (cs *ChannelState[TMemory]) GetChannelVolume() volume.Volume { return cs.RenderChannel.ChannelVolume } // SetEnvelopePosition sets the envelope position for the active instrument -func (cs *ChannelState[TMemory, TChannelData]) SetEnvelopePosition(ticks int) { +func (cs *ChannelState[TMemory]) SetEnvelopePosition(ticks int) { if nc := cs.GetVoice(); nc != nil { voice.SetVolumeEnvelopePosition(nc, ticks) voice.SetPanEnvelopePosition(nc, ticks) @@ -406,7 +406,7 @@ func (cs *ChannelState[TMemory, TChannelData]) SetEnvelopePosition(ticks int) { // 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() { +func (cs *ChannelState[TMemory]) TransitionActiveToPastState() { if cs.PastNotes != nil { switch cs.NewNoteAction { case note.ActionCut: @@ -439,35 +439,35 @@ func (cs *ChannelState[TMemory, TChannelData]) TransitionActiveToPastState() { } // DoPastNoteEffect performs an action on all past-note playbacks associated with the channel -func (cs *ChannelState[TMemory, TChannelData]) DoPastNoteEffect(action note.Action) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetNewNoteAction(nna note.Action) { cs.NewNoteAction = nna } // GetNewNoteAction gets the New-Note Action on the channel -func (cs *ChannelState[TMemory, TChannelData]) GetNewNoteAction() note.Action { +func (cs *ChannelState[TMemory]) GetNewNoteAction() note.Action { return cs.NewNoteAction } // SetVolumeEnvelopeEnable sets the enable flag on the active volume envelope -func (cs *ChannelState[TMemory, TChannelData]) SetVolumeEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) 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) { +func (cs *ChannelState[TMemory]) SetPitchEnvelopeEnable(enabled bool) { voice.EnablePitchEnvelope(cs.activeState.Voice, enabled) } -func (cs *ChannelState[TMemory, TChannelData]) NoteCut() { +func (cs *ChannelState[TMemory]) NoteCut() { cs.activeState.Period = nil } diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index a539fca..64cde3b 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -11,20 +11,20 @@ import ( "github.com/heucuva/optional" ) -type ChannelDataTransaction[TMemory, TChannelData any] interface { - GetData() *TChannelData - SetData(data *TChannelData, s song.Data, cs *ChannelState[TMemory, TChannelData]) error +type ChannelDataTransaction[TMemory any] interface { + GetData() song.ChannelData + SetData(data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) 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 + CommitPreRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error + CommitRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error + CommitPostRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) 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 + CommitPreTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error + CommitTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error + CommitPostTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error - AddVolOp(op VolOp[TMemory, TChannelData]) - AddNoteOp(op NoteOp[TMemory, TChannelData]) + AddVolOp(op VolOp[TMemory]) + AddNoteOp(op NoteOp[TMemory]) } type ChannelDataActions struct { @@ -39,43 +39,43 @@ type ChannelDataActions struct { 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 ChannelDataConverter[TMemory any] interface { + Process(out *ChannelDataActions, data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error } -type ChannelDataTxnHelper[TMemory, TChannelData any, TChannelDataConverter ChannelDataConverter[TMemory, TChannelData]] struct { - Data *TChannelData +type ChannelDataTxnHelper[TMemory any, TChannelDataConverter ChannelDataConverter[TMemory]] struct { + Data song.ChannelData ChannelDataActions - VolOps []VolOp[TMemory, TChannelData] - NoteOps []NoteOp[TMemory, TChannelData] + VolOps []VolOp[TMemory] + NoteOps []NoteOp[TMemory] } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) GetData() *TChannelData { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) GetData() song.ChannelData { return d.Data } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) SetData(cd *TChannelData, s song.Data, cs *ChannelState[TMemory, TChannelData]) error { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) SetData(cd song.ChannelData, s song.Data, cs *ChannelState[TMemory]) 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 { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPreRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) CommitPostRow(p playback.Playback, cs *ChannelState[TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TMemory, TChannelData]) error { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPostRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) 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 { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPreTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { // pre-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -87,15 +87,15 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) Com 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 { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { + if err := playback.DoEffect[TMemory](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 { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPostTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { // post-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -107,11 +107,11 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) Com return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) AddVolOp(op VolOp[TMemory, TChannelData]) { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) AddVolOp(op VolOp[TMemory]) { d.VolOps = append(d.VolOps, op) } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) ProcessVolOps(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) ProcessVolOps(p playback.Playback, cs *ChannelState[TMemory]) error { for _, op := range d.VolOps { if op == nil { continue @@ -125,11 +125,11 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) Pro return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) AddNoteOp(op NoteOp[TMemory, TChannelData]) { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) AddNoteOp(op NoteOp[TMemory]) { d.NoteOps = append(d.NoteOps, op) } -func (d *ChannelDataTxnHelper[TMemory, TChannelData, TChannelDataConverter]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TMemory, TChannelData]) error { +func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TMemory]) error { for _, op := range d.NoteOps { if op == nil { continue diff --git a/song/pattern.go b/song/pattern.go index b922cc9..66c3869 100644 --- a/song/pattern.go +++ b/song/pattern.go @@ -12,16 +12,16 @@ var ( ) // Pattern is an interface for pattern data -type Pattern[TChannelData any] interface { - GetRow(index.Row) Row[TChannelData] - GetRows() Rows[TChannelData] +type Pattern interface { + GetRow(index.Row) Row + GetRows() Rows } // Patterns is an array of pattern interfaces -type Patterns[TChannelData any] []Pattern[TChannelData] +type Patterns []Pattern // Rows is an interface to obtain row data -type Rows[TChannelData any] interface { - GetRow(index.Row) Row[TChannelData] +type Rows interface { + GetRow(index.Row) Row NumRows() int } diff --git a/song/row.go b/song/row.go index 487fd38..4393047 100644 --- a/song/row.go +++ b/song/row.go @@ -1,6 +1,6 @@ package song // Row is an interface to a row -type Row[TChannelData any] interface { - GetChannels() []TChannelData +type Row interface { + GetChannels() []ChannelData } diff --git a/song/song.go b/song/song.go index 922a584..de372e8 100644 --- a/song/song.go +++ b/song/song.go @@ -17,6 +17,6 @@ type Data interface { GetName() string } -type PatternData[TChannelData any] interface { - GetPattern(index.Pattern) Pattern[TChannelData] +type PatternData interface { + GetPattern(index.Pattern) Pattern } From 95a589fbae684797b4f7c9b5b91b263454f0d535 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 16 Dec 2023 14:33:34 -0800 Subject: [PATCH 02/63] Simplify pattern processing --- format/common/genericpattern.go | 20 ++++++++++++++++++++ format/common/genericrow.go | 16 ++++++++++++++++ format/it/effect/effectfactory.go | 13 +++++++------ format/it/pattern/pattern.go | 18 +++--------------- format/it/pattern/row.go | 12 ++---------- format/it/playback/playback_pattern.go | 17 +++++------------ format/it/playback/playback_textoutput.go | 4 ++-- format/s3m/effect/effectfactory.go | 6 +++--- format/s3m/pattern/pattern.go | 18 +++--------------- format/s3m/pattern/row.go | 12 ++---------- format/s3m/playback/playback_pattern.go | 11 ++++------- format/s3m/playback/playback_textoutput.go | 4 ++-- format/xm/effect/effectfactory.go | 4 ++-- format/xm/effect/effectfactory_standard.go | 2 +- format/xm/pattern/pattern.go | 18 +++--------------- format/xm/pattern/row.go | 12 ++---------- format/xm/playback/playback_pattern.go | 17 +++++------------ format/xm/playback/playback_textoutput.go | 4 ++-- index/channel.go | 3 +++ song/row.go | 5 ++++- 20 files changed, 91 insertions(+), 125 deletions(-) create mode 100644 format/common/genericpattern.go create mode 100644 format/common/genericrow.go create mode 100644 index/channel.go diff --git a/format/common/genericpattern.go b/format/common/genericpattern.go new file mode 100644 index 0000000..0fdae19 --- /dev/null +++ b/format/common/genericpattern.go @@ -0,0 +1,20 @@ +package common + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" +) + +type Pattern[TChannelData song.ChannelData] []Row[TChannelData] + +func (p Pattern[TChannelData]) GetRow(row index.Row) song.Row { + return p[row] +} + +func (p Pattern[TChannelData]) NumRows() int { + return len(p) +} + +func (p Pattern[TChannelData]) GetRows() song.Rows { + return p +} diff --git a/format/common/genericrow.go b/format/common/genericrow.go new file mode 100644 index 0000000..446b80c --- /dev/null +++ b/format/common/genericrow.go @@ -0,0 +1,16 @@ +package common + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" +) + +type Row[TChannelData song.ChannelData] []TChannelData + +func (r Row[TChannelData]) GetNumChannels() int { + return len(r) +} + +func (r Row[TChannelData]) GetChannel(ch index.Channel) song.ChannelData { + return r[ch] +} diff --git a/format/it/effect/effectfactory.go b/format/it/effect/effectfactory.go index 20eb706..8b8c7f1 100644 --- a/format/it/effect/effectfactory.go +++ b/format/it/effect/effectfactory.go @@ -27,11 +27,12 @@ func (e VolEff) String() string { // Factory produces an effect for the provided channel pattern data func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { - d, _ := data.(*channel.Data) - if d == nil { + if data == nil { return nil } + d, _ := data.(channel.Data) + if !d.What.HasCommand() && !d.What.HasVolPan() { return nil } @@ -59,7 +60,7 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { } } -func standardEffectFactory(mem *channel.Memory, data *channel.Data) EffectIT { +func standardEffectFactory(mem *channel.Memory, data channel.Data) EffectIT { switch data.Effect + '@' { case '@': // unused return nil @@ -134,7 +135,7 @@ func standardEffectFactory(mem *channel.Memory, data *channel.Data) EffectIT { return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} } -func specialEffect(data *channel.Data) EffectIT { +func specialEffect(data channel.Data) EffectIT { switch data.EffectParameter >> 4 { case 0x0: // unused return nil @@ -173,7 +174,7 @@ func specialEffect(data *channel.Data) EffectIT { return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} } -func specialNoteEffects(data *channel.Data) EffectIT { +func specialNoteEffects(data channel.Data) EffectIT { switch data.EffectParameter & 0xf { case 0x0: // Past Note Cut return PastNoteCut(data.EffectParameter) @@ -226,7 +227,7 @@ func volumeSlideFactory(mem *channel.Memory, cd channel.Command, ce channel.Data return nil } -func soundControlEffect(data *channel.Data) EffectIT { +func soundControlEffect(data channel.Data) EffectIT { switch data.EffectParameter & 0xF { case 0x0: // Surround Off case 0x1: // Surround On diff --git a/format/it/pattern/pattern.go b/format/it/pattern/pattern.go index 92a7b4f..7e0f9a1 100644 --- a/format/it/pattern/pattern.go +++ b/format/it/pattern/pattern.go @@ -1,20 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/song" + "github.com/gotracker/playback/format/common" + "github.com/gotracker/playback/format/it/channel" ) -type Pattern []Row - -func (p Pattern) GetRow(row index.Row) song.Row { - return p[row] -} - -func (p Pattern) NumRows() int { - return len(p) -} - -func (p Pattern) GetRows() song.Rows { - return p -} +type Pattern = common.Pattern[channel.Data] diff --git a/format/it/pattern/row.go b/format/it/pattern/row.go index d376e3a..ccf10bb 100644 --- a/format/it/pattern/row.go +++ b/format/it/pattern/row.go @@ -1,16 +1,8 @@ package pattern import ( + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/song" ) -type Row []channel.Data - -func (r Row) GetChannels() []song.ChannelData { - c := make([]song.ChannelData, len(r)) - for i := range r { - c[i] = &r[i] - } - return c -} +type Row = common.Row[channel.Data] diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index 690710a..ee4c507 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" ) @@ -96,13 +97,9 @@ func (m *Manager) processPatternRow() error { } // generate effects and run prestart - channels := row.GetChannels() - for channelNum := range channels { - if channelNum >= m.GetNumChannels() { - continue - } - - cdata := channels[channelNum] + nch := row.GetNumChannels() + for channelNum := 0; channelNum < nch; channelNum++ { + cdata := row.GetChannel(index.Channel(channelNum)) cs := &m.channels[channelNum] if err := cs.SetData(cdata); err != nil { @@ -132,11 +129,7 @@ func (m *Manager) processPatternRow() error { m.rowRenderState.currentTick = 0 // run row processing, now that prestart has completed - for channelNum := range row.GetChannels() { - if channelNum >= m.GetNumChannels() { - continue - } - + for channelNum := 0; channelNum < nch; channelNum++ { cs := &m.channels[channelNum] if err := m.processRowForChannel(cs); err != nil { diff --git a/format/it/playback/playback_textoutput.go b/format/it/playback/playback_textoutput.go index 84ee63f..c52f8ac 100644 --- a/format/it/playback/playback_textoutput.go +++ b/format/it/playback/playback_textoutput.go @@ -19,8 +19,8 @@ func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if cd := cs.GetData().(*channel.Data); cd != nil { - rowText.Channels[ch] = *cd + if data := cs.GetData(); data != nil { + rowText.Channels[ch], _ = data.(channel.Data) } } return &rowText diff --git a/format/s3m/effect/effectfactory.go b/format/s3m/effect/effectfactory.go index e64d248..d67f39b 100644 --- a/format/s3m/effect/effectfactory.go +++ b/format/s3m/effect/effectfactory.go @@ -14,11 +14,11 @@ type ChannelCommand channel.DataEffect // Factory produces an effect for the provided channel pattern data func Factory(mem *channel.Memory, data song.ChannelData) EffectS3M { - d, _ := data.(*channel.Data) - if d == nil { + if data == nil { return nil } + d, _ := data.(channel.Data) if !d.What.HasCommand() { return nil } @@ -90,7 +90,7 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectS3M { return UnhandledCommand{Command: d.Command, Info: d.Info} } -func specialEffect(mem *channel.Memory, data *channel.Data) EffectS3M { +func specialEffect(mem *channel.Memory, data channel.Data) EffectS3M { var cmd = mem.LastNonZero(data.Info) switch cmd >> 4 { case 0x0: // Set Filter on/off diff --git a/format/s3m/pattern/pattern.go b/format/s3m/pattern/pattern.go index 92a7b4f..9d6c8d8 100644 --- a/format/s3m/pattern/pattern.go +++ b/format/s3m/pattern/pattern.go @@ -1,20 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/song" + "github.com/gotracker/playback/format/common" + "github.com/gotracker/playback/format/s3m/channel" ) -type Pattern []Row - -func (p Pattern) GetRow(row index.Row) song.Row { - return p[row] -} - -func (p Pattern) NumRows() int { - return len(p) -} - -func (p Pattern) GetRows() song.Rows { - return p -} +type Pattern = common.Pattern[channel.Data] diff --git a/format/s3m/pattern/row.go b/format/s3m/pattern/row.go index 7f7215f..329f789 100644 --- a/format/s3m/pattern/row.go +++ b/format/s3m/pattern/row.go @@ -1,16 +1,8 @@ package pattern import ( + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/song" ) -type Row []channel.Data - -func (r Row) GetChannels() []song.ChannelData { - c := make([]song.ChannelData, len(r)) - for i := range r { - c[i] = &r[i] - } - return c -} +type Row = common.Row[channel.Data] diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index aeca8c0..754921f 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" ) @@ -96,13 +97,9 @@ func (m *Manager) processPatternRow() error { } // generate effects and run prestart - channels := row.GetChannels() - for channelNum := range channels { - if channelNum >= m.GetNumChannels() { - continue - } - - cdata := channels[channelNum] + nch := row.GetNumChannels() + for channelNum := 0; channelNum < nch; channelNum++ { + cdata := row.GetChannel(index.Channel(channelNum)) cs := &m.channels[channelNum] if err := cs.SetData(cdata); err != nil { diff --git a/format/s3m/playback/playback_textoutput.go b/format/s3m/playback/playback_textoutput.go index ba38f64..1853c38 100644 --- a/format/s3m/playback/playback_textoutput.go +++ b/format/s3m/playback/playback_textoutput.go @@ -19,8 +19,8 @@ func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if cd, _ := cs.GetData().(*channel.Data); cd != nil { - rowText.Channels[ch] = *cd + if data := cs.GetData(); data != nil { + rowText.Channels[ch], _ = data.(channel.Data) } } return &rowText diff --git a/format/xm/effect/effectfactory.go b/format/xm/effect/effectfactory.go index 7a3ccb4..bbb8c47 100644 --- a/format/xm/effect/effectfactory.go +++ b/format/xm/effect/effectfactory.go @@ -27,11 +27,11 @@ func (e VolEff) String() string { // Factory produces an effect for the provided channel pattern data func Factory(mem *channel.Memory, data song.ChannelData) EffectXM { - d, _ := data.(*channel.Data) - if d == nil { + if data == nil { return nil } + d, _ := data.(channel.Data) if !d.HasCommand() { return nil } diff --git a/format/xm/effect/effectfactory_standard.go b/format/xm/effect/effectfactory_standard.go index a32b533..04e96b4 100644 --- a/format/xm/effect/effectfactory_standard.go +++ b/format/xm/effect/effectfactory_standard.go @@ -4,7 +4,7 @@ import ( "github.com/gotracker/playback/format/xm/channel" ) -func standardEffectFactory(mem *channel.Memory, cd *channel.Data) EffectXM { +func standardEffectFactory(mem *channel.Memory, cd channel.Data) EffectXM { if !cd.What.HasEffect() && !cd.What.HasEffectParameter() { return nil } diff --git a/format/xm/pattern/pattern.go b/format/xm/pattern/pattern.go index 92a7b4f..6daa5a1 100644 --- a/format/xm/pattern/pattern.go +++ b/format/xm/pattern/pattern.go @@ -1,20 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/song" + "github.com/gotracker/playback/format/common" + "github.com/gotracker/playback/format/xm/channel" ) -type Pattern []Row - -func (p Pattern) GetRow(row index.Row) song.Row { - return p[row] -} - -func (p Pattern) NumRows() int { - return len(p) -} - -func (p Pattern) GetRows() song.Rows { - return p -} +type Pattern = common.Pattern[channel.Data] diff --git a/format/xm/pattern/row.go b/format/xm/pattern/row.go index 8a810fe..1fb55a4 100644 --- a/format/xm/pattern/row.go +++ b/format/xm/pattern/row.go @@ -1,16 +1,8 @@ package pattern import ( + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/song" ) -type Row []channel.Data - -func (r Row) GetChannels() []song.ChannelData { - c := make([]song.ChannelData, len(r)) - for i := range r { - c[i] = &r[i] - } - return c -} +type Row = common.Row[channel.Data] diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index a41d7ac..98112c1 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" ) @@ -96,13 +97,9 @@ func (m *Manager) processPatternRow() error { } // generate effects and run prestart - channels := row.GetChannels() - for channelNum := range channels { - if channelNum >= m.GetNumChannels() { - continue - } - - cdata := channels[channelNum] + nch := row.GetNumChannels() + for channelNum := 0; channelNum < nch; channelNum++ { + cdata := row.GetChannel(index.Channel(channelNum)) cs := &m.channels[channelNum] if err := cs.SetData(cdata); err != nil { @@ -132,11 +129,7 @@ func (m *Manager) processPatternRow() error { m.rowRenderState.currentTick = 0 // run row processing, now that prestart has completed - for channelNum := range row.GetChannels() { - if channelNum >= m.GetNumChannels() { - continue - } - + for channelNum := 0; channelNum < nch; channelNum++ { cs := &m.channels[channelNum] if err := m.processRowForChannel(cs); err != nil { diff --git a/format/xm/playback/playback_textoutput.go b/format/xm/playback/playback_textoutput.go index df9a5cd..7956a1c 100644 --- a/format/xm/playback/playback_textoutput.go +++ b/format/xm/playback/playback_textoutput.go @@ -19,8 +19,8 @@ func (m *Manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if cd := cs.GetData().(*channel.Data); cd != nil { - rowText.Channels[ch] = *cd + if data := cs.GetData(); data != nil { + rowText.Channels[ch], _ = data.(channel.Data) } } return &rowText diff --git a/index/channel.go b/index/channel.go new file mode 100644 index 0000000..dc8de36 --- /dev/null +++ b/index/channel.go @@ -0,0 +1,3 @@ +package index + +type Channel uint8 diff --git a/song/row.go b/song/row.go index 4393047..04ed9e8 100644 --- a/song/row.go +++ b/song/row.go @@ -1,6 +1,9 @@ package song +import "github.com/gotracker/playback/index" + // Row is an interface to a row type Row interface { - GetChannels() []ChannelData + GetNumChannels() int + GetChannel(index.Channel) ChannelData } From 844c8ea605724d0aa9dde806cdedade3e22a4491 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 16 Dec 2023 15:20:37 -0800 Subject: [PATCH 03/63] simplify playback system a little --- format/it/playback/playback.go | 1 + format/it/playback/playback_pattern.go | 4 +++ format/it/playback/playback_render.go | 37 +++++++++++-------------- format/s3m/playback/playback_pattern.go | 4 +++ format/s3m/playback/playback_render.go | 30 ++++++++------------ format/xm/playback/playback_pattern.go | 4 +++ format/xm/playback/playback_render.go | 30 ++++++++------------ player/posttick.go | 14 ++++++++++ player/pretick.go | 14 ++++++++++ player/tracker.go | 10 +++++++ 10 files changed, 89 insertions(+), 59 deletions(-) create mode 100644 player/posttick.go create mode 100644 player/pretick.go diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index da31d5f..be86588 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -51,6 +51,7 @@ func NewManager(song *layout.Song) (*Manager, error) { m.PastNotes.SetMaxPerChannel(1) + m.Tracker.PreTickable = &m m.Tracker.Tickable = &m m.Tracker.Premixable = &m m.Tracker.Traceable = &m diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index ee4c507..6d97e24 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -98,6 +98,10 @@ func (m *Manager) processPatternRow() error { // generate effects and run prestart nch := row.GetNumChannels() + if actual := m.GetNumChannels(); nch > actual { + nch = actual + } + for channelNum := 0; channelNum < nch; channelNum++ { cdata := row.GetChannel(index.Channel(channelNum)) diff --git a/format/it/playback/playback_render.go b/format/it/playback/playback_render.go index b2add6c..fceca16 100644 --- a/format/it/playback/playback_render.go +++ b/format/it/playback/playback_render.go @@ -6,28 +6,16 @@ import ( "github.com/gotracker/playback/player/state" ) -// OnTick runs the IT tick processing -func (m *Manager) OnTick() error { - m.premix = nil - +// OnPreTick runs the IT pre-tick processing +func (m *Manager) OnPreTick() error { 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 -} +// OnTick runs the IT tick processing +func (m *Manager) OnTick() error { + 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() @@ -37,7 +25,7 @@ func (m *Manager) renderTick() (*output.PremixData, error) { if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { if err := m.processPatternRow(); err != nil { - return nil, err + return err } } @@ -48,7 +36,7 @@ func (m *Manager) renderTick() (*output.PremixData, error) { } if err := m.soundRenderTick(premix); err != nil { - return nil, err + return err } finalData.Order = int(m.pattern.GetCurrentOrder()) @@ -64,10 +52,16 @@ func (m *Manager) renderTick() (*output.PremixData, error) { } if err := postMixRowTxn.Commit(); err != nil { - return nil, err + return err } - return premix, nil + 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 } type rowRenderState struct { @@ -84,6 +78,7 @@ func (m *Manager) soundRenderTick(premix *output.PremixData) error { 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 } diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index 754921f..e754bfa 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -98,6 +98,10 @@ func (m *Manager) processPatternRow() error { // generate effects and run prestart nch := row.GetNumChannels() + if actual := m.GetNumChannels(); nch > actual { + nch = actual + } + for channelNum := 0; channelNum < nch; channelNum++ { cdata := row.GetChannel(index.Channel(channelNum)) diff --git a/format/s3m/playback/playback_render.go b/format/s3m/playback/playback_render.go index b946c1e..6ce46e8 100644 --- a/format/s3m/playback/playback_render.go +++ b/format/s3m/playback/playback_render.go @@ -9,22 +9,7 @@ import ( // 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() @@ -34,7 +19,7 @@ func (m *Manager) renderTick() (*output.PremixData, error) { if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { if err := m.processPatternRow(); err != nil { - return nil, err + return err } } @@ -45,7 +30,7 @@ func (m *Manager) renderTick() (*output.PremixData, error) { } if err := m.soundRenderTick(premix); err != nil { - return nil, err + return err } finalData.Order = int(m.pattern.GetCurrentOrder()) @@ -61,9 +46,16 @@ func (m *Manager) renderTick() (*output.PremixData, error) { } if err := postMixRowTxn.Commit(); err != nil { - return nil, err + return err } - return premix, nil + + 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 } type rowRenderState struct { diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index 98112c1..0b0d6cb 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -98,6 +98,10 @@ func (m *Manager) processPatternRow() error { // generate effects and run prestart nch := row.GetNumChannels() + if actual := m.GetNumChannels(); nch > actual { + nch = actual + } + for channelNum := 0; channelNum < nch; channelNum++ { cdata := row.GetChannel(index.Channel(channelNum)) diff --git a/format/xm/playback/playback_render.go b/format/xm/playback/playback_render.go index 466b40a..9a40ce5 100644 --- a/format/xm/playback/playback_render.go +++ b/format/xm/playback/playback_render.go @@ -9,22 +9,7 @@ import ( // 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() @@ -34,7 +19,7 @@ func (m *Manager) renderTick() (*output.PremixData, error) { if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { if err := m.processPatternRow(); err != nil { - return nil, err + return err } } @@ -45,7 +30,7 @@ func (m *Manager) renderTick() (*output.PremixData, error) { } if err := m.soundRenderTick(premix); err != nil { - return nil, err + return err } finalData.Order = int(m.pattern.GetCurrentOrder()) @@ -61,9 +46,16 @@ func (m *Manager) renderTick() (*output.PremixData, error) { } if err := postMixRowTxn.Commit(); err != nil { - return nil, err + return err } - return premix, nil + + 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 } type rowRenderState struct { diff --git a/player/posttick.go b/player/posttick.go new file mode 100644 index 0000000..e147d57 --- /dev/null +++ b/player/posttick.go @@ -0,0 +1,14 @@ +package player + +// PostTickable is an interface which exposes the OnPostTick call +type PostTickable interface { + OnPostTick() error +} + +// DoPostTick calls the OnPostTick() function on the interface, if possible +func DoPostTick(t PostTickable) error { + if t != nil { + return t.OnPostTick() + } + return nil +} diff --git a/player/pretick.go b/player/pretick.go new file mode 100644 index 0000000..109d8e1 --- /dev/null +++ b/player/pretick.go @@ -0,0 +1,14 @@ +package player + +// PreTickable is an interface which exposes the OnPreTick call +type PreTickable interface { + OnPreTick() error +} + +// DoPreTick calls the OnPreTick() function on the interface, if possible +func DoPreTick(t PreTickable) error { + if t != nil { + return t.OnPreTick() + } + return nil +} diff --git a/player/tracker.go b/player/tracker.go index a336c84..8202875 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -24,7 +24,9 @@ type Premixable interface { // Tracker is an extensible music tracker type Tracker struct { BaseClockRate period.Frequency + PreTickable PreTickable Tickable Tickable + PostTickable PostTickable Premixable Premixable Traceable Traceable @@ -111,10 +113,18 @@ func (t *Tracker) GetSampleRate() period.Frequency { } func (t *Tracker) renderTick() (*output.PremixData, error) { + if err := DoPreTick(t.PreTickable); err != nil { + return nil, err + } + if err := DoTick(t.Tickable); err != nil { return nil, err } + if err := DoPostTick(t.PostTickable); err != nil { + return nil, err + } + premix, err := t.Premixable.GetPremixData() if err != nil { return nil, err From 82f90aa05bda6c9402c87770f919184373607832 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 17 Dec 2023 10:49:42 -0800 Subject: [PATCH 04/63] clean up more playback object duplication - fix issue with IT sample speed calculations --- format/it/effect/effectfactory.go | 6 +- format/it/load/instrument.go | 4 + format/it/period/amiga.go | 9 +- format/it/playback/channeldata_transaction.go | 149 ---------------- format/it/playback/playback_pattern.go | 3 +- format/s3m/effect/effectfactory.go | 4 +- .../s3m/playback/channeldata_transaction.go | 137 --------------- format/s3m/playback/playback_pattern.go | 3 +- format/xm/effect/effectfactory.go | 4 +- format/xm/playback/channeldata_transaction.go | 137 --------------- format/xm/playback/playback_pattern.go | 3 +- player/state/channel_transaction.go | 164 ++++++++++++++++-- 12 files changed, 164 insertions(+), 459 deletions(-) delete mode 100644 format/it/playback/channeldata_transaction.go delete mode 100644 format/s3m/playback/channeldata_transaction.go delete mode 100755 format/xm/playback/channeldata_transaction.go diff --git a/format/it/effect/effectfactory.go b/format/it/effect/effectfactory.go index 8b8c7f1..8a4851d 100644 --- a/format/it/effect/effectfactory.go +++ b/format/it/effect/effectfactory.go @@ -8,9 +8,7 @@ import ( "github.com/gotracker/playback/song" ) -type EffectIT interface { - playback.Effect -} +type EffectIT = playback.Effect // VolEff is a combined effect that includes a volume effect and a standard effect type VolEff struct { @@ -37,7 +35,7 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { return nil } - eff := VolEff{} + var eff VolEff if d.What.HasVolPan() { ve := volPanEffectFactory(mem, d.VolPan) if ve != nil { diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 70e2d58..ce1a6e3 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -459,6 +459,10 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrumen ii.C2Spd = 8363.0 } + if si.Header.Flags.IsStereo() { + ii.C2Spd /= 2.0 + } + if !convSettings.linearFrequencySlides { ii.Static.AutoVibrato.Depth /= 64.0 } diff --git a/format/it/period/amiga.go b/format/it/period/amiga.go index 9047d32..3460f92 100644 --- a/format/it/period/amiga.go +++ b/format/it/period/amiga.go @@ -29,9 +29,10 @@ func (p Amiga) AddDelta(delta period.Delta) period.Period { } // 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 +// +// -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() @@ -76,7 +77,7 @@ func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { if finetunes < 0 { finetunes = 0 } - pow := math.Pow(2, float64(finetunes)/semitonesPerOctave) + pow := math.Pow(2, 1.0+float64(finetunes)/semitonesPerOctave) linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) period := Amiga(float64(semitonePeriodTable[0]) / linFreq) diff --git a/format/it/playback/channeldata_transaction.go b/format/it/playback/channeldata_transaction.go deleted file mode 100644 index 781e65f..0000000 --- a/format/it/playback/channeldata_transaction.go +++ /dev/null @@ -1,149 +0,0 @@ -package playback - -import ( - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/format/it/effect" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/player/state" - "github.com/gotracker/playback/song" -) - -type channelDataConverter struct{} - -func (c channelDataConverter) Process(out *state.ChannelDataActions, data song.ChannelData, s song.Data, cs *state.ChannelState[channel.Memory]) error { - if data == nil { - return nil - } - - var n note.Note = note.EmptyNote{} - inst := cs.GetInstrument() - prevInst := inst - - if data.HasNote() || data.HasInstrument() { - instID := data.GetInstrument(cs.StoredSemitone) - n = data.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = prevInst - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - if !note.IsEmpty(n) && inst == nil { - inst = prevInst - } - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) - } - out.NoteAction.Set(note.ActionRetrigger) - out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) - } - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Set(nil) - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else if !note.IsEmpty(n) { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } else { - out.NoteAction.Set(note.ActionCut) - } - } - - if data.HasVolume() { - v := data.GetVolume() - if v == volume.VolumeUseInstVol { - if inst != nil { - v = inst.GetDefaultVolume() - } - } - out.TargetVolume.Set(v) - } - - return nil -} - -type channelDataTransaction struct { - state.ChannelDataTxnHelper[channel.Memory, channelDataConverter] -} - -func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { - e := effect.Factory(cs.GetMemory(), d.Data) - cs.SetActiveEffect(e) - if e != nil { - if onEff := p.GetOnEffect(); onEff != nil { - onEff(e) - } - if err := playback.EffectPreStart[channel.Memory](e, cs, p); err != nil { - return err - } - } - - return nil -} - -func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { - if pos, ok := d.TargetPos.Get(); ok { - cs.SetTargetPos(pos) - } - - if inst, ok := d.TargetInst.Get(); ok { - cs.SetTargetInst(inst) - } - - if period, ok := d.TargetPeriod.Get(); ok { - cs.SetTargetPeriod(period) - cs.SetPortaTargetPeriod(period) - } - - if st, ok := d.TargetStoredSemitone.Get(); ok { - cs.SetStoredSemitone(st) - } - - if nna, ok := d.TargetNewNoteAction.Get(); ok { - cs.SetNewNoteAction(nna) - } - - if v, ok := d.TargetVolume.Get(); ok { - cs.SetActiveVolume(v) - } - - na, targetTick := d.NoteAction.Get() - cs.UseTargetPeriod = targetTick - cs.SetNotePlayTick(targetTick, na, 0) - - if st, ok := d.NoteCalcST.Get(); ok { - d.AddNoteOp(semitoneSetterFactory(st, cs.SetTargetPeriod)) - } - - return nil -} - -func init() { - var _ channelDataConverter -} diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index 6d97e24..756183b 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/format/it/effect" "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" @@ -89,7 +90,7 @@ func (m *Manager) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(&channelDataTransaction{}) + cs.AdvanceRow(state.NewChannelDataTxn[channel.Memory](effect.Factory)) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/format/s3m/effect/effectfactory.go b/format/s3m/effect/effectfactory.go index d67f39b..6d2a05b 100644 --- a/format/s3m/effect/effectfactory.go +++ b/format/s3m/effect/effectfactory.go @@ -6,9 +6,7 @@ import ( "github.com/gotracker/playback/song" ) -type EffectS3M interface { - playback.Effect -} +type EffectS3M = playback.Effect type ChannelCommand channel.DataEffect diff --git a/format/s3m/playback/channeldata_transaction.go b/format/s3m/playback/channeldata_transaction.go deleted file mode 100644 index 2121799..0000000 --- a/format/s3m/playback/channeldata_transaction.go +++ /dev/null @@ -1,137 +0,0 @@ -package playback - -import ( - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/format/s3m/effect" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/player/state" - "github.com/gotracker/playback/song" -) - -type channelDataConverter struct{} - -func (c channelDataConverter) Process(out *state.ChannelDataActions, data song.ChannelData, s song.Data, cs *state.ChannelState[channel.Memory]) error { - if data == nil { - return nil - } - - var inst *instrument.Instrument - - if data.HasNote() || data.HasInstrument() { - instID := data.GetInstrument(cs.StoredSemitone) - n := data.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = cs.GetInstrument() - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) - } - out.NoteAction.Set(note.ActionRetrigger) - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Set(nil) - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } - } - } - - if data.HasVolume() { - v := data.GetVolume() - if v == volume.VolumeUseInstVol { - if inst != nil { - v = inst.GetDefaultVolume() - } - } - out.TargetVolume.Set(v) - } - - return nil -} - -type channelDataTransaction struct { - state.ChannelDataTxnHelper[channel.Memory, channelDataConverter] -} - -func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { - e := effect.Factory(cs.GetMemory(), d.Data) - cs.SetActiveEffect(e) - if e != nil { - if onEff := p.GetOnEffect(); onEff != nil { - onEff(e) - } - if err := playback.EffectPreStart[channel.Memory](e, cs, p); err != nil { - return err - } - } - - return nil -} - -func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { - if pos, ok := d.TargetPos.Get(); ok { - cs.SetTargetPos(pos) - } - - if inst, ok := d.TargetInst.Get(); ok { - cs.SetTargetInst(inst) - } - - if period, ok := d.TargetPeriod.Get(); ok { - cs.SetTargetPeriod(period) - } - - if st, ok := d.TargetStoredSemitone.Get(); ok { - cs.SetStoredSemitone(st) - } - - if v, ok := d.TargetVolume.Get(); ok { - cs.SetActiveVolume(v) - } - - na, targetTick := d.NoteAction.Get() - cs.UseTargetPeriod = targetTick - cs.SetNotePlayTick(targetTick, na, 0) - - if st, ok := d.NoteCalcST.Get(); ok { - d.AddNoteOp(semitoneSetterFactory(st, cs.SetTargetPeriod)) - } - - return nil -} - -func init() { - var _ channelDataConverter -} diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index e754bfa..7bafb06 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/format/s3m/effect" "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" @@ -89,7 +90,7 @@ func (m *Manager) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(&channelDataTransaction{}) + cs.AdvanceRow(state.NewChannelDataTxn[channel.Memory](effect.Factory)) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/format/xm/effect/effectfactory.go b/format/xm/effect/effectfactory.go index bbb8c47..752ca65 100644 --- a/format/xm/effect/effectfactory.go +++ b/format/xm/effect/effectfactory.go @@ -8,9 +8,7 @@ import ( "github.com/gotracker/playback/song" ) -type EffectXM interface { - playback.Effect -} +type EffectXM = playback.Effect // VolEff is a combined effect that includes a volume effect and a standard effect type VolEff struct { diff --git a/format/xm/playback/channeldata_transaction.go b/format/xm/playback/channeldata_transaction.go deleted file mode 100755 index 499e50d..0000000 --- a/format/xm/playback/channeldata_transaction.go +++ /dev/null @@ -1,137 +0,0 @@ -package playback - -import ( - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/format/xm/effect" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/player/state" - "github.com/gotracker/playback/song" -) - -type channelDataConverter struct{} - -func (c channelDataConverter) Process(out *state.ChannelDataActions, data song.ChannelData, s song.Data, cs *state.ChannelState[channel.Memory]) error { - if data == nil { - return nil - } - - var inst *instrument.Instrument - - if data.HasNote() || data.HasInstrument() { - instID := data.GetInstrument(cs.StoredSemitone) - n := data.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = cs.GetInstrument() - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) - } - out.NoteAction.Set(note.ActionRetrigger) - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Set(nil) - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } - } - } - - if data.HasVolume() { - v := data.GetVolume() - if v == volume.VolumeUseInstVol { - if inst != nil { - v = inst.GetDefaultVolume() - } - } - out.TargetVolume.Set(v) - } - - return nil -} - -type channelDataTransaction struct { - state.ChannelDataTxnHelper[channel.Memory, channelDataConverter] -} - -func (d *channelDataTransaction) CommitPreRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { - e := effect.Factory(cs.GetMemory(), d.Data) - cs.SetActiveEffect(e) - if e != nil { - if onEff := p.GetOnEffect(); onEff != nil { - onEff(e) - } - if err := playback.EffectPreStart[channel.Memory](e, cs, p); err != nil { - return err - } - } - - return nil -} - -func (d *channelDataTransaction) CommitRow(p playback.Playback, cs *state.ChannelState[channel.Memory], semitoneSetterFactory state.SemitoneSetterFactory[channel.Memory]) error { - if pos, ok := d.TargetPos.Get(); ok { - cs.SetTargetPos(pos) - } - - if inst, ok := d.TargetInst.Get(); ok { - cs.SetTargetInst(inst) - } - - if period, ok := d.TargetPeriod.Get(); ok { - cs.SetTargetPeriod(period) - } - - if st, ok := d.TargetStoredSemitone.Get(); ok { - cs.SetStoredSemitone(st) - } - - if v, ok := d.TargetVolume.Get(); ok { - cs.SetActiveVolume(v) - } - - na, targetTick := d.NoteAction.Get() - cs.UseTargetPeriod = targetTick - cs.SetNotePlayTick(targetTick, na, 0) - - if st, ok := d.NoteCalcST.Get(); ok { - d.AddNoteOp(semitoneSetterFactory(st, cs.SetTargetPeriod)) - } - - return nil -} - -func init() { - var _ channelDataConverter -} diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index 0b0d6cb..1d0acde 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/format/xm/effect" "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" @@ -89,7 +90,7 @@ func (m *Manager) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(&channelDataTransaction{}) + cs.AdvanceRow(state.NewChannelDataTxn[channel.Memory](effect.Factory)) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index 64cde3b..41e6295 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -39,12 +39,11 @@ type ChannelDataActions struct { TargetVolume optional.Value[volume.Volume] } -type ChannelDataConverter[TMemory any] interface { - Process(out *ChannelDataActions, data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error -} +type ChannelDataConverter[TMemory any] func(out *ChannelDataActions, data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error -type ChannelDataTxnHelper[TMemory any, TChannelDataConverter ChannelDataConverter[TMemory]] struct { - Data song.ChannelData +type channelDataTxnHelper[TMemory any] struct { + Data song.ChannelData + effectFactory func(*TMemory, song.ChannelData) playback.Effect ChannelDataActions @@ -52,30 +51,82 @@ type ChannelDataTxnHelper[TMemory any, TChannelDataConverter ChannelDataConverte NoteOps []NoteOp[TMemory] } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) GetData() song.ChannelData { +func NewChannelDataTxn[TMemory any](effectFactory func(*TMemory, song.ChannelData) playback.Effect) ChannelDataTransaction[TMemory] { + return &channelDataTxnHelper[TMemory]{ + effectFactory: effectFactory, + } +} + +func (d *channelDataTxnHelper[TMemory]) GetData() song.ChannelData { return d.Data } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) SetData(cd song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) SetData(cd song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error { d.Data = cd - var converter TChannelDataConverter - return converter.Process(&d.ChannelDataActions, cd, s, cs) + if d.ProcessData != nil { + return d.ProcessData(&d.ChannelDataActions, cd, s, cs) + } + return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPreRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) CommitPreRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { + e := d.effectFactory(cs.GetMemory(), d.Data) + cs.SetActiveEffect(e) + if e != nil { + if onEff := p.GetOnEffect(); onEff != nil { + onEff(e) + } + if err := playback.EffectPreStart[TMemory](e, cs, p); err != nil { + return err + } + } + return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) CommitRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { + if pos, ok := d.TargetPos.Get(); ok { + cs.SetTargetPos(pos) + } + + if inst, ok := d.TargetInst.Get(); ok { + cs.SetTargetInst(inst) + } + + if period, ok := d.TargetPeriod.Get(); ok { + cs.SetTargetPeriod(period) + cs.SetPortaTargetPeriod(period) + } + + if st, ok := d.TargetStoredSemitone.Get(); ok { + cs.SetStoredSemitone(st) + } + + if nna, ok := d.TargetNewNoteAction.Get(); ok { + cs.SetNewNoteAction(nna) + } + + if v, ok := d.TargetVolume.Get(); ok { + cs.SetActiveVolume(v) + } + + na, targetTick := d.NoteAction.Get() + cs.UseTargetPeriod = targetTick + cs.SetNotePlayTick(targetTick, na, 0) + + if st, ok := d.NoteCalcST.Get(); ok { + d.AddNoteOp(semitoneSetterFactory(st, cs.SetTargetPeriod)) + } + return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPostRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) CommitPostRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPreTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) CommitPreTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { // pre-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -87,7 +138,7 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPreTick(p p return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) CommitTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { if err := playback.DoEffect[TMemory](cs.ActiveEffect, cs, p, currentTick, lastTick); err != nil { return err } @@ -95,7 +146,7 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitTick(p play return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPostTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) CommitPostTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { // post-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -107,11 +158,11 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) CommitPostTick(p return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) AddVolOp(op VolOp[TMemory]) { +func (d *channelDataTxnHelper[TMemory]) AddVolOp(op VolOp[TMemory]) { d.VolOps = append(d.VolOps, op) } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) ProcessVolOps(p playback.Playback, cs *ChannelState[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) ProcessVolOps(p playback.Playback, cs *ChannelState[TMemory]) error { for _, op := range d.VolOps { if op == nil { continue @@ -125,11 +176,11 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) ProcessVolOps(p p return nil } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) AddNoteOp(op NoteOp[TMemory]) { +func (d *channelDataTxnHelper[TMemory]) AddNoteOp(op NoteOp[TMemory]) { d.NoteOps = append(d.NoteOps, op) } -func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TMemory]) error { +func (d *channelDataTxnHelper[TMemory]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TMemory]) error { for _, op := range d.NoteOps { if op == nil { continue @@ -142,3 +193,78 @@ func (d *ChannelDataTxnHelper[TMemory, TChannelDataConverter]) ProcessNoteOps(p return nil } + +func (d *channelDataTxnHelper[TMemory]) ProcessData(out *ChannelDataActions, data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error { + if data == nil { + return nil + } + + var n note.Note = note.EmptyNote{} + inst := cs.GetInstrument() + prevInst := inst + + if data.HasNote() || data.HasInstrument() { + instID := data.GetInstrument(cs.StoredSemitone) + n = data.GetNote() + var ( + wantRetrigger bool + wantRetriggerVol bool + ) + if instID.IsEmpty() { + // use current + inst = prevInst + wantRetrigger = true + } else if !s.IsValidInstrumentID(instID) { + out.TargetInst.Set(nil) + n = note.InvalidNote{} + } else { + var str note.Semitone + inst, str = s.GetInstrument(instID) + n = note.CoalesceNoteSemitone(n, str) + if !note.IsEmpty(n) && inst == nil { + inst = prevInst + } + wantRetrigger = true + wantRetriggerVol = true + } + + if wantRetrigger { + out.TargetInst.Set(inst) + out.TargetPos.Set(sampling.Pos{}) + if inst != nil { + if wantRetriggerVol { + out.TargetVolume.Set(inst.GetDefaultVolume()) + } + out.NoteAction.Set(note.ActionRetrigger) + out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) + } + } + } + + if note.IsInvalid(n) { + out.TargetPeriod.Set(nil) + out.NoteAction.Set(note.ActionCut) + } else if note.IsRelease(n) { + out.NoteAction.Set(note.ActionRelease) + } else if !note.IsEmpty(n) { + if nn, ok := n.(note.Normal); ok { + st := note.Semitone(nn) + out.TargetStoredSemitone.Set(st) + out.NoteCalcST.Set(st) + } else { + out.NoteAction.Set(note.ActionCut) + } + } + + if data.HasVolume() { + v := data.GetVolume() + if v == volume.VolumeUseInstVol { + if inst != nil { + v = inst.GetDefaultVolume() + } + } + out.TargetVolume.Set(v) + } + + return nil +} From 0ed614cd9a193d7c2dd8611bab75bfec065b1190 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 18 Dec 2023 18:15:41 -0800 Subject: [PATCH 05/63] Fix period/frequency calculations --- format/it/period/amiga.go | 12 ++++++------ format/it/period/linear.go | 19 ++++++++++--------- format/it/period/util.go | 18 ++++++++++-------- format/s3m/period/amiga.go | 15 ++++++++------- format/s3m/period/util.go | 3 +-- format/xm/period/amiga.go | 17 +++++++++-------- format/xm/period/linear.go | 20 +++++++++----------- format/xm/period/util.go | 17 ++++++++++------- period/protracker.go | 8 ++++---- 9 files changed, 67 insertions(+), 62 deletions(-) diff --git a/format/it/period/amiga.go b/format/it/period/amiga.go index 3460f92..877cd77 100644 --- a/format/it/period/amiga.go +++ b/format/it/period/amiga.go @@ -12,7 +12,7 @@ import ( // Amiga defines a sampler period that follows the Amiga-style approach of note // definition. Useful in calculating resampling. -type Amiga period.AmigaPeriod +type Amiga period.Amiga // AddInteger truncates the current period to an integer and adds the delta integer in // then returns the resulting period @@ -54,22 +54,22 @@ func (p Amiga) Lerp(t float64, rhs period.Period) period.Period { right = r } - period := Amiga(period.AmigaPeriod(p).Lerp(t, period.AmigaPeriod(right))) + period := Amiga(period.Amiga(p).Lerp(t, period.Amiga(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))) + return float64(period.Amiga(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)) + return period.Amiga(p).GetFrequency(period.Frequency(ITBaseClock)) } func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%f }", float32(p)) + return fmt.Sprintf("Amiga{ Period:%d }", p) } // ToAmigaPeriod calculates an amiga period for a linear finetune period @@ -77,7 +77,7 @@ func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { if finetunes < 0 { finetunes = 0 } - pow := math.Pow(2, 1.0+float64(finetunes)/semitonesPerOctave) + pow := math.Pow(2, 1.0+float64(finetunes)/SlideFinesPerOctave) linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) period := Amiga(float64(semitonePeriodTable[0]) / linFreq) diff --git a/format/it/period/linear.go b/format/it/period/linear.go index a2a0380..344be3e 100644 --- a/format/it/period/linear.go +++ b/format/it/period/linear.go @@ -30,9 +30,10 @@ func (p Linear) AddDelta(delta period.Delta) period.Period { } // 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 +// +// -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() @@ -55,19 +56,19 @@ func (p Linear) Lerp(t float64, rhs period.Period) period.Period { rnft := float64(right.Finetune) delta := period.PeriodDelta(t * (rnft - lnft)) - p.AddDelta(delta) - return p + 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 { - return ToAmigaPeriod(p.Finetune, p.C2Spd).GetSamplerAdd(samplerSpeed) + return float64(p.GetFrequency()) * samplerSpeed / float64(ITBaseClock) } // GetFrequency returns the frequency defined by the period func (p Linear) GetFrequency() period.Frequency { - am := ToAmigaPeriod(p.Finetune, p.C2Spd) - return am.GetFrequency() + pft := float64(p.Finetune-C5SlideFines) / float64(SlideFinesPerOctave) + f := p.C2Spd * period.Frequency(math.Pow(2.0, pft)) + return f } func (p Linear) String() string { @@ -82,7 +83,7 @@ func ToLinearPeriod(p period.Period) Linear { case Amiga: linFreq := float64(semitonePeriodTable[0]) / float64(pp) - fts := note.Finetune(semitonesPerOctave * math.Log2(linFreq)) + fts := note.Finetune(SlideFinesPerOctave * math.Log2(linFreq)) lp := Linear{ Finetune: fts, diff --git a/format/it/period/util.go b/format/it/period/util.go index 6e097ca..b059edf 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -16,9 +16,12 @@ const ( // ITBaseClock is the base clock speed of IT files ITBaseClock period.Frequency = DefaultC2Spd * C5Period - notesPerOctave = 12 - semitonesPerNote = 64 - semitonesPerOctave = notesPerOctave * semitonesPerNote + NotesPerOctave = 12 + SlideFinesPerSemitone = 4 + SemitonesPerNote = 16 + SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote + SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave + C5SlideFines = 5 * SlideFinesPerOctave ) var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} @@ -29,7 +32,7 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ panic("how?") } if linearFreqSlides { - nft := int(semi)*semitonesPerNote + int(ft) + nft := int(semi)*SlideFinesPerNote + 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. @@ -53,8 +56,7 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) } - p := (Amiga(floatDefaultC2Spd*semitonePeriodTable[key]) / Amiga(uint32(c2spd)< Date: Tue, 19 Dec 2023 09:52:35 -0800 Subject: [PATCH 06/63] update go.mod --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 0dafad9..50d3ff2 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/gotracker/playback go 1.21 require ( - github.com/gotracker/goaudiofile v1.0.14 + github.com/gotracker/goaudiofile v1.0.15 github.com/gotracker/gomixing v1.3.0 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-20231214170342-aacd6d4b4611 + golang.org/x/exp v0.0.0-20231219160207-73b9e39aefca ) require github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 542d993..4414422 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -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/goaudiofile v1.0.15 h1:90MtPQB1i99DSTradMol49VZlV4smJ5iLYGrdecfIps= +github.com/gotracker/goaudiofile v1.0.15/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/opl2 v1.0.1 h1:1PVNs0dXqEAQxdws7fz2WEE3nSKkMb1osTTT7KgEi5g= @@ -10,7 +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-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= -golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20231219160207-73b9e39aefca h1:+xQfFu/HO/82Wwg4zuJ5xiLp0yaOLJjBGnuafXp85YQ= +golang.org/x/exp v0.0.0-20231219160207-73b9e39aefca/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 2645d2cd479a48c12fdf9ad8e40a95cceed98d2c Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 19 Dec 2023 11:51:06 -0800 Subject: [PATCH 07/63] reduce generics usages - simplify period calculations - reuse period patterns - require derivations of period types --- format.go | 2 +- format/common/genericpattern.go | 20 ------ format/common/genericrow.go | 16 ----- format/format.go | 7 +- format/it/layout/song.go | 2 +- format/it/pattern/pattern.go | 4 +- format/it/pattern/row.go | 4 +- format/it/pattern/state.go | 7 +- format/it/period/amiga.go | 63 +++++++----------- format/it/period/linear.go | 70 +++++--------------- format/it/period/util.go | 15 +++-- format/it/playback/playback.go | 2 +- format/it/playback/playback_pattern.go | 4 +- format/s3m/layout/song.go | 2 +- format/s3m/pattern/pattern.go | 4 +- format/s3m/pattern/row.go | 4 +- format/s3m/pattern/state.go | 7 +- format/s3m/period/amiga.go | 72 +++++++------------- format/s3m/period/util.go | 7 +- format/s3m/playback/playback.go | 2 +- format/s3m/playback/playback_pattern.go | 4 +- format/xm/layout/song.go | 2 +- format/xm/pattern/pattern.go | 4 +- format/xm/pattern/row.go | 4 +- format/xm/pattern/state.go | 7 +- format/xm/period/amiga.go | 61 +++++++---------- format/xm/period/linear.go | 78 ++++++---------------- format/xm/period/util.go | 13 ++-- format/xm/playback/playback.go | 2 +- format/xm/playback/playback_pattern.go | 4 +- period/amiga.go | 79 ++++++++++++++++++++++ period/linear.go | 88 +++++++++++++++++++++++++ period/period.go | 2 + period/protracker.go | 18 ----- song/pattern.go | 23 +++---- song/row.go | 13 ++-- song/song.go | 4 +- 37 files changed, 351 insertions(+), 369 deletions(-) delete mode 100644 format/common/genericpattern.go delete mode 100644 format/common/genericrow.go create mode 100644 period/amiga.go create mode 100644 period/linear.go delete mode 100644 period/protracker.go diff --git a/format.go b/format.go index 9dcede3..2708d56 100644 --- a/format.go +++ b/format.go @@ -7,7 +7,7 @@ import ( ) // Format is an interface to a music file format loader -type Format[TChannelData any] interface { +type Format interface { Load(filename string, features []feature.Feature) (Playback, error) LoadFromReader(r io.Reader, features []feature.Feature) (Playback, error) } diff --git a/format/common/genericpattern.go b/format/common/genericpattern.go deleted file mode 100644 index 0fdae19..0000000 --- a/format/common/genericpattern.go +++ /dev/null @@ -1,20 +0,0 @@ -package common - -import ( - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/song" -) - -type Pattern[TChannelData song.ChannelData] []Row[TChannelData] - -func (p Pattern[TChannelData]) GetRow(row index.Row) song.Row { - return p[row] -} - -func (p Pattern[TChannelData]) NumRows() int { - return len(p) -} - -func (p Pattern[TChannelData]) GetRows() song.Rows { - return p -} diff --git a/format/common/genericrow.go b/format/common/genericrow.go deleted file mode 100644 index 446b80c..0000000 --- a/format/common/genericrow.go +++ /dev/null @@ -1,16 +0,0 @@ -package common - -import ( - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/song" -) - -type Row[TChannelData song.ChannelData] []TChannelData - -func (r Row[TChannelData]) GetNumChannels() int { - return len(r) -} - -func (r Row[TChannelData]) GetChannel(ch index.Channel) song.ChannelData { - return r[ch] -} diff --git a/format/format.go b/format/format.go index a3ab28e..c1d08d9 100644 --- a/format/format.go +++ b/format/format.go @@ -11,15 +11,14 @@ import ( "github.com/gotracker/playback/format/s3m" "github.com/gotracker/playback/format/xm" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" ) var ( - supportedFormats = make(map[string]playback.Format[song.ChannelData]) + supportedFormats = make(map[string]playback.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) (playback.Playback, playback.Format, error) { for _, f := range supportedFormats { if pb, err := f.Load(filename, features); err == nil { return pb, f, nil @@ -31,7 +30,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) (playback.Playback, playback.Format, error) { pos, _ := r.Seek(0, io.SeekCurrent) if format != "" { f, ok := supportedFormats[format] diff --git a/format/it/layout/song.go b/format/it/layout/song.go index 8d43228..8e207a4 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -27,7 +27,7 @@ func (s Song) GetOrderList() []index.Pattern { } // GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern { +func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { if int(patNum) >= len(s.Patterns) { return nil } diff --git a/format/it/pattern/pattern.go b/format/it/pattern/pattern.go index 7e0f9a1..96c4958 100644 --- a/format/it/pattern/pattern.go +++ b/format/it/pattern/pattern.go @@ -1,8 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/song" ) -type Pattern = common.Pattern[channel.Data] +type Pattern = song.Pattern[channel.Data] diff --git a/format/it/pattern/row.go b/format/it/pattern/row.go index ccf10bb..8af113e 100644 --- a/format/it/pattern/row.go +++ b/format/it/pattern/row.go @@ -1,8 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/song" ) -type Row = common.Row[channel.Data] +type Row = song.Row[channel.Data] diff --git a/format/it/pattern/state.go b/format/it/pattern/state.go index 75eaa01..1d3f4d0 100644 --- a/format/it/pattern/state.go +++ b/format/it/pattern/state.go @@ -3,6 +3,7 @@ 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" @@ -233,7 +234,7 @@ func (state *State) nextRow() error { } // GetRows returns all the rows in the pattern -func (state *State) GetRows() (song.Rows, error) { +func (state *State) GetRows() (*song.Pattern[channel.Data], error) { nextRow: for loops := 0; loops < len(state.Patterns); loops++ { var patNum = state.GetPatNum() @@ -249,8 +250,8 @@ nextRow: if int(patNum) >= len(state.Patterns) { return nil, nil } - pattern := state.Patterns[patNum] - return pattern.GetRows(), nil + pat := &state.Patterns[patNum] + return pat, nil } } return nil, nil diff --git a/format/it/period/amiga.go b/format/it/period/amiga.go index 877cd77..ee50672 100644 --- a/format/it/period/amiga.go +++ b/format/it/period/amiga.go @@ -1,7 +1,6 @@ package period import ( - "fmt" "math" "github.com/gotracker/playback/note" @@ -12,19 +11,16 @@ import ( // Amiga defines a sampler period that follows the Amiga-style approach of note // definition. Useful in calculating resampling. -type Amiga period.Amiga - -// 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 +type Amiga struct { + period.Amiga } +var _ period.Period = (*Amiga)(nil) + // 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) + p.Amiga += period.Amiga(d) return p } @@ -34,42 +30,28 @@ func (p Amiga) AddDelta(delta period.Delta) period.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 + if q, ok := rhs.ToAmigaPeriod().(Amiga); ok { + return period.CompareAmiga(p.Amiga, q.Amiga) } + return comparison.SpaceshipLeftGreater } -// 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 +// GetFrequency returns the frequency defined by the period +func (p Amiga) GetFrequency() period.Frequency { + if p.Amiga == 0 { + return 0 } - - period := Amiga(period.Amiga(p).Lerp(t, period.Amiga(right))) - return period + return period.Frequency(ITBaseClock) / period.Frequency(p.Amiga) } -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { - return float64(period.Amiga(p).GetFrequency(period.Frequency(samplerSpeed))) +// ToLinearPeriod returns the linear frequency period for a given period +func (p Amiga) ToLinearPeriod() period.Period { + return nil } -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - return period.Amiga(p).GetFrequency(period.Frequency(ITBaseClock)) -} - -func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%d }", p) +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Amiga) ToAmigaPeriod() period.Period { + return p } // ToAmigaPeriod calculates an amiga period for a linear finetune period @@ -77,9 +59,10 @@ func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { if finetunes < 0 { finetunes = 0 } - pow := math.Pow(2, 1.0+float64(finetunes)/SlideFinesPerOctave) + pow := math.Pow(2, float64(finetunes)/SlideFinesPerOctave) linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - period := Amiga(float64(semitonePeriodTable[0]) / linFreq) - return period + return Amiga{ + Amiga: period.Amiga(float64(semitonePeriodTable[0]) / linFreq), + } } diff --git a/format/it/period/linear.go b/format/it/period/linear.go index 344be3e..a8aa9e9 100644 --- a/format/it/period/linear.go +++ b/format/it/period/linear.go @@ -1,31 +1,22 @@ package period import ( - "fmt" "math" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - "github.com/gotracker/playback/period" + "github.com/heucuva/comparison" ) // Linear is a linear period, based on semitone and finetune values type Linear struct { - Finetune note.Finetune - C2Spd period.Frequency + period.Linear } +var _ period.Period = (*Linear)(nil) + // 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 - } - } + p.Linear = period.AddLinearDelta(p.Linear, delta) return p } @@ -35,28 +26,10 @@ func (p Linear) AddDelta(delta period.Delta) period.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 + if q, ok := rhs.ToLinearPeriod().(Linear); ok { + return period.CompareLinear(p.Linear, q.Linear) } -} - -// 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) + return comparison.SpaceshipLeftGreater } // GetSamplerAdd returns the number of samples to advance an instrument by given the period @@ -67,29 +40,16 @@ func (p Linear) GetSamplerAdd(samplerSpeed float64) float64 { // GetFrequency returns the frequency defined by the period func (p Linear) GetFrequency() period.Frequency { pft := float64(p.Finetune-C5SlideFines) / float64(SlideFinesPerOctave) - f := p.C2Spd * period.Frequency(math.Pow(2.0, pft)) + f := p.CommonRate * period.Frequency(math.Pow(2.0, pft)) return f } -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(SlideFinesPerOctave * math.Log2(linFreq)) +func (p Linear) ToLinearPeriod() period.Period { + return p +} - lp := Linear{ - Finetune: fts, - C2Spd: DefaultC2Spd, - } - return lp - } - return Linear{} +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Linear) ToAmigaPeriod() period.Period { + return nil } diff --git a/format/it/period/util.go b/format/it/period/util.go index b059edf..9003332 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -34,10 +34,10 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ if linearFreqSlides { nft := int(semi)*SlideFinesPerNote + 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, + Linear: period.Linear{ + Finetune: note.Finetune(nft), + CommonRate: c2spd, + }, } } @@ -56,8 +56,9 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) } - p := Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)<= len(s.Patterns) { return nil } diff --git a/format/s3m/pattern/pattern.go b/format/s3m/pattern/pattern.go index 9d6c8d8..7d25c82 100644 --- a/format/s3m/pattern/pattern.go +++ b/format/s3m/pattern/pattern.go @@ -1,8 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/song" ) -type Pattern = common.Pattern[channel.Data] +type Pattern = song.Pattern[channel.Data] diff --git a/format/s3m/pattern/row.go b/format/s3m/pattern/row.go index 329f789..c64c038 100644 --- a/format/s3m/pattern/row.go +++ b/format/s3m/pattern/row.go @@ -1,8 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/channel" + "github.com/gotracker/playback/song" ) -type Row = common.Row[channel.Data] +type Row = song.Row[channel.Data] diff --git a/format/s3m/pattern/state.go b/format/s3m/pattern/state.go index ff7de04..8f4cb8f 100644 --- a/format/s3m/pattern/state.go +++ b/format/s3m/pattern/state.go @@ -3,6 +3,7 @@ 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" @@ -234,7 +235,7 @@ func (state *State) nextRow() error { } // GetRows returns all the rows in the pattern -func (state *State) GetRows() (song.Rows, error) { +func (state *State) GetRows() (*song.Pattern[channel.Data], error) { nextRow: for loops := 0; loops < len(state.Patterns); loops++ { var patNum = state.GetPatNum() @@ -250,8 +251,8 @@ nextRow: if int(patNum) >= len(state.Patterns) { return nil, nil } - pattern := state.Patterns[patNum] - return pattern.GetRows(), nil + pat := &state.Patterns[patNum] + return pat, nil } } return nil, nil diff --git a/format/s3m/period/amiga.go b/format/s3m/period/amiga.go index e485747..9367f69 100644 --- a/format/s3m/period/amiga.go +++ b/format/s3m/period/amiga.go @@ -1,7 +1,6 @@ package period import ( - "fmt" "math" "github.com/gotracker/playback/note" @@ -12,29 +11,17 @@ import ( // Amiga defines a sampler period that follows the Amiga-style approach of note // definition. Useful in calculating resampling. -type Amiga period.Amiga - -// 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 +type Amiga struct { + period.Amiga } +var _ period.Period = (*Amiga)(nil) + // 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 + p.Amiga += period.Amiga(d) + return p } // Compare returns: @@ -43,42 +30,28 @@ func (p Amiga) AddDelta(delta period.Delta) period.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 + if q, ok := rhs.ToAmigaPeriod().(Amiga); ok { + return period.CompareAmiga(p.Amiga, q.Amiga) } + return comparison.SpaceshipLeftGreater } -// 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 +// GetFrequency returns the frequency defined by the period +func (p Amiga) GetFrequency() period.Frequency { + if p.Amiga == 0 { + return 0 } - - ret := Amiga(period.Amiga(p).Lerp(t, period.Amiga(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.Amiga(p).GetFrequency(period.Frequency(samplerSpeed))) + return period.Frequency(S3MBaseClock) / period.Frequency(p.Amiga) } -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - return period.Amiga(p).GetFrequency(period.Frequency(S3MBaseClock)) +// ToLinearPeriod returns the linear frequency period for a given period +func (p Amiga) ToLinearPeriod() period.Period { + return nil } -func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%f }", float32(p)) +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Amiga) ToAmigaPeriod() period.Period { + return p } // ToAmigaPeriod calculates an amiga period for a linear finetune period @@ -89,6 +62,7 @@ func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { pow := math.Pow(2, float64(finetunes)/semitonesPerOctave) linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - period := Amiga(float64(semitonePeriodTable[0]) / linFreq) - return period + return Amiga{ + Amiga: period.Amiga(float64(semitonePeriodTable[0]) / linFreq), + } } diff --git a/format/s3m/period/util.go b/format/s3m/period/util.go index d949f55..063e823 100644 --- a/format/s3m/period/util.go +++ b/format/s3m/period/util.go @@ -44,8 +44,9 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ c2spd = CalcFinetuneC2Spd(c2spd, ft) } - p := Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)<= len(s.Patterns) { return nil } diff --git a/format/xm/pattern/pattern.go b/format/xm/pattern/pattern.go index 6daa5a1..4aa50db 100644 --- a/format/xm/pattern/pattern.go +++ b/format/xm/pattern/pattern.go @@ -1,8 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/song" ) -type Pattern = common.Pattern[channel.Data] +type Pattern = song.Pattern[channel.Data] diff --git a/format/xm/pattern/row.go b/format/xm/pattern/row.go index 1fb55a4..23396d4 100644 --- a/format/xm/pattern/row.go +++ b/format/xm/pattern/row.go @@ -1,8 +1,8 @@ package pattern import ( - "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/song" ) -type Row = common.Row[channel.Data] +type Row = song.Row[channel.Data] diff --git a/format/xm/pattern/state.go b/format/xm/pattern/state.go index 75eaa01..e45e4da 100644 --- a/format/xm/pattern/state.go +++ b/format/xm/pattern/state.go @@ -3,6 +3,7 @@ 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" @@ -233,7 +234,7 @@ func (state *State) nextRow() error { } // GetRows returns all the rows in the pattern -func (state *State) GetRows() (song.Rows, error) { +func (state *State) GetRows() (*song.Pattern[channel.Data], error) { nextRow: for loops := 0; loops < len(state.Patterns); loops++ { var patNum = state.GetPatNum() @@ -249,8 +250,8 @@ nextRow: if int(patNum) >= len(state.Patterns) { return nil, nil } - pattern := state.Patterns[patNum] - return pattern.GetRows(), nil + pat := &state.Patterns[patNum] + return pat, nil } } return nil, nil diff --git a/format/xm/period/amiga.go b/format/xm/period/amiga.go index d0679bf..7d0ca21 100644 --- a/format/xm/period/amiga.go +++ b/format/xm/period/amiga.go @@ -1,7 +1,6 @@ package period import ( - "fmt" "math" "github.com/gotracker/playback/note" @@ -12,19 +11,16 @@ import ( // Amiga defines a sampler period that follows the Amiga-style approach of note // definition. Useful in calculating resampling. -type Amiga period.Amiga - -// 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 +type Amiga struct { + period.Amiga } +var _ period.Period = (*Amiga)(nil) + // 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) + p.Amiga += period.Amiga(d) return p } @@ -34,42 +30,28 @@ func (p Amiga) AddDelta(delta period.Delta) period.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 + if q, ok := rhs.ToAmigaPeriod().(Amiga); ok { + return period.CompareAmiga(p.Amiga, q.Amiga) } + return comparison.SpaceshipLeftGreater } -// 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 +// GetFrequency returns the frequency defined by the period +func (p Amiga) GetFrequency() period.Frequency { + if p.Amiga == 0 { + return 0 } - - p = Amiga(period.Amiga(p).Lerp(t, period.Amiga(right))) - return p + return period.Frequency(XMBaseClock) / period.Frequency(p.Amiga) } -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { - return float64(period.Amiga(p).GetFrequency(period.Frequency(samplerSpeed))) +// ToLinearPeriod returns the linear frequency period for a given period +func (p Amiga) ToLinearPeriod() period.Period { + return nil } -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - return period.Amiga(p).GetFrequency(XMBaseClock) -} - -func (p Amiga) String() string { - return fmt.Sprintf("Amiga{ Period:%f }", float32(p)) +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Amiga) ToAmigaPeriod() period.Period { + return p } // ToAmigaPeriod calculates an amiga period for a linear finetune period @@ -80,6 +62,7 @@ func ToAmigaPeriod(finetunes note.Finetune, c2spd period.Frequency) Amiga { pow := math.Pow(2, float64(finetunes)/SlideFinesPerOctave) linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - period := Amiga(float64(semitonePeriodTable[0]) / linFreq) - return period + return Amiga{ + Amiga: period.Amiga(float64(semitonePeriodTable[0]) / linFreq), + } } diff --git a/format/xm/period/linear.go b/format/xm/period/linear.go index a6ecd96..86709a6 100644 --- a/format/xm/period/linear.go +++ b/format/xm/period/linear.go @@ -1,31 +1,22 @@ package period import ( - "fmt" "math" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" - "github.com/gotracker/playback/period" + "github.com/heucuva/comparison" ) // Linear is a linear period, based on semitone and finetune values type Linear struct { - Finetune note.Finetune - C2Spd period.Frequency + period.Linear } +var _ period.Period = (*Linear)(nil) + // 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 - } - } + p.Linear = period.AddLinearDelta(p.Linear, delta) return p } @@ -35,28 +26,10 @@ func (p Linear) AddDelta(delta period.Delta) period.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 + if q, ok := rhs.ToLinearPeriod().(Linear); ok { + return period.CompareLinear(p.Linear, q.Linear) } -} - -// 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) + return comparison.SpaceshipLeftGreater } // GetSamplerAdd returns the number of samples to advance an instrument by given the period @@ -64,32 +37,19 @@ func (p Linear) GetSamplerAdd(samplerSpeed float64) float64 { return float64(p.GetFrequency()) * samplerSpeed / float64(XMBaseClock) } -// GetFrequency returns the frequency defined by the period -func (p Linear) GetFrequency() period.Frequency { - pft := float64(p.Finetune-C4SlideFines) / float64(SlideFinesPerOctave) - f := p.C2Spd * period.Frequency(math.Pow(2.0, pft)) - return f +// ToLinearPeriod returns the linear frequency period for a given period +func (p Linear) ToLinearPeriod() period.Period { + return p } -func (p Linear) String() string { - return fmt.Sprintf("Linear{ Finetune:%v C2Spd:%v }", p.Finetune, p.C2Spd) +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Linear) ToAmigaPeriod() period.Period { + return nil } -// 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(SlideFinesPerOctave * math.Log2(linFreq)) - - lp := Linear{ - Finetune: fts, - C2Spd: DefaultC2Spd, - } - return lp - } - return Linear{} +// GetFrequency returns the frequency defined by the period +func (p Linear) GetFrequency() period.Frequency { + pft := float64(p.Finetune-C4SlideFines) / float64(SlideFinesPerOctave) + f := p.CommonRate * period.Frequency(math.Pow(2.0, pft)) + return f } diff --git a/format/xm/period/util.go b/format/xm/period/util.go index be25b9d..79a41bb 100644 --- a/format/xm/period/util.go +++ b/format/xm/period/util.go @@ -34,8 +34,10 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ if linearFreqSlides { nft := int(semi)*64 + int(ft) return Linear{ - Finetune: note.Finetune(nft), - C2Spd: c2spd, + Linear: period.Linear{ + Finetune: note.Finetune(nft), + CommonRate: c2spd, + }, } } @@ -54,8 +56,9 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) } - period := Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)< 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 Period) Period { + right := Amiga(0) + if r, ok := rhs.(Amiga); ok { + right = r + } + + return Amiga(util.LerpFloat64(t, float64(p), float64(right))) +} + +// GetSamplerAdd returns the number of samples to advance an instrument by given the period +func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { + if p == 0 { + return 0 + } + return float64(samplerSpeed) / float64(p) +} + +// GetFrequency returns the frequency defined by the period +func (p Amiga) GetFrequency() Frequency { + panic("unimplemented") // must be implemented by format +} + +func (p Amiga) String() string { + return fmt.Sprintf("Amiga{ Period:%d }", p) +} + +// ToLinearPeriod returns the linear frequency period for a given period +func (p Amiga) ToLinearPeriod() Period { + panic("unimplemented") // must be implemented by format +} + +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Amiga) ToAmigaPeriod() Period { + panic("unimplemented") // must be implemented by format +} diff --git a/period/linear.go b/period/linear.go new file mode 100644 index 0000000..e3daefb --- /dev/null +++ b/period/linear.go @@ -0,0 +1,88 @@ +package period + +import ( + "fmt" + + "github.com/gotracker/playback/note" + "github.com/heucuva/comparison" +) + +// Linear is a linear period, based on semitone and finetune values +type Linear struct { + Finetune note.Finetune + CommonRate Frequency +} + +// Add adds the current period to a delta value then returns the resulting period +func (p Linear) AddDelta(delta Delta) Period { + panic("unimplemented") // must be implemented by format +} + +// Add adds the current period to a delta value then returns the resulting period +func AddLinearDelta(p Linear, delta Delta) Linear { + // 0 means "not playing", so keep it that way + if p.Finetune > 0 { + d := 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) comparison.Spaceship { + panic("unimplemented") // must be implemented by format +} + +// CompareLinear compares (<=>) two linear periods +func CompareLinear(lhs, rhs Linear) comparison.Spaceship { + switch { + case lhs.Finetune < rhs.Finetune: + return comparison.SpaceshipRightGreater + case lhs.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 Period) Period { + right, _ := rhs.ToLinearPeriod().(Linear) + + lnft := float64(p.Finetune) + rnft := float64(right.Finetune) + + delta := 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 { + panic("unimplemented") // must be implemented by format +} + +// GetFrequency returns the frequency defined by the period +func (p Linear) GetFrequency() Frequency { + panic("unimplemented") // must be implemented by format +} + +func (p Linear) String() string { + return fmt.Sprintf("LinearPeriod{ Finetune:%v CommonRate:%v }", p.Finetune, p.CommonRate) +} + +// ToLinearPeriod returns the linear frequency period for a given period +func (p Linear) ToLinearPeriod() Period { + panic("unimplemented") // must be implemented by format +} + +// ToAmigaPeriod returns the amiga (protracker) representation for a given period +func (p Linear) ToAmigaPeriod() Period { + panic("unimplemented") // must be implemented by format +} diff --git a/period/period.go b/period/period.go index 58239f4..7db6a9b 100644 --- a/period/period.go +++ b/period/period.go @@ -11,6 +11,8 @@ type Period interface { Lerp(float64, Period) Period GetSamplerAdd(float64) float64 GetFrequency() Frequency + ToLinearPeriod() Period + ToAmigaPeriod() Period } // PeriodDelta is an amount of delta specific to the period type it modifies diff --git a/period/protracker.go b/period/protracker.go deleted file mode 100644 index 159828f..0000000 --- a/period/protracker.go +++ /dev/null @@ -1,18 +0,0 @@ -package period - -import ( - "github.com/gotracker/playback/util" -) - -type Amiga uint16 - -func (p Amiga) Lerp(t float64, rhs Amiga) Amiga { - return Amiga(util.LerpFloat64(t, float64(p), float64(rhs))) -} - -func (p Amiga) GetFrequency(baseClockRate Frequency) Frequency { - if p == 0 { - return 0 - } - return baseClockRate / Frequency(p) -} diff --git a/song/pattern.go b/song/pattern.go index 66c3869..16e108b 100644 --- a/song/pattern.go +++ b/song/pattern.go @@ -11,17 +11,18 @@ var ( ErrStopSong = errors.New("stop song") ) -// Pattern is an interface for pattern data -type Pattern interface { - GetRow(index.Row) Row - GetRows() Rows -} +// Pattern is structure containing the pattern data +type Pattern[TChannelData ChannelData] []Row[TChannelData] -// Patterns is an array of pattern interfaces -type Patterns []Pattern +// GetRow returns a single row of channel data +func (p Pattern[TChannelData]) GetRow(row index.Row) Row[TChannelData] { + return p[row] +} -// Rows is an interface to obtain row data -type Rows interface { - GetRow(index.Row) Row - NumRows() int +// NumRows returns the number of rows contained within the pattern +func (p Pattern[TChannelData]) NumRows() int { + return len(p) } + +// Patterns is an array of pattern interfaces +type Patterns[TChannelData ChannelData] []Pattern[TChannelData] diff --git a/song/row.go b/song/row.go index 04ed9e8..b6bc192 100644 --- a/song/row.go +++ b/song/row.go @@ -2,8 +2,13 @@ package song import "github.com/gotracker/playback/index" -// Row is an interface to a row -type Row interface { - GetNumChannels() int - GetChannel(index.Channel) ChannelData +// Row is a structure containing a single row +type Row[TChannelData ChannelData] []TChannelData + +func (r Row[TChannelData]) GetNumChannels() int { + return len(r) +} + +func (r Row[TChannelData]) GetChannel(ch index.Channel) TChannelData { + return r[ch] } diff --git a/song/song.go b/song/song.go index de372e8..ec6ff2d 100644 --- a/song/song.go +++ b/song/song.go @@ -17,6 +17,6 @@ type Data interface { GetName() string } -type PatternData interface { - GetPattern(index.Pattern) Pattern +type PatternData[TChannelData ChannelData] interface { + GetPattern(index.Pattern) Pattern[TChannelData] } From 524b8ea3f86f069791d9638a0b952764cb1f995d Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 19 Dec 2023 22:16:46 -0800 Subject: [PATCH 08/63] reduce complexity of period calculations - less direct playback structs - simpler playback management --- channel.go | 16 +- effect.go | 52 +++--- format/common/loadformat.go | 5 +- format/it/effect/effect_arpeggio.go | 9 +- format/it/effect/effect_channelvolumeslide.go | 9 +- format/it/effect/effect_extrafineportadown.go | 9 +- format/it/effect/effect_extrafineportaup.go | 9 +- format/it/effect/effect_finepatterndelay.go | 7 +- format/it/effect/effect_fineportadown.go | 9 +- format/it/effect/effect_fineportaup.go | 9 +- format/it/effect/effect_finevibrato.go | 9 +- format/it/effect/effect_finevolslidedown.go | 15 +- format/it/effect/effect_finevolslideup.go | 15 +- format/it/effect/effect_globalvolumeslide.go | 9 +- format/it/effect/effect_highoffset.go | 7 +- .../effect_newnoteactionnotecontinue.go | 7 +- .../it/effect/effect_newnoteactionnotecut.go | 7 +- .../it/effect/effect_newnoteactionnotefade.go | 7 +- .../it/effect/effect_newnoteactionnoteoff.go | 7 +- format/it/effect/effect_notecut.go | 9 +- format/it/effect/effect_notedelay.go | 9 +- format/it/effect/effect_orderjump.go | 9 +- format/it/effect/effect_panningenvelopeoff.go | 7 +- format/it/effect/effect_panningenvelopeon.go | 7 +- format/it/effect/effect_pastnotecut.go | 7 +- format/it/effect/effect_pastnotefadeout.go | 7 +- format/it/effect/effect_pastnoteoff.go | 7 +- format/it/effect/effect_patterndelay.go | 9 +- format/it/effect/effect_patternloop.go | 9 +- format/it/effect/effect_pitchenvelopeoff.go | 7 +- format/it/effect/effect_pitchenvelopeon.go | 7 +- format/it/effect/effect_portadown.go | 11 +- format/it/effect/effect_portatonote.go | 22 ++- format/it/effect/effect_portaup.go | 11 +- format/it/effect/effect_portavolslide.go | 15 +- format/it/effect/effect_retrigvolslide.go | 9 +- format/it/effect/effect_rowjump.go | 9 +- format/it/effect/effect_sampleoffset.go | 7 +- format/it/effect/effect_setchannelvolume.go | 7 +- .../it/effect/effect_setcoarsepanposition.go | 7 +- format/it/effect/effect_setfinetune.go | 9 +- format/it/effect/effect_setglobalvolume.go | 9 +- .../it/effect/effect_setpanbrellowaveform.go | 7 +- format/it/effect/effect_setpanposition.go | 7 +- format/it/effect/effect_setspeed.go | 9 +- format/it/effect/effect_settempo.go | 11 +- format/it/effect/effect_settremolowaveform.go | 7 +- format/it/effect/effect_setvibratowaveform.go | 7 +- format/it/effect/effect_tremolo.go | 9 +- format/it/effect/effect_tremor.go | 9 +- format/it/effect/effect_vibrato.go | 9 +- format/it/effect/effect_vibratovolslide.go | 15 +- format/it/effect/effect_volslidedown.go | 15 +- format/it/effect/effect_volslideup.go | 15 +- format/it/effect/effect_volumeenvelopeoff.go | 7 +- format/it/effect/effect_volumeenvelopeon.go | 7 +- format/it/effect/effectfactory.go | 149 ++++++++-------- format/it/effect/effectfactory_volume.go | 55 +++--- format/it/effect/unhandled.go | 13 +- format/it/effect/util.go | 57 +++--- format/it/period/amiga.go | 9 +- format/it/period/linear.go | 9 +- format/it/period/util.go | 56 +++--- format/it/playback/playback.go | 116 ++++++++----- format/it/playback/playback_command.go | 23 ++- format/it/playback/playback_pattern.go | 6 +- format/it/playback/playback_render.go | 8 +- format/it/playback/playback_textoutput.go | 2 +- format/it/playback/playback_tracing.go | 8 +- format/s3m/effect/effect_arpeggio.go | 4 +- format/s3m/effect/effect_enablefilter.go | 2 +- .../s3m/effect/effect_extrafineportadown.go | 2 +- format/s3m/effect/effect_extrafineportaup.go | 2 +- format/s3m/effect/effect_finepatterndelay.go | 2 +- format/s3m/effect/effect_fineportadown.go | 2 +- format/s3m/effect/effect_fineportaup.go | 2 +- format/s3m/effect/effect_finevibrato.go | 4 +- format/s3m/effect/effect_finevolslidedown.go | 4 +- format/s3m/effect/effect_finevolslideup.go | 4 +- format/s3m/effect/effect_notecut.go | 4 +- format/s3m/effect/effect_notedelay.go | 4 +- format/s3m/effect/effect_orderjump.go | 4 +- format/s3m/effect/effect_patterndelay.go | 4 +- format/s3m/effect/effect_patternloop.go | 4 +- format/s3m/effect/effect_portadown.go | 4 +- format/s3m/effect/effect_portatonote.go | 6 +- format/s3m/effect/effect_portaup.go | 4 +- format/s3m/effect/effect_portavolslide.go | 3 +- format/s3m/effect/effect_retrigvolslide.go | 4 +- format/s3m/effect/effect_rowjump.go | 4 +- format/s3m/effect/effect_sampleoffset.go | 2 +- format/s3m/effect/effect_setfinetune.go | 4 +- format/s3m/effect/effect_setglobalvolume.go | 4 +- format/s3m/effect/effect_setpanposition.go | 2 +- format/s3m/effect/effect_setspeed.go | 4 +- format/s3m/effect/effect_settempo.go | 6 +- .../s3m/effect/effect_settremolowaveform.go | 2 +- .../s3m/effect/effect_setvibratowaveform.go | 2 +- format/s3m/effect/effect_stereocontrol.go | 2 +- format/s3m/effect/effect_tremolo.go | 4 +- format/s3m/effect/effect_tremor.go | 4 +- format/s3m/effect/effect_vibrato.go | 4 +- format/s3m/effect/effect_vibratovolslide.go | 3 +- format/s3m/effect/effect_volslidedown.go | 4 +- format/s3m/effect/effect_volslideup.go | 4 +- format/s3m/effect/unhandled.go | 3 +- format/s3m/effect/util.go | 39 +++-- format/s3m/period/amiga.go | 9 +- format/s3m/period/util.go | 4 +- format/s3m/playback/playback.go | 67 +++---- format/s3m/playback/playback_command.go | 19 +- format/s3m/playback/playback_pattern.go | 7 +- format/s3m/playback/playback_render.go | 8 +- format/s3m/playback/playback_textoutput.go | 2 +- format/xm/effect/effect_arpeggio.go | 9 +- format/xm/effect/effect_extrafineportadown.go | 9 +- format/xm/effect/effect_extrafineportaup.go | 9 +- format/xm/effect/effect_fineportadown.go | 9 +- format/xm/effect/effect_fineportaup.go | 9 +- format/xm/effect/effect_finevolslidedown.go | 7 +- format/xm/effect/effect_finevolslideup.go | 7 +- format/xm/effect/effect_globalvolumeslide.go | 9 +- format/xm/effect/effect_notecut.go | 9 +- format/xm/effect/effect_notedelay.go | 9 +- format/xm/effect/effect_orderjump.go | 9 +- format/xm/effect/effect_panslide.go | 7 +- format/xm/effect/effect_patterndelay.go | 9 +- format/xm/effect/effect_patternloop.go | 7 +- format/xm/effect/effect_portadown.go | 11 +- format/xm/effect/effect_portatonote.go | 12 +- format/xm/effect/effect_portaup.go | 11 +- format/xm/effect/effect_portavolslide.go | 15 +- format/xm/effect/effect_retriggernote.go | 9 +- format/xm/effect/effect_retrigvolslide.go | 9 +- format/xm/effect/effect_rowjump.go | 9 +- format/xm/effect/effect_sampleoffset.go | 7 +- .../xm/effect/effect_setcoarsepanposition.go | 7 +- .../xm/effect/effect_setenvelopeposition.go | 7 +- format/xm/effect/effect_setfinetune.go | 9 +- format/xm/effect/effect_setglobalvolume.go | 9 +- format/xm/effect/effect_setpanposition.go | 7 +- format/xm/effect/effect_setspeed.go | 9 +- format/xm/effect/effect_settempo.go | 11 +- format/xm/effect/effect_settremolowaveform.go | 7 +- format/xm/effect/effect_setvibratowaveform.go | 7 +- format/xm/effect/effect_setvolume.go | 7 +- format/xm/effect/effect_tremolo.go | 9 +- format/xm/effect/effect_tremor.go | 9 +- format/xm/effect/effect_vibrato.go | 9 +- format/xm/effect/effect_vibratovolslide.go | 15 +- format/xm/effect/effect_volslide.go | 9 +- format/xm/effect/effectfactory.go | 15 +- format/xm/effect/effectfactory_standard.go | 91 +++++----- format/xm/effect/effectfactory_volume.go | 23 +-- format/xm/effect/unhandled.go | 13 +- format/xm/effect/util.go | 57 +++--- format/xm/load/xmformat.go | 6 +- format/xm/period/amiga.go | 9 +- format/xm/period/linear.go | 9 +- format/xm/period/util.go | 59 +++---- format/xm/playback/playback.go | 110 +++++++----- format/xm/playback/playback_command.go | 23 ++- format/xm/playback/playback_pattern.go | 6 +- format/xm/playback/playback_render.go | 8 +- format/xm/playback/playback_textoutput.go | 2 +- note/note_test.go | 21 ++- period/linear.go | 5 +- period/period.go | 4 +- player/state/active.go | 22 +-- player/state/channel.go | 164 +++++++++--------- player/state/channel_transaction.go | 80 ++++----- player/state/pastnotes.go | 33 ++-- player/state/playback.go | 6 +- 173 files changed, 1342 insertions(+), 1110 deletions(-) diff --git a/channel.go b/channel.go index 3fd8b8c..5836b83 100644 --- a/channel.go +++ b/channel.go @@ -14,7 +14,7 @@ import ( ) // Channel is an interface for channel state -type Channel[TMemory any] interface { +type Channel[TPeriod period.Period, TMemory any] interface { ResetRetriggerCount() SetMemory(*TMemory) GetMemory() *TMemory @@ -23,13 +23,13 @@ type Channel[TMemory any] interface { FreezePlayback() UnfreezePlayback() GetData() song.ChannelData - GetPortaTargetPeriod() period.Period - SetPortaTargetPeriod(period.Period) - GetTargetPeriod() period.Period - SetTargetPeriod(period.Period) - SetPeriodOverride(period.Period) - GetPeriod() period.Period - SetPeriod(period.Period) + GetPortaTargetPeriod() *TPeriod + SetPortaTargetPeriod(*TPeriod) + GetTargetPeriod() *TPeriod + SetTargetPeriod(*TPeriod) + SetPeriodOverride(*TPeriod) + GetPeriod() *TPeriod + SetPeriod(*TPeriod) SetPeriodDelta(period.PeriodDelta) GetPeriodDelta() period.PeriodDelta SetInstrument(*instrument.Instrument) diff --git a/effect.go b/effect.go index da42d71..f066771 100644 --- a/effect.go +++ b/effect.go @@ -1,19 +1,23 @@ package playback -import "fmt" +import ( + "fmt" + + "github.com/gotracker/playback/period" +) // Effect is an interface to command/effect type Effect interface { //fmt.Stringer } -type effectPreStartIntf[TMemory any] interface { - PreStart(Channel[TMemory], Playback) error +type effectPreStartIntf[TPeriod period.Period, TMemory any] interface { + PreStart(Channel[TPeriod, TMemory], Playback) error } // EffectPreStart triggers when the effect enters onto the channel state -func EffectPreStart[TMemory any](e Effect, cs Channel[TMemory], p Playback) error { - if eff, ok := e.(effectPreStartIntf[TMemory]); ok { +func EffectPreStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback) error { + if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory]); ok { if err := eff.PreStart(cs, p); err != nil { return err } @@ -21,13 +25,13 @@ func EffectPreStart[TMemory any](e Effect, cs Channel[TMemory], p Playback) erro return nil } -type effectStartIntf[TMemory any] interface { - Start(Channel[TMemory], Playback) error +type effectStartIntf[TPeriod period.Period, TMemory any] interface { + Start(Channel[TPeriod, TMemory], Playback) error } // EffectStart triggers on the first tick, but before the Tick() function is called -func EffectStart[TMemory any](e Effect, cs Channel[TMemory], p Playback) error { - if eff, ok := e.(effectStartIntf[TMemory]); ok { +func EffectStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback) error { + if eff, ok := e.(effectStartIntf[TPeriod, TMemory]); ok { if err := eff.Start(cs, p); err != nil { return err } @@ -35,13 +39,13 @@ func EffectStart[TMemory any](e Effect, cs Channel[TMemory], p Playback) error { return nil } -type effectTickIntf[TMemory any] interface { - Tick(Channel[TMemory], Playback, int) error +type effectTickIntf[TPeriod period.Period, TMemory any] interface { + Tick(Channel[TPeriod, TMemory], Playback, int) error } // EffectTick is called on every tick -func EffectTick[TMemory any](e Effect, cs Channel[TMemory], p Playback, currentTick int) error { - if eff, ok := e.(effectTickIntf[TMemory]); ok { +func EffectTick[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, currentTick int) error { + if eff, ok := e.(effectTickIntf[TPeriod, TMemory]); ok { if err := eff.Tick(cs, p, currentTick); err != nil { return err } @@ -49,13 +53,13 @@ func EffectTick[TMemory any](e Effect, cs Channel[TMemory], p Playback, currentT return nil } -type effectStopIntf[TMemory any] interface { - Stop(Channel[TMemory], Playback, int) error +type effectStopIntf[TPeriod period.Period, TMemory any] interface { + Stop(Channel[TPeriod, TMemory], Playback, int) error } // EffectStop is called on the last tick of the row, but after the Tick() function is called -func EffectStop[TMemory any](e Effect, cs Channel[TMemory], p Playback, lastTick int) error { - if eff, ok := e.(effectStopIntf[TMemory]); ok { +func EffectStop[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, lastTick int) error { + if eff, ok := e.(effectStopIntf[TPeriod, TMemory]); ok { if err := eff.Stop(cs, p, lastTick); err != nil { return err } @@ -64,12 +68,12 @@ func EffectStop[TMemory any](e Effect, cs Channel[TMemory], p Playback, lastTick } // CombinedEffect specifies multiple simultaneous effects into one -type CombinedEffect[TMemory any] struct { +type CombinedEffect[TPeriod period.Period, TMemory any] struct { Effects []Effect } // PreStart triggers when the effect enters onto the channel state -func (e CombinedEffect[TMemory]) PreStart(cs Channel[TMemory], p Playback) error { +func (e CombinedEffect[TPeriod, TMemory]) PreStart(cs Channel[TPeriod, TMemory], p Playback) error { for _, effect := range e.Effects { if err := EffectPreStart(effect, cs, p); err != nil { return err @@ -79,7 +83,7 @@ func (e CombinedEffect[TMemory]) PreStart(cs Channel[TMemory], p Playback) error } // Start triggers on the first tick, but before the Tick() function is called -func (e CombinedEffect[TMemory]) Start(cs Channel[TMemory], p Playback) error { +func (e CombinedEffect[TPeriod, TMemory]) Start(cs Channel[TPeriod, TMemory], p Playback) error { for _, effect := range e.Effects { if err := EffectStart(effect, cs, p); err != nil { return err @@ -89,7 +93,7 @@ func (e CombinedEffect[TMemory]) Start(cs Channel[TMemory], p Playback) error { } // Tick is called on every tick -func (e CombinedEffect[TMemory]) Tick(cs Channel[TMemory], p Playback, currentTick int) error { +func (e CombinedEffect[TPeriod, TMemory]) Tick(cs Channel[TPeriod, TMemory], p Playback, currentTick int) error { for _, effect := range e.Effects { if err := EffectTick(effect, cs, p, currentTick); err != nil { return err @@ -99,7 +103,7 @@ func (e CombinedEffect[TMemory]) Tick(cs Channel[TMemory], p Playback, currentTi } // Stop is called on the last tick of the row, but after the Tick() function is called -func (e CombinedEffect[TMemory]) Stop(cs Channel[TMemory], p Playback, lastTick int) error { +func (e CombinedEffect[TPeriod, TMemory]) Stop(cs Channel[TPeriod, TMemory], p Playback, lastTick int) error { for _, effect := range e.Effects { if err := EffectStop(effect, cs, p, lastTick); err != nil { return err @@ -109,7 +113,7 @@ func (e CombinedEffect[TMemory]) Stop(cs Channel[TMemory], p Playback, lastTick } // String returns the string for the effect list -func (e CombinedEffect[TMemory]) String() string { +func (e CombinedEffect[TPeriod, TMemory]) String() string { for _, eff := range e.Effects { s := fmt.Sprint(eff) if s != "" { @@ -120,7 +124,7 @@ func (e CombinedEffect[TMemory]) String() string { } // DoEffect runs the standard tick lifetime of an effect -func DoEffect[TMemory any](e Effect, cs Channel[TMemory], p Playback, currentTick int, lastTick bool) error { +func DoEffect[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, currentTick int, lastTick bool) error { if e == nil { return nil } diff --git a/format/common/loadformat.go b/format/common/loadformat.go index 6afd91d..a9950c6 100644 --- a/format/common/loadformat.go +++ b/format/common/loadformat.go @@ -3,14 +3,15 @@ package common import ( "io" + "github.com/gotracker/playback" "github.com/gotracker/playback/player/feature" ) type ReaderFunc[TSong any] func(r io.Reader, features []feature.Feature) (*TSong, error) -type ManagerFactory[TSong, TManager any] func(*TSong) (*TManager, error) +type ManagerFactory[TSong any] func(*TSong) (playback.Playback, error) -func Load[TSong, TManager any](r io.Reader, reader ReaderFunc[TSong], factory ManagerFactory[TSong, TManager], features []feature.Feature) (*TManager, error) { +func Load[TSong any](r io.Reader, reader ReaderFunc[TSong], factory ManagerFactory[TSong], features []feature.Feature) (playback.Playback, error) { song, err := reader(r, features) if err != nil { return nil, err diff --git a/format/it/effect/effect_arpeggio.go b/format/it/effect/effect_arpeggio.go index bf73de9..db1701f 100644 --- a/format/it/effect/effect_arpeggio.go +++ b/format/it/effect/effect_arpeggio.go @@ -5,13 +5,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // Arpeggio defines an arpeggio effect -type Arpeggio channel.DataEffect // 'J' +type Arpeggio[TPeriod period.Period] 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], p playback.Playback) error { +func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -19,12 +20,12 @@ func (e Arpeggio) Start(cs playback.Channel[channel.Memory], p playback.Playback } // Tick is called on every tick -func (e Arpeggio) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e Arpeggio[TPeriod]) 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 index f573b64..4109186 100644 --- a/format/it/effect/effect_channelvolumeslide.go +++ b/format/it/effect/effect_channelvolumeslide.go @@ -7,13 +7,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // ChannelVolumeSlide defines a set channel volume effect -type ChannelVolumeSlide channel.DataEffect // 'Nxy' +type ChannelVolumeSlide[TPeriod period.Period] 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], p playback.Playback) error { +func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() @@ -39,7 +40,7 @@ func (e ChannelVolumeSlide) Start(cs playback.Channel[channel.Memory], p playbac } // Tick is called on every tick -func (e ChannelVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.ChannelVolumeSlide(channel.DataEffect(e)) @@ -63,6 +64,6 @@ func (e ChannelVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback return nil } -func (e ChannelVolumeSlide) String() string { +func (e ChannelVolumeSlide[TPeriod]) 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 index 76ab002..003c25e 100644 --- a/format/it/effect/effect_extrafineportadown.go +++ b/format/it/effect/effect_extrafineportadown.go @@ -5,22 +5,23 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // ExtraFinePortaDown defines an extra-fine portamento down effect -type ExtraFinePortaDown channel.DataEffect // 'EEx' +type ExtraFinePortaDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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) + return doPortaDown(cs, float32(y), 1) } -func (e ExtraFinePortaDown) String() string { +func (e ExtraFinePortaDown[TPeriod]) 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 index 357ac1b..632bbc2 100644 --- a/format/it/effect/effect_extrafineportaup.go +++ b/format/it/effect/effect_extrafineportaup.go @@ -5,22 +5,23 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // ExtraFinePortaUp defines an extra-fine portamento up effect -type ExtraFinePortaUp channel.DataEffect // 'FEx' +type ExtraFinePortaUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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) + return doPortaUp(cs, float32(y), 1) } -func (e ExtraFinePortaUp) String() string { +func (e ExtraFinePortaUp[TPeriod]) 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 index 3e23d82..339f911 100644 --- a/format/it/effect/effect_finepatterndelay.go +++ b/format/it/effect/effect_finepatterndelay.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" effectIntf "github.com/gotracker/playback/format/it/effect/intf" + "github.com/gotracker/playback/period" ) // FinePatternDelay defines an fine pattern delay effect -type FinePatternDelay channel.DataEffect // 'S6x' +type FinePatternDelay[TPeriod period.Period] 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], p playback.Playback) error { +func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -24,6 +25,6 @@ func (e FinePatternDelay) Start(cs playback.Channel[channel.Memory], p playback. return nil } -func (e FinePatternDelay) String() string { +func (e FinePatternDelay[TPeriod]) 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 index b5799bb..9ad107c 100644 --- a/format/it/effect/effect_fineportadown.go +++ b/format/it/effect/effect_fineportadown.go @@ -5,22 +5,23 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // FinePortaDown defines an fine portamento down effect -type FinePortaDown channel.DataEffect // 'EFx' +type FinePortaDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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) + return doPortaDown(cs, float32(y), 4) } -func (e FinePortaDown) String() string { +func (e FinePortaDown[TPeriod]) 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 index fa2bf9b..83f6b66 100644 --- a/format/it/effect/effect_fineportaup.go +++ b/format/it/effect/effect_fineportaup.go @@ -5,22 +5,23 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // FinePortaUp defines an fine portamento up effect -type FinePortaUp channel.DataEffect // 'FFx' +type FinePortaUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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) + return doPortaUp(cs, float32(y), 4) } -func (e FinePortaUp) String() string { +func (e FinePortaUp[TPeriod]) 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 index 03e58df..2019592 100644 --- a/format/it/effect/effect_finevibrato.go +++ b/format/it/effect/effect_finevibrato.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // FineVibrato defines an fine vibrato effect -type FineVibrato channel.DataEffect // 'U' +type FineVibrato[TPeriod period.Period] 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], p playback.Playback) error { +func (e FineVibrato[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(channel.DataEffect(e)) if currentTick != 0 { @@ -27,6 +28,6 @@ func (e FineVibrato) Tick(cs playback.Channel[channel.Memory], p playback.Playba return nil } -func (e FineVibrato) String() string { +func (e FineVibrato[TPeriod]) 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 index 6f52009..f9d37ce 100644 --- a/format/it/effect/effect_finevolslidedown.go +++ b/format/it/effect/effect_finevolslidedown.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // FineVolumeSlideDown defines a fine volume slide down effect -type FineVolumeSlideDown channel.DataEffect // 'D' +type FineVolumeSlideDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e FineVolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() _, y := mem.VolumeSlide(channel.DataEffect(e)) @@ -27,23 +28,23 @@ func (e FineVolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playbac return nil } -func (e FineVolumeSlideDown) String() string { +func (e FineVolumeSlideDown[TPeriod]) 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' +type VolChanFineVolumeSlideDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolChanFineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e VolChanFineVolumeSlideDown[TPeriod]) 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 index b153e65..54706fd 100644 --- a/format/it/effect/effect_finevolslideup.go +++ b/format/it/effect/effect_finevolslideup.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // FineVolumeSlideUp defines a fine volume slide up effect -type FineVolumeSlideUp channel.DataEffect // 'D' +type FineVolumeSlideUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e FineVolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, _ := mem.VolumeSlide(channel.DataEffect(e)) @@ -27,23 +28,23 @@ func (e FineVolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback. return nil } -func (e FineVolumeSlideUp) String() string { +func (e FineVolumeSlideUp[TPeriod]) 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' +type VolChanFineVolumeSlideUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolChanFineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e VolChanFineVolumeSlideUp[TPeriod]) 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 index 32687f3..d43d2c6 100644 --- a/format/it/effect/effect_globalvolumeslide.go +++ b/format/it/effect/effect_globalvolumeslide.go @@ -6,19 +6,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" effectIntf "github.com/gotracker/playback/format/it/effect/intf" + "github.com/gotracker/playback/period" ) // GlobalVolumeSlide defines a global volume slide effect -type GlobalVolumeSlide channel.DataEffect // 'W' +type GlobalVolumeSlide[TPeriod period.Period] 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], p playback.Playback) error { +func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e GlobalVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) @@ -38,6 +39,6 @@ func (e GlobalVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback. return nil } -func (e GlobalVolumeSlide) String() string { +func (e GlobalVolumeSlide[TPeriod]) 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 index d92af4c..7a060e7 100644 --- a/format/it/effect/effect_highoffset.go +++ b/format/it/effect/effect_highoffset.go @@ -5,13 +5,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // HighOffset defines a sample high offset effect -type HighOffset channel.DataEffect // 'SAx' +type HighOffset[TPeriod period.Period] 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], p playback.Playback) error { +func (e HighOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() @@ -21,6 +22,6 @@ func (e HighOffset) Start(cs playback.Channel[channel.Memory], p playback.Playba return nil } -func (e HighOffset) String() string { +func (e HighOffset[TPeriod]) 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 index 9897a3d..01081ef 100644 --- a/format/it/effect/effect_newnoteactionnotecontinue.go +++ b/format/it/effect/effect_newnoteactionnotecontinue.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // NewNoteActionNoteContinue defines a NewNoteAction: Note Continue effect -type NewNoteActionNoteContinue channel.DataEffect // 'S74' +type NewNoteActionNoteContinue[TPeriod period.Period] 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], p playback.Playback) error { +func (e NewNoteActionNoteContinue[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionContinue) return nil } -func (e NewNoteActionNoteContinue) String() string { +func (e NewNoteActionNoteContinue[TPeriod]) 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 index 8001490..7fe5911 100644 --- a/format/it/effect/effect_newnoteactionnotecut.go +++ b/format/it/effect/effect_newnoteactionnotecut.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // NewNoteActionNoteCut defines a NewNoteAction: Note Cut effect -type NewNoteActionNoteCut channel.DataEffect // 'S73' +type NewNoteActionNoteCut[TPeriod period.Period] 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], p playback.Playback) error { +func (e NewNoteActionNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionCut) return nil } -func (e NewNoteActionNoteCut) String() string { +func (e NewNoteActionNoteCut[TPeriod]) 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 index e9054e1..f6c4ae8 100644 --- a/format/it/effect/effect_newnoteactionnotefade.go +++ b/format/it/effect/effect_newnoteactionnotefade.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // NewNoteActionNoteFade defines a NewNoteAction: Note Fade effect -type NewNoteActionNoteFade channel.DataEffect // 'S76' +type NewNoteActionNoteFade[TPeriod period.Period] 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], p playback.Playback) error { +func (e NewNoteActionNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionFadeout) return nil } -func (e NewNoteActionNoteFade) String() string { +func (e NewNoteActionNoteFade[TPeriod]) 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 index e49e5d1..4a6e04e 100644 --- a/format/it/effect/effect_newnoteactionnoteoff.go +++ b/format/it/effect/effect_newnoteactionnoteoff.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // NewNoteActionNoteOff defines a NewNoteAction: Note Off effect -type NewNoteActionNoteOff channel.DataEffect // 'S75' +type NewNoteActionNoteOff[TPeriod period.Period] 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], p playback.Playback) error { +func (e NewNoteActionNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionRelease) return nil } -func (e NewNoteActionNoteOff) String() string { +func (e NewNoteActionNoteOff[TPeriod]) 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 index 3fac390..fe39a6f 100644 --- a/format/it/effect/effect_notecut.go +++ b/format/it/effect/effect_notecut.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // NoteCut defines a note cut effect -type NoteCut channel.DataEffect // 'SCx' +type NoteCut[TPeriod period.Period] 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], p playback.Playback) error { +func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e NoteCut) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { @@ -26,6 +27,6 @@ func (e NoteCut) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return nil } -func (e NoteCut) String() string { +func (e NoteCut[TPeriod]) 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 index b8b97ee..52baa51 100644 --- a/format/it/effect/effect_notedelay.go +++ b/format/it/effect/effect_notedelay.go @@ -6,23 +6,24 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // NoteDelay defines a note delay effect -type NoteDelay channel.DataEffect // 'SDx' +type NoteDelay[TPeriod period.Period] channel.DataEffect // 'SDx' // PreStart triggers when the effect enters onto the channel state -func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback) error { +func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e NoteDelay) String() string { +func (e NoteDelay[TPeriod]) 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 index 292b373..271ae0c 100644 --- a/format/it/effect/effect_orderjump.go +++ b/format/it/effect/effect_orderjump.go @@ -6,22 +6,23 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" ) // OrderJump defines an order jump effect -type OrderJump channel.DataEffect // 'B' +type OrderJump[TPeriod period.Period] 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], p playback.Playback) error { +func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, lastTick int) error { +func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } -func (e OrderJump) String() string { +func (e OrderJump[TPeriod]) 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 index 1b54720..3366e4f 100644 --- a/format/it/effect/effect_panningenvelopeoff.go +++ b/format/it/effect/effect_panningenvelopeoff.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PanningEnvelopeOff defines a panning envelope: off effect -type PanningEnvelopeOff channel.DataEffect // 'S79' +type PanningEnvelopeOff[TPeriod period.Period] 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], p playback.Playback) error { +func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(false) return nil } -func (e PanningEnvelopeOff) String() string { +func (e PanningEnvelopeOff[TPeriod]) 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 index da0fc09..29d708d 100644 --- a/format/it/effect/effect_panningenvelopeon.go +++ b/format/it/effect/effect_panningenvelopeon.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PanningEnvelopeOn defines a panning envelope: on effect -type PanningEnvelopeOn channel.DataEffect // 'S7A' +type PanningEnvelopeOn[TPeriod period.Period] 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], p playback.Playback) error { +func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(true) return nil } -func (e PanningEnvelopeOn) String() string { +func (e PanningEnvelopeOn[TPeriod]) 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 index 9e71b1e..097ce8f 100644 --- a/format/it/effect/effect_pastnotecut.go +++ b/format/it/effect/effect_pastnotecut.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // PastNoteCut defines a past note cut effect -type PastNoteCut channel.DataEffect // 'S70' +type PastNoteCut[TPeriod period.Period] 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], p playback.Playback) error { +func (e PastNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionCut) return nil } -func (e PastNoteCut) String() string { +func (e PastNoteCut[TPeriod]) 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 index 794bce2..a6e35f6 100644 --- a/format/it/effect/effect_pastnotefadeout.go +++ b/format/it/effect/effect_pastnotefadeout.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // PastNoteFade defines a past note fadeout effect -type PastNoteFade channel.DataEffect // 'S72' +type PastNoteFade[TPeriod period.Period] 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], p playback.Playback) error { +func (e PastNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionFadeout) return nil } -func (e PastNoteFade) String() string { +func (e PastNoteFade[TPeriod]) 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 index 545af35..c925480 100644 --- a/format/it/effect/effect_pastnoteoff.go +++ b/format/it/effect/effect_pastnoteoff.go @@ -6,17 +6,18 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // PastNoteOff defines a past note off effect -type PastNoteOff channel.DataEffect // 'S71' +type PastNoteOff[TPeriod period.Period] 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], p playback.Playback) error { +func (e PastNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionRelease) return nil } -func (e PastNoteOff) String() string { +func (e PastNoteOff[TPeriod]) 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 index dd861b6..0b938d4 100644 --- a/format/it/effect/effect_patterndelay.go +++ b/format/it/effect/effect_patterndelay.go @@ -6,23 +6,24 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" effectIntf "github.com/gotracker/playback/format/it/effect/intf" + "github.com/gotracker/playback/period" ) // PatternDelay defines a pattern delay effect -type PatternDelay channel.DataEffect // 'SEx' +type PatternDelay[TPeriod period.Period] channel.DataEffect // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback) error { +func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e PatternDelay) String() string { +func (e PatternDelay[TPeriod]) 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 index 46923aa..c6ff81e 100644 --- a/format/it/effect/effect_patternloop.go +++ b/format/it/effect/effect_patternloop.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PatternLoop defines a pattern loop effect -type PatternLoop channel.DataEffect // 'SBx' +type PatternLoop[TPeriod period.Period] 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], p playback.Playback) error { +func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, lastTick int) error { +func (e PatternLoop[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { x := uint8(e) & 0xF mem := cs.GetMemory() @@ -39,6 +40,6 @@ func (e PatternLoop) Stop(cs playback.Channel[channel.Memory], p playback.Playba return nil } -func (e PatternLoop) String() string { +func (e PatternLoop[TPeriod]) 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 index 17427c9..c214b6b 100644 --- a/format/it/effect/effect_pitchenvelopeoff.go +++ b/format/it/effect/effect_pitchenvelopeoff.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PitchEnvelopeOff defines a panning envelope: off effect -type PitchEnvelopeOff channel.DataEffect // 'S7B' +type PitchEnvelopeOff[TPeriod period.Period] 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], p playback.Playback) error { +func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(false) return nil } -func (e PitchEnvelopeOff) String() string { +func (e PitchEnvelopeOff[TPeriod]) 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 index 79befb1..1afb9f3 100644 --- a/format/it/effect/effect_pitchenvelopeon.go +++ b/format/it/effect/effect_pitchenvelopeon.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PitchEnvelopeOn defines a panning envelope: on effect -type PitchEnvelopeOn channel.DataEffect // 'S7C' +type PitchEnvelopeOn[TPeriod period.Period] 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], p playback.Playback) error { +func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(true) return nil } -func (e PitchEnvelopeOn) String() string { +func (e PitchEnvelopeOn[TPeriod]) 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 index df55836..9eb6bf0 100644 --- a/format/it/effect/effect_portadown.go +++ b/format/it/effect/effect_portadown.go @@ -5,26 +5,27 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PortaDown defines a portamento down effect -type PortaDown channel.DataEffect // 'E' +type PortaDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaDown(channel.DataEffect(e)) - return doPortaDown(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) + return doPortaDown(cs, float32(xx), 4) } -func (e PortaDown) String() string { +func (e PortaDown[TPeriod]) 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 index e490ea1..cd652f9 100644 --- a/format/it/effect/effect_portatonote.go +++ b/format/it/effect/effect_portatonote.go @@ -5,16 +5,17 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + itPeriod "github.com/gotracker/playback/format/it/period" "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' +type PortaToNote[TPeriod period.Period] 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], p playback.Playback) error { +func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,7 +26,7 @@ func (e PortaToNote) Start(cs playback.Channel[channel.Memory], p playback.Playb } // Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaToNote(channel.DataEffect(e)) @@ -34,18 +35,25 @@ func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playba if cur == nil { return nil } - cur = cur.AddDelta(cs.GetPeriodDelta()) + switch pc := any(cur).(type) { + case *itPeriod.Linear: + cur = any(pc.Add(cs.GetPeriodDelta())).(*TPeriod) + case *itPeriod.Amiga: + cur = any(pc.Add(cs.GetPeriodDelta())).(*TPeriod) + default: + panic("unhandled period type") + } 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 + return doPortaUpToNote(cs, float32(xx), 4, ptp) // subtracts } else { - return doPortaDownToNote(cs, float32(xx), 4, ptp, mem.Shared.LinearFreqSlides) // adds + return doPortaDownToNote(cs, float32(xx), 4, ptp) // adds } } return nil } -func (e PortaToNote) String() string { +func (e PortaToNote[TPeriod]) 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 index d3491c7..9f7f091 100644 --- a/format/it/effect/effect_portaup.go +++ b/format/it/effect/effect_portaup.go @@ -5,26 +5,27 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PortaUp defines a portamento up effect -type PortaUp channel.DataEffect // 'F' +type PortaUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaUp(channel.DataEffect(e)) - return doPortaUp(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) + return doPortaUp(cs, float32(xx), 4) } -func (e PortaUp) String() string { +func (e PortaUp[TPeriod]) 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 index 30f6da3..62dac63 100644 --- a/format/it/effect/effect_portavolslide.go +++ b/format/it/effect/effect_portavolslide.go @@ -5,21 +5,22 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect -type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[channel.Memory] +type PortaVolumeSlide[TPeriod period.Period] struct { // 'L' + playback.CombinedEffect[TPeriod, channel.Memory] } // 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)) +func NewPortaVolumeSlide[TPeriod period.Period](mem *channel.Memory, cd channel.Command, val channel.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) String() string { +func (e PortaVolumeSlide[TPeriod]) 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 index 495edb9..856dc43 100644 --- a/format/it/effect/effect_retrigvolslide.go +++ b/format/it/effect/effect_retrigvolslide.go @@ -7,19 +7,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide channel.DataEffect // 'Q' +type RetrigVolumeSlide[TPeriod period.Period] 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], p playback.Playback) error { +func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.RetrigVolumeSlide(channel.DataEffect(e)) if y == 0 { @@ -66,6 +67,6 @@ func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback. return nil } -func (e RetrigVolumeSlide) String() string { +func (e RetrigVolumeSlide[TPeriod]) 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 index eb02a07..3fbc03a 100644 --- a/format/it/effect/effect_rowjump.go +++ b/format/it/effect/effect_rowjump.go @@ -6,24 +6,25 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" ) // RowJump defines a row jump effect -type RowJump channel.DataEffect // 'C' +type RowJump[TPeriod period.Period] 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], p playback.Playback) error { +func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, lastTick int) error { +func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { r := channel.DataEffect(e) rowIdx := index.Row(r) return p.SetNextRow(rowIdx) } -func (e RowJump) String() string { +func (e RowJump[TPeriod]) 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 index 895f61c..49cb1a4 100644 --- a/format/it/effect/effect_sampleoffset.go +++ b/format/it/effect/effect_sampleoffset.go @@ -7,13 +7,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // SampleOffset defines a sample offset effect -type SampleOffset channel.DataEffect // 'O' +type SampleOffset[TPeriod period.Period] 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], p playback.Playback) error { +func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(channel.DataEffect(e)) @@ -29,6 +30,6 @@ func (e SampleOffset) Start(cs playback.Channel[channel.Memory], p playback.Play return nil } -func (e SampleOffset) String() string { +func (e SampleOffset[TPeriod]) 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 index 0f12150..55dd96d 100644 --- a/format/it/effect/effect_setchannelvolume.go +++ b/format/it/effect/effect_setchannelvolume.go @@ -8,13 +8,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // SetChannelVolume defines a set channel volume effect -type SetChannelVolume channel.DataEffect // 'Mxx' +type SetChannelVolume[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := channel.DataEffect(e) @@ -30,6 +31,6 @@ func (e SetChannelVolume) Start(cs playback.Channel[channel.Memory], p playback. return nil } -func (e SetChannelVolume) String() string { +func (e SetChannelVolume[TPeriod]) 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 index 972131f..c690d03 100644 --- a/format/it/effect/effect_setcoarsepanposition.go +++ b/format/it/effect/effect_setcoarsepanposition.go @@ -8,13 +8,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" itPanning "github.com/gotracker/playback/format/it/panning" + "github.com/gotracker/playback/period" ) // SetCoarsePanPosition defines a set coarse pan position effect -type SetCoarsePanPosition channel.DataEffect // 'S8x' +type SetCoarsePanPosition[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -25,6 +26,6 @@ func (e SetCoarsePanPosition) Start(cs playback.Channel[channel.Memory], p playb return nil } -func (e SetCoarsePanPosition) String() string { +func (e SetCoarsePanPosition[TPeriod]) 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 index df3c827..9aad787 100644 --- a/format/it/effect/effect_setfinetune.go +++ b/format/it/effect/effect_setfinetune.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // SetFinetune defines a mod-style set finetune effect -type SetFinetune channel.DataEffect // 'S2x' +type SetFinetune[TPeriod period.Period] channel.DataEffect // 'S2x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { x := channel.DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,11 +25,11 @@ func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Pl } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e SetFinetune) String() string { +func (e SetFinetune[TPeriod]) 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 index f5a5cfb..cd6f2a2 100644 --- a/format/it/effect/effect_setglobalvolume.go +++ b/format/it/effect/effect_setglobalvolume.go @@ -7,13 +7,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // SetGlobalVolume defines a set global volume effect -type SetGlobalVolume channel.DataEffect // 'V' +type SetGlobalVolume[TPeriod period.Period] channel.DataEffect // 'V' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { v := volume.Volume(channel.DataEffect(e)) / 0x80 if v > 1 { v = 1 @@ -23,11 +24,11 @@ func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], p playbac } // Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e SetGlobalVolume) String() string { +func (e SetGlobalVolume[TPeriod]) 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 index 20ae48c..fc3459d 100644 --- a/format/it/effect/effect_setpanbrellowaveform.go +++ b/format/it/effect/effect_setpanbrellowaveform.go @@ -3,6 +3,7 @@ package effect import ( "fmt" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" @@ -10,10 +11,10 @@ import ( ) // SetPanbrelloWaveform defines a set panbrello waveform effect -type SetPanbrelloWaveform channel.DataEffect // 'S5x' +type SetPanbrelloWaveform[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -24,6 +25,6 @@ func (e SetPanbrelloWaveform) Start(cs playback.Channel[channel.Memory], p playb return nil } -func (e SetPanbrelloWaveform) String() string { +func (e SetPanbrelloWaveform[TPeriod]) 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 index dc8b0fc..199501e 100644 --- a/format/it/effect/effect_setpanposition.go +++ b/format/it/effect/effect_setpanposition.go @@ -8,13 +8,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" itPanning "github.com/gotracker/playback/format/it/panning" + "github.com/gotracker/playback/period" ) // SetPanPosition defines a set pan position effect -type SetPanPosition channel.DataEffect // 'Xxx' +type SetPanPosition[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) @@ -25,6 +26,6 @@ func (e SetPanPosition) Start(cs playback.Channel[channel.Memory], p playback.Pl return nil } -func (e SetPanPosition) String() string { +func (e SetPanPosition[TPeriod]) 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 index 387ba87..d17cf2a 100644 --- a/format/it/effect/effect_setspeed.go +++ b/format/it/effect/effect_setspeed.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" effectIntf "github.com/gotracker/playback/format/it/effect/intf" + "github.com/gotracker/playback/period" ) // SetSpeed defines a set speed effect -type SetSpeed channel.DataEffect // 'A' +type SetSpeed[TPeriod period.Period] channel.DataEffect // 'A' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { if e != 0 { m := p.(effectIntf.IT) if err := m.SetTicks(int(e)); err != nil { @@ -23,11 +24,11 @@ func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playb } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e SetSpeed) String() string { +func (e SetSpeed[TPeriod]) 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 index a1cd884..28df49d 100644 --- a/format/it/effect/effect_settempo.go +++ b/format/it/effect/effect_settempo.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" effectIntf "github.com/gotracker/playback/format/it/effect/intf" + "github.com/gotracker/playback/period" ) // SetTempo defines a set tempo effect -type SetTempo channel.DataEffect // 'T' +type SetTempo[TPeriod period.Period] channel.DataEffect // 'T' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { if e > 0x20 { m := p.(effectIntf.IT) if err := m.SetTempo(int(e)); err != nil { @@ -23,13 +24,13 @@ func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playb } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { m := p.(effectIntf.IT) switch channel.DataEffect(e >> 4) { case 0: // decrease tempo @@ -56,6 +57,6 @@ func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return nil } -func (e SetTempo) String() string { +func (e SetTempo[TPeriod]) 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 index 1fefedc..704976b 100644 --- a/format/it/effect/effect_settremolowaveform.go +++ b/format/it/effect/effect_settremolowaveform.go @@ -3,6 +3,7 @@ package effect import ( "fmt" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" @@ -10,10 +11,10 @@ import ( ) // SetTremoloWaveform defines a set tremolo waveform effect -type SetTremoloWaveform channel.DataEffect // 'S4x' +type SetTremoloWaveform[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -24,6 +25,6 @@ func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory], p playbac return nil } -func (e SetTremoloWaveform) String() string { +func (e SetTremoloWaveform[TPeriod]) 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 index 6fdb06e..f41adce 100644 --- a/format/it/effect/effect_setvibratowaveform.go +++ b/format/it/effect/effect_setvibratowaveform.go @@ -3,6 +3,7 @@ package effect import ( "fmt" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" @@ -10,10 +11,10 @@ import ( ) // SetVibratoWaveform defines a set vibrato waveform effect -type SetVibratoWaveform channel.DataEffect // 'S3x' +type SetVibratoWaveform[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -24,6 +25,6 @@ func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory], p playbac return nil } -func (e SetVibratoWaveform) String() string { +func (e SetVibratoWaveform[TPeriod]) 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 index 5bf3173..4103f2c 100644 --- a/format/it/effect/effect_tremolo.go +++ b/format/it/effect/effect_tremolo.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // Tremolo defines a tremolo effect -type Tremolo channel.DataEffect // 'R' +type Tremolo[TPeriod period.Period] 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], p playback.Playback) error { +func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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. @@ -28,6 +29,6 @@ func (e Tremolo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return nil } -func (e Tremolo) String() string { +func (e Tremolo[TPeriod]) 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 index 30ea4d8..7bede8c 100644 --- a/format/it/effect/effect_tremor.go +++ b/format/it/effect/effect_tremor.go @@ -5,24 +5,25 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // Tremor defines a tremor effect -type Tremor channel.DataEffect // 'I' +type Tremor[TPeriod period.Period] 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], p playback.Playback) error { +func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremor) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e Tremor[TPeriod]) 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 index 1d0a0e4..a03ed4e 100644 --- a/format/it/effect/effect_vibrato.go +++ b/format/it/effect/effect_vibrato.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // Vibrato defines a vibrato effect -type Vibrato channel.DataEffect // 'H' +type Vibrato[TPeriod period.Period] 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], p playback.Playback) error { +func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(channel.DataEffect(e)) if mem.Shared.OldEffectMode { @@ -31,6 +32,6 @@ func (e Vibrato) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return nil } -func (e Vibrato) String() string { +func (e Vibrato[TPeriod]) 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 index 3fc3e34..c1d25c7 100644 --- a/format/it/effect/effect_vibratovolslide.go +++ b/format/it/effect/effect_vibratovolslide.go @@ -5,21 +5,22 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // VibratoVolumeSlide defines a combination vibrato and volume slide effect -type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[channel.Memory] +type VibratoVolumeSlide[TPeriod period.Period] struct { // 'K' + playback.CombinedEffect[TPeriod, channel.Memory] } // 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)) +func NewVibratoVolumeSlide[TPeriod period.Period](mem *channel.Memory, cd channel.Command, val channel.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) String() string { +func (e VibratoVolumeSlide[TPeriod]) 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 index 90bd080..d89b9d2 100644 --- a/format/it/effect/effect_volslidedown.go +++ b/format/it/effect/effect_volslidedown.go @@ -5,42 +5,43 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // VolumeSlideDown defines a volume slide down effect -type VolumeSlideDown channel.DataEffect // 'D' +type VolumeSlideDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e VolumeSlideDown[TPeriod]) 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' +type VolChanVolumeSlideDown[TPeriod period.Period] channel.DataEffect // 'd' // Tick is called on every tick -func (e VolChanVolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolChanVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e VolChanVolumeSlideDown[TPeriod]) 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 index 42c972d..e146413 100644 --- a/format/it/effect/effect_volslideup.go +++ b/format/it/effect/effect_volslideup.go @@ -5,42 +5,43 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // VolumeSlideUp defines a volume slide up effect -type VolumeSlideUp channel.DataEffect // 'D' +type VolumeSlideUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e VolumeSlideUp[TPeriod]) 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' +type VolChanVolumeSlideUp[TPeriod period.Period] channel.DataEffect // 'd' // Tick is called on every tick -func (e VolChanVolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolChanVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e VolChanVolumeSlideUp[TPeriod]) 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 index e398cbd..a8b8fff 100644 --- a/format/it/effect/effect_volumeenvelopeoff.go +++ b/format/it/effect/effect_volumeenvelopeoff.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // VolumeEnvelopeOff defines a volume envelope: off effect -type VolumeEnvelopeOff channel.DataEffect // 'S77' +type VolumeEnvelopeOff[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(false) return nil } -func (e VolumeEnvelopeOff) String() string { +func (e VolumeEnvelopeOff[TPeriod]) 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 index 53b4c67..647f949 100644 --- a/format/it/effect/effect_volumeenvelopeon.go +++ b/format/it/effect/effect_volumeenvelopeon.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // VolumeEnvelopeOn defines a volume envelope: on effect -type VolumeEnvelopeOn channel.DataEffect // 'S78' +type VolumeEnvelopeOn[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(true) return nil } -func (e VolumeEnvelopeOn) String() string { +func (e VolumeEnvelopeOn[TPeriod]) 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 index 8a4851d..278b132 100644 --- a/format/it/effect/effectfactory.go +++ b/format/it/effect/effectfactory.go @@ -5,18 +5,19 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" + "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 struct { - playback.CombinedEffect[channel.Memory] +type VolEff[TPeriod period.Period] struct { + playback.CombinedEffect[TPeriod, channel.Memory] eff EffectIT } -func (e VolEff) String() string { +func (e VolEff[TPeriod]) String() string { if e.eff == nil { return "..." } @@ -24,7 +25,7 @@ func (e VolEff) String() string { } // Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { +func Factory[TPeriod period.Period](mem *channel.Memory, data song.ChannelData) EffectIT { if data == nil { return nil } @@ -35,15 +36,15 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { return nil } - var eff VolEff + var eff VolEff[TPeriod] if d.What.HasVolPan() { - ve := volPanEffectFactory(mem, d.VolPan) + ve := volPanEffectFactory[TPeriod](mem, d.VolPan) if ve != nil { eff.Effects = append(eff.Effects, ve) } } - if e := standardEffectFactory(mem, d); e != nil { + if e := standardEffectFactory[TPeriod](mem, d); e != nil { eff.Effects = append(eff.Effects, e) eff.eff = e } @@ -58,165 +59,165 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectIT { } } -func standardEffectFactory(mem *channel.Memory, data channel.Data) EffectIT { +func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, data channel.Data) EffectIT { switch data.Effect + '@' { case '@': // unused return nil case 'A': // Set Speed - return SetSpeed(data.EffectParameter) + return SetSpeed[TPeriod](data.EffectParameter) case 'B': // Pattern Jump - return OrderJump(data.EffectParameter) + return OrderJump[TPeriod](data.EffectParameter) case 'C': // Pattern Break - return RowJump(data.EffectParameter) + return RowJump[TPeriod](data.EffectParameter) case 'D': // Volume Slide / Fine Volume Slide - return volumeSlideFactory(mem, data.Effect, data.EffectParameter) + return volumeSlideFactory[TPeriod](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) + return FinePortaDown[TPeriod](xx) } else if x == 0x0E { - return ExtraFinePortaDown(xx) + return ExtraFinePortaDown[TPeriod](xx) } - return PortaDown(data.EffectParameter) + return PortaDown[TPeriod](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) + return FinePortaUp[TPeriod](xx) } else if x == 0x0E { - return ExtraFinePortaUp(xx) + return ExtraFinePortaUp[TPeriod](xx) } - return PortaUp(data.EffectParameter) + return PortaUp[TPeriod](data.EffectParameter) case 'G': // Porta to note - return PortaToNote(data.EffectParameter) + return PortaToNote[TPeriod](data.EffectParameter) case 'H': // Vibrato - return Vibrato(data.EffectParameter) + return Vibrato[TPeriod](data.EffectParameter) case 'I': // Tremor - return Tremor(data.EffectParameter) + return Tremor[TPeriod](data.EffectParameter) case 'J': // Arpeggio - return Arpeggio(data.EffectParameter) + return Arpeggio[TPeriod](data.EffectParameter) case 'K': // Vibrato+Volume Slide - return NewVibratoVolumeSlide(mem, data.Effect, data.EffectParameter) + return NewVibratoVolumeSlide[TPeriod](mem, data.Effect, data.EffectParameter) case 'L': // Porta+Volume Slide - return NewPortaVolumeSlide(mem, data.Effect, data.EffectParameter) + return NewPortaVolumeSlide[TPeriod](mem, data.Effect, data.EffectParameter) case 'M': // Set Channel Volume - return SetChannelVolume(data.EffectParameter) + return SetChannelVolume[TPeriod](data.EffectParameter) case 'N': // Channel Volume Slide - return ChannelVolumeSlide(data.EffectParameter) + return ChannelVolumeSlide[TPeriod](data.EffectParameter) case 'O': // Sample Offset - return SampleOffset(data.EffectParameter) + return SampleOffset[TPeriod](data.EffectParameter) case 'P': // Panning Slide //return panningSlideFactory(mem, data.Effect, data.EffectParameter) case 'Q': // Retrig + Volume Slide - return RetrigVolumeSlide(data.EffectParameter) + return RetrigVolumeSlide[TPeriod](data.EffectParameter) case 'R': // Tremolo - return Tremolo(data.EffectParameter) + return Tremolo[TPeriod](data.EffectParameter) case 'S': // Special - return specialEffect(data) + return specialEffect[TPeriod](data) case 'T': // Set Tempo - return SetTempo(data.EffectParameter) + return SetTempo[TPeriod](data.EffectParameter) case 'U': // Fine Vibrato - return FineVibrato(data.EffectParameter) + return FineVibrato[TPeriod](data.EffectParameter) case 'V': // Global Volume - return SetGlobalVolume(data.EffectParameter) + return SetGlobalVolume[TPeriod](data.EffectParameter) case 'W': // Global Volume Slide - return GlobalVolumeSlide(data.EffectParameter) + return GlobalVolumeSlide[TPeriod](data.EffectParameter) case 'X': // Set Pan Position - return SetPanPosition(data.EffectParameter) + return SetPanPosition[TPeriod](data.EffectParameter) case 'Y': // Panbrello - //return Panbrello(data.EffectParameter) + //return Panbrello[TPeriod](data.EffectParameter) case 'Z': // MIDI Macro return nil // TODO: MIDIMacro default: } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func specialEffect(data channel.Data) EffectIT { +func specialEffect[TPeriod period.Period](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) + return SetFinetune[TPeriod](data.EffectParameter) case 0x3: // Set Vibrato Waveform - return SetVibratoWaveform(data.EffectParameter) + return SetVibratoWaveform[TPeriod](data.EffectParameter) case 0x4: // Set Tremolo Waveform - return SetTremoloWaveform(data.EffectParameter) + return SetTremoloWaveform[TPeriod](data.EffectParameter) case 0x5: // Set Panbrello Waveform - return SetPanbrelloWaveform(data.EffectParameter) + return SetPanbrelloWaveform[TPeriod](data.EffectParameter) case 0x6: // Fine Pattern Delay - return FinePatternDelay(data.EffectParameter) + return FinePatternDelay[TPeriod](data.EffectParameter) case 0x7: // special note operations - return specialNoteEffects(data) + return specialNoteEffects[TPeriod](data) case 0x8: // Set Coarse Pan Position - return SetCoarsePanPosition(data.EffectParameter) + return SetCoarsePanPosition[TPeriod](data.EffectParameter) case 0x9: // Sound Control - return soundControlEffect(data) + return soundControlEffect[TPeriod](data) case 0xA: // High Offset - return HighOffset(data.EffectParameter) + return HighOffset[TPeriod](data.EffectParameter) case 0xB: // Pattern Loop - return PatternLoop(data.EffectParameter) + return PatternLoop[TPeriod](data.EffectParameter) case 0xC: // Note Cut - return NoteCut(data.EffectParameter) + return NoteCut[TPeriod](data.EffectParameter) case 0xD: // Note Delay - return NoteDelay(data.EffectParameter) + return NoteDelay[TPeriod](data.EffectParameter) case 0xE: // Pattern Delay - return PatternDelay(data.EffectParameter) + return PatternDelay[TPeriod](data.EffectParameter) case 0xF: // Set Active Macro return nil // TODO: SetActiveMacro default: } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func specialNoteEffects(data channel.Data) EffectIT { +func specialNoteEffects[TPeriod period.Period](data channel.Data) EffectIT { switch data.EffectParameter & 0xf { case 0x0: // Past Note Cut - return PastNoteCut(data.EffectParameter) + return PastNoteCut[TPeriod](data.EffectParameter) case 0x1: // Past Note Off - return PastNoteOff(data.EffectParameter) + return PastNoteOff[TPeriod](data.EffectParameter) case 0x2: // Past Note Fade - return PastNoteFade(data.EffectParameter) + return PastNoteFade[TPeriod](data.EffectParameter) case 0x3: // New Note Action: Note Cut - return NewNoteActionNoteCut(data.EffectParameter) + return NewNoteActionNoteCut[TPeriod](data.EffectParameter) case 0x4: // New Note Action: Note Continue - return NewNoteActionNoteContinue(data.EffectParameter) + return NewNoteActionNoteContinue[TPeriod](data.EffectParameter) case 0x5: // New Note Action: Note Off - return NewNoteActionNoteOff(data.EffectParameter) + return NewNoteActionNoteOff[TPeriod](data.EffectParameter) case 0x6: // New Note Action: Note Fade - return NewNoteActionNoteFade(data.EffectParameter) + return NewNoteActionNoteFade[TPeriod](data.EffectParameter) case 0x7: // Volume Envelope Off - return VolumeEnvelopeOff(data.EffectParameter) + return VolumeEnvelopeOff[TPeriod](data.EffectParameter) case 0x8: // Volume Envelope On - return VolumeEnvelopeOn(data.EffectParameter) + return VolumeEnvelopeOn[TPeriod](data.EffectParameter) case 0x9: // Panning Envelope Off - return PanningEnvelopeOff(data.EffectParameter) + return PanningEnvelopeOff[TPeriod](data.EffectParameter) case 0xA: // Panning Envelope On - return PanningEnvelopeOn(data.EffectParameter) + return PanningEnvelopeOn[TPeriod](data.EffectParameter) case 0xB: // Pitch Envelope Off - return PitchEnvelopeOff(data.EffectParameter) + return PitchEnvelopeOff[TPeriod](data.EffectParameter) case 0xC: // Pitch Envelope On - return PitchEnvelopeOn(data.EffectParameter) + return PitchEnvelopeOn[TPeriod](data.EffectParameter) case 0xD, 0xE, 0xF: // unused return nil } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func volumeSlideFactory(mem *channel.Memory, cd channel.Command, ce channel.DataEffect) EffectIT { +func volumeSlideFactory[TPeriod period.Period](mem *channel.Memory, cd channel.Command, ce channel.DataEffect) EffectIT { x, y := mem.VolumeSlide(channel.DataEffect(ce)) switch { case x == 0: - return VolumeSlideDown(ce) + return VolumeSlideDown[TPeriod](ce) case y == 0: - return VolumeSlideUp(ce) + return VolumeSlideUp[TPeriod](ce) case x == 0x0f: - return FineVolumeSlideDown(ce) + return FineVolumeSlideDown[TPeriod](ce) case y == 0x0f: - return FineVolumeSlideUp(ce) + 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, @@ -225,7 +226,7 @@ func volumeSlideFactory(mem *channel.Memory, cd channel.Command, ce channel.Data return nil } -func soundControlEffect(data channel.Data) EffectIT { +func soundControlEffect[TPeriod period.Period](data channel.Data) EffectIT { switch data.EffectParameter & 0xF { case 0x0: // Surround Off case 0x1: // Surround On @@ -240,5 +241,5 @@ func soundControlEffect(data channel.Data) EffectIT { case 0xE: // Play Forward case 0xF: // Play Backward } - return UnhandledCommand{Command: data.Effect, Info: data.EffectParameter} + return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } diff --git a/format/it/effect/effectfactory_volume.go b/format/it/effect/effectfactory_volume.go index 6c0fd56..c400455 100644 --- a/format/it/effect/effectfactory_volume.go +++ b/format/it/effect/effectfactory_volume.go @@ -2,64 +2,65 @@ package effect import ( "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) -func volPanEffectFactory(mem *channel.Memory, v uint8) EffectIT { +func volPanEffectFactory[TPeriod period.Period](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) + return VolChanFineVolumeSlideUp[TPeriod](v - 0x41) case v >= 0x4b && v <= 0x54: // fine volume slide down - return VolChanFineVolumeSlideDown(v - 0x4b) + return VolChanFineVolumeSlideDown[TPeriod](v - 0x4b) case v >= 0x55 && v <= 0x5e: // volume slide up - return VolChanVolumeSlideUp(v - 0x55) + return VolChanVolumeSlideUp[TPeriod](v - 0x55) case v >= 0x5f && v <= 0x68: // volume slide down - return VolChanVolumeSlideDown(v - 0x5f) + return VolChanVolumeSlideDown[TPeriod](v - 0x5f) case v >= 0x69 && v <= 0x72: // portamento down - return volPortaDown(v - 0x69) + return volPortaDown[TPeriod](v - 0x69) case v >= 0x73 && v <= 0x7c: // portamento up - return volPortaUp(v - 0x73) + return volPortaUp[TPeriod](v - 0x73) case v >= 0x80 && v <= 0xc0: // set panning - return SetPanPosition(v - 0x80) + return SetPanPosition[TPeriod](v - 0x80) case v >= 0xc1 && v <= 0xca: // portamento to note - return volPortaToNote(v - 0xc1) + return volPortaToNote[TPeriod](v - 0xc1) case v >= 0xcb && v <= 0xd4: // vibrato - return Vibrato(v - 0xcb) + return Vibrato[TPeriod](v - 0xcb) } - return UnhandledVolCommand{Vol: v} + return UnhandledVolCommand[TPeriod]{Vol: v} } -func volPortaDown(v uint8) EffectIT { - return PortaDown(v * 4) +func volPortaDown[TPeriod period.Period](v uint8) EffectIT { + return PortaDown[TPeriod](v * 4) } -func volPortaUp(v uint8) EffectIT { - return PortaUp(v * 4) +func volPortaUp[TPeriod period.Period](v uint8) EffectIT { + return PortaUp[TPeriod](v * 4) } -func volPortaToNote(v uint8) EffectIT { +func volPortaToNote[TPeriod period.Period](v uint8) EffectIT { switch v { case 0: - return PortaToNote(0x00) + return PortaToNote[TPeriod](0x00) case 1: - return PortaToNote(0x01) + return PortaToNote[TPeriod](0x01) case 2: - return PortaToNote(0x04) + return PortaToNote[TPeriod](0x04) case 3: - return PortaToNote(0x08) + return PortaToNote[TPeriod](0x08) case 4: - return PortaToNote(0x10) + return PortaToNote[TPeriod](0x10) case 5: - return PortaToNote(0x20) + return PortaToNote[TPeriod](0x20) case 6: - return PortaToNote(0x40) + return PortaToNote[TPeriod](0x40) case 7: - return PortaToNote(0x60) + return PortaToNote[TPeriod](0x60) case 8: - return PortaToNote(0x80) + return PortaToNote[TPeriod](0x80) case 9: - return PortaToNote(0xFF) + return PortaToNote[TPeriod](0xFF) } // impossible, but hey... - return UnhandledVolCommand{Vol: v + 0xc1} + return UnhandledVolCommand[TPeriod]{Vol: v + 0xc1} } diff --git a/format/it/effect/unhandled.go b/format/it/effect/unhandled.go index 40cdc40..9827136 100644 --- a/format/it/effect/unhandled.go +++ b/format/it/effect/unhandled.go @@ -6,39 +6,40 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/channel" effectIntf "github.com/gotracker/playback/format/it/effect/intf" + "github.com/gotracker/playback/period" ) // UnhandledCommand is an unhandled command -type UnhandledCommand struct { +type UnhandledCommand[TPeriod period.Period] 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], m effectIntf.IT) error { +func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], 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 { +func (e UnhandledCommand[TPeriod]) String() string { return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) } // UnhandledVolCommand is an unhandled volume command -type UnhandledVolCommand struct { +type UnhandledVolCommand[TPeriod period.Period] struct { Vol uint8 } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.IT) error { +func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: volCmd:%0.2X", e.Vol)) } return nil } -func (e UnhandledVolCommand) String() string { +func (e UnhandledVolCommand[TPeriod]) String() string { return fmt.Sprintf("v%0.2x", e.Vol) } diff --git a/format/it/effect/util.go b/format/it/effect/util.go index f4d04ba..b12be34 100644 --- a/format/it/effect/util.go +++ b/format/it/effect/util.go @@ -2,18 +2,19 @@ package effect import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" + "github.com/gotracker/playback" "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" + itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/note" "github.com/heucuva/comparison" ) -func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier float32) error { +func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := itVolume.ToItVolume(av) vol := int16((float32(v) + delta) * multiplier) @@ -45,40 +46,44 @@ func doGlobalVolSlide(m effectIntf.IT, delta float32, multiplier float32) error return nil } -func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory], delta int) error { +func doPortaByDeltaAmiga(cs playback.Channel[itPeriod.Amiga, channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil } d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) + cur = cur.Add(d) cs.SetPeriod(cur) return nil } -func doPortaByDeltaLinear(cs playback.Channel[channel.Memory], delta int) error { +func doPortaByDeltaLinear(cs playback.Channel[itPeriod.Linear, channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil } finetune := period.PeriodDelta(delta) - cur = cur.AddDelta(finetune) + cur = cur.Add(finetune) cs.SetPeriod(cur) return nil } -func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, delta) + switch csp := any(cs).(type) { + case playback.Channel[itPeriod.Linear, channel.Memory]: + return doPortaByDeltaLinear(csp, delta) + case playback.Channel[itPeriod.Amiga, channel.Memory]: + return doPortaByDeltaAmiga(csp, -delta) + default: + panic("unhandled channel type") } - return doPortaByDeltaAmiga(cs, -delta) } -func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaUp(cs, amount, multiplier, linearFreqSlides); err != nil { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { + if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { return err } if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { @@ -87,16 +92,20 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multip return nil } -func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, -delta) + switch csp := any(cs).(type) { + case playback.Channel[itPeriod.Linear, channel.Memory]: + return doPortaByDeltaLinear(csp, -delta) + case playback.Channel[itPeriod.Amiga, channel.Memory]: + return doPortaByDeltaAmiga(csp, delta) + default: + panic("unhandled channel type") } - return doPortaByDeltaAmiga(cs, delta) } -func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaDown(cs, amount, multiplier, linearFreqSlides); err != nil { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { + if err := doPortaDown(cs, amount, multiplier); err != nil { return err } if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { @@ -105,7 +114,7 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, mult return nil } -func doVibrato(cs playback.Channel[channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], 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) @@ -113,7 +122,7 @@ func doVibrato(cs playback.Channel[channel.Memory], currentTick int, speed chann return nil } -func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, offTicks int) error { +func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -129,7 +138,7 @@ func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, return nil } -func doArpeggio(cs playback.Channel[channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -154,7 +163,7 @@ var ( } ) -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { +func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory]) error { vol := itVolume.ToItVolume(cs.GetActiveVolume()) if vol >= 0x10 && vol <= 0x50 { vol -= 0x10 @@ -173,13 +182,13 @@ func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { return nil } -func doTremolo(cs playback.Channel[channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], 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], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], 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/period/amiga.go b/format/it/period/amiga.go index ee50672..794cfe0 100644 --- a/format/it/period/amiga.go +++ b/format/it/period/amiga.go @@ -18,10 +18,15 @@ type Amiga struct { var _ period.Period = (*Amiga)(nil) // Add adds the current period to a delta value then returns the resulting period +func (p Amiga) Add(delta period.PeriodDelta) *Amiga { + p.Amiga += period.Amiga(delta) + return &p +} + +// AddDelta 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 += period.Amiga(d) - return p + return p.Add(d) } // Compare returns: diff --git a/format/it/period/linear.go b/format/it/period/linear.go index a8aa9e9..04f1070 100644 --- a/format/it/period/linear.go +++ b/format/it/period/linear.go @@ -15,9 +15,14 @@ type Linear struct { var _ period.Period = (*Linear)(nil) // Add adds the current period to a delta value then returns the resulting period -func (p Linear) AddDelta(delta period.Delta) period.Period { +func (p Linear) Add(delta period.PeriodDelta) *Linear { p.Linear = period.AddLinearDelta(p.Linear, delta) - return p + return &p +} + +// AddDelta adds the current period to a delta value then returns the resulting period +func (p Linear) AddDelta(delta period.Delta) period.Period { + return p.Add(period.ToPeriodDelta(delta)) } // Compare returns: diff --git a/format/it/period/util.go b/format/it/period/util.go index 9003332..35f11c2 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -27,53 +27,53 @@ const ( 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 { +func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c2spd period.Frequency) *TPeriod { if semi == note.UnchangedSemitone { panic("how?") } - if linearFreqSlides { + var result TPeriod + switch p := any(&result).(type) { + case *Linear: nft := int(semi)*SlideFinesPerNote + int(ft) - return Linear{ - Linear: period.Linear{ - Finetune: note.Finetune(nft), - CommonRate: c2spd, - }, - } - } + p.Finetune = note.Finetune(nft) + p.CommonRate = c2spd + case *Amiga: + key := int(semi.Key()) + octave := uint32(semi.Octave()) - key := int(semi.Key()) - octave := uint32(semi.Octave()) + if key >= len(semitonePeriodTable) { + return nil + } - if key >= len(semitonePeriodTable) { - return nil - } + if c2spd == 0 { + c2spd = period.Frequency(DefaultC2Spd) + } - if c2spd == 0 { - c2spd = period.Frequency(DefaultC2Spd) - } + if ft != 0 { + c2spd = CalcFinetuneC2Spd[Amiga](c2spd, ft) + } - if ft != 0 { - c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) + p.Amiga = period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)<> 4 if x != 0x0F && currentTick == 0 { diff --git a/format/s3m/effect/effect_notecut.go b/format/s3m/effect/effect_notecut.go index b44fc5c..cf58d1a 100644 --- a/format/s3m/effect/effect_notecut.go +++ b/format/s3m/effect/effect_notecut.go @@ -11,13 +11,13 @@ import ( 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], p playback.Playback) error { +func (e NoteCut) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e NoteCut) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e NoteCut) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { x := channel.DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { diff --git a/format/s3m/effect/effect_notedelay.go b/format/s3m/effect/effect_notedelay.go index 9a5ec27..4eb79ac 100644 --- a/format/s3m/effect/effect_notedelay.go +++ b/format/s3m/effect/effect_notedelay.go @@ -12,13 +12,13 @@ import ( type NoteDelay ChannelCommand // 'SDx' // PreStart triggers when the effect enters onto the channel state -func (e NoteDelay) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e NoteDelay) PreStart(cs S3MChannel, 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], p playback.Playback) error { +func (e NoteDelay) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_orderjump.go b/format/s3m/effect/effect_orderjump.go index 01de809..9834aee 100644 --- a/format/s3m/effect/effect_orderjump.go +++ b/format/s3m/effect/effect_orderjump.go @@ -12,13 +12,13 @@ import ( 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], p playback.Playback) error { +func (e OrderJump) Start(cs S3MChannel, 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], p playback.Playback, lastTick int) error { +func (e OrderJump) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } diff --git a/format/s3m/effect/effect_patterndelay.go b/format/s3m/effect/effect_patterndelay.go index 801bfc9..03af22a 100644 --- a/format/s3m/effect/effect_patterndelay.go +++ b/format/s3m/effect/effect_patterndelay.go @@ -12,13 +12,13 @@ import ( type PatternDelay ChannelCommand // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e PatternDelay) PreStart(cs S3MChannel, 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], p playback.Playback) error { +func (e PatternDelay) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_patternloop.go b/format/s3m/effect/effect_patternloop.go index f105195..64e37c4 100644 --- a/format/s3m/effect/effect_patternloop.go +++ b/format/s3m/effect/effect_patternloop.go @@ -11,13 +11,13 @@ import ( 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], p playback.Playback) error { +func (e PatternLoop) Start(cs S3MChannel, 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], p playback.Playback, lastTick int) error { +func (e PatternLoop) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { x := channel.DataEffect(e) & 0xF mem := cs.GetMemory() diff --git a/format/s3m/effect/effect_portadown.go b/format/s3m/effect/effect_portadown.go index ecd7328..8290ba1 100644 --- a/format/s3m/effect/effect_portadown.go +++ b/format/s3m/effect/effect_portadown.go @@ -11,14 +11,14 @@ import ( 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], p playback.Playback) error { +func (e PortaDown) Start(cs S3MChannel, 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], p playback.Playback, currentTick int) error { +func (e PortaDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.LastNonZero(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_portatonote.go b/format/s3m/effect/effect_portatonote.go index 4cade4c..147d695 100644 --- a/format/s3m/effect/effect_portatonote.go +++ b/format/s3m/effect/effect_portatonote.go @@ -14,7 +14,7 @@ import ( 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], p playback.Playback) error { +func (e PortaToNote) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,7 +25,7 @@ func (e PortaToNote) Start(cs playback.Channel[channel.Memory], p playback.Playb } // Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaToNote(channel.DataEffect(e)) @@ -34,7 +34,7 @@ func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playba if cur == nil { return nil } - cur = cur.AddDelta(cs.GetPeriodDelta()) + cur = cur.Add(cs.GetPeriodDelta()) ptp := cs.GetPortaTargetPeriod() if currentTick != 0 { if period.ComparePeriods(cur, ptp) == comparison.SpaceshipRightGreater { diff --git a/format/s3m/effect/effect_portaup.go b/format/s3m/effect/effect_portaup.go index b3fe2dc..42f3d4c 100644 --- a/format/s3m/effect/effect_portaup.go +++ b/format/s3m/effect/effect_portaup.go @@ -11,14 +11,14 @@ import ( 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], p playback.Playback) error { +func (e PortaUp) Start(cs S3MChannel, 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], p playback.Playback, currentTick int) error { +func (e PortaUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.LastNonZero(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_portavolslide.go b/format/s3m/effect/effect_portavolslide.go index 1c1d1cb..c95a073 100644 --- a/format/s3m/effect/effect_portavolslide.go +++ b/format/s3m/effect/effect_portavolslide.go @@ -5,11 +5,12 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/channel" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" ) // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[channel.Memory] + playback.CombinedEffect[s3mPeriod.Amiga, channel.Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/s3m/effect/effect_retrigvolslide.go b/format/s3m/effect/effect_retrigvolslide.go index fc7c4d9..727e731 100644 --- a/format/s3m/effect/effect_retrigvolslide.go +++ b/format/s3m/effect/effect_retrigvolslide.go @@ -13,13 +13,13 @@ import ( 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], p playback.Playback) error { +func (e RetrigVolumeSlide) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e RetrigVolumeSlide) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { x := channel.DataEffect(e) >> 4 y := channel.DataEffect(e) & 0x0F if y == 0 { diff --git a/format/s3m/effect/effect_rowjump.go b/format/s3m/effect/effect_rowjump.go index cc3a86f..6d2edb9 100644 --- a/format/s3m/effect/effect_rowjump.go +++ b/format/s3m/effect/effect_rowjump.go @@ -12,13 +12,13 @@ import ( 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], p playback.Playback) error { +func (e RowJump) Start(cs S3MChannel, 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], p playback.Playback, lastTick int) error { +func (e RowJump) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { r := channel.DataEffect(e) rowIdx := index.Row((r >> 4) * 10) rowIdx += index.Row(r & 0xf) diff --git a/format/s3m/effect/effect_sampleoffset.go b/format/s3m/effect/effect_sampleoffset.go index 0b629d6..9d82a2f 100644 --- a/format/s3m/effect/effect_sampleoffset.go +++ b/format/s3m/effect/effect_sampleoffset.go @@ -13,7 +13,7 @@ import ( 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], p playback.Playback) error { +func (e SampleOffset) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(channel.DataEffect(e)) diff --git a/format/s3m/effect/effect_setfinetune.go b/format/s3m/effect/effect_setfinetune.go index 7f7ecb5..8ea1fdb 100644 --- a/format/s3m/effect/effect_setfinetune.go +++ b/format/s3m/effect/effect_setfinetune.go @@ -12,7 +12,7 @@ import ( type SetFinetune ChannelCommand // 'S2x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetFinetune) PreStart(cs S3MChannel, p playback.Playback) error { x := channel.DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,7 +24,7 @@ func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Pl } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetFinetune) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_setglobalvolume.go b/format/s3m/effect/effect_setglobalvolume.go index 28d978d..c01416d 100644 --- a/format/s3m/effect/effect_setglobalvolume.go +++ b/format/s3m/effect/effect_setglobalvolume.go @@ -14,13 +14,13 @@ import ( type SetGlobalVolume ChannelCommand // 'V' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume) PreStart(cs S3MChannel, 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], p playback.Playback) error { +func (e SetGlobalVolume) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_setpanposition.go b/format/s3m/effect/effect_setpanposition.go index 8fcc366..e2a3a3a 100644 --- a/format/s3m/effect/effect_setpanposition.go +++ b/format/s3m/effect/effect_setpanposition.go @@ -12,7 +12,7 @@ import ( 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], p playback.Playback) error { +func (e SetPanPosition) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() x := uint8(e) & 0xf diff --git a/format/s3m/effect/effect_setspeed.go b/format/s3m/effect/effect_setspeed.go index ea942df..d647269 100644 --- a/format/s3m/effect/effect_setspeed.go +++ b/format/s3m/effect/effect_setspeed.go @@ -12,7 +12,7 @@ import ( type SetSpeed ChannelCommand // 'A' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetSpeed) PreStart(cs S3MChannel, p playback.Playback) error { if e != 0 { m := p.(effectIntf.S3M) if err := m.SetTicks(int(e)); err != nil { @@ -23,7 +23,7 @@ func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playb } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetSpeed) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/s3m/effect/effect_settempo.go b/format/s3m/effect/effect_settempo.go index a343cf5..785155b 100644 --- a/format/s3m/effect/effect_settempo.go +++ b/format/s3m/effect/effect_settempo.go @@ -12,7 +12,7 @@ import ( type SetTempo ChannelCommand // 'T' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetTempo) PreStart(cs S3MChannel, p playback.Playback) error { if e > 0x20 { m := p.(effectIntf.S3M) if err := m.SetTempo(int(e)); err != nil { @@ -23,13 +23,13 @@ func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playb } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetTempo) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e SetTempo) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { m := p.(effectIntf.S3M) switch channel.DataEffect(e >> 4) { case 0: // decrease tempo diff --git a/format/s3m/effect/effect_settremolowaveform.go b/format/s3m/effect/effect_settremolowaveform.go index f6997bb..6719677 100644 --- a/format/s3m/effect/effect_settremolowaveform.go +++ b/format/s3m/effect/effect_settremolowaveform.go @@ -13,7 +13,7 @@ import ( 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], p playback.Playback) error { +func (e SetTremoloWaveform) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/s3m/effect/effect_setvibratowaveform.go b/format/s3m/effect/effect_setvibratowaveform.go index 5b53d6c..e053544 100644 --- a/format/s3m/effect/effect_setvibratowaveform.go +++ b/format/s3m/effect/effect_setvibratowaveform.go @@ -13,7 +13,7 @@ import ( 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], p playback.Playback) error { +func (e SetVibratoWaveform) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf diff --git a/format/s3m/effect/effect_stereocontrol.go b/format/s3m/effect/effect_stereocontrol.go index 186ccd5..443e2ed 100644 --- a/format/s3m/effect/effect_stereocontrol.go +++ b/format/s3m/effect/effect_stereocontrol.go @@ -12,7 +12,7 @@ import ( 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], p playback.Playback) error { +func (e StereoControl) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() x := uint8(e) & 0xf diff --git a/format/s3m/effect/effect_tremolo.go b/format/s3m/effect/effect_tremolo.go index 38bfea2..1ae546d 100644 --- a/format/s3m/effect/effect_tremolo.go +++ b/format/s3m/effect/effect_tremolo.go @@ -11,13 +11,13 @@ import ( 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], p playback.Playback) error { +func (e Tremolo) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremolo) Tick(cs S3MChannel, 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. diff --git a/format/s3m/effect/effect_tremor.go b/format/s3m/effect/effect_tremor.go index 8a1b703..477017d 100644 --- a/format/s3m/effect/effect_tremor.go +++ b/format/s3m/effect/effect_tremor.go @@ -11,13 +11,13 @@ import ( 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], p playback.Playback) error { +func (e Tremor) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremor) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremor) Tick(cs S3MChannel, 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) diff --git a/format/s3m/effect/effect_vibrato.go b/format/s3m/effect/effect_vibrato.go index cf75611..f133ed4 100644 --- a/format/s3m/effect/effect_vibrato.go +++ b/format/s3m/effect/effect_vibrato.go @@ -11,14 +11,14 @@ import ( 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], p playback.Playback) error { +func (e Vibrato) Start(cs S3MChannel, 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], p playback.Playback, currentTick int) error { +func (e Vibrato) Tick(cs S3MChannel, 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. diff --git a/format/s3m/effect/effect_vibratovolslide.go b/format/s3m/effect/effect_vibratovolslide.go index 6b2bffb..d91637a 100644 --- a/format/s3m/effect/effect_vibratovolslide.go +++ b/format/s3m/effect/effect_vibratovolslide.go @@ -5,11 +5,12 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/channel" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" ) // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[channel.Memory] + playback.CombinedEffect[s3mPeriod.Amiga, channel.Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/s3m/effect/effect_volslidedown.go b/format/s3m/effect/effect_volslidedown.go index 7007c29..df835d9 100644 --- a/format/s3m/effect/effect_volslidedown.go +++ b/format/s3m/effect/effect_volslidedown.go @@ -11,13 +11,13 @@ import ( 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], p playback.Playback) error { +func (e VolumeSlideDown) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() y := channel.DataEffect(e) & 0x0F diff --git a/format/s3m/effect/effect_volslideup.go b/format/s3m/effect/effect_volslideup.go index 930ba7c..1ee00f4 100644 --- a/format/s3m/effect/effect_volslideup.go +++ b/format/s3m/effect/effect_volslideup.go @@ -11,13 +11,13 @@ import ( 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], p playback.Playback) error { +func (e VolumeSlideUp) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() x := channel.DataEffect(e) >> 4 diff --git a/format/s3m/effect/unhandled.go b/format/s3m/effect/unhandled.go index 2157eaa..0a41b58 100644 --- a/format/s3m/effect/unhandled.go +++ b/format/s3m/effect/unhandled.go @@ -3,7 +3,6 @@ package effect import ( "fmt" - "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/channel" effectIntf "github.com/gotracker/playback/format/s3m/effect/intf" ) @@ -15,7 +14,7 @@ type UnhandledCommand struct { } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.S3M) error { +func (e UnhandledCommand) PreStart(cs S3MChannel, m effectIntf.S3M) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } diff --git a/format/s3m/effect/util.go b/format/s3m/effect/util.go index c0e0ea0..6358eac 100644 --- a/format/s3m/effect/util.go +++ b/format/s3m/effect/util.go @@ -7,12 +7,15 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/channel" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/note" "github.com/heucuva/comparison" ) -func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier float32) error { +type S3MChannel = playback.Channel[s3mPeriod.Amiga, channel.Memory] + +func doVolSlide(cs S3MChannel, delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := s3mVolume.VolumeToS3M(av) vol := int16((float32(v) + delta) * multiplier) @@ -22,13 +25,13 @@ func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier f if vol < 0 { vol = 0 } - sv := s3mfile.Volume(channel.DataEffect(vol)) + sv := s3mfile.Volume(vol) nv := s3mVolume.VolumeFromS3M(sv) cs.SetActiveVolume(nv) return nil } -func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier float32) error { +func doPortaUp(cs S3MChannel, amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -36,12 +39,16 @@ func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier f delta := int(amount * multiplier) d := period.PeriodDelta(-delta) - cur = cur.AddDelta(d) + cur = cur.Add(d) cs.SetPeriod(cur) return nil } -func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period) error { +func doPortaUpToNote(cs S3MChannel, amount float32, multiplier float32, target *s3mPeriod.Amiga) error { + if target == nil { + return nil + } + cur := cs.GetPeriod() if cur == nil { return nil @@ -49,7 +56,7 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multip delta := int(amount * multiplier) d := period.PeriodDelta(-delta) - cur = cur.AddDelta(d) + cur = cur.Add(d) if period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { cur = target } @@ -57,7 +64,7 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multip return nil } -func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier float32) error { +func doPortaDown(cs S3MChannel, amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -65,12 +72,12 @@ func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier delta := int(amount * multiplier) d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) + cur = cur.Add(d) cs.SetPeriod(cur) return nil } -func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period) error { +func doPortaDownToNote(cs S3MChannel, amount float32, multiplier float32, target *s3mPeriod.Amiga) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -78,7 +85,7 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, mult delta := int(amount * multiplier) d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) + cur = cur.Add(d) if period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { cur = target } @@ -86,14 +93,14 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, mult return nil } -func doVibrato(cs playback.Channel[channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato(cs S3MChannel, 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], currentTick int, onTicks int, offTicks int) error { +func doTremor(cs S3MChannel, currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -109,7 +116,7 @@ func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, return nil } -func doArpeggio(cs playback.Channel[channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio(cs S3MChannel, currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -134,7 +141,7 @@ var ( } ) -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { +func doVolSlideTwoThirds(cs S3MChannel) error { vol := s3mVolume.VolumeToS3M(cs.GetActiveVolume()) if vol >= 64 { vol = 63 @@ -143,13 +150,13 @@ func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { return nil } -func doTremolo(cs playback.Channel[channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo(cs S3MChannel, 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], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable(cs S3MChannel, 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/period/amiga.go b/format/s3m/period/amiga.go index 9367f69..f0568e0 100644 --- a/format/s3m/period/amiga.go +++ b/format/s3m/period/amiga.go @@ -18,10 +18,15 @@ type Amiga struct { var _ period.Period = (*Amiga)(nil) // Add adds the current period to a delta value then returns the resulting period +func (p Amiga) Add(delta period.PeriodDelta) *Amiga { + p.Amiga += period.Amiga(delta) + return &p +} + +// AddDelta 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 += period.Amiga(d) - return p + return p.Add(d) } // Compare returns: diff --git a/format/s3m/period/util.go b/format/s3m/period/util.go index 063e823..2f356eb 100644 --- a/format/s3m/period/util.go +++ b/format/s3m/period/util.go @@ -24,7 +24,7 @@ const ( 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 { +func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequency) *Amiga { if semi == note.UnchangedSemitone { panic("how?") } @@ -44,7 +44,7 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ c2spd = CalcFinetuneC2Spd(c2spd, ft) } - return Amiga{ + return &Amiga{ Amiga: period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)<> 4 y := xx & 0x0F @@ -37,6 +38,6 @@ func (e PanSlide) Start(cs playback.Channel[channel.Memory], p playback.Playback return nil } -func (e PanSlide) String() string { +func (e PanSlide[TPeriod]) 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 index ad3561e..ad1565b 100644 --- a/format/xm/effect/effect_patterndelay.go +++ b/format/xm/effect/effect_patterndelay.go @@ -6,23 +6,24 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" effectIntf "github.com/gotracker/playback/format/xm/effect/intf" + "github.com/gotracker/playback/period" ) // PatternDelay defines a pattern delay effect -type PatternDelay channel.DataEffect // 'SEx' +type PatternDelay[TPeriod period.Period] channel.DataEffect // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback) error { +func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e PatternDelay) String() string { +func (e PatternDelay[TPeriod]) 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 index 97c6456..0c7370e 100644 --- a/format/xm/effect/effect_patternloop.go +++ b/format/xm/effect/effect_patternloop.go @@ -5,13 +5,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // PatternLoop defines a pattern loop effect -type PatternLoop channel.DataEffect // 'E6x' +type PatternLoop[TPeriod period.Period] 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], p playback.Playback) error { +func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xF @@ -35,6 +36,6 @@ func (e PatternLoop) Start(cs playback.Channel[channel.Memory], p playback.Playb return nil } -func (e PatternLoop) String() string { +func (e PatternLoop[TPeriod]) 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 index 223a560..382ef6b 100644 --- a/format/xm/effect/effect_portadown.go +++ b/format/xm/effect/effect_portadown.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // PortaDown defines a portamento down effect -type PortaDown channel.DataEffect // '2' +type PortaDown[TPeriod period.Period] 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], p playback.Playback) error { +func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaDown(channel.DataEffect(e)) @@ -26,9 +27,9 @@ func (e PortaDown) Tick(cs playback.Channel[channel.Memory], p playback.Playback return nil } - return doPortaDown(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) + return doPortaDown(cs, float32(xx), 4) } -func (e PortaDown) String() string { +func (e PortaDown[TPeriod]) 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 index 6a19524..3b1c29d 100644 --- a/format/xm/effect/effect_portatonote.go +++ b/format/xm/effect/effect_portatonote.go @@ -11,10 +11,10 @@ import ( ) // PortaToNote defines a portamento-to-note effect -type PortaToNote channel.DataEffect // '3' +type PortaToNote[TPeriod period.Period] 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], p playback.Playback) error { +func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,7 +25,7 @@ func (e PortaToNote) Start(cs playback.Channel[channel.Memory], p playback.Playb } // Tick is called on every tick -func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { if currentTick == 0 { return nil } @@ -36,12 +36,12 @@ func (e PortaToNote) Tick(cs playback.Channel[channel.Memory], p playback.Playba current := cs.GetPeriod() target := cs.GetPortaTargetPeriod() if period.ComparePeriods(current, target) == comparison.SpaceshipRightGreater { - return doPortaUpToNote(cs, float32(xx), 4, target, mem.Shared.LinearFreqSlides) // subtracts + return doPortaUpToNote(cs, float32(xx), 4, target) // subtracts } else { - return doPortaDownToNote(cs, float32(xx), 4, target, mem.Shared.LinearFreqSlides) // adds + return doPortaDownToNote(cs, float32(xx), 4, target) // adds } } -func (e PortaToNote) String() string { +func (e PortaToNote[TPeriod]) 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 index be9231e..8430dbf 100644 --- a/format/xm/effect/effect_portaup.go +++ b/format/xm/effect/effect_portaup.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // PortaUp defines a portamento up effect -type PortaUp channel.DataEffect // '1' +type PortaUp[TPeriod period.Period] 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], p playback.Playback) error { +func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaUp(channel.DataEffect(e)) @@ -26,9 +27,9 @@ func (e PortaUp) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return nil } - return doPortaUp(cs, float32(xx), 4, mem.Shared.LinearFreqSlides) + return doPortaUp(cs, float32(xx), 4) } -func (e PortaUp) String() string { +func (e PortaUp[TPeriod]) 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 index 953daf9..4c9d696 100644 --- a/format/xm/effect/effect_portavolslide.go +++ b/format/xm/effect/effect_portavolslide.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect -type PortaVolumeSlide struct { // '5' - playback.CombinedEffect[channel.Memory] +type PortaVolumeSlide[TPeriod period.Period] struct { // '5' + playback.CombinedEffect[TPeriod, channel.Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide(val channel.DataEffect) PortaVolumeSlide { - pvs := PortaVolumeSlide{} - pvs.Effects = append(pvs.Effects, VolumeSlide(val), PortaToNote(0x00)) +func NewPortaVolumeSlide[TPeriod period.Period](val channel.DataEffect) PortaVolumeSlide[TPeriod] { + pvs := PortaVolumeSlide[TPeriod]{} + pvs.Effects = append(pvs.Effects, VolumeSlide[TPeriod](val), PortaToNote[TPeriod](0x00)) return pvs } -func (e PortaVolumeSlide) String() string { - return fmt.Sprintf("5%0.2x", channel.DataEffect(e.Effects[0].(VolumeSlide))) +func (e PortaVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("5%0.2x", channel.DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) } diff --git a/format/xm/effect/effect_retriggernote.go b/format/xm/effect/effect_retriggernote.go index 6708e46..bf76c58 100644 --- a/format/xm/effect/effect_retriggernote.go +++ b/format/xm/effect/effect_retriggernote.go @@ -7,19 +7,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // RetriggerNote defines a retriggering effect -type RetriggerNote channel.DataEffect // 'E9x' +type RetriggerNote[TPeriod period.Period] 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], p playback.Playback) error { +func (e RetriggerNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetriggerNote) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { y := channel.DataEffect(e) & 0x0F if y == 0 { return nil @@ -34,6 +35,6 @@ func (e RetriggerNote) Tick(cs playback.Channel[channel.Memory], p playback.Play return nil } -func (e RetriggerNote) String() string { +func (e RetriggerNote[TPeriod]) 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 index dcdfdf1..4a37eb2 100644 --- a/format/xm/effect/effect_retrigvolslide.go +++ b/format/xm/effect/effect_retrigvolslide.go @@ -7,19 +7,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide channel.DataEffect // 'R' +type RetrigVolumeSlide[TPeriod period.Period] 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], p playback.Playback) error { +func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { x := channel.DataEffect(e) >> 4 y := channel.DataEffect(e) & 0x0F if y == 0 { @@ -66,6 +67,6 @@ func (e RetrigVolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback. return nil } -func (e RetrigVolumeSlide) String() string { +func (e RetrigVolumeSlide[TPeriod]) 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 index 0570903..b327e00 100644 --- a/format/xm/effect/effect_rowjump.go +++ b/format/xm/effect/effect_rowjump.go @@ -6,19 +6,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/index" + "github.com/gotracker/playback/period" ) // RowJump defines a row jump effect -type RowJump channel.DataEffect // 'D' +type RowJump[TPeriod period.Period] 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], p playback.Playback) error { +func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, lastTick int) error { +func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { xy := channel.DataEffect(e) x := xy >> 4 y := xy & 0x0f @@ -29,6 +30,6 @@ func (e RowJump) Stop(cs playback.Channel[channel.Memory], p playback.Playback, return p.SetNextRow(row) } -func (e RowJump) String() string { +func (e RowJump[TPeriod]) 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 index f9e1aad..8da5b62 100644 --- a/format/xm/effect/effect_sampleoffset.go +++ b/format/xm/effect/effect_sampleoffset.go @@ -7,13 +7,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // SampleOffset defines a sample offset effect -type SampleOffset channel.DataEffect // '9' +type SampleOffset[TPeriod period.Period] 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], p playback.Playback) error { +func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(channel.DataEffect(e)) @@ -21,6 +22,6 @@ func (e SampleOffset) Start(cs playback.Channel[channel.Memory], p playback.Play return nil } -func (e SampleOffset) String() string { +func (e SampleOffset[TPeriod]) 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 index a4c38b8..1002158 100644 --- a/format/xm/effect/effect_setcoarsepanposition.go +++ b/format/xm/effect/effect_setcoarsepanposition.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" xmPanning "github.com/gotracker/playback/format/xm/panning" + "github.com/gotracker/playback/period" ) // SetCoarsePanPosition defines a set pan position effect -type SetCoarsePanPosition channel.DataEffect // 'E8x' +type SetCoarsePanPosition[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xy := channel.DataEffect(e) @@ -22,6 +23,6 @@ func (e SetCoarsePanPosition) Start(cs playback.Channel[channel.Memory], p playb return nil } -func (e SetCoarsePanPosition) String() string { +func (e SetCoarsePanPosition[TPeriod]) 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 index 87b6b25..4cd0ca6 100644 --- a/format/xm/effect/effect_setenvelopeposition.go +++ b/format/xm/effect/effect_setenvelopeposition.go @@ -5,13 +5,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // SetEnvelopePosition defines a set envelope position effect -type SetEnvelopePosition channel.DataEffect // 'Lxx' +type SetEnvelopePosition[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetEnvelopePosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := channel.DataEffect(e) @@ -20,6 +21,6 @@ func (e SetEnvelopePosition) Start(cs playback.Channel[channel.Memory], p playba return nil } -func (e SetEnvelopePosition) String() string { +func (e SetEnvelopePosition[TPeriod]) 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 index 290f918..901172b 100644 --- a/format/xm/effect/effect_setfinetune.go +++ b/format/xm/effect/effect_setfinetune.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) // SetFinetune defines a mod-style set finetune effect -type SetFinetune channel.DataEffect // 'E5x' +type SetFinetune[TPeriod period.Period] channel.DataEffect // 'E5x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { x := channel.DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,11 +25,11 @@ func (e SetFinetune) PreStart(cs playback.Channel[channel.Memory], p playback.Pl } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e SetFinetune) String() string { +func (e SetFinetune[TPeriod]) 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 index 458a36b..3cbb3bd 100644 --- a/format/xm/effect/effect_setglobalvolume.go +++ b/format/xm/effect/effect_setglobalvolume.go @@ -6,24 +6,25 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" ) // SetGlobalVolume defines a set global volume effect -type SetGlobalVolume channel.DataEffect // 'G' +type SetGlobalVolume[TPeriod period.Period] channel.DataEffect // 'G' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e SetGlobalVolume) String() string { +func (e SetGlobalVolume[TPeriod]) 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 index f10f62b..6b0fd47 100644 --- a/format/xm/effect/effect_setpanposition.go +++ b/format/xm/effect/effect_setpanposition.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" xmPanning "github.com/gotracker/playback/format/xm/panning" + "github.com/gotracker/playback/period" ) // SetPanPosition defines a set pan position effect -type SetPanPosition channel.DataEffect // '8xx' +type SetPanPosition[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := uint8(e) @@ -21,6 +22,6 @@ func (e SetPanPosition) Start(cs playback.Channel[channel.Memory], p playback.Pl return nil } -func (e SetPanPosition) String() string { +func (e SetPanPosition[TPeriod]) 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 index 75fef6b..88ae67a 100644 --- a/format/xm/effect/effect_setspeed.go +++ b/format/xm/effect/effect_setspeed.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" effectIntf "github.com/gotracker/playback/format/xm/effect/intf" + "github.com/gotracker/playback/period" ) // SetSpeed defines a set speed effect -type SetSpeed channel.DataEffect // 'F' +type SetSpeed[TPeriod period.Period] channel.DataEffect // 'F' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { if e != 0 { m := p.(effectIntf.XM) if err := m.SetTicks(int(e)); err != nil { @@ -23,11 +24,11 @@ func (e SetSpeed) PreStart(cs playback.Channel[channel.Memory], p playback.Playb } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } -func (e SetSpeed) String() string { +func (e SetSpeed[TPeriod]) 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 index 4c83cb2..d3760fa 100644 --- a/format/xm/effect/effect_settempo.go +++ b/format/xm/effect/effect_settempo.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" effectIntf "github.com/gotracker/playback/format/xm/effect/intf" + "github.com/gotracker/playback/period" ) // SetTempo defines a set tempo effect -type SetTempo channel.DataEffect // 'F' +type SetTempo[TPeriod period.Period] channel.DataEffect // 'F' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { if e > 0x20 { m := p.(effectIntf.XM) if err := m.SetTempo(int(e)); err != nil { @@ -23,17 +24,17 @@ func (e SetTempo) PreStart(cs playback.Channel[channel.Memory], p playback.Playb } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo) Start(cs playback.Channel[channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { m := p.(effectIntf.XM) return m.SetTempo(int(e)) } -func (e SetTempo) String() string { +func (e SetTempo[TPeriod]) 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 index 63e4cd6..9f58ab5 100644 --- a/format/xm/effect/effect_settremolowaveform.go +++ b/format/xm/effect/effect_settremolowaveform.go @@ -3,6 +3,7 @@ package effect import ( "fmt" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" @@ -10,10 +11,10 @@ import ( ) // SetTremoloWaveform defines a set tremolo waveform effect -type SetTremoloWaveform channel.DataEffect // 'E7x' +type SetTremoloWaveform[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -24,6 +25,6 @@ func (e SetTremoloWaveform) Start(cs playback.Channel[channel.Memory], p playbac return nil } -func (e SetTremoloWaveform) String() string { +func (e SetTremoloWaveform[TPeriod]) 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 index 3729af9..d37eadf 100644 --- a/format/xm/effect/effect_setvibratowaveform.go +++ b/format/xm/effect/effect_setvibratowaveform.go @@ -3,6 +3,7 @@ package effect import ( "fmt" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" @@ -10,10 +11,10 @@ import ( ) // SetVibratoWaveform defines a set vibrato waveform effect -type SetVibratoWaveform channel.DataEffect // 'E4x' +type SetVibratoWaveform[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() x := channel.DataEffect(e) & 0xf @@ -24,6 +25,6 @@ func (e SetVibratoWaveform) Start(cs playback.Channel[channel.Memory], p playbac return nil } -func (e SetVibratoWaveform) String() string { +func (e SetVibratoWaveform[TPeriod]) 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 index 40ee961..11bc16b 100644 --- a/format/xm/effect/effect_setvolume.go +++ b/format/xm/effect/effect_setvolume.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" ) // SetVolume defines a volume slide effect -type SetVolume channel.DataEffect // 'C' +type SetVolume[TPeriod period.Period] 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], p playback.Playback) error { +func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := xmVolume.XmVolume(e) @@ -21,6 +22,6 @@ func (e SetVolume) Start(cs playback.Channel[channel.Memory], p playback.Playbac return nil } -func (e SetVolume) String() string { +func (e SetVolume[TPeriod]) 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 index 21b8c22..1d41bf3 100644 --- a/format/xm/effect/effect_tremolo.go +++ b/format/xm/effect/effect_tremolo.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // Tremolo defines a tremolo effect -type Tremolo channel.DataEffect // '7' +type Tremolo[TPeriod period.Period] 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], p playback.Playback) error { +func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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. @@ -25,6 +26,6 @@ func (e Tremolo) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return doTremolo(cs, currentTick, x, y, 4) } -func (e Tremolo) String() string { +func (e Tremolo[TPeriod]) 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 index 06adc03..2355765 100644 --- a/format/xm/effect/effect_tremor.go +++ b/format/xm/effect/effect_tremor.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // Tremor defines a tremor effect -type Tremor channel.DataEffect // 'T' +type Tremor[TPeriod period.Period] 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], p playback.Playback) error { +func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremor) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { if currentTick != 0 { mem := cs.GetMemory() x, y := mem.Tremor(channel.DataEffect(e)) @@ -26,6 +27,6 @@ func (e Tremor) Tick(cs playback.Channel[channel.Memory], p playback.Playback, c return nil } -func (e Tremor) String() string { +func (e Tremor[TPeriod]) 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 index bdad63c..53f91ef 100644 --- a/format/xm/effect/effect_vibrato.go +++ b/format/xm/effect/effect_vibrato.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // Vibrato defines a vibrato effect -type Vibrato channel.DataEffect // '4' +type Vibrato[TPeriod period.Period] 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], p playback.Playback) error { +func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], 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], p playback.Playback, currentTick int) error { +func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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. @@ -26,6 +27,6 @@ func (e Vibrato) Tick(cs playback.Channel[channel.Memory], p playback.Playback, return doVibrato(cs, currentTick, x, y, 4) } -func (e Vibrato) String() string { +func (e Vibrato[TPeriod]) 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 index 6dcbfda..f5ceebe 100644 --- a/format/xm/effect/effect_vibratovolslide.go +++ b/format/xm/effect/effect_vibratovolslide.go @@ -5,20 +5,21 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // VibratoVolumeSlide defines a combination vibrato and volume slide effect -type VibratoVolumeSlide struct { // '6' - playback.CombinedEffect[channel.Memory] +type VibratoVolumeSlide[TPeriod period.Period] struct { // '6' + playback.CombinedEffect[TPeriod, channel.Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide(val channel.DataEffect) VibratoVolumeSlide { - vvs := VibratoVolumeSlide{} - vvs.Effects = append(vvs.Effects, VolumeSlide(val), Vibrato(0x00)) +func NewVibratoVolumeSlide[TPeriod period.Period](val channel.DataEffect) VibratoVolumeSlide[TPeriod] { + vvs := VibratoVolumeSlide[TPeriod]{} + vvs.Effects = append(vvs.Effects, VolumeSlide[TPeriod](val), Vibrato[TPeriod](0x00)) return vvs } -func (e VibratoVolumeSlide) String() string { - return fmt.Sprintf("6%0.2x", channel.DataEffect(e.Effects[0].(VolumeSlide))) +func (e VibratoVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("6%0.2x", channel.DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) } diff --git a/format/xm/effect/effect_volslide.go b/format/xm/effect/effect_volslide.go index 38b4ef4..8ecc8a3 100644 --- a/format/xm/effect/effect_volslide.go +++ b/format/xm/effect/effect_volslide.go @@ -5,19 +5,20 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) // VolumeSlide defines a volume slide effect -type VolumeSlide channel.DataEffect // 'A' +type VolumeSlide[TPeriod period.Period] 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], p playback.Playback) error { +func (e VolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.VolumeSlide(channel.DataEffect(e)) @@ -35,6 +36,6 @@ func (e VolumeSlide) Tick(cs playback.Channel[channel.Memory], p playback.Playba return nil } -func (e VolumeSlide) String() string { +func (e VolumeSlide[TPeriod]) 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 index 752ca65..9f9bc0f 100644 --- a/format/xm/effect/effectfactory.go +++ b/format/xm/effect/effectfactory.go @@ -5,18 +5,19 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/channel" + "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 struct { - playback.CombinedEffect[channel.Memory] +type VolEff[TPeriod period.Period] struct { + playback.CombinedEffect[TPeriod, channel.Memory] eff EffectXM } -func (e VolEff) String() string { +func (e VolEff[TPeriod]) String() string { if e.eff == nil { return "..." } @@ -24,7 +25,7 @@ func (e VolEff) String() string { } // Factory produces an effect for the provided channel pattern data -func Factory(mem *channel.Memory, data song.ChannelData) EffectXM { +func Factory[TPeriod period.Period](mem *channel.Memory, data song.ChannelData) EffectXM { if data == nil { return nil } @@ -34,15 +35,15 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectXM { return nil } - eff := VolEff{} + var eff VolEff[TPeriod] if d.What.HasVolume() { - ve := volumeEffectFactory(mem, d.Volume) + ve := volumeEffectFactory[TPeriod](mem, d.Volume) if ve != nil { eff.Effects = append(eff.Effects, ve) } } - if e := standardEffectFactory(mem, d); e != nil { + if e := standardEffectFactory[TPeriod](mem, d); e != nil { eff.Effects = append(eff.Effects, e) eff.eff = e } diff --git a/format/xm/effect/effectfactory_standard.go b/format/xm/effect/effectfactory_standard.go index 04e96b4..0c2e884 100644 --- a/format/xm/effect/effectfactory_standard.go +++ b/format/xm/effect/effectfactory_standard.go @@ -2,114 +2,115 @@ package effect import ( "github.com/gotracker/playback/format/xm/channel" + "github.com/gotracker/playback/period" ) -func standardEffectFactory(mem *channel.Memory, cd channel.Data) EffectXM { +func standardEffectFactory[TPeriod period.Period](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) + return Arpeggio[TPeriod](cd.EffectParameter) case 0x01: // Porta up - return PortaUp(cd.EffectParameter) + return PortaUp[TPeriod](cd.EffectParameter) case 0x02: // Porta down - return PortaDown(cd.EffectParameter) + return PortaDown[TPeriod](cd.EffectParameter) case 0x03: // Tone porta - return PortaToNote(cd.EffectParameter) + return PortaToNote[TPeriod](cd.EffectParameter) case 0x04: // Vibrato - return Vibrato(cd.EffectParameter) + return Vibrato[TPeriod](cd.EffectParameter) case 0x05: // Tone porta + Volume slide - return NewPortaVolumeSlide(cd.EffectParameter) + return NewPortaVolumeSlide[TPeriod](cd.EffectParameter) case 0x06: // Vibrato + Volume slide - return NewVibratoVolumeSlide(cd.EffectParameter) + return NewVibratoVolumeSlide[TPeriod](cd.EffectParameter) case 0x07: // Tremolo - return Tremolo(cd.EffectParameter) + return Tremolo[TPeriod](cd.EffectParameter) case 0x08: // Set (fine) panning - return SetPanPosition(cd.EffectParameter) + return SetPanPosition[TPeriod](cd.EffectParameter) case 0x09: // Sample offset - return SampleOffset(cd.EffectParameter) + return SampleOffset[TPeriod](cd.EffectParameter) case 0x0A: // Volume slide - return VolumeSlide(cd.EffectParameter) + return VolumeSlide[TPeriod](cd.EffectParameter) case 0x0B: // Position jump - return OrderJump(cd.EffectParameter) + return OrderJump[TPeriod](cd.EffectParameter) case 0x0C: // Set volume - return SetVolume(cd.EffectParameter) + return SetVolume[TPeriod](cd.EffectParameter) case 0x0D: // Pattern break - return RowJump(cd.EffectParameter) + return RowJump[TPeriod](cd.EffectParameter) case 0x0E: // extra... - return specialEffectFactory(mem, cd.Effect, cd.EffectParameter) + return specialEffectFactory[TPeriod](mem, cd.Effect, cd.EffectParameter) case 0x0F: // Set tempo/BPM if cd.EffectParameter < 0x20 { - return SetSpeed(cd.EffectParameter) + return SetSpeed[TPeriod](cd.EffectParameter) } - return SetTempo(cd.EffectParameter) + return SetTempo[TPeriod](cd.EffectParameter) case 0x10: // Set global volume - return SetGlobalVolume(cd.EffectParameter) + return SetGlobalVolume[TPeriod](cd.EffectParameter) case 0x11: // Global volume slide - return GlobalVolumeSlide(cd.EffectParameter) + return GlobalVolumeSlide[TPeriod](cd.EffectParameter) case 0x15: // Set envelope position - return SetEnvelopePosition(cd.EffectParameter) + return SetEnvelopePosition[TPeriod](cd.EffectParameter) case 0x19: // Panning slide - return PanSlide(cd.EffectParameter) + return PanSlide[TPeriod](cd.EffectParameter) case 0x1B: // Multi retrig note - return RetrigVolumeSlide(cd.EffectParameter) + return RetrigVolumeSlide[TPeriod](cd.EffectParameter) case 0x1D: // Tremor - return Tremor(cd.EffectParameter) + return Tremor[TPeriod](cd.EffectParameter) case 0x21: // Extra fine porta commands - return extraFinePortaEffectFactory(mem, cd.Effect, cd.EffectParameter) + return extraFinePortaEffectFactory[TPeriod](mem, cd.Effect, cd.EffectParameter) } - return UnhandledCommand{Command: cd.Effect, Info: cd.EffectParameter} + return UnhandledCommand[TPeriod]{Command: cd.Effect, Info: cd.EffectParameter} } -func extraFinePortaEffectFactory(mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { +func extraFinePortaEffectFactory[TPeriod period.Period](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) + return ExtraFinePortaUp[TPeriod](cp) case 0x2: // Extra fine porta down - return ExtraFinePortaDown(cp) + return ExtraFinePortaDown[TPeriod](cp) } - return UnhandledCommand{Command: ce, Info: cp} + return UnhandledCommand[TPeriod]{Command: ce, Info: cp} } -func specialEffectFactory(mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { +func specialEffectFactory[TPeriod period.Period](mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { switch cp >> 4 { case 0x1: // Fine porta up - return FinePortaUp(cp) + return FinePortaUp[TPeriod](cp) case 0x2: // Fine porta down - return FinePortaDown(cp) + return FinePortaDown[TPeriod](cp) //case 0x3: // Set glissando control case 0x4: // Set vibrato control - return SetVibratoWaveform(cp) + return SetVibratoWaveform[TPeriod](cp) case 0x5: // Set finetune - return SetFinetune(cp) + return SetFinetune[TPeriod](cp) case 0x6: // Set loop begin/loop - return PatternLoop(cp) + return PatternLoop[TPeriod](cp) case 0x7: // Set tremolo control - return SetTremoloWaveform(cp) + return SetTremoloWaveform[TPeriod](cp) case 0x8: // Set coarse panning - return SetCoarsePanPosition(cp) + return SetCoarsePanPosition[TPeriod](cp) case 0x9: // Retrig note - return RetriggerNote(cp) + return RetriggerNote[TPeriod](cp) case 0xA: // Fine volume slide up - return FineVolumeSlideUp(cp) + return FineVolumeSlideUp[TPeriod](cp) case 0xB: // Fine volume slide down - return FineVolumeSlideDown(cp) + return FineVolumeSlideDown[TPeriod](cp) case 0xC: // Note cut - return NoteCut(cp) + return NoteCut[TPeriod](cp) case 0xD: // Note delay - return NoteDelay(cp) + return NoteDelay[TPeriod](cp) case 0xE: // Pattern delay - return PatternDelay(cp) + return PatternDelay[TPeriod](cp) } - return UnhandledCommand{Command: ce, Info: cp} + return UnhandledCommand[TPeriod]{Command: ce, Info: cp} } diff --git a/format/xm/effect/effectfactory_volume.go b/format/xm/effect/effectfactory_volume.go index 4a57234..4a2ff58 100644 --- a/format/xm/effect/effectfactory_volume.go +++ b/format/xm/effect/effectfactory_volume.go @@ -3,9 +3,10 @@ package effect 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 *channel.Memory, v xmVolume.VolEffect) EffectXM { switch { case v <= 0x0f: // nothing return nil @@ -13,27 +14,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) 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 | (channel.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/effect/unhandled.go b/format/xm/effect/unhandled.go index 6edbef5..ccb84ea 100644 --- a/format/xm/effect/unhandled.go +++ b/format/xm/effect/unhandled.go @@ -7,39 +7,40 @@ import ( "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/period" ) // UnhandledCommand is an unhandled command -type UnhandledCommand struct { +type UnhandledCommand[TPeriod period.Period] 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], m effectIntf.XM) error { +func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } return nil } -func (e UnhandledCommand) String() string { +func (e UnhandledCommand[TPeriod]) String() string { return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) } // UnhandledVolCommand is an unhandled volume command -type UnhandledVolCommand struct { +type UnhandledVolCommand[TPeriod period.Period] struct { Vol xmVolume.VolEffect } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand) PreStart(cs playback.Channel[channel.Memory], m effectIntf.XM) error { +func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } return nil } -func (e UnhandledVolCommand) String() string { +func (e UnhandledVolCommand[TPeriod]) String() string { return fmt.Sprintf("v%0.2x", e.Vol) } diff --git a/format/xm/effect/util.go b/format/xm/effect/util.go index a97844f..ac366f3 100644 --- a/format/xm/effect/util.go +++ b/format/xm/effect/util.go @@ -1,18 +1,19 @@ package effect import ( + "github.com/gotracker/playback" "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" + xmPeriod "github.com/gotracker/playback/format/xm/period" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/note" "github.com/heucuva/comparison" ) -func doVolSlide(cs playback.Channel[channel.Memory], delta float32, multiplier float32) error { +func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := xmVolume.ToVolumeXM(av) vol := int16((float32(v) + delta) * multiplier) @@ -42,40 +43,44 @@ func doGlobalVolSlide(m effectIntf.XM, delta float32, multiplier float32) error return nil } -func doPortaByDeltaAmiga(cs playback.Channel[channel.Memory], delta int) error { +func doPortaByDeltaAmiga(cs playback.Channel[xmPeriod.Amiga, channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil } d := period.PeriodDelta(delta) - cur = cur.AddDelta(d) + cur = cur.Add(d) cs.SetPeriod(cur) return nil } -func doPortaByDeltaLinear(cs playback.Channel[channel.Memory], delta int) error { +func doPortaByDeltaLinear(cs playback.Channel[xmPeriod.Linear, channel.Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil } finetune := period.PeriodDelta(delta) - cur = cur.AddDelta(finetune) + cur = cur.Add(finetune) cs.SetPeriod(cur) return nil } -func doPortaUp(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, delta) + switch csp := any(cs).(type) { + case playback.Channel[xmPeriod.Linear, channel.Memory]: + return doPortaByDeltaLinear(csp, delta) + case playback.Channel[xmPeriod.Amiga, channel.Memory]: + return doPortaByDeltaAmiga(csp, -delta) + default: + panic("unhandled channel type") } - return doPortaByDeltaAmiga(cs, -delta) } -func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaUp(cs, amount, multiplier, linearFreqSlides); err != nil { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { + if err := doPortaUp(cs, amount, multiplier); err != nil { return err } if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { @@ -84,16 +89,20 @@ func doPortaUpToNote(cs playback.Channel[channel.Memory], amount float32, multip return nil } -func doPortaDown(cs playback.Channel[channel.Memory], amount float32, multiplier float32, linearFreqSlides bool) error { +func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) - if linearFreqSlides { - return doPortaByDeltaLinear(cs, -delta) + switch csp := any(cs).(type) { + case playback.Channel[xmPeriod.Linear, channel.Memory]: + return doPortaByDeltaLinear(csp, -delta) + case playback.Channel[xmPeriod.Amiga, channel.Memory]: + return doPortaByDeltaAmiga(csp, delta) + default: + panic("unhandled channel type") } - return doPortaByDeltaAmiga(cs, delta) } -func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, multiplier float32, target period.Period, linearFreqSlides bool) error { - if err := doPortaDown(cs, amount, multiplier, linearFreqSlides); err != nil { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { + if err := doPortaDown(cs, amount, multiplier); err != nil { return err } if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { @@ -102,7 +111,7 @@ func doPortaDownToNote(cs playback.Channel[channel.Memory], amount float32, mult return nil } -func doVibrato(cs playback.Channel[channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], 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) @@ -110,7 +119,7 @@ func doVibrato(cs playback.Channel[channel.Memory], currentTick int, speed chann return nil } -func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, offTicks int) error { +func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -126,7 +135,7 @@ func doTremor(cs playback.Channel[channel.Memory], currentTick int, onTicks int, return nil } -func doArpeggio(cs playback.Channel[channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -151,7 +160,7 @@ var ( } ) -func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { +func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory]) error { vol := xmVolume.ToVolumeXM(cs.GetActiveVolume()) if vol >= 64 { vol = 63 @@ -166,13 +175,13 @@ func doVolSlideTwoThirds(cs playback.Channel[channel.Memory]) error { return nil } -func doTremolo(cs playback.Channel[channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], 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], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], 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/load/xmformat.go b/format/xm/load/xmformat.go index fb466b3..9c812ed 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -187,7 +187,11 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid } if si.Finetune != 0 { - sample.C2Spd = xmPeriod.CalcFinetuneC2Spd(xmPeriod.DefaultC2Spd, note.Finetune(si.Finetune), linearFrequencySlides) + if linearFrequencySlides { + sample.C2Spd = xmPeriod.CalcFinetuneC2Spd[xmPeriod.Linear](xmPeriod.DefaultC2Spd, note.Finetune(si.Finetune)) + } else { + sample.C2Spd = xmPeriod.CalcFinetuneC2Spd[xmPeriod.Amiga](xmPeriod.DefaultC2Spd, note.Finetune(si.Finetune)) + } } if sample.C2Spd == 0 { sample.C2Spd = period.Frequency(xmPeriod.DefaultC2Spd) diff --git a/format/xm/period/amiga.go b/format/xm/period/amiga.go index 7d0ca21..52fbc83 100644 --- a/format/xm/period/amiga.go +++ b/format/xm/period/amiga.go @@ -18,10 +18,15 @@ type Amiga struct { var _ period.Period = (*Amiga)(nil) // Add adds the current period to a delta value then returns the resulting period +func (p Amiga) Add(delta period.PeriodDelta) *Amiga { + p.Amiga += period.Amiga(delta) + return &p +} + +// AddDelta 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 += period.Amiga(d) - return p + return p.Add(d) } // Compare returns: diff --git a/format/xm/period/linear.go b/format/xm/period/linear.go index 86709a6..6b1c6cf 100644 --- a/format/xm/period/linear.go +++ b/format/xm/period/linear.go @@ -15,9 +15,14 @@ type Linear struct { var _ period.Period = (*Linear)(nil) // Add adds the current period to a delta value then returns the resulting period -func (p Linear) AddDelta(delta period.Delta) period.Period { +func (p Linear) Add(delta period.PeriodDelta) *Linear { p.Linear = period.AddLinearDelta(p.Linear, delta) - return p + return &p +} + +// AddDelta adds the current period to a delta value then returns the resulting period +func (p Linear) AddDelta(delta period.Delta) period.Period { + return p.Add(period.ToPeriodDelta(delta)) } // Compare returns: diff --git a/format/xm/period/util.go b/format/xm/period/util.go index 79a41bb..cc1710b 100644 --- a/format/xm/period/util.go +++ b/format/xm/period/util.go @@ -27,53 +27,54 @@ const ( 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 { +func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c2spd period.Frequency) *TPeriod { if semi == note.UnchangedSemitone { panic("how?") } - if linearFreqSlides { - nft := int(semi)*64 + int(ft) - return Linear{ - Linear: period.Linear{ - Finetune: note.Finetune(nft), - CommonRate: c2spd, - }, - } - } - key := int(semi.Key()) - octave := uint32(semi.Octave()) + var result TPeriod + switch p := any(&result).(type) { + case *Linear: + nft := int(semi)*SlideFinesPerNote + int(ft) + p.Finetune = note.Finetune(nft) + p.CommonRate = c2spd + case *Amiga: + key := int(semi.Key()) + octave := uint32(semi.Octave()) + + if key >= len(semitonePeriodTable) { + return nil + } - if key >= len(semitonePeriodTable) { - return nil - } + if c2spd == 0 { + c2spd = period.Frequency(DefaultC2Spd) + } - if c2spd == 0 { - c2spd = period.Frequency(DefaultC2Spd) - } + if ft != 0 { + c2spd = CalcFinetuneC2Spd[Amiga](c2spd, ft) + } - if ft != 0 { - c2spd = CalcFinetuneC2Spd(c2spd, ft, linearFreqSlides) + p.Amiga = period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)< 0 { - d := ToPeriodDelta(delta) - p.Finetune += note.Finetune(d) + p.Finetune += note.Finetune(delta) if p.Finetune < 1 { p.Finetune = 1 } diff --git a/period/period.go b/period/period.go index 7db6a9b..f39cc6f 100644 --- a/period/period.go +++ b/period/period.go @@ -33,7 +33,7 @@ func ToPeriodDelta(delta Delta) PeriodDelta { } // ComparePeriods compares two periods, taking nil into account -func ComparePeriods(lhs Period, rhs Period) comparison.Spaceship { +func ComparePeriods[TPeriod Period](lhs *TPeriod, rhs *TPeriod) comparison.Spaceship { if lhs == nil { if rhs == nil { return comparison.SpaceshipEqual @@ -43,5 +43,5 @@ func ComparePeriods(lhs Period, rhs Period) comparison.Spaceship { return comparison.SpaceshipLeftGreater } - return lhs.Compare(rhs) + return (*lhs).Compare(*rhs) } diff --git a/player/state/active.go b/player/state/active.go index 0894192..a30bab7 100644 --- a/player/state/active.go +++ b/player/state/active.go @@ -11,14 +11,14 @@ import ( ) // Active is the active state of a channel -type Active struct { - Playback +type Active[TPeriod period.Period] struct { + Playback[TPeriod] Voice voice.Voice PeriodDelta period.PeriodDelta } // Reset sets the active state to defaults -func (a *Active) Reset() { +func (a *Active[TPeriod]) Reset() { if v := a.Voice; v != nil { v.Release() a.Voice = nil @@ -28,8 +28,8 @@ func (a *Active) Reset() { } // Clone clones the active state so that various interfaces do not collide -func (a *Active) Clone() *Active { - var c Active = *a +func (a *Active[TPeriod]) Clone() *Active[TPeriod] { + var c Active[TPeriod] = *a if a.Voice != nil { c.Voice = a.Voice.Clone() } @@ -38,10 +38,10 @@ func (a *Active) Clone() *Active { } // Transitions the active state so that various interfaces do not collide -func (a *Active) Transition() *Active { - var c *Active +func (a *Active[TPeriod]) Transition() *Active[TPeriod] { + var c *Active[TPeriod] if a.Voice != nil && !a.Voice.IsDone() { - c = &Active{ + c = &Active[TPeriod]{ Playback: a.Playback, Voice: a.Voice, PeriodDelta: a.PeriodDelta, @@ -63,7 +63,7 @@ type RenderDetails struct { } // 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 { +func RenderStatesTogether[TPeriod period.Period](activeState *Active[TPeriod], pastNotes []*Active[TPeriod], details RenderDetails) []mixing.Data { var mixData []mixing.Data centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) @@ -85,7 +85,7 @@ func RenderStatesTogether(activeState *Active, pastNotes []*Active, details Rend return mixData } -func (a *Active) renderState(centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { +func (a *Active[TPeriod]) renderState(centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { if a.Period == nil || a.Volume == 0 { return nil } @@ -96,7 +96,7 @@ func (a *Active) renderState(centerAheadPan volume.Matrix, details RenderDetails } // Commit the playback settings to the note-control - voice.SetPeriod(ncv, a.Period) + voice.SetPeriod(ncv, any(a.Period).(period.Period)) voice.SetVolume(ncv, a.Volume) voice.SetPos(ncv, a.Pos) voice.SetPan(ncv, a.Pan) diff --git a/player/state/channel.go b/player/state/channel.go index 46b37f9..3ab0fad 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -22,52 +22,52 @@ type NoteTrigger struct { Tick int } -type VolOp[TMemory any] interface { - Process(p playback.Playback, cs *ChannelState[TMemory]) error +type VolOp[TPeriod period.Period, TMemory any] interface { + Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error } -type NoteOp[TMemory any] interface { - Process(p playback.Playback, cs *ChannelState[TMemory]) error +type NoteOp[TPeriod period.Period, TMemory any] interface { + Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error } -type PeriodUpdateFunc func(period.Period) +type PeriodUpdateFunc[TPeriod period.Period] func(*TPeriod) -type SemitoneSetterFactory[TMemory any] func(note.Semitone, PeriodUpdateFunc) NoteOp[TMemory] +type SemitoneSetterFactory[TPeriod period.Period, TMemory any] func(note.Semitone, PeriodUpdateFunc[TPeriod]) NoteOp[TPeriod, TMemory] // ChannelState is the state of a single channel -type ChannelState[TMemory any] struct { - activeState Active - targetState Playback - prevState Active +type ChannelState[TPeriod period.Period, TMemory any] struct { + activeState Active[TPeriod] + targetState Playback[TPeriod] + prevState Active[TPeriod] ActiveEffect playback.Effect s song.Data - txn ChannelDataTransaction[TMemory] - prevTxn ChannelDataTransaction[TMemory] + txn ChannelDataTransaction[TPeriod, TMemory] + prevTxn ChannelDataTransaction[TPeriod, TMemory] - SemitoneSetterFactory SemitoneSetterFactory[TMemory] + SemitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory] StoredSemitone note.Semitone // from pattern, unmodified, current note - PortaTargetPeriod optional.Value[period.Period] + PortaTargetPeriod optional.Value[TPeriod] 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 + periodOverride *TPeriod UsePeriodOverride bool volumeActive bool PanEnabled bool NewNoteAction note.Action - PastNotes *PastNotesProcessor + PastNotes *PastNotesProcessor[TPeriod] RenderChannel *render.Channel } // WillTriggerOn returns true if a note will trigger on the tick specified -func (cs *ChannelState[TMemory]) WillTriggerOn(tick int) (bool, note.Action) { +func (cs *ChannelState[TPeriod, TMemory]) WillTriggerOn(tick int) (bool, note.Action) { if trigger, ok := cs.Trigger.Get(); ok { return trigger.Tick == tick, trigger.NoteAction } @@ -76,7 +76,7 @@ func (cs *ChannelState[TMemory]) WillTriggerOn(tick int) (bool, note.Action) { } // AdvanceRow will update the current state to make room for the next row's state data -func (cs *ChannelState[TMemory]) AdvanceRow(txn ChannelDataTransaction[TMemory]) { +func (cs *ChannelState[TPeriod, TMemory]) AdvanceRow(txn ChannelDataTransaction[TPeriod, TMemory]) { cs.prevState = cs.activeState cs.targetState = cs.activeState.Playback cs.Trigger.Reset() @@ -89,7 +89,7 @@ func (cs *ChannelState[TMemory]) AdvanceRow(txn ChannelDataTransaction[TMemory]) } // RenderRowTick renders a channel's row data for a single tick -func (cs *ChannelState[TMemory]) RenderRowTick(details RenderDetails, pastNotes []*Active) ([]mixing.Data, error) { +func (cs *ChannelState[TPeriod, TMemory]) RenderRowTick(details RenderDetails, pastNotes []*Active[TPeriod]) ([]mixing.Data, error) { if cs.PlaybackFrozen() { return nil, nil } @@ -100,68 +100,68 @@ func (cs *ChannelState[TMemory]) RenderRowTick(details RenderDetails, pastNotes } // ResetStates resets the channel's internal states -func (cs *ChannelState[TMemory]) ResetStates() { +func (cs *ChannelState[TPeriod, TMemory]) ResetStates() { cs.activeState.Reset() cs.targetState.Reset() cs.prevState.Reset() } -func (cs *ChannelState[TMemory]) GetActiveEffect() playback.Effect { +func (cs *ChannelState[TPeriod, TMemory]) GetActiveEffect() playback.Effect { return cs.ActiveEffect } -func (cs *ChannelState[TMemory]) SetActiveEffect(e playback.Effect) { +func (cs *ChannelState[TPeriod, TMemory]) SetActiveEffect(e playback.Effect) { cs.ActiveEffect = e } // FreezePlayback suspends mixer progression on the channel -func (cs *ChannelState[TMemory]) FreezePlayback() { +func (cs *ChannelState[TPeriod, TMemory]) FreezePlayback() { cs.freezePlayback = true } // UnfreezePlayback resumes mixer progression on the channel -func (cs *ChannelState[TMemory]) UnfreezePlayback() { +func (cs *ChannelState[TPeriod, TMemory]) UnfreezePlayback() { cs.freezePlayback = false } // PlaybackFrozen returns true if the mixer progression for the channel is suspended -func (cs ChannelState[TMemory]) PlaybackFrozen() bool { +func (cs ChannelState[TPeriod, TMemory]) PlaybackFrozen() bool { return cs.freezePlayback } // ResetRetriggerCount sets the retrigger count to 0 -func (cs *ChannelState[TMemory]) ResetRetriggerCount() { +func (cs *ChannelState[TPeriod, TMemory]) ResetRetriggerCount() { cs.RetriggerCount = 0 } // GetMemory returns the interface to the custom effect memory module -func (cs *ChannelState[TMemory]) GetMemory() *TMemory { +func (cs *ChannelState[TPeriod, TMemory]) GetMemory() *TMemory { return cs.Memory } // SetMemory sets the custom effect memory interface -func (cs *ChannelState[TMemory]) SetMemory(mem *TMemory) { +func (cs *ChannelState[TPeriod, TMemory]) SetMemory(mem *TMemory) { cs.Memory = mem } // GetActiveVolume returns the current active volume on the channel -func (cs *ChannelState[TMemory]) GetActiveVolume() volume.Volume { +func (cs *ChannelState[TPeriod, TMemory]) GetActiveVolume() volume.Volume { return cs.activeState.Volume } // SetActiveVolume sets the active volume on the channel -func (cs *ChannelState[TMemory]) SetActiveVolume(vol volume.Volume) { +func (cs *ChannelState[TPeriod, TMemory]) SetActiveVolume(vol volume.Volume) { if vol != volume.VolumeUseInstVol { cs.activeState.Volume = vol } } -func (cs *ChannelState[TMemory]) SetSongDataInterface(s song.Data) { +func (cs *ChannelState[TPeriod, TMemory]) SetSongDataInterface(s song.Data) { cs.s = s } // GetData returns the interface to the current channel song pattern data -func (cs *ChannelState[TMemory]) GetData() song.ChannelData { +func (cs *ChannelState[TPeriod, TMemory]) GetData() song.ChannelData { if cs.txn == nil { return nil } @@ -169,7 +169,7 @@ func (cs *ChannelState[TMemory]) GetData() song.ChannelData { return cs.txn.GetData() } -func (cs *ChannelState[TMemory]) SetData(cdata song.ChannelData) error { +func (cs *ChannelState[TPeriod, TMemory]) SetData(cdata song.ChannelData) error { if cs.txn == nil { return nil } @@ -177,70 +177,70 @@ func (cs *ChannelState[TMemory]) SetData(cdata song.ChannelData) error { return cs.txn.SetData(cdata, cs.s, cs) } -func (cs *ChannelState[TMemory]) GetTxn() ChannelDataTransaction[TMemory] { +func (cs *ChannelState[TPeriod, TMemory]) GetTxn() ChannelDataTransaction[TPeriod, TMemory] { return cs.txn } // GetPortaTargetPeriod returns the current target portamento (to note) sampler period -func (cs *ChannelState[TMemory]) GetPortaTargetPeriod() period.Period { +func (cs *ChannelState[TPeriod, TMemory]) GetPortaTargetPeriod() *TPeriod { if p, ok := cs.PortaTargetPeriod.Get(); ok { - return p + return &p } return nil } // SetPortaTargetPeriod sets the current target portamento (to note) sampler period -func (cs *ChannelState[TMemory]) SetPortaTargetPeriod(period period.Period) { +func (cs *ChannelState[TPeriod, TMemory]) SetPortaTargetPeriod(period *TPeriod) { if period != nil { - cs.PortaTargetPeriod.Set(period) + 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]) GetTargetPeriod() period.Period { +func (cs *ChannelState[TPeriod, TMemory]) GetTargetPeriod() *TPeriod { return cs.targetState.Period } // SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory]) SetTargetPeriod(period period.Period) { +func (cs *ChannelState[TPeriod, TMemory]) SetTargetPeriod(period *TPeriod) { cs.targetState.Period = period } // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory]) GetPeriodOverride() period.Period { +func (cs *ChannelState[TPeriod, TMemory]) GetPeriodOverride() *TPeriod { return cs.periodOverride } // SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TMemory]) SetPeriodOverride(period period.Period) { +func (cs *ChannelState[TPeriod, TMemory]) SetPeriodOverride(period *TPeriod) { cs.periodOverride = period cs.UsePeriodOverride = true } // SetPeriodDelta sets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TMemory]) SetPeriodDelta(delta period.PeriodDelta) { +func (cs *ChannelState[TPeriod, TMemory]) SetPeriodDelta(delta period.PeriodDelta) { cs.activeState.PeriodDelta = delta } // GetPeriodDelta gets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TMemory]) GetPeriodDelta() period.PeriodDelta { +func (cs *ChannelState[TPeriod, TMemory]) GetPeriodDelta() period.PeriodDelta { return cs.activeState.PeriodDelta } // SetVolumeActive enables or disables the sample of the instrument -func (cs *ChannelState[TMemory]) SetVolumeActive(on bool) { +func (cs *ChannelState[TPeriod, TMemory]) SetVolumeActive(on bool) { cs.volumeActive = on } // GetInstrument returns the interface to the active instrument -func (cs *ChannelState[TMemory]) GetInstrument() *instrument.Instrument { +func (cs *ChannelState[TPeriod, TMemory]) GetInstrument() *instrument.Instrument { return cs.activeState.Instrument } // SetInstrument sets the interface to the active instrument -func (cs *ChannelState[TMemory]) SetInstrument(inst *instrument.Instrument) { +func (cs *ChannelState[TPeriod, TMemory]) SetInstrument(inst *instrument.Instrument) { cs.activeState.Instrument = inst if inst != nil { if inst == cs.prevState.Instrument { @@ -252,67 +252,67 @@ func (cs *ChannelState[TMemory]) SetInstrument(inst *instrument.Instrument) { } // GetVoice returns the active voice interface -func (cs *ChannelState[TMemory]) GetVoice() voice.Voice { +func (cs *ChannelState[TPeriod, TMemory]) 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]) GetTargetInst() *instrument.Instrument { +func (cs *ChannelState[TPeriod, TMemory]) GetTargetInst() *instrument.Instrument { return cs.targetState.Instrument } // SetTargetInst sets the soon-to-be-committed active instrument (when the note retriggers) -func (cs *ChannelState[TMemory]) SetTargetInst(inst *instrument.Instrument) { +func (cs *ChannelState[TPeriod, TMemory]) SetTargetInst(inst *instrument.Instrument) { cs.targetState.Instrument = inst } // GetPrevInst returns the interface to the last row's active instrument -func (cs *ChannelState[TMemory]) GetPrevInst() *instrument.Instrument { +func (cs *ChannelState[TPeriod, TMemory]) GetPrevInst() *instrument.Instrument { return cs.prevState.Instrument } // GetPrevVoice returns the interface to the last row's active voice -func (cs *ChannelState[TMemory]) GetPrevVoice() voice.Voice { +func (cs *ChannelState[TPeriod, TMemory]) GetPrevVoice() voice.Voice { return cs.prevState.Voice } // GetNoteSemitone returns the note semitone for the channel -func (cs *ChannelState[TMemory]) GetNoteSemitone() note.Semitone { +func (cs *ChannelState[TPeriod, TMemory]) GetNoteSemitone() note.Semitone { return cs.StoredSemitone } // GetTargetPos returns the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TMemory]) GetTargetPos() sampling.Pos { +func (cs *ChannelState[TPeriod, TMemory]) GetTargetPos() sampling.Pos { return cs.targetState.Pos } // SetTargetPos sets the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TMemory]) SetTargetPos(pos sampling.Pos) { +func (cs *ChannelState[TPeriod, TMemory]) SetTargetPos(pos sampling.Pos) { cs.targetState.Pos = pos } // GetPeriod returns the current sampler period of the active instrument -func (cs *ChannelState[TMemory]) GetPeriod() period.Period { +func (cs *ChannelState[TPeriod, TMemory]) GetPeriod() *TPeriod { return cs.activeState.Period } // SetPeriod sets the current sampler period of the active instrument -func (cs *ChannelState[TMemory]) SetPeriod(period period.Period) { +func (cs *ChannelState[TPeriod, TMemory]) SetPeriod(period *TPeriod) { cs.activeState.Period = period } // GetPos returns the sample position of the active instrument -func (cs *ChannelState[TMemory]) GetPos() sampling.Pos { +func (cs *ChannelState[TPeriod, TMemory]) GetPos() sampling.Pos { return cs.activeState.Pos } // SetPos sets the sample position of the active instrument -func (cs *ChannelState[TMemory]) SetPos(pos sampling.Pos) { +func (cs *ChannelState[TPeriod, TMemory]) SetPos(pos sampling.Pos) { cs.activeState.Pos = pos } // SetNotePlayTick sets the tick on which the note will retrigger -func (cs *ChannelState[TMemory]) SetNotePlayTick(enabled bool, action note.Action, tick int) { +func (cs *ChannelState[TPeriod, TMemory]) SetNotePlayTick(enabled bool, action note.Action, tick int) { if enabled { cs.Trigger.Set(NoteTrigger{ NoteAction: action, @@ -324,78 +324,78 @@ func (cs *ChannelState[TMemory]) SetNotePlayTick(enabled bool, action note.Actio } // GetRetriggerCount returns the current count of the retrigger counter -func (cs *ChannelState[TMemory]) GetRetriggerCount() uint8 { +func (cs *ChannelState[TPeriod, TMemory]) GetRetriggerCount() uint8 { return cs.RetriggerCount } // SetRetriggerCount sets the current count of the retrigger counter -func (cs *ChannelState[TMemory]) SetRetriggerCount(cnt uint8) { +func (cs *ChannelState[TPeriod, TMemory]) SetRetriggerCount(cnt uint8) { cs.RetriggerCount = cnt } // SetPanEnabled activates or deactivates the panning. If enabled, then pan updates work (see SetPan) -func (cs *ChannelState[TMemory]) SetPanEnabled(on bool) { +func (cs *ChannelState[TPeriod, TMemory]) SetPanEnabled(on bool) { cs.PanEnabled = on } // SetPan sets the active panning value of the channel -func (cs *ChannelState[TMemory]) SetPan(pan panning.Position) { +func (cs *ChannelState[TPeriod, TMemory]) SetPan(pan panning.Position) { if cs.PanEnabled { cs.activeState.Pan = pan } } // GetPan gets the active panning value of the channel -func (cs *ChannelState[TMemory]) GetPan() panning.Position { +func (cs *ChannelState[TPeriod, TMemory]) GetPan() panning.Position { return cs.activeState.Pan } // SetTargetSemitone sets the target semitone for the channel -func (cs *ChannelState[TMemory]) SetTargetSemitone(st note.Semitone) { +func (cs *ChannelState[TPeriod, TMemory]) SetTargetSemitone(st note.Semitone) { if cs.txn != nil { cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, cs.SetTargetPeriod)) } } -func (cs *ChannelState[TMemory]) SetOverrideSemitone(st note.Semitone) { +func (cs *ChannelState[TPeriod, TMemory]) 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]) SetStoredSemitone(st note.Semitone) { +func (cs *ChannelState[TPeriod, TMemory]) SetStoredSemitone(st note.Semitone) { cs.StoredSemitone = st } // SetRenderChannel sets the output channel for the channel -func (cs *ChannelState[TMemory]) SetRenderChannel(outputCh *render.Channel) { +func (cs *ChannelState[TPeriod, TMemory]) SetRenderChannel(outputCh *render.Channel) { cs.RenderChannel = outputCh } // GetRenderChannel returns the output channel for the channel -func (cs *ChannelState[TMemory]) GetRenderChannel() *render.Channel { +func (cs *ChannelState[TPeriod, TMemory]) GetRenderChannel() *render.Channel { return cs.RenderChannel } // SetGlobalVolume sets the last-known global volume on the channel -func (cs *ChannelState[TMemory]) SetGlobalVolume(gv volume.Volume) { +func (cs *ChannelState[TPeriod, TMemory]) SetGlobalVolume(gv volume.Volume) { cs.RenderChannel.LastGlobalVolume = gv cs.RenderChannel.SetGlobalVolume(gv) } // SetChannelVolume sets the channel volume on the channel -func (cs *ChannelState[TMemory]) SetChannelVolume(cv volume.Volume) { +func (cs *ChannelState[TPeriod, TMemory]) SetChannelVolume(cv volume.Volume) { cs.RenderChannel.ChannelVolume = cv } // GetChannelVolume gets the channel volume on the channel -func (cs *ChannelState[TMemory]) GetChannelVolume() volume.Volume { +func (cs *ChannelState[TPeriod, TMemory]) GetChannelVolume() volume.Volume { return cs.RenderChannel.ChannelVolume } // SetEnvelopePosition sets the envelope position for the active instrument -func (cs *ChannelState[TMemory]) SetEnvelopePosition(ticks int) { +func (cs *ChannelState[TPeriod, TMemory]) SetEnvelopePosition(ticks int) { if nc := cs.GetVoice(); nc != nil { voice.SetVolumeEnvelopePosition(nc, ticks) voice.SetPanEnvelopePosition(nc, ticks) @@ -406,7 +406,7 @@ func (cs *ChannelState[TMemory]) SetEnvelopePosition(ticks int) { // 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]) TransitionActiveToPastState() { +func (cs *ChannelState[TPeriod, TMemory]) TransitionActiveToPastState() { if cs.PastNotes != nil { switch cs.NewNoteAction { case note.ActionCut: @@ -439,35 +439,35 @@ func (cs *ChannelState[TMemory]) TransitionActiveToPastState() { } // DoPastNoteEffect performs an action on all past-note playbacks associated with the channel -func (cs *ChannelState[TMemory]) DoPastNoteEffect(action note.Action) { +func (cs *ChannelState[TPeriod, TMemory]) DoPastNoteEffect(action note.Action) { cs.PastNotes.Do(cs.RenderChannel.ChannelNum, action) } // SetNewNoteAction sets the New-Note Action on the channel -func (cs *ChannelState[TMemory]) SetNewNoteAction(nna note.Action) { +func (cs *ChannelState[TPeriod, TMemory]) SetNewNoteAction(nna note.Action) { cs.NewNoteAction = nna } // GetNewNoteAction gets the New-Note Action on the channel -func (cs *ChannelState[TMemory]) GetNewNoteAction() note.Action { +func (cs *ChannelState[TPeriod, TMemory]) GetNewNoteAction() note.Action { return cs.NewNoteAction } // SetVolumeEnvelopeEnable sets the enable flag on the active volume envelope -func (cs *ChannelState[TMemory]) SetVolumeEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TPeriod, TMemory]) SetVolumeEnvelopeEnable(enabled bool) { voice.EnableVolumeEnvelope(cs.activeState.Voice, enabled) } // SetPanningEnvelopeEnable sets the enable flag on the active panning envelope -func (cs *ChannelState[TMemory]) SetPanningEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TPeriod, TMemory]) SetPanningEnvelopeEnable(enabled bool) { voice.EnablePanEnvelope(cs.activeState.Voice, enabled) } // SetPitchEnvelopeEnable sets the enable flag on the active pitch/filter envelope -func (cs *ChannelState[TMemory]) SetPitchEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TPeriod, TMemory]) SetPitchEnvelopeEnable(enabled bool) { voice.EnablePitchEnvelope(cs.activeState.Voice, enabled) } -func (cs *ChannelState[TMemory]) NoteCut() { +func (cs *ChannelState[TPeriod, TMemory]) NoteCut() { cs.activeState.Period = nil } diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index 41e6295..1671234 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -11,73 +11,69 @@ import ( "github.com/heucuva/optional" ) -type ChannelDataTransaction[TMemory any] interface { +type ChannelDataTransaction[TPeriod period.Period, TMemory any] interface { GetData() song.ChannelData - SetData(data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error + SetData(data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error - CommitPreRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error - CommitRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error - CommitPostRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error + CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error + CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error + CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error - CommitPreTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error - CommitTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error - CommitPostTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error + CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error + CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error + CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error - AddVolOp(op VolOp[TMemory]) - AddNoteOp(op NoteOp[TMemory]) + AddVolOp(op VolOp[TPeriod, TMemory]) + AddNoteOp(op NoteOp[TPeriod, TMemory]) } -type ChannelDataActions struct { +type ChannelDataActions[TPeriod period.Period] 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] + TargetPeriod optional.Value[TPeriod] TargetStoredSemitone optional.Value[note.Semitone] TargetNewNoteAction optional.Value[note.Action] TargetVolume optional.Value[volume.Volume] } -type ChannelDataConverter[TMemory any] func(out *ChannelDataActions, data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error +type ChannelDataConverter[TPeriod period.Period, TMemory any] func(out *ChannelDataActions[TPeriod], data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error -type channelDataTxnHelper[TMemory any] struct { +type ChannelDataTxnHelper[TPeriod period.Period, TMemory any] struct { Data song.ChannelData effectFactory func(*TMemory, song.ChannelData) playback.Effect - ChannelDataActions + ChannelDataActions[TPeriod] - VolOps []VolOp[TMemory] - NoteOps []NoteOp[TMemory] + VolOps []VolOp[TPeriod, TMemory] + NoteOps []NoteOp[TPeriod, TMemory] } -func NewChannelDataTxn[TMemory any](effectFactory func(*TMemory, song.ChannelData) playback.Effect) ChannelDataTransaction[TMemory] { - return &channelDataTxnHelper[TMemory]{ +func NewChannelDataTxn[TPeriod period.Period, TMemory any](effectFactory func(*TMemory, song.ChannelData) playback.Effect) ChannelDataTransaction[TPeriod, TMemory] { + return &ChannelDataTxnHelper[TPeriod, TMemory]{ effectFactory: effectFactory, } } -func (d *channelDataTxnHelper[TMemory]) GetData() song.ChannelData { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) GetData() song.ChannelData { return d.Data } -func (d *channelDataTxnHelper[TMemory]) SetData(cd song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) SetData(cd song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error { d.Data = cd - - if d.ProcessData != nil { - return d.ProcessData(&d.ChannelDataActions, cd, s, cs) - } - return nil + return d.ProcessData(&d.ChannelDataActions, cd, s, cs) } -func (d *channelDataTxnHelper[TMemory]) CommitPreRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { e := d.effectFactory(cs.GetMemory(), d.Data) cs.SetActiveEffect(e) if e != nil { if onEff := p.GetOnEffect(); onEff != nil { onEff(e) } - if err := playback.EffectPreStart[TMemory](e, cs, p); err != nil { + if err := playback.EffectPreStart[TPeriod, TMemory](e, any(cs).(playback.Channel[TPeriod, TMemory]), p); err != nil { return err } } @@ -85,7 +81,7 @@ func (d *channelDataTxnHelper[TMemory]) CommitPreRow(p playback.Playback, cs *Ch return nil } -func (d *channelDataTxnHelper[TMemory]) CommitRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { if pos, ok := d.TargetPos.Get(); ok { cs.SetTargetPos(pos) } @@ -95,8 +91,8 @@ func (d *channelDataTxnHelper[TMemory]) CommitRow(p playback.Playback, cs *Chann } if period, ok := d.TargetPeriod.Get(); ok { - cs.SetTargetPeriod(period) - cs.SetPortaTargetPeriod(period) + cs.SetTargetPeriod(&period) + cs.SetPortaTargetPeriod(&period) } if st, ok := d.TargetStoredSemitone.Get(); ok { @@ -122,11 +118,11 @@ func (d *channelDataTxnHelper[TMemory]) CommitRow(p playback.Playback, cs *Chann return nil } -func (d *channelDataTxnHelper[TMemory]) CommitPostRow(p playback.Playback, cs *ChannelState[TMemory], semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { return nil } -func (d *channelDataTxnHelper[TMemory]) CommitPreTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { // pre-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -138,15 +134,15 @@ func (d *channelDataTxnHelper[TMemory]) CommitPreTick(p playback.Playback, cs *C return nil } -func (d *channelDataTxnHelper[TMemory]) CommitTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { - if err := playback.DoEffect[TMemory](cs.ActiveEffect, cs, p, currentTick, lastTick); err != nil { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { + if err := playback.DoEffect[TPeriod, TMemory](cs.ActiveEffect, cs, p, currentTick, lastTick); err != nil { return err } return nil } -func (d *channelDataTxnHelper[TMemory]) CommitPostTick(p playback.Playback, cs *ChannelState[TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { // post-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -158,11 +154,11 @@ func (d *channelDataTxnHelper[TMemory]) CommitPostTick(p playback.Playback, cs * return nil } -func (d *channelDataTxnHelper[TMemory]) AddVolOp(op VolOp[TMemory]) { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) AddVolOp(op VolOp[TPeriod, TMemory]) { d.VolOps = append(d.VolOps, op) } -func (d *channelDataTxnHelper[TMemory]) ProcessVolOps(p playback.Playback, cs *ChannelState[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessVolOps(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error { for _, op := range d.VolOps { if op == nil { continue @@ -176,11 +172,11 @@ func (d *channelDataTxnHelper[TMemory]) ProcessVolOps(p playback.Playback, cs *C return nil } -func (d *channelDataTxnHelper[TMemory]) AddNoteOp(op NoteOp[TMemory]) { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) AddNoteOp(op NoteOp[TPeriod, TMemory]) { d.NoteOps = append(d.NoteOps, op) } -func (d *channelDataTxnHelper[TMemory]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error { for _, op := range d.NoteOps { if op == nil { continue @@ -194,7 +190,7 @@ func (d *channelDataTxnHelper[TMemory]) ProcessNoteOps(p playback.Playback, cs * return nil } -func (d *channelDataTxnHelper[TMemory]) ProcessData(out *ChannelDataActions, data song.ChannelData, s song.Data, cs *ChannelState[TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessData(out *ChannelDataActions[TPeriod], data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error { if data == nil { return nil } @@ -242,7 +238,7 @@ func (d *channelDataTxnHelper[TMemory]) ProcessData(out *ChannelDataActions, dat } if note.IsInvalid(n) { - out.TargetPeriod.Set(nil) + out.TargetPeriod.Reset() out.NoteAction.Set(note.ActionCut) } else if note.IsRelease(n) { out.NoteAction.Set(note.ActionRelease) diff --git a/player/state/pastnotes.go b/player/state/pastnotes.go index ddcf971..e1c5a7e 100644 --- a/player/state/pastnotes.go +++ b/player/state/pastnotes.go @@ -2,25 +2,26 @@ package state import ( "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" "github.com/heucuva/optional" ) -type pastNote struct { +type pastNote[TPeriod period.Period] struct { ch int - activeState *Active + activeState *Active[TPeriod] } -func (pn *pastNote) IsValid() bool { +func (pn *pastNote[TPeriod]) IsValid() bool { return pn.activeState.Voice != nil && !pn.activeState.Voice.IsDone() } -type PastNotesProcessor struct { - order []pastNote +type PastNotesProcessor[TPeriod period.Period] struct { + order []pastNote[TPeriod] max optional.Value[int] maxPerCh optional.Value[int] } -func (p *PastNotesProcessor) Add(ch int, data *Active) { +func (p *PastNotesProcessor[TPeriod]) Add(ch int, data *Active[TPeriod]) { if data == nil { return } @@ -36,7 +37,7 @@ func (p *PastNotesProcessor) Add(ch int, data *Active) { } } - cl := pastNote{ + cl := pastNote[TPeriod]{ ch: ch, activeState: data, } @@ -44,7 +45,7 @@ func (p *PastNotesProcessor) Add(ch int, data *Active) { p.order = append(p.order, cl) } -func (p *PastNotesProcessor) Do(ch int, action note.Action) { +func (p *PastNotesProcessor[TPeriod]) Do(ch int, action note.Action) { if action == note.ActionContinue { return } @@ -70,8 +71,8 @@ func (p *PastNotesProcessor) Do(ch int, action note.Action) { } } -func (p *PastNotesProcessor) Update() { - var nl []pastNote +func (p *PastNotesProcessor[TPeriod]) Update() { + var nl []pastNote[TPeriod] for _, o := range p.order { if !o.IsValid() { o.activeState.Reset() @@ -83,8 +84,8 @@ func (p *PastNotesProcessor) Update() { p.order = nl } -func (p *PastNotesProcessor) GetNotesForChannel(ch int) []*Active { - var pastNotes []*Active +func (p *PastNotesProcessor[TPeriod]) GetNotesForChannel(ch int) []*Active[TPeriod] { + var pastNotes []*Active[TPeriod] for _, pn := range p.order { if pn.ch != ch { continue @@ -109,18 +110,18 @@ func (p *PastNotesProcessor) GetNotesForChannel(ch int) []*Active { return pastNotes } -func (p *PastNotesProcessor) SetMax(max int) { +func (p *PastNotesProcessor[TPeriod]) SetMax(max int) { p.max.Set(max) } -func (p *PastNotesProcessor) ClearMax() { +func (p *PastNotesProcessor[TPeriod]) ClearMax() { p.max.Reset() } -func (p *PastNotesProcessor) SetMaxPerChannel(max int) { +func (p *PastNotesProcessor[TPeriod]) SetMaxPerChannel(max int) { p.maxPerCh.Set(max) } -func (p *PastNotesProcessor) ClearMaxPerChannel() { +func (p *PastNotesProcessor[TPeriod]) ClearMaxPerChannel() { p.maxPerCh.Reset() } diff --git a/player/state/playback.go b/player/state/playback.go index b1ec8de..d48d5de 100644 --- a/player/state/playback.go +++ b/player/state/playback.go @@ -10,16 +10,16 @@ import ( ) // Playback is the information needed to make an instrument play -type Playback struct { +type Playback[TPeriod period.Period] struct { Instrument *instrument.Instrument - Period period.Period + Period *TPeriod Volume volume.Volume Pos sampling.Pos Pan panning.Position } // Reset sets the render state to defaults -func (p *Playback) Reset() { +func (p *Playback[TPeriod]) Reset() { p.Instrument = nil p.Period = nil p.Pos = sampling.Pos{} From 101e1e80d412fe257f2209a3d145442e6fd09f21 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 21 Dec 2023 10:47:55 -0800 Subject: [PATCH 09/63] effect cleanup --- effect.go | 36 ++++++++ format/it/channel/data.go | 19 ++++ .../it/{effect => channel}/effect_arpeggio.go | 14 +-- .../effect_channelvolumeslide.go | 15 ++-- .../effect_extrafineportadown.go | 11 ++- .../effect_extrafineportaup.go | 11 ++- .../effect_finepatterndelay.go | 14 ++- .../effect_fineportadown.go | 11 ++- .../{effect => channel}/effect_fineportaup.go | 11 ++- .../{effect => channel}/effect_finevibrato.go | 13 ++- .../effect_finevolslidedown.go | 21 +++-- .../effect_finevolslideup.go | 21 +++-- .../channel}/effect_globalvolumeslide.go | 16 ++-- .../{effect => channel}/effect_highoffset.go | 11 ++- .../effect_newnoteactionnotecontinue.go | 9 +- .../effect_newnoteactionnotecut.go | 9 +- .../effect_newnoteactionnotefade.go | 9 +- .../effect_newnoteactionnoteoff.go | 9 +- format/it/channel/effect_notecut.go | 31 +++++++ .../effect => it/channel}/effect_notedelay.go | 13 ++- .../effect => it/channel}/effect_orderjump.go | 11 ++- .../effect_panningenvelopeoff.go | 9 +- .../effect_panningenvelopeon.go | 9 +- .../{effect => channel}/effect_pastnotecut.go | 9 +- .../effect_pastnotefadeout.go | 9 +- .../{effect => channel}/effect_pastnoteoff.go | 9 +- .../effect_patterndelay.go | 16 ++-- .../{effect => channel}/effect_patternloop.go | 11 ++- .../effect_pitchenvelopeoff.go | 9 +- .../effect_pitchenvelopeon.go | 9 +- format/it/channel/effect_portadown.go | 30 +++++++ .../{effect => channel}/effect_portatonote.go | 13 ++- format/it/channel/effect_portaup.go | 30 +++++++ .../effect_portavolslide.go | 9 +- .../effect_retrigvolslide.go | 15 ++-- .../it/{effect => channel}/effect_rowjump.go | 13 ++- .../effect_sampleoffset.go | 11 ++- .../effect_setchannelvolume.go | 11 ++- .../effect_setcoarsepanposition.go | 11 ++- .../channel}/effect_setfinetune.go | 13 ++- .../effect_setglobalvolume.go | 13 ++- .../effect_setpanbrellowaveform.go | 11 ++- .../effect_setpanposition.go | 11 ++- .../it/{effect => channel}/effect_setspeed.go | 14 ++- .../it/{effect => channel}/effect_settempo.go | 24 +++-- .../effect_settremolowaveform.go | 11 ++- .../channel}/effect_setvibratowaveform.go | 11 ++- .../it/{effect => channel}/effect_tremolo.go | 13 ++- format/it/channel/effect_tremor.go | 28 ++++++ .../it/{effect => channel}/effect_vibrato.go | 13 ++- .../effect_vibratovolslide.go | 9 +- .../effect_volslidedown.go | 21 +++-- .../{effect => channel}/effect_volslideup.go | 21 +++-- .../effect_volumeenvelopeoff.go | 9 +- .../effect_volumeenvelopeon.go | 9 +- .../it/{effect => channel}/effectfactory.go | 25 +++--- .../effectfactory_volume.go | 5 +- format/it/{effect => channel}/unhandled.go | 24 +++-- format/it/{effect => channel}/util.go | 61 ++++++++----- format/it/effect/effect_notecut.go | 32 ------- format/it/effect/effect_portadown.go | 31 ------- format/it/effect/effect_portaup.go | 31 ------- format/it/effect/effect_tremor.go | 29 ------- format/it/effect/intf/intf.go | 23 ----- format/it/playback/playback_command.go | 3 +- format/it/playback/playback_pattern.go | 3 +- format/it/playback/playback_tracing.go | 47 +++++----- format/s3m/channel/data.go | 46 +++++++++- .../{effect => channel}/effect_arpeggio.go | 7 +- .../effect_enablefilter.go | 10 +-- .../effect_extrafineportadown.go | 7 +- .../effect_extrafineportaup.go | 7 +- .../effect_finepatterndelay.go | 10 +-- .../effect_fineportadown.go | 7 +- .../{effect => channel}/effect_fineportaup.go | 7 +- .../{effect => channel}/effect_finevibrato.go | 7 +- .../effect_finevolslidedown.go | 7 +- .../effect_finevolslideup.go | 7 +- .../s3m/{effect => channel}/effect_notecut.go | 7 +- .../{effect => channel}/effect_notedelay.go | 7 +- .../{effect => channel}/effect_orderjump.go | 5 +- .../effect_patterndelay.go | 10 +-- .../{effect => channel}/effect_patternloop.go | 7 +- .../{effect => channel}/effect_portadown.go | 7 +- .../{effect => channel}/effect_portatonote.go | 7 +- .../s3m/{effect => channel}/effect_portaup.go | 7 +- .../effect_portavolslide.go | 9 +- .../effect_retrigvolslide.go | 11 ++- .../s3m/{effect => channel}/effect_rowjump.go | 7 +- .../effect_sampleoffset.go | 7 +- .../{effect => channel}/effect_setfinetune.go | 7 +- .../effect_setglobalvolume.go | 7 +- .../effect_setpanposition.go | 5 +- .../{effect => channel}/effect_setspeed.go | 8 +- .../{effect => channel}/effect_settempo.go | 16 ++-- .../effect_settremolowaveform.go | 7 +- .../effect_setvibratowaveform.go | 7 +- .../effect_stereocontrol.go | 5 +- .../s3m/{effect => channel}/effect_tremolo.go | 9 +- .../s3m/{effect => channel}/effect_tremor.go | 7 +- .../s3m/{effect => channel}/effect_vibrato.go | 7 +- .../effect_vibratovolslide.go | 9 +- .../effect_volslidedown.go | 7 +- .../{effect => channel}/effect_volslideup.go | 7 +- .../s3m/{effect => channel}/effectfactory.go | 20 ++--- format/s3m/channel/memory.go | 26 ++++++ format/s3m/channel/note_start.go | 87 +++++++++++++++++++ format/s3m/channel/note_stop.go | 36 ++++++++ format/s3m/channel/notefactory.go | 27 ++++++ format/s3m/{effect => channel}/unhandled.go | 15 ++-- format/s3m/{effect => channel}/util.go | 43 ++++++--- format/s3m/channel/volume_setvolume.go | 23 +++++ format/s3m/channel/volumefactory.go | 7 ++ format/s3m/effect/intf/intf.go | 23 ----- format/s3m/note/note.go | 24 ----- format/s3m/playback/playback_pattern.go | 3 +- format/xm/channel/data.go | 19 ++++ .../xm/{effect => channel}/effect_arpeggio.go | 13 ++- .../effect_extrafineportadown.go | 11 ++- .../effect_extrafineportaup.go | 11 ++- .../effect_fineportadown.go | 11 ++- .../{effect => channel}/effect_fineportaup.go | 11 ++- .../effect_finevolslidedown.go | 13 ++- .../effect_finevolslideup.go | 13 ++- .../channel}/effect_globalvolumeslide.go | 16 ++-- format/xm/channel/effect_notecut.go | 31 +++++++ .../effect => xm/channel}/effect_notedelay.go | 13 ++- .../effect => xm/channel}/effect_orderjump.go | 11 ++- .../xm/{effect => channel}/effect_panslide.go | 13 ++- .../effect_patterndelay.go | 16 ++-- .../{effect => channel}/effect_patternloop.go | 11 ++- .../{effect => channel}/effect_portadown.go | 13 ++- .../{effect => channel}/effect_portatonote.go | 13 ++- .../xm/{effect => channel}/effect_portaup.go | 13 ++- .../effect_portavolslide.go | 9 +- .../effect_retriggernote.go | 15 ++-- .../effect_retrigvolslide.go | 17 ++-- .../xm/{effect => channel}/effect_rowjump.go | 13 ++- .../effect_sampleoffset.go | 11 ++- .../effect_setcoarsepanposition.go | 11 ++- .../effect_setenvelopeposition.go | 11 ++- .../channel}/effect_setfinetune.go | 13 ++- .../effect_setglobalvolume.go | 11 ++- .../effect_setpanposition.go | 9 +- .../xm/{effect => channel}/effect_setspeed.go | 14 ++- .../xm/{effect => channel}/effect_settempo.go | 18 ++-- .../effect_settremolowaveform.go | 11 ++- .../channel}/effect_setvibratowaveform.go | 11 ++- .../{effect => channel}/effect_setvolume.go | 9 +- .../xm/{effect => channel}/effect_tremolo.go | 13 ++- format/xm/channel/effect_tremor.go | 31 +++++++ .../xm/{effect => channel}/effect_vibrato.go | 13 ++- .../effect_vibratovolslide.go | 9 +- .../xm/{effect => channel}/effect_volslide.go | 13 ++- .../xm/{effect => channel}/effectfactory.go | 17 ++-- .../effectfactory_standard.go | 9 +- .../effectfactory_volume.go | 9 +- format/xm/{effect => channel}/unhandled.go | 24 +++-- format/xm/{effect => channel}/util.go | 62 ++++++++----- format/xm/effect/effect_notecut.go | 32 ------- format/xm/effect/effect_tremor.go | 32 ------- format/xm/effect/intf/intf.go | 22 ----- format/xm/playback/playback_pattern.go | 3 +- instrument/instrument.go | 6 +- player/state/channel.go | 10 +-- player/state/channel_transaction.go | 26 +++--- 166 files changed, 1344 insertions(+), 1170 deletions(-) rename format/it/{effect => channel}/effect_arpeggio.go (50%) rename format/it/{effect => channel}/effect_channelvolumeslide.go (74%) rename format/it/{effect => channel}/effect_extrafineportadown.go (61%) rename format/it/{effect => channel}/effect_extrafineportaup.go (61%) rename format/it/{effect => channel}/effect_finepatterndelay.go (55%) rename format/it/{effect => channel}/effect_fineportadown.go (61%) rename format/it/{effect => channel}/effect_fineportaup.go (61%) rename format/it/{effect => channel}/effect_finevibrato.go (63%) rename format/it/{effect => channel}/effect_finevolslidedown.go (63%) rename format/it/{effect => channel}/effect_finevolslideup.go (63%) rename format/{xm/effect => it/channel}/effect_globalvolumeslide.go (62%) rename format/it/{effect => channel}/effect_highoffset.go (62%) rename format/it/{effect => channel}/effect_newnoteactionnotecontinue.go (64%) rename format/it/{effect => channel}/effect_newnoteactionnotecut.go (64%) rename format/it/{effect => channel}/effect_newnoteactionnotefade.go (64%) rename format/it/{effect => channel}/effect_newnoteactionnoteoff.go (64%) create mode 100644 format/it/channel/effect_notecut.go rename format/{xm/effect => it/channel}/effect_notedelay.go (53%) rename format/{xm/effect => it/channel}/effect_orderjump.go (53%) rename format/it/{effect => channel}/effect_panningenvelopeoff.go (63%) rename format/it/{effect => channel}/effect_panningenvelopeon.go (63%) rename format/it/{effect => channel}/effect_pastnotecut.go (64%) rename format/it/{effect => channel}/effect_pastnotefadeout.go (64%) rename format/it/{effect => channel}/effect_pastnoteoff.go (64%) rename format/it/{effect => channel}/effect_patterndelay.go (53%) rename format/it/{effect => channel}/effect_patternloop.go (75%) rename format/it/{effect => channel}/effect_pitchenvelopeoff.go (63%) rename format/it/{effect => channel}/effect_pitchenvelopeon.go (63%) create mode 100644 format/it/channel/effect_portadown.go rename format/it/{effect => channel}/effect_portatonote.go (81%) create mode 100644 format/it/channel/effect_portaup.go rename format/it/{effect => channel}/effect_portavolslide.go (62%) rename format/it/{effect => channel}/effect_retrigvolslide.go (77%) rename format/it/{effect => channel}/effect_rowjump.go (52%) rename format/it/{effect => channel}/effect_sampleoffset.go (70%) rename format/it/{effect => channel}/effect_setchannelvolume.go (68%) rename format/it/{effect => channel}/effect_setcoarsepanposition.go (67%) rename format/{xm/effect => it/channel}/effect_setfinetune.go (67%) rename format/it/{effect => channel}/effect_setglobalvolume.go (64%) rename format/it/{effect => channel}/effect_setpanbrellowaveform.go (66%) rename format/it/{effect => channel}/effect_setpanposition.go (68%) rename format/it/{effect => channel}/effect_setspeed.go (55%) rename format/it/{effect => channel}/effect_settempo.go (56%) rename format/it/{effect => channel}/effect_settremolowaveform.go (66%) rename format/{xm/effect => it/channel}/effect_setvibratowaveform.go (66%) rename format/it/{effect => channel}/effect_tremolo.go (55%) create mode 100644 format/it/channel/effect_tremor.go rename format/it/{effect => channel}/effect_vibrato.go (54%) rename format/it/{effect => channel}/effect_vibratovolslide.go (61%) rename format/it/{effect => channel}/effect_volslidedown.go (60%) rename format/it/{effect => channel}/effect_volslideup.go (60%) rename format/it/{effect => channel}/effect_volumeenvelopeoff.go (63%) rename format/it/{effect => channel}/effect_volumeenvelopeon.go (63%) rename format/it/{effect => channel}/effectfactory.go (89%) rename format/it/{effect => channel}/effectfactory_volume.go (92%) rename format/it/{effect => channel}/unhandled.go (72%) rename format/it/{effect => channel}/util.go (66%) delete mode 100644 format/it/effect/effect_notecut.go delete mode 100644 format/it/effect/effect_portadown.go delete mode 100644 format/it/effect/effect_portaup.go delete mode 100644 format/it/effect/effect_tremor.go delete mode 100644 format/it/effect/intf/intf.go rename format/s3m/{effect => channel}/effect_arpeggio.go (78%) rename format/s3m/{effect => channel}/effect_enablefilter.go (63%) rename format/s3m/{effect => channel}/effect_extrafineportadown.go (75%) rename format/s3m/{effect => channel}/effect_extrafineportaup.go (74%) rename format/s3m/{effect => channel}/effect_finepatterndelay.go (63%) rename format/s3m/{effect => channel}/effect_fineportadown.go (73%) rename format/s3m/{effect => channel}/effect_fineportaup.go (73%) rename format/s3m/{effect => channel}/effect_finevibrato.go (81%) rename format/s3m/{effect => channel}/effect_finevolslidedown.go (80%) rename format/s3m/{effect => channel}/effect_finevolslideup.go (80%) rename format/s3m/{effect => channel}/effect_notecut.go (78%) rename format/s3m/{effect => channel}/effect_notedelay.go (73%) rename format/s3m/{effect => channel}/effect_orderjump.go (83%) rename format/s3m/{effect => channel}/effect_patterndelay.go (65%) rename format/s3m/{effect => channel}/effect_patternloop.go (85%) rename format/s3m/{effect => channel}/effect_portadown.go (78%) rename format/s3m/{effect => channel}/effect_portatonote.go (88%) rename format/s3m/{effect => channel}/effect_portaup.go (78%) rename format/s3m/{effect => channel}/effect_portavolslide.go (62%) rename format/s3m/{effect => channel}/effect_retrigvolslide.go (86%) rename format/s3m/{effect => channel}/effect_rowjump.go (81%) rename format/s3m/{effect => channel}/effect_sampleoffset.go (74%) rename format/s3m/{effect => channel}/effect_setfinetune.go (81%) rename format/s3m/{effect => channel}/effect_setglobalvolume.go (76%) rename format/s3m/{effect => channel}/effect_setpanposition.go (81%) rename format/s3m/{effect => channel}/effect_setspeed.go (73%) rename format/s3m/{effect => channel}/effect_settempo.go (73%) rename format/s3m/{effect => channel}/effect_settremolowaveform.go (78%) rename format/s3m/{effect => channel}/effect_setvibratowaveform.go (78%) rename format/s3m/{effect => channel}/effect_stereocontrol.go (83%) rename format/s3m/{effect => channel}/effect_tremolo.go (71%) rename format/s3m/{effect => channel}/effect_tremor.go (75%) rename format/s3m/{effect => channel}/effect_vibrato.go (80%) rename format/s3m/{effect => channel}/effect_vibratovolslide.go (62%) rename format/s3m/{effect => channel}/effect_volslidedown.go (80%) rename format/s3m/{effect => channel}/effect_volslideup.go (80%) rename format/s3m/{effect => channel}/effectfactory.go (87%) create mode 100644 format/s3m/channel/note_start.go create mode 100644 format/s3m/channel/note_stop.go create mode 100644 format/s3m/channel/notefactory.go rename format/s3m/{effect => channel}/unhandled.go (60%) rename format/s3m/{effect => channel}/util.go (69%) create mode 100644 format/s3m/channel/volume_setvolume.go create mode 100644 format/s3m/channel/volumefactory.go delete mode 100644 format/s3m/effect/intf/intf.go delete mode 100644 format/s3m/note/note.go rename format/xm/{effect => channel}/effect_arpeggio.go (53%) rename format/xm/{effect => channel}/effect_extrafineportadown.go (62%) rename format/xm/{effect => channel}/effect_extrafineportaup.go (62%) rename format/xm/{effect => channel}/effect_fineportadown.go (62%) rename format/xm/{effect => channel}/effect_fineportaup.go (62%) rename format/xm/{effect => channel}/effect_finevolslidedown.go (56%) rename format/xm/{effect => channel}/effect_finevolslideup.go (56%) rename format/{it/effect => xm/channel}/effect_globalvolumeslide.go (62%) create mode 100644 format/xm/channel/effect_notecut.go rename format/{it/effect => xm/channel}/effect_notedelay.go (53%) rename format/{it/effect => xm/channel}/effect_orderjump.go (53%) rename format/xm/{effect => channel}/effect_panslide.go (61%) rename format/xm/{effect => channel}/effect_patterndelay.go (53%) rename format/xm/{effect => channel}/effect_patternloop.go (72%) rename format/xm/{effect => channel}/effect_portadown.go (50%) rename format/xm/{effect => channel}/effect_portatonote.go (76%) rename format/xm/{effect => channel}/effect_portaup.go (50%) rename format/xm/{effect => channel}/effect_portavolslide.go (62%) rename format/xm/{effect => channel}/effect_retriggernote.go (64%) rename format/xm/{effect => channel}/effect_retrigvolslide.go (76%) rename format/xm/{effect => channel}/effect_rowjump.go (56%) rename format/xm/{effect => channel}/effect_sampleoffset.go (63%) rename format/xm/{effect => channel}/effect_setcoarsepanposition.go (65%) rename format/xm/{effect => channel}/effect_setenvelopeposition.go (60%) rename format/{it/effect => xm/channel}/effect_setfinetune.go (67%) rename format/xm/{effect => channel}/effect_setglobalvolume.go (68%) rename format/xm/{effect => channel}/effect_setpanposition.go (67%) rename format/xm/{effect => channel}/effect_setspeed.go (55%) rename format/xm/{effect => channel}/effect_settempo.go (50%) rename format/xm/{effect => channel}/effect_settremolowaveform.go (66%) rename format/{it/effect => xm/channel}/effect_setvibratowaveform.go (66%) rename format/xm/{effect => channel}/effect_setvolume.go (59%) rename format/xm/{effect => channel}/effect_tremolo.go (52%) create mode 100644 format/xm/channel/effect_tremor.go rename format/xm/{effect => channel}/effect_vibrato.go (53%) rename format/xm/{effect => channel}/effect_vibratovolslide.go (61%) rename format/xm/{effect => channel}/effect_volslide.go (67%) rename format/xm/{effect => channel}/effectfactory.go (73%) rename format/xm/{effect => channel}/effectfactory_standard.go (89%) rename format/xm/{effect => channel}/effectfactory_volume.go (81%) rename format/xm/{effect => channel}/unhandled.go (72%) rename format/xm/{effect => channel}/util.go (65%) delete mode 100644 format/xm/effect/effect_notecut.go delete mode 100644 format/xm/effect/effect_tremor.go delete mode 100644 format/xm/effect/intf/intf.go diff --git a/effect.go b/effect.go index f066771..37cb1ca 100644 --- a/effect.go +++ b/effect.go @@ -2,8 +2,10 @@ package playback import ( "fmt" + "reflect" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" ) // Effect is an interface to command/effect @@ -11,10 +13,36 @@ type Effect interface { //fmt.Stringer } +type Effecter[TMemory any] interface { + GetEffects(*TMemory, period.Period) []Effect +} + +func GetEffects[TPeriod period.Period, TMemory any](mem *TMemory, d song.ChannelData) []Effect { + var e []Effect + if eff, ok := d.(Effecter[TMemory]); ok { + var p TPeriod + e = eff.GetEffects(mem, p) + } + return e +} + +type EffectNamer interface { + Names() []string +} + type effectPreStartIntf[TPeriod period.Period, TMemory any] interface { PreStart(Channel[TPeriod, TMemory], Playback) error } +func GetEffectNames(e Effect) []string { + if namer, ok := e.(EffectNamer); ok { + return namer.Names() + } else { + typ := reflect.TypeOf(e) + return []string{typ.Name()} + } +} + // EffectPreStart triggers when the effect enters onto the channel state func EffectPreStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback) error { if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory]); ok { @@ -123,6 +151,14 @@ func (e CombinedEffect[TPeriod, TMemory]) String() string { return "" } +func (e CombinedEffect[TPeriod, TMemory]) Names() []string { + var names []string + for _, eff := range e.Effects { + names = append(names, GetEffectNames(eff)...) + } + return names +} + // DoEffect runs the standard tick lifetime of an effect func DoEffect[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, currentTick int, lastTick bool) error { if e == nil { diff --git a/format/it/channel/data.go b/format/it/channel/data.go index 4896593..3a7d753 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -7,10 +7,13 @@ 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" + itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) const MaxTotalChannels = 64 @@ -102,6 +105,22 @@ func (d Data) Channel() uint8 { return 0 } +func (d Data) GetEffects(mem *Memory, periodType period.Period) []playback.Effect { + switch periodType.(type) { + case itPeriod.Linear: + if e := EffectFactory[itPeriod.Linear](mem, d); e != nil { + return []playback.Effect{e} + } + case itPeriod.Amiga: + if e := EffectFactory[itPeriod.Amiga](mem, d); e != nil { + return []playback.Effect{e} + } + default: + panic("unhandled period type") + } + return nil +} + func (Data) getNoteString(n note.Note) string { switch note.Type(n) { case note.SpecialTypeRelease: diff --git a/format/it/effect/effect_arpeggio.go b/format/it/channel/effect_arpeggio.go similarity index 50% rename from format/it/effect/effect_arpeggio.go rename to format/it/channel/effect_arpeggio.go index db1701f..0265805 100644 --- a/format/it/effect/effect_arpeggio.go +++ b/format/it/channel/effect_arpeggio.go @@ -1,18 +1,18 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" + "github.com/gotracker/playback/period" ) // Arpeggio defines an arpeggio effect -type Arpeggio[TPeriod period.Period] channel.DataEffect // 'J' +type Arpeggio[TPeriod period.Period] DataEffect // 'J' // Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -20,12 +20,12 @@ func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p } // Tick is called on every tick -func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Arpeggio(channel.DataEffect(e)) + x, y := mem.Arpeggio(DataEffect(e)) return doArpeggio(cs, currentTick, int8(x), int8(y)) } func (e Arpeggio[TPeriod]) String() string { - return fmt.Sprintf("J%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("J%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_channelvolumeslide.go b/format/it/channel/effect_channelvolumeslide.go similarity index 74% rename from format/it/effect/effect_channelvolumeslide.go rename to format/it/channel/effect_channelvolumeslide.go index 4109186..6a92b9f 100644 --- a/format/it/effect/effect_channelvolumeslide.go +++ b/format/it/channel/effect_channelvolumeslide.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,19 +6,18 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // ChannelVolumeSlide defines a set channel volume effect -type ChannelVolumeSlide[TPeriod period.Period] channel.DataEffect // 'Nxy' +type ChannelVolumeSlide[TPeriod period.Period] DataEffect // 'Nxy' // Start triggers on the first tick, but before the Tick() function is called -func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - x, y := mem.ChannelVolumeSlide(channel.DataEffect(e)) + x, y := mem.ChannelVolumeSlide(DataEffect(e)) switch { case y == 0x0 && x != 0xF: @@ -40,9 +39,9 @@ func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel. } // Tick is called on every tick -func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.ChannelVolumeSlide(channel.DataEffect(e)) + x, y := mem.ChannelVolumeSlide(DataEffect(e)) switch { case y == 0x0 && x != 0xF: @@ -65,5 +64,5 @@ func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.M } func (e ChannelVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("N%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("N%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_extrafineportadown.go b/format/it/channel/effect_extrafineportadown.go similarity index 61% rename from format/it/effect/effect_extrafineportadown.go rename to format/it/channel/effect_extrafineportadown.go index 003c25e..96bcb0d 100644 --- a/format/it/effect/effect_extrafineportadown.go +++ b/format/it/channel/effect_extrafineportadown.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // ExtraFinePortaDown defines an extra-fine portamento down effect -type ExtraFinePortaDown[TPeriod period.Period] channel.DataEffect // 'EEx' +type ExtraFinePortaDown[TPeriod period.Period] DataEffect // 'EEx' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - y := mem.PortaDown(channel.DataEffect(e)) & 0x0F + y := mem.PortaDown(DataEffect(e)) & 0x0F return doPortaDown(cs, float32(y), 1) } func (e ExtraFinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_extrafineportaup.go b/format/it/channel/effect_extrafineportaup.go similarity index 61% rename from format/it/effect/effect_extrafineportaup.go rename to format/it/channel/effect_extrafineportaup.go index 632bbc2..fc2411d 100644 --- a/format/it/effect/effect_extrafineportaup.go +++ b/format/it/channel/effect_extrafineportaup.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // ExtraFinePortaUp defines an extra-fine portamento up effect -type ExtraFinePortaUp[TPeriod period.Period] channel.DataEffect // 'FEx' +type ExtraFinePortaUp[TPeriod period.Period] DataEffect // 'FEx' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - y := mem.PortaUp(channel.DataEffect(e)) & 0x0F + y := mem.PortaUp(DataEffect(e)) & 0x0F return doPortaUp(cs, float32(y), 1) } func (e ExtraFinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_finepatterndelay.go b/format/it/channel/effect_finepatterndelay.go similarity index 55% rename from format/it/effect/effect_finepatterndelay.go rename to format/it/channel/effect_finepatterndelay.go index 339f911..6ecf96f 100644 --- a/format/it/effect/effect_finepatterndelay.go +++ b/format/it/channel/effect_finepatterndelay.go @@ -1,24 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" "github.com/gotracker/playback/period" ) // FinePatternDelay defines an fine pattern delay effect -type FinePatternDelay[TPeriod period.Period] channel.DataEffect // 'S6x' +type FinePatternDelay[TPeriod period.Period] DataEffect // 'S6x' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf - m := p.(effectIntf.IT) + m := p.(IT) if err := m.AddRowTicks(int(x)); err != nil { return err } @@ -26,5 +24,5 @@ func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Me } func (e FinePatternDelay[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_fineportadown.go b/format/it/channel/effect_fineportadown.go similarity index 61% rename from format/it/effect/effect_fineportadown.go rename to format/it/channel/effect_fineportadown.go index 9ad107c..b12e60b 100644 --- a/format/it/effect/effect_fineportadown.go +++ b/format/it/channel/effect_fineportadown.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // FinePortaDown defines an fine portamento down effect -type FinePortaDown[TPeriod period.Period] channel.DataEffect // 'EFx' +type FinePortaDown[TPeriod period.Period] DataEffect // 'EFx' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - y := mem.PortaDown(channel.DataEffect(e)) & 0x0F + y := mem.PortaDown(DataEffect(e)) & 0x0F return doPortaDown(cs, float32(y), 4) } func (e FinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_fineportaup.go b/format/it/channel/effect_fineportaup.go similarity index 61% rename from format/it/effect/effect_fineportaup.go rename to format/it/channel/effect_fineportaup.go index 83f6b66..bc5562b 100644 --- a/format/it/effect/effect_fineportaup.go +++ b/format/it/channel/effect_fineportaup.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // FinePortaUp defines an fine portamento up effect -type FinePortaUp[TPeriod period.Period] channel.DataEffect // 'FFx' +type FinePortaUp[TPeriod period.Period] DataEffect // 'FFx' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - y := mem.PortaUp(channel.DataEffect(e)) & 0x0F + y := mem.PortaUp(DataEffect(e)) & 0x0F return doPortaUp(cs, float32(y), 4) } func (e FinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_finevibrato.go b/format/it/channel/effect_finevibrato.go similarity index 63% rename from format/it/effect/effect_finevibrato.go rename to format/it/channel/effect_finevibrato.go index 2019592..fd5e94c 100644 --- a/format/it/effect/effect_finevibrato.go +++ b/format/it/channel/effect_finevibrato.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // FineVibrato defines an fine vibrato effect -type FineVibrato[TPeriod period.Period] channel.DataEffect // 'U' +type FineVibrato[TPeriod period.Period] DataEffect // 'U' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVibrato[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FineVibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) + x, y := mem.Vibrato(DataEffect(e)) if currentTick != 0 { return doVibrato(cs, currentTick, x, y, 1) } @@ -29,5 +28,5 @@ func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], } func (e FineVibrato[TPeriod]) String() string { - return fmt.Sprintf("U%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("U%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_finevolslidedown.go b/format/it/channel/effect_finevolslidedown.go similarity index 63% rename from format/it/effect/effect_finevolslidedown.go rename to format/it/channel/effect_finevolslidedown.go index f9d37ce..e1d2f81 100644 --- a/format/it/effect/effect_finevolslidedown.go +++ b/format/it/channel/effect_finevolslidedown.go @@ -1,26 +1,25 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // FineVolumeSlideDown defines a fine volume slide down effect -type FineVolumeSlideDown[TPeriod period.Period] channel.DataEffect // 'D' +type FineVolumeSlideDown[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - _, y := mem.VolumeSlide(channel.DataEffect(e)) + _, y := mem.VolumeSlide(DataEffect(e)) if y != 0x0F && currentTick == 0 { return doVolSlide(cs, -float32(y), 1.0) @@ -29,22 +28,22 @@ func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel. } func (e FineVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } //==================================================== // VolChanFineVolumeSlideDown defines a fine volume slide down effect (from the volume channel) -type VolChanFineVolumeSlideDown[TPeriod period.Period] channel.DataEffect // 'd' +type VolChanFineVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' // Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolChanFineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { mem := cs.GetMemory() - y := mem.VolChanVolumeSlide(channel.DataEffect(e)) + y := mem.VolChanVolumeSlide(DataEffect(e)) return doVolSlide(cs, -float32(y), 1.0) } func (e VolChanFineVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("dF%x", channel.DataEffect(e)) + return fmt.Sprintf("dF%x", DataEffect(e)) } diff --git a/format/it/effect/effect_finevolslideup.go b/format/it/channel/effect_finevolslideup.go similarity index 63% rename from format/it/effect/effect_finevolslideup.go rename to format/it/channel/effect_finevolslideup.go index 54706fd..79e0911 100644 --- a/format/it/effect/effect_finevolslideup.go +++ b/format/it/channel/effect_finevolslideup.go @@ -1,26 +1,25 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // FineVolumeSlideUp defines a fine volume slide up effect -type FineVolumeSlideUp[TPeriod period.Period] channel.DataEffect // 'D' +type FineVolumeSlideUp[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, _ := mem.VolumeSlide(channel.DataEffect(e)) + x, _ := mem.VolumeSlide(DataEffect(e)) if x != 0x0F && currentTick == 0 { return doVolSlide(cs, float32(x), 1.0) @@ -29,22 +28,22 @@ func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Me } func (e FineVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } //==================================================== // VolChanFineVolumeSlideUp defines a fine volume slide up effect (from the volume channel) -type VolChanFineVolumeSlideUp[TPeriod period.Period] channel.DataEffect // 'd' +type VolChanFineVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' // Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolChanFineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { mem := cs.GetMemory() - x := mem.VolChanVolumeSlide(channel.DataEffect(e)) + x := mem.VolChanVolumeSlide(DataEffect(e)) return doVolSlide(cs, float32(x), 1.0) } func (e VolChanFineVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("d%xF", channel.DataEffect(e)) + return fmt.Sprintf("d%xF", DataEffect(e)) } diff --git a/format/xm/effect/effect_globalvolumeslide.go b/format/it/channel/effect_globalvolumeslide.go similarity index 62% rename from format/xm/effect/effect_globalvolumeslide.go rename to format/it/channel/effect_globalvolumeslide.go index 57dbe8f..7073443 100644 --- a/format/xm/effect/effect_globalvolumeslide.go +++ b/format/it/channel/effect_globalvolumeslide.go @@ -1,33 +1,31 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" "github.com/gotracker/playback/period" ) // GlobalVolumeSlide defines a global volume slide effect -type GlobalVolumeSlide[TPeriod period.Period] channel.DataEffect // 'H' +type GlobalVolumeSlide[TPeriod period.Period] DataEffect // 'W' // Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) + x, y := mem.GlobalVolumeSlide(DataEffect(e)) if currentTick == 0 { return nil } - m := p.(effectIntf.XM) + m := p.(IT) if x == 0 { // global vol slide down @@ -40,5 +38,5 @@ func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Me } func (e GlobalVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("H%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("W%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_highoffset.go b/format/it/channel/effect_highoffset.go similarity index 62% rename from format/it/effect/effect_highoffset.go rename to format/it/channel/effect_highoffset.go index 7a060e7..2065541 100644 --- a/format/it/effect/effect_highoffset.go +++ b/format/it/channel/effect_highoffset.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // HighOffset defines a sample high offset effect -type HighOffset[TPeriod period.Period] channel.DataEffect // 'SAx' +type HighOffset[TPeriod period.Period] DataEffect // 'SAx' // Start triggers on the first tick, but before the Tick() function is called -func (e HighOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e HighOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - xx := channel.DataEffect(e) + xx := DataEffect(e) mem.HighOffset = int(xx) * 0x10000 return nil } func (e HighOffset[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_newnoteactionnotecontinue.go b/format/it/channel/effect_newnoteactionnotecontinue.go similarity index 64% rename from format/it/effect/effect_newnoteactionnotecontinue.go rename to format/it/channel/effect_newnoteactionnotecontinue.go index 01081ef..ac05cdb 100644 --- a/format/it/effect/effect_newnoteactionnotecontinue.go +++ b/format/it/channel/effect_newnoteactionnotecontinue.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // NewNoteActionNoteContinue defines a NewNoteAction: Note Continue effect -type NewNoteActionNoteContinue[TPeriod period.Period] channel.DataEffect // 'S74' +type NewNoteActionNoteContinue[TPeriod period.Period] DataEffect // 'S74' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteContinue[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e NewNoteActionNoteContinue[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionContinue) return nil } func (e NewNoteActionNoteContinue[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_newnoteactionnotecut.go b/format/it/channel/effect_newnoteactionnotecut.go similarity index 64% rename from format/it/effect/effect_newnoteactionnotecut.go rename to format/it/channel/effect_newnoteactionnotecut.go index 7fe5911..e43fc80 100644 --- a/format/it/effect/effect_newnoteactionnotecut.go +++ b/format/it/channel/effect_newnoteactionnotecut.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // NewNoteActionNoteCut defines a NewNoteAction: Note Cut effect -type NewNoteActionNoteCut[TPeriod period.Period] channel.DataEffect // 'S73' +type NewNoteActionNoteCut[TPeriod period.Period] DataEffect // 'S73' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e NewNoteActionNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionCut) return nil } func (e NewNoteActionNoteCut[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_newnoteactionnotefade.go b/format/it/channel/effect_newnoteactionnotefade.go similarity index 64% rename from format/it/effect/effect_newnoteactionnotefade.go rename to format/it/channel/effect_newnoteactionnotefade.go index f6c4ae8..4c1baf7 100644 --- a/format/it/effect/effect_newnoteactionnotefade.go +++ b/format/it/channel/effect_newnoteactionnotefade.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // NewNoteActionNoteFade defines a NewNoteAction: Note Fade effect -type NewNoteActionNoteFade[TPeriod period.Period] channel.DataEffect // 'S76' +type NewNoteActionNoteFade[TPeriod period.Period] DataEffect // 'S76' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e NewNoteActionNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionFadeout) return nil } func (e NewNoteActionNoteFade[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_newnoteactionnoteoff.go b/format/it/channel/effect_newnoteactionnoteoff.go similarity index 64% rename from format/it/effect/effect_newnoteactionnoteoff.go rename to format/it/channel/effect_newnoteactionnoteoff.go index 4a6e04e..641cb1b 100644 --- a/format/it/effect/effect_newnoteactionnoteoff.go +++ b/format/it/channel/effect_newnoteactionnoteoff.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // NewNoteActionNoteOff defines a NewNoteAction: Note Off effect -type NewNoteActionNoteOff[TPeriod period.Period] channel.DataEffect // 'S75' +type NewNoteActionNoteOff[TPeriod period.Period] DataEffect // 'S75' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e NewNoteActionNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.SetNewNoteAction(note.ActionRelease) return nil } func (e NewNoteActionNoteOff[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/channel/effect_notecut.go b/format/it/channel/effect_notecut.go new file mode 100644 index 0000000..a5daab4 --- /dev/null +++ b/format/it/channel/effect_notecut.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +// NoteCut defines a note cut effect +type NoteCut[TPeriod period.Period] DataEffect // 'SCx' + +// Start triggers on the first tick, but before the Tick() function is called +func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.ResetRetriggerCount() + return nil +} + +// Tick is called on every tick +func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + x := DataEffect(e) & 0xf + + if x != 0 && currentTick == int(x) { + cs.FreezePlayback() + } + return nil +} + +func (e NoteCut[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} diff --git a/format/xm/effect/effect_notedelay.go b/format/it/channel/effect_notedelay.go similarity index 53% rename from format/xm/effect/effect_notedelay.go rename to format/it/channel/effect_notedelay.go index ab2fe86..c2493f9 100644 --- a/format/xm/effect/effect_notedelay.go +++ b/format/it/channel/effect_notedelay.go @@ -1,29 +1,28 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // NoteDelay defines a note delay effect -type NoteDelay[TPeriod period.Period] channel.DataEffect // 'EDx' +type NoteDelay[TPeriod period.Period] DataEffect // 'SDx' // PreStart triggers when the effect enters onto the channel state -func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(channel.DataEffect(e)&0x0F)) +func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) return nil } // Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e NoteDelay[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_orderjump.go b/format/it/channel/effect_orderjump.go similarity index 53% rename from format/xm/effect/effect_orderjump.go rename to format/it/channel/effect_orderjump.go index ff97ead..45257a8 100644 --- a/format/xm/effect/effect_orderjump.go +++ b/format/it/channel/effect_orderjump.go @@ -1,28 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" ) // OrderJump defines an order jump effect -type OrderJump[TPeriod period.Period] channel.DataEffect // 'B' +type OrderJump[TPeriod period.Period] DataEffect // 'B' // Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], 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[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { +func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } func (e OrderJump[TPeriod]) String() string { - return fmt.Sprintf("B%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("B%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_panningenvelopeoff.go b/format/it/channel/effect_panningenvelopeoff.go similarity index 63% rename from format/it/effect/effect_panningenvelopeoff.go rename to format/it/channel/effect_panningenvelopeoff.go index 3366e4f..dbf5cb8 100644 --- a/format/it/effect/effect_panningenvelopeoff.go +++ b/format/it/channel/effect_panningenvelopeoff.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // PanningEnvelopeOff defines a panning envelope: off effect -type PanningEnvelopeOff[TPeriod period.Period] channel.DataEffect // 'S79' +type PanningEnvelopeOff[TPeriod period.Period] DataEffect // 'S79' // Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(false) @@ -20,5 +19,5 @@ func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel. } func (e PanningEnvelopeOff[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_panningenvelopeon.go b/format/it/channel/effect_panningenvelopeon.go similarity index 63% rename from format/it/effect/effect_panningenvelopeon.go rename to format/it/channel/effect_panningenvelopeon.go index 29d708d..85359fe 100644 --- a/format/it/effect/effect_panningenvelopeon.go +++ b/format/it/channel/effect_panningenvelopeon.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // PanningEnvelopeOn defines a panning envelope: on effect -type PanningEnvelopeOn[TPeriod period.Period] channel.DataEffect // 'S7A' +type PanningEnvelopeOn[TPeriod period.Period] DataEffect // 'S7A' // Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(true) @@ -20,5 +19,5 @@ func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.M } func (e PanningEnvelopeOn[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_pastnotecut.go b/format/it/channel/effect_pastnotecut.go similarity index 64% rename from format/it/effect/effect_pastnotecut.go rename to format/it/channel/effect_pastnotecut.go index 097ce8f..7241f5d 100644 --- a/format/it/effect/effect_pastnotecut.go +++ b/format/it/channel/effect_pastnotecut.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // PastNoteCut defines a past note cut effect -type PastNoteCut[TPeriod period.Period] channel.DataEffect // 'S70' +type PastNoteCut[TPeriod period.Period] DataEffect // 'S70' // Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PastNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionCut) return nil } func (e PastNoteCut[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_pastnotefadeout.go b/format/it/channel/effect_pastnotefadeout.go similarity index 64% rename from format/it/effect/effect_pastnotefadeout.go rename to format/it/channel/effect_pastnotefadeout.go index a6e35f6..88f4f8d 100644 --- a/format/it/effect/effect_pastnotefadeout.go +++ b/format/it/channel/effect_pastnotefadeout.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // PastNoteFade defines a past note fadeout effect -type PastNoteFade[TPeriod period.Period] channel.DataEffect // 'S72' +type PastNoteFade[TPeriod period.Period] DataEffect // 'S72' // Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PastNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionFadeout) return nil } func (e PastNoteFade[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_pastnoteoff.go b/format/it/channel/effect_pastnoteoff.go similarity index 64% rename from format/it/effect/effect_pastnoteoff.go rename to format/it/channel/effect_pastnoteoff.go index c925480..aa14eac 100644 --- a/format/it/effect/effect_pastnoteoff.go +++ b/format/it/channel/effect_pastnoteoff.go @@ -1,23 +1,22 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // PastNoteOff defines a past note off effect -type PastNoteOff[TPeriod period.Period] channel.DataEffect // 'S71' +type PastNoteOff[TPeriod period.Period] DataEffect // 'S71' // Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PastNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionRelease) return nil } func (e PastNoteOff[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_patterndelay.go b/format/it/channel/effect_patterndelay.go similarity index 53% rename from format/it/effect/effect_patterndelay.go rename to format/it/channel/effect_patterndelay.go index 0b938d4..1cf3c49 100644 --- a/format/it/effect/effect_patterndelay.go +++ b/format/it/channel/effect_patterndelay.go @@ -1,29 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" "github.com/gotracker/playback/period" ) // PatternDelay defines a pattern delay effect -type PatternDelay[TPeriod period.Period] channel.DataEffect // 'SEx' +type PatternDelay[TPeriod period.Period] DataEffect // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - m := p.(effectIntf.IT) - return m.SetPatternDelay(int(channel.DataEffect(e) & 0x0F)) +func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + m := p.(IT) + return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) } // Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e PatternDelay[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_patternloop.go b/format/it/channel/effect_patternloop.go similarity index 75% rename from format/it/effect/effect_patternloop.go rename to format/it/channel/effect_patternloop.go index c6ff81e..4ea053e 100644 --- a/format/it/effect/effect_patternloop.go +++ b/format/it/channel/effect_patternloop.go @@ -1,24 +1,23 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // PatternLoop defines a pattern loop effect -type PatternLoop[TPeriod period.Period] channel.DataEffect // 'SBx' +type PatternLoop[TPeriod period.Period] DataEffect // 'SBx' // Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], 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[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { +func (e PatternLoop[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { x := uint8(e) & 0xF mem := cs.GetMemory() @@ -41,5 +40,5 @@ func (e PatternLoop[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], } func (e PatternLoop[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_pitchenvelopeoff.go b/format/it/channel/effect_pitchenvelopeoff.go similarity index 63% rename from format/it/effect/effect_pitchenvelopeoff.go rename to format/it/channel/effect_pitchenvelopeoff.go index c214b6b..e54d422 100644 --- a/format/it/effect/effect_pitchenvelopeoff.go +++ b/format/it/channel/effect_pitchenvelopeoff.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // PitchEnvelopeOff defines a panning envelope: off effect -type PitchEnvelopeOff[TPeriod period.Period] channel.DataEffect // 'S7B' +type PitchEnvelopeOff[TPeriod period.Period] DataEffect // 'S7B' // Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(false) @@ -20,5 +19,5 @@ func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Me } func (e PitchEnvelopeOff[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_pitchenvelopeon.go b/format/it/channel/effect_pitchenvelopeon.go similarity index 63% rename from format/it/effect/effect_pitchenvelopeon.go rename to format/it/channel/effect_pitchenvelopeon.go index 1afb9f3..f3ee5b3 100644 --- a/format/it/effect/effect_pitchenvelopeon.go +++ b/format/it/channel/effect_pitchenvelopeon.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // PitchEnvelopeOn defines a panning envelope: on effect -type PitchEnvelopeOn[TPeriod period.Period] channel.DataEffect // 'S7C' +type PitchEnvelopeOn[TPeriod period.Period] DataEffect // 'S7C' // Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(true) @@ -20,5 +19,5 @@ func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Mem } func (e PitchEnvelopeOn[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/channel/effect_portadown.go b/format/it/channel/effect_portadown.go new file mode 100644 index 0000000..0562b25 --- /dev/null +++ b/format/it/channel/effect_portadown.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +// PortaDown defines a portamento down effect +type PortaDown[TPeriod period.Period] DataEffect // 'E' + +// Start triggers on the first tick, but before the Tick() function is called +func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.ResetRetriggerCount() + cs.UnfreezePlayback() + return nil +} + +// Tick is called on every tick +func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + mem := cs.GetMemory() + xx := mem.PortaDown(DataEffect(e)) + + return doPortaDown(cs, float32(xx), 4) +} + +func (e PortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} diff --git a/format/it/effect/effect_portatonote.go b/format/it/channel/effect_portatonote.go similarity index 81% rename from format/it/effect/effect_portatonote.go rename to format/it/channel/effect_portatonote.go index cd652f9..f6f5467 100644 --- a/format/it/effect/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" itPeriod "github.com/gotracker/playback/format/it/period" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" @@ -12,10 +11,10 @@ import ( ) // PortaToNote defines a portamento-to-note effect -type PortaToNote[TPeriod period.Period] channel.DataEffect // 'G' +type PortaToNote[TPeriod period.Period] DataEffect // 'G' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -26,9 +25,9 @@ func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory] } // Tick is called on every tick -func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - xx := mem.PortaToNote(channel.DataEffect(e)) + xx := mem.PortaToNote(DataEffect(e)) // vibrato modifies current period for portamento cur := cs.GetPeriod() @@ -55,5 +54,5 @@ func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], } func (e PortaToNote[TPeriod]) String() string { - return fmt.Sprintf("G%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("G%0.2x", DataEffect(e)) } diff --git a/format/it/channel/effect_portaup.go b/format/it/channel/effect_portaup.go new file mode 100644 index 0000000..5643415 --- /dev/null +++ b/format/it/channel/effect_portaup.go @@ -0,0 +1,30 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +// PortaUp defines a portamento up effect +type PortaUp[TPeriod period.Period] DataEffect // 'F' + +// Start triggers on the first tick, but before the Tick() function is called +func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.ResetRetriggerCount() + cs.UnfreezePlayback() + return nil +} + +// Tick is called on every tick +func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + mem := cs.GetMemory() + xx := mem.PortaUp(DataEffect(e)) + + return doPortaUp(cs, float32(xx), 4) +} + +func (e PortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} diff --git a/format/it/effect/effect_portavolslide.go b/format/it/channel/effect_portavolslide.go similarity index 62% rename from format/it/effect/effect_portavolslide.go rename to format/it/channel/effect_portavolslide.go index 62dac63..9f610f5 100644 --- a/format/it/effect/effect_portavolslide.go +++ b/format/it/channel/effect_portavolslide.go @@ -1,20 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "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, channel.Memory] + playback.CombinedEffect[TPeriod, Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide[TPeriod period.Period](mem *channel.Memory, cd channel.Command, val channel.DataEffect) PortaVolumeSlide[TPeriod] { +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)) @@ -22,5 +21,5 @@ func NewPortaVolumeSlide[TPeriod period.Period](mem *channel.Memory, cd channel. } func (e PortaVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("L%0.2x", e.Effects[0].(channel.DataEffect)) + return fmt.Sprintf("L%0.2x", e.Effects[0].(DataEffect)) } diff --git a/format/it/effect/effect_retrigvolslide.go b/format/it/channel/effect_retrigvolslide.go similarity index 77% rename from format/it/effect/effect_retrigvolslide.go rename to format/it/channel/effect_retrigvolslide.go index 856dc43..9e47cb1 100644 --- a/format/it/effect/effect_retrigvolslide.go +++ b/format/it/channel/effect_retrigvolslide.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,30 +6,29 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide[TPeriod period.Period] channel.DataEffect // 'Q' +type RetrigVolumeSlide[TPeriod period.Period] DataEffect // 'Q' // Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.RetrigVolumeSlide(channel.DataEffect(e)) + x, y := mem.RetrigVolumeSlide(DataEffect(e)) if y == 0 { return nil } rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= x { + if DataEffect(rt) >= x { cs.SetPos(sampling.Pos{}) cs.ResetRetriggerCount() switch x { @@ -68,5 +67,5 @@ func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Me } func (e RetrigVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("Q%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("Q%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_rowjump.go b/format/it/channel/effect_rowjump.go similarity index 52% rename from format/it/effect/effect_rowjump.go rename to format/it/channel/effect_rowjump.go index 3fbc03a..5a12f61 100644 --- a/format/it/effect/effect_rowjump.go +++ b/format/it/channel/effect_rowjump.go @@ -1,30 +1,29 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" ) // RowJump defines a row jump effect -type RowJump[TPeriod period.Period] channel.DataEffect // 'C' +type RowJump[TPeriod period.Period] DataEffect // 'C' // Start triggers on the first tick, but before the Tick() function is called -func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], 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[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { - r := channel.DataEffect(e) +func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { + r := DataEffect(e) rowIdx := index.Row(r) return p.SetNextRow(rowIdx) } func (e RowJump[TPeriod]) String() string { - return fmt.Sprintf("C%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("C%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_sampleoffset.go b/format/it/channel/effect_sampleoffset.go similarity index 70% rename from format/it/effect/effect_sampleoffset.go rename to format/it/channel/effect_sampleoffset.go index 49cb1a4..c7eecff 100644 --- a/format/it/effect/effect_sampleoffset.go +++ b/format/it/channel/effect_sampleoffset.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,18 +6,17 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // SampleOffset defines a sample offset effect -type SampleOffset[TPeriod period.Period] channel.DataEffect // 'O' +type SampleOffset[TPeriod period.Period] DataEffect // 'O' // Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - xx := mem.SampleOffset(channel.DataEffect(e)) + xx := mem.SampleOffset(DataEffect(e)) pos := sampling.Pos{Pos: mem.HighOffset + int(xx)*0x100} if mem.Shared.OldEffectMode { @@ -31,5 +30,5 @@ func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory } func (e SampleOffset[TPeriod]) String() string { - return fmt.Sprintf("O%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("O%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setchannelvolume.go b/format/it/channel/effect_setchannelvolume.go similarity index 68% rename from format/it/effect/effect_setchannelvolume.go rename to format/it/channel/effect_setchannelvolume.go index 55dd96d..e009dc0 100644 --- a/format/it/effect/effect_setchannelvolume.go +++ b/format/it/channel/effect_setchannelvolume.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -7,18 +7,17 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // SetChannelVolume defines a set channel volume effect -type SetChannelVolume[TPeriod period.Period] channel.DataEffect // 'Mxx' +type SetChannelVolume[TPeriod period.Period] DataEffect // 'Mxx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - xx := channel.DataEffect(e) + xx := DataEffect(e) cv := itfile.Volume(xx) @@ -32,5 +31,5 @@ func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Me } func (e SetChannelVolume[TPeriod]) String() string { - return fmt.Sprintf("M%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("M%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setcoarsepanposition.go b/format/it/channel/effect_setcoarsepanposition.go similarity index 67% rename from format/it/effect/effect_setcoarsepanposition.go rename to format/it/channel/effect_setcoarsepanposition.go index c690d03..f22d60c 100644 --- a/format/it/effect/effect_setcoarsepanposition.go +++ b/format/it/channel/effect_setcoarsepanposition.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,19 +6,18 @@ import ( 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" "github.com/gotracker/playback/period" ) // SetCoarsePanPosition defines a set coarse pan position effect -type SetCoarsePanPosition[TPeriod period.Period] channel.DataEffect // 'S8x' +type SetCoarsePanPosition[TPeriod period.Period] DataEffect // 'S8x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf pan := itfile.PanValue(x << 2) @@ -27,5 +26,5 @@ func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channe } func (e SetCoarsePanPosition[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setfinetune.go b/format/it/channel/effect_setfinetune.go similarity index 67% rename from format/xm/effect/effect_setfinetune.go rename to format/it/channel/effect_setfinetune.go index 901172b..7929c39 100644 --- a/format/xm/effect/effect_setfinetune.go +++ b/format/it/channel/effect_setfinetune.go @@ -1,20 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // SetFinetune defines a mod-style set finetune effect -type SetFinetune[TPeriod period.Period] channel.DataEffect // 'E5x' +type SetFinetune[TPeriod period.Period] DataEffect // 'S2x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - x := channel.DataEffect(e) & 0xf +func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + x := DataEffect(e) & 0xf inst := cs.GetTargetInst() if inst != nil { @@ -25,11 +24,11 @@ func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memo } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e SetFinetune[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setglobalvolume.go b/format/it/channel/effect_setglobalvolume.go similarity index 64% rename from format/it/effect/effect_setglobalvolume.go rename to format/it/channel/effect_setglobalvolume.go index cd6f2a2..64d4369 100644 --- a/format/it/effect/effect_setglobalvolume.go +++ b/format/it/channel/effect_setglobalvolume.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,16 +6,15 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // SetGlobalVolume defines a set global volume effect -type SetGlobalVolume[TPeriod period.Period] channel.DataEffect // 'V' +type SetGlobalVolume[TPeriod period.Period] DataEffect // 'V' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - v := volume.Volume(channel.DataEffect(e)) / 0x80 +func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + v := volume.Volume(DataEffect(e)) / 0x80 if v > 1 { v = 1 } @@ -24,11 +23,11 @@ func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel. } // Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e SetGlobalVolume[TPeriod]) String() string { - return fmt.Sprintf("V%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("V%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setpanbrellowaveform.go b/format/it/channel/effect_setpanbrellowaveform.go similarity index 66% rename from format/it/effect/effect_setpanbrellowaveform.go rename to format/it/channel/effect_setpanbrellowaveform.go index fc3459d..c3e668f 100644 --- a/format/it/effect/effect_setpanbrellowaveform.go +++ b/format/it/channel/effect_setpanbrellowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -7,17 +7,16 @@ import ( "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[TPeriod period.Period] channel.DataEffect // 'S5x' +type SetPanbrelloWaveform[TPeriod period.Period] DataEffect // 'S5x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() panb := mem.PanbrelloOscillator() @@ -26,5 +25,5 @@ func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channe } func (e SetPanbrelloWaveform[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setpanposition.go b/format/it/channel/effect_setpanposition.go similarity index 68% rename from format/it/effect/effect_setpanposition.go rename to format/it/channel/effect_setpanposition.go index 199501e..f306bf0 100644 --- a/format/it/effect/effect_setpanposition.go +++ b/format/it/channel/effect_setpanposition.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,19 +6,18 @@ import ( 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" "github.com/gotracker/playback/period" ) // SetPanPosition defines a set pan position effect -type SetPanPosition[TPeriod period.Period] channel.DataEffect // 'Xxx' +type SetPanPosition[TPeriod period.Period] DataEffect // 'Xxx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) + x := DataEffect(e) pan := itfile.PanValue(x) @@ -27,5 +26,5 @@ func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memo } func (e SetPanPosition[TPeriod]) String() string { - return fmt.Sprintf("X%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("X%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setspeed.go b/format/it/channel/effect_setspeed.go similarity index 55% rename from format/it/effect/effect_setspeed.go rename to format/it/channel/effect_setspeed.go index d17cf2a..302bcdc 100644 --- a/format/it/effect/effect_setspeed.go +++ b/format/it/channel/effect_setspeed.go @@ -1,21 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" "github.com/gotracker/playback/period" ) // SetSpeed defines a set speed effect -type SetSpeed[TPeriod period.Period] channel.DataEffect // 'A' +type SetSpeed[TPeriod period.Period] DataEffect // 'A' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { if e != 0 { - m := p.(effectIntf.IT) + m := p.(IT) if err := m.SetTicks(int(e)); err != nil { return err } @@ -24,11 +22,11 @@ func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory] } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e SetSpeed[TPeriod]) String() string { - return fmt.Sprintf("A%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("A%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_settempo.go b/format/it/channel/effect_settempo.go similarity index 56% rename from format/it/effect/effect_settempo.go rename to format/it/channel/effect_settempo.go index 28df49d..e36f196 100644 --- a/format/it/effect/effect_settempo.go +++ b/format/it/channel/effect_settempo.go @@ -1,21 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" "github.com/gotracker/playback/period" ) // SetTempo defines a set tempo effect -type SetTempo[TPeriod period.Period] channel.DataEffect // 'T' +type SetTempo[TPeriod period.Period] DataEffect // 'T' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { if e > 0x20 { - m := p.(effectIntf.IT) + m := p.(IT) if err := m.SetTempo(int(e)); err != nil { return err } @@ -24,19 +22,19 @@ func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory] } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - m := p.(effectIntf.IT) - switch channel.DataEffect(e >> 4) { +func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + m := p.(IT) + switch DataEffect(e >> 4) { case 0: // decrease tempo if currentTick != 0 { mem := cs.GetMemory() - val := int(mem.TempoDecrease(channel.DataEffect(e & 0x0F))) + val := int(mem.TempoDecrease(DataEffect(e & 0x0F))) if err := m.DecreaseTempo(val); err != nil { return err } @@ -44,7 +42,7 @@ func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p case 1: // increase tempo if currentTick != 0 { mem := cs.GetMemory() - val := int(mem.TempoIncrease(channel.DataEffect(e & 0x0F))) + val := int(mem.TempoIncrease(DataEffect(e & 0x0F))) if err := m.IncreaseTempo(val); err != nil { return err } @@ -58,5 +56,5 @@ func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p } func (e SetTempo[TPeriod]) String() string { - return fmt.Sprintf("T%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("T%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_settremolowaveform.go b/format/it/channel/effect_settremolowaveform.go similarity index 66% rename from format/it/effect/effect_settremolowaveform.go rename to format/it/channel/effect_settremolowaveform.go index 704976b..c4dd895 100644 --- a/format/it/effect/effect_settremolowaveform.go +++ b/format/it/channel/effect_settremolowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -7,17 +7,16 @@ import ( "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[TPeriod period.Period] channel.DataEffect // 'S4x' +type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'S4x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() trem := mem.TremoloOscillator() @@ -26,5 +25,5 @@ func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel. } func (e SetTremoloWaveform[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setvibratowaveform.go b/format/it/channel/effect_setvibratowaveform.go similarity index 66% rename from format/xm/effect/effect_setvibratowaveform.go rename to format/it/channel/effect_setvibratowaveform.go index d37eadf..91b70f8 100644 --- a/format/xm/effect/effect_setvibratowaveform.go +++ b/format/it/channel/effect_setvibratowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -7,17 +7,16 @@ import ( "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[TPeriod period.Period] channel.DataEffect // 'E4x' +type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'S3x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() vib := mem.VibratoOscillator() @@ -26,5 +25,5 @@ func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel. } func (e SetVibratoWaveform[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_tremolo.go b/format/it/channel/effect_tremolo.go similarity index 55% rename from format/it/effect/effect_tremolo.go rename to format/it/channel/effect_tremolo.go index 4103f2c..d76e5f8 100644 --- a/format/it/effect/effect_tremolo.go +++ b/format/it/channel/effect_tremolo.go @@ -1,26 +1,25 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // Tremolo defines a tremolo effect -type Tremolo[TPeriod period.Period] channel.DataEffect // 'R' +type Tremolo[TPeriod period.Period] DataEffect // 'R' // Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Tremolo(channel.DataEffect(e)) + 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 currentTick != 0 { @@ -30,5 +29,5 @@ func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p p } func (e Tremolo[TPeriod]) String() string { - return fmt.Sprintf("R%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("R%0.2x", DataEffect(e)) } diff --git a/format/it/channel/effect_tremor.go b/format/it/channel/effect_tremor.go new file mode 100644 index 0000000..3b1671d --- /dev/null +++ b/format/it/channel/effect_tremor.go @@ -0,0 +1,28 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +// Tremor defines a tremor effect +type Tremor[TPeriod period.Period] DataEffect // 'I' + +// Start triggers on the first tick, but before the Tick() function is called +func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.ResetRetriggerCount() + return nil +} + +// Tick is called on every tick +func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + mem := cs.GetMemory() + x, y := mem.Tremor(DataEffect(e)) + return doTremor(cs, currentTick, int(x)+1, int(y)+1) +} + +func (e Tremor[TPeriod]) String() string { + return fmt.Sprintf("I%0.2x", DataEffect(e)) +} diff --git a/format/it/effect/effect_vibrato.go b/format/it/channel/effect_vibrato.go similarity index 54% rename from format/it/effect/effect_vibrato.go rename to format/it/channel/effect_vibrato.go index a03ed4e..b8e8b84 100644 --- a/format/it/effect/effect_vibrato.go +++ b/format/it/channel/effect_vibrato.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // Vibrato defines a vibrato effect -type Vibrato[TPeriod period.Period] channel.DataEffect // 'H' +type Vibrato[TPeriod period.Period] DataEffect // 'H' // Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) + x, y := mem.Vibrato(DataEffect(e)) if mem.Shared.OldEffectMode { if currentTick != 0 { return doVibrato(cs, currentTick, x, y, 8) @@ -33,5 +32,5 @@ func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p p } func (e Vibrato[TPeriod]) String() string { - return fmt.Sprintf("H%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("H%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_vibratovolslide.go b/format/it/channel/effect_vibratovolslide.go similarity index 61% rename from format/it/effect/effect_vibratovolslide.go rename to format/it/channel/effect_vibratovolslide.go index c1d25c7..84f3972 100644 --- a/format/it/effect/effect_vibratovolslide.go +++ b/format/it/channel/effect_vibratovolslide.go @@ -1,20 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide[TPeriod period.Period] struct { // 'K' - playback.CombinedEffect[TPeriod, channel.Memory] + playback.CombinedEffect[TPeriod, Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide[TPeriod period.Period](mem *channel.Memory, cd channel.Command, val channel.DataEffect) VibratoVolumeSlide[TPeriod] { +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)) @@ -22,5 +21,5 @@ func NewVibratoVolumeSlide[TPeriod period.Period](mem *channel.Memory, cd channe } func (e VibratoVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("K%0.2x", e.Effects[0].(channel.DataEffect)) + return fmt.Sprintf("K%0.2x", e.Effects[0].(DataEffect)) } diff --git a/format/it/effect/effect_volslidedown.go b/format/it/channel/effect_volslidedown.go similarity index 60% rename from format/it/effect/effect_volslidedown.go rename to format/it/channel/effect_volslidedown.go index d89b9d2..77f9ebd 100644 --- a/format/it/effect/effect_volslidedown.go +++ b/format/it/channel/effect_volslidedown.go @@ -1,47 +1,46 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // VolumeSlideDown defines a volume slide down effect -type VolumeSlideDown[TPeriod period.Period] channel.DataEffect // 'D' +type VolumeSlideDown[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - _, y := mem.VolumeSlide(channel.DataEffect(e)) + _, y := mem.VolumeSlide(DataEffect(e)) return doVolSlide(cs, -float32(y), 1.0) } func (e VolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } //==================================================== // VolChanVolumeSlideDown defines a volume slide down effect (from the volume channel) -type VolChanVolumeSlideDown[TPeriod period.Period] channel.DataEffect // 'd' +type VolChanVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' // Tick is called on every tick -func (e VolChanVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e VolChanVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - y := mem.VolChanVolumeSlide(channel.DataEffect(e)) + y := mem.VolChanVolumeSlide(DataEffect(e)) return doVolSlide(cs, -float32(y), 1.0) } func (e VolChanVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("d0%x", channel.DataEffect(e)) + return fmt.Sprintf("d0%x", DataEffect(e)) } diff --git a/format/it/effect/effect_volslideup.go b/format/it/channel/effect_volslideup.go similarity index 60% rename from format/it/effect/effect_volslideup.go rename to format/it/channel/effect_volslideup.go index e146413..76223f9 100644 --- a/format/it/effect/effect_volslideup.go +++ b/format/it/channel/effect_volslideup.go @@ -1,47 +1,46 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // VolumeSlideUp defines a volume slide up effect -type VolumeSlideUp[TPeriod period.Period] channel.DataEffect // 'D' +type VolumeSlideUp[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, _ := mem.VolumeSlide(channel.DataEffect(e)) + x, _ := mem.VolumeSlide(DataEffect(e)) return doVolSlide(cs, float32(x), 1.0) } func (e VolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } //==================================================== // VolChanVolumeSlideUp defines a volume slide up effect (from the volume channel) -type VolChanVolumeSlideUp[TPeriod period.Period] channel.DataEffect // 'd' +type VolChanVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' // Tick is called on every tick -func (e VolChanVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e VolChanVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x := mem.VolChanVolumeSlide(channel.DataEffect(e)) + x := mem.VolChanVolumeSlide(DataEffect(e)) return doVolSlide(cs, float32(x), 1.0) } func (e VolChanVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("d%x0", channel.DataEffect(e)) + return fmt.Sprintf("d%x0", DataEffect(e)) } diff --git a/format/it/effect/effect_volumeenvelopeoff.go b/format/it/channel/effect_volumeenvelopeoff.go similarity index 63% rename from format/it/effect/effect_volumeenvelopeoff.go rename to format/it/channel/effect_volumeenvelopeoff.go index a8b8fff..13a569a 100644 --- a/format/it/effect/effect_volumeenvelopeoff.go +++ b/format/it/channel/effect_volumeenvelopeoff.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // VolumeEnvelopeOff defines a volume envelope: off effect -type VolumeEnvelopeOff[TPeriod period.Period] channel.DataEffect // 'S77' +type VolumeEnvelopeOff[TPeriod period.Period] DataEffect // 'S77' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(false) @@ -20,5 +19,5 @@ func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, channel.M } func (e VolumeEnvelopeOff[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_volumeenvelopeon.go b/format/it/channel/effect_volumeenvelopeon.go similarity index 63% rename from format/it/effect/effect_volumeenvelopeon.go rename to format/it/channel/effect_volumeenvelopeon.go index 647f949..6853cd9 100644 --- a/format/it/effect/effect_volumeenvelopeon.go +++ b/format/it/channel/effect_volumeenvelopeon.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) // VolumeEnvelopeOn defines a volume envelope: on effect -type VolumeEnvelopeOn[TPeriod period.Period] channel.DataEffect // 'S78' +type VolumeEnvelopeOn[TPeriod period.Period] DataEffect // 'S78' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(true) @@ -20,5 +19,5 @@ func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Me } func (e VolumeEnvelopeOn[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effectfactory.go b/format/it/channel/effectfactory.go similarity index 89% rename from format/it/effect/effectfactory.go rename to format/it/channel/effectfactory.go index 278b132..1b61b0b 100644 --- a/format/it/effect/effectfactory.go +++ b/format/it/channel/effectfactory.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" ) @@ -13,7 +12,7 @@ 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, channel.Memory] + playback.CombinedEffect[TPeriod, Memory] eff EffectIT } @@ -25,12 +24,12 @@ func (e VolEff[TPeriod]) String() string { } // Factory produces an effect for the provided channel pattern data -func Factory[TPeriod period.Period](mem *channel.Memory, data song.ChannelData) EffectIT { +func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData) EffectIT { if data == nil { return nil } - d, _ := data.(channel.Data) + d, _ := data.(Data) if !d.What.HasCommand() && !d.What.HasVolPan() { return nil @@ -59,7 +58,7 @@ func Factory[TPeriod period.Period](mem *channel.Memory, data song.ChannelData) } } -func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, data channel.Data) EffectIT { +func standardEffectFactory[TPeriod period.Period](mem *Memory, data Data) EffectIT { switch data.Effect + '@' { case '@': // unused return nil @@ -72,7 +71,7 @@ func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, data chan 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(channel.DataEffect(data.EffectParameter)) + xx := mem.PortaDown(DataEffect(data.EffectParameter)) x := xx >> 4 if x == 0x0F { return FinePortaDown[TPeriod](xx) @@ -81,7 +80,7 @@ func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, data chan } return PortaDown[TPeriod](data.EffectParameter) case 'F': // Porta Up/Fine Porta Up/Extra Fine Porta Down - xx := mem.PortaUp(channel.DataEffect(data.EffectParameter)) + xx := mem.PortaUp(DataEffect(data.EffectParameter)) x := xx >> 4 if x == 0x0F { return FinePortaUp[TPeriod](xx) @@ -134,7 +133,7 @@ func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, data chan return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func specialEffect[TPeriod period.Period](data channel.Data) EffectIT { +func specialEffect[TPeriod period.Period](data Data) EffectIT { switch data.EffectParameter >> 4 { case 0x0: // unused return nil @@ -173,7 +172,7 @@ func specialEffect[TPeriod period.Period](data channel.Data) EffectIT { return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func specialNoteEffects[TPeriod period.Period](data channel.Data) EffectIT { +func specialNoteEffects[TPeriod period.Period](data Data) EffectIT { switch data.EffectParameter & 0xf { case 0x0: // Past Note Cut return PastNoteCut[TPeriod](data.EffectParameter) @@ -207,8 +206,8 @@ func specialNoteEffects[TPeriod period.Period](data channel.Data) EffectIT { return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func volumeSlideFactory[TPeriod period.Period](mem *channel.Memory, cd channel.Command, ce channel.DataEffect) EffectIT { - x, y := mem.VolumeSlide(channel.DataEffect(ce)) +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) @@ -226,7 +225,7 @@ func volumeSlideFactory[TPeriod period.Period](mem *channel.Memory, cd channel.C return nil } -func soundControlEffect[TPeriod period.Period](data channel.Data) EffectIT { +func soundControlEffect[TPeriod period.Period](data Data) EffectIT { switch data.EffectParameter & 0xF { case 0x0: // Surround Off case 0x1: // Surround On diff --git a/format/it/effect/effectfactory_volume.go b/format/it/channel/effectfactory_volume.go similarity index 92% rename from format/it/effect/effectfactory_volume.go rename to format/it/channel/effectfactory_volume.go index c400455..18ce90f 100644 --- a/format/it/effect/effectfactory_volume.go +++ b/format/it/channel/effectfactory_volume.go @@ -1,11 +1,10 @@ -package effect +package channel import ( - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/period" ) -func volPanEffectFactory[TPeriod period.Period](mem *channel.Memory, v uint8) EffectIT { +func volPanEffectFactory[TPeriod period.Period](mem *Memory, v uint8) EffectIT { switch { case v <= 0x40: // volume set - handled elsewhere return nil diff --git a/format/it/effect/unhandled.go b/format/it/channel/unhandled.go similarity index 72% rename from format/it/effect/unhandled.go rename to format/it/channel/unhandled.go index 9827136..92dac96 100644 --- a/format/it/effect/unhandled.go +++ b/format/it/channel/unhandled.go @@ -1,22 +1,20 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" "github.com/gotracker/playback/period" ) // UnhandledCommand is an unhandled command type UnhandledCommand[TPeriod period.Period] struct { - Command channel.Command - Info channel.DataEffect + Command Command + Info DataEffect } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.IT) error { +func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: ce:%0.2X cp:%0.2X", e.Command, e.Info)) } @@ -27,13 +25,19 @@ 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()), + } +} + // UnhandledVolCommand is an unhandled volume command type UnhandledVolCommand[TPeriod period.Period] struct { Vol uint8 } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.IT) error { +func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: volCmd:%0.2X", e.Vol)) } @@ -43,3 +47,9 @@ func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, chan 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()), + } +} diff --git a/format/it/effect/util.go b/format/it/channel/util.go similarity index 66% rename from format/it/effect/util.go rename to format/it/channel/util.go index b12be34..fc7b8e7 100644 --- a/format/it/effect/util.go +++ b/format/it/channel/util.go @@ -1,20 +1,37 @@ -package effect +package channel import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" + "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/note" "github.com/heucuva/comparison" ) -func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], delta float32, multiplier float32) error { +// 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 +} + +func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := itVolume.ToItVolume(av) vol := int16((float32(v) + delta) * multiplier) @@ -30,7 +47,7 @@ func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memo return nil } -func doGlobalVolSlide(m effectIntf.IT, delta float32, multiplier float32) error { +func doGlobalVolSlide(m IT, delta float32, multiplier float32) error { gv := m.GetGlobalVolume() v := itVolume.ToItVolume(gv) vol := int16((float32(v) + delta) * multiplier) @@ -46,7 +63,7 @@ func doGlobalVolSlide(m effectIntf.IT, delta float32, multiplier float32) error return nil } -func doPortaByDeltaAmiga(cs playback.Channel[itPeriod.Amiga, channel.Memory], delta int) error { +func doPortaByDeltaAmiga(cs playback.Channel[itPeriod.Amiga, Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -58,7 +75,7 @@ func doPortaByDeltaAmiga(cs playback.Channel[itPeriod.Amiga, channel.Memory], de return nil } -func doPortaByDeltaLinear(cs playback.Channel[itPeriod.Linear, channel.Memory], delta int) error { +func doPortaByDeltaLinear(cs playback.Channel[itPeriod.Linear, Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -70,19 +87,19 @@ func doPortaByDeltaLinear(cs playback.Channel[itPeriod.Linear, channel.Memory], return nil } -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) switch csp := any(cs).(type) { - case playback.Channel[itPeriod.Linear, channel.Memory]: + case playback.Channel[itPeriod.Linear, Memory]: return doPortaByDeltaLinear(csp, delta) - case playback.Channel[itPeriod.Amiga, channel.Memory]: + case playback.Channel[itPeriod.Amiga, Memory]: return doPortaByDeltaAmiga(csp, -delta) default: panic("unhandled channel type") } } -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target *TPeriod) error { if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { return err } @@ -92,19 +109,19 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel return nil } -func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { +func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) switch csp := any(cs).(type) { - case playback.Channel[itPeriod.Linear, channel.Memory]: + case playback.Channel[itPeriod.Linear, Memory]: return doPortaByDeltaLinear(csp, -delta) - case playback.Channel[itPeriod.Amiga, channel.Memory]: + case playback.Channel[itPeriod.Amiga, Memory]: return doPortaByDeltaAmiga(csp, delta) default: panic("unhandled channel type") } } -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target *TPeriod) error { if err := doPortaDown(cs, amount, multiplier); err != nil { return err } @@ -114,7 +131,7 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, chann return nil } -func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) delta := period.PeriodDelta(vib) @@ -122,7 +139,7 @@ func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memor return nil } -func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, onTicks int, offTicks int) error { +func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -138,7 +155,7 @@ func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory return nil } -func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -155,7 +172,7 @@ func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memo } var ( - volSlideTwoThirdsTable = [...]channel.DataEffect{ + volSlideTwoThirdsTable = [...]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, @@ -163,7 +180,7 @@ var ( } ) -func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory]) error { +func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory]) error { vol := itVolume.ToItVolume(cs.GetActiveVolume()) if vol >= 0x10 && vol <= 0x50 { vol -= 0x10 @@ -182,13 +199,13 @@ func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, cha return nil } -func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) return doVolSlide(cs, delta, 1.0) } -func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { delta := o.GetWave(float32(depth) * multiplier) o.Advance(int(speed)) return delta diff --git a/format/it/effect/effect_notecut.go b/format/it/effect/effect_notecut.go deleted file mode 100644 index fe39a6f..0000000 --- a/format/it/effect/effect_notecut.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/period" -) - -// NoteCut defines a note cut effect -type NoteCut[TPeriod period.Period] channel.DataEffect // 'SCx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.FreezePlayback() - } - return nil -} - -func (e NoteCut[TPeriod]) 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 9eb6bf0..0000000 --- a/format/it/effect/effect_portadown.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/period" -) - -// PortaDown defines a portamento down effect -type PortaDown[TPeriod period.Period] channel.DataEffect // 'E' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaDown(channel.DataEffect(e)) - - return doPortaDown(cs, float32(xx), 4) -} - -func (e PortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%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 9f7f091..0000000 --- a/format/it/effect/effect_portaup.go +++ /dev/null @@ -1,31 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/period" -) - -// PortaUp defines a portamento up effect -type PortaUp[TPeriod period.Period] channel.DataEffect // 'F' - -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - -// Tick is called on every tick -func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaUp(channel.DataEffect(e)) - - return doPortaUp(cs, float32(xx), 4) -} - -func (e PortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%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 7bede8c..0000000 --- a/format/it/effect/effect_tremor.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/period" -) - -// Tremor defines a tremor effect -type Tremor[TPeriod period.Period] channel.DataEffect // 'I' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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[TPeriod]) String() string { - return fmt.Sprintf("I%0.2x", channel.DataEffect(e)) -} 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/playback/playback_command.go b/format/it/playback/playback_command.go index e15c55c..c21fe8c 100644 --- a/format/it/playback/playback_command.go +++ b/format/it/playback/playback_command.go @@ -22,7 +22,8 @@ func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState if inst := cs.GetTargetInst(); inst != nil { cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) - period := itPeriod.CalcSemitonePeriod[TPeriod](cs.Semitone, inst.GetFinetune(), inst.GetC2Spd()) + ft := inst.GetFinetune() + period := itPeriod.CalcSemitonePeriod[TPeriod](cs.Semitone, ft, inst.GetC2Spd()) o.UpdateFunc(period) } return nil diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index 52d3a16..9d47045 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -5,7 +5,6 @@ import ( "time" "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/format/it/effect" "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" @@ -88,7 +87,7 @@ func (m *manager[TPeriod]) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory](effect.Factory[TPeriod])) + cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory]()) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/format/it/playback/playback_tracing.go b/format/it/playback/playback_tracing.go index 1994927..e7bcb76 100644 --- a/format/it/playback/playback_tracing.go +++ b/format/it/playback/playback_tracing.go @@ -6,6 +6,7 @@ import ( "reflect" "text/tabwriter" + "github.com/gotracker/playback" "github.com/gotracker/playback/player" ) @@ -95,30 +96,36 @@ func (m *manager[TPeriod]) outputChannelsTrace() func(w io.Writer) { ) 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() + var trackData string + effects := ch.GetActiveEffects() + if len(effects) == 0 { + effects = []playback.Effect{nil} } 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, - ) + for _, effect := range effects { + var ( + activeEffect string + activeEffectType string + ) + if effect != nil { + activeEffect = fmt.Sprint(effect) + activeEffectType = reflect.TypeOf(effect).Name() + } + cs.AddRow( + c+1, + ch.GetChannelVolume(), + activeEffect, + activeEffectType, + trackData, + ch.RetriggerCount, + ch.Semitone, + ch.UseTargetPeriod, + ch.PanEnabled, + ch.NewNoteAction, + ) + } } return func(w io.Writer) { diff --git a/format/s3m/channel/data.go b/format/s3m/channel/data.go index ce440d9..50dc62e 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -7,7 +7,8 @@ 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" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" @@ -33,7 +34,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 @@ -66,6 +67,29 @@ func (d Data) Channel() uint8 { return d.What.Channel() } +func (d Data) GetEffects(mem *Memory, p s3mPeriod.Amiga) []playback.Effect { + var effects []playback.Effect + if d.HasNote() || d.HasInstrument() { + if n := NoteFactory(mem, d.Note, d.Instrument); n != nil { + effects = append(effects, n) + } + } + + if d.HasVolume() { + if v := VolumeFactory(mem, d.Volume); v != nil { + effects = append(effects, v) + } + } + + if d.HasCommand() { + if e := EffectFactory(mem, d); e != nil { + effects = append(effects, e) + } + } + + return effects +} + func (d Data) String() string { pieces := []string{ "...", // note @@ -94,3 +118,21 @@ func (d Data) ShortString() string { } return "..." } + +// NoteFromS3MNote converts an S3M file note into a player note +func NoteFromS3MNote(sn s3mfile.Note) note.Note { + switch { + case sn == s3mfile.EmptyNote: + return note.EmptyNote{} + case sn == s3mfile.StopNote: + return note.StopOrReleaseNote{} + default: + k := uint8(sn.Key()) & 0x0f + o := uint8(sn.Octave()) & 0x0f + if k < 12 && o < 10 { + s := note.Semitone(o*12 + k) + return note.Normal(s) + } + } + return note.InvalidNote{} +} diff --git a/format/s3m/effect/effect_arpeggio.go b/format/s3m/channel/effect_arpeggio.go similarity index 78% rename from format/s3m/effect/effect_arpeggio.go rename to format/s3m/channel/effect_arpeggio.go index a277251..b37e9c2 100644 --- a/format/s3m/effect/effect_arpeggio.go +++ b/format/s3m/channel/effect_arpeggio.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // Arpeggio defines an arpeggio effect @@ -21,10 +20,10 @@ func (e Arpeggio) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e Arpeggio) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.LastNonZeroXY(channel.DataEffect(e)) + x, y := mem.LastNonZeroXY(DataEffect(e)) return doArpeggio(cs, currentTick, int8(x), int8(y)) } func (e Arpeggio) String() string { - return fmt.Sprintf("J%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("J%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_enablefilter.go b/format/s3m/channel/effect_enablefilter.go similarity index 63% rename from format/s3m/effect/effect_enablefilter.go rename to format/s3m/channel/effect_enablefilter.go index 0f17606..b48da13 100644 --- a/format/s3m/effect/effect_enablefilter.go +++ b/format/s3m/channel/effect_enablefilter.go @@ -1,11 +1,9 @@ -package effect +package channel 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 @@ -15,14 +13,14 @@ type EnableFilter ChannelCommand // 'S0x' func (e EnableFilter) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf on := x != 0 - pb := p.(effectIntf.S3M) + pb := p.(S3M) pb.SetFilterEnable(on) return nil } func (e EnableFilter) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_extrafineportadown.go b/format/s3m/channel/effect_extrafineportadown.go similarity index 75% rename from format/s3m/effect/effect_extrafineportadown.go rename to format/s3m/channel/effect_extrafineportadown.go index b5c10cd..044e073 100644 --- a/format/s3m/effect/effect_extrafineportadown.go +++ b/format/s3m/channel/effect_extrafineportadown.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // ExtraFinePortaDown defines an extra-fine portamento down effect @@ -15,11 +14,11 @@ func (e ExtraFinePortaDown) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - y := channel.DataEffect(e) & 0x0F + y := DataEffect(e) & 0x0F return doPortaDown(cs, float32(y), 1) } func (e ExtraFinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_extrafineportaup.go b/format/s3m/channel/effect_extrafineportaup.go similarity index 74% rename from format/s3m/effect/effect_extrafineportaup.go rename to format/s3m/channel/effect_extrafineportaup.go index f9b8344..66fc8b6 100644 --- a/format/s3m/effect/effect_extrafineportaup.go +++ b/format/s3m/channel/effect_extrafineportaup.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // ExtraFinePortaUp defines an extra-fine portamento up effect @@ -15,11 +14,11 @@ func (e ExtraFinePortaUp) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - y := channel.DataEffect(e) & 0x0F + y := DataEffect(e) & 0x0F return doPortaUp(cs, float32(y), 1) } func (e ExtraFinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_finepatterndelay.go b/format/s3m/channel/effect_finepatterndelay.go similarity index 63% rename from format/s3m/effect/effect_finepatterndelay.go rename to format/s3m/channel/effect_finepatterndelay.go index a1fa476..cf9ebf8 100644 --- a/format/s3m/effect/effect_finepatterndelay.go +++ b/format/s3m/channel/effect_finepatterndelay.go @@ -1,11 +1,9 @@ -package effect +package channel 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 @@ -15,12 +13,12 @@ type FinePatternDelay ChannelCommand // 'S6x' func (e FinePatternDelay) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf - m := p.(effectIntf.S3M) + m := p.(S3M) return m.AddRowTicks(int(x)) } func (e FinePatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_fineportadown.go b/format/s3m/channel/effect_fineportadown.go similarity index 73% rename from format/s3m/effect/effect_fineportadown.go rename to format/s3m/channel/effect_fineportadown.go index 29af012..e48a932 100644 --- a/format/s3m/effect/effect_fineportadown.go +++ b/format/s3m/channel/effect_fineportadown.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // FinePortaDown defines an fine portamento down effect @@ -15,11 +14,11 @@ func (e FinePortaDown) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - y := channel.DataEffect(e) & 0x0F + y := DataEffect(e) & 0x0F return doPortaDown(cs, float32(y), 4) } func (e FinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_fineportaup.go b/format/s3m/channel/effect_fineportaup.go similarity index 73% rename from format/s3m/effect/effect_fineportaup.go rename to format/s3m/channel/effect_fineportaup.go index 0ceb361..61843d8 100644 --- a/format/s3m/effect/effect_fineportaup.go +++ b/format/s3m/channel/effect_fineportaup.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // FinePortaUp defines an fine portamento up effect @@ -15,11 +14,11 @@ func (e FinePortaUp) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - y := channel.DataEffect(e) & 0x0F + y := DataEffect(e) & 0x0F return doPortaUp(cs, float32(y), 4) } func (e FinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_finevibrato.go b/format/s3m/channel/effect_finevibrato.go similarity index 81% rename from format/s3m/effect/effect_finevibrato.go rename to format/s3m/channel/effect_finevibrato.go index 43b39c4..9172457 100644 --- a/format/s3m/effect/effect_finevibrato.go +++ b/format/s3m/channel/effect_finevibrato.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // FineVibrato defines an fine vibrato effect @@ -20,7 +19,7 @@ func (e FineVibrato) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e FineVibrato) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) + x, y := mem.Vibrato(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) @@ -29,5 +28,5 @@ func (e FineVibrato) Tick(cs S3MChannel, p playback.Playback, currentTick int) e } func (e FineVibrato) String() string { - return fmt.Sprintf("U%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("U%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_finevolslidedown.go b/format/s3m/channel/effect_finevolslidedown.go similarity index 80% rename from format/s3m/effect/effect_finevolslidedown.go rename to format/s3m/channel/effect_finevolslidedown.go index a5cba0d..f178881 100644 --- a/format/s3m/effect/effect_finevolslidedown.go +++ b/format/s3m/channel/effect_finevolslidedown.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // FineVolumeSlideDown defines a fine volume slide down effect @@ -18,7 +17,7 @@ func (e FineVolumeSlideDown) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e FineVolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - y := channel.DataEffect(e) & 0x0F + y := DataEffect(e) & 0x0F if y != 0x0F && currentTick == 0 { return doVolSlide(cs, -float32(y), 1.0) @@ -27,5 +26,5 @@ func (e FineVolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTic } func (e FineVolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_finevolslideup.go b/format/s3m/channel/effect_finevolslideup.go similarity index 80% rename from format/s3m/effect/effect_finevolslideup.go rename to format/s3m/channel/effect_finevolslideup.go index f0129d6..b3aa4cf 100644 --- a/format/s3m/effect/effect_finevolslideup.go +++ b/format/s3m/channel/effect_finevolslideup.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // FineVolumeSlideUp defines a fine volume slide up effect @@ -18,7 +17,7 @@ func (e FineVolumeSlideUp) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e FineVolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) >> 4 + x := DataEffect(e) >> 4 if x != 0x0F && currentTick == 0 { return doVolSlide(cs, float32(x), 1.0) @@ -27,5 +26,5 @@ func (e FineVolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick } func (e FineVolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_notecut.go b/format/s3m/channel/effect_notecut.go similarity index 78% rename from format/s3m/effect/effect_notecut.go rename to format/s3m/channel/effect_notecut.go index cf58d1a..faf4cd6 100644 --- a/format/s3m/effect/effect_notecut.go +++ b/format/s3m/channel/effect_notecut.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // NoteCut defines a note cut effect @@ -18,7 +17,7 @@ func (e NoteCut) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e NoteCut) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { cs.FreezePlayback() @@ -27,5 +26,5 @@ func (e NoteCut) Tick(cs S3MChannel, p playback.Playback, currentTick int) error } func (e NoteCut) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_notedelay.go b/format/s3m/channel/effect_notedelay.go similarity index 73% rename from format/s3m/effect/effect_notedelay.go rename to format/s3m/channel/effect_notedelay.go index 4eb79ac..1620704 100644 --- a/format/s3m/effect/effect_notedelay.go +++ b/format/s3m/channel/effect_notedelay.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/note" ) @@ -13,7 +12,7 @@ type NoteDelay ChannelCommand // 'SDx' // PreStart triggers when the effect enters onto the channel state func (e NoteDelay) PreStart(cs S3MChannel, p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(channel.DataEffect(e)&0x0F)) + cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) return nil } @@ -24,5 +23,5 @@ func (e NoteDelay) Start(cs S3MChannel, p playback.Playback) error { } func (e NoteDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_orderjump.go b/format/s3m/channel/effect_orderjump.go similarity index 83% rename from format/s3m/effect/effect_orderjump.go rename to format/s3m/channel/effect_orderjump.go index 9834aee..b80dc98 100644 --- a/format/s3m/effect/effect_orderjump.go +++ b/format/s3m/channel/effect_orderjump.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/index" ) @@ -23,5 +22,5 @@ func (e OrderJump) Stop(cs S3MChannel, p playback.Playback, lastTick int) error } func (e OrderJump) String() string { - return fmt.Sprintf("B%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("B%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_patterndelay.go b/format/s3m/channel/effect_patterndelay.go similarity index 65% rename from format/s3m/effect/effect_patterndelay.go rename to format/s3m/channel/effect_patterndelay.go index 03af22a..fbbd8fe 100644 --- a/format/s3m/effect/effect_patterndelay.go +++ b/format/s3m/channel/effect_patterndelay.go @@ -1,11 +1,9 @@ -package effect +package channel 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 @@ -13,8 +11,8 @@ type PatternDelay ChannelCommand // 'SEx' // PreStart triggers when the effect enters onto the channel state func (e PatternDelay) PreStart(cs S3MChannel, p playback.Playback) error { - m := p.(effectIntf.S3M) - return m.SetPatternDelay(int(channel.DataEffect(e) & 0x0F)) + m := p.(S3M) + return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) } // Start triggers on the first tick, but before the Tick() function is called @@ -24,5 +22,5 @@ func (e PatternDelay) Start(cs S3MChannel, p playback.Playback) error { } func (e PatternDelay) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_patternloop.go b/format/s3m/channel/effect_patternloop.go similarity index 85% rename from format/s3m/effect/effect_patternloop.go rename to format/s3m/channel/effect_patternloop.go index 64e37c4..f3dc736 100644 --- a/format/s3m/effect/effect_patternloop.go +++ b/format/s3m/channel/effect_patternloop.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // PatternLoop defines a pattern loop effect @@ -18,7 +17,7 @@ func (e PatternLoop) Start(cs S3MChannel, p playback.Playback) error { // Stop is called on the last tick of the row, but after the Tick() function is called func (e PatternLoop) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { - x := channel.DataEffect(e) & 0xF + x := DataEffect(e) & 0xF mem := cs.GetMemory() pl := mem.GetPatternLoop() @@ -40,5 +39,5 @@ func (e PatternLoop) Stop(cs S3MChannel, p playback.Playback, lastTick int) erro } func (e PatternLoop) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_portadown.go b/format/s3m/channel/effect_portadown.go similarity index 78% rename from format/s3m/effect/effect_portadown.go rename to format/s3m/channel/effect_portadown.go index 8290ba1..05d38e1 100644 --- a/format/s3m/effect/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // PortaDown defines a portamento down effect @@ -20,7 +19,7 @@ func (e PortaDown) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e PortaDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - xx := mem.LastNonZero(channel.DataEffect(e)) + xx := mem.LastNonZero(DataEffect(e)) if currentTick != 0 { return doPortaDown(cs, float32(xx), 4) @@ -29,5 +28,5 @@ func (e PortaDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) err } func (e PortaDown) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go similarity index 88% rename from format/s3m/effect/effect_portatonote.go rename to format/s3m/channel/effect_portatonote.go index 147d695..5933c6b 100644 --- a/format/s3m/effect/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -1,10 +1,9 @@ -package effect +package channel 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" @@ -27,7 +26,7 @@ func (e PortaToNote) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e PortaToNote) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - xx := mem.PortaToNote(channel.DataEffect(e)) + xx := mem.PortaToNote(DataEffect(e)) // vibrato modifies current period for portamento cur := cs.GetPeriod() @@ -47,5 +46,5 @@ func (e PortaToNote) Tick(cs S3MChannel, p playback.Playback, currentTick int) e } func (e PortaToNote) String() string { - return fmt.Sprintf("G%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("G%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_portaup.go b/format/s3m/channel/effect_portaup.go similarity index 78% rename from format/s3m/effect/effect_portaup.go rename to format/s3m/channel/effect_portaup.go index 42f3d4c..f3ca33f 100644 --- a/format/s3m/effect/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // PortaUp defines a portamento up effect @@ -20,7 +19,7 @@ func (e PortaUp) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e PortaUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - xx := mem.LastNonZero(channel.DataEffect(e)) + xx := mem.LastNonZero(DataEffect(e)) if currentTick != 0 { return doPortaUp(cs, float32(xx), 4) @@ -29,5 +28,5 @@ func (e PortaUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error } func (e PortaUp) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_portavolslide.go b/format/s3m/channel/effect_portavolslide.go similarity index 62% rename from format/s3m/effect/effect_portavolslide.go rename to format/s3m/channel/effect_portavolslide.go index c95a073..7d8dd84 100644 --- a/format/s3m/effect/effect_portavolslide.go +++ b/format/s3m/channel/effect_portavolslide.go @@ -1,20 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" s3mPeriod "github.com/gotracker/playback/format/s3m/period" ) // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[s3mPeriod.Amiga, channel.Memory] + playback.CombinedEffect[s3mPeriod.Amiga, Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide(mem *channel.Memory, cd uint8, val channel.DataEffect) PortaVolumeSlide { +func NewPortaVolumeSlide(mem *Memory, cd uint8, val DataEffect) PortaVolumeSlide { pvs := PortaVolumeSlide{} vs := volumeSlideFactory(mem, cd, val) pvs.Effects = append(pvs.Effects, vs, PortaToNote(0x00)) @@ -22,5 +21,5 @@ func NewPortaVolumeSlide(mem *channel.Memory, cd uint8, val channel.DataEffect) } func (e PortaVolumeSlide) String() string { - return fmt.Sprintf("L%0.2x", e.Effects[0].(channel.DataEffect)) + return fmt.Sprintf("L%0.2x", e.Effects[0].(DataEffect)) } diff --git a/format/s3m/effect/effect_retrigvolslide.go b/format/s3m/channel/effect_retrigvolslide.go similarity index 86% rename from format/s3m/effect/effect_retrigvolslide.go rename to format/s3m/channel/effect_retrigvolslide.go index 727e731..037853b 100644 --- a/format/s3m/effect/effect_retrigvolslide.go +++ b/format/s3m/channel/effect_retrigvolslide.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,7 +6,6 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // RetrigVolumeSlide defines a retriggering volume slide effect @@ -20,15 +19,15 @@ func (e RetrigVolumeSlide) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e RetrigVolumeSlide) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) >> 4 - y := channel.DataEffect(e) & 0x0F + x := DataEffect(e) >> 4 + y := DataEffect(e) & 0x0F if y == 0 { return nil } rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= x { + if DataEffect(rt) >= x { cs.SetPos(sampling.Pos{}) cs.ResetRetriggerCount() switch x { @@ -67,5 +66,5 @@ func (e RetrigVolumeSlide) Tick(cs S3MChannel, p playback.Playback, currentTick } func (e RetrigVolumeSlide) String() string { - return fmt.Sprintf("Q%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("Q%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_rowjump.go b/format/s3m/channel/effect_rowjump.go similarity index 81% rename from format/s3m/effect/effect_rowjump.go rename to format/s3m/channel/effect_rowjump.go index 6d2edb9..63a0e3c 100644 --- a/format/s3m/effect/effect_rowjump.go +++ b/format/s3m/channel/effect_rowjump.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/index" ) @@ -19,12 +18,12 @@ func (e RowJump) Start(cs S3MChannel, p playback.Playback) error { // Stop is called on the last tick of the row, but after the Tick() function is called func (e RowJump) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { - r := channel.DataEffect(e) + r := 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)) + return fmt.Sprintf("C%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_sampleoffset.go b/format/s3m/channel/effect_sampleoffset.go similarity index 74% rename from format/s3m/effect/effect_sampleoffset.go rename to format/s3m/channel/effect_sampleoffset.go index 9d82a2f..a20ae97 100644 --- a/format/s3m/effect/effect_sampleoffset.go +++ b/format/s3m/channel/effect_sampleoffset.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,7 +6,6 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // SampleOffset defines a sample offset effect @@ -16,11 +15,11 @@ type SampleOffset ChannelCommand // 'O' func (e SampleOffset) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - xx := mem.SampleOffset(channel.DataEffect(e)) + xx := mem.SampleOffset(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)) + return fmt.Sprintf("O%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_setfinetune.go b/format/s3m/channel/effect_setfinetune.go similarity index 81% rename from format/s3m/effect/effect_setfinetune.go rename to format/s3m/channel/effect_setfinetune.go index 8ea1fdb..5ae891b 100644 --- a/format/s3m/effect/effect_setfinetune.go +++ b/format/s3m/channel/effect_setfinetune.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/note" ) @@ -13,7 +12,7 @@ type SetFinetune ChannelCommand // 'S2x' // PreStart triggers when the effect enters onto the channel state func (e SetFinetune) PreStart(cs S3MChannel, p playback.Playback) error { - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf inst := cs.GetTargetInst() if inst != nil { @@ -30,5 +29,5 @@ func (e SetFinetune) Start(cs S3MChannel, p playback.Playback) error { } func (e SetFinetune) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_setglobalvolume.go b/format/s3m/channel/effect_setglobalvolume.go similarity index 76% rename from format/s3m/effect/effect_setglobalvolume.go rename to format/s3m/channel/effect_setglobalvolume.go index c01416d..86c24ba 100644 --- a/format/s3m/effect/effect_setglobalvolume.go +++ b/format/s3m/channel/effect_setglobalvolume.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,7 +6,6 @@ import ( 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" ) @@ -15,7 +14,7 @@ type SetGlobalVolume ChannelCommand // 'V' // PreStart triggers when the effect enters onto the channel state func (e SetGlobalVolume) PreStart(cs S3MChannel, p playback.Playback) error { - p.SetGlobalVolume(s3mVolume.VolumeFromS3M(s3mfile.Volume(channel.DataEffect(e)))) + p.SetGlobalVolume(s3mVolume.VolumeFromS3M(s3mfile.Volume(DataEffect(e)))) return nil } @@ -26,5 +25,5 @@ func (e SetGlobalVolume) Start(cs S3MChannel, p playback.Playback) error { } func (e SetGlobalVolume) String() string { - return fmt.Sprintf("V%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("V%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_setpanposition.go b/format/s3m/channel/effect_setpanposition.go similarity index 81% rename from format/s3m/effect/effect_setpanposition.go rename to format/s3m/channel/effect_setpanposition.go index e2a3a3a..16725bb 100644 --- a/format/s3m/effect/effect_setpanposition.go +++ b/format/s3m/channel/effect_setpanposition.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" s3mPanning "github.com/gotracker/playback/format/s3m/panning" ) @@ -22,5 +21,5 @@ func (e SetPanPosition) Start(cs S3MChannel, p playback.Playback) error { } func (e SetPanPosition) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_setspeed.go b/format/s3m/channel/effect_setspeed.go similarity index 73% rename from format/s3m/effect/effect_setspeed.go rename to format/s3m/channel/effect_setspeed.go index d647269..3fabfe3 100644 --- a/format/s3m/effect/effect_setspeed.go +++ b/format/s3m/channel/effect_setspeed.go @@ -1,11 +1,9 @@ -package effect +package channel 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 @@ -14,7 +12,7 @@ type SetSpeed ChannelCommand // 'A' // PreStart triggers when the effect enters onto the channel state func (e SetSpeed) PreStart(cs S3MChannel, p playback.Playback) error { if e != 0 { - m := p.(effectIntf.S3M) + m := p.(S3M) if err := m.SetTicks(int(e)); err != nil { return err } @@ -29,5 +27,5 @@ func (e SetSpeed) Start(cs S3MChannel, p playback.Playback) error { } func (e SetSpeed) String() string { - return fmt.Sprintf("A%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("A%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_settempo.go b/format/s3m/channel/effect_settempo.go similarity index 73% rename from format/s3m/effect/effect_settempo.go rename to format/s3m/channel/effect_settempo.go index 785155b..6a13926 100644 --- a/format/s3m/effect/effect_settempo.go +++ b/format/s3m/channel/effect_settempo.go @@ -1,11 +1,9 @@ -package effect +package channel 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 @@ -14,7 +12,7 @@ type SetTempo ChannelCommand // 'T' // PreStart triggers when the effect enters onto the channel state func (e SetTempo) PreStart(cs S3MChannel, p playback.Playback) error { if e > 0x20 { - m := p.(effectIntf.S3M) + m := p.(S3M) if err := m.SetTempo(int(e)); err != nil { return err } @@ -30,12 +28,12 @@ func (e SetTempo) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e SetTempo) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - m := p.(effectIntf.S3M) - switch channel.DataEffect(e >> 4) { + m := p.(S3M) + switch DataEffect(e >> 4) { case 0: // decrease tempo if currentTick != 0 { mem := cs.GetMemory() - val := int(mem.TempoDecrease(channel.DataEffect(e & 0x0F))) + val := int(mem.TempoDecrease(DataEffect(e & 0x0F))) if err := m.DecreaseTempo(val); err != nil { return err } @@ -43,7 +41,7 @@ func (e SetTempo) Tick(cs S3MChannel, p playback.Playback, currentTick int) erro case 1: // increase tempo if currentTick != 0 { mem := cs.GetMemory() - val := int(mem.TempoIncrease(channel.DataEffect(e & 0x0F))) + val := int(mem.TempoIncrease(DataEffect(e & 0x0F))) if err := m.IncreaseTempo(val); err != nil { return err } @@ -57,5 +55,5 @@ func (e SetTempo) Tick(cs S3MChannel, p playback.Playback, currentTick int) erro } func (e SetTempo) String() string { - return fmt.Sprintf("T%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("T%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_settremolowaveform.go b/format/s3m/channel/effect_settremolowaveform.go similarity index 78% rename from format/s3m/effect/effect_settremolowaveform.go rename to format/s3m/channel/effect_settremolowaveform.go index 6719677..fe14d9f 100644 --- a/format/s3m/effect/effect_settremolowaveform.go +++ b/format/s3m/channel/effect_settremolowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,7 +6,6 @@ import ( "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // SetTremoloWaveform defines a set tremolo waveform effect @@ -16,7 +15,7 @@ type SetTremoloWaveform ChannelCommand // 'S4x' func (e SetTremoloWaveform) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() trem := mem.TremoloOscillator() @@ -25,5 +24,5 @@ func (e SetTremoloWaveform) Start(cs S3MChannel, p playback.Playback) error { } func (e SetTremoloWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_setvibratowaveform.go b/format/s3m/channel/effect_setvibratowaveform.go similarity index 78% rename from format/s3m/effect/effect_setvibratowaveform.go rename to format/s3m/channel/effect_setvibratowaveform.go index e053544..9383fc2 100644 --- a/format/s3m/effect/effect_setvibratowaveform.go +++ b/format/s3m/channel/effect_setvibratowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,7 +6,6 @@ import ( "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // SetVibratoWaveform defines a set vibrato waveform effect @@ -16,7 +15,7 @@ type SetVibratoWaveform ChannelCommand // 'S3x' func (e SetVibratoWaveform) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() vib := mem.VibratoOscillator() @@ -25,5 +24,5 @@ func (e SetVibratoWaveform) Start(cs S3MChannel, p playback.Playback) error { } func (e SetVibratoWaveform) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_stereocontrol.go b/format/s3m/channel/effect_stereocontrol.go similarity index 83% rename from format/s3m/effect/effect_stereocontrol.go rename to format/s3m/channel/effect_stereocontrol.go index 443e2ed..c953405 100644 --- a/format/s3m/effect/effect_stereocontrol.go +++ b/format/s3m/channel/effect_stereocontrol.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" s3mPanning "github.com/gotracker/playback/format/s3m/panning" ) @@ -26,5 +25,5 @@ func (e StereoControl) Start(cs S3MChannel, p playback.Playback) error { } func (e StereoControl) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_tremolo.go b/format/s3m/channel/effect_tremolo.go similarity index 71% rename from format/s3m/effect/effect_tremolo.go rename to format/s3m/channel/effect_tremolo.go index 1ae546d..75071c6 100644 --- a/format/s3m/effect/effect_tremolo.go +++ b/format/s3m/channel/effect_tremolo.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // Tremolo defines a tremolo effect @@ -19,14 +18,14 @@ func (e Tremolo) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e Tremolo) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Tremolo(channel.DataEffect(e)) + x, y := mem.Tremolo(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 doTremolo(cs, currentTick, DataEffect(x), DataEffect(y), 4) } return nil } func (e Tremolo) String() string { - return fmt.Sprintf("R%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("R%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_tremor.go b/format/s3m/channel/effect_tremor.go similarity index 75% rename from format/s3m/effect/effect_tremor.go rename to format/s3m/channel/effect_tremor.go index 477017d..d378bfe 100644 --- a/format/s3m/effect/effect_tremor.go +++ b/format/s3m/channel/effect_tremor.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // Tremor defines a tremor effect @@ -19,10 +18,10 @@ func (e Tremor) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e Tremor) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.LastNonZeroXY(channel.DataEffect(e)) + x, y := mem.LastNonZeroXY(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)) + return fmt.Sprintf("I%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_vibrato.go b/format/s3m/channel/effect_vibrato.go similarity index 80% rename from format/s3m/effect/effect_vibrato.go rename to format/s3m/channel/effect_vibrato.go index f133ed4..6fb7e9c 100644 --- a/format/s3m/effect/effect_vibrato.go +++ b/format/s3m/channel/effect_vibrato.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // Vibrato defines a vibrato effect @@ -20,7 +19,7 @@ func (e Vibrato) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e Vibrato) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) + x, y := mem.Vibrato(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) @@ -29,5 +28,5 @@ func (e Vibrato) Tick(cs S3MChannel, p playback.Playback, currentTick int) error } func (e Vibrato) String() string { - return fmt.Sprintf("H%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("H%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_vibratovolslide.go b/format/s3m/channel/effect_vibratovolslide.go similarity index 62% rename from format/s3m/effect/effect_vibratovolslide.go rename to format/s3m/channel/effect_vibratovolslide.go index d91637a..0a27d98 100644 --- a/format/s3m/effect/effect_vibratovolslide.go +++ b/format/s3m/channel/effect_vibratovolslide.go @@ -1,20 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" s3mPeriod "github.com/gotracker/playback/format/s3m/period" ) // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[s3mPeriod.Amiga, channel.Memory] + playback.CombinedEffect[s3mPeriod.Amiga, Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide(mem *channel.Memory, cd uint8, val channel.DataEffect) VibratoVolumeSlide { +func NewVibratoVolumeSlide(mem *Memory, cd uint8, val DataEffect) VibratoVolumeSlide { vvs := VibratoVolumeSlide{} vs := volumeSlideFactory(mem, cd, val) vvs.Effects = append(vvs.Effects, vs, Vibrato(0x00)) @@ -22,5 +21,5 @@ func NewVibratoVolumeSlide(mem *channel.Memory, cd uint8, val channel.DataEffect } func (e VibratoVolumeSlide) String() string { - return fmt.Sprintf("K%0.2x", e.Effects[0].(channel.DataEffect)) + return fmt.Sprintf("K%0.2x", e.Effects[0].(DataEffect)) } diff --git a/format/s3m/effect/effect_volslidedown.go b/format/s3m/channel/effect_volslidedown.go similarity index 80% rename from format/s3m/effect/effect_volslidedown.go rename to format/s3m/channel/effect_volslidedown.go index df835d9..ff744f6 100644 --- a/format/s3m/effect/effect_volslidedown.go +++ b/format/s3m/channel/effect_volslidedown.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // VolumeSlideDown defines a volume slide down effect @@ -19,7 +18,7 @@ func (e VolumeSlideDown) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e VolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - y := channel.DataEffect(e) & 0x0F + y := DataEffect(e) & 0x0F if mem.Shared.VolSlideEveryFrame || currentTick != 0 { return doVolSlide(cs, -float32(y), 1.0) @@ -28,5 +27,5 @@ func (e VolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTick in } func (e VolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effect_volslideup.go b/format/s3m/channel/effect_volslideup.go similarity index 80% rename from format/s3m/effect/effect_volslideup.go rename to format/s3m/channel/effect_volslideup.go index 1ee00f4..6b761a6 100644 --- a/format/s3m/effect/effect_volslideup.go +++ b/format/s3m/channel/effect_volslideup.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" ) // VolumeSlideUp defines a volume slide up effect @@ -19,7 +18,7 @@ func (e VolumeSlideUp) Start(cs S3MChannel, p playback.Playback) error { // Tick is called on every tick func (e VolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x := channel.DataEffect(e) >> 4 + x := DataEffect(e) >> 4 if mem.Shared.VolSlideEveryFrame || currentTick != 0 { return doVolSlide(cs, float32(x), 1.0) @@ -28,5 +27,5 @@ func (e VolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) } func (e VolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } diff --git a/format/s3m/effect/effectfactory.go b/format/s3m/channel/effectfactory.go similarity index 87% rename from format/s3m/effect/effectfactory.go rename to format/s3m/channel/effectfactory.go index 6d2a05b..c5cbd94 100644 --- a/format/s3m/effect/effectfactory.go +++ b/format/s3m/channel/effectfactory.go @@ -1,22 +1,18 @@ -package effect +package channel import ( - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/song" ) -type EffectS3M = 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 song.ChannelData) EffectS3M { +func EffectFactory(mem *Memory, data song.ChannelData) EffectS3M { if data == nil { return nil } - d, _ := data.(channel.Data) + d, _ := data.(Data) if !d.What.HasCommand() { return nil } @@ -88,7 +84,7 @@ func Factory(mem *channel.Memory, data song.ChannelData) EffectS3M { return UnhandledCommand{Command: d.Command, Info: d.Info} } -func specialEffect(mem *channel.Memory, data channel.Data) EffectS3M { +func specialEffect(mem *Memory, data Data) EffectS3M { var cmd = mem.LastNonZero(data.Info) switch cmd >> 4 { case 0x0: // Set Filter on/off @@ -125,10 +121,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) EffectS3M { 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) diff --git a/format/s3m/channel/memory.go b/format/s3m/channel/memory.go index 676fb91..cd03448 100644 --- a/format/s3m/channel/memory.go +++ b/format/s3m/channel/memory.go @@ -1,6 +1,7 @@ package channel import ( + "github.com/gotracker/playback/note" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback/memory" @@ -11,6 +12,9 @@ import ( // Memory is the storage object for custom effect/command values type Memory struct { + semitone note.Semitone + inst InstID + portaToNote memory.Value[DataEffect] vibratoSpeed memory.Value[DataEffect] vibratoDepth memory.Value[DataEffect] @@ -29,6 +33,26 @@ type Memory struct { Shared *SharedMemory } +// Semitone gets or sets the most recent non-zero value (or input) for the semitone +func (m *Memory) Semitone(input note.Semitone) note.Semitone { + if input == 0 { + return m.semitone + } + + m.semitone = input + return input +} + +// Inst gets or sets the most recent non-zero value (or input) for inst +func (m *Memory) Inst(input InstID) InstID { + if input == 0 { + return m.inst + } + + m.inst = input + return input +} + // ResetOscillators resets the oscillators to defaults func (m *Memory) ResetOscillators() { m.vibratoOscillator = oscillatorImpl.NewProtrackerOscillator() @@ -112,6 +136,8 @@ func (m *Memory) GetPatternLoop() *formatutil.PatternLoop { // StartOrder is called when the first order's row at tick 0 is started func (m *Memory) StartOrder() { if m.Shared.ResetMemoryAtStartOfOrder0 { + m.semitone = 0 + m.inst = 0 m.portaToNote.Reset() m.vibratoSpeed.Reset() m.vibratoDepth.Reset() diff --git a/format/s3m/channel/note_start.go b/format/s3m/channel/note_start.go new file mode 100644 index 0000000..ba2caf2 --- /dev/null +++ b/format/s3m/channel/note_start.go @@ -0,0 +1,87 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/playback" + s3mPeriod "github.com/gotracker/playback/format/s3m/period" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" +) + +type Start struct { + semitone note.Semitone + instId InstID +} + +func start(s note.Semitone, i InstID) EffectS3M { + if s == 0 && i == 0 { + return nil + } + + return Start{ + semitone: 0, + instId: i, + } +} + +// Start triggers on the first tick, but before the Tick() function is called +func (e Start) Start(cs S3MChannel, p playback.Playback) error { + cs.ResetRetriggerCount() + + mem := cs.GetMemory() + st := mem.Semitone(e.semitone) + instID := mem.Inst(e.instId) + + m := p.(S3M) + + inst := cs.GetInstrument() + prevInst := inst + + var ( + wantRetrigger bool + wantRetriggerVol bool + ) + if instID.IsEmpty() { + // use current + inst = prevInst + wantRetrigger = true + } else if !m.IsValidInstrumentID(instID) { + cs.SetTargetInst(nil) + } else { + var str note.Semitone + inst, str = m.GetInstrument(instID) + if str != note.UnchangedSemitone { + st = str + } + wantRetrigger = true + wantRetriggerVol = true + } + + if wantRetrigger { + var ( + c2Spd period.Frequency + ft note.Finetune + ) + if inst != nil { + c2Spd = inst.GetC2Spd() + ft = inst.GetFinetune() + } + p := s3mPeriod.CalcSemitonePeriod(st, ft, c2Spd) + cs.SetTargetPos(sampling.Pos{}) + cs.SetTargetSemitone(st) + cs.SetTargetPeriod(p) + cs.SetTargetInst(inst) + } + + if wantRetriggerVol && inst != nil { + cs.SetActiveVolume(inst.GetDefaultVolume()) + } + + return nil +} + +func (e Start) String() string { + return fmt.Sprintf("%v %02d", e.semitone, e.instId) +} diff --git a/format/s3m/channel/note_stop.go b/format/s3m/channel/note_stop.go new file mode 100644 index 0000000..cde48ce --- /dev/null +++ b/format/s3m/channel/note_stop.go @@ -0,0 +1,36 @@ +package channel + +import ( + "github.com/gotracker/playback" + "github.com/gotracker/playback/note" +) + +type Stop struct { + s note.Semitone + i InstID +} + +func stop() EffectS3M { + return Stop{} +} + +// Start triggers on the first tick, but before the Tick() function is called +func (e Stop) Start(cs S3MChannel, p playback.Playback) error { + cs.ResetRetriggerCount() + + if inst := cs.GetInstrument(); inst != nil { + if inst.IsReleaseNote(note.StopOrReleaseNote{}) { + if v := cs.GetVoice(); v != nil { + v.Release() + } + } else { + cs.NoteCut() + } + } + + return nil +} + +func (e Stop) String() string { + return "=== .." +} diff --git a/format/s3m/channel/notefactory.go b/format/s3m/channel/notefactory.go new file mode 100644 index 0000000..da4b5aa --- /dev/null +++ b/format/s3m/channel/notefactory.go @@ -0,0 +1,27 @@ +package channel + +import ( + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + "github.com/gotracker/playback/note" +) + +// NoteFactory produces a note effect for the provided channel pattern data +func NoteFactory(mem *Memory, n s3mfile.Note, i InstID) EffectS3M { + switch { + case n == s3mfile.EmptyNote: + if i != 0 { + return start(0, i) + } + return nil + case n == s3mfile.StopNote: + return stop() + default: + k := uint8(n.Key()) & 0x0f + o := uint8(n.Octave()) & 0x0f + if k < 12 && o < 10 { + s := note.Semitone(o*12 + k) + return start(s, i) + } + } + return note.InvalidNote{} +} diff --git a/format/s3m/effect/unhandled.go b/format/s3m/channel/unhandled.go similarity index 60% rename from format/s3m/effect/unhandled.go rename to format/s3m/channel/unhandled.go index 0a41b58..44d8056 100644 --- a/format/s3m/effect/unhandled.go +++ b/format/s3m/channel/unhandled.go @@ -1,20 +1,17 @@ -package effect +package channel import ( "fmt" - - "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 + Info DataEffect } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand) PreStart(cs S3MChannel, m effectIntf.S3M) error { +func (e UnhandledCommand) PreStart(cs S3MChannel, m S3M) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } @@ -24,3 +21,9 @@ func (e UnhandledCommand) PreStart(cs S3MChannel, m effectIntf.S3M) error { 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()), + } +} diff --git a/format/s3m/effect/util.go b/format/s3m/channel/util.go similarity index 69% rename from format/s3m/effect/util.go rename to format/s3m/channel/util.go index 6358eac..e08c6ef 100644 --- a/format/s3m/effect/util.go +++ b/format/s3m/channel/util.go @@ -1,19 +1,42 @@ -package effect +package channel import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/gomixing/volume" + "github.com/heucuva/comparison" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" 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/instrument" "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/oscillator" ) -type S3MChannel = playback.Channel[s3mPeriod.Amiga, channel.Memory] +type EffectS3M = playback.Effect +type S3MChannel = playback.Channel[s3mPeriod.Amiga, Memory] + +// 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 + + GetInstrument(instrument.ID) (*instrument.Instrument, note.Semitone) + IsValidInstrumentID(instNum instrument.ID) bool +} func doVolSlide(cs S3MChannel, delta float32, multiplier float32) error { av := cs.GetActiveVolume() @@ -93,9 +116,9 @@ func doPortaDownToNote(cs S3MChannel, amount float32, multiplier float32, target return nil } -func doVibrato(cs S3MChannel, currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, channel.DataEffect(speed), channel.DataEffect(depth), multiplier, mem.VibratoOscillator()) + delta := calculateWaveTable(cs, currentTick, DataEffect(speed), DataEffect(depth), multiplier, mem.VibratoOscillator()) cs.SetPeriodDelta(period.PeriodDelta(delta)) return nil } @@ -150,13 +173,13 @@ func doVolSlideTwoThirds(cs S3MChannel) error { return nil } -func doTremolo(cs S3MChannel, currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo(cs S3MChannel, currentTick int, speed DataEffect, depth 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 S3MChannel, currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { delta := o.GetWave(float32(depth)) * multiplier o.Advance(int(speed)) return delta diff --git a/format/s3m/channel/volume_setvolume.go b/format/s3m/channel/volume_setvolume.go new file mode 100644 index 0000000..f54552c --- /dev/null +++ b/format/s3m/channel/volume_setvolume.go @@ -0,0 +1,23 @@ +package channel + +import ( + "fmt" + + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + + "github.com/gotracker/playback" +) + +// SetVolume defines a SetVolume effect +type SetVolume s3mfile.Volume + +// Start triggers on the first tick, but before the Tick() function is called +func (e SetVolume) Start(cs S3MChannel, p playback.Playback) error { + cs.SetActiveVolume(s3mVolume.VolumeFromS3M(s3mfile.Volume(e))) + return nil +} + +func (e SetVolume) String() string { + return fmt.Sprintf("%02d", s3mfile.Volume(e)) +} diff --git a/format/s3m/channel/volumefactory.go b/format/s3m/channel/volumefactory.go new file mode 100644 index 0000000..08ca196 --- /dev/null +++ b/format/s3m/channel/volumefactory.go @@ -0,0 +1,7 @@ +package channel + +import s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + +func VolumeFactory(mem *Memory, v s3mfile.Volume) EffectS3M { + return SetVolume(v) +} 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/note/note.go b/format/s3m/note/note.go deleted file mode 100644 index a3815e0..0000000 --- a/format/s3m/note/note.go +++ /dev/null @@ -1,24 +0,0 @@ -package note - -import ( - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/playback/note" -) - -// NoteFromS3MNote converts an S3M file note into a player note -func NoteFromS3MNote(sn s3mfile.Note) note.Note { - switch { - case sn == s3mfile.EmptyNote: - return note.EmptyNote{} - case sn == s3mfile.StopNote: - return note.StopOrReleaseNote{} - default: - k := uint8(sn.Key()) & 0x0f - o := uint8(sn.Octave()) & 0x0f - if k < 12 && o < 10 { - s := note.Semitone(o*12 + k) - return note.Normal(s) - } - } - return note.InvalidNote{} -} diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index ca4e469..74f3b30 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -5,7 +5,6 @@ import ( "time" "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/format/s3m/effect" s3mPeriod "github.com/gotracker/playback/format/s3m/period" "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" @@ -89,7 +88,7 @@ func (m *manager) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(state.NewChannelDataTxn[s3mPeriod.Amiga, channel.Memory](effect.Factory)) + cs.AdvanceRow(state.NewChannelDataTxn[s3mPeriod.Amiga, channel.Memory]()) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index 0d3a0e5..650dd99 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -7,10 +7,13 @@ 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" + xmPeriod "github.com/gotracker/playback/format/xm/period" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" ) type Command uint8 @@ -101,6 +104,22 @@ func (d Data) Channel() uint8 { return 0 } +func (d Data) GetEffects(mem *Memory, periodType period.Period) []playback.Effect { + switch periodType.(type) { + case xmPeriod.Linear: + if e := EffectFactory[xmPeriod.Linear](mem, d); e != nil { + return []playback.Effect{e} + } + case xmPeriod.Amiga: + if e := EffectFactory[xmPeriod.Amiga](mem, d); e != nil { + return []playback.Effect{e} + } + default: + panic("unhandled period type") + } + return nil +} + func (Data) getNoteString(n note.Note) string { switch note.Type(n) { case note.SpecialTypeRelease: diff --git a/format/xm/effect/effect_arpeggio.go b/format/xm/channel/effect_arpeggio.go similarity index 53% rename from format/xm/effect/effect_arpeggio.go rename to format/xm/channel/effect_arpeggio.go index 1440b66..8a25939 100644 --- a/format/xm/effect/effect_arpeggio.go +++ b/format/xm/channel/effect_arpeggio.go @@ -1,18 +1,17 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // Arpeggio defines an arpeggio effect -type Arpeggio[TPeriod period.Period] channel.DataEffect // '0' +type Arpeggio[TPeriod period.Period] DataEffect // '0' // Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -20,8 +19,8 @@ func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p } // Tick is called on every tick -func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - xy := channel.DataEffect(e) +func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + xy := DataEffect(e) if xy == 0 { return nil } @@ -32,5 +31,5 @@ func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p } func (e Arpeggio[TPeriod]) String() string { - return fmt.Sprintf("0%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("0%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_extrafineportadown.go b/format/xm/channel/effect_extrafineportadown.go similarity index 62% rename from format/xm/effect/effect_extrafineportadown.go rename to format/xm/channel/effect_extrafineportadown.go index 23357ad..b01ad1d 100644 --- a/format/xm/effect/effect_extrafineportadown.go +++ b/format/xm/channel/effect_extrafineportadown.go @@ -1,28 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // ExtraFinePortaDown defines an extra-fine portamento down effect -type ExtraFinePortaDown[TPeriod period.Period] channel.DataEffect // 'X2x' +type ExtraFinePortaDown[TPeriod period.Period] DataEffect // 'X2x' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - xx := mem.ExtraFinePortaDown(channel.DataEffect(e)) + xx := mem.ExtraFinePortaDown(DataEffect(e)) y := xx & 0x0F return doPortaDown(cs, float32(y), 1) } func (e ExtraFinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_extrafineportaup.go b/format/xm/channel/effect_extrafineportaup.go similarity index 62% rename from format/xm/effect/effect_extrafineportaup.go rename to format/xm/channel/effect_extrafineportaup.go index 548bbf1..e20587d 100644 --- a/format/xm/effect/effect_extrafineportaup.go +++ b/format/xm/channel/effect_extrafineportaup.go @@ -1,28 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // ExtraFinePortaUp defines an extra-fine portamento up effect -type ExtraFinePortaUp[TPeriod period.Period] channel.DataEffect // 'X1x' +type ExtraFinePortaUp[TPeriod period.Period] DataEffect // 'X1x' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - xx := mem.ExtraFinePortaUp(channel.DataEffect(e)) + xx := mem.ExtraFinePortaUp(DataEffect(e)) y := xx & 0x0F return doPortaUp(cs, float32(y), 1) } func (e ExtraFinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_fineportadown.go b/format/xm/channel/effect_fineportadown.go similarity index 62% rename from format/xm/effect/effect_fineportadown.go rename to format/xm/channel/effect_fineportadown.go index 1b30e39..9ce05ba 100644 --- a/format/xm/effect/effect_fineportadown.go +++ b/format/xm/channel/effect_fineportadown.go @@ -1,28 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // FinePortaDown defines an fine portamento down effect -type FinePortaDown[TPeriod period.Period] channel.DataEffect // 'E2x' +type FinePortaDown[TPeriod period.Period] DataEffect // 'E2x' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - xy := mem.FinePortaDown(channel.DataEffect(e)) + xy := mem.FinePortaDown(DataEffect(e)) y := xy & 0x0F return doPortaDown(cs, float32(y), 4) } func (e FinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_fineportaup.go b/format/xm/channel/effect_fineportaup.go similarity index 62% rename from format/xm/effect/effect_fineportaup.go rename to format/xm/channel/effect_fineportaup.go index dc2e684..51ca598 100644 --- a/format/xm/effect/effect_fineportaup.go +++ b/format/xm/channel/effect_fineportaup.go @@ -1,28 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // FinePortaUp defines an fine portamento up effect -type FinePortaUp[TPeriod period.Period] channel.DataEffect // 'E1x' +type FinePortaUp[TPeriod period.Period] DataEffect // 'E1x' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() mem := cs.GetMemory() - xy := mem.FinePortaUp(channel.DataEffect(e)) + xy := mem.FinePortaUp(DataEffect(e)) y := xy & 0x0F return doPortaUp(cs, float32(y), 4) } func (e FinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_finevolslidedown.go b/format/xm/channel/effect_finevolslidedown.go similarity index 56% rename from format/xm/effect/effect_finevolslidedown.go rename to format/xm/channel/effect_finevolslidedown.go index 12df2e1..2213efa 100644 --- a/format/xm/effect/effect_finevolslidedown.go +++ b/format/xm/channel/effect_finevolslidedown.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // FineVolumeSlideDown defines a volume slide effect -type FineVolumeSlideDown[TPeriod period.Period] channel.DataEffect // 'EAx' +type FineVolumeSlideDown[TPeriod period.Period] DataEffect // 'EAx' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - xy := mem.FineVolumeSlideDown(channel.DataEffect(e)) - y := channel.DataEffect(xy & 0x0F) + xy := mem.FineVolumeSlideDown(DataEffect(e)) + y := DataEffect(xy & 0x0F) return doVolSlide(cs, -float32(y), 1.0) } func (e FineVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_finevolslideup.go b/format/xm/channel/effect_finevolslideup.go similarity index 56% rename from format/xm/effect/effect_finevolslideup.go rename to format/xm/channel/effect_finevolslideup.go index 2375182..0312536 100644 --- a/format/xm/effect/effect_finevolslideup.go +++ b/format/xm/channel/effect_finevolslideup.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // FineVolumeSlideUp defines a volume slide effect -type FineVolumeSlideUp[TPeriod period.Period] channel.DataEffect // 'EAx' +type FineVolumeSlideUp[TPeriod period.Period] DataEffect // 'EAx' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - xy := mem.FineVolumeSlideUp(channel.DataEffect(e)) - y := channel.DataEffect(xy & 0x0F) + xy := mem.FineVolumeSlideUp(DataEffect(e)) + y := DataEffect(xy & 0x0F) return doVolSlide(cs, float32(y), 1.0) } func (e FineVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_globalvolumeslide.go b/format/xm/channel/effect_globalvolumeslide.go similarity index 62% rename from format/it/effect/effect_globalvolumeslide.go rename to format/xm/channel/effect_globalvolumeslide.go index d43d2c6..3a7303f 100644 --- a/format/it/effect/effect_globalvolumeslide.go +++ b/format/xm/channel/effect_globalvolumeslide.go @@ -1,33 +1,31 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" - effectIntf "github.com/gotracker/playback/format/it/effect/intf" "github.com/gotracker/playback/period" ) // GlobalVolumeSlide defines a global volume slide effect -type GlobalVolumeSlide[TPeriod period.Period] channel.DataEffect // 'W' +type GlobalVolumeSlide[TPeriod period.Period] DataEffect // 'H' // Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.GlobalVolumeSlide(channel.DataEffect(e)) + x, y := mem.GlobalVolumeSlide(DataEffect(e)) if currentTick == 0 { return nil } - m := p.(effectIntf.IT) + m := p.(XM) if x == 0 { // global vol slide down @@ -40,5 +38,5 @@ func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Me } func (e GlobalVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("W%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("H%0.2x", DataEffect(e)) } diff --git a/format/xm/channel/effect_notecut.go b/format/xm/channel/effect_notecut.go new file mode 100644 index 0000000..728d860 --- /dev/null +++ b/format/xm/channel/effect_notecut.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +// NoteCut defines a note cut effect +type NoteCut[TPeriod period.Period] DataEffect // 'ECx' + +// Start triggers on the first tick, but before the Tick() function is called +func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.ResetRetriggerCount() + return nil +} + +// Tick is called on every tick +func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + x := DataEffect(e) & 0xf + + if x != 0 && currentTick == int(x) { + cs.NoteCut() + } + return nil +} + +func (e NoteCut[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} diff --git a/format/it/effect/effect_notedelay.go b/format/xm/channel/effect_notedelay.go similarity index 53% rename from format/it/effect/effect_notedelay.go rename to format/xm/channel/effect_notedelay.go index 52baa51..6856e2f 100644 --- a/format/it/effect/effect_notedelay.go +++ b/format/xm/channel/effect_notedelay.go @@ -1,29 +1,28 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // NoteDelay defines a note delay effect -type NoteDelay[TPeriod period.Period] channel.DataEffect // 'SDx' +type NoteDelay[TPeriod period.Period] DataEffect // 'EDx' // PreStart triggers when the effect enters onto the channel state -func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(channel.DataEffect(e)&0x0F)) +func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) return nil } // Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e NoteDelay[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_orderjump.go b/format/xm/channel/effect_orderjump.go similarity index 53% rename from format/it/effect/effect_orderjump.go rename to format/xm/channel/effect_orderjump.go index 271ae0c..45257a8 100644 --- a/format/it/effect/effect_orderjump.go +++ b/format/xm/channel/effect_orderjump.go @@ -1,28 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" ) // OrderJump defines an order jump effect -type OrderJump[TPeriod period.Period] channel.DataEffect // 'B' +type OrderJump[TPeriod period.Period] DataEffect // 'B' // Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], 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[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { +func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } func (e OrderJump[TPeriod]) String() string { - return fmt.Sprintf("B%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("B%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_panslide.go b/format/xm/channel/effect_panslide.go similarity index 61% rename from format/xm/effect/effect_panslide.go rename to format/xm/channel/effect_panslide.go index 210f610..4fb3ee2 100644 --- a/format/xm/effect/effect_panslide.go +++ b/format/xm/channel/effect_panslide.go @@ -1,24 +1,23 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" xmPanning "github.com/gotracker/playback/format/xm/panning" "github.com/gotracker/playback/period" ) // PanSlide defines a pan slide effect -type PanSlide[TPeriod period.Period] channel.DataEffect // 'Pxx' +type PanSlide[TPeriod period.Period] DataEffect // 'Pxx' // Start triggers on the first tick, but before the Tick() function is called -func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - xx := channel.DataEffect(e) +func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + xx := DataEffect(e) x := xx >> 4 y := xx & 0x0F - xp := channel.DataEffect(xmPanning.PanningToXm(cs.GetPan())) + xp := DataEffect(xmPanning.PanningToXm(cs.GetPan())) if x == 0 { // slide left y units if xp < y { @@ -39,5 +38,5 @@ func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p } func (e PanSlide[TPeriod]) String() string { - return fmt.Sprintf("P%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("P%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_patterndelay.go b/format/xm/channel/effect_patterndelay.go similarity index 53% rename from format/xm/effect/effect_patterndelay.go rename to format/xm/channel/effect_patterndelay.go index ad1565b..f50a28e 100644 --- a/format/xm/effect/effect_patterndelay.go +++ b/format/xm/channel/effect_patterndelay.go @@ -1,29 +1,27 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" "github.com/gotracker/playback/period" ) // PatternDelay defines a pattern delay effect -type PatternDelay[TPeriod period.Period] channel.DataEffect // 'SEx' +type PatternDelay[TPeriod period.Period] DataEffect // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - m := p.(effectIntf.XM) - return m.SetPatternDelay(int(channel.DataEffect(e) & 0x0F)) +func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + m := p.(XM) + return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) } // Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e PatternDelay[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("S%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_patternloop.go b/format/xm/channel/effect_patternloop.go similarity index 72% rename from format/xm/effect/effect_patternloop.go rename to format/xm/channel/effect_patternloop.go index 0c7370e..61a7cb2 100644 --- a/format/xm/effect/effect_patternloop.go +++ b/format/xm/channel/effect_patternloop.go @@ -1,21 +1,20 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // PatternLoop defines a pattern loop effect -type PatternLoop[TPeriod period.Period] channel.DataEffect // 'E6x' +type PatternLoop[TPeriod period.Period] DataEffect // 'E6x' // Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xF + x := DataEffect(e) & 0xF mem := cs.GetMemory() pl := mem.GetPatternLoop() @@ -37,5 +36,5 @@ func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory] } func (e PatternLoop[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_portadown.go b/format/xm/channel/effect_portadown.go similarity index 50% rename from format/xm/effect/effect_portadown.go rename to format/xm/channel/effect_portadown.go index 382ef6b..0353e42 100644 --- a/format/xm/effect/effect_portadown.go +++ b/format/xm/channel/effect_portadown.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // PortaDown defines a portamento down effect -type PortaDown[TPeriod period.Period] channel.DataEffect // '2' +type PortaDown[TPeriod period.Period] DataEffect // '2' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - xx := mem.PortaDown(channel.DataEffect(e)) + xx := mem.PortaDown(DataEffect(e)) if currentTick == 0 { return nil @@ -31,5 +30,5 @@ func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p } func (e PortaDown[TPeriod]) String() string { - return fmt.Sprintf("2%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("2%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_portatonote.go b/format/xm/channel/effect_portatonote.go similarity index 76% rename from format/xm/effect/effect_portatonote.go rename to format/xm/channel/effect_portatonote.go index 3b1c29d..63364b2 100644 --- a/format/xm/effect/effect_portatonote.go +++ b/format/xm/channel/effect_portatonote.go @@ -1,20 +1,19 @@ -package effect +package channel 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[TPeriod period.Period] channel.DataEffect // '3' +type PortaToNote[TPeriod period.Period] DataEffect // '3' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { @@ -25,13 +24,13 @@ func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory] } // Tick is called on every tick -func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { if currentTick == 0 { return nil } mem := cs.GetMemory() - xx := mem.PortaToNote(channel.DataEffect(e)) + xx := mem.PortaToNote(DataEffect(e)) current := cs.GetPeriod() target := cs.GetPortaTargetPeriod() @@ -43,5 +42,5 @@ func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], } func (e PortaToNote[TPeriod]) String() string { - return fmt.Sprintf("3%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("3%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_portaup.go b/format/xm/channel/effect_portaup.go similarity index 50% rename from format/xm/effect/effect_portaup.go rename to format/xm/channel/effect_portaup.go index 8430dbf..8ff08cc 100644 --- a/format/xm/effect/effect_portaup.go +++ b/format/xm/channel/effect_portaup.go @@ -1,27 +1,26 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // PortaUp defines a portamento up effect -type PortaUp[TPeriod period.Period] channel.DataEffect // '1' +type PortaUp[TPeriod period.Period] DataEffect // '1' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - xx := mem.PortaUp(channel.DataEffect(e)) + xx := mem.PortaUp(DataEffect(e)) if currentTick == 0 { return nil @@ -31,5 +30,5 @@ func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p p } func (e PortaUp[TPeriod]) String() string { - return fmt.Sprintf("1%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("1%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_portavolslide.go b/format/xm/channel/effect_portavolslide.go similarity index 62% rename from format/xm/effect/effect_portavolslide.go rename to format/xm/channel/effect_portavolslide.go index 4c9d696..352cf2d 100644 --- a/format/xm/effect/effect_portavolslide.go +++ b/format/xm/channel/effect_portavolslide.go @@ -1,25 +1,24 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "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, channel.Memory] + playback.CombinedEffect[TPeriod, Memory] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object -func NewPortaVolumeSlide[TPeriod period.Period](val channel.DataEffect) PortaVolumeSlide[TPeriod] { +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", channel.DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) + return fmt.Sprintf("5%0.2x", DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) } diff --git a/format/xm/effect/effect_retriggernote.go b/format/xm/channel/effect_retriggernote.go similarity index 64% rename from format/xm/effect/effect_retriggernote.go rename to format/xm/channel/effect_retriggernote.go index bf76c58..7f7a886 100644 --- a/format/xm/effect/effect_retriggernote.go +++ b/format/xm/channel/effect_retriggernote.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,29 +6,28 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // RetriggerNote defines a retriggering effect -type RetriggerNote[TPeriod period.Period] channel.DataEffect // 'E9x' +type RetriggerNote[TPeriod period.Period] DataEffect // 'E9x' // Start triggers on the first tick, but before the Tick() function is called -func (e RetriggerNote[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e RetriggerNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - y := channel.DataEffect(e) & 0x0F +func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + y := DataEffect(e) & 0x0F if y == 0 { return nil } rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= y { + if DataEffect(rt) >= y { cs.SetPos(sampling.Pos{}) cs.ResetRetriggerCount() } @@ -36,5 +35,5 @@ func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory } func (e RetriggerNote[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_retrigvolslide.go b/format/xm/channel/effect_retrigvolslide.go similarity index 76% rename from format/xm/effect/effect_retrigvolslide.go rename to format/xm/channel/effect_retrigvolslide.go index 4a37eb2..0307650 100644 --- a/format/xm/effect/effect_retrigvolslide.go +++ b/format/xm/channel/effect_retrigvolslide.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,30 +6,29 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // RetrigVolumeSlide defines a retriggering volume slide effect -type RetrigVolumeSlide[TPeriod period.Period] channel.DataEffect // 'R' +type RetrigVolumeSlide[TPeriod period.Period] DataEffect // 'R' // Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) >> 4 - y := channel.DataEffect(e) & 0x0F +func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + x := DataEffect(e) >> 4 + y := DataEffect(e) & 0x0F if y == 0 { return nil } rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) - if channel.DataEffect(rt) >= x { + if DataEffect(rt) >= x { cs.SetPos(sampling.Pos{}) cs.ResetRetriggerCount() switch x { @@ -68,5 +67,5 @@ func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Me } func (e RetrigVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("R%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("R%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_rowjump.go b/format/xm/channel/effect_rowjump.go similarity index 56% rename from format/xm/effect/effect_rowjump.go rename to format/xm/channel/effect_rowjump.go index b327e00..4b4cb59 100644 --- a/format/xm/effect/effect_rowjump.go +++ b/format/xm/channel/effect_rowjump.go @@ -1,26 +1,25 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" ) // RowJump defines a row jump effect -type RowJump[TPeriod period.Period] channel.DataEffect // 'D' +type RowJump[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], 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[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, lastTick int) error { - xy := channel.DataEffect(e) +func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { + xy := DataEffect(e) x := xy >> 4 y := xy & 0x0f row := index.Row(x*10 + y) @@ -31,5 +30,5 @@ func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, channel.Memory], p p } func (e RowJump[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("D%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_sampleoffset.go b/format/xm/channel/effect_sampleoffset.go similarity index 63% rename from format/xm/effect/effect_sampleoffset.go rename to format/xm/channel/effect_sampleoffset.go index 8da5b62..ba19746 100644 --- a/format/xm/effect/effect_sampleoffset.go +++ b/format/xm/channel/effect_sampleoffset.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -6,22 +6,21 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // SampleOffset defines a sample offset effect -type SampleOffset[TPeriod period.Period] channel.DataEffect // '9' +type SampleOffset[TPeriod period.Period] DataEffect // '9' // Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() - xx := mem.SampleOffset(channel.DataEffect(e)) + xx := mem.SampleOffset(DataEffect(e)) cs.SetTargetPos(sampling.Pos{Pos: int(xx) * 0x100}) return nil } func (e SampleOffset[TPeriod]) String() string { - return fmt.Sprintf("9%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("9%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setcoarsepanposition.go b/format/xm/channel/effect_setcoarsepanposition.go similarity index 65% rename from format/xm/effect/effect_setcoarsepanposition.go rename to format/xm/channel/effect_setcoarsepanposition.go index 1002158..48b5e9b 100644 --- a/format/xm/effect/effect_setcoarsepanposition.go +++ b/format/xm/channel/effect_setcoarsepanposition.go @@ -1,22 +1,21 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" xmPanning "github.com/gotracker/playback/format/xm/panning" "github.com/gotracker/playback/period" ) // SetCoarsePanPosition defines a set pan position effect -type SetCoarsePanPosition[TPeriod period.Period] channel.DataEffect // 'E8x' +type SetCoarsePanPosition[TPeriod period.Period] DataEffect // 'E8x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - xy := channel.DataEffect(e) + xy := DataEffect(e) y := xy & 0x0F cs.SetPan(xmPanning.PanningFromXm(uint8(y) << 4)) @@ -24,5 +23,5 @@ func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channe } func (e SetCoarsePanPosition[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setenvelopeposition.go b/format/xm/channel/effect_setenvelopeposition.go similarity index 60% rename from format/xm/effect/effect_setenvelopeposition.go rename to format/xm/channel/effect_setenvelopeposition.go index 4cd0ca6..21ed626 100644 --- a/format/xm/effect/effect_setenvelopeposition.go +++ b/format/xm/channel/effect_setenvelopeposition.go @@ -1,26 +1,25 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // SetEnvelopePosition defines a set envelope position effect -type SetEnvelopePosition[TPeriod period.Period] channel.DataEffect // 'Lxx' +type SetEnvelopePosition[TPeriod period.Period] DataEffect // 'Lxx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetEnvelopePosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetEnvelopePosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - xx := channel.DataEffect(e) + xx := DataEffect(e) cs.SetEnvelopePosition(int(xx)) return nil } func (e SetEnvelopePosition[TPeriod]) String() string { - return fmt.Sprintf("L%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("L%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setfinetune.go b/format/xm/channel/effect_setfinetune.go similarity index 67% rename from format/it/effect/effect_setfinetune.go rename to format/xm/channel/effect_setfinetune.go index 9aad787..f109399 100644 --- a/format/it/effect/effect_setfinetune.go +++ b/format/xm/channel/effect_setfinetune.go @@ -1,20 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) // SetFinetune defines a mod-style set finetune effect -type SetFinetune[TPeriod period.Period] channel.DataEffect // 'S2x' +type SetFinetune[TPeriod period.Period] DataEffect // 'E5x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - x := channel.DataEffect(e) & 0xf +func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + x := DataEffect(e) & 0xf inst := cs.GetTargetInst() if inst != nil { @@ -25,11 +24,11 @@ func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memo } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e SetFinetune[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setglobalvolume.go b/format/xm/channel/effect_setglobalvolume.go similarity index 68% rename from format/xm/effect/effect_setglobalvolume.go rename to format/xm/channel/effect_setglobalvolume.go index 3cbb3bd..825ce1b 100644 --- a/format/xm/effect/effect_setglobalvolume.go +++ b/format/xm/channel/effect_setglobalvolume.go @@ -1,30 +1,29 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/period" ) // SetGlobalVolume defines a set global volume effect -type SetGlobalVolume[TPeriod period.Period] channel.DataEffect // 'G' +type SetGlobalVolume[TPeriod period.Period] DataEffect // 'G' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], 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[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e SetGlobalVolume[TPeriod]) String() string { - return fmt.Sprintf("G%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("G%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setpanposition.go b/format/xm/channel/effect_setpanposition.go similarity index 67% rename from format/xm/effect/effect_setpanposition.go rename to format/xm/channel/effect_setpanposition.go index 6b0fd47..39563af 100644 --- a/format/xm/effect/effect_setpanposition.go +++ b/format/xm/channel/effect_setpanposition.go @@ -1,19 +1,18 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" xmPanning "github.com/gotracker/playback/format/xm/panning" "github.com/gotracker/playback/period" ) // SetPanPosition defines a set pan position effect -type SetPanPosition[TPeriod period.Period] channel.DataEffect // '8xx' +type SetPanPosition[TPeriod period.Period] DataEffect // '8xx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := uint8(e) @@ -23,5 +22,5 @@ func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memo } func (e SetPanPosition[TPeriod]) String() string { - return fmt.Sprintf("8%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("8%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setspeed.go b/format/xm/channel/effect_setspeed.go similarity index 55% rename from format/xm/effect/effect_setspeed.go rename to format/xm/channel/effect_setspeed.go index 88ae67a..37740f6 100644 --- a/format/xm/effect/effect_setspeed.go +++ b/format/xm/channel/effect_setspeed.go @@ -1,21 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" "github.com/gotracker/playback/period" ) // SetSpeed defines a set speed effect -type SetSpeed[TPeriod period.Period] channel.DataEffect // 'F' +type SetSpeed[TPeriod period.Period] DataEffect // 'F' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { if e != 0 { - m := p.(effectIntf.XM) + m := p.(XM) if err := m.SetTicks(int(e)); err != nil { return err } @@ -24,11 +22,11 @@ func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory] } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } func (e SetSpeed[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_settempo.go b/format/xm/channel/effect_settempo.go similarity index 50% rename from format/xm/effect/effect_settempo.go rename to format/xm/channel/effect_settempo.go index d3760fa..2a51f21 100644 --- a/format/xm/effect/effect_settempo.go +++ b/format/xm/channel/effect_settempo.go @@ -1,21 +1,19 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" "github.com/gotracker/playback/period" ) // SetTempo defines a set tempo effect -type SetTempo[TPeriod period.Period] channel.DataEffect // 'F' +type SetTempo[TPeriod period.Period] DataEffect // 'F' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { if e > 0x20 { - m := p.(effectIntf.XM) + m := p.(XM) if err := m.SetTempo(int(e)); err != nil { return err } @@ -24,17 +22,17 @@ func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory] } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - m := p.(effectIntf.XM) +func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + m := p.(XM) return m.SetTempo(int(e)) } func (e SetTempo[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("F%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_settremolowaveform.go b/format/xm/channel/effect_settremolowaveform.go similarity index 66% rename from format/xm/effect/effect_settremolowaveform.go rename to format/xm/channel/effect_settremolowaveform.go index 9f58ab5..2651061 100644 --- a/format/xm/effect/effect_settremolowaveform.go +++ b/format/xm/channel/effect_settremolowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -7,17 +7,16 @@ import ( "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[TPeriod period.Period] channel.DataEffect // 'E7x' +type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'E7x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() trem := mem.TremoloOscillator() @@ -26,5 +25,5 @@ func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel. } func (e SetTremoloWaveform[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/it/effect/effect_setvibratowaveform.go b/format/xm/channel/effect_setvibratowaveform.go similarity index 66% rename from format/it/effect/effect_setvibratowaveform.go rename to format/xm/channel/effect_setvibratowaveform.go index f41adce..8547ad2 100644 --- a/format/it/effect/effect_setvibratowaveform.go +++ b/format/xm/channel/effect_setvibratowaveform.go @@ -1,4 +1,4 @@ -package effect +package channel import ( "fmt" @@ -7,17 +7,16 @@ import ( "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[TPeriod period.Period] channel.DataEffect // 'S3x' +type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'E4x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() - x := channel.DataEffect(e) & 0xf + x := DataEffect(e) & 0xf mem := cs.GetMemory() vib := mem.VibratoOscillator() @@ -26,5 +25,5 @@ func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, channel. } func (e SetVibratoWaveform[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("E%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_setvolume.go b/format/xm/channel/effect_setvolume.go similarity index 59% rename from format/xm/effect/effect_setvolume.go rename to format/xm/channel/effect_setvolume.go index 11bc16b..0f7b9ff 100644 --- a/format/xm/effect/effect_setvolume.go +++ b/format/xm/channel/effect_setvolume.go @@ -1,19 +1,18 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/period" ) // SetVolume defines a volume slide effect -type SetVolume[TPeriod period.Period] channel.DataEffect // 'C' +type SetVolume[TPeriod period.Period] DataEffect // 'C' // Start triggers on the first tick, but before the Tick() function is called -func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() xx := xmVolume.XmVolume(e) @@ -23,5 +22,5 @@ func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], } func (e SetVolume[TPeriod]) String() string { - return fmt.Sprintf("C%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("C%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_tremolo.go b/format/xm/channel/effect_tremolo.go similarity index 52% rename from format/xm/effect/effect_tremolo.go rename to format/xm/channel/effect_tremolo.go index 1d41bf3..4272454 100644 --- a/format/xm/effect/effect_tremolo.go +++ b/format/xm/channel/effect_tremolo.go @@ -1,31 +1,30 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // Tremolo defines a tremolo effect -type Tremolo[TPeriod period.Period] channel.DataEffect // '7' +type Tremolo[TPeriod period.Period] DataEffect // '7' // Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Tremolo(channel.DataEffect(e)) + 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 doTremolo(cs, currentTick, x, y, 4) } func (e Tremolo[TPeriod]) String() string { - return fmt.Sprintf("7%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("7%0.2x", DataEffect(e)) } diff --git a/format/xm/channel/effect_tremor.go b/format/xm/channel/effect_tremor.go new file mode 100644 index 0000000..8210e13 --- /dev/null +++ b/format/xm/channel/effect_tremor.go @@ -0,0 +1,31 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +// Tremor defines a tremor effect +type Tremor[TPeriod period.Period] DataEffect // 'T' + +// Start triggers on the first tick, but before the Tick() function is called +func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { + cs.ResetRetriggerCount() + return nil +} + +// Tick is called on every tick +func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { + if currentTick != 0 { + mem := cs.GetMemory() + x, y := mem.Tremor(DataEffect(e)) + return doTremor(cs, currentTick, int(x)+1, int(y)+1) + } + return nil +} + +func (e Tremor[TPeriod]) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) +} diff --git a/format/xm/effect/effect_vibrato.go b/format/xm/channel/effect_vibrato.go similarity index 53% rename from format/xm/effect/effect_vibrato.go rename to format/xm/channel/effect_vibrato.go index 53f91ef..3e72976 100644 --- a/format/xm/effect/effect_vibrato.go +++ b/format/xm/channel/effect_vibrato.go @@ -1,32 +1,31 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // Vibrato defines a vibrato effect -type Vibrato[TPeriod period.Period] channel.DataEffect // '4' +type Vibrato[TPeriod period.Period] DataEffect // '4' // Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.Vibrato(channel.DataEffect(e)) + 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 doVibrato(cs, currentTick, x, y, 4) } func (e Vibrato[TPeriod]) String() string { - return fmt.Sprintf("4%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("4%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effect_vibratovolslide.go b/format/xm/channel/effect_vibratovolslide.go similarity index 61% rename from format/xm/effect/effect_vibratovolslide.go rename to format/xm/channel/effect_vibratovolslide.go index f5ceebe..237c1f5 100644 --- a/format/xm/effect/effect_vibratovolslide.go +++ b/format/xm/channel/effect_vibratovolslide.go @@ -1,25 +1,24 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide[TPeriod period.Period] struct { // '6' - playback.CombinedEffect[TPeriod, channel.Memory] + playback.CombinedEffect[TPeriod, Memory] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object -func NewVibratoVolumeSlide[TPeriod period.Period](val channel.DataEffect) VibratoVolumeSlide[TPeriod] { +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", channel.DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) + return fmt.Sprintf("6%0.2x", DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) } diff --git a/format/xm/effect/effect_volslide.go b/format/xm/channel/effect_volslide.go similarity index 67% rename from format/xm/effect/effect_volslide.go rename to format/xm/channel/effect_volslide.go index 8ecc8a3..1b24508 100644 --- a/format/xm/effect/effect_volslide.go +++ b/format/xm/channel/effect_volslide.go @@ -1,26 +1,25 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) // VolumeSlide defines a volume slide effect -type VolumeSlide[TPeriod period.Period] channel.DataEffect // 'A' +type VolumeSlide[TPeriod period.Period] DataEffect // 'A' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { +func (e VolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { mem := cs.GetMemory() - x, y := mem.VolumeSlide(channel.DataEffect(e)) + x, y := mem.VolumeSlide(DataEffect(e)) if currentTick == 0 { return nil @@ -37,5 +36,5 @@ func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], } func (e VolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("A%0.2x", channel.DataEffect(e)) + return fmt.Sprintf("A%0.2x", DataEffect(e)) } diff --git a/format/xm/effect/effectfactory.go b/format/xm/channel/effectfactory.go similarity index 73% rename from format/xm/effect/effectfactory.go rename to format/xm/channel/effectfactory.go index 9f9bc0f..df7efae 100644 --- a/format/xm/effect/effectfactory.go +++ b/format/xm/channel/effectfactory.go @@ -1,10 +1,9 @@ -package effect +package channel import ( "fmt" "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" ) @@ -13,7 +12,7 @@ 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, channel.Memory] + playback.CombinedEffect[TPeriod, Memory] eff EffectXM } @@ -24,13 +23,21 @@ func (e VolEff[TPeriod]) String() string { 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 +} + // Factory produces an effect for the provided channel pattern data -func Factory[TPeriod period.Period](mem *channel.Memory, data song.ChannelData) EffectXM { +func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData) EffectXM { if data == nil { return nil } - d, _ := data.(channel.Data) + d, _ := data.(Data) if !d.HasCommand() { return nil } diff --git a/format/xm/effect/effectfactory_standard.go b/format/xm/channel/effectfactory_standard.go similarity index 89% rename from format/xm/effect/effectfactory_standard.go rename to format/xm/channel/effectfactory_standard.go index 0c2e884..9821ac7 100644 --- a/format/xm/effect/effectfactory_standard.go +++ b/format/xm/channel/effectfactory_standard.go @@ -1,11 +1,10 @@ -package effect +package channel import ( - "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/period" ) -func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, cd channel.Data) EffectXM { +func standardEffectFactory[TPeriod period.Period](mem *Memory, cd Data) EffectXM { if !cd.What.HasEffect() && !cd.What.HasEffectParameter() { return nil } @@ -69,7 +68,7 @@ func standardEffectFactory[TPeriod period.Period](mem *channel.Memory, cd channe return UnhandledCommand[TPeriod]{Command: cd.Effect, Info: cd.EffectParameter} } -func extraFinePortaEffectFactory[TPeriod period.Period](mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { +func extraFinePortaEffectFactory[TPeriod period.Period](mem *Memory, ce Command, cp DataEffect) EffectXM { switch cp >> 4 { case 0x0: // none return nil @@ -81,7 +80,7 @@ func extraFinePortaEffectFactory[TPeriod period.Period](mem *channel.Memory, ce return UnhandledCommand[TPeriod]{Command: ce, Info: cp} } -func specialEffectFactory[TPeriod period.Period](mem *channel.Memory, ce channel.Command, cp channel.DataEffect) EffectXM { +func specialEffectFactory[TPeriod period.Period](mem *Memory, ce Command, cp DataEffect) EffectXM { switch cp >> 4 { case 0x1: // Fine porta up return FinePortaUp[TPeriod](cp) diff --git a/format/xm/effect/effectfactory_volume.go b/format/xm/channel/effectfactory_volume.go similarity index 81% rename from format/xm/effect/effectfactory_volume.go rename to format/xm/channel/effectfactory_volume.go index 4a2ff58..e49db15 100644 --- a/format/xm/effect/effectfactory_volume.go +++ b/format/xm/channel/effectfactory_volume.go @@ -1,12 +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[TPeriod period.Period](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 @@ -22,11 +21,11 @@ func volumeEffectFactory[TPeriod period.Period](mem *channel.Memory, v xmVolume. case v >= 0x90 && v <= 0x9f: // fine volume slide up 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[TPeriod](vs<<4 | (channel.DataEffect(v) & 0x0f)) + return Vibrato[TPeriod](vs<<4 | (DataEffect(v) & 0x0f)) case v >= 0xC0 && v <= 0xCf: // set panning return SetCoarsePanPosition[TPeriod](v & 0x0f) case v >= 0xD0 && v <= 0xDf: // panning slide left diff --git a/format/xm/effect/unhandled.go b/format/xm/channel/unhandled.go similarity index 72% rename from format/xm/effect/unhandled.go rename to format/xm/channel/unhandled.go index ccb84ea..1311bcd 100644 --- a/format/xm/effect/unhandled.go +++ b/format/xm/channel/unhandled.go @@ -1,23 +1,21 @@ -package effect +package channel 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" "github.com/gotracker/playback/period" ) // UnhandledCommand is an unhandled command type UnhandledCommand[TPeriod period.Period] struct { - Command channel.Command - Info channel.DataEffect + Command Command + Info DataEffect } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.XM) error { +func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } @@ -28,13 +26,19 @@ 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()), + } +} + // UnhandledVolCommand is an unhandled volume command type UnhandledVolCommand[TPeriod period.Period] struct { Vol xmVolume.VolEffect } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, channel.Memory], m effectIntf.XM) error { +func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } @@ -44,3 +48,9 @@ func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, chan 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()), + } +} diff --git a/format/xm/effect/util.go b/format/xm/channel/util.go similarity index 65% rename from format/xm/effect/util.go rename to format/xm/channel/util.go index ac366f3..58efa74 100644 --- a/format/xm/effect/util.go +++ b/format/xm/channel/util.go @@ -1,19 +1,35 @@ -package effect +package channel import ( + "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" - "github.com/gotracker/playback/format/xm/channel" - effectIntf "github.com/gotracker/playback/format/xm/effect/intf" xmPeriod "github.com/gotracker/playback/format/xm/period" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/note" "github.com/heucuva/comparison" ) -func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], delta float32, multiplier float32) error { +// 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 +} + +func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := xmVolume.ToVolumeXM(av) vol := int16((float32(v) + delta) * multiplier) @@ -23,12 +39,12 @@ func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memo if vol < 0x00 { vol = 0x00 } - v = xmVolume.XmVolume(channel.DataEffect(vol)) + v = xmVolume.XmVolume(DataEffect(vol)) cs.SetActiveVolume(v.Volume()) return nil } -func doGlobalVolSlide(m effectIntf.XM, delta float32, multiplier float32) error { +func doGlobalVolSlide(m XM, delta float32, multiplier float32) error { gv := m.GetGlobalVolume() v := xmVolume.ToVolumeXM(gv) vol := int16((float32(v) + delta) * multiplier) @@ -38,12 +54,12 @@ func doGlobalVolSlide(m effectIntf.XM, delta float32, multiplier float32) error if vol < 0x00 { vol = 0x00 } - v = xmVolume.XmVolume(channel.DataEffect(vol)) + v = xmVolume.XmVolume(DataEffect(vol)) m.SetGlobalVolume(v.Volume()) return nil } -func doPortaByDeltaAmiga(cs playback.Channel[xmPeriod.Amiga, channel.Memory], delta int) error { +func doPortaByDeltaAmiga(cs playback.Channel[xmPeriod.Amiga, Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -55,7 +71,7 @@ func doPortaByDeltaAmiga(cs playback.Channel[xmPeriod.Amiga, channel.Memory], de return nil } -func doPortaByDeltaLinear(cs playback.Channel[xmPeriod.Linear, channel.Memory], delta int) error { +func doPortaByDeltaLinear(cs playback.Channel[xmPeriod.Linear, Memory], delta int) error { cur := cs.GetPeriod() if cur == nil { return nil @@ -67,19 +83,19 @@ func doPortaByDeltaLinear(cs playback.Channel[xmPeriod.Linear, channel.Memory], return nil } -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) switch csp := any(cs).(type) { - case playback.Channel[xmPeriod.Linear, channel.Memory]: + case playback.Channel[xmPeriod.Linear, Memory]: return doPortaByDeltaLinear(csp, delta) - case playback.Channel[xmPeriod.Amiga, channel.Memory]: + case playback.Channel[xmPeriod.Amiga, Memory]: return doPortaByDeltaAmiga(csp, -delta) default: panic("unhandled channel type") } } -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target *TPeriod) error { if err := doPortaUp(cs, amount, multiplier); err != nil { return err } @@ -89,19 +105,19 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel return nil } -func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32) error { +func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) switch csp := any(cs).(type) { - case playback.Channel[xmPeriod.Linear, channel.Memory]: + case playback.Channel[xmPeriod.Linear, Memory]: return doPortaByDeltaLinear(csp, -delta) - case playback.Channel[xmPeriod.Amiga, channel.Memory]: + case playback.Channel[xmPeriod.Amiga, Memory]: return doPortaByDeltaAmiga(csp, delta) default: panic("unhandled channel type") } } -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], amount float32, multiplier float32, target *TPeriod) error { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target *TPeriod) error { if err := doPortaDown(cs, amount, multiplier); err != nil { return err } @@ -111,7 +127,7 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, chann return nil } -func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) delta := period.PeriodDelta(vib) @@ -119,7 +135,7 @@ func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memor return nil } -func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, onTicks int, offTicks int) error { +func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -135,7 +151,7 @@ func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory return nil } -func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -160,7 +176,7 @@ var ( } ) -func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory]) error { +func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory]) error { vol := xmVolume.ToVolumeXM(cs.GetActiveVolume()) if vol >= 64 { vol = 63 @@ -175,13 +191,13 @@ func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, cha return nil } -func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32) error { +func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) return doVolSlide(cs, delta, 1.0) } -func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, channel.Memory], currentTick int, speed channel.DataEffect, depth channel.DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { delta := o.GetWave(float32(depth) * multiplier) o.Advance(int(speed)) return delta diff --git a/format/xm/effect/effect_notecut.go b/format/xm/effect/effect_notecut.go deleted file mode 100644 index 6e43888..0000000 --- a/format/xm/effect/effect_notecut.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/period" -) - -// NoteCut defines a note cut effect -type NoteCut[TPeriod period.Period] channel.DataEffect // 'ECx' - -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback, currentTick int) error { - x := channel.DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.NoteCut() - } - return nil -} - -func (e NoteCut[TPeriod]) String() string { - return fmt.Sprintf("E%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 2355765..0000000 --- a/format/xm/effect/effect_tremor.go +++ /dev/null @@ -1,32 +0,0 @@ -package effect - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/period" -) - -// Tremor defines a tremor effect -type Tremor[TPeriod period.Period] channel.DataEffect // 'T' - -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, channel.Memory], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -// Tick is called on every tick -func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, channel.Memory], 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[TPeriod]) String() string { - return fmt.Sprintf("T%0.2x", channel.DataEffect(e)) -} 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/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index 53d4142..e27277a 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -5,7 +5,6 @@ import ( "time" "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/format/xm/effect" "github.com/gotracker/playback/index" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" @@ -88,7 +87,7 @@ func (m *manager[TPeriod]) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory](effect.Factory[TPeriod])) + cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory]()) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/instrument/instrument.go b/instrument/instrument.go index efeae86..acf1402 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -3,12 +3,12 @@ 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/note" - "github.com/heucuva/optional" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" ) // StaticValues are the static values associated with an instrument diff --git a/player/state/channel.go b/player/state/channel.go index 3ab0fad..2398387 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -40,7 +40,7 @@ type ChannelState[TPeriod period.Period, TMemory any] struct { targetState Playback[TPeriod] prevState Active[TPeriod] - ActiveEffect playback.Effect + ActiveEffects []playback.Effect s song.Data txn ChannelDataTransaction[TPeriod, TMemory] @@ -106,12 +106,12 @@ func (cs *ChannelState[TPeriod, TMemory]) ResetStates() { cs.prevState.Reset() } -func (cs *ChannelState[TPeriod, TMemory]) GetActiveEffect() playback.Effect { - return cs.ActiveEffect +func (cs *ChannelState[TPeriod, TMemory]) GetActiveEffects() []playback.Effect { + return cs.ActiveEffects } -func (cs *ChannelState[TPeriod, TMemory]) SetActiveEffect(e playback.Effect) { - cs.ActiveEffect = e +func (cs *ChannelState[TPeriod, TMemory]) SetActiveEffects(effects []playback.Effect) { + cs.ActiveEffects = effects } // FreezePlayback suspends mixer progression on the channel diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index 1671234..26e8f10 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -42,8 +42,7 @@ type ChannelDataActions[TPeriod period.Period] struct { type ChannelDataConverter[TPeriod period.Period, TMemory any] func(out *ChannelDataActions[TPeriod], data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error type ChannelDataTxnHelper[TPeriod period.Period, TMemory any] struct { - Data song.ChannelData - effectFactory func(*TMemory, song.ChannelData) playback.Effect + Data song.ChannelData ChannelDataActions[TPeriod] @@ -51,10 +50,8 @@ type ChannelDataTxnHelper[TPeriod period.Period, TMemory any] struct { NoteOps []NoteOp[TPeriod, TMemory] } -func NewChannelDataTxn[TPeriod period.Period, TMemory any](effectFactory func(*TMemory, song.ChannelData) playback.Effect) ChannelDataTransaction[TPeriod, TMemory] { - return &ChannelDataTxnHelper[TPeriod, TMemory]{ - effectFactory: effectFactory, - } +func NewChannelDataTxn[TPeriod period.Period, TMemory any]() ChannelDataTransaction[TPeriod, TMemory] { + return &ChannelDataTxnHelper[TPeriod, TMemory]{} } func (d *ChannelDataTxnHelper[TPeriod, TMemory]) GetData() song.ChannelData { @@ -67,13 +64,14 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) SetData(cd song.ChannelData, s } func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { - e := d.effectFactory(cs.GetMemory(), d.Data) - cs.SetActiveEffect(e) - if e != nil { - if onEff := p.GetOnEffect(); onEff != nil { + effects := playback.GetEffects[TPeriod, TMemory](cs.GetMemory(), d.Data) + cs.SetActiveEffects(effects) + onEff := p.GetOnEffect() + for _, e := range effects { + if onEff != nil { onEff(e) } - if err := playback.EffectPreStart[TPeriod, TMemory](e, any(cs).(playback.Channel[TPeriod, TMemory]), p); err != nil { + if err := playback.EffectPreStart[TPeriod, TMemory](e, cs, p); err != nil { return err } } @@ -135,8 +133,10 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreTick(p playback.Playba } func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { - if err := playback.DoEffect[TPeriod, TMemory](cs.ActiveEffect, cs, p, currentTick, lastTick); err != nil { - return err + for _, e := range cs.GetActiveEffects() { + if err := playback.DoEffect[TPeriod, TMemory](e, cs, p, currentTick, lastTick); err != nil { + return err + } } return nil From 0e64e52d705754400706ff01335c6a408cb66ae0 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 21 Dec 2023 11:09:47 -0800 Subject: [PATCH 10/63] spacing --- player/render/render.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/render/render.go b/player/render/render.go index 4d3b29a..5349b1c 100644 --- a/player/render/render.go +++ b/player/render/render.go @@ -44,7 +44,7 @@ type RowStringer interface { String(options ...any) string } -//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 From 5b894e3bf3167e077769b22cab91dbd4ae316abc Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 21 Dec 2023 19:14:49 -0800 Subject: [PATCH 11/63] drastically simplify period considerations --- channel.go | 18 +-- format/it/channel/data.go | 9 +- format/it/channel/effect_portatonote.go | 12 +- format/it/channel/util.go | 48 ++----- format/it/period/amiga.go | 73 ---------- format/it/period/amigaconverter.go | 27 ++++ format/it/period/linear.go | 60 --------- format/it/period/linearconverter.go | 24 ++++ format/it/period/util.go | 36 +++-- format/it/playback/playback.go | 23 ++-- format/it/playback/playback_command.go | 5 +- format/s3m/channel/data.go | 21 +-- format/s3m/channel/effect_portatonote.go | 5 +- format/s3m/channel/effect_portavolslide.go | 4 +- format/s3m/channel/effect_vibratovolslide.go | 4 +- format/s3m/channel/note_start.go | 87 ------------ format/s3m/channel/note_stop.go | 36 ----- format/s3m/channel/notefactory.go | 27 ---- format/s3m/channel/util.go | 32 ++--- format/s3m/channel/volume_setvolume.go | 23 ---- format/s3m/channel/volumefactory.go | 7 - format/s3m/period/amiga.go | 73 ---------- format/s3m/period/amigaconverter.go | 27 ++++ format/s3m/period/util.go | 22 ++- format/s3m/playback/playback.go | 8 +- format/s3m/playback/playback_command.go | 7 +- format/s3m/playback/playback_pattern.go | 4 +- format/xm/channel/data.go | 9 +- format/xm/channel/util.go | 45 ++----- format/xm/load/xmformat.go | 4 +- format/xm/period/amiga.go | 73 ---------- format/xm/period/amigaconverter.go | 27 ++++ format/xm/period/linear.go | 60 --------- format/xm/period/linearconverter.go | 24 ++++ format/xm/period/util.go | 23 ++-- format/xm/playback/playback.go | 23 ++-- format/xm/playback/playback_command.go | 8 +- period/amiga.go | 62 +++------ period/delta.go | 2 +- period/linear.go | 68 +++------- period/period.go | 45 +++---- period/periodconverter.go | 6 + player/state/active.go | 20 +-- player/state/channel.go | 46 ++++--- player/state/channel_transaction.go | 4 +- player/state/playback.go | 5 +- player/voice/opl2.go | 91 ++++++------- player/voice/pcm.go | 133 ++++++++++--------- player/voice/transaction.go | 86 ++++++------ player/voice/voice.go | 7 +- util/lerp.go | 18 +-- voice/component/envelope_pitch.go | 29 ++-- voice/component/modulator_freq.go | 29 ++-- voice/component/opl2.go | 33 ++--- voice/freqmodulator.go | 8 +- voice/pitchenveloper.go | 2 +- voice/transaction.go | 10 +- voice/voice.go | 46 ++++--- 58 files changed, 650 insertions(+), 1118 deletions(-) delete mode 100644 format/it/period/amiga.go create mode 100644 format/it/period/amigaconverter.go delete mode 100644 format/it/period/linear.go create mode 100644 format/it/period/linearconverter.go delete mode 100644 format/s3m/channel/note_start.go delete mode 100644 format/s3m/channel/note_stop.go delete mode 100644 format/s3m/channel/notefactory.go delete mode 100644 format/s3m/channel/volume_setvolume.go delete mode 100644 format/s3m/channel/volumefactory.go delete mode 100644 format/s3m/period/amiga.go create mode 100644 format/s3m/period/amigaconverter.go delete mode 100644 format/xm/period/amiga.go create mode 100644 format/xm/period/amigaconverter.go delete mode 100644 format/xm/period/linear.go create mode 100644 format/xm/period/linearconverter.go create mode 100644 period/periodconverter.go diff --git a/channel.go b/channel.go index 5836b83..f4eba8f 100644 --- a/channel.go +++ b/channel.go @@ -23,15 +23,15 @@ type Channel[TPeriod period.Period, TMemory any] interface { FreezePlayback() UnfreezePlayback() GetData() song.ChannelData - GetPortaTargetPeriod() *TPeriod - SetPortaTargetPeriod(*TPeriod) - GetTargetPeriod() *TPeriod - SetTargetPeriod(*TPeriod) - SetPeriodOverride(*TPeriod) - GetPeriod() *TPeriod - SetPeriod(*TPeriod) - SetPeriodDelta(period.PeriodDelta) - GetPeriodDelta() period.PeriodDelta + GetPortaTargetPeriod() TPeriod + SetPortaTargetPeriod(TPeriod) + GetTargetPeriod() TPeriod + SetTargetPeriod(TPeriod) + SetPeriodOverride(TPeriod) + GetPeriod() TPeriod + SetPeriod(TPeriod) + SetPeriodDelta(period.Delta) + GetPeriodDelta() period.Delta SetInstrument(*instrument.Instrument) GetInstrument() *instrument.Instrument GetVoice() voice.Voice diff --git a/format/it/channel/data.go b/format/it/channel/data.go index 3a7d753..b3bd98f 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -9,7 +9,6 @@ import ( "github.com/gotracker/playback" itNote "github.com/gotracker/playback/format/it/note" - itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" @@ -107,12 +106,12 @@ func (d Data) Channel() uint8 { func (d Data) GetEffects(mem *Memory, periodType period.Period) []playback.Effect { switch periodType.(type) { - case itPeriod.Linear: - if e := EffectFactory[itPeriod.Linear](mem, d); e != nil { + case period.Linear: + if e := EffectFactory[period.Linear](mem, d); e != nil { return []playback.Effect{e} } - case itPeriod.Amiga: - if e := EffectFactory[itPeriod.Amiga](mem, d); e != nil { + case period.Amiga: + if e := EffectFactory[period.Amiga](mem, d); e != nil { return []playback.Effect{e} } default: diff --git a/format/it/channel/effect_portatonote.go b/format/it/channel/effect_portatonote.go index f6f5467..52f1dd8 100644 --- a/format/it/channel/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/gotracker/playback" - itPeriod "github.com/gotracker/playback/format/it/period" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" "github.com/heucuva/comparison" @@ -31,17 +30,10 @@ func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playb // vibrato modifies current period for portamento cur := cs.GetPeriod() - if cur == nil { + if cur.IsInvalid() { return nil } - switch pc := any(cur).(type) { - case *itPeriod.Linear: - cur = any(pc.Add(cs.GetPeriodDelta())).(*TPeriod) - case *itPeriod.Amiga: - cur = any(pc.Add(cs.GetPeriodDelta())).(*TPeriod) - default: - panic("unhandled period type") - } + cur = period.AddDelta(cur, cs.GetPeriodDelta()) ptp := cs.GetPortaTargetPeriod() if !mem.Shared.OldEffectMode || currentTick != 0 { if period.ComparePeriods(cur, ptp) == comparison.SpaceshipRightGreater { diff --git a/format/it/channel/util.go b/format/it/channel/util.go index fc7b8e7..7b4a884 100644 --- a/format/it/channel/util.go +++ b/format/it/channel/util.go @@ -8,7 +8,6 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" - itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/note" "github.com/heucuva/comparison" @@ -63,43 +62,24 @@ func doGlobalVolSlide(m IT, delta float32, multiplier float32) error { return nil } -func doPortaByDeltaAmiga(cs playback.Channel[itPeriod.Amiga, Memory], delta int) error { +func doPortaByDelta[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta period.Delta) error { cur := cs.GetPeriod() - if cur == nil { + if cur.IsInvalid() { return nil } - d := period.PeriodDelta(delta) - cur = cur.Add(d) - cs.SetPeriod(cur) - return nil -} - -func doPortaByDeltaLinear(cs playback.Channel[itPeriod.Linear, Memory], delta int) error { - cur := cs.GetPeriod() - if cur == nil { - return nil - } - - finetune := period.PeriodDelta(delta) - cur = cur.Add(finetune) + cur = period.AddDelta(cur, delta) cs.SetPeriod(cur) return nil } func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) - switch csp := any(cs).(type) { - case playback.Channel[itPeriod.Linear, Memory]: - return doPortaByDeltaLinear(csp, delta) - case playback.Channel[itPeriod.Amiga, Memory]: - return doPortaByDeltaAmiga(csp, -delta) - default: - panic("unhandled channel type") - } + d := period.Delta(delta) + return doPortaByDelta(cs, d) } -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target *TPeriod) error { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { return err } @@ -111,17 +91,11 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory] func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { delta := int(amount * multiplier) - switch csp := any(cs).(type) { - case playback.Channel[itPeriod.Linear, Memory]: - return doPortaByDeltaLinear(csp, -delta) - case playback.Channel[itPeriod.Amiga, Memory]: - return doPortaByDeltaAmiga(csp, delta) - default: - panic("unhandled channel type") - } + d := period.Delta(-delta) + return doPortaByDelta(cs, d) } -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target *TPeriod) error { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { if err := doPortaDown(cs, amount, multiplier); err != nil { return err } @@ -134,8 +108,8 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memor func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) - delta := period.PeriodDelta(vib) - cs.SetPeriodDelta(delta) + d := period.Delta(vib) + cs.SetPeriodDelta(d) return nil } diff --git a/format/it/period/amiga.go b/format/it/period/amiga.go deleted file mode 100644 index 794cfe0..0000000 --- a/format/it/period/amiga.go +++ /dev/null @@ -1,73 +0,0 @@ -package period - -import ( - "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 struct { - period.Amiga -} - -var _ period.Period = (*Amiga)(nil) - -// Add adds the current period to a delta value then returns the resulting period -func (p Amiga) Add(delta period.PeriodDelta) *Amiga { - p.Amiga += period.Amiga(delta) - return &p -} - -// AddDelta 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) - return p.Add(d) -} - -// 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 { - if q, ok := rhs.ToAmigaPeriod().(Amiga); ok { - return period.CompareAmiga(p.Amiga, q.Amiga) - } - return comparison.SpaceshipLeftGreater -} - -// GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() period.Frequency { - if p.Amiga == 0 { - return 0 - } - return period.Frequency(ITBaseClock) / period.Frequency(p.Amiga) -} - -// ToLinearPeriod returns the linear frequency period for a given period -func (p Amiga) ToLinearPeriod() period.Period { - return nil -} - -// ToAmigaPeriod returns the amiga (protracker) representation for a given period -func (p Amiga) ToAmigaPeriod() period.Period { - return 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)/SlideFinesPerOctave) - linFreq := float64(c2spd) * pow / float64(DefaultC2Spd) - - return Amiga{ - Amiga: period.Amiga(float64(semitonePeriodTable[0]) / linFreq), - } -} diff --git a/format/it/period/amigaconverter.go b/format/it/period/amigaconverter.go new file mode 100644 index 0000000..3972d9e --- /dev/null +++ b/format/it/period/amigaconverter.go @@ -0,0 +1,27 @@ +package period + +import ( + "github.com/gotracker/playback/period" +) + +// AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note +// definition. Useful in calculating resampling. +type amigaConverter struct{} + +var AmigaConverter period.PeriodConverter[period.Amiga] = amigaConverter{} + +// GetFrequency returns the frequency defined by the period +func (amigaConverter) GetFrequency(p period.Amiga) period.Frequency { + if p.IsInvalid() { + return 0 + } + return period.Frequency(ITBaseClock) / period.Frequency(p) +} + +// GetSamplerAdd returns the number of samples to advance an instrument by given the period +func (amigaConverter) GetSamplerAdd(p period.Amiga, samplerSpeed float64) float64 { + if p.IsInvalid() { + return 0 + } + return samplerSpeed / float64(p) +} diff --git a/format/it/period/linear.go b/format/it/period/linear.go deleted file mode 100644 index 04f1070..0000000 --- a/format/it/period/linear.go +++ /dev/null @@ -1,60 +0,0 @@ -package period - -import ( - "math" - - "github.com/gotracker/playback/period" - "github.com/heucuva/comparison" -) - -// Linear is a linear period, based on semitone and finetune values -type Linear struct { - period.Linear -} - -var _ period.Period = (*Linear)(nil) - -// Add adds the current period to a delta value then returns the resulting period -func (p Linear) Add(delta period.PeriodDelta) *Linear { - p.Linear = period.AddLinearDelta(p.Linear, delta) - return &p -} - -// AddDelta adds the current period to a delta value then returns the resulting period -func (p Linear) AddDelta(delta period.Delta) period.Period { - return p.Add(period.ToPeriodDelta(delta)) -} - -// 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 { - if q, ok := rhs.ToLinearPeriod().(Linear); ok { - return period.CompareLinear(p.Linear, q.Linear) - } - return comparison.SpaceshipLeftGreater -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Linear) GetSamplerAdd(samplerSpeed float64) float64 { - return float64(p.GetFrequency()) * samplerSpeed / float64(ITBaseClock) -} - -// GetFrequency returns the frequency defined by the period -func (p Linear) GetFrequency() period.Frequency { - pft := float64(p.Finetune-C5SlideFines) / float64(SlideFinesPerOctave) - f := p.CommonRate * period.Frequency(math.Pow(2.0, pft)) - return f -} - -// ToLinearPeriod returns the linear frequency period for a given period -func (p Linear) ToLinearPeriod() period.Period { - return p -} - -// ToAmigaPeriod returns the amiga (protracker) representation for a given period -func (p Linear) ToAmigaPeriod() period.Period { - return nil -} diff --git a/format/it/period/linearconverter.go b/format/it/period/linearconverter.go new file mode 100644 index 0000000..946e7e3 --- /dev/null +++ b/format/it/period/linearconverter.go @@ -0,0 +1,24 @@ +package period + +import ( + "math" + + "github.com/gotracker/playback/period" +) + +// Linear is a linear period, based on semitone and finetune values +type linearConverter struct{} + +var LinearConverter period.PeriodConverter[period.Linear] = linearConverter{} + +// GetSamplerAdd returns the number of samples to advance an instrument by given the period +func (c linearConverter) GetSamplerAdd(p period.Linear, samplerSpeed float64) float64 { + return float64(c.GetFrequency(p)) * samplerSpeed / float64(ITBaseClock) +} + +// GetFrequency returns the frequency defined by the period +func (linearConverter) GetFrequency(p period.Linear) period.Frequency { + pft := float64(p.Finetune-C5SlideFines) / float64(SlideFinesPerOctave) + f := p.CommonRate * period.Frequency(math.Pow(2.0, pft)) + return f +} diff --git a/format/it/period/util.go b/format/it/period/util.go index 35f11c2..d841bf1 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -1,6 +1,8 @@ package period import ( + "math" + "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) @@ -27,22 +29,22 @@ const ( 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[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c2spd period.Frequency) *TPeriod { +func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c2spd period.Frequency) TPeriod { if semi == note.UnchangedSemitone { panic("how?") } var result TPeriod switch p := any(&result).(type) { - case *Linear: + case *period.Linear: nft := int(semi)*SlideFinesPerNote + int(ft) p.Finetune = note.Finetune(nft) p.CommonRate = c2spd - case *Amiga: + case *period.Amiga: key := int(semi.Key()) octave := uint32(semi.Octave()) if key >= len(semitonePeriodTable) { - return nil + return result } if c2spd == 0 { @@ -50,30 +52,40 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet } if ft != 0 { - c2spd = CalcFinetuneC2Spd[Amiga](c2spd, ft) + c2spd = CalcFinetuneC2Spd[period.Amiga](AmigaConverter, c2spd, ft) } - p.Amiga = period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)<= len(semitonePeriodTable) { - return nil + var empty period.Amiga + return empty } if c2spd == 0 { @@ -44,9 +47,7 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c2spd period.Frequ c2spd = CalcFinetuneC2Spd(c2spd, ft) } - return &Amiga{ - Amiga: period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)<= len(semitonePeriodTable) { - return nil + return result } if c2spd == 0 { @@ -51,30 +51,29 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet } if ft != 0 { - c2spd = CalcFinetuneC2Spd[Amiga](c2spd, ft) + c2spd = CalcFinetuneC2Spd[period.Amiga](AmigaConverter, c2spd, ft) } - p.Amiga = period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)< rhs: + case p > rhs: return comparison.SpaceshipRightGreater default: return comparison.SpaceshipEqual @@ -42,21 +46,9 @@ func CompareAmiga(lhs, rhs Amiga) comparison.Spaceship { } // Lerp linear-interpolates the current period with the `rhs` period -func (p Amiga) Lerp(t float64, rhs Period) Period { - right := Amiga(0) - if r, ok := rhs.(Amiga); ok { - right = r - } - - return Amiga(util.LerpFloat64(t, float64(p), float64(right))) -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (p Amiga) GetSamplerAdd(samplerSpeed float64) float64 { - if p == 0 { - return 0 - } - return float64(samplerSpeed) / float64(p) +func (p Amiga) Lerp(t float64, rhs Amiga) Amiga { + p = util.Lerp(t, p, rhs) + return p } // GetFrequency returns the frequency defined by the period @@ -67,13 +59,3 @@ func (p Amiga) GetFrequency() Frequency { func (p Amiga) String() string { return fmt.Sprintf("Amiga{ Period:%d }", p) } - -// ToLinearPeriod returns the linear frequency period for a given period -func (p Amiga) ToLinearPeriod() Period { - panic("unimplemented") // must be implemented by format -} - -// ToAmigaPeriod returns the amiga (protracker) representation for a given period -func (p Amiga) ToAmigaPeriod() Period { - panic("unimplemented") // must be implemented by format -} 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/linear.go b/period/linear.go index cf8e450..243eb29 100644 --- a/period/linear.go +++ b/period/linear.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/util" "github.com/heucuva/comparison" ) @@ -14,37 +15,35 @@ type Linear struct { } // Add adds the current period to a delta value then returns the resulting period -func (p Linear) AddDelta(delta Delta) Period { - panic("unimplemented") // must be implemented by format -} +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 + } -// Add adds the current period to a delta value then returns the resulting period -func AddLinearDelta(p Linear, delta PeriodDelta) Linear { - // 0 means "not playing", so keep it that way - if p.Finetune > 0 { - p.Finetune += note.Finetune(delta) - if p.Finetune < 1 { - p.Finetune = 1 - } + a += int(d) + if a < 1 { + a = 1 } + p.Finetune = note.Finetune(a) 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 Period) comparison.Spaceship { - panic("unimplemented") // must be implemented by format -} - -// CompareLinear compares (<=>) two linear periods -func CompareLinear(lhs, rhs Linear) comparison.Spaceship { +func (p Linear) Compare(rhs Linear) comparison.Spaceship { switch { - case lhs.Finetune < rhs.Finetune: + case p.Finetune < rhs.Finetune: return comparison.SpaceshipRightGreater - case lhs.Finetune > rhs.Finetune: + case p.Finetune > rhs.Finetune: return comparison.SpaceshipLeftGreater default: return comparison.SpaceshipEqual @@ -52,36 +51,11 @@ func CompareLinear(lhs, rhs Linear) comparison.Spaceship { } // Lerp linear-interpolates the current period with the `rhs` period -func (p Linear) Lerp(t float64, rhs Period) Period { - right, _ := rhs.ToLinearPeriod().(Linear) - - lnft := float64(p.Finetune) - rnft := float64(right.Finetune) - - delta := 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 { - panic("unimplemented") // must be implemented by format -} - -// GetFrequency returns the frequency defined by the period -func (p Linear) GetFrequency() Frequency { - panic("unimplemented") // must be implemented by format +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 { return fmt.Sprintf("LinearPeriod{ Finetune:%v CommonRate:%v }", p.Finetune, p.CommonRate) } - -// ToLinearPeriod returns the linear frequency period for a given period -func (p Linear) ToLinearPeriod() Period { - panic("unimplemented") // must be implemented by format -} - -// ToAmigaPeriod returns the amiga (protracker) representation for a given period -func (p Linear) ToAmigaPeriod() Period { - panic("unimplemented") // must be implemented by format -} diff --git a/period/period.go b/period/period.go index f39cc6f..572e0c8 100644 --- a/period/period.go +++ b/period/period.go @@ -6,42 +6,37 @@ import ( // 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 - ToLinearPeriod() Period - ToAmigaPeriod() Period + IsInvalid() bool } -// 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 +type PeriodAdder[TPeriod Period] interface { + Add(d Delta) TPeriod +} -// 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") +func AddDelta[TPeriod Period](p TPeriod, d Delta) TPeriod { + if pa, ok := any(p).(PeriodAdder[TPeriod]); ok { + return pa.Add(d) } + return p } // ComparePeriods compares two periods, taking nil into account -func ComparePeriods[TPeriod Period](lhs *TPeriod, rhs *TPeriod) comparison.Spaceship { - if lhs == nil { - if rhs == nil { +func ComparePeriods[TPeriod Period](lhs TPeriod, rhs TPeriod) comparison.Spaceship { + if lhs.IsInvalid() { + if rhs.IsInvalid() { return comparison.SpaceshipEqual } return comparison.SpaceshipRightGreater - } else if rhs == nil { + } else if rhs.IsInvalid() { return comparison.SpaceshipLeftGreater } - return (*lhs).Compare(*rhs) + switch p := any(lhs).(type) { + case Linear: + return p.Compare(any(rhs).(Linear)) + case Amiga: + return p.Compare(any(rhs).(Amiga)) + default: + panic("unhandled period type") + } } diff --git a/period/periodconverter.go b/period/periodconverter.go new file mode 100644 index 0000000..ad05c8c --- /dev/null +++ b/period/periodconverter.go @@ -0,0 +1,6 @@ +package period + +type PeriodConverter[TPeriod Period] interface { + GetSamplerAdd(TPeriod, float64) float64 + GetFrequency(TPeriod) Frequency +} diff --git a/player/state/active.go b/player/state/active.go index a30bab7..544844c 100644 --- a/player/state/active.go +++ b/player/state/active.go @@ -14,7 +14,7 @@ import ( type Active[TPeriod period.Period] struct { Playback[TPeriod] Voice voice.Voice - PeriodDelta period.PeriodDelta + PeriodDelta period.Delta } // Reset sets the active state to defaults @@ -63,20 +63,20 @@ type RenderDetails struct { } // RenderStatesTogether renders a channel's series of sample data for a the provided number of samples -func RenderStatesTogether[TPeriod period.Period](activeState *Active[TPeriod], pastNotes []*Active[TPeriod], details RenderDetails) []mixing.Data { +func RenderStatesTogether[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], activeState *Active[TPeriod], pastNotes []*Active[TPeriod], 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 { + if data := activeState.renderState(periodConverter, centerAheadPan, details); data != nil { mixData = append(mixData, *data) } } for _, pn := range pastNotes { if pn != nil { - if data := pn.renderState(centerAheadPan, details); data != nil { + if data := pn.renderState(periodConverter, centerAheadPan, details); data != nil { mixData = append(mixData, *data) } } @@ -85,8 +85,8 @@ func RenderStatesTogether[TPeriod period.Period](activeState *Active[TPeriod], p return mixData } -func (a *Active[TPeriod]) renderState(centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { - if a.Period == nil || a.Volume == 0 { +func (a *Active[TPeriod]) renderState(periodConverter period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { + if a.Period.IsInvalid() || a.Volume == 0 { return nil } @@ -96,12 +96,12 @@ func (a *Active[TPeriod]) renderState(centerAheadPan volume.Matrix, details Rend } // Commit the playback settings to the note-control - voice.SetPeriod(ncv, any(a.Period).(period.Period)) + 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) + voice.SetPeriodDelta[TPeriod](ncv, a.PeriodDelta) // the period might be updated by the auto-vibrato system, here ncv.Advance(details.Duration) @@ -117,7 +117,7 @@ func (a *Active[TPeriod]) renderState(centerAheadPan volume.Matrix, details Rend } // ... so grab the new value now. - period := voice.GetFinalPeriod(ncv) + p := voice.GetFinalPeriod[TPeriod](ncv) pan := voice.GetFinalPan(ncv) // make a stand-alone data buffer for this channel for this tick @@ -140,7 +140,7 @@ func (a *Active[TPeriod]) renderState(centerAheadPan volume.Matrix, details Rend } a.Pos = voice.GetPos(ncv) - samplerAdd := float32(period.GetSamplerAdd(float64(details.SamplerSpeed))) + samplerAdd := float32(periodConverter.GetSamplerAdd(p, float64(details.SamplerSpeed))) a.Pos.Add(samplerAdd * float32(details.Samples)) return data diff --git a/player/state/channel.go b/player/state/channel.go index 2398387..6d09b10 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -30,7 +30,7 @@ type NoteOp[TPeriod period.Period, TMemory any] interface { Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error } -type PeriodUpdateFunc[TPeriod period.Period] func(*TPeriod) +type PeriodUpdateFunc[TPeriod period.Period] func(TPeriod) type SemitoneSetterFactory[TPeriod period.Period, TMemory any] func(note.Semitone, PeriodUpdateFunc[TPeriod]) NoteOp[TPeriod, TMemory] @@ -56,7 +56,7 @@ type ChannelState[TPeriod period.Period, TMemory any] struct { freezePlayback bool Semitone note.Semitone // from TargetSemitone, modified further, used in period calculations UseTargetPeriod bool - periodOverride *TPeriod + periodOverride TPeriod UsePeriodOverride bool volumeActive bool PanEnabled bool @@ -64,6 +64,8 @@ type ChannelState[TPeriod period.Period, TMemory any] struct { PastNotes *PastNotesProcessor[TPeriod] RenderChannel *render.Channel + + PeriodConverter period.PeriodConverter[TPeriod] } // WillTriggerOn returns true if a note will trigger on the tick specified @@ -94,7 +96,7 @@ func (cs *ChannelState[TPeriod, TMemory]) RenderRowTick(details RenderDetails, p return nil, nil } - mixData := RenderStatesTogether(&cs.activeState, pastNotes, details) + mixData := RenderStatesTogether(cs.PeriodConverter, &cs.activeState, pastNotes, details) return mixData, nil } @@ -182,50 +184,51 @@ func (cs *ChannelState[TPeriod, TMemory]) GetTxn() ChannelDataTransaction[TPerio } // GetPortaTargetPeriod returns the current target portamento (to note) sampler period -func (cs *ChannelState[TPeriod, TMemory]) GetPortaTargetPeriod() *TPeriod { +func (cs *ChannelState[TPeriod, TMemory]) GetPortaTargetPeriod() TPeriod { if p, ok := cs.PortaTargetPeriod.Get(); ok { - return &p + return p } - return nil + var empty TPeriod + return empty } // SetPortaTargetPeriod sets the current target portamento (to note) sampler period -func (cs *ChannelState[TPeriod, TMemory]) SetPortaTargetPeriod(period *TPeriod) { - if period != nil { - cs.PortaTargetPeriod.Set(*period) +func (cs *ChannelState[TPeriod, TMemory]) SetPortaTargetPeriod(period TPeriod) { + if !period.IsInvalid() { + cs.PortaTargetPeriod.Set(period) } else { cs.PortaTargetPeriod.Reset() } } // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) GetTargetPeriod() *TPeriod { +func (cs *ChannelState[TPeriod, TMemory]) GetTargetPeriod() TPeriod { return cs.targetState.Period } // SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) SetTargetPeriod(period *TPeriod) { +func (cs *ChannelState[TPeriod, TMemory]) SetTargetPeriod(period TPeriod) { cs.targetState.Period = period } // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) GetPeriodOverride() *TPeriod { +func (cs *ChannelState[TPeriod, TMemory]) GetPeriodOverride() TPeriod { return cs.periodOverride } // SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) SetPeriodOverride(period *TPeriod) { +func (cs *ChannelState[TPeriod, TMemory]) SetPeriodOverride(period TPeriod) { cs.periodOverride = period cs.UsePeriodOverride = true } // SetPeriodDelta sets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TPeriod, TMemory]) SetPeriodDelta(delta period.PeriodDelta) { +func (cs *ChannelState[TPeriod, TMemory]) SetPeriodDelta(delta period.Delta) { cs.activeState.PeriodDelta = delta } // GetPeriodDelta gets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TPeriod, TMemory]) GetPeriodDelta() period.PeriodDelta { +func (cs *ChannelState[TPeriod, TMemory]) GetPeriodDelta() period.Delta { return cs.activeState.PeriodDelta } @@ -246,7 +249,7 @@ func (cs *ChannelState[TPeriod, TMemory]) SetInstrument(inst *instrument.Instrum if inst == cs.prevState.Instrument { cs.activeState.Voice = cs.prevState.Voice } else { - cs.activeState.Voice = voiceImpl.New(inst, cs.RenderChannel) + cs.activeState.Voice = voiceImpl.New[TPeriod](cs.PeriodConverter, inst, cs.RenderChannel) } } } @@ -292,12 +295,12 @@ func (cs *ChannelState[TPeriod, TMemory]) SetTargetPos(pos sampling.Pos) { } // GetPeriod returns the current sampler period of the active instrument -func (cs *ChannelState[TPeriod, TMemory]) GetPeriod() *TPeriod { +func (cs *ChannelState[TPeriod, TMemory]) GetPeriod() TPeriod { return cs.activeState.Period } // SetPeriod sets the current sampler period of the active instrument -func (cs *ChannelState[TPeriod, TMemory]) SetPeriod(period *TPeriod) { +func (cs *ChannelState[TPeriod, TMemory]) SetPeriod(period TPeriod) { cs.activeState.Period = period } @@ -399,7 +402,7 @@ func (cs *ChannelState[TPeriod, TMemory]) SetEnvelopePosition(ticks int) { if nc := cs.GetVoice(); nc != nil { voice.SetVolumeEnvelopePosition(nc, ticks) voice.SetPanEnvelopePosition(nc, ticks) - voice.SetPitchEnvelopePosition(nc, ticks) + voice.SetPitchEnvelopePosition[TPeriod](nc, ticks) voice.SetFilterEnvelopePosition(nc, ticks) } } @@ -465,9 +468,10 @@ func (cs *ChannelState[TPeriod, TMemory]) SetPanningEnvelopeEnable(enabled bool) // SetPitchEnvelopeEnable sets the enable flag on the active pitch/filter envelope func (cs *ChannelState[TPeriod, TMemory]) SetPitchEnvelopeEnable(enabled bool) { - voice.EnablePitchEnvelope(cs.activeState.Voice, enabled) + voice.EnablePitchEnvelope[TPeriod](cs.activeState.Voice, enabled) } func (cs *ChannelState[TPeriod, TMemory]) NoteCut() { - cs.activeState.Period = nil + var empty TPeriod + cs.activeState.Period = empty } diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index 26e8f10..b9ffbc1 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -89,8 +89,8 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitRow(p playback.Playback, } if period, ok := d.TargetPeriod.Get(); ok { - cs.SetTargetPeriod(&period) - cs.SetPortaTargetPeriod(&period) + cs.SetTargetPeriod(period) + cs.SetPortaTargetPeriod(period) } if st, ok := d.TargetStoredSemitone.Get(); ok { diff --git a/player/state/playback.go b/player/state/playback.go index d48d5de..865cada 100644 --- a/player/state/playback.go +++ b/player/state/playback.go @@ -12,7 +12,7 @@ import ( // Playback is the information needed to make an instrument play type Playback[TPeriod period.Period] struct { Instrument *instrument.Instrument - Period *TPeriod + Period TPeriod Volume volume.Volume Pos sampling.Pos Pan panning.Position @@ -21,7 +21,8 @@ type Playback[TPeriod period.Period] struct { // Reset sets the render state to defaults func (p *Playback[TPeriod]) Reset() { p.Instrument = nil - p.Period = nil + var empty TPeriod + p.Period = empty p.Pos = sampling.Pos{} p.Pan = panning.CenterAhead } diff --git a/player/voice/opl2.go b/player/voice/opl2.go index 434783b..91de567 100644 --- a/player/voice/opl2.go +++ b/player/voice/opl2.go @@ -15,31 +15,31 @@ import ( ) // OPL2 is an OPL2 voice interface -type OPL2 interface { +type OPL2[TPeriod period.Period] interface { voice.Voice - voice.FreqModulator + voice.FreqModulator[TPeriod] voice.AmpModulator voice.VolumeEnveloper - voice.PitchEnveloper + voice.PitchEnveloper[TPeriod] } // 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 { +type OPLConfiguration[TPeriod period.Period] struct { Chip render.OPL2Chip Channel int C2SPD period.Frequency InitialVolume volume.Volume - InitialPeriod period.Period + InitialPeriod TPeriod AutoVibrato voice.AutoVibrato Data instrument.Data } // == the actual opl2 voice == -type opl2Voice struct { +type opl2Voice[TPeriod period.Period] struct { c2spd period.Frequency initialVolume volume.Volume @@ -49,16 +49,16 @@ type opl2Voice struct { fadeoutMode fadeout.Mode - o component.OPL2 + o component.OPL2[TPeriod] amp component.AmpModulator - freq component.FreqModulator + freq component.FreqModulator[TPeriod] volEnv component.VolumeEnvelope - pitchEnv component.PitchEnvelope + pitchEnv component.PitchEnvelope[TPeriod] } // NewOPL2 creates a new OPL2 voice -func NewOPL2(config OPLConfiguration) voice.Voice { - v := opl2Voice{ +func NewOPL2[TPeriod period.Period](config OPLConfiguration[TPeriod]) voice.Voice { + v := opl2Voice[TPeriod]{ c2spd: config.C2SPD, initialVolume: config.InitialVolume, fadeoutMode: fadeout.ModeDisabled, @@ -99,13 +99,12 @@ func NewOPL2(config OPLConfiguration) voice.Voice { v.freq.ResetAutoVibrato(config.AutoVibrato.Sweep) } - var o OPL2 = &v - return o + return &v } // == Controller == -func (v *opl2Voice) Attack() { +func (v *opl2Voice[TPeriod]) Attack() { v.keyOn = true v.amp.Attack() v.freq.ResetAutoVibrato() @@ -114,13 +113,13 @@ func (v *opl2Voice) Attack() { } -func (v *opl2Voice) Release() { +func (v *opl2Voice[TPeriod]) Release() { v.keyOn = false v.amp.Release() v.o.Release() } -func (v *opl2Voice) Fadeout() { +func (v *opl2Voice[TPeriod]) Fadeout() { switch v.fadeoutMode { case fadeout.ModeAlwaysActive: v.amp.Fadeout() @@ -131,15 +130,15 @@ func (v *opl2Voice) Fadeout() { } } -func (v *opl2Voice) IsKeyOn() bool { +func (v *opl2Voice[TPeriod]) IsKeyOn() bool { return v.keyOn } -func (v *opl2Voice) IsFadeout() bool { +func (v *opl2Voice[TPeriod]) IsFadeout() bool { return v.amp.IsFadeoutEnabled() } -func (v *opl2Voice) IsDone() bool { +func (v *opl2Voice[TPeriod]) IsDone() bool { if !v.amp.IsFadeoutEnabled() { return false } @@ -148,44 +147,45 @@ func (v *opl2Voice) IsDone() bool { // == FreqModulator == -func (v *opl2Voice) SetPeriod(period period.Period) { +func (v *opl2Voice[TPeriod]) SetPeriod(period TPeriod) { v.freq.SetPeriod(period) } -func (v *opl2Voice) GetPeriod() period.Period { +func (v *opl2Voice[TPeriod]) GetPeriod() TPeriod { return v.freq.GetPeriod() } -func (v *opl2Voice) SetPeriodDelta(delta period.Delta) { +func (v *opl2Voice[TPeriod]) SetPeriodDelta(delta period.Delta) { v.freq.SetDelta(delta) } -func (v *opl2Voice) GetPeriodDelta() period.Delta { +func (v *opl2Voice[TPeriod]) GetPeriodDelta() period.Delta { return v.freq.GetDelta() } -func (v *opl2Voice) GetFinalPeriod() period.Period { +func (v *opl2Voice[TPeriod]) GetFinalPeriod() TPeriod { p := v.freq.GetFinalPeriod() if v.IsPitchEnvelopeEnabled() { - p = p.AddDelta(v.GetCurrentPitchEnvelope()) + d := v.GetCurrentPitchEnvelope() + p = period.AddDelta(p, d) } return p } // == AmpModulator == -func (v *opl2Voice) SetVolume(vol volume.Volume) { +func (v *opl2Voice[TPeriod]) SetVolume(vol volume.Volume) { if vol == volume.VolumeUseInstVol { vol = v.initialVolume } v.amp.SetVolume(vol) } -func (v *opl2Voice) GetVolume() volume.Volume { +func (v *opl2Voice[TPeriod]) GetVolume() volume.Volume { return v.amp.GetVolume() } -func (v *opl2Voice) GetFinalVolume() volume.Volume { +func (v *opl2Voice[TPeriod]) GetFinalVolume() volume.Volume { vol := v.amp.GetFinalVolume() if v.IsVolumeEnvelopeEnabled() { vol *= v.GetCurrentVolumeEnvelope() @@ -195,22 +195,22 @@ func (v *opl2Voice) GetFinalVolume() volume.Volume { // == VolumeEnveloper == -func (v *opl2Voice) EnableVolumeEnvelope(enabled bool) { +func (v *opl2Voice[TPeriod]) EnableVolumeEnvelope(enabled bool) { v.volEnv.SetEnabled(enabled) } -func (v *opl2Voice) IsVolumeEnvelopeEnabled() bool { +func (v *opl2Voice[TPeriod]) IsVolumeEnvelopeEnabled() bool { return v.volEnv.IsEnabled() } -func (v *opl2Voice) GetCurrentVolumeEnvelope() volume.Volume { +func (v *opl2Voice[TPeriod]) GetCurrentVolumeEnvelope() volume.Volume { if v.volEnv.IsEnabled() { return v.volEnv.GetCurrentValue() } return 1 } -func (v *opl2Voice) SetVolumeEnvelopePosition(pos int) { +func (v *opl2Voice[TPeriod]) SetVolumeEnvelopePosition(pos int) { if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { doneCB(v) } @@ -218,22 +218,23 @@ func (v *opl2Voice) SetVolumeEnvelopePosition(pos int) { // == PitchEnveloper == -func (v *opl2Voice) EnablePitchEnvelope(enabled bool) { +func (v *opl2Voice[TPeriod]) EnablePitchEnvelope(enabled bool) { v.pitchEnv.SetEnabled(enabled) } -func (v *opl2Voice) IsPitchEnvelopeEnabled() bool { +func (v *opl2Voice[TPeriod]) IsPitchEnvelopeEnabled() bool { return v.pitchEnv.IsEnabled() } -func (v *opl2Voice) GetCurrentPitchEnvelope() period.Delta { +func (v *opl2Voice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { if v.pitchEnv.IsEnabled() { return v.pitchEnv.GetCurrentValue() } - return 0 + var empty period.Delta + return empty } -func (v *opl2Voice) SetPitchEnvelopePosition(pos int) { +func (v *opl2Voice[TPeriod]) SetPitchEnvelopePosition(pos int) { if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { doneCB(v) } @@ -241,7 +242,7 @@ func (v *opl2Voice) SetPitchEnvelopePosition(pos int) { // == required function interfaces == -func (v *opl2Voice) Advance(tickDuration time.Duration) { +func (v *opl2Voice[TPeriod]) Advance(tickDuration time.Duration) { defer func() { v.prevKeyOn = v.keyOn }() @@ -270,30 +271,30 @@ func (v *opl2Voice) Advance(tickDuration time.Duration) { v.o.Advance(v.GetFinalVolume(), v.GetFinalPeriod()) } -func (v *opl2Voice) GetSample(pos sampling.Pos) volume.Matrix { +func (v *opl2Voice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { return volume.Matrix{} } -func (v *opl2Voice) GetSampler(samplerRate float32) sampling.Sampler { +func (v *opl2Voice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { return nil } -func (v *opl2Voice) Clone() voice.Voice { +func (v *opl2Voice[TPeriod]) Clone() voice.Voice { o := *v return &o } -func (v *opl2Voice) StartTransaction() voice.Transaction { - t := txn{ +func (v *opl2Voice[TPeriod]) StartTransaction() voice.Transaction[TPeriod] { + t := txn[TPeriod]{ Voice: v, } return &t } -func (v *opl2Voice) SetActive(active bool) { +func (v *opl2Voice[TPeriod]) SetActive(active bool) { v.active = active } -func (v *opl2Voice) IsActive() bool { +func (v *opl2Voice[TPeriod]) IsActive() bool { return v.active } diff --git a/player/voice/pcm.go b/player/voice/pcm.go index 9aa290f..5742523 100644 --- a/player/voice/pcm.go +++ b/player/voice/pcm.go @@ -17,23 +17,23 @@ import ( ) // PCM is a PCM voice interface -type PCM interface { +type PCM[TPeriod period.Period] interface { voice.Voice voice.Positioner - voice.FreqModulator + voice.FreqModulator[TPeriod] voice.AmpModulator voice.PanModulator voice.VolumeEnveloper - voice.PitchEnveloper + voice.PitchEnveloper[TPeriod] voice.PanEnveloper voice.FilterEnveloper } // PCMConfiguration is the information needed to configure an PCM2 voice -type PCMConfiguration struct { +type PCMConfiguration[TPeriod period.Period] struct { C2SPD period.Frequency InitialVolume volume.Volume - InitialPeriod period.Period + InitialPeriod TPeriod AutoVibrato voice.AutoVibrato Data instrument.Data OutputFilter voice.FilterApplier @@ -43,7 +43,7 @@ type PCMConfiguration struct { // == the actual pcm voice == -type pcmVoice struct { +type pcmVoice[TPeriod period.Period] struct { c2spd period.Frequency initialVolume volume.Volume outputFilter voice.FilterApplier @@ -61,25 +61,28 @@ type pcmVoice struct { sampler component.Sampler amp component.AmpModulator - freq component.FreqModulator + freq component.FreqModulator[TPeriod] pan component.PanModulator volEnv component.VolumeEnvelope - pitchEnv component.PitchEnvelope + pitchEnv component.PitchEnvelope[TPeriod] panEnv component.PanEnvelope filterEnv component.FilterEnvelope vol0ticks int done bool + + periodConverter period.PeriodConverter[TPeriod] } // 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, +func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], config PCMConfiguration[TPeriod]) voice.Voice { + v := pcmVoice[TPeriod]{ + c2spd: config.C2SPD, + initialVolume: config.InitialVolume, + outputFilter: config.OutputFilter, + voiceFilter: config.VoiceFilter, + pluginFilter: config.PluginFilter, + active: true, + periodConverter: periodConverter, } switch d := config.Data.(type) { @@ -110,13 +113,12 @@ func NewPCM(config PCMConfiguration) voice.Voice { v.freq.ResetAutoVibrato(config.AutoVibrato.Sweep) } - var o PCM = &v - return o + return &v } // == Controller == -func (v *pcmVoice) Attack() { +func (v *pcmVoice[TPeriod]) Attack() { v.keyOn = true v.vol0ticks = 0 v.done = false @@ -129,13 +131,13 @@ func (v *pcmVoice) Attack() { v.SetFilterEnvelopePosition(0) } -func (v *pcmVoice) Release() { +func (v *pcmVoice[TPeriod]) Release() { v.keyOn = false v.amp.Release() v.sampler.Release() } -func (v *pcmVoice) Fadeout() { +func (v *pcmVoice[TPeriod]) Fadeout() { switch v.fadeoutMode { case fadeout.ModeAlwaysActive: v.amp.Fadeout() @@ -148,15 +150,15 @@ func (v *pcmVoice) Fadeout() { v.sampler.Fadeout() } -func (v *pcmVoice) IsKeyOn() bool { +func (v *pcmVoice[TPeriod]) IsKeyOn() bool { return v.keyOn } -func (v *pcmVoice) IsFadeout() bool { +func (v *pcmVoice[TPeriod]) IsFadeout() bool { return v.amp.IsFadeoutEnabled() } -func (v *pcmVoice) IsDone() bool { +func (v *pcmVoice[TPeriod]) IsDone() bool { if v.done { return true } @@ -170,7 +172,7 @@ func (v *pcmVoice) IsDone() bool { // == SampleStream == -func (v *pcmVoice) GetSample(pos sampling.Pos) volume.Matrix { +func (v *pcmVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { samp := v.sampler.GetSample(pos) if samp.Channels == 0 { v.done = true @@ -189,55 +191,55 @@ func (v *pcmVoice) GetSample(pos sampling.Pos) volume.Matrix { // == Positioner == -func (v *pcmVoice) SetPos(pos sampling.Pos) { +func (v *pcmVoice[TPeriod]) SetPos(pos sampling.Pos) { v.sampler.SetPos(pos) } -func (v *pcmVoice) GetPos() sampling.Pos { +func (v *pcmVoice[TPeriod]) GetPos() sampling.Pos { return v.sampler.GetPos() } // == FreqModulator == -func (v *pcmVoice) SetPeriod(period period.Period) { +func (v *pcmVoice[TPeriod]) SetPeriod(period TPeriod) { v.freq.SetPeriod(period) } -func (v *pcmVoice) GetPeriod() period.Period { +func (v *pcmVoice[TPeriod]) GetPeriod() TPeriod { return v.freq.GetPeriod() } -func (v *pcmVoice) SetPeriodDelta(delta period.Delta) { +func (v *pcmVoice[TPeriod]) SetPeriodDelta(delta period.Delta) { v.freq.SetDelta(delta) } -func (v *pcmVoice) GetPeriodDelta() period.Delta { +func (v *pcmVoice[TPeriod]) GetPeriodDelta() period.Delta { return v.freq.GetDelta() } -func (v *pcmVoice) GetFinalPeriod() period.Period { +func (v *pcmVoice[TPeriod]) GetFinalPeriod() TPeriod { p := v.freq.GetFinalPeriod() if v.IsPitchEnvelopeEnabled() { delta := v.GetCurrentPitchEnvelope() - p = p.AddDelta(delta) + p = period.AddDelta(p, delta) } return p } // == AmpModulator == -func (v *pcmVoice) SetVolume(vol volume.Volume) { +func (v *pcmVoice[TPeriod]) SetVolume(vol volume.Volume) { if vol == volume.VolumeUseInstVol { vol = v.initialVolume } v.amp.SetVolume(vol) } -func (v *pcmVoice) GetVolume() volume.Volume { +func (v *pcmVoice[TPeriod]) GetVolume() volume.Volume { return v.amp.GetVolume() } -func (v *pcmVoice) GetFinalVolume() volume.Volume { +func (v *pcmVoice[TPeriod]) GetFinalVolume() volume.Volume { vol := v.amp.GetFinalVolume() if v.IsVolumeEnvelopeEnabled() { vol *= v.GetCurrentVolumeEnvelope() @@ -247,15 +249,15 @@ func (v *pcmVoice) GetFinalVolume() volume.Volume { // == PanModulator == -func (v *pcmVoice) SetPan(pan panning.Position) { +func (v *pcmVoice[TPeriod]) SetPan(pan panning.Position) { v.pan.SetPan(pan) } -func (v *pcmVoice) GetPan() panning.Position { +func (v *pcmVoice[TPeriod]) GetPan() panning.Position { return v.pan.GetPan() } -func (v *pcmVoice) GetFinalPan() panning.Position { +func (v *pcmVoice[TPeriod]) GetFinalPan() panning.Position { p := v.pan.GetFinalPan() if v.IsPanEnvelopeEnabled() { p = pan.CalculateCombinedPanning(p, v.panEnv.GetCurrentValue()) @@ -265,22 +267,22 @@ func (v *pcmVoice) GetFinalPan() panning.Position { // == VolumeEnveloper == -func (v *pcmVoice) EnableVolumeEnvelope(enabled bool) { +func (v *pcmVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) { v.volEnv.SetEnabled(enabled) } -func (v *pcmVoice) IsVolumeEnvelopeEnabled() bool { +func (v *pcmVoice[TPeriod]) IsVolumeEnvelopeEnabled() bool { return v.volEnv.IsEnabled() } -func (v *pcmVoice) GetCurrentVolumeEnvelope() volume.Volume { +func (v *pcmVoice[TPeriod]) GetCurrentVolumeEnvelope() volume.Volume { if v.volEnv.IsEnabled() { return v.volEnv.GetCurrentValue() } return 1 } -func (v *pcmVoice) SetVolumeEnvelopePosition(pos int) { +func (v *pcmVoice[TPeriod]) SetVolumeEnvelopePosition(pos int) { if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { doneCB(v) } @@ -288,25 +290,26 @@ func (v *pcmVoice) SetVolumeEnvelopePosition(pos int) { // == PitchEnveloper == -func (v *pcmVoice) EnablePitchEnvelope(enabled bool) { +func (v *pcmVoice[TPeriod]) EnablePitchEnvelope(enabled bool) { v.pitchEnv.SetEnabled(enabled) } -func (v *pcmVoice) IsPitchEnvelopeEnabled() bool { +func (v *pcmVoice[TPeriod]) IsPitchEnvelopeEnabled() bool { if v.pitchAndFilterEnvShared && v.filterEnvActive { return false } return v.pitchEnv.IsEnabled() } -func (v *pcmVoice) GetCurrentPitchEnvelope() period.Delta { +func (v *pcmVoice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { if v.pitchEnv.IsEnabled() { return v.pitchEnv.GetCurrentValue() } - return 0 + var empty period.Delta + return empty } -func (v *pcmVoice) SetPitchEnvelopePosition(pos int) { +func (v *pcmVoice[TPeriod]) SetPitchEnvelopePosition(pos int) { if !v.pitchAndFilterEnvShared || !v.filterEnvActive { if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { doneCB(v) @@ -316,7 +319,7 @@ func (v *pcmVoice) SetPitchEnvelopePosition(pos int) { // == FilterEnveloper == -func (v *pcmVoice) EnableFilterEnvelope(enabled bool) { +func (v *pcmVoice[TPeriod]) EnableFilterEnvelope(enabled bool) { if !v.pitchAndFilterEnvShared { v.filterEnv.SetEnabled(enabled) return @@ -330,18 +333,18 @@ func (v *pcmVoice) EnableFilterEnvelope(enabled bool) { v.filterEnv.SetEnabled(enabled) } -func (v *pcmVoice) IsFilterEnvelopeEnabled() bool { +func (v *pcmVoice[TPeriod]) IsFilterEnvelopeEnabled() bool { if v.pitchAndFilterEnvShared && !v.filterEnvActive { return false } return v.filterEnv.IsEnabled() } -func (v *pcmVoice) GetCurrentFilterEnvelope() int8 { +func (v *pcmVoice[TPeriod]) GetCurrentFilterEnvelope() int8 { return v.filterEnv.GetCurrentValue() } -func (v *pcmVoice) SetFilterEnvelopePosition(pos int) { +func (v *pcmVoice[TPeriod]) SetFilterEnvelopePosition(pos int) { if !v.pitchAndFilterEnvShared || v.filterEnvActive { if doneCB := v.filterEnv.SetEnvelopePosition(pos); doneCB != nil { doneCB(v) @@ -351,19 +354,19 @@ func (v *pcmVoice) SetFilterEnvelopePosition(pos int) { // == PanEnveloper == -func (v *pcmVoice) EnablePanEnvelope(enabled bool) { +func (v *pcmVoice[TPeriod]) EnablePanEnvelope(enabled bool) { v.panEnv.SetEnabled(enabled) } -func (v *pcmVoice) IsPanEnvelopeEnabled() bool { +func (v *pcmVoice[TPeriod]) IsPanEnvelopeEnabled() bool { return v.panEnv.IsEnabled() } -func (v *pcmVoice) GetCurrentPanEnvelope() panning.Position { +func (v *pcmVoice[TPeriod]) GetCurrentPanEnvelope() panning.Position { return v.panEnv.GetCurrentValue() } -func (v *pcmVoice) SetPanEnvelopePosition(pos int) { +func (v *pcmVoice[TPeriod]) SetPanEnvelopePosition(pos int) { if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { doneCB(v) } @@ -371,7 +374,7 @@ func (v *pcmVoice) SetPanEnvelopePosition(pos int) { // == required function interfaces == -func (v *pcmVoice) Advance(tickDuration time.Duration) { +func (v *pcmVoice[TPeriod]) Advance(tickDuration time.Duration) { defer func() { v.prevKeyOn = v.keyOn }() @@ -411,9 +414,9 @@ func (v *pcmVoice) Advance(tickDuration time.Duration) { } } -func (v *pcmVoice) GetSampler(samplerRate float32) sampling.Sampler { - period := v.GetFinalPeriod() - samplerAdd := float32(period.GetSamplerAdd(float64(samplerRate))) +func (v *pcmVoice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { + p := v.GetFinalPeriod() + samplerAdd := float32(v.periodConverter.GetSamplerAdd(p, float64(samplerRate))) o := component.OutputFilter{ Input: v, Output: v.outputFilter, @@ -421,7 +424,7 @@ func (v *pcmVoice) GetSampler(samplerRate float32) sampling.Sampler { return sampling.NewSampler(&o, v.GetPos(), samplerAdd) } -func (v *pcmVoice) Clone() voice.Voice { +func (v *pcmVoice[TPeriod]) Clone() voice.Voice { p := *v if p.voiceFilter != nil { p.voiceFilter = p.voiceFilter.Clone() @@ -432,17 +435,17 @@ func (v *pcmVoice) Clone() voice.Voice { return &p } -func (v *pcmVoice) StartTransaction() voice.Transaction { - t := txn{ +func (v *pcmVoice[TPeriod]) StartTransaction() voice.Transaction[TPeriod] { + t := txn[TPeriod]{ Voice: v, } return &t } -func (v *pcmVoice) SetActive(active bool) { +func (v *pcmVoice[TPeriod]) SetActive(active bool) { v.active = active } -func (v *pcmVoice) IsActive() bool { +func (v *pcmVoice[TPeriod]) IsActive() bool { return v.active } diff --git a/player/voice/transaction.go b/player/voice/transaction.go index 154b9df..197cf13 100644 --- a/player/voice/transaction.go +++ b/player/voice/transaction.go @@ -22,14 +22,14 @@ const ( playingModeRelease ) -type txn struct { +type txn[TPeriod period.Period] struct { cancelled bool Voice voice.Voice active optional.Value[bool] playing optional.Value[playingMode] fadeout optional.Value[struct{}] - period optional.Value[period.Period] + period optional.Value[TPeriod] periodDelta optional.Value[period.Delta] vol optional.Value[volume.Volume] pos optional.Value[sampling.Pos] @@ -40,148 +40,148 @@ type txn struct { filterEnv envSettings } -func (t *txn) SetActive(active bool) { +func (t *txn[TPeriod]) SetActive(active bool) { t.active.Set(active) } -func (t *txn) IsPendingActive() (bool, bool) { +func (t *txn[TPeriod]) IsPendingActive() (bool, bool) { return t.active.Get() } -func (t *txn) IsCurrentlyActive() bool { +func (t *txn[TPeriod]) IsCurrentlyActive() bool { return t.Voice.IsActive() } // Attack sets the playing mode to Attack -func (t *txn) Attack() { +func (t *txn[TPeriod]) Attack() { t.playing.Set(playingModeAttack) } // Release sets the playing mode to Release -func (t *txn) Release() { +func (t *txn[TPeriod]) Release() { t.playing.Set(playingModeRelease) } // Fadeout activates the voice's fade-out function -func (t *txn) Fadeout() { +func (t *txn[TPeriod]) Fadeout() { t.fadeout.Set(struct{}{}) } // SetPeriod sets the period -func (t *txn) SetPeriod(period period.Period) { +func (t *txn[TPeriod]) SetPeriod(period TPeriod) { t.period.Set(period) } -func (t *txn) GetPendingPeriod() (period.Period, bool) { +func (t *txn[TPeriod]) GetPendingPeriod() (TPeriod, bool) { return t.period.Get() } -func (t *txn) GetCurrentPeriod() period.Period { - return voice.GetPeriod(t.Voice) +func (t *txn[TPeriod]) GetCurrentPeriod() TPeriod { + return voice.GetPeriod[TPeriod](t.Voice) } // SetPeriodDelta sets the period delta -func (t *txn) SetPeriodDelta(delta period.Delta) { +func (t *txn[TPeriod]) SetPeriodDelta(delta period.Delta) { t.periodDelta.Set(delta) } -func (t *txn) GetPendingPeriodDelta() (period.Delta, bool) { +func (t *txn[TPeriod]) GetPendingPeriodDelta() (period.Delta, bool) { return t.periodDelta.Get() } -func (t *txn) GetCurrentPeriodDelta() period.Delta { - return voice.GetPeriodDelta(t.Voice) +func (t *txn[TPeriod]) GetCurrentPeriodDelta() period.Delta { + return voice.GetPeriodDelta[TPeriod](t.Voice) } // SetVolume sets the volume -func (t *txn) SetVolume(vol volume.Volume) { +func (t *txn[TPeriod]) SetVolume(vol volume.Volume) { t.vol.Set(vol) } -func (t *txn) GetPendingVolume() (volume.Volume, bool) { +func (t *txn[TPeriod]) GetPendingVolume() (volume.Volume, bool) { return t.vol.Get() } -func (t *txn) GetCurrentVolume() volume.Volume { +func (t *txn[TPeriod]) GetCurrentVolume() volume.Volume { return voice.GetVolume(t.Voice) } // SetPos sets the position -func (t *txn) SetPos(pos sampling.Pos) { +func (t *txn[TPeriod]) SetPos(pos sampling.Pos) { t.pos.Set(pos) } -func (t *txn) GetPendingPos() (sampling.Pos, bool) { +func (t *txn[TPeriod]) GetPendingPos() (sampling.Pos, bool) { return t.pos.Get() } -func (t *txn) GetCurrentPos() sampling.Pos { +func (t *txn[TPeriod]) GetCurrentPos() sampling.Pos { return voice.GetPos(t.Voice) } // SetPan sets the panning position -func (t *txn) SetPan(pan panning.Position) { +func (t *txn[TPeriod]) SetPan(pan panning.Position) { t.pan.Set(pan) } -func (t *txn) GetPendingPan() (panning.Position, bool) { +func (t *txn[TPeriod]) GetPendingPan() (panning.Position, bool) { return t.pan.Get() } -func (t *txn) GetCurrentPan() panning.Position { +func (t *txn[TPeriod]) GetCurrentPan() panning.Position { return voice.GetPan(t.Voice) } // SetVolumeEnvelopePosition sets the volume envelope position -func (t *txn) SetVolumeEnvelopePosition(pos int) { +func (t *txn[TPeriod]) SetVolumeEnvelopePosition(pos int) { t.volEnv.pos.Set(pos) } // EnableVolumeEnvelope sets the volume envelope enable flag -func (t *txn) EnableVolumeEnvelope(enabled bool) { +func (t *txn[TPeriod]) EnableVolumeEnvelope(enabled bool) { t.volEnv.enabled.Set(enabled) } -func (t *txn) IsPendingVolumeEnvelopeEnabled() (bool, bool) { +func (t *txn[TPeriod]) IsPendingVolumeEnvelopeEnabled() (bool, bool) { return t.volEnv.enabled.Get() } -func (t *txn) IsCurrentVolumeEnvelopeEnabled() bool { +func (t *txn[TPeriod]) IsCurrentVolumeEnvelopeEnabled() bool { return voice.IsVolumeEnvelopeEnabled(t.Voice) } // SetPitchEnvelopePosition sets the pitch envelope position -func (t *txn) SetPitchEnvelopePosition(pos int) { +func (t *txn[TPeriod]) SetPitchEnvelopePosition(pos int) { t.pitchEnv.pos.Set(pos) } // EnablePitchEnvelope sets the pitch envelope enable flag -func (t *txn) EnablePitchEnvelope(enabled bool) { +func (t *txn[TPeriod]) EnablePitchEnvelope(enabled bool) { t.pitchEnv.enabled.Set(enabled) } // SetPanEnvelopePosition sets the panning envelope position -func (t *txn) SetPanEnvelopePosition(pos int) { +func (t *txn[TPeriod]) SetPanEnvelopePosition(pos int) { t.panEnv.pos.Set(pos) } // EnablePanEnvelope sets the pan envelope enable flag -func (t *txn) EnablePanEnvelope(enabled bool) { +func (t *txn[TPeriod]) EnablePanEnvelope(enabled bool) { t.panEnv.enabled.Set(enabled) } // SetFilterEnvelopePosition sets the pitch envelope position -func (t *txn) SetFilterEnvelopePosition(pos int) { +func (t *txn[TPeriod]) SetFilterEnvelopePosition(pos int) { t.filterEnv.pos.Set(pos) } // EnableFilterEnvelope sets the filter envelope enable flag -func (t *txn) EnableFilterEnvelope(enabled bool) { +func (t *txn[TPeriod]) EnableFilterEnvelope(enabled bool) { t.filterEnv.enabled.Set(enabled) } // SetAllEnvelopePositions sets all the envelope positions to the same value -func (t *txn) SetAllEnvelopePositions(pos int) { +func (t *txn[TPeriod]) SetAllEnvelopePositions(pos int) { t.volEnv.pos.Set(pos) t.pitchEnv.pos.Set(pos) t.panEnv.pos.Set(pos) @@ -191,12 +191,12 @@ func (t *txn) SetAllEnvelopePositions(pos int) { // ====== // Cancel cancels a pending transaction -func (t *txn) Cancel() { +func (t *txn[TPeriod]) Cancel() { t.cancelled = true } // Commit commits the transaction by applying pending updates -func (t *txn) Commit() { +func (t *txn[TPeriod]) Commit() { if t.cancelled { return } @@ -215,7 +215,7 @@ func (t *txn) Commit() { } if delta, ok := t.periodDelta.Get(); ok { - voice.SetPeriodDelta(t.Voice, delta) + voice.SetPeriodDelta[TPeriod](t.Voice, delta) } if vol, ok := t.vol.Get(); ok { @@ -239,11 +239,11 @@ func (t *txn) Commit() { } if pos, ok := t.pitchEnv.pos.Get(); ok { - voice.SetPitchEnvelopePosition(t.Voice, pos) + voice.SetPitchEnvelopePosition[TPeriod](t.Voice, pos) } if enabled, ok := t.pitchEnv.enabled.Get(); ok { - voice.EnablePitchEnvelope(t.Voice, enabled) + voice.EnablePitchEnvelope[TPeriod](t.Voice, enabled) } if pos, ok := t.panEnv.pos.Get(); ok { @@ -276,11 +276,11 @@ func (t *txn) Commit() { } } -func (t *txn) GetVoice() voice.Voice { +func (t *txn[TPeriod]) GetVoice() voice.Voice { return t.Voice } -func (t *txn) Clone() voice.Transaction { +func (t *txn[TPeriod]) Clone() voice.Transaction[TPeriod] { c := *t return &c } diff --git a/player/voice/voice.go b/player/voice/voice.go index 5f72d52..a7aa749 100644 --- a/player/voice/voice.go +++ b/player/voice/voice.go @@ -1,6 +1,7 @@ package voice import ( + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/filter" @@ -9,7 +10,7 @@ import ( ) // New returns a new Voice from the instrument and output channel provided -func New(inst *instrument.Instrument, output *render.Channel) voice.Voice { +func New[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], inst *instrument.Instrument, output *render.Channel) voice.Voice { switch data := inst.GetData().(type) { case *instrument.PCM: var ( @@ -22,7 +23,7 @@ func New(inst *instrument.Instrument, output *render.Channel) voice.Voice { if factory := inst.GetPluginFilterFactory(); factory != nil { pluginFilter = factory(inst.C2Spd, output.GetSampleRate()) } - return NewPCM(PCMConfiguration{ + return NewPCM[TPeriod](periodConverter, PCMConfiguration[TPeriod]{ C2SPD: inst.GetC2Spd(), InitialVolume: inst.GetDefaultVolume(), AutoVibrato: inst.GetAutoVibrato(), @@ -32,7 +33,7 @@ func New(inst *instrument.Instrument, output *render.Channel) voice.Voice { PluginFilter: pluginFilter, }) case *instrument.OPL2: - return NewOPL2(OPLConfiguration{ + return NewOPL2[TPeriod](OPLConfiguration[TPeriod]{ Chip: output.GetOPL2Chip(), Channel: output.ChannelNum, C2SPD: inst.GetC2Spd(), diff --git a/util/lerp.go b/util/lerp.go index 2b62893..2219a95 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-a)) } diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index e229fb3..c0f1f58 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -2,12 +2,13 @@ package component import ( "github.com/gotracker/playback/period" + "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/envelope" ) // PitchEnvelope is an frequency modulation envelope -type PitchEnvelope struct { +type PitchEnvelope[TPeriod period.Period] struct { enabled bool state envelope.State[int8] delta period.Delta @@ -16,7 +17,7 @@ type PitchEnvelope struct { } // Reset resets the state to defaults based on the envelope provided -func (e *PitchEnvelope) Reset(env *envelope.Envelope[int8]) { +func (e *PitchEnvelope[TPeriod]) Reset(env *envelope.Envelope[int8]) { e.state.Reset(env) e.keyOn = false e.prevKeyOn = false @@ -24,22 +25,22 @@ func (e *PitchEnvelope) Reset(env *envelope.Envelope[int8]) { } // SetEnabled sets the enabled flag for the envelope -func (e *PitchEnvelope) SetEnabled(enabled bool) { +func (e *PitchEnvelope[TPeriod]) SetEnabled(enabled bool) { e.enabled = enabled } // IsEnabled returns the enabled flag for the envelope -func (e *PitchEnvelope) IsEnabled() bool { +func (e *PitchEnvelope[TPeriod]) IsEnabled() bool { return e.enabled } // GetCurrentValue returns the current cached envelope value -func (e *PitchEnvelope) GetCurrentValue() period.Delta { +func (e *PitchEnvelope[TPeriod]) GetCurrentValue() period.Delta { return e.delta } // SetEnvelopePosition sets the current position in the envelope -func (e *PitchEnvelope) SetEnvelopePosition(pos int) voice.Callback { +func (e *PitchEnvelope[TPeriod]) SetEnvelopePosition(pos int) voice.Callback { keyOn := e.keyOn prevKeyOn := e.prevKeyOn env := e.state.Envelope() @@ -54,7 +55,7 @@ func (e *PitchEnvelope) SetEnvelopePosition(pos int) voice.Callback { } // Advance advances the envelope state 1 tick and calculates the current envelope value -func (e *PitchEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { +func (e *PitchEnvelope[TPeriod]) Advance(keyOn bool, prevKeyOn bool) voice.Callback { e.keyOn = keyOn e.prevKeyOn = prevKeyOn var doneCB voice.Callback @@ -65,19 +66,19 @@ func (e *PitchEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { return doneCB } -func (e *PitchEnvelope) update() { +func (e *PitchEnvelope[TPeriod]) update() { cur, next, t := e.state.GetCurrentValue(e.keyOn) - var y0 float32 + var y0 period.Delta if cur != nil { - y0 = float32(cur.Value()) + y0 = period.Delta(cur.Value()) } - var y1 float32 + var y1 period.Delta if next != nil { - y1 = float32(next.Value()) + y1 = period.Delta(next.Value()) } - val := y0 + t*(y1-y0) - e.delta = period.Delta(-val) + d := -util.Lerp(float64(t), y0, y1) + e.delta = d } diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index e26c5bb..d9e5413 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -7,8 +7,8 @@ import ( ) // FreqModulator is a frequency (pitch) modulator -type FreqModulator struct { - period period.Period +type FreqModulator[TPeriod period.Period] struct { + period TPeriod delta period.Delta autoVibratoEnabled bool autoVibrato oscillator.Oscillator @@ -19,39 +19,39 @@ type FreqModulator struct { } // SetPeriod sets the current period (before AutoVibrato and Delta calculation) -func (a *FreqModulator) SetPeriod(period period.Period) { +func (a *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { a.period = period } // GetPeriod returns the current period (before AutoVibrato and Delta calculation) -func (a *FreqModulator) GetPeriod() period.Period { +func (a *FreqModulator[TPeriod]) GetPeriod() TPeriod { return a.period } // SetDelta sets the current period delta (before AutoVibrato calculation) -func (a *FreqModulator) SetDelta(delta period.Delta) { +func (a *FreqModulator[TPeriod]) SetDelta(delta period.Delta) { a.delta = delta } // GetDelta returns the current period delta (before AutoVibrato calculation) -func (a *FreqModulator) GetDelta() period.Delta { +func (a *FreqModulator[TPeriod]) GetDelta() period.Delta { return a.delta } // SetAutoVibratoEnabled sets the status of the AutoVibrato enablement flag -func (a *FreqModulator) SetAutoVibratoEnabled(enabled bool) { +func (a *FreqModulator[TPeriod]) SetAutoVibratoEnabled(enabled bool) { a.autoVibratoEnabled = enabled } // ConfigureAutoVibrato sets the AutoVibrato oscillator settings -func (a *FreqModulator) ConfigureAutoVibrato(av voice.AutoVibrato) { +func (a *FreqModulator[TPeriod]) ConfigureAutoVibrato(av voice.AutoVibrato) { a.autoVibrato = av.Generate() a.autoVibratoRate = int(av.Rate) a.autoVibratoDepth = av.Depth } // ResetAutoVibrato resets the current AutoVibrato -func (a *FreqModulator) ResetAutoVibrato(sweep ...int) { +func (a *FreqModulator[TPeriod]) ResetAutoVibrato(sweep ...int) { if a.autoVibrato != nil { a.autoVibrato.Reset(true) } @@ -64,27 +64,28 @@ func (a *FreqModulator) ResetAutoVibrato(sweep ...int) { } // IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag -func (a *FreqModulator) IsAutoVibratoEnabled() bool { +func (a *FreqModulator[TPeriod]) IsAutoVibratoEnabled() bool { return a.autoVibratoEnabled } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) -func (a *FreqModulator) GetFinalPeriod() period.Period { - p := a.period.AddDelta(a.delta) +func (a *FreqModulator[TPeriod]) GetFinalPeriod() TPeriod { + p := period.AddDelta(a.period, 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)) + d := period.Delta(avDelta) + p = period.AddDelta(p, d) } return p } // Advance advances the autoVibrato value by 1 tick -func (a *FreqModulator) Advance() { +func (a *FreqModulator[TPeriod]) Advance() { if !a.autoVibratoEnabled { return } diff --git a/voice/component/opl2.go b/voice/component/opl2.go index 5ba3427..b1b7390 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -25,16 +25,17 @@ type OPL2Registers struct { } // OPL2 is an OPL2 component -type OPL2 struct { - chip render.OPL2Chip - channel int - reg OPL2Registers - baseFreq period.Frequency - keyOn bool +type OPL2[TPeriod period.Period] struct { + chip render.OPL2Chip + channel int + reg OPL2Registers + baseFreq period.Frequency + periodConverter period.PeriodConverter[TPeriod] + 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]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency) { o.chip = chip o.channel = channel o.reg = reg @@ -43,7 +44,7 @@ func (o *OPL2) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseF } // Attack activates the key-on bit -func (o *OPL2) Attack() { +func (o *OPL2[TPeriod]) Attack() { o.keyOn = true // calculate the register addressing information @@ -69,7 +70,7 @@ func (o *OPL2) Attack() { } // Release deactivates the key-on bit -func (o *OPL2) Release() { +func (o *OPL2[TPeriod]) Release() { o.keyOn = false // calculate the register addressing information @@ -81,7 +82,7 @@ func (o *OPL2) Release() { } // Advance advances the playback -func (o *OPL2) Advance(carVol volume.Volume, period period.Period) { +func (o *OPL2[TPeriod]) Advance(carVol volume.Volume, period TPeriod) { // calculate the register addressing information index := uint32(o.channel) mod := o.getChannelIndex(o.channel) @@ -117,11 +118,11 @@ var twoOperatorMelodic = [...]uint32{ 0x100, 0x101, 0x102, 0x108, 0x109, 0x10A, 0x110, 0x111, 0x112, } -func (o *OPL2) getChannelIndex(channelIdx int) uint32 { +func (o *OPL2[TPeriod]) getChannelIndex(channelIdx int) uint32 { return twoOperatorMelodic[channelIdx%18] } -func (o *OPL2) calc40(reg40 uint8, vol volume.Volume) uint8 { +func (o *OPL2[TPeriod]) calc40(reg40 uint8, vol volume.Volume) uint8 { oVol := volume.Volume(63-uint16(reg40&0x3f)) / 63 totalVol := oVol * vol * 63 if totalVol > 63 { @@ -134,21 +135,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]) periodToFreqBlock(p TPeriod, baseFreq period.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]) 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]) freqToFnumBlock(freq float64) (uint16, uint8) { if freq > 6208.431 { return 0, 0 } diff --git a/voice/freqmodulator.go b/voice/freqmodulator.go index c46ffb7..0cbc6a0 100755 --- a/voice/freqmodulator.go +++ b/voice/freqmodulator.go @@ -5,10 +5,10 @@ import ( ) // FreqModulator is the instrument frequency control interface -type FreqModulator interface { - SetPeriod(period period.Period) - GetPeriod() period.Period +type FreqModulator[TPeriod period.Period] interface { + SetPeriod(period TPeriod) + GetPeriod() TPeriod SetPeriodDelta(delta period.Delta) GetPeriodDelta() period.Delta - GetFinalPeriod() period.Period + GetFinalPeriod() TPeriod } diff --git a/voice/pitchenveloper.go b/voice/pitchenveloper.go index 08d6739..f1d31a2 100755 --- a/voice/pitchenveloper.go +++ b/voice/pitchenveloper.go @@ -5,7 +5,7 @@ import ( ) // PitchEnveloper is a pitch envelope interface -type PitchEnveloper interface { +type PitchEnveloper[TPeriod period.Period] interface { EnablePitchEnvelope(enabled bool) IsPitchEnvelopeEnabled() bool GetCurrentPitchEnvelope() period.Delta diff --git a/voice/transaction.go b/voice/transaction.go index e04a295..8f87b8b 100755 --- a/voice/transaction.go +++ b/voice/transaction.go @@ -9,11 +9,11 @@ import ( ) // Transaction is an interface for updating Voice settings -type Transaction interface { +type Transaction[TPeriod period.Period] interface { Cancel() Commit() GetVoice() Voice - Clone() Transaction + Clone() Transaction[TPeriod] SetActive(active bool) IsPendingActive() (bool, bool) @@ -22,9 +22,9 @@ type Transaction interface { Attack() Release() Fadeout() - SetPeriod(period period.Period) - GetPendingPeriod() (period.Period, bool) - GetCurrentPeriod() period.Period + SetPeriod(period TPeriod) + GetPendingPeriod() (TPeriod, bool) + GetCurrentPeriod() TPeriod SetPeriodDelta(delta period.Delta) GetPendingPeriodDelta() (period.Delta, bool) GetCurrentPeriodDelta() period.Delta diff --git a/voice/voice.go b/voice/voice.go index c170375..510e4f4 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -28,7 +28,10 @@ type Voice interface { Advance(tickDuration time.Duration) GetSampler(samplerRate float32) sampling.Sampler Clone() Voice - StartTransaction() Transaction +} + +type VoiceTransactioner[TPeriod period.Period] interface { + StartTransaction() Transaction[TPeriod] } // Controller is the instrument actuation control interface @@ -63,41 +66,44 @@ func GetPos(v Voice) 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 { +func SetPeriod[TPeriod period.Period](v Voice, period TPeriod) { + if fm, ok := v.(FreqModulator[TPeriod]); 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 { +func GetPeriod[TPeriod period.Period](v Voice) TPeriod { + if fm, ok := v.(FreqModulator[TPeriod]); ok { return fm.GetPeriod() } - return nil + var empty TPeriod + return empty } // 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 { +func SetPeriodDelta[TPeriod period.Period](v Voice, delta period.Delta) { + if fm, ok := v.(FreqModulator[TPeriod]); ok { fm.SetPeriodDelta(delta) } } // 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 { +func GetPeriodDelta[TPeriod period.Period](v Voice) period.Delta { + if fm, ok := v.(FreqModulator[TPeriod]); ok { return fm.GetPeriodDelta() } - return period.Delta(0) + var empty period.Delta + return empty } // 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 { +func GetFinalPeriod[TPeriod period.Period](v Voice) TPeriod { + if fm, ok := v.(FreqModulator[TPeriod]); ok { return fm.GetFinalPeriod() } - return nil + var empty TPeriod + return empty } // == AmpModulator == @@ -193,15 +199,15 @@ func SetPanEnvelopePosition(v Voice, pos int) { // == 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 { +func EnablePitchEnvelope[TPeriod period.Period](v Voice, enabled bool) { + if pe, ok := v.(PitchEnveloper[TPeriod]); ok { pe.EnablePitchEnvelope(enabled) } } // 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 { +func SetPitchEnvelopePosition[TPeriod period.Period](v Voice, pos int) { + if pe, ok := v.(PitchEnveloper[TPeriod]); ok { pe.SetPitchEnvelopePosition(pos) } } @@ -233,9 +239,9 @@ func GetCurrentFilterEnvelope(v Voice) int8 { // == Envelopes == // SetEnvelopePosition sets the envelope position(s) on the voice -func SetAllEnvelopePositions(v Voice, pos int) { +func SetAllEnvelopePositions[TPeriod period.Period](v Voice, pos int) { SetVolumeEnvelopePosition(v, pos) SetPanEnvelopePosition(v, pos) - SetPitchEnvelopePosition(v, pos) + SetPitchEnvelopePosition[TPeriod](v, pos) SetFilterEnvelopePosition(v, pos) } From c8af97c375b8068161ffc2fa23b0a1fd5e108e2f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 22 Dec 2023 09:00:17 -0800 Subject: [PATCH 12/63] simplify porta of periods --- format/it/channel/util.go | 21 +++++++++++---------- format/xm/channel/util.go | 20 +++++++++++--------- period/amiga.go | 8 ++++++++ period/linear.go | 8 ++++++++ period/period.go | 20 ++++++++++++++++++-- 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/format/it/channel/util.go b/format/it/channel/util.go index 7b4a884..c105975 100644 --- a/format/it/channel/util.go +++ b/format/it/channel/util.go @@ -62,23 +62,18 @@ func doGlobalVolSlide(m IT, delta float32, multiplier float32) error { return nil } -func doPortaByDelta[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta period.Delta) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur.IsInvalid() { return nil } - cur = period.AddDelta(cur, delta) + delta := int(amount * multiplier) + cur = period.PortaUp(cur, delta) cs.SetPeriod(cur) return nil } -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { - delta := int(amount * multiplier) - d := period.Delta(delta) - return doPortaByDelta(cs, d) -} - func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { return err @@ -90,9 +85,15 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory] } func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { + cur := cs.GetPeriod() + if cur.IsInvalid() { + return nil + } + delta := int(amount * multiplier) - d := period.Delta(-delta) - return doPortaByDelta(cs, d) + cur = period.PortaDown(cur, delta) + cs.SetPeriod(cur) + return nil } func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { diff --git a/format/xm/channel/util.go b/format/xm/channel/util.go index badd952..be3fa34 100644 --- a/format/xm/channel/util.go +++ b/format/xm/channel/util.go @@ -58,23 +58,18 @@ func doGlobalVolSlide(m XM, delta float32, multiplier float32) error { return nil } -func doPortaByDelta[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta int) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur.IsInvalid() { return nil } - d := period.Delta(delta) - cur = period.AddDelta(cur, d) + delta := int(amount * multiplier) + cur = period.PortaUp(cur, delta) cs.SetPeriod(cur) return nil } -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { - delta := int(amount * multiplier) - return doPortaByDelta(cs, delta) -} - func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { if err := doPortaUp(cs, amount, multiplier); err != nil { return err @@ -86,8 +81,15 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory] } func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { + cur := cs.GetPeriod() + if cur.IsInvalid() { + return nil + } + delta := int(amount * multiplier) - return doPortaByDelta(cs, -delta) + cur = period.PortaDown(cur, delta) + cs.SetPeriod(cur) + return nil } func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { diff --git a/period/amiga.go b/period/amiga.go index 2d8371f..3735d5a 100644 --- a/period/amiga.go +++ b/period/amiga.go @@ -25,6 +25,14 @@ func (p Amiga) Add(d Delta) Amiga { return p } +func (p Amiga) PortaDown(amount int) Amiga { + return p.Add(Delta(-amount)) +} + +func (p Amiga) PortaUp(amount int) Amiga { + return p.Add(Delta(amount)) +} + func (p Amiga) IsInvalid() bool { return p == 0 } diff --git a/period/linear.go b/period/linear.go index 243eb29..c86c4fa 100644 --- a/period/linear.go +++ b/period/linear.go @@ -30,6 +30,14 @@ func (p Linear) Add(d Delta) Linear { 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) IsInvalid() bool { return p.Finetune == 0 } diff --git a/period/period.go b/period/period.go index 572e0c8..fd905db 100644 --- a/period/period.go +++ b/period/period.go @@ -9,17 +9,33 @@ type Period interface { IsInvalid() bool } -type PeriodAdder[TPeriod Period] interface { +type PeriodPorta[TPeriod Period] interface { Add(d Delta) TPeriod + PortaDown(amount int) TPeriod + PortaUp(amount int) TPeriod } func AddDelta[TPeriod Period](p TPeriod, d Delta) TPeriod { - if pa, ok := any(p).(PeriodAdder[TPeriod]); ok { + if pa, ok := any(p).(PeriodPorta[TPeriod]); ok { return pa.Add(d) } return p } +func PortaDown[TPeriod Period](p TPeriod, amount int) TPeriod { + if pa, ok := any(p).(PeriodPorta[TPeriod]); ok { + return pa.PortaDown(amount) + } + return p +} + +func PortaUp[TPeriod Period](p TPeriod, amount int) TPeriod { + if pa, ok := any(p).(PeriodPorta[TPeriod]); ok { + return pa.PortaUp(amount) + } + return p +} + // ComparePeriods compares two periods, taking nil into account func ComparePeriods[TPeriod Period](lhs TPeriod, rhs TPeriod) comparison.Spaceship { if lhs.IsInvalid() { From 04330693c4ad72a5c44da1dc2a2e64b4c9ff4f7b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 22 Dec 2023 09:00:27 -0800 Subject: [PATCH 13/63] missed in last checkin --- format/s3m/channel/util.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/format/s3m/channel/util.go b/format/s3m/channel/util.go index 58b032f..4264a17 100644 --- a/format/s3m/channel/util.go +++ b/format/s3m/channel/util.go @@ -56,8 +56,7 @@ func doPortaUp(cs S3MChannel, amount float32, multiplier float32) error { } delta := int(amount * multiplier) - d := period.Delta(delta) - cur = cur.Add(d) + cur = cur.PortaUp(delta) cs.SetPeriod(cur) return nil } @@ -89,8 +88,7 @@ func doPortaDown(cs S3MChannel, amount float32, multiplier float32) error { } delta := int(amount * multiplier) - d := period.Delta(-delta) - cur = cur.Add(d) + cur = cur.PortaDown(delta) cs.SetPeriod(cur) return nil } From c90073ad81425c23dd9ff01efd37d89560185b2d Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 22 Dec 2023 09:59:15 -0800 Subject: [PATCH 14/63] fix test and update go.mod --- go.mod | 2 +- go.sum | 4 +-- note/note_test.go | 82 +++-------------------------------------------- 3 files changed, 8 insertions(+), 80 deletions(-) diff --git a/go.mod b/go.mod index 50d3ff2..b58d5a2 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( 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-20231219160207-73b9e39aefca + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 ) require github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 4414422..9deebce 100644 --- a/go.sum +++ b/go.sum @@ -10,7 +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-20231219160207-73b9e39aefca h1:+xQfFu/HO/82Wwg4zuJ5xiLp0yaOLJjBGnuafXp85YQ= -golang.org/x/exp v0.0.0-20231219160207-73b9e39aefca/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 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/note/note_test.go b/note/note_test.go index 2d32339..8183ab8 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,80 +10,9 @@ 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 (p testPeriod) ToAmigaPeriod() period.Period { - return nil -} - -func (p testPeriod) ToLinearPeriod() period.Period { - return nil -} - -func periodCompareTest(t *testing.T, lhs, rhs *testPeriod, expected comparison.Spaceship) { +func periodCompareTest(t *testing.T, lhs, rhs testPeriod, expected comparison.Spaceship) { t.Helper() if period.ComparePeriods(lhs, rhs) != expected { @@ -95,13 +23,13 @@ func periodCompareTest(t *testing.T, lhs, rhs *testPeriod, expected comparison.S 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) } From 29866e002c8b806ef742e887b4ad1ad1a17e5a81 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 22 Dec 2023 10:07:22 -0800 Subject: [PATCH 15/63] extra simplification of period converters --- format/it/period/amigaconverter.go | 22 ++-------------------- format/it/period/linearconverter.go | 21 ++++----------------- format/s3m/period/amigaconverter.go | 22 ++-------------------- format/xm/period/amigaconverter.go | 22 ++-------------------- format/xm/period/linearconverter.go | 21 ++++----------------- period/amigaconverter.go | 25 +++++++++++++++++++++++++ period/linearconverter.go | 29 +++++++++++++++++++++++++++++ 7 files changed, 68 insertions(+), 94 deletions(-) create mode 100644 period/amigaconverter.go create mode 100644 period/linearconverter.go diff --git a/format/it/period/amigaconverter.go b/format/it/period/amigaconverter.go index 3972d9e..e4067a8 100644 --- a/format/it/period/amigaconverter.go +++ b/format/it/period/amigaconverter.go @@ -4,24 +4,6 @@ import ( "github.com/gotracker/playback/period" ) -// AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note -// definition. Useful in calculating resampling. -type amigaConverter struct{} - -var AmigaConverter period.PeriodConverter[period.Amiga] = amigaConverter{} - -// GetFrequency returns the frequency defined by the period -func (amigaConverter) GetFrequency(p period.Amiga) period.Frequency { - if p.IsInvalid() { - return 0 - } - return period.Frequency(ITBaseClock) / period.Frequency(p) -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (amigaConverter) GetSamplerAdd(p period.Amiga, samplerSpeed float64) float64 { - if p.IsInvalid() { - return 0 - } - return samplerSpeed / float64(p) +var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + BaseClock: ITBaseClock, } diff --git a/format/it/period/linearconverter.go b/format/it/period/linearconverter.go index 946e7e3..03e6d84 100644 --- a/format/it/period/linearconverter.go +++ b/format/it/period/linearconverter.go @@ -1,24 +1,11 @@ package period import ( - "math" - "github.com/gotracker/playback/period" ) -// Linear is a linear period, based on semitone and finetune values -type linearConverter struct{} - -var LinearConverter period.PeriodConverter[period.Linear] = linearConverter{} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (c linearConverter) GetSamplerAdd(p period.Linear, samplerSpeed float64) float64 { - return float64(c.GetFrequency(p)) * samplerSpeed / float64(ITBaseClock) -} - -// GetFrequency returns the frequency defined by the period -func (linearConverter) GetFrequency(p period.Linear) period.Frequency { - pft := float64(p.Finetune-C5SlideFines) / float64(SlideFinesPerOctave) - f := p.CommonRate * period.Frequency(math.Pow(2.0, pft)) - return f +var LinearConverter period.PeriodConverter[period.Linear] = period.LinearConverter{ + BaseClock: ITBaseClock, + BaseFinetune: C5SlideFines, + FinesPerOctave: SlideFinesPerOctave, } diff --git a/format/s3m/period/amigaconverter.go b/format/s3m/period/amigaconverter.go index b8046e2..01f7bbd 100644 --- a/format/s3m/period/amigaconverter.go +++ b/format/s3m/period/amigaconverter.go @@ -4,24 +4,6 @@ import ( "github.com/gotracker/playback/period" ) -// AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note -// definition. Useful in calculating resampling. -type amigaConverter struct{} - -var AmigaConverter period.PeriodConverter[period.Amiga] = amigaConverter{} - -// GetFrequency returns the frequency defined by the period -func (amigaConverter) GetFrequency(p period.Amiga) period.Frequency { - if p.IsInvalid() { - return 0 - } - return period.Frequency(S3MBaseClock) / period.Frequency(p) -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (amigaConverter) GetSamplerAdd(p period.Amiga, samplerSpeed float64) float64 { - if p.IsInvalid() { - return 0 - } - return samplerSpeed / float64(p) +var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + BaseClock: S3MBaseClock, } diff --git a/format/xm/period/amigaconverter.go b/format/xm/period/amigaconverter.go index c75d93d..6284324 100644 --- a/format/xm/period/amigaconverter.go +++ b/format/xm/period/amigaconverter.go @@ -4,24 +4,6 @@ import ( "github.com/gotracker/playback/period" ) -// AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note -// definition. Useful in calculating resampling. -type amigaConverter struct{} - -var AmigaConverter period.PeriodConverter[period.Amiga] = amigaConverter{} - -// GetFrequency returns the frequency defined by the period -func (amigaConverter) GetFrequency(p period.Amiga) period.Frequency { - if p.IsInvalid() { - return 0 - } - return period.Frequency(XMBaseClock) / period.Frequency(p) -} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (amigaConverter) GetSamplerAdd(p period.Amiga, samplerSpeed float64) float64 { - if p.IsInvalid() { - return 0 - } - return samplerSpeed / float64(p) +var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + BaseClock: XMBaseClock, } diff --git a/format/xm/period/linearconverter.go b/format/xm/period/linearconverter.go index efeba17..ade03e5 100644 --- a/format/xm/period/linearconverter.go +++ b/format/xm/period/linearconverter.go @@ -1,24 +1,11 @@ package period import ( - "math" - "github.com/gotracker/playback/period" ) -// Linear is a linear period, based on semitone and finetune values -type linearConverter struct{} - -var LinearConverter period.PeriodConverter[period.Linear] = linearConverter{} - -// GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (c linearConverter) GetSamplerAdd(p period.Linear, samplerSpeed float64) float64 { - return float64(c.GetFrequency(p)) * samplerSpeed / float64(XMBaseClock) -} - -// GetFrequency returns the frequency defined by the period -func (linearConverter) GetFrequency(p period.Linear) period.Frequency { - pft := float64(p.Finetune-C4SlideFines) / float64(SlideFinesPerOctave) - f := p.CommonRate * period.Frequency(math.Pow(2.0, pft)) - return f +var LinearConverter period.PeriodConverter[period.Linear] = period.LinearConverter{ + BaseClock: XMBaseClock, + BaseFinetune: C4SlideFines, + FinesPerOctave: SlideFinesPerOctave, } diff --git a/period/amigaconverter.go b/period/amigaconverter.go new file mode 100644 index 0000000..febe369 --- /dev/null +++ b/period/amigaconverter.go @@ -0,0 +1,25 @@ +package period + +// AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note +// definition. Useful in calculating resampling. +type AmigaConverter struct { + BaseClock Frequency +} + +var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) + +// GetFrequency returns the frequency defined by the period +func (c AmigaConverter) GetFrequency(p Amiga) Frequency { + if p.IsInvalid() { + return 0 + } + return c.BaseClock / Frequency(p) +} + +// GetSamplerAdd returns the number of samples to advance an instrument by given the period +func (AmigaConverter) GetSamplerAdd(p Amiga, samplerSpeed float64) float64 { + if p.IsInvalid() { + return 0 + } + return samplerSpeed / float64(p) +} diff --git a/period/linearconverter.go b/period/linearconverter.go new file mode 100644 index 0000000..13da8a8 --- /dev/null +++ b/period/linearconverter.go @@ -0,0 +1,29 @@ +package period + +import ( + "math" + + "github.com/gotracker/playback/note" +) + +// LinearConverter defines a sampler period that follows the Linear-style approach of note +// definition. Useful in calculating resampling. +type LinearConverter struct { + BaseClock Frequency + BaseFinetune note.Finetune + FinesPerOctave note.Finetune +} + +var _ PeriodConverter[Linear] = (*LinearConverter)(nil) + +// GetSamplerAdd returns the number of samples to advance an instrument by given the period +func (c LinearConverter) GetSamplerAdd(p Linear, samplerSpeed float64) float64 { + return float64(c.GetFrequency(p)) * samplerSpeed / float64(c.BaseClock) +} + +// GetFrequency returns the frequency defined by the period +func (c LinearConverter) GetFrequency(p Linear) Frequency { + pft := float64(p.Finetune-c.BaseFinetune) / float64(c.FinesPerOctave) + f := p.CommonRate * Frequency(math.Pow(2.0, pft)) + return f +} From 89c447a15d0968fa90e0ce5e80da1a400d30a05c Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 22 Dec 2023 13:03:12 -0800 Subject: [PATCH 16/63] clean up voice cloning - reenable fadeout code --- oscillator/impulsetracker.go | 8 +++++ oscillator/protracker.go | 7 +++++ player/voice/pcm.go | 47 +++++++++++++++++++++++++----- voice/component/envelope_filter.go | 10 +++++++ voice/component/envelope_pan.go | 10 +++++++ voice/component/envelope_pitch.go | 10 +++++++ voice/component/envelope_volume.go | 10 +++++++ voice/component/modulator_freq.go | 15 +++++++++- voice/component/sampler.go | 11 +++++++ voice/envelope/envelope.go | 9 ++++++ voice/oscillator/oscillator.go | 1 + 11 files changed, 129 insertions(+), 9 deletions(-) diff --git a/oscillator/impulsetracker.go b/oscillator/impulsetracker.go index 7c942b6..576495d 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 diff --git a/oscillator/protracker.go b/oscillator/protracker.go index fb7bfc5..9e226d5 100644 --- a/oscillator/protracker.go +++ b/oscillator/protracker.go @@ -34,6 +34,13 @@ 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 { var vib float32 diff --git a/player/voice/pcm.go b/player/voice/pcm.go index 5742523..5cae098 100644 --- a/player/voice/pcm.go +++ b/player/voice/pcm.go @@ -90,6 +90,8 @@ func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPerio v.pitchAndFilterEnvShared = true v.filterEnvActive = d.PitchFiltMode v.sampler.Setup(d.Sample, d.Loop, d.SustainLoop) + v.fadeoutMode = d.FadeOut.Mode + v.amp.ResetFadeoutValue(d.FadeOut.Amount) //v.sampler.SetPos(d.InitialPos) v.amp.Setup(d.MixingVolume) v.amp.ResetFadeoutValue(d.FadeOut.Amount) @@ -105,6 +107,14 @@ func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPerio v.channels = d.Sample.Channels() } + switch v.fadeoutMode { + case fadeout.ModeAlwaysActive: + v.amp.SetFadeoutEnabled(true) + case fadeout.ModeOnlyIfVolEnvActive: + v.amp.SetFadeoutEnabled(v.volEnv.IsEnabled()) + default: + } + v.amp.SetVolume(config.InitialVolume) v.freq.SetPeriod(config.InitialPeriod) v.freq.SetAutoVibratoEnabled(config.AutoVibrato.Enabled) @@ -375,9 +385,6 @@ func (v *pcmVoice[TPeriod]) SetPanEnvelopePosition(pos int) { // == required function interfaces == func (v *pcmVoice[TPeriod]) Advance(tickDuration time.Duration) { - defer func() { - v.prevKeyOn = v.keyOn - }() v.amp.Advance() v.freq.Advance() v.pan.Advance() @@ -412,6 +419,7 @@ func (v *pcmVoice[TPeriod]) Advance(tickDuration time.Duration) { } else { v.vol0ticks = 0 } + v.prevKeyOn = v.keyOn } func (v *pcmVoice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { @@ -425,12 +433,35 @@ func (v *pcmVoice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { } func (v *pcmVoice[TPeriod]) Clone() voice.Voice { - p := *v - if p.voiceFilter != nil { - p.voiceFilter = p.voiceFilter.Clone() + p := pcmVoice[TPeriod]{ + c2spd: v.c2spd, + initialVolume: v.initialVolume, + outputFilter: v.outputFilter, + fadeoutMode: v.fadeoutMode, + channels: v.channels, + active: true, + keyOn: false, + prevKeyOn: false, + pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, + filterEnvActive: v.filterEnvActive, + sampler: v.sampler.Clone(), + amp: v.amp, + freq: v.freq, + pan: v.pan, + volEnv: v.volEnv.Clone(), + pitchEnv: v.pitchEnv.Clone(), + panEnv: v.panEnv.Clone(), + filterEnv: v.filterEnv.Clone(), + vol0ticks: 0, + done: false, + periodConverter: v.periodConverter, } - if p.pluginFilter != nil { - p.pluginFilter = p.pluginFilter.Clone() + + if v.voiceFilter != nil { + p.voiceFilter = v.voiceFilter.Clone() + } + if v.pluginFilter != nil { + p.pluginFilter = v.pluginFilter.Clone() } return &p } diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 03b6cf5..7c6db84 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -14,6 +14,16 @@ type FilterEnvelope struct { prevKeyOn bool } +func (e FilterEnvelope) Clone() FilterEnvelope { + return FilterEnvelope{ + enabled: e.enabled, + state: e.state.Clone(), + value: e.value, + keyOn: false, + prevKeyOn: false, + } +} + // Reset resets the state to defaults based on the envelope provided func (e *FilterEnvelope) Reset(env *envelope.Envelope[int8]) { e.state.Reset(env) diff --git a/voice/component/envelope_pan.go b/voice/component/envelope_pan.go index a956980..9c0dce7 100755 --- a/voice/component/envelope_pan.go +++ b/voice/component/envelope_pan.go @@ -16,6 +16,16 @@ type PanEnvelope struct { prevKeyOn bool } +func (e PanEnvelope) Clone() PanEnvelope { + return PanEnvelope{ + enabled: e.enabled, + state: e.state.Clone(), + pan: e.pan, + keyOn: false, + prevKeyOn: false, + } +} + // Reset resets the state to defaults based on the envelope provided func (e *PanEnvelope) Reset(env *envelope.Envelope[panning.Position]) { e.state.Reset(env) diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index c0f1f58..80dbc48 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -16,6 +16,16 @@ type PitchEnvelope[TPeriod period.Period] struct { prevKeyOn bool } +func (e PitchEnvelope[TPeriod]) Clone() PitchEnvelope[TPeriod] { + return PitchEnvelope[TPeriod]{ + enabled: e.enabled, + state: e.state.Clone(), + delta: e.delta, + keyOn: false, + prevKeyOn: false, + } +} + // Reset resets the state to defaults based on the envelope provided func (e *PitchEnvelope[TPeriod]) Reset(env *envelope.Envelope[int8]) { e.state.Reset(env) diff --git a/voice/component/envelope_volume.go b/voice/component/envelope_volume.go index 4ef157e..d28dafc 100755 --- a/voice/component/envelope_volume.go +++ b/voice/component/envelope_volume.go @@ -16,6 +16,16 @@ type VolumeEnvelope struct { prevKeyOn bool } +func (e VolumeEnvelope) Clone() VolumeEnvelope { + return VolumeEnvelope{ + enabled: e.enabled, + state: e.state.Clone(), + vol: e.vol, + keyOn: false, + prevKeyOn: false, + } +} + // Reset resets the state to defaults based on the envelope provided func (e *VolumeEnvelope) Reset(env *envelope.Envelope[volume.Volume]) { e.state.Reset(env) diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index d9e5413..b2d02ab 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -18,6 +18,19 @@ type FreqModulator[TPeriod period.Period] struct { autoVibratoAge int // current age of oscillator (in ticks) } +func (a FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { + return FreqModulator[TPeriod]{ + period: a.period, + delta: a.delta, + autoVibratoEnabled: a.autoVibratoEnabled, + autoVibrato: a.autoVibrato.Clone(), + autoVibratoDepth: a.autoVibratoDepth, + autoVibratoRate: a.autoVibratoRate, + autoVibratoSweep: a.autoVibratoSweep, + autoVibratoAge: 0, + } +} + // SetPeriod sets the current period (before AutoVibrato and Delta calculation) func (a *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { a.period = period @@ -58,7 +71,7 @@ func (a *FreqModulator[TPeriod]) ResetAutoVibrato(sweep ...int) { a.autoVibratoAge = 0 - if sweep != nil { + if len(sweep) > 0 { a.autoVibratoSweep = sweep[0] } } diff --git a/voice/component/sampler.go b/voice/component/sampler.go index 45f8390..f3b09ea 100644 --- a/voice/component/sampler.go +++ b/voice/component/sampler.go @@ -18,6 +18,17 @@ type Sampler struct { sustainLoop loop.Loop } +func (s Sampler) Clone() Sampler { + return Sampler{ + sample: s.sample, + pos: sampling.Pos{}, + keyOn: false, + loopsEnabled: s.loopsEnabled, + wholeLoop: s.wholeLoop, + sustainLoop: s.sustainLoop, + } +} + // Setup sets up the sampler func (s *Sampler) Setup(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop) { s.sample = sample diff --git a/voice/envelope/envelope.go b/voice/envelope/envelope.go index 2bc0cf0..5739c67 100644 --- a/voice/envelope/envelope.go +++ b/voice/envelope/envelope.go @@ -12,6 +12,15 @@ type State[T any] struct { env *Envelope[T] } +func (e State[T]) Clone() State[T] { + return State[T]{ + position: 0, + length: e.length, + stopped: false, + env: e.env, + } +} + // Stopped returns true if the envelope state is stopped func (e *State[T]) Stopped() bool { return e.stopped diff --git a/voice/oscillator/oscillator.go b/voice/oscillator/oscillator.go index 317c2db..b9ad62d 100755 --- a/voice/oscillator/oscillator.go +++ b/voice/oscillator/oscillator.go @@ -5,6 +5,7 @@ type WaveTableSelect uint8 // Oscillator is an oscillator type Oscillator interface { + Clone() Oscillator GetWave(depth float32) float32 Advance(speed int) SetWaveform(table WaveTableSelect) From 30d81cda8e8dedf132c7ce58fb18f49a8c388dc3 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 22 Dec 2023 18:22:34 -0800 Subject: [PATCH 17/63] clean up format field names --- format/it/load/instrument.go | 8 ++-- format/it/period/amigaconverter.go | 3 +- format/it/period/linearconverter.go | 5 +-- format/it/period/util.go | 57 +++++++++---------------- format/it/playback/playback.go | 6 ++- format/it/playback/playback_command.go | 4 +- format/it/system/system.go | 28 ++++++++++++ format/s3m/load/modconv/modconverter.go | 4 +- format/s3m/load/s3mformat.go | 10 ++--- format/s3m/period/amigaconverter.go | 3 +- format/s3m/period/util.go | 50 ++++++++-------------- format/s3m/playback/playback.go | 5 ++- format/s3m/playback/playback_command.go | 4 +- format/s3m/system/system.go | 33 ++++++++++++++ format/xm/load/xmformat.go | 10 ++--- format/xm/period/amigaconverter.go | 3 +- format/xm/period/linearconverter.go | 5 +-- format/xm/period/util.go | 36 ++++++++-------- format/xm/playback/playback_command.go | 4 +- format/xm/system/system.go | 30 +++++++++++++ instrument/instrument.go | 20 ++++----- period/amigaconverter.go | 6 ++- period/frequency.go | 6 ++- period/linearconverter.go | 10 ++--- player/tracker.go | 2 +- player/voice/opl2.go | 8 ++-- player/voice/pcm.go | 8 ++-- player/voice/voice.go | 8 ++-- system/clockedsystem.go | 23 ++++++++++ system/frequency.go | 4 ++ system/system.go | 9 ++++ 31 files changed, 260 insertions(+), 152 deletions(-) create mode 100644 format/it/system/system.go create mode 100644 format/s3m/system/system.go create mode 100644 format/xm/system/system.go create mode 100644 system/clockedsystem.go create mode 100644 system/frequency.go create mode 100644 system/system.go diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index ce1a6e3..208500e 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -442,7 +442,7 @@ 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.SampleRate = period.Frequency(si.Header.C5Speed) ii.Static.AutoVibrato = voice.AutoVibrato{ Enabled: (si.Header.VibratoDepth != 0 && si.Header.VibratoSpeed != 0 && si.Header.VibratoSweep != 0), Sweep: 255, @@ -455,12 +455,12 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrumen } ii.Static.Volume = volume.Volume(si.Header.Volume.Value()) - if ii.C2Spd == 0 { - ii.C2Spd = 8363.0 + if ii.SampleRate == 0 { + ii.SampleRate = 8363.0 } if si.Header.Flags.IsStereo() { - ii.C2Spd /= 2.0 + ii.SampleRate /= 2.0 } if !convSettings.linearFrequencySlides { diff --git a/format/it/period/amigaconverter.go b/format/it/period/amigaconverter.go index e4067a8..a0cda98 100644 --- a/format/it/period/amigaconverter.go +++ b/format/it/period/amigaconverter.go @@ -1,9 +1,10 @@ package period import ( + "github.com/gotracker/playback/format/it/system" "github.com/gotracker/playback/period" ) var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ - BaseClock: ITBaseClock, + System: system.ITSystem, } diff --git a/format/it/period/linearconverter.go b/format/it/period/linearconverter.go index 03e6d84..fd03f03 100644 --- a/format/it/period/linearconverter.go +++ b/format/it/period/linearconverter.go @@ -1,11 +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{ - BaseClock: ITBaseClock, - BaseFinetune: C5SlideFines, - FinesPerOctave: SlideFinesPerOctave, + System: system.ITSystem, } diff --git a/format/it/period/util.go b/format/it/period/util.go index d841bf1..a940f8b 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -3,42 +3,25 @@ package period import ( "math" + "github.com/gotracker/playback/format/it/system" "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 - SlideFinesPerSemitone = 4 - SemitonesPerNote = 16 - SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote - SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave - C5SlideFines = 5 * SlideFinesPerOctave -) - +var DefaultC5SampleRate = system.DefaultC5SampleRate 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[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c2spd period.Frequency) TPeriod { +func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c5SampleRate period.Frequency) TPeriod { if semi == note.UnchangedSemitone { panic("how?") } var result TPeriod switch p := any(&result).(type) { case *period.Linear: - nft := int(semi)*SlideFinesPerNote + int(ft) + nft := int(semi)*system.SlideFinesPerNote + int(ft) p.Finetune = note.Finetune(nft) - p.CommonRate = c2spd + p.CommonRate = c5SampleRate case *period.Amiga: key := int(semi.Key()) octave := uint32(semi.Octave()) @@ -47,45 +30,45 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet return result } - if c2spd == 0 { - c2spd = period.Frequency(DefaultC2Spd) + if c5SampleRate == 0 { + c5SampleRate = period.Frequency(system.DefaultC5SampleRate) } if ft != 0 { - c2spd = CalcFinetuneC2Spd[period.Amiga](AmigaConverter, c2spd, ft) + c5SampleRate = CalcFinetuneC5SampleRate[period.Amiga](AmigaConverter, c5SampleRate, ft) } - *p = period.Amiga(float64(floatDefaultC2Spd*semitonePeriodTable[key]) / float64(uint32(c2spd)< 0 { out <- premix } diff --git a/player/voice/opl2.go b/player/voice/opl2.go index 91de567..6ba6e64 100644 --- a/player/voice/opl2.go +++ b/player/voice/opl2.go @@ -30,7 +30,7 @@ type OPL2Registers component.OPL2Registers type OPLConfiguration[TPeriod period.Period] struct { Chip render.OPL2Chip Channel int - C2SPD period.Frequency + SampleRate period.Frequency InitialVolume volume.Volume InitialPeriod TPeriod AutoVibrato voice.AutoVibrato @@ -40,7 +40,7 @@ type OPLConfiguration[TPeriod period.Period] struct { // == the actual opl2 voice == type opl2Voice[TPeriod period.Period] struct { - c2spd period.Frequency + sampleRate period.Frequency initialVolume volume.Volume active bool @@ -59,7 +59,7 @@ type opl2Voice[TPeriod period.Period] struct { // NewOPL2 creates a new OPL2 voice func NewOPL2[TPeriod period.Period](config OPLConfiguration[TPeriod]) voice.Voice { v := opl2Voice[TPeriod]{ - c2spd: config.C2SPD, + sampleRate: config.SampleRate, initialVolume: config.InitialVolume, fadeoutMode: fadeout.ModeDisabled, active: true, @@ -90,7 +90,7 @@ func NewOPL2[TPeriod period.Period](config OPLConfiguration[TPeriod]) voice.Voic _ = d } - v.o.Setup(config.Chip, config.Channel, regs, config.C2SPD) + v.o.Setup(config.Chip, config.Channel, regs, config.SampleRate) v.amp.SetVolume(config.InitialVolume) v.freq.SetPeriod(config.InitialPeriod) v.freq.SetAutoVibratoEnabled(config.AutoVibrato.Enabled) diff --git a/player/voice/pcm.go b/player/voice/pcm.go index 5cae098..fbe937b 100644 --- a/player/voice/pcm.go +++ b/player/voice/pcm.go @@ -31,7 +31,7 @@ type PCM[TPeriod period.Period] interface { // PCMConfiguration is the information needed to configure an PCM2 voice type PCMConfiguration[TPeriod period.Period] struct { - C2SPD period.Frequency + SampleRate period.Frequency InitialVolume volume.Volume InitialPeriod TPeriod AutoVibrato voice.AutoVibrato @@ -44,7 +44,7 @@ type PCMConfiguration[TPeriod period.Period] struct { // == the actual pcm voice == type pcmVoice[TPeriod period.Period] struct { - c2spd period.Frequency + sampleRate period.Frequency initialVolume volume.Volume outputFilter voice.FilterApplier voiceFilter filter.Filter @@ -76,7 +76,7 @@ type pcmVoice[TPeriod period.Period] struct { // NewPCM creates a new PCM voice func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], config PCMConfiguration[TPeriod]) voice.Voice { v := pcmVoice[TPeriod]{ - c2spd: config.C2SPD, + sampleRate: config.SampleRate, initialVolume: config.InitialVolume, outputFilter: config.OutputFilter, voiceFilter: config.VoiceFilter, @@ -434,7 +434,7 @@ func (v *pcmVoice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { func (v *pcmVoice[TPeriod]) Clone() voice.Voice { p := pcmVoice[TPeriod]{ - c2spd: v.c2spd, + sampleRate: v.sampleRate, initialVolume: v.initialVolume, outputFilter: v.outputFilter, fadeoutMode: v.fadeoutMode, diff --git a/player/voice/voice.go b/player/voice/voice.go index a7aa749..b8aaeec 100644 --- a/player/voice/voice.go +++ b/player/voice/voice.go @@ -18,13 +18,13 @@ func New[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], pluginFilter filter.Filter ) if factory := inst.GetFilterFactory(); factory != nil { - voiceFilter = factory(inst.C2Spd, output.GetSampleRate()) + voiceFilter = factory(inst.SampleRate, output.GetSampleRate()) } if factory := inst.GetPluginFilterFactory(); factory != nil { - pluginFilter = factory(inst.C2Spd, output.GetSampleRate()) + pluginFilter = factory(inst.SampleRate, output.GetSampleRate()) } return NewPCM[TPeriod](periodConverter, PCMConfiguration[TPeriod]{ - C2SPD: inst.GetC2Spd(), + SampleRate: inst.GetSampleRate(), InitialVolume: inst.GetDefaultVolume(), AutoVibrato: inst.GetAutoVibrato(), Data: data, @@ -36,7 +36,7 @@ func New[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], return NewOPL2[TPeriod](OPLConfiguration[TPeriod]{ Chip: output.GetOPL2Chip(), Channel: output.ChannelNum, - C2SPD: inst.GetC2Spd(), + SampleRate: inst.GetSampleRate(), InitialVolume: inst.GetDefaultVolume(), AutoVibrato: inst.GetAutoVibrato(), Data: data, diff --git a/system/clockedsystem.go b/system/clockedsystem.go new file mode 100644 index 0000000..405876f --- /dev/null +++ b/system/clockedsystem.go @@ -0,0 +1,23 @@ +package system + +import ( + "github.com/gotracker/playback/note" +) + +type ClockedSystem struct { + BaseClock Frequency + BaseFinetunes note.Finetune + FinetunesPerOctave note.Finetune +} + +func (s ClockedSystem) GetBaseClock() Frequency { + return s.BaseClock +} + +func (s ClockedSystem) GetBaseFinetunes() note.Finetune { + return s.BaseFinetunes +} + +func (s ClockedSystem) GetFinetunesPerOctave() note.Finetune { + return s.FinetunesPerOctave +} diff --git a/system/frequency.go b/system/frequency.go new file mode 100644 index 0000000..31774f2 --- /dev/null +++ b/system/frequency.go @@ -0,0 +1,4 @@ +package system + +// Frequency is a frequency value, in Hertz (Hz) +type Frequency float64 diff --git a/system/system.go b/system/system.go new file mode 100644 index 0000000..b12ba64 --- /dev/null +++ b/system/system.go @@ -0,0 +1,9 @@ +package system + +import "github.com/gotracker/playback/note" + +type System interface { + GetBaseClock() Frequency + GetBaseFinetunes() note.Finetune + GetFinetunesPerOctave() note.Finetune +} From 0c4148c70cf5fb4f637c850bdc599dda0b1014ab Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 12:49:34 -0800 Subject: [PATCH 18/63] clean up type conversions for channel data - split channel processing out to format --- channel.go | 7 +- effect.go | 52 +++--- format/it/channel/data.go | 74 ++++++++ format/it/channel/effect_arpeggio.go | 4 +- .../it/channel/effect_channelvolumeslide.go | 4 +- .../it/channel/effect_extrafineportadown.go | 2 +- format/it/channel/effect_extrafineportaup.go | 2 +- format/it/channel/effect_finepatterndelay.go | 2 +- format/it/channel/effect_fineportadown.go | 2 +- format/it/channel/effect_fineportaup.go | 2 +- format/it/channel/effect_finevibrato.go | 4 +- format/it/channel/effect_finevolslidedown.go | 6 +- format/it/channel/effect_finevolslideup.go | 6 +- format/it/channel/effect_globalvolumeslide.go | 4 +- format/it/channel/effect_highoffset.go | 2 +- .../effect_newnoteactionnotecontinue.go | 2 +- .../it/channel/effect_newnoteactionnotecut.go | 2 +- .../channel/effect_newnoteactionnotefade.go | 2 +- .../it/channel/effect_newnoteactionnoteoff.go | 2 +- format/it/channel/effect_notecut.go | 4 +- format/it/channel/effect_notedelay.go | 4 +- format/it/channel/effect_orderjump.go | 4 +- .../it/channel/effect_panningenvelopeoff.go | 2 +- format/it/channel/effect_panningenvelopeon.go | 2 +- format/it/channel/effect_pastnotecut.go | 2 +- format/it/channel/effect_pastnotefadeout.go | 2 +- format/it/channel/effect_pastnoteoff.go | 2 +- format/it/channel/effect_patterndelay.go | 4 +- format/it/channel/effect_patternloop.go | 4 +- format/it/channel/effect_pitchenvelopeoff.go | 2 +- format/it/channel/effect_pitchenvelopeon.go | 2 +- format/it/channel/effect_portadown.go | 4 +- format/it/channel/effect_portatonote.go | 6 +- format/it/channel/effect_portaup.go | 4 +- format/it/channel/effect_portavolslide.go | 2 +- format/it/channel/effect_retrigvolslide.go | 4 +- format/it/channel/effect_rowjump.go | 4 +- format/it/channel/effect_sampleoffset.go | 2 +- format/it/channel/effect_setchannelvolume.go | 2 +- .../it/channel/effect_setcoarsepanposition.go | 2 +- format/it/channel/effect_setfinetune.go | 4 +- format/it/channel/effect_setglobalvolume.go | 4 +- .../it/channel/effect_setpanbrellowaveform.go | 2 +- format/it/channel/effect_setpanposition.go | 2 +- format/it/channel/effect_setspeed.go | 4 +- format/it/channel/effect_settempo.go | 6 +- .../it/channel/effect_settremolowaveform.go | 2 +- .../it/channel/effect_setvibratowaveform.go | 2 +- format/it/channel/effect_tremolo.go | 4 +- format/it/channel/effect_tremor.go | 4 +- format/it/channel/effect_vibrato.go | 4 +- format/it/channel/effect_vibratovolslide.go | 2 +- format/it/channel/effect_volslidedown.go | 6 +- format/it/channel/effect_volslideup.go | 6 +- format/it/channel/effect_volumeenvelopeoff.go | 2 +- format/it/channel/effect_volumeenvelopeon.go | 2 +- format/it/channel/effectfactory.go | 2 +- format/it/channel/unhandled.go | 4 +- format/it/channel/util.go | 22 +-- format/it/playback/playback.go | 14 +- format/it/playback/playback_command.go | 8 +- format/it/playback/playback_pattern.go | 4 +- format/it/playback/playback_textoutput.go | 4 +- format/it/playback/playback_tracing.go | 4 +- format/s3m/channel/data.go | 74 ++++++++ format/s3m/channel/effect_portatonote.go | 2 +- format/s3m/channel/effect_portavolslide.go | 2 +- format/s3m/channel/effect_vibratovolslide.go | 2 +- format/s3m/channel/util.go | 2 +- format/s3m/playback/playback.go | 8 +- format/s3m/playback/playback_command.go | 5 +- format/s3m/playback/playback_pattern.go | 2 +- format/s3m/playback/playback_textoutput.go | 4 +- format/xm/channel/data.go | 74 ++++++++ format/xm/channel/effect_arpeggio.go | 4 +- .../xm/channel/effect_extrafineportadown.go | 2 +- format/xm/channel/effect_extrafineportaup.go | 2 +- format/xm/channel/effect_fineportadown.go | 2 +- format/xm/channel/effect_fineportaup.go | 2 +- format/xm/channel/effect_finevolslidedown.go | 2 +- format/xm/channel/effect_finevolslideup.go | 2 +- format/xm/channel/effect_globalvolumeslide.go | 4 +- format/xm/channel/effect_notecut.go | 4 +- format/xm/channel/effect_notedelay.go | 4 +- format/xm/channel/effect_orderjump.go | 4 +- format/xm/channel/effect_panslide.go | 2 +- format/xm/channel/effect_patterndelay.go | 4 +- format/xm/channel/effect_patternloop.go | 2 +- format/xm/channel/effect_portadown.go | 4 +- format/xm/channel/effect_portatonote.go | 6 +- format/xm/channel/effect_portaup.go | 4 +- format/xm/channel/effect_portavolslide.go | 2 +- format/xm/channel/effect_retriggernote.go | 4 +- format/xm/channel/effect_retrigvolslide.go | 4 +- format/xm/channel/effect_rowjump.go | 4 +- format/xm/channel/effect_sampleoffset.go | 2 +- .../xm/channel/effect_setcoarsepanposition.go | 2 +- .../xm/channel/effect_setenvelopeposition.go | 2 +- format/xm/channel/effect_setfinetune.go | 4 +- format/xm/channel/effect_setglobalvolume.go | 4 +- format/xm/channel/effect_setpanposition.go | 2 +- format/xm/channel/effect_setspeed.go | 4 +- format/xm/channel/effect_settempo.go | 6 +- .../xm/channel/effect_settremolowaveform.go | 2 +- .../xm/channel/effect_setvibratowaveform.go | 2 +- format/xm/channel/effect_setvolume.go | 2 +- format/xm/channel/effect_tremolo.go | 4 +- format/xm/channel/effect_tremor.go | 4 +- format/xm/channel/effect_vibrato.go | 4 +- format/xm/channel/effect_vibratovolslide.go | 2 +- format/xm/channel/effect_volslide.go | 4 +- format/xm/channel/effectfactory.go | 2 +- format/xm/channel/unhandled.go | 4 +- format/xm/channel/util.go | 22 +-- format/xm/playback/playback.go | 14 +- format/xm/playback/playback_command.go | 13 +- format/xm/playback/playback_pattern.go | 4 +- format/xm/playback/playback_textoutput.go | 4 +- player/op/channeltargets.go | 22 +++ player/state/channel.go | 153 +++++++++-------- player/state/channel_transaction.go | 162 ++++-------------- song/{channel.go => channeldata.go} | 0 122 files changed, 601 insertions(+), 455 deletions(-) create mode 100644 player/op/channeltargets.go rename song/{channel.go => channeldata.go} (100%) diff --git a/channel.go b/channel.go index f4eba8f..1b2376b 100644 --- a/channel.go +++ b/channel.go @@ -5,6 +5,7 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" @@ -14,7 +15,7 @@ import ( ) // Channel is an interface for channel state -type Channel[TPeriod period.Period, TMemory any] interface { +type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { ResetRetriggerCount() SetMemory(*TMemory) GetMemory() *TMemory @@ -22,7 +23,7 @@ type Channel[TPeriod period.Period, TMemory any] interface { SetActiveVolume(volume.Volume) FreezePlayback() UnfreezePlayback() - GetData() song.ChannelData + GetChannelData() TChannelData GetPortaTargetPeriod() TPeriod SetPortaTargetPeriod(TPeriod) GetTargetPeriod() TPeriod @@ -69,3 +70,5 @@ type Channel[TPeriod period.Period, TMemory any] interface { SetPitchEnvelopeEnable(bool) NoteCut() } + +type ChannelTargeter[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(out *op.ChannelTargets[TPeriod], d TChannelData, s song.Data, cs Channel[TPeriod, TMemory, TChannelData]) error diff --git a/effect.go b/effect.go index 37cb1ca..6ce554a 100644 --- a/effect.go +++ b/effect.go @@ -17,9 +17,9 @@ type Effecter[TMemory any] interface { GetEffects(*TMemory, period.Period) []Effect } -func GetEffects[TPeriod period.Period, TMemory any](mem *TMemory, d song.ChannelData) []Effect { +func GetEffects[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](mem *TMemory, d TChannelData) []Effect { var e []Effect - if eff, ok := d.(Effecter[TMemory]); ok { + if eff, ok := any(d).(Effecter[TMemory]); ok { var p TPeriod e = eff.GetEffects(mem, p) } @@ -30,8 +30,8 @@ type EffectNamer interface { Names() []string } -type effectPreStartIntf[TPeriod period.Period, TMemory any] interface { - PreStart(Channel[TPeriod, TMemory], Playback) error +type effectPreStartIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + PreStart(Channel[TPeriod, TMemory, TChannelData], Playback) error } func GetEffectNames(e Effect) []string { @@ -44,8 +44,8 @@ func GetEffectNames(e Effect) []string { } // EffectPreStart triggers when the effect enters onto the channel state -func EffectPreStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback) error { - if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory]); ok { +func EffectPreStart[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { + if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory, TChannelData]); ok { if err := eff.PreStart(cs, p); err != nil { return err } @@ -53,13 +53,13 @@ func EffectPreStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPe return nil } -type effectStartIntf[TPeriod period.Period, TMemory any] interface { - Start(Channel[TPeriod, TMemory], Playback) error +type effectStartIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + Start(Channel[TPeriod, TMemory, TChannelData], Playback) error } // EffectStart triggers on the first tick, but before the Tick() function is called -func EffectStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback) error { - if eff, ok := e.(effectStartIntf[TPeriod, TMemory]); ok { +func EffectStart[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { + if eff, ok := e.(effectStartIntf[TPeriod, TMemory, TChannelData]); ok { if err := eff.Start(cs, p); err != nil { return err } @@ -67,13 +67,13 @@ func EffectStart[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPerio return nil } -type effectTickIntf[TPeriod period.Period, TMemory any] interface { - Tick(Channel[TPeriod, TMemory], Playback, int) error +type effectTickIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + Tick(Channel[TPeriod, TMemory, TChannelData], Playback, int) error } // EffectTick is called on every tick -func EffectTick[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, currentTick int) error { - if eff, ok := e.(effectTickIntf[TPeriod, TMemory]); ok { +func EffectTick[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback, currentTick int) error { + if eff, ok := e.(effectTickIntf[TPeriod, TMemory, TChannelData]); ok { if err := eff.Tick(cs, p, currentTick); err != nil { return err } @@ -81,13 +81,13 @@ func EffectTick[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod return nil } -type effectStopIntf[TPeriod period.Period, TMemory any] interface { - Stop(Channel[TPeriod, TMemory], Playback, int) error +type effectStopIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + Stop(Channel[TPeriod, TMemory, TChannelData], Playback, int) error } // EffectStop is called on the last tick of the row, but after the Tick() function is called -func EffectStop[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, lastTick int) error { - if eff, ok := e.(effectStopIntf[TPeriod, TMemory]); ok { +func EffectStop[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback, lastTick int) error { + if eff, ok := e.(effectStopIntf[TPeriod, TMemory, TChannelData]); ok { if err := eff.Stop(cs, p, lastTick); err != nil { return err } @@ -96,12 +96,12 @@ func EffectStop[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod } // CombinedEffect specifies multiple simultaneous effects into one -type CombinedEffect[TPeriod period.Period, TMemory any] struct { +type CombinedEffect[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { Effects []Effect } // PreStart triggers when the effect enters onto the channel state -func (e CombinedEffect[TPeriod, TMemory]) PreStart(cs Channel[TPeriod, TMemory], p Playback) error { +func (e CombinedEffect[TPeriod, TMemory, TChannelData]) PreStart(cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { for _, effect := range e.Effects { if err := EffectPreStart(effect, cs, p); err != nil { return err @@ -111,7 +111,7 @@ func (e CombinedEffect[TPeriod, TMemory]) PreStart(cs Channel[TPeriod, TMemory], } // Start triggers on the first tick, but before the Tick() function is called -func (e CombinedEffect[TPeriod, TMemory]) Start(cs Channel[TPeriod, TMemory], p Playback) error { +func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Start(cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { for _, effect := range e.Effects { if err := EffectStart(effect, cs, p); err != nil { return err @@ -121,7 +121,7 @@ func (e CombinedEffect[TPeriod, TMemory]) Start(cs Channel[TPeriod, TMemory], p } // Tick is called on every tick -func (e CombinedEffect[TPeriod, TMemory]) Tick(cs Channel[TPeriod, TMemory], p Playback, currentTick int) error { +func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Tick(cs Channel[TPeriod, TMemory, TChannelData], p Playback, currentTick int) error { for _, effect := range e.Effects { if err := EffectTick(effect, cs, p, currentTick); err != nil { return err @@ -131,7 +131,7 @@ func (e CombinedEffect[TPeriod, TMemory]) Tick(cs Channel[TPeriod, TMemory], p P } // Stop is called on the last tick of the row, but after the Tick() function is called -func (e CombinedEffect[TPeriod, TMemory]) Stop(cs Channel[TPeriod, TMemory], p Playback, lastTick int) error { +func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Stop(cs Channel[TPeriod, TMemory, TChannelData], p Playback, lastTick int) error { for _, effect := range e.Effects { if err := EffectStop(effect, cs, p, lastTick); err != nil { return err @@ -141,7 +141,7 @@ func (e CombinedEffect[TPeriod, TMemory]) Stop(cs Channel[TPeriod, TMemory], p P } // String returns the string for the effect list -func (e CombinedEffect[TPeriod, TMemory]) String() string { +func (e CombinedEffect[TPeriod, TMemory, TChannelData]) String() string { for _, eff := range e.Effects { s := fmt.Sprint(eff) if s != "" { @@ -151,7 +151,7 @@ func (e CombinedEffect[TPeriod, TMemory]) String() string { return "" } -func (e CombinedEffect[TPeriod, TMemory]) Names() []string { +func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Names() []string { var names []string for _, eff := range e.Effects { names = append(names, GetEffectNames(eff)...) @@ -160,7 +160,7 @@ func (e CombinedEffect[TPeriod, TMemory]) Names() []string { } // DoEffect runs the standard tick lifetime of an effect -func DoEffect[TPeriod period.Period, TMemory any](e Effect, cs Channel[TPeriod, TMemory], p Playback, currentTick int, lastTick bool) error { +func DoEffect[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback, currentTick int, lastTick bool) error { if e == nil { return nil } diff --git a/format/it/channel/data.go b/format/it/channel/data.go index b3bd98f..bfa75a5 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -5,6 +5,7 @@ import ( "strings" itfile "github.com/gotracker/goaudiofile/music/tracked/it" + "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" @@ -13,6 +14,8 @@ import ( "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/op" + "github.com/gotracker/playback/song" ) const MaxTotalChannels = 64 @@ -161,3 +164,74 @@ func (d Data) ShortString() string { } return "..." } + +func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], d Data, s song.Data, cs playback.Channel[TPeriod, Memory, Data]) error { + var n note.Note = note.EmptyNote{} + inst := cs.GetInstrument() + prevInst := inst + + if d.HasNote() || d.HasInstrument() { + instID := d.GetInstrument(cs.GetNoteSemitone()) + n = d.GetNote() + var ( + wantRetrigger bool + wantRetriggerVol bool + ) + if instID.IsEmpty() { + // use current + inst = prevInst + wantRetrigger = true + } else if !s.IsValidInstrumentID(instID) { + out.TargetInst.Set(nil) + n = note.InvalidNote{} + } else { + var str note.Semitone + inst, str = s.GetInstrument(instID) + n = note.CoalesceNoteSemitone(n, str) + if !note.IsEmpty(n) && inst == nil { + inst = prevInst + } + wantRetrigger = true + wantRetriggerVol = true + } + + if wantRetrigger { + out.TargetInst.Set(inst) + out.TargetPos.Set(sampling.Pos{}) + if inst != nil { + if wantRetriggerVol { + out.TargetVolume.Set(inst.GetDefaultVolume()) + } + out.NoteAction.Set(note.ActionRetrigger) + out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) + } + } + } + + if note.IsInvalid(n) { + out.TargetPeriod.Reset() + out.NoteAction.Set(note.ActionCut) + } else if note.IsRelease(n) { + out.NoteAction.Set(note.ActionRelease) + } else if !note.IsEmpty(n) { + if nn, ok := n.(note.Normal); ok { + st := note.Semitone(nn) + out.TargetStoredSemitone.Set(st) + out.NoteCalcST.Set(st) + } else { + out.NoteAction.Set(note.ActionCut) + } + } + + if d.HasVolume() { + v := d.GetVolume() + if v == volume.VolumeUseInstVol { + if inst != nil { + v = inst.GetDefaultVolume() + } + } + out.TargetVolume.Set(v) + } + + return nil +} diff --git a/format/it/channel/effect_arpeggio.go b/format/it/channel/effect_arpeggio.go index 0265805..2d2c194 100644 --- a/format/it/channel/effect_arpeggio.go +++ b/format/it/channel/effect_arpeggio.go @@ -12,7 +12,7 @@ import ( type Arpeggio[TPeriod period.Period] DataEffect // 'J' // Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -20,7 +20,7 @@ func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playbac } // Tick is called on every tick -func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Arpeggio(DataEffect(e)) return doArpeggio(cs, currentTick, int8(x), int8(y)) diff --git a/format/it/channel/effect_channelvolumeslide.go b/format/it/channel/effect_channelvolumeslide.go index 6a92b9f..22783cb 100644 --- a/format/it/channel/effect_channelvolumeslide.go +++ b/format/it/channel/effect_channelvolumeslide.go @@ -13,7 +13,7 @@ import ( type ChannelVolumeSlide[TPeriod period.Period] DataEffect // 'Nxy' // Start triggers on the first tick, but before the Tick() function is called -func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() @@ -39,7 +39,7 @@ func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], } // Tick is called on every tick -func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.ChannelVolumeSlide(DataEffect(e)) diff --git a/format/it/channel/effect_extrafineportadown.go b/format/it/channel/effect_extrafineportadown.go index 96bcb0d..bb6f4fa 100644 --- a/format/it/channel/effect_extrafineportadown.go +++ b/format/it/channel/effect_extrafineportadown.go @@ -11,7 +11,7 @@ import ( type ExtraFinePortaDown[TPeriod period.Period] DataEffect // 'EEx' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/channel/effect_extrafineportaup.go b/format/it/channel/effect_extrafineportaup.go index fc2411d..782cb21 100644 --- a/format/it/channel/effect_extrafineportaup.go +++ b/format/it/channel/effect_extrafineportaup.go @@ -11,7 +11,7 @@ import ( type ExtraFinePortaUp[TPeriod period.Period] DataEffect // 'FEx' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/channel/effect_finepatterndelay.go b/format/it/channel/effect_finepatterndelay.go index 6ecf96f..224ce06 100644 --- a/format/it/channel/effect_finepatterndelay.go +++ b/format/it/channel/effect_finepatterndelay.go @@ -11,7 +11,7 @@ import ( type FinePatternDelay[TPeriod period.Period] DataEffect // 'S6x' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/it/channel/effect_fineportadown.go b/format/it/channel/effect_fineportadown.go index b12e60b..88d941c 100644 --- a/format/it/channel/effect_fineportadown.go +++ b/format/it/channel/effect_fineportadown.go @@ -11,7 +11,7 @@ import ( type FinePortaDown[TPeriod period.Period] DataEffect // 'EFx' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/channel/effect_fineportaup.go b/format/it/channel/effect_fineportaup.go index bc5562b..3a6587c 100644 --- a/format/it/channel/effect_fineportaup.go +++ b/format/it/channel/effect_fineportaup.go @@ -11,7 +11,7 @@ import ( type FinePortaUp[TPeriod period.Period] DataEffect // 'FFx' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/it/channel/effect_finevibrato.go b/format/it/channel/effect_finevibrato.go index fd5e94c..bc95b32 100644 --- a/format/it/channel/effect_finevibrato.go +++ b/format/it/channel/effect_finevibrato.go @@ -11,14 +11,14 @@ import ( type FineVibrato[TPeriod period.Period] DataEffect // 'U' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FineVibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(DataEffect(e)) if currentTick != 0 { diff --git a/format/it/channel/effect_finevolslidedown.go b/format/it/channel/effect_finevolslidedown.go index e1d2f81..84edcb8 100644 --- a/format/it/channel/effect_finevolslidedown.go +++ b/format/it/channel/effect_finevolslidedown.go @@ -11,13 +11,13 @@ import ( type FineVolumeSlideDown[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() _, y := mem.VolumeSlide(DataEffect(e)) @@ -37,7 +37,7 @@ func (e FineVolumeSlideDown[TPeriod]) String() string { type VolChanFineVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' // Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolChanFineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { mem := cs.GetMemory() y := mem.VolChanVolumeSlide(DataEffect(e)) diff --git a/format/it/channel/effect_finevolslideup.go b/format/it/channel/effect_finevolslideup.go index 79e0911..bffc446 100644 --- a/format/it/channel/effect_finevolslideup.go +++ b/format/it/channel/effect_finevolslideup.go @@ -11,13 +11,13 @@ import ( type FineVolumeSlideUp[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, _ := mem.VolumeSlide(DataEffect(e)) @@ -37,7 +37,7 @@ func (e FineVolumeSlideUp[TPeriod]) String() string { type VolChanFineVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' // Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolChanFineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { mem := cs.GetMemory() x := mem.VolChanVolumeSlide(DataEffect(e)) diff --git a/format/it/channel/effect_globalvolumeslide.go b/format/it/channel/effect_globalvolumeslide.go index 7073443..c916089 100644 --- a/format/it/channel/effect_globalvolumeslide.go +++ b/format/it/channel/effect_globalvolumeslide.go @@ -11,13 +11,13 @@ import ( type GlobalVolumeSlide[TPeriod period.Period] DataEffect // 'W' // Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.GlobalVolumeSlide(DataEffect(e)) diff --git a/format/it/channel/effect_highoffset.go b/format/it/channel/effect_highoffset.go index 2065541..7980a3c 100644 --- a/format/it/channel/effect_highoffset.go +++ b/format/it/channel/effect_highoffset.go @@ -11,7 +11,7 @@ import ( type HighOffset[TPeriod period.Period] DataEffect // 'SAx' // Start triggers on the first tick, but before the Tick() function is called -func (e HighOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e HighOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() diff --git a/format/it/channel/effect_newnoteactionnotecontinue.go b/format/it/channel/effect_newnoteactionnotecontinue.go index ac05cdb..a523d61 100644 --- a/format/it/channel/effect_newnoteactionnotecontinue.go +++ b/format/it/channel/effect_newnoteactionnotecontinue.go @@ -12,7 +12,7 @@ import ( type NewNoteActionNoteContinue[TPeriod period.Period] DataEffect // 'S74' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteContinue[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NewNoteActionNoteContinue[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.SetNewNoteAction(note.ActionContinue) return nil } diff --git a/format/it/channel/effect_newnoteactionnotecut.go b/format/it/channel/effect_newnoteactionnotecut.go index e43fc80..b99b7f9 100644 --- a/format/it/channel/effect_newnoteactionnotecut.go +++ b/format/it/channel/effect_newnoteactionnotecut.go @@ -12,7 +12,7 @@ import ( type NewNoteActionNoteCut[TPeriod period.Period] DataEffect // 'S73' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NewNoteActionNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.SetNewNoteAction(note.ActionCut) return nil } diff --git a/format/it/channel/effect_newnoteactionnotefade.go b/format/it/channel/effect_newnoteactionnotefade.go index 4c1baf7..e7bff81 100644 --- a/format/it/channel/effect_newnoteactionnotefade.go +++ b/format/it/channel/effect_newnoteactionnotefade.go @@ -12,7 +12,7 @@ import ( type NewNoteActionNoteFade[TPeriod period.Period] DataEffect // 'S76' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NewNoteActionNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.SetNewNoteAction(note.ActionFadeout) return nil } diff --git a/format/it/channel/effect_newnoteactionnoteoff.go b/format/it/channel/effect_newnoteactionnoteoff.go index 641cb1b..ff96af1 100644 --- a/format/it/channel/effect_newnoteactionnoteoff.go +++ b/format/it/channel/effect_newnoteactionnoteoff.go @@ -12,7 +12,7 @@ import ( type NewNoteActionNoteOff[TPeriod period.Period] DataEffect // 'S75' // Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NewNoteActionNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.SetNewNoteAction(note.ActionRelease) return nil } diff --git a/format/it/channel/effect_notecut.go b/format/it/channel/effect_notecut.go index a5daab4..a02ba66 100644 --- a/format/it/channel/effect_notecut.go +++ b/format/it/channel/effect_notecut.go @@ -11,13 +11,13 @@ import ( type NoteCut[TPeriod period.Period] DataEffect // 'SCx' // Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { x := DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { diff --git a/format/it/channel/effect_notedelay.go b/format/it/channel/effect_notedelay.go index c2493f9..c454f61 100644 --- a/format/it/channel/effect_notedelay.go +++ b/format/it/channel/effect_notedelay.go @@ -12,13 +12,13 @@ import ( type NoteDelay[TPeriod period.Period] DataEffect // 'SDx' // PreStart triggers when the effect enters onto the channel state -func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) return nil } // Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/channel/effect_orderjump.go b/format/it/channel/effect_orderjump.go index 45257a8..3f29cf4 100644 --- a/format/it/channel/effect_orderjump.go +++ b/format/it/channel/effect_orderjump.go @@ -12,13 +12,13 @@ import ( type OrderJump[TPeriod period.Period] DataEffect // 'B' // Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, 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[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { +func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } diff --git a/format/it/channel/effect_panningenvelopeoff.go b/format/it/channel/effect_panningenvelopeoff.go index dbf5cb8..c99e4da 100644 --- a/format/it/channel/effect_panningenvelopeoff.go +++ b/format/it/channel/effect_panningenvelopeoff.go @@ -11,7 +11,7 @@ import ( type PanningEnvelopeOff[TPeriod period.Period] DataEffect // 'S79' // Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(false) diff --git a/format/it/channel/effect_panningenvelopeon.go b/format/it/channel/effect_panningenvelopeon.go index 85359fe..8e57c6d 100644 --- a/format/it/channel/effect_panningenvelopeon.go +++ b/format/it/channel/effect_panningenvelopeon.go @@ -11,7 +11,7 @@ import ( type PanningEnvelopeOn[TPeriod period.Period] DataEffect // 'S7A' // Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPanningEnvelopeEnable(true) diff --git a/format/it/channel/effect_pastnotecut.go b/format/it/channel/effect_pastnotecut.go index 7241f5d..4276d77 100644 --- a/format/it/channel/effect_pastnotecut.go +++ b/format/it/channel/effect_pastnotecut.go @@ -12,7 +12,7 @@ import ( type PastNoteCut[TPeriod period.Period] DataEffect // 'S70' // Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PastNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionCut) return nil } diff --git a/format/it/channel/effect_pastnotefadeout.go b/format/it/channel/effect_pastnotefadeout.go index 88f4f8d..c7100ca 100644 --- a/format/it/channel/effect_pastnotefadeout.go +++ b/format/it/channel/effect_pastnotefadeout.go @@ -12,7 +12,7 @@ import ( type PastNoteFade[TPeriod period.Period] DataEffect // 'S72' // Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PastNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionFadeout) return nil } diff --git a/format/it/channel/effect_pastnoteoff.go b/format/it/channel/effect_pastnoteoff.go index aa14eac..777e2bf 100644 --- a/format/it/channel/effect_pastnoteoff.go +++ b/format/it/channel/effect_pastnoteoff.go @@ -12,7 +12,7 @@ import ( type PastNoteOff[TPeriod period.Period] DataEffect // 'S71' // Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PastNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.DoPastNoteEffect(note.ActionRelease) return nil } diff --git a/format/it/channel/effect_patterndelay.go b/format/it/channel/effect_patterndelay.go index 1cf3c49..8ae5878 100644 --- a/format/it/channel/effect_patterndelay.go +++ b/format/it/channel/effect_patterndelay.go @@ -11,13 +11,13 @@ import ( type PatternDelay[TPeriod period.Period] DataEffect // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { m := p.(IT) return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) } // Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/channel/effect_patternloop.go b/format/it/channel/effect_patternloop.go index 4ea053e..645fbfe 100644 --- a/format/it/channel/effect_patternloop.go +++ b/format/it/channel/effect_patternloop.go @@ -11,13 +11,13 @@ import ( type PatternLoop[TPeriod period.Period] DataEffect // 'SBx' // Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, 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[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { +func (e PatternLoop[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { x := uint8(e) & 0xF mem := cs.GetMemory() diff --git a/format/it/channel/effect_pitchenvelopeoff.go b/format/it/channel/effect_pitchenvelopeoff.go index e54d422..b4851b4 100644 --- a/format/it/channel/effect_pitchenvelopeoff.go +++ b/format/it/channel/effect_pitchenvelopeoff.go @@ -11,7 +11,7 @@ import ( type PitchEnvelopeOff[TPeriod period.Period] DataEffect // 'S7B' // Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(false) diff --git a/format/it/channel/effect_pitchenvelopeon.go b/format/it/channel/effect_pitchenvelopeon.go index f3ee5b3..d77bcbf 100644 --- a/format/it/channel/effect_pitchenvelopeon.go +++ b/format/it/channel/effect_pitchenvelopeon.go @@ -11,7 +11,7 @@ import ( type PitchEnvelopeOn[TPeriod period.Period] DataEffect // 'S7C' // Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetPitchEnvelopeEnable(true) diff --git a/format/it/channel/effect_portadown.go b/format/it/channel/effect_portadown.go index 0562b25..63274a3 100644 --- a/format/it/channel/effect_portadown.go +++ b/format/it/channel/effect_portadown.go @@ -11,14 +11,14 @@ import ( type PortaDown[TPeriod period.Period] DataEffect // 'E' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaDown(DataEffect(e)) diff --git a/format/it/channel/effect_portatonote.go b/format/it/channel/effect_portatonote.go index 52f1dd8..7c5d29c 100644 --- a/format/it/channel/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -13,10 +13,10 @@ import ( type PortaToNote[TPeriod period.Period] DataEffect // 'G' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { + if cmd := cs.GetChannelData(); cmd.HasNote() { cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) cs.SetNotePlayTick(false, note.ActionContinue, 0) } @@ -24,7 +24,7 @@ func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p play } // Tick is called on every tick -func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaToNote(DataEffect(e)) diff --git a/format/it/channel/effect_portaup.go b/format/it/channel/effect_portaup.go index 5643415..43a2602 100644 --- a/format/it/channel/effect_portaup.go +++ b/format/it/channel/effect_portaup.go @@ -11,14 +11,14 @@ import ( type PortaUp[TPeriod period.Period] DataEffect // 'F' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaUp(DataEffect(e)) diff --git a/format/it/channel/effect_portavolslide.go b/format/it/channel/effect_portavolslide.go index 9f610f5..18746bb 100644 --- a/format/it/channel/effect_portavolslide.go +++ b/format/it/channel/effect_portavolslide.go @@ -9,7 +9,7 @@ import ( // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide[TPeriod period.Period] struct { // 'L' - playback.CombinedEffect[TPeriod, Memory] + playback.CombinedEffect[TPeriod, Memory, Data] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/it/channel/effect_retrigvolslide.go b/format/it/channel/effect_retrigvolslide.go index 9e47cb1..040eae9 100644 --- a/format/it/channel/effect_retrigvolslide.go +++ b/format/it/channel/effect_retrigvolslide.go @@ -13,13 +13,13 @@ import ( type RetrigVolumeSlide[TPeriod period.Period] DataEffect // 'Q' // Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.RetrigVolumeSlide(DataEffect(e)) if y == 0 { diff --git a/format/it/channel/effect_rowjump.go b/format/it/channel/effect_rowjump.go index 5a12f61..759c2fd 100644 --- a/format/it/channel/effect_rowjump.go +++ b/format/it/channel/effect_rowjump.go @@ -12,13 +12,13 @@ import ( type RowJump[TPeriod period.Period] DataEffect // 'C' // Start triggers on the first tick, but before the Tick() function is called -func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, 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[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { +func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { r := DataEffect(e) rowIdx := index.Row(r) return p.SetNextRow(rowIdx) diff --git a/format/it/channel/effect_sampleoffset.go b/format/it/channel/effect_sampleoffset.go index c7eecff..7e6e253 100644 --- a/format/it/channel/effect_sampleoffset.go +++ b/format/it/channel/effect_sampleoffset.go @@ -13,7 +13,7 @@ import ( type SampleOffset[TPeriod period.Period] DataEffect // 'O' // Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(DataEffect(e)) diff --git a/format/it/channel/effect_setchannelvolume.go b/format/it/channel/effect_setchannelvolume.go index e009dc0..44e233e 100644 --- a/format/it/channel/effect_setchannelvolume.go +++ b/format/it/channel/effect_setchannelvolume.go @@ -14,7 +14,7 @@ import ( type SetChannelVolume[TPeriod period.Period] DataEffect // 'Mxx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() xx := DataEffect(e) diff --git a/format/it/channel/effect_setcoarsepanposition.go b/format/it/channel/effect_setcoarsepanposition.go index f22d60c..695cd00 100644 --- a/format/it/channel/effect_setcoarsepanposition.go +++ b/format/it/channel/effect_setcoarsepanposition.go @@ -14,7 +14,7 @@ import ( type SetCoarsePanPosition[TPeriod period.Period] DataEffect // 'S8x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/it/channel/effect_setfinetune.go b/format/it/channel/effect_setfinetune.go index 7929c39..a884f7b 100644 --- a/format/it/channel/effect_setfinetune.go +++ b/format/it/channel/effect_setfinetune.go @@ -12,7 +12,7 @@ import ( type SetFinetune[TPeriod period.Period] DataEffect // 'S2x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { x := DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,7 +24,7 @@ func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p p } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/channel/effect_setglobalvolume.go b/format/it/channel/effect_setglobalvolume.go index 64d4369..bb14853 100644 --- a/format/it/channel/effect_setglobalvolume.go +++ b/format/it/channel/effect_setglobalvolume.go @@ -13,7 +13,7 @@ import ( type SetGlobalVolume[TPeriod period.Period] DataEffect // 'V' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { v := volume.Volume(DataEffect(e)) / 0x80 if v > 1 { v = 1 @@ -23,7 +23,7 @@ func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], } // Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/channel/effect_setpanbrellowaveform.go b/format/it/channel/effect_setpanbrellowaveform.go index c3e668f..9e292d1 100644 --- a/format/it/channel/effect_setpanbrellowaveform.go +++ b/format/it/channel/effect_setpanbrellowaveform.go @@ -13,7 +13,7 @@ import ( type SetPanbrelloWaveform[TPeriod period.Period] DataEffect // 'S5x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/it/channel/effect_setpanposition.go b/format/it/channel/effect_setpanposition.go index f306bf0..7ff4586 100644 --- a/format/it/channel/effect_setpanposition.go +++ b/format/it/channel/effect_setpanposition.go @@ -14,7 +14,7 @@ import ( type SetPanPosition[TPeriod period.Period] DataEffect // 'Xxx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) diff --git a/format/it/channel/effect_setspeed.go b/format/it/channel/effect_setspeed.go index 302bcdc..067914d 100644 --- a/format/it/channel/effect_setspeed.go +++ b/format/it/channel/effect_setspeed.go @@ -11,7 +11,7 @@ import ( type SetSpeed[TPeriod period.Period] DataEffect // 'A' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { if e != 0 { m := p.(IT) if err := m.SetTicks(int(e)); err != nil { @@ -22,7 +22,7 @@ func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p play } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/it/channel/effect_settempo.go b/format/it/channel/effect_settempo.go index e36f196..38c99df 100644 --- a/format/it/channel/effect_settempo.go +++ b/format/it/channel/effect_settempo.go @@ -11,7 +11,7 @@ import ( type SetTempo[TPeriod period.Period] DataEffect // 'T' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { if e > 0x20 { m := p.(IT) if err := m.SetTempo(int(e)); err != nil { @@ -22,13 +22,13 @@ func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p play } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { m := p.(IT) switch DataEffect(e >> 4) { case 0: // decrease tempo diff --git a/format/it/channel/effect_settremolowaveform.go b/format/it/channel/effect_settremolowaveform.go index c4dd895..11ef243 100644 --- a/format/it/channel/effect_settremolowaveform.go +++ b/format/it/channel/effect_settremolowaveform.go @@ -13,7 +13,7 @@ import ( type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'S4x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/it/channel/effect_setvibratowaveform.go b/format/it/channel/effect_setvibratowaveform.go index 91b70f8..c30194e 100644 --- a/format/it/channel/effect_setvibratowaveform.go +++ b/format/it/channel/effect_setvibratowaveform.go @@ -13,7 +13,7 @@ import ( type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'S3x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/it/channel/effect_tremolo.go b/format/it/channel/effect_tremolo.go index d76e5f8..632c441 100644 --- a/format/it/channel/effect_tremolo.go +++ b/format/it/channel/effect_tremolo.go @@ -11,13 +11,13 @@ import ( type Tremolo[TPeriod period.Period] DataEffect // 'R' // Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Tremolo(DataEffect(e)) // NOTE: JBC - IT dos not update on tick 0, but MOD does. diff --git a/format/it/channel/effect_tremor.go b/format/it/channel/effect_tremor.go index 3b1671d..2e03157 100644 --- a/format/it/channel/effect_tremor.go +++ b/format/it/channel/effect_tremor.go @@ -11,13 +11,13 @@ import ( type Tremor[TPeriod period.Period] DataEffect // 'I' // Start triggers on the first tick, but before the Tick() function is called -func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Tremor(DataEffect(e)) return doTremor(cs, currentTick, int(x)+1, int(y)+1) diff --git a/format/it/channel/effect_vibrato.go b/format/it/channel/effect_vibrato.go index b8e8b84..7b26db1 100644 --- a/format/it/channel/effect_vibrato.go +++ b/format/it/channel/effect_vibrato.go @@ -11,14 +11,14 @@ import ( type Vibrato[TPeriod period.Period] DataEffect // 'H' // Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(DataEffect(e)) if mem.Shared.OldEffectMode { diff --git a/format/it/channel/effect_vibratovolslide.go b/format/it/channel/effect_vibratovolslide.go index 84f3972..323bace 100644 --- a/format/it/channel/effect_vibratovolslide.go +++ b/format/it/channel/effect_vibratovolslide.go @@ -9,7 +9,7 @@ import ( // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide[TPeriod period.Period] struct { // 'K' - playback.CombinedEffect[TPeriod, Memory] + playback.CombinedEffect[TPeriod, Memory, Data] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/it/channel/effect_volslidedown.go b/format/it/channel/effect_volslidedown.go index 77f9ebd..a613f52 100644 --- a/format/it/channel/effect_volslidedown.go +++ b/format/it/channel/effect_volslidedown.go @@ -11,13 +11,13 @@ import ( type VolumeSlideDown[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() _, y := mem.VolumeSlide(DataEffect(e)) @@ -34,7 +34,7 @@ func (e VolumeSlideDown[TPeriod]) String() string { type VolChanVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' // Tick is called on every tick -func (e VolChanVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e VolChanVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() y := mem.VolChanVolumeSlide(DataEffect(e)) diff --git a/format/it/channel/effect_volslideup.go b/format/it/channel/effect_volslideup.go index 76223f9..764f52d 100644 --- a/format/it/channel/effect_volslideup.go +++ b/format/it/channel/effect_volslideup.go @@ -11,13 +11,13 @@ import ( type VolumeSlideUp[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, _ := mem.VolumeSlide(DataEffect(e)) @@ -34,7 +34,7 @@ func (e VolumeSlideUp[TPeriod]) String() string { type VolChanVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' // Tick is called on every tick -func (e VolChanVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e VolChanVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x := mem.VolChanVolumeSlide(DataEffect(e)) diff --git a/format/it/channel/effect_volumeenvelopeoff.go b/format/it/channel/effect_volumeenvelopeoff.go index 13a569a..c5da35e 100644 --- a/format/it/channel/effect_volumeenvelopeoff.go +++ b/format/it/channel/effect_volumeenvelopeoff.go @@ -11,7 +11,7 @@ import ( type VolumeEnvelopeOff[TPeriod period.Period] DataEffect // 'S77' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(false) diff --git a/format/it/channel/effect_volumeenvelopeon.go b/format/it/channel/effect_volumeenvelopeon.go index 6853cd9..49f63e6 100644 --- a/format/it/channel/effect_volumeenvelopeon.go +++ b/format/it/channel/effect_volumeenvelopeon.go @@ -11,7 +11,7 @@ import ( type VolumeEnvelopeOn[TPeriod period.Period] DataEffect // 'S78' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.SetVolumeEnvelopeEnable(true) diff --git a/format/it/channel/effectfactory.go b/format/it/channel/effectfactory.go index 1b61b0b..b59181e 100644 --- a/format/it/channel/effectfactory.go +++ b/format/it/channel/effectfactory.go @@ -12,7 +12,7 @@ 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, Memory] + playback.CombinedEffect[TPeriod, Memory, Data] eff EffectIT } diff --git a/format/it/channel/unhandled.go b/format/it/channel/unhandled.go index 92dac96..4b9a265 100644 --- a/format/it/channel/unhandled.go +++ b/format/it/channel/unhandled.go @@ -14,7 +14,7 @@ type UnhandledCommand[TPeriod period.Period] struct { } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m IT) error { +func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: ce:%0.2X cp:%0.2X", e.Command, e.Info)) } @@ -37,7 +37,7 @@ type UnhandledVolCommand[TPeriod period.Period] struct { } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m IT) error { +func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m IT) error { if !m.IgnoreUnknownEffect() { panic(fmt.Sprintf("unhandled command: volCmd:%0.2X", e.Vol)) } diff --git a/format/it/channel/util.go b/format/it/channel/util.go index c105975..7d75ba5 100644 --- a/format/it/channel/util.go +++ b/format/it/channel/util.go @@ -30,7 +30,7 @@ type IT interface { IgnoreUnknownEffect() bool // Unhandled } -func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta float32, multiplier float32) error { +func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := itVolume.ToItVolume(av) vol := int16((float32(v) + delta) * multiplier) @@ -62,7 +62,7 @@ func doGlobalVolSlide(m IT, delta float32, multiplier float32) error { return nil } -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur.IsInvalid() { return nil @@ -74,7 +74,7 @@ func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amou return nil } -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { return err } @@ -84,7 +84,7 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory] return nil } -func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { +func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur.IsInvalid() { return nil @@ -96,7 +96,7 @@ func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], am return nil } -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { if err := doPortaDown(cs, amount, multiplier); err != nil { return err } @@ -106,7 +106,7 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memor return nil } -func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { +func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) d := period.Delta(vib) @@ -114,7 +114,7 @@ func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], curr return nil } -func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, onTicks int, offTicks int) error { +func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -130,7 +130,7 @@ func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], curre return nil } -func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -155,7 +155,7 @@ var ( } ) -func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory]) error { +func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data]) error { vol := itVolume.ToItVolume(cs.GetActiveVolume()) if vol >= 0x10 && vol <= 0x50 { vol -= 0x10 @@ -174,13 +174,13 @@ func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Mem return nil } -func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { +func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) return doVolSlide(cs, delta, 1.0) } -func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { delta := o.GetWave(float32(depth) * multiplier) o.Advance(int(speed)) return delta diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index 204d209..947c5aa 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -32,7 +32,7 @@ type manager[TPeriod period.Period] struct { song *layout.Song - channels []state.ChannelState[TPeriod, channel.Memory] + channels []state.ChannelState[TPeriod, channel.Memory, channel.Data] PastNotes state.PastNotesProcessor[TPeriod] pattern pattern.State @@ -50,8 +50,8 @@ var it system.System = itSystem.ITSystem var _ playback.Playback = (*manager[period.Linear])(nil) var _ playback.Playback = (*manager[period.Amiga])(nil) -var _ playback.Channel[period.Linear, channel.Memory] = (*state.ChannelState[period.Linear, channel.Memory])(nil) -var _ playback.Channel[period.Amiga, channel.Memory] = (*state.ChannelState[period.Amiga, channel.Memory])(nil) +var _ playback.Channel[period.Linear, channel.Memory, channel.Data] = (*state.ChannelState[period.Linear, channel.Memory, channel.Data])(nil) +var _ playback.Channel[period.Amiga, channel.Memory, channel.Data] = (*state.ChannelState[period.Amiga, channel.Memory, channel.Data])(nil) func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.PeriodConverter[TPeriod]) error { m.Tracker.BaseClockRate = it.GetBaseClock() @@ -135,7 +135,7 @@ func (m *manager[TPeriod]) GetNumChannels() int { return len(m.channels) } -func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[TPeriod]) state.NoteOp[TPeriod, channel.Memory] { +func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[TPeriod]) state.NoteOp[TPeriod, channel.Memory, channel.Data] { return doNoteCalc[TPeriod]{ Semitone: st, UpdateFunc: fn, @@ -144,7 +144,7 @@ func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.Peri // SetNumChannels updates the song to have the specified number of channels and resets their states func (m *manager[TPeriod]) SetNumChannels(num int, periodConverter period.PeriodConverter[TPeriod]) { - m.channels = make([]state.ChannelState[TPeriod, channel.Memory], num) + m.channels = make([]state.ChannelState[TPeriod, channel.Memory, channel.Data], num) m.PastNotes.SetMax(channel.MaxTotalChannels - num) for ch := range m.channels { @@ -156,7 +156,7 @@ func (m *manager[TPeriod]) SetNumChannels(num int, periodConverter period.Period cs.PortaTargetPeriod.Reset() cs.Trigger.Reset() cs.RetriggerCount = 0 - _ = cs.SetData(nil) + _ = cs.SetData(channel.Data{}) ocNum := m.song.GetRenderChannel(ch) cs.RenderChannel = m.GetRenderChannel(ocNum, m.channelInit) @@ -347,7 +347,7 @@ func (m *manager[TPeriod]) GetSongData() song.Data { } // GetChannel returns the channel interface for the specified channel number -func (m *manager[TPeriod]) GetChannel(ch int) *state.ChannelState[TPeriod, channel.Memory] { +func (m *manager[TPeriod]) GetChannel(ch int) *state.ChannelState[TPeriod, channel.Memory, channel.Data] { return &m.channels[ch] } diff --git a/format/it/playback/playback_command.go b/format/it/playback/playback_command.go index 93c8fdc..b926115 100644 --- a/format/it/playback/playback_command.go +++ b/format/it/playback/playback_command.go @@ -15,7 +15,7 @@ type doNoteCalc[TPeriod period.Period] struct { UpdateFunc state.PeriodUpdateFunc[TPeriod] } -func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState[TPeriod, channel.Memory]) error { +func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data]) error { if o.UpdateFunc == nil { return nil } @@ -29,7 +29,7 @@ func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState return nil } -func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, channel.Memory], currentTick int, lastTick bool) error { +func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, 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 @@ -54,7 +54,7 @@ func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, return nil } -func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod, channel.Memory], currentTick int, lastTick bool) error { +func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data], currentTick int, lastTick bool) error { targetTick, noteAction := cs.WillTriggerOn(currentTick) if !targetTick { return nil @@ -107,7 +107,7 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod return nil } -func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TPeriod, channel.Memory], currentTick int, lastTick bool) error { +func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data], currentTick int, lastTick bool) error { if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index 9d47045..42b8bd3 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -87,7 +87,7 @@ func (m *manager[TPeriod]) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory]()) + cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory](channel.GetTargetsFromData[TPeriod])) if resetMemory { mem := cs.GetMemory() mem.StartOrder() @@ -142,7 +142,7 @@ func (m *manager[TPeriod]) processPatternRow() error { return nil } -func (m *manager[TPeriod]) processRowForChannel(cs *state.ChannelState[TPeriod, channel.Memory]) error { +func (m *manager[TPeriod]) processRowForChannel(cs *state.ChannelState[TPeriod, channel.Memory, channel.Data]) error { mem := cs.GetMemory() mem.TremorMem().Reset() diff --git a/format/it/playback/playback_textoutput.go b/format/it/playback/playback_textoutput.go index 7123fde..ea0248c 100644 --- a/format/it/playback/playback_textoutput.go +++ b/format/it/playback/playback_textoutput.go @@ -19,9 +19,7 @@ func (m *manager[TPeriod]) getRowText() *render.RowDisplay[channel.Data] { continue } - if data := cs.GetData(); data != nil { - rowText.Channels[ch], _ = data.(channel.Data) - } + rowText.Channels[ch] = cs.GetChannelData() } return &rowText } diff --git a/format/it/playback/playback_tracing.go b/format/it/playback/playback_tracing.go index e7bcb76..75de935 100644 --- a/format/it/playback/playback_tracing.go +++ b/format/it/playback/playback_tracing.go @@ -101,9 +101,7 @@ func (m *manager[TPeriod]) outputChannelsTrace() func(w io.Writer) { if len(effects) == 0 { effects = []playback.Effect{nil} } - if cdata := ch.GetData(); cdata != nil { - trackData = fmt.Sprint(cdata) - } + trackData = fmt.Sprint(ch.GetChannelData()) for _, effect := range effects { var ( activeEffect string diff --git a/format/s3m/channel/data.go b/format/s3m/channel/data.go index cb52d3d..9f632d8 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -5,6 +5,7 @@ import ( "strings" s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" @@ -12,6 +13,8 @@ import ( "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/op" + "github.com/gotracker/playback/song" ) // DataEffect is the type of a channel's EffectParameter value @@ -123,3 +126,74 @@ func NoteFromS3MNote(sn s3mfile.Note) note.Note { } return note.InvalidNote{} } + +func GetTargetsFromData(out *op.ChannelTargets[period.Amiga], d Data, s song.Data, cs playback.Channel[period.Amiga, Memory, Data]) error { + var n note.Note = note.EmptyNote{} + inst := cs.GetInstrument() + prevInst := inst + + if d.HasNote() || d.HasInstrument() { + instID := d.GetInstrument(cs.GetNoteSemitone()) + n = d.GetNote() + var ( + wantRetrigger bool + wantRetriggerVol bool + ) + if instID.IsEmpty() { + // use current + inst = prevInst + wantRetrigger = true + } else if !s.IsValidInstrumentID(instID) { + out.TargetInst.Set(nil) + n = note.InvalidNote{} + } else { + var str note.Semitone + inst, str = s.GetInstrument(instID) + n = note.CoalesceNoteSemitone(n, str) + if !note.IsEmpty(n) && inst == nil { + inst = prevInst + } + wantRetrigger = true + wantRetriggerVol = true + } + + if wantRetrigger { + out.TargetInst.Set(inst) + out.TargetPos.Set(sampling.Pos{}) + if inst != nil { + if wantRetriggerVol { + out.TargetVolume.Set(inst.GetDefaultVolume()) + } + out.NoteAction.Set(note.ActionRetrigger) + out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) + } + } + } + + if note.IsInvalid(n) { + out.TargetPeriod.Reset() + out.NoteAction.Set(note.ActionCut) + } else if note.IsRelease(n) { + out.NoteAction.Set(note.ActionRelease) + } else if !note.IsEmpty(n) { + if nn, ok := n.(note.Normal); ok { + st := note.Semitone(nn) + out.TargetStoredSemitone.Set(st) + out.NoteCalcST.Set(st) + } else { + out.NoteAction.Set(note.ActionCut) + } + } + + if d.HasVolume() { + v := d.GetVolume() + if v == volume.VolumeUseInstVol { + if inst != nil { + v = inst.GetDefaultVolume() + } + } + out.TargetVolume.Set(v) + } + + return nil +} diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 9136f35..2472ad2 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -16,7 +16,7 @@ type PortaToNote ChannelCommand // 'G' func (e PortaToNote) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { + if cmd := cs.GetChannelData(); cmd.HasNote() { cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) cs.SetNotePlayTick(false, note.ActionContinue, 0) } diff --git a/format/s3m/channel/effect_portavolslide.go b/format/s3m/channel/effect_portavolslide.go index c63d636..294b3df 100644 --- a/format/s3m/channel/effect_portavolslide.go +++ b/format/s3m/channel/effect_portavolslide.go @@ -9,7 +9,7 @@ import ( // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide struct { // 'L' - playback.CombinedEffect[period.Amiga, Memory] + playback.CombinedEffect[period.Amiga, Memory, Data] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/s3m/channel/effect_vibratovolslide.go b/format/s3m/channel/effect_vibratovolslide.go index e83faf9..3972f03 100644 --- a/format/s3m/channel/effect_vibratovolslide.go +++ b/format/s3m/channel/effect_vibratovolslide.go @@ -9,7 +9,7 @@ import ( // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide struct { // 'K' - playback.CombinedEffect[period.Amiga, Memory] + playback.CombinedEffect[period.Amiga, Memory, Data] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/s3m/channel/util.go b/format/s3m/channel/util.go index 4264a17..40b00aa 100644 --- a/format/s3m/channel/util.go +++ b/format/s3m/channel/util.go @@ -14,7 +14,7 @@ import ( ) type EffectS3M = playback.Effect -type S3MChannel = playback.Channel[period.Amiga, Memory] +type S3MChannel = playback.Channel[period.Amiga, Memory, Data] // S3M is an interface to S3M effect operations type S3M interface { diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index 92e825e..c390323 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -24,7 +24,7 @@ import ( "github.com/gotracker/playback/system" ) -type channelState = state.ChannelState[period.Amiga, channel.Memory] +type channelState = state.ChannelState[period.Amiga, channel.Memory, channel.Data] // manager is a playback manager for S3M music type manager struct { @@ -46,7 +46,7 @@ type manager struct { } var _ playback.Playback = (*manager)(nil) -var _ playback.Channel[period.Amiga, channel.Memory] = (*state.ChannelState[period.Amiga, channel.Memory])(nil) +var _ playback.Channel[period.Amiga, channel.Memory, channel.Data] = (*state.ChannelState[period.Amiga, channel.Memory, channel.Data])(nil) var s3m system.System = s3mSystem.S3MSystem // NewManager creates a new manager for an S3M song @@ -142,7 +142,7 @@ func (m *manager) GetNumChannels() int { return len(m.channels) } -func (m *manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[period.Amiga]) state.NoteOp[period.Amiga, channel.Memory] { +func (m *manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[period.Amiga]) state.NoteOp[period.Amiga, channel.Memory, channel.Data] { return doNoteCalc{ Semitone: st, UpdateFunc: fn, @@ -161,7 +161,7 @@ func (m *manager) SetNumChannels(num int) { cs.PortaTargetPeriod.Reset() cs.Trigger.Reset() cs.RetriggerCount = 0 - _ = cs.SetData(nil) + _ = cs.SetData(channel.Data{}) ocNum := m.song.GetRenderChannel(ch) cs.RenderChannel = m.GetRenderChannel(ocNum, m.channelInit) } diff --git a/format/s3m/playback/playback_command.go b/format/s3m/playback/playback_command.go index 4742b0b..b87ff2b 100644 --- a/format/s3m/playback/playback_command.go +++ b/format/s3m/playback/playback_command.go @@ -56,10 +56,7 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last if !triggerTick { return nil } - var n note.Note = note.EmptyNote{} - if cs.GetData() != nil { - n = cs.GetData().GetNote() - } + n := cs.GetChannelData().GetNote() keyOn := false keyOff := false stop := false diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index f633b8d..f11eca0 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -88,7 +88,7 @@ func (m *manager) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(state.NewChannelDataTxn[period.Amiga, channel.Memory]()) + cs.AdvanceRow(state.NewChannelDataTxn[period.Amiga, channel.Memory, channel.Data](channel.GetTargetsFromData)) if resetMemory { mem := cs.GetMemory() mem.StartOrder() diff --git a/format/s3m/playback/playback_textoutput.go b/format/s3m/playback/playback_textoutput.go index 2a4387e..00ac663 100644 --- a/format/s3m/playback/playback_textoutput.go +++ b/format/s3m/playback/playback_textoutput.go @@ -19,9 +19,7 @@ func (m *manager) getRowText() *render.RowDisplay[channel.Data] { continue } - if data := cs.GetData(); data != nil { - rowText.Channels[ch], _ = data.(channel.Data) - } + rowText.Channels[ch] = cs.GetChannelData() } return &rowText } diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index fa6f2c1..23360b3 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -5,6 +5,7 @@ import ( "strings" xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" + "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" @@ -13,6 +14,8 @@ import ( "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/op" + "github.com/gotracker/playback/song" ) type Command uint8 @@ -159,3 +162,74 @@ func (d Data) ShortString() string { } return "..." } + +func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], d Data, s song.Data, cs playback.Channel[TPeriod, Memory, Data]) error { + var n note.Note = note.EmptyNote{} + inst := cs.GetInstrument() + prevInst := inst + + if d.HasNote() || d.HasInstrument() { + instID := d.GetInstrument(cs.GetNoteSemitone()) + n = d.GetNote() + var ( + wantRetrigger bool + wantRetriggerVol bool + ) + if instID.IsEmpty() { + // use current + inst = prevInst + wantRetrigger = true + } else if !s.IsValidInstrumentID(instID) { + out.TargetInst.Set(nil) + n = note.InvalidNote{} + } else { + var str note.Semitone + inst, str = s.GetInstrument(instID) + n = note.CoalesceNoteSemitone(n, str) + if !note.IsEmpty(n) && inst == nil { + inst = prevInst + } + wantRetrigger = true + wantRetriggerVol = true + } + + if wantRetrigger { + out.TargetInst.Set(inst) + out.TargetPos.Set(sampling.Pos{}) + if inst != nil { + if wantRetriggerVol { + out.TargetVolume.Set(inst.GetDefaultVolume()) + } + out.NoteAction.Set(note.ActionRetrigger) + out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) + } + } + } + + if note.IsInvalid(n) { + out.TargetPeriod.Reset() + out.NoteAction.Set(note.ActionCut) + } else if note.IsRelease(n) { + out.NoteAction.Set(note.ActionRelease) + } else if !note.IsEmpty(n) { + if nn, ok := n.(note.Normal); ok { + st := note.Semitone(nn) + out.TargetStoredSemitone.Set(st) + out.NoteCalcST.Set(st) + } else { + out.NoteAction.Set(note.ActionCut) + } + } + + if d.HasVolume() { + v := d.GetVolume() + if v == volume.VolumeUseInstVol { + if inst != nil { + v = inst.GetDefaultVolume() + } + } + out.TargetVolume.Set(v) + } + + return nil +} diff --git a/format/xm/channel/effect_arpeggio.go b/format/xm/channel/effect_arpeggio.go index 8a25939..d41ce76 100644 --- a/format/xm/channel/effect_arpeggio.go +++ b/format/xm/channel/effect_arpeggio.go @@ -11,7 +11,7 @@ import ( type Arpeggio[TPeriod period.Period] DataEffect // '0' // Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() cs.SetPos(cs.GetTargetPos()) @@ -19,7 +19,7 @@ func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playbac } // Tick is called on every tick -func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { xy := DataEffect(e) if xy == 0 { return nil diff --git a/format/xm/channel/effect_extrafineportadown.go b/format/xm/channel/effect_extrafineportadown.go index b01ad1d..238bf68 100644 --- a/format/xm/channel/effect_extrafineportadown.go +++ b/format/xm/channel/effect_extrafineportadown.go @@ -11,7 +11,7 @@ import ( type ExtraFinePortaDown[TPeriod period.Period] DataEffect // 'X2x' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/channel/effect_extrafineportaup.go b/format/xm/channel/effect_extrafineportaup.go index e20587d..c507a48 100644 --- a/format/xm/channel/effect_extrafineportaup.go +++ b/format/xm/channel/effect_extrafineportaup.go @@ -11,7 +11,7 @@ import ( type ExtraFinePortaUp[TPeriod period.Period] DataEffect // 'X1x' // Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/channel/effect_fineportadown.go b/format/xm/channel/effect_fineportadown.go index 9ce05ba..90600b0 100644 --- a/format/xm/channel/effect_fineportadown.go +++ b/format/xm/channel/effect_fineportadown.go @@ -11,7 +11,7 @@ import ( type FinePortaDown[TPeriod period.Period] DataEffect // 'E2x' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/channel/effect_fineportaup.go b/format/xm/channel/effect_fineportaup.go index 51ca598..a1cc6c2 100644 --- a/format/xm/channel/effect_fineportaup.go +++ b/format/xm/channel/effect_fineportaup.go @@ -11,7 +11,7 @@ import ( type FinePortaUp[TPeriod period.Period] DataEffect // 'E1x' // Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() diff --git a/format/xm/channel/effect_finevolslidedown.go b/format/xm/channel/effect_finevolslidedown.go index 2213efa..73fb653 100644 --- a/format/xm/channel/effect_finevolslidedown.go +++ b/format/xm/channel/effect_finevolslidedown.go @@ -11,7 +11,7 @@ import ( type FineVolumeSlideDown[TPeriod period.Period] DataEffect // 'EAx' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() diff --git a/format/xm/channel/effect_finevolslideup.go b/format/xm/channel/effect_finevolslideup.go index 0312536..424f8b1 100644 --- a/format/xm/channel/effect_finevolslideup.go +++ b/format/xm/channel/effect_finevolslideup.go @@ -11,7 +11,7 @@ import ( type FineVolumeSlideUp[TPeriod period.Period] DataEffect // 'EAx' // Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() diff --git a/format/xm/channel/effect_globalvolumeslide.go b/format/xm/channel/effect_globalvolumeslide.go index 3a7303f..9f71c6f 100644 --- a/format/xm/channel/effect_globalvolumeslide.go +++ b/format/xm/channel/effect_globalvolumeslide.go @@ -11,13 +11,13 @@ import ( type GlobalVolumeSlide[TPeriod period.Period] DataEffect // 'H' // Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.GlobalVolumeSlide(DataEffect(e)) diff --git a/format/xm/channel/effect_notecut.go b/format/xm/channel/effect_notecut.go index 728d860..8e7f729 100644 --- a/format/xm/channel/effect_notecut.go +++ b/format/xm/channel/effect_notecut.go @@ -11,13 +11,13 @@ import ( type NoteCut[TPeriod period.Period] DataEffect // 'ECx' // Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { x := DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { diff --git a/format/xm/channel/effect_notedelay.go b/format/xm/channel/effect_notedelay.go index 6856e2f..527db11 100644 --- a/format/xm/channel/effect_notedelay.go +++ b/format/xm/channel/effect_notedelay.go @@ -12,13 +12,13 @@ import ( type NoteDelay[TPeriod period.Period] DataEffect // 'EDx' // PreStart triggers when the effect enters onto the channel state -func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) return nil } // Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/channel/effect_orderjump.go b/format/xm/channel/effect_orderjump.go index 45257a8..3f29cf4 100644 --- a/format/xm/channel/effect_orderjump.go +++ b/format/xm/channel/effect_orderjump.go @@ -12,13 +12,13 @@ import ( type OrderJump[TPeriod period.Period] DataEffect // 'B' // Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, 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[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { +func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { return p.SetNextOrder(index.Order(e)) } diff --git a/format/xm/channel/effect_panslide.go b/format/xm/channel/effect_panslide.go index 4fb3ee2..0f2aa3f 100644 --- a/format/xm/channel/effect_panslide.go +++ b/format/xm/channel/effect_panslide.go @@ -12,7 +12,7 @@ import ( type PanSlide[TPeriod period.Period] DataEffect // 'Pxx' // Start triggers on the first tick, but before the Tick() function is called -func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { xx := DataEffect(e) x := xx >> 4 y := xx & 0x0F diff --git a/format/xm/channel/effect_patterndelay.go b/format/xm/channel/effect_patterndelay.go index f50a28e..3953504 100644 --- a/format/xm/channel/effect_patterndelay.go +++ b/format/xm/channel/effect_patterndelay.go @@ -11,13 +11,13 @@ import ( type PatternDelay[TPeriod period.Period] DataEffect // 'SEx' // PreStart triggers when the effect enters onto the channel state -func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { m := p.(XM) return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) } // Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/channel/effect_patternloop.go b/format/xm/channel/effect_patternloop.go index 61a7cb2..c272497 100644 --- a/format/xm/channel/effect_patternloop.go +++ b/format/xm/channel/effect_patternloop.go @@ -11,7 +11,7 @@ import ( type PatternLoop[TPeriod period.Period] DataEffect // 'E6x' // Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xF diff --git a/format/xm/channel/effect_portadown.go b/format/xm/channel/effect_portadown.go index 0353e42..974fc75 100644 --- a/format/xm/channel/effect_portadown.go +++ b/format/xm/channel/effect_portadown.go @@ -11,14 +11,14 @@ import ( type PortaDown[TPeriod period.Period] DataEffect // '2' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaDown(DataEffect(e)) diff --git a/format/xm/channel/effect_portatonote.go b/format/xm/channel/effect_portatonote.go index 63364b2..e001a7d 100644 --- a/format/xm/channel/effect_portatonote.go +++ b/format/xm/channel/effect_portatonote.go @@ -13,10 +13,10 @@ import ( type PortaToNote[TPeriod period.Period] DataEffect // '3' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - if cmd := cs.GetData(); cmd != nil && cmd.HasNote() { + if cmd := cs.GetChannelData(); cmd.HasNote() { cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) cs.SetNotePlayTick(false, note.ActionContinue, 0) } @@ -24,7 +24,7 @@ func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p play } // Tick is called on every tick -func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { if currentTick == 0 { return nil } diff --git a/format/xm/channel/effect_portaup.go b/format/xm/channel/effect_portaup.go index 8ff08cc..5e35063 100644 --- a/format/xm/channel/effect_portaup.go +++ b/format/xm/channel/effect_portaup.go @@ -11,14 +11,14 @@ import ( type PortaUp[TPeriod period.Period] DataEffect // '1' // Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() xx := mem.PortaUp(DataEffect(e)) diff --git a/format/xm/channel/effect_portavolslide.go b/format/xm/channel/effect_portavolslide.go index 352cf2d..576031f 100644 --- a/format/xm/channel/effect_portavolslide.go +++ b/format/xm/channel/effect_portavolslide.go @@ -9,7 +9,7 @@ import ( // PortaVolumeSlide defines a portamento-to-note combined with a volume slide effect type PortaVolumeSlide[TPeriod period.Period] struct { // '5' - playback.CombinedEffect[TPeriod, Memory] + playback.CombinedEffect[TPeriod, Memory, Data] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object diff --git a/format/xm/channel/effect_retriggernote.go b/format/xm/channel/effect_retriggernote.go index 7f7a886..e29e00c 100644 --- a/format/xm/channel/effect_retriggernote.go +++ b/format/xm/channel/effect_retriggernote.go @@ -13,13 +13,13 @@ import ( type RetriggerNote[TPeriod period.Period] DataEffect // 'E9x' // Start triggers on the first tick, but before the Tick() function is called -func (e RetriggerNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e RetriggerNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { y := DataEffect(e) & 0x0F if y == 0 { return nil diff --git a/format/xm/channel/effect_retrigvolslide.go b/format/xm/channel/effect_retrigvolslide.go index 0307650..950cdbf 100644 --- a/format/xm/channel/effect_retrigvolslide.go +++ b/format/xm/channel/effect_retrigvolslide.go @@ -13,13 +13,13 @@ import ( type RetrigVolumeSlide[TPeriod period.Period] DataEffect // 'R' // Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { x := DataEffect(e) >> 4 y := DataEffect(e) & 0x0F if y == 0 { diff --git a/format/xm/channel/effect_rowjump.go b/format/xm/channel/effect_rowjump.go index 4b4cb59..d7d57e2 100644 --- a/format/xm/channel/effect_rowjump.go +++ b/format/xm/channel/effect_rowjump.go @@ -12,13 +12,13 @@ import ( type RowJump[TPeriod period.Period] DataEffect // 'D' // Start triggers on the first tick, but before the Tick() function is called -func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, 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[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory], p playback.Playback, lastTick int) error { +func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { xy := DataEffect(e) x := xy >> 4 y := xy & 0x0f diff --git a/format/xm/channel/effect_sampleoffset.go b/format/xm/channel/effect_sampleoffset.go index ba19746..7567c57 100644 --- a/format/xm/channel/effect_sampleoffset.go +++ b/format/xm/channel/effect_sampleoffset.go @@ -13,7 +13,7 @@ import ( type SampleOffset[TPeriod period.Period] DataEffect // '9' // Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(DataEffect(e)) diff --git a/format/xm/channel/effect_setcoarsepanposition.go b/format/xm/channel/effect_setcoarsepanposition.go index 48b5e9b..27cdd2e 100644 --- a/format/xm/channel/effect_setcoarsepanposition.go +++ b/format/xm/channel/effect_setcoarsepanposition.go @@ -12,7 +12,7 @@ import ( type SetCoarsePanPosition[TPeriod period.Period] DataEffect // 'E8x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() xy := DataEffect(e) diff --git a/format/xm/channel/effect_setenvelopeposition.go b/format/xm/channel/effect_setenvelopeposition.go index 21ed626..7fc46fb 100644 --- a/format/xm/channel/effect_setenvelopeposition.go +++ b/format/xm/channel/effect_setenvelopeposition.go @@ -11,7 +11,7 @@ import ( type SetEnvelopePosition[TPeriod period.Period] DataEffect // 'Lxx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetEnvelopePosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetEnvelopePosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() xx := DataEffect(e) diff --git a/format/xm/channel/effect_setfinetune.go b/format/xm/channel/effect_setfinetune.go index f109399..a4ed198 100644 --- a/format/xm/channel/effect_setfinetune.go +++ b/format/xm/channel/effect_setfinetune.go @@ -12,7 +12,7 @@ import ( type SetFinetune[TPeriod period.Period] DataEffect // 'E5x' // PreStart triggers when the effect enters onto the channel state -func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { x := DataEffect(e) & 0xf inst := cs.GetTargetInst() @@ -24,7 +24,7 @@ func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p p } // Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/channel/effect_setglobalvolume.go b/format/xm/channel/effect_setglobalvolume.go index 825ce1b..2564ec6 100644 --- a/format/xm/channel/effect_setglobalvolume.go +++ b/format/xm/channel/effect_setglobalvolume.go @@ -12,14 +12,14 @@ import ( type SetGlobalVolume[TPeriod period.Period] DataEffect // 'G' // PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, 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[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/channel/effect_setpanposition.go b/format/xm/channel/effect_setpanposition.go index 39563af..9e43e91 100644 --- a/format/xm/channel/effect_setpanposition.go +++ b/format/xm/channel/effect_setpanposition.go @@ -12,7 +12,7 @@ import ( type SetPanPosition[TPeriod period.Period] DataEffect // '8xx' // Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() xx := uint8(e) diff --git a/format/xm/channel/effect_setspeed.go b/format/xm/channel/effect_setspeed.go index 37740f6..5d2b489 100644 --- a/format/xm/channel/effect_setspeed.go +++ b/format/xm/channel/effect_setspeed.go @@ -11,7 +11,7 @@ import ( type SetSpeed[TPeriod period.Period] DataEffect // 'F' // PreStart triggers when the effect enters onto the channel state -func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { if e != 0 { m := p.(XM) if err := m.SetTicks(int(e)); err != nil { @@ -22,7 +22,7 @@ func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p play } // Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } diff --git a/format/xm/channel/effect_settempo.go b/format/xm/channel/effect_settempo.go index 2a51f21..e5eb2b3 100644 --- a/format/xm/channel/effect_settempo.go +++ b/format/xm/channel/effect_settempo.go @@ -11,7 +11,7 @@ import ( type SetTempo[TPeriod period.Period] DataEffect // 'F' // PreStart triggers when the effect enters onto the channel state -func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { if e > 0x20 { m := p.(XM) if err := m.SetTempo(int(e)); err != nil { @@ -22,13 +22,13 @@ func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], p play } // Start triggers on the first tick, but before the Tick() function is called -func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetTempo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { m := p.(XM) return m.SetTempo(int(e)) } diff --git a/format/xm/channel/effect_settremolowaveform.go b/format/xm/channel/effect_settremolowaveform.go index 2651061..aa8e6a8 100644 --- a/format/xm/channel/effect_settremolowaveform.go +++ b/format/xm/channel/effect_settremolowaveform.go @@ -13,7 +13,7 @@ import ( type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'E7x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/xm/channel/effect_setvibratowaveform.go b/format/xm/channel/effect_setvibratowaveform.go index 8547ad2..7429d69 100644 --- a/format/xm/channel/effect_setvibratowaveform.go +++ b/format/xm/channel/effect_setvibratowaveform.go @@ -13,7 +13,7 @@ import ( type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'E4x' // Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() x := DataEffect(e) & 0xf diff --git a/format/xm/channel/effect_setvolume.go b/format/xm/channel/effect_setvolume.go index 0f7b9ff..948ba90 100644 --- a/format/xm/channel/effect_setvolume.go +++ b/format/xm/channel/effect_setvolume.go @@ -12,7 +12,7 @@ import ( type SetVolume[TPeriod period.Period] DataEffect // 'C' // Start triggers on the first tick, but before the Tick() function is called -func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() xx := xmVolume.XmVolume(e) diff --git a/format/xm/channel/effect_tremolo.go b/format/xm/channel/effect_tremolo.go index 4272454..ae1985f 100644 --- a/format/xm/channel/effect_tremolo.go +++ b/format/xm/channel/effect_tremolo.go @@ -11,13 +11,13 @@ import ( type Tremolo[TPeriod period.Period] DataEffect // '7' // Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Tremolo(DataEffect(e)) // NOTE: JBC - XM updates on tick 0, but MOD does not. diff --git a/format/xm/channel/effect_tremor.go b/format/xm/channel/effect_tremor.go index 8210e13..8e76075 100644 --- a/format/xm/channel/effect_tremor.go +++ b/format/xm/channel/effect_tremor.go @@ -11,13 +11,13 @@ import ( type Tremor[TPeriod period.Period] DataEffect // 'T' // Start triggers on the first tick, but before the Tick() function is called -func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { if currentTick != 0 { mem := cs.GetMemory() x, y := mem.Tremor(DataEffect(e)) diff --git a/format/xm/channel/effect_vibrato.go b/format/xm/channel/effect_vibrato.go index 3e72976..85f91a0 100644 --- a/format/xm/channel/effect_vibrato.go +++ b/format/xm/channel/effect_vibrato.go @@ -11,14 +11,14 @@ import ( type Vibrato[TPeriod period.Period] DataEffect // '4' // Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() return nil } // Tick is called on every tick -func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.Vibrato(DataEffect(e)) // NOTE: JBC - XM updates on tick 0, but MOD does not. diff --git a/format/xm/channel/effect_vibratovolslide.go b/format/xm/channel/effect_vibratovolslide.go index 237c1f5..7b52194 100644 --- a/format/xm/channel/effect_vibratovolslide.go +++ b/format/xm/channel/effect_vibratovolslide.go @@ -9,7 +9,7 @@ import ( // VibratoVolumeSlide defines a combination vibrato and volume slide effect type VibratoVolumeSlide[TPeriod period.Period] struct { // '6' - playback.CombinedEffect[TPeriod, Memory] + playback.CombinedEffect[TPeriod, Memory, Data] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object diff --git a/format/xm/channel/effect_volslide.go b/format/xm/channel/effect_volslide.go index 1b24508..732a96d 100644 --- a/format/xm/channel/effect_volslide.go +++ b/format/xm/channel/effect_volslide.go @@ -11,13 +11,13 @@ import ( type VolumeSlide[TPeriod period.Period] DataEffect // 'A' // Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory], p playback.Playback) error { +func (e VolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() return nil } // Tick is called on every tick -func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory], p playback.Playback, currentTick int) error { +func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { mem := cs.GetMemory() x, y := mem.VolumeSlide(DataEffect(e)) diff --git a/format/xm/channel/effectfactory.go b/format/xm/channel/effectfactory.go index df7efae..8f9d12d 100644 --- a/format/xm/channel/effectfactory.go +++ b/format/xm/channel/effectfactory.go @@ -12,7 +12,7 @@ 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, Memory] + playback.CombinedEffect[TPeriod, Memory, Data] eff EffectXM } diff --git a/format/xm/channel/unhandled.go b/format/xm/channel/unhandled.go index 1311bcd..e60ddc8 100644 --- a/format/xm/channel/unhandled.go +++ b/format/xm/channel/unhandled.go @@ -15,7 +15,7 @@ type UnhandledCommand[TPeriod period.Period] struct { } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m XM) error { +func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } @@ -38,7 +38,7 @@ type UnhandledVolCommand[TPeriod period.Period] struct { } // PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory], m XM) error { +func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m XM) error { if !m.IgnoreUnknownEffect() { panic("unhandled command") } diff --git a/format/xm/channel/util.go b/format/xm/channel/util.go index be3fa34..2c8db29 100644 --- a/format/xm/channel/util.go +++ b/format/xm/channel/util.go @@ -28,7 +28,7 @@ type XM interface { IgnoreUnknownEffect() bool // Unhandled } -func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], delta float32, multiplier float32) error { +func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], delta float32, multiplier float32) error { av := cs.GetActiveVolume() v := xmVolume.ToVolumeXM(av) vol := int16((float32(v) + delta) * multiplier) @@ -58,7 +58,7 @@ func doGlobalVolSlide(m XM, delta float32, multiplier float32) error { return nil } -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { +func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur.IsInvalid() { return nil @@ -70,7 +70,7 @@ func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amou return nil } -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { +func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { if err := doPortaUp(cs, amount, multiplier); err != nil { return err } @@ -80,7 +80,7 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory] return nil } -func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32) error { +func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { cur := cs.GetPeriod() if cur.IsInvalid() { return nil @@ -92,7 +92,7 @@ func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], am return nil } -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], amount float32, multiplier float32, target TPeriod) error { +func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { if err := doPortaDown(cs, amount, multiplier); err != nil { return err } @@ -102,7 +102,7 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memor return nil } -func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { +func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) delta := period.Delta(vib) @@ -110,7 +110,7 @@ func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], curr return nil } -func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, onTicks int, offTicks int) error { +func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -126,7 +126,7 @@ func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], curre return nil } -func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -151,7 +151,7 @@ var ( } ) -func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory]) error { +func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data]) error { vol := xmVolume.ToVolumeXM(cs.GetActiveVolume()) if vol >= 64 { vol = 63 @@ -166,13 +166,13 @@ func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Mem return nil } -func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { +func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) return doVolSlide(cs, delta, 1.0) } -func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { +func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { delta := o.GetWave(float32(depth) * multiplier) o.Advance(int(speed)) return delta diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index 0df003b..7b46ba5 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -29,7 +29,7 @@ type manager[TPeriod period.Period] struct { song *layout.Song - channels []state.ChannelState[TPeriod, channel.Memory] + channels []state.ChannelState[TPeriod, channel.Memory, channel.Data] pattern pattern.State preMixRowTxn *playpattern.RowUpdateTransaction @@ -42,8 +42,8 @@ type manager[TPeriod period.Period] struct { var _ playback.Playback = (*manager[period.Linear])(nil) var _ playback.Playback = (*manager[period.Amiga])(nil) -var _ playback.Channel[period.Linear, channel.Memory] = (*state.ChannelState[period.Linear, channel.Memory])(nil) -var _ playback.Channel[period.Amiga, channel.Memory] = (*state.ChannelState[period.Amiga, channel.Memory])(nil) +var _ playback.Channel[period.Linear, channel.Memory, channel.Data] = (*state.ChannelState[period.Linear, channel.Memory, channel.Data])(nil) +var _ playback.Channel[period.Amiga, channel.Memory, channel.Data] = (*state.ChannelState[period.Amiga, channel.Memory, channel.Data])(nil) func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.PeriodConverter[TPeriod]) error { m.Tracker.BaseClockRate = xmPeriod.XMBaseClock @@ -133,7 +133,7 @@ func (m *manager[TPeriod]) GetNumChannels() int { return len(m.channels) } -func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[TPeriod]) state.NoteOp[TPeriod, channel.Memory] { +func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[TPeriod]) state.NoteOp[TPeriod, channel.Memory, channel.Data] { return doNoteCalc[TPeriod]{ Semitone: st, UpdateFunc: fn, @@ -142,7 +142,7 @@ func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.Peri // SetNumChannels updates the song to have the specified number of channels and resets their states func (m *manager[TPeriod]) SetNumChannels(num int, periodConverter period.PeriodConverter[TPeriod]) { - m.channels = make([]state.ChannelState[TPeriod, channel.Memory], num) + m.channels = make([]state.ChannelState[TPeriod, channel.Memory, channel.Data], num) for ch := range m.channels { cs := &m.channels[ch] @@ -153,7 +153,7 @@ func (m *manager[TPeriod]) SetNumChannels(num int, periodConverter period.Period cs.PortaTargetPeriod.Reset() cs.Trigger.Reset() cs.RetriggerCount = 0 - _ = cs.SetData(nil) + _ = cs.SetData(channel.Data{}) ocNum := m.song.GetRenderChannel(ch) cs.RenderChannel = m.GetRenderChannel(ocNum, m.channelInit) } @@ -317,7 +317,7 @@ func (m *manager[TPeriod]) GetSongData() song.Data { } // GetChannel returns the channel interface for the specified channel number -func (m *manager[TPeriod]) GetChannel(ch int) *state.ChannelState[TPeriod, channel.Memory] { +func (m *manager[TPeriod]) GetChannel(ch int) *state.ChannelState[TPeriod, channel.Memory, channel.Data] { return &m.channels[ch] } diff --git a/format/xm/playback/playback_command.go b/format/xm/playback/playback_command.go index 152e84b..8e6672c 100644 --- a/format/xm/playback/playback_command.go +++ b/format/xm/playback/playback_command.go @@ -17,7 +17,7 @@ type doNoteCalc[TPeriod period.Period] struct { UpdateFunc state.PeriodUpdateFunc[TPeriod] } -func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState[TPeriod, channel.Memory]) error { +func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data]) error { if o.UpdateFunc == nil { return nil } @@ -30,7 +30,7 @@ func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState return nil } -func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, channel.Memory], currentTick int, lastTick bool) error { +func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, 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 @@ -54,11 +54,8 @@ func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, return nil } -func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod, channel.Memory], currentTick int, lastTick bool) error { - var n note.Note = note.EmptyNote{} - if cs.GetData() != nil { - n = cs.GetData().GetNote() - } +func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data], currentTick int, lastTick bool) error { + n := cs.GetChannelData().GetNote() keyOff := false keyOn := false if nc := cs.GetVoice(); nc != nil { @@ -114,7 +111,7 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod return nil } -func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TPeriod, channel.Memory], currentTick int, lastTick bool) error { +func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data], currentTick int, lastTick bool) error { if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index e27277a..02e4e67 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -87,7 +87,7 @@ func (m *manager[TPeriod]) processPatternRow() error { for ch := range m.channels { cs := &m.channels[ch] - cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory]()) + cs.AdvanceRow(state.NewChannelDataTxn[TPeriod, channel.Memory](channel.GetTargetsFromData[TPeriod])) if resetMemory { mem := cs.GetMemory() mem.StartOrder() @@ -142,7 +142,7 @@ func (m *manager[TPeriod]) processPatternRow() error { return nil } -func (m *manager[TPeriod]) processRowForChannel(cs *state.ChannelState[TPeriod, channel.Memory]) error { +func (m *manager[TPeriod]) processRowForChannel(cs *state.ChannelState[TPeriod, channel.Memory, channel.Data]) error { mem := cs.GetMemory() mem.TremorMem().Reset() diff --git a/format/xm/playback/playback_textoutput.go b/format/xm/playback/playback_textoutput.go index 4e6ec0f..cdadafa 100644 --- a/format/xm/playback/playback_textoutput.go +++ b/format/xm/playback/playback_textoutput.go @@ -19,9 +19,7 @@ func (m *manager[TPeriod]) getRowText() *render.RowDisplay[channel.Data] { continue } - if data := cs.GetData(); data != nil { - rowText.Channels[ch], _ = data.(channel.Data) - } + rowText.Channels[ch] = cs.GetChannelData() } return &rowText } diff --git a/player/op/channeltargets.go b/player/op/channeltargets.go new file mode 100644 index 0000000..b0d1ea0 --- /dev/null +++ b/player/op/channeltargets.go @@ -0,0 +1,22 @@ +package op + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/heucuva/optional" +) + +type ChannelTargets[TPeriod period.Period] struct { + NoteAction optional.Value[note.Action] + NoteCalcST optional.Value[note.Semitone] + + TargetPos optional.Value[sampling.Pos] + TargetInst optional.Value[*instrument.Instrument] + TargetPeriod optional.Value[TPeriod] + TargetStoredSemitone optional.Value[note.Semitone] + TargetNewNoteAction optional.Value[note.Action] + TargetVolume optional.Value[volume.Volume] +} diff --git a/player/state/channel.go b/player/state/channel.go index 6d09b10..06775fe 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -22,20 +22,20 @@ type NoteTrigger struct { Tick int } -type VolOp[TPeriod period.Period, TMemory any] interface { - Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error +type VolOp[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData]) error } -type NoteOp[TPeriod period.Period, TMemory any] interface { - Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error +type NoteOp[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData]) error } type PeriodUpdateFunc[TPeriod period.Period] func(TPeriod) -type SemitoneSetterFactory[TPeriod period.Period, TMemory any] func(note.Semitone, PeriodUpdateFunc[TPeriod]) NoteOp[TPeriod, TMemory] +type SemitoneSetterFactory[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(note.Semitone, PeriodUpdateFunc[TPeriod]) NoteOp[TPeriod, TMemory, TChannelData] // ChannelState is the state of a single channel -type ChannelState[TPeriod period.Period, TMemory any] struct { +type ChannelState[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { activeState Active[TPeriod] targetState Playback[TPeriod] prevState Active[TPeriod] @@ -43,10 +43,10 @@ type ChannelState[TPeriod period.Period, TMemory any] struct { ActiveEffects []playback.Effect s song.Data - txn ChannelDataTransaction[TPeriod, TMemory] - prevTxn ChannelDataTransaction[TPeriod, TMemory] + txn ChannelDataTransaction[TPeriod, TMemory, TChannelData] + prevTxn ChannelDataTransaction[TPeriod, TMemory, TChannelData] - SemitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory] + SemitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData] StoredSemitone note.Semitone // from pattern, unmodified, current note PortaTargetPeriod optional.Value[TPeriod] @@ -69,7 +69,7 @@ type ChannelState[TPeriod period.Period, TMemory any] struct { } // WillTriggerOn returns true if a note will trigger on the tick specified -func (cs *ChannelState[TPeriod, TMemory]) WillTriggerOn(tick int) (bool, note.Action) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) WillTriggerOn(tick int) (bool, note.Action) { if trigger, ok := cs.Trigger.Get(); ok { return trigger.Tick == tick, trigger.NoteAction } @@ -78,7 +78,7 @@ func (cs *ChannelState[TPeriod, TMemory]) WillTriggerOn(tick int) (bool, note.Ac } // AdvanceRow will update the current state to make room for the next row's state data -func (cs *ChannelState[TPeriod, TMemory]) AdvanceRow(txn ChannelDataTransaction[TPeriod, TMemory]) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) AdvanceRow(txn ChannelDataTransaction[TPeriod, TMemory, TChannelData]) { cs.prevState = cs.activeState cs.targetState = cs.activeState.Playback cs.Trigger.Reset() @@ -91,7 +91,7 @@ func (cs *ChannelState[TPeriod, TMemory]) AdvanceRow(txn ChannelDataTransaction[ } // RenderRowTick renders a channel's row data for a single tick -func (cs *ChannelState[TPeriod, TMemory]) RenderRowTick(details RenderDetails, pastNotes []*Active[TPeriod]) ([]mixing.Data, error) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) RenderRowTick(details RenderDetails, pastNotes []*Active[TPeriod]) ([]mixing.Data, error) { if cs.PlaybackFrozen() { return nil, nil } @@ -102,76 +102,77 @@ func (cs *ChannelState[TPeriod, TMemory]) RenderRowTick(details RenderDetails, p } // ResetStates resets the channel's internal states -func (cs *ChannelState[TPeriod, TMemory]) ResetStates() { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) ResetStates() { cs.activeState.Reset() cs.targetState.Reset() cs.prevState.Reset() } -func (cs *ChannelState[TPeriod, TMemory]) GetActiveEffects() []playback.Effect { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetActiveEffects() []playback.Effect { return cs.ActiveEffects } -func (cs *ChannelState[TPeriod, TMemory]) SetActiveEffects(effects []playback.Effect) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetActiveEffects(effects []playback.Effect) { cs.ActiveEffects = effects } // FreezePlayback suspends mixer progression on the channel -func (cs *ChannelState[TPeriod, TMemory]) FreezePlayback() { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) FreezePlayback() { cs.freezePlayback = true } // UnfreezePlayback resumes mixer progression on the channel -func (cs *ChannelState[TPeriod, TMemory]) UnfreezePlayback() { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) UnfreezePlayback() { cs.freezePlayback = false } // PlaybackFrozen returns true if the mixer progression for the channel is suspended -func (cs ChannelState[TPeriod, TMemory]) PlaybackFrozen() bool { +func (cs ChannelState[TPeriod, TMemory, TChannelData]) PlaybackFrozen() bool { return cs.freezePlayback } // ResetRetriggerCount sets the retrigger count to 0 -func (cs *ChannelState[TPeriod, TMemory]) ResetRetriggerCount() { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) ResetRetriggerCount() { cs.RetriggerCount = 0 } // GetMemory returns the interface to the custom effect memory module -func (cs *ChannelState[TPeriod, TMemory]) GetMemory() *TMemory { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetMemory() *TMemory { return cs.Memory } // SetMemory sets the custom effect memory interface -func (cs *ChannelState[TPeriod, TMemory]) SetMemory(mem *TMemory) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetMemory(mem *TMemory) { cs.Memory = mem } // GetActiveVolume returns the current active volume on the channel -func (cs *ChannelState[TPeriod, TMemory]) GetActiveVolume() volume.Volume { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetActiveVolume() volume.Volume { return cs.activeState.Volume } // SetActiveVolume sets the active volume on the channel -func (cs *ChannelState[TPeriod, TMemory]) SetActiveVolume(vol volume.Volume) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetActiveVolume(vol volume.Volume) { if vol != volume.VolumeUseInstVol { cs.activeState.Volume = vol } } -func (cs *ChannelState[TPeriod, TMemory]) SetSongDataInterface(s song.Data) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetSongDataInterface(s song.Data) { cs.s = s } -// GetData returns the interface to the current channel song pattern data -func (cs *ChannelState[TPeriod, TMemory]) GetData() song.ChannelData { +// GetChannelData returns the interface to the current channel song pattern data +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetChannelData() TChannelData { if cs.txn == nil { - return nil + var empty TChannelData + return empty } - return cs.txn.GetData() + return cs.txn.GetChannelData() } -func (cs *ChannelState[TPeriod, TMemory]) SetData(cdata song.ChannelData) error { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetData(cdata TChannelData) error { if cs.txn == nil { return nil } @@ -179,12 +180,12 @@ func (cs *ChannelState[TPeriod, TMemory]) SetData(cdata song.ChannelData) error return cs.txn.SetData(cdata, cs.s, cs) } -func (cs *ChannelState[TPeriod, TMemory]) GetTxn() ChannelDataTransaction[TPeriod, TMemory] { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTxn() ChannelDataTransaction[TPeriod, TMemory, TChannelData] { return cs.txn } // GetPortaTargetPeriod returns the current target portamento (to note) sampler period -func (cs *ChannelState[TPeriod, TMemory]) GetPortaTargetPeriod() TPeriod { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPortaTargetPeriod() TPeriod { if p, ok := cs.PortaTargetPeriod.Get(); ok { return p } @@ -193,7 +194,7 @@ func (cs *ChannelState[TPeriod, TMemory]) GetPortaTargetPeriod() TPeriod { } // SetPortaTargetPeriod sets the current target portamento (to note) sampler period -func (cs *ChannelState[TPeriod, TMemory]) SetPortaTargetPeriod(period TPeriod) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPortaTargetPeriod(period TPeriod) { if !period.IsInvalid() { cs.PortaTargetPeriod.Set(period) } else { @@ -202,48 +203,48 @@ func (cs *ChannelState[TPeriod, TMemory]) SetPortaTargetPeriod(period TPeriod) { } // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) GetTargetPeriod() TPeriod { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTargetPeriod() TPeriod { return cs.targetState.Period } // SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) SetTargetPeriod(period TPeriod) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetPeriod(period TPeriod) { cs.targetState.Period = period } // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) GetPeriodOverride() TPeriod { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriodOverride() TPeriod { return cs.periodOverride } // SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory]) SetPeriodOverride(period TPeriod) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPeriodOverride(period TPeriod) { cs.periodOverride = period cs.UsePeriodOverride = true } // SetPeriodDelta sets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TPeriod, TMemory]) SetPeriodDelta(delta period.Delta) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPeriodDelta(delta period.Delta) { cs.activeState.PeriodDelta = delta } // GetPeriodDelta gets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TPeriod, TMemory]) GetPeriodDelta() period.Delta { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriodDelta() period.Delta { return cs.activeState.PeriodDelta } // SetVolumeActive enables or disables the sample of the instrument -func (cs *ChannelState[TPeriod, TMemory]) SetVolumeActive(on bool) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetVolumeActive(on bool) { cs.volumeActive = on } // GetInstrument returns the interface to the active instrument -func (cs *ChannelState[TPeriod, TMemory]) GetInstrument() *instrument.Instrument { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetInstrument() *instrument.Instrument { return cs.activeState.Instrument } // SetInstrument sets the interface to the active instrument -func (cs *ChannelState[TPeriod, TMemory]) SetInstrument(inst *instrument.Instrument) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetInstrument(inst *instrument.Instrument) { cs.activeState.Instrument = inst if inst != nil { if inst == cs.prevState.Instrument { @@ -255,67 +256,67 @@ func (cs *ChannelState[TPeriod, TMemory]) SetInstrument(inst *instrument.Instrum } // GetVoice returns the active voice interface -func (cs *ChannelState[TPeriod, TMemory]) GetVoice() voice.Voice { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) GetTargetInst() *instrument.Instrument { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) SetTargetInst(inst *instrument.Instrument) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetInst(inst *instrument.Instrument) { cs.targetState.Instrument = inst } // GetPrevInst returns the interface to the last row's active instrument -func (cs *ChannelState[TPeriod, TMemory]) GetPrevInst() *instrument.Instrument { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPrevInst() *instrument.Instrument { return cs.prevState.Instrument } // GetPrevVoice returns the interface to the last row's active voice -func (cs *ChannelState[TPeriod, TMemory]) GetPrevVoice() voice.Voice { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPrevVoice() voice.Voice { return cs.prevState.Voice } // GetNoteSemitone returns the note semitone for the channel -func (cs *ChannelState[TPeriod, TMemory]) GetNoteSemitone() note.Semitone { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetNoteSemitone() note.Semitone { return cs.StoredSemitone } // GetTargetPos returns the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TPeriod, TMemory]) GetTargetPos() sampling.Pos { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTargetPos() sampling.Pos { return cs.targetState.Pos } // SetTargetPos sets the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TPeriod, TMemory]) SetTargetPos(pos sampling.Pos) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetPos(pos sampling.Pos) { cs.targetState.Pos = pos } // GetPeriod returns the current sampler period of the active instrument -func (cs *ChannelState[TPeriod, TMemory]) GetPeriod() TPeriod { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriod() TPeriod { return cs.activeState.Period } // SetPeriod sets the current sampler period of the active instrument -func (cs *ChannelState[TPeriod, TMemory]) SetPeriod(period TPeriod) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPeriod(period TPeriod) { cs.activeState.Period = period } // GetPos returns the sample position of the active instrument -func (cs *ChannelState[TPeriod, TMemory]) GetPos() sampling.Pos { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPos() sampling.Pos { return cs.activeState.Pos } // SetPos sets the sample position of the active instrument -func (cs *ChannelState[TPeriod, TMemory]) SetPos(pos sampling.Pos) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPos(pos sampling.Pos) { cs.activeState.Pos = pos } // SetNotePlayTick sets the tick on which the note will retrigger -func (cs *ChannelState[TPeriod, TMemory]) SetNotePlayTick(enabled bool, action note.Action, tick int) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetNotePlayTick(enabled bool, action note.Action, tick int) { if enabled { cs.Trigger.Set(NoteTrigger{ NoteAction: action, @@ -327,78 +328,78 @@ func (cs *ChannelState[TPeriod, TMemory]) SetNotePlayTick(enabled bool, action n } // GetRetriggerCount returns the current count of the retrigger counter -func (cs *ChannelState[TPeriod, TMemory]) GetRetriggerCount() uint8 { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetRetriggerCount() uint8 { return cs.RetriggerCount } // SetRetriggerCount sets the current count of the retrigger counter -func (cs *ChannelState[TPeriod, TMemory]) SetRetriggerCount(cnt uint8) { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) SetPanEnabled(on bool) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPanEnabled(on bool) { cs.PanEnabled = on } // SetPan sets the active panning value of the channel -func (cs *ChannelState[TPeriod, TMemory]) SetPan(pan panning.Position) { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) GetPan() panning.Position { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPan() panning.Position { return cs.activeState.Pan } // SetTargetSemitone sets the target semitone for the channel -func (cs *ChannelState[TPeriod, TMemory]) SetTargetSemitone(st note.Semitone) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetSemitone(st note.Semitone) { if cs.txn != nil { cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, cs.SetTargetPeriod)) } } -func (cs *ChannelState[TPeriod, TMemory]) SetOverrideSemitone(st note.Semitone) { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) SetStoredSemitone(st note.Semitone) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetStoredSemitone(st note.Semitone) { cs.StoredSemitone = st } // SetRenderChannel sets the output channel for the channel -func (cs *ChannelState[TPeriod, TMemory]) SetRenderChannel(outputCh *render.Channel) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetRenderChannel(outputCh *render.Channel) { cs.RenderChannel = outputCh } // GetRenderChannel returns the output channel for the channel -func (cs *ChannelState[TPeriod, TMemory]) GetRenderChannel() *render.Channel { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetRenderChannel() *render.Channel { return cs.RenderChannel } // SetGlobalVolume sets the last-known global volume on the channel -func (cs *ChannelState[TPeriod, TMemory]) SetGlobalVolume(gv volume.Volume) { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) SetChannelVolume(cv volume.Volume) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetChannelVolume(cv volume.Volume) { cs.RenderChannel.ChannelVolume = cv } // GetChannelVolume gets the channel volume on the channel -func (cs *ChannelState[TPeriod, TMemory]) GetChannelVolume() volume.Volume { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetChannelVolume() volume.Volume { return cs.RenderChannel.ChannelVolume } // SetEnvelopePosition sets the envelope position for the active instrument -func (cs *ChannelState[TPeriod, TMemory]) SetEnvelopePosition(ticks int) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetEnvelopePosition(ticks int) { if nc := cs.GetVoice(); nc != nil { voice.SetVolumeEnvelopePosition(nc, ticks) voice.SetPanEnvelopePosition(nc, ticks) @@ -409,7 +410,7 @@ func (cs *ChannelState[TPeriod, TMemory]) SetEnvelopePosition(ticks int) { // TransitionActiveToPastState will transition the current active state to the 'past' state // and will activate the specified New-Note Action on it -func (cs *ChannelState[TPeriod, TMemory]) TransitionActiveToPastState() { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) TransitionActiveToPastState() { if cs.PastNotes != nil { switch cs.NewNoteAction { case note.ActionCut: @@ -442,36 +443,36 @@ func (cs *ChannelState[TPeriod, TMemory]) TransitionActiveToPastState() { } // DoPastNoteEffect performs an action on all past-note playbacks associated with the channel -func (cs *ChannelState[TPeriod, TMemory]) DoPastNoteEffect(action note.Action) { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) SetNewNoteAction(nna note.Action) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetNewNoteAction(nna note.Action) { cs.NewNoteAction = nna } // GetNewNoteAction gets the New-Note Action on the channel -func (cs *ChannelState[TPeriod, TMemory]) GetNewNoteAction() note.Action { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetNewNoteAction() note.Action { return cs.NewNoteAction } // SetVolumeEnvelopeEnable sets the enable flag on the active volume envelope -func (cs *ChannelState[TPeriod, TMemory]) SetVolumeEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetVolumeEnvelopeEnable(enabled bool) { voice.EnableVolumeEnvelope(cs.activeState.Voice, enabled) } // SetPanningEnvelopeEnable sets the enable flag on the active panning envelope -func (cs *ChannelState[TPeriod, TMemory]) SetPanningEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory]) SetPitchEnvelopeEnable(enabled bool) { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPitchEnvelopeEnable(enabled bool) { voice.EnablePitchEnvelope[TPeriod](cs.activeState.Voice, enabled) } -func (cs *ChannelState[TPeriod, TMemory]) NoteCut() { +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) NoteCut() { var empty TPeriod cs.activeState.Period = empty } diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index b9ffbc1..bdd9c45 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -1,70 +1,57 @@ 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/player/op" "github.com/gotracker/playback/song" - "github.com/heucuva/optional" ) -type ChannelDataTransaction[TPeriod period.Period, TMemory any] interface { - GetData() song.ChannelData - SetData(data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error +type ChannelDataTransaction[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { + GetChannelData() TChannelData + SetData(data TChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory, TChannelData]) error - CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error - CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error - CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error + CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error + CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error + CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error - CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error - CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error + CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error + CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error + CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - AddVolOp(op VolOp[TPeriod, TMemory]) - AddNoteOp(op NoteOp[TPeriod, TMemory]) + AddVolOp(op VolOp[TPeriod, TMemory, TChannelData]) + AddNoteOp(op NoteOp[TPeriod, TMemory, TChannelData]) } -type ChannelDataActions[TPeriod period.Period] struct { - NoteAction optional.Value[note.Action] - NoteCalcST optional.Value[note.Semitone] +type ChannelDataConverter[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(out *op.ChannelTargets[TPeriod], data TChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory, TChannelData]) error - TargetPos optional.Value[sampling.Pos] - TargetInst optional.Value[*instrument.Instrument] - TargetPeriod optional.Value[TPeriod] - TargetStoredSemitone optional.Value[note.Semitone] - TargetNewNoteAction optional.Value[note.Action] - TargetVolume optional.Value[volume.Volume] -} - -type ChannelDataConverter[TPeriod period.Period, TMemory any] func(out *ChannelDataActions[TPeriod], data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error - -type ChannelDataTxnHelper[TPeriod period.Period, TMemory any] struct { - Data song.ChannelData +type ChannelDataTxnHelper[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { + Data TChannelData + Targeter playback.ChannelTargeter[TPeriod, TMemory, TChannelData] - ChannelDataActions[TPeriod] + op.ChannelTargets[TPeriod] - VolOps []VolOp[TPeriod, TMemory] - NoteOps []NoteOp[TPeriod, TMemory] + VolOps []VolOp[TPeriod, TMemory, TChannelData] + NoteOps []NoteOp[TPeriod, TMemory, TChannelData] } -func NewChannelDataTxn[TPeriod period.Period, TMemory any]() ChannelDataTransaction[TPeriod, TMemory] { - return &ChannelDataTxnHelper[TPeriod, TMemory]{} +func NewChannelDataTxn[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](targeter playback.ChannelTargeter[TPeriod, TMemory, TChannelData]) ChannelDataTransaction[TPeriod, TMemory, TChannelData] { + return &ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]{ + Targeter: targeter, + } } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) GetData() song.ChannelData { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) GetChannelData() TChannelData { return d.Data } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) SetData(cd song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) SetData(cd TChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory, TChannelData]) error { d.Data = cd - return d.ProcessData(&d.ChannelDataActions, cd, s, cs) + return d.Targeter(&d.ChannelTargets, cd, s, cs) } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { - effects := playback.GetEffects[TPeriod, TMemory](cs.GetMemory(), d.Data) +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { + effects := playback.GetEffects[TPeriod, TMemory, TChannelData](cs.GetMemory(), d.Data) cs.SetActiveEffects(effects) onEff := p.GetOnEffect() for _, e := range effects { @@ -79,7 +66,7 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreRow(p playback.Playbac return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { if pos, ok := d.TargetPos.Get(); ok { cs.SetTargetPos(pos) } @@ -116,11 +103,11 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitRow(p playback.Playback, return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { // pre-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -132,7 +119,7 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPreTick(p playback.Playba return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { for _, e := range cs.GetActiveEffects() { if err := playback.DoEffect[TPeriod, TMemory](e, cs, p, currentTick, lastTick); err != nil { return err @@ -142,7 +129,7 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitTick(p playback.Playback, return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { // post-effect if err := d.ProcessVolOps(p, cs); err != nil { return err @@ -154,11 +141,11 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) CommitPostTick(p playback.Playb return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) AddVolOp(op VolOp[TPeriod, TMemory]) { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) AddVolOp(op VolOp[TPeriod, TMemory, TChannelData]) { d.VolOps = append(d.VolOps, op) } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessVolOps(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) ProcessVolOps(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData]) error { for _, op := range d.VolOps { if op == nil { continue @@ -172,11 +159,11 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessVolOps(p playback.Playba return nil } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) AddNoteOp(op NoteOp[TPeriod, TMemory]) { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) AddNoteOp(op NoteOp[TPeriod, TMemory, TChannelData]) { d.NoteOps = append(d.NoteOps, op) } -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TPeriod, TMemory]) error { +func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData]) error { for _, op := range d.NoteOps { if op == nil { continue @@ -189,78 +176,3 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessNoteOps(p playback.Playb return nil } - -func (d *ChannelDataTxnHelper[TPeriod, TMemory]) ProcessData(out *ChannelDataActions[TPeriod], data song.ChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory]) error { - if data == nil { - return nil - } - - var n note.Note = note.EmptyNote{} - inst := cs.GetInstrument() - prevInst := inst - - if data.HasNote() || data.HasInstrument() { - instID := data.GetInstrument(cs.StoredSemitone) - n = data.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = prevInst - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - if !note.IsEmpty(n) && inst == nil { - inst = prevInst - } - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) - } - out.NoteAction.Set(note.ActionRetrigger) - out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) - } - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Reset() - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else if !note.IsEmpty(n) { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } else { - out.NoteAction.Set(note.ActionCut) - } - } - - if data.HasVolume() { - v := data.GetVolume() - if v == volume.VolumeUseInstVol { - if inst != nil { - v = inst.GetDefaultVolume() - } - } - out.TargetVolume.Set(v) - } - - return nil -} diff --git a/song/channel.go b/song/channeldata.go similarity index 100% rename from song/channel.go rename to song/channeldata.go From 7f4ea9b504163abd89de248bcd5484693cf0d1f4 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 15:36:53 -0800 Subject: [PATCH 19/63] clean up envelope code - fix issue were instrument envelopes might not get setup properly --- format/it/load/instrument.go | 3 +++ oscillator/impulsetracker.go | 16 ++++++++++------ oscillator/protracker.go | 16 ++++++++++------ player/voice/opl2.go | 6 +++--- player/voice/pcm.go | 11 +++++------ voice/component/envelope.go | 3 ++- voice/component/envelope_filter.go | 12 ++++++++---- voice/component/envelope_pan.go | 12 ++++++++---- voice/component/envelope_pitch.go | 12 ++++++++---- voice/component/envelope_volume.go | 12 ++++++++---- voice/component/modulator_freq.go | 11 ++++++----- voice/envelope/envelope.go | 18 +++++++++++------- voice/oscillator/oscillator.go | 3 ++- 13 files changed, 84 insertions(+), 51 deletions(-) diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 208500e..433f7d4 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -234,6 +234,9 @@ func convertPitchEnvValue(v int8) int8 { 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 } diff --git a/oscillator/impulsetracker.go b/oscillator/impulsetracker.go index 576495d..8dc508f 100644 --- a/oscillator/impulsetracker.go +++ b/oscillator/impulsetracker.go @@ -115,13 +115,17 @@ func (o *impulseOscillator) SetWaveform(table oscillator.WaveTableSelect) { } // 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 9e226d5..c074fcb 100644 --- a/oscillator/protracker.go +++ b/oscillator/protracker.go @@ -82,13 +82,17 @@ func (o *protrackerOscillator) SetWaveform(table oscillator.WaveTableSelect) { } // 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/player/voice/opl2.go b/player/voice/opl2.go index 6ba6e64..fa1f180 100644 --- a/player/voice/opl2.go +++ b/player/voice/opl2.go @@ -72,9 +72,9 @@ func NewOPL2[TPeriod period.Period](config OPLConfiguration[TPeriod]) voice.Voic v.amp.Setup(1) v.amp.ResetFadeoutValue(0) v.volEnv.SetEnabled(false) - v.volEnv.Reset(nil) + v.volEnv.Reset() v.pitchEnv.SetEnabled(false) - v.pitchEnv.Reset(nil) + v.pitchEnv.Reset() regs.Mod.Reg20 = d.Modulator.GetReg20() regs.Mod.Reg40 = d.Modulator.GetReg40() regs.Mod.Reg60 = d.Modulator.GetReg60() @@ -96,7 +96,7 @@ func NewOPL2[TPeriod period.Period](config OPLConfiguration[TPeriod]) voice.Voic v.freq.SetAutoVibratoEnabled(config.AutoVibrato.Enabled) if config.AutoVibrato.Enabled { v.freq.ConfigureAutoVibrato(config.AutoVibrato) - v.freq.ResetAutoVibrato(config.AutoVibrato.Sweep) + v.freq.ResetAutoVibratoAndSweep(config.AutoVibrato.Sweep) } return &v diff --git a/player/voice/pcm.go b/player/voice/pcm.go index fbe937b..a31770d 100644 --- a/player/voice/pcm.go +++ b/player/voice/pcm.go @@ -91,19 +91,18 @@ func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPerio v.filterEnvActive = d.PitchFiltMode v.sampler.Setup(d.Sample, d.Loop, d.SustainLoop) v.fadeoutMode = d.FadeOut.Mode - v.amp.ResetFadeoutValue(d.FadeOut.Amount) //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.volEnv.Init(&d.VolEnv) v.pitchEnv.SetEnabled(d.PitchFiltEnv.Enabled) - v.pitchEnv.Reset(&d.PitchFiltEnv) + v.pitchEnv.Init(&d.PitchFiltEnv) v.panEnv.SetEnabled(d.PanEnv.Enabled) - v.panEnv.Reset(&d.PanEnv) + v.panEnv.Init(&d.PanEnv) v.filterEnv.SetEnabled(d.PitchFiltEnv.Enabled) - v.filterEnv.Reset(&d.PitchFiltEnv) + v.filterEnv.Init(&d.PitchFiltEnv) v.channels = d.Sample.Channels() } @@ -120,7 +119,7 @@ func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPerio v.freq.SetAutoVibratoEnabled(config.AutoVibrato.Enabled) if config.AutoVibrato.Enabled { v.freq.ConfigureAutoVibrato(config.AutoVibrato) - v.freq.ResetAutoVibrato(config.AutoVibrato.Sweep) + v.freq.ResetAutoVibratoAndSweep(config.AutoVibrato.Sweep) } return &v diff --git a/voice/component/envelope.go b/voice/component/envelope.go index a4060b3..909563b 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -2,8 +2,9 @@ package component // Envelope is an envelope component interface type Envelope interface { - //Reset(env *envelope.Envelope) + //Init(env *envelope.Envelope) SetEnabled(enabled bool) IsEnabled() bool + Reset() Advance(keyOn bool, prevKeyOn bool) } diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 7c6db84..cd7f5b6 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -14,6 +14,11 @@ type FilterEnvelope struct { prevKeyOn bool } +func (e *FilterEnvelope) Init(env *envelope.Envelope[int8]) { + e.state.Init(env) + e.Reset() +} + func (e FilterEnvelope) Clone() FilterEnvelope { return FilterEnvelope{ enabled: e.enabled, @@ -25,8 +30,8 @@ func (e FilterEnvelope) Clone() FilterEnvelope { } // Reset resets the state to defaults based on the envelope provided -func (e *FilterEnvelope) Reset(env *envelope.Envelope[int8]) { - e.state.Reset(env) +func (e *FilterEnvelope) Reset() { + e.state.Reset() e.keyOn = false e.prevKeyOn = false e.update() @@ -51,8 +56,7 @@ func (e *FilterEnvelope) GetCurrentValue() int8 { func (e *FilterEnvelope) SetEnvelopePosition(pos int) voice.Callback { keyOn := e.keyOn prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) + e.state.Reset() // 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 { diff --git a/voice/component/envelope_pan.go b/voice/component/envelope_pan.go index 9c0dce7..98308e8 100755 --- a/voice/component/envelope_pan.go +++ b/voice/component/envelope_pan.go @@ -16,6 +16,11 @@ type PanEnvelope struct { prevKeyOn bool } +func (e *PanEnvelope) Init(env *envelope.Envelope[panning.Position]) { + e.state.Init(env) + e.Reset() +} + func (e PanEnvelope) Clone() PanEnvelope { return PanEnvelope{ enabled: e.enabled, @@ -27,8 +32,8 @@ func (e PanEnvelope) Clone() PanEnvelope { } // Reset resets the state to defaults based on the envelope provided -func (e *PanEnvelope) Reset(env *envelope.Envelope[panning.Position]) { - e.state.Reset(env) +func (e *PanEnvelope) Reset() { + e.state.Reset() e.keyOn = false e.prevKeyOn = false e.update() @@ -53,8 +58,7 @@ func (e *PanEnvelope) GetCurrentValue() panning.Position { func (e *PanEnvelope) SetEnvelopePosition(pos int) voice.Callback { keyOn := e.keyOn prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) + e.state.Reset() // 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 { diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index 80dbc48..4780957 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -16,6 +16,11 @@ type PitchEnvelope[TPeriod period.Period] struct { prevKeyOn bool } +func (e *PitchEnvelope[TPeriod]) Init(env *envelope.Envelope[int8]) { + e.state.Init(env) + e.Reset() +} + func (e PitchEnvelope[TPeriod]) Clone() PitchEnvelope[TPeriod] { return PitchEnvelope[TPeriod]{ enabled: e.enabled, @@ -27,8 +32,8 @@ func (e PitchEnvelope[TPeriod]) Clone() PitchEnvelope[TPeriod] { } // Reset resets the state to defaults based on the envelope provided -func (e *PitchEnvelope[TPeriod]) Reset(env *envelope.Envelope[int8]) { - e.state.Reset(env) +func (e *PitchEnvelope[TPeriod]) Reset() { + e.state.Reset() e.keyOn = false e.prevKeyOn = false e.update() @@ -53,8 +58,7 @@ func (e *PitchEnvelope[TPeriod]) GetCurrentValue() period.Delta { func (e *PitchEnvelope[TPeriod]) SetEnvelopePosition(pos int) voice.Callback { keyOn := e.keyOn prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) + e.state.Reset() // 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 { diff --git a/voice/component/envelope_volume.go b/voice/component/envelope_volume.go index d28dafc..66e2481 100755 --- a/voice/component/envelope_volume.go +++ b/voice/component/envelope_volume.go @@ -16,6 +16,11 @@ type VolumeEnvelope struct { prevKeyOn bool } +func (e *VolumeEnvelope) Init(env *envelope.Envelope[volume.Volume]) { + e.state.Init(env) + e.Reset() +} + func (e VolumeEnvelope) Clone() VolumeEnvelope { return VolumeEnvelope{ enabled: e.enabled, @@ -27,8 +32,8 @@ func (e VolumeEnvelope) Clone() VolumeEnvelope { } // Reset resets the state to defaults based on the envelope provided -func (e *VolumeEnvelope) Reset(env *envelope.Envelope[volume.Volume]) { - e.state.Reset(env) +func (e *VolumeEnvelope) Reset() { + e.state.Reset() e.keyOn = false e.prevKeyOn = false e.update() @@ -53,8 +58,7 @@ func (e *VolumeEnvelope) GetCurrentValue() volume.Volume { func (e *VolumeEnvelope) SetEnvelopePosition(pos int) voice.Callback { keyOn := e.keyOn prevKeyOn := e.prevKeyOn - env := e.state.Envelope() - e.state.Reset(env) + e.state.Reset() // 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 { diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index b2d02ab..bd36cdd 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -64,16 +64,17 @@ func (a *FreqModulator[TPeriod]) ConfigureAutoVibrato(av voice.AutoVibrato) { } // ResetAutoVibrato resets the current AutoVibrato -func (a *FreqModulator[TPeriod]) ResetAutoVibrato(sweep ...int) { +func (a *FreqModulator[TPeriod]) ResetAutoVibrato() { if a.autoVibrato != nil { - a.autoVibrato.Reset(true) + a.autoVibrato.HardReset() } a.autoVibratoAge = 0 +} - if len(sweep) > 0 { - a.autoVibratoSweep = sweep[0] - } +func (a *FreqModulator[TPeriod]) ResetAutoVibratoAndSweep(sweep int) { + a.ResetAutoVibrato() + a.autoVibratoSweep = sweep } // IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag diff --git a/voice/envelope/envelope.go b/voice/envelope/envelope.go index 5739c67..a5f6faa 100644 --- a/voice/envelope/envelope.go +++ b/voice/envelope/envelope.go @@ -12,6 +12,16 @@ type State[T any] struct { env *Envelope[T] } +func (e *State[T]) Init(env *Envelope[T]) { + e.env = env + if e.env == nil || !e.env.Enabled { + e.stopped = true + return + } + + e.Reset() +} + func (e State[T]) Clone() State[T] { return State[T]{ position: 0, @@ -37,13 +47,7 @@ func (e *State[T]) Envelope() *Envelope[T] { } // 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 - } - +func (e *State[T]) Reset() { e.position = 0 pos, _, _ := e.calcLoopedPos(true) if pos < len(e.env.Values) { diff --git a/voice/oscillator/oscillator.go b/voice/oscillator/oscillator.go index b9ad62d..5c3c9b4 100755 --- a/voice/oscillator/oscillator.go +++ b/voice/oscillator/oscillator.go @@ -9,5 +9,6 @@ type Oscillator interface { GetWave(depth float32) float32 Advance(speed int) SetWaveform(table WaveTableSelect) - Reset(hard ...bool) + HardReset() + Reset() } From de71b6087f6aab0634b70b269f3029e2a676cece Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 16:47:28 -0800 Subject: [PATCH 20/63] Fix filter envelope --- filter/amigafilter.go | 2 +- filter/echofilter.go | 2 +- filter/filter.go | 2 +- filter/pitchfiltervalue.go | 11 +++++++++ format/it/filter/resonantfilter.go | 36 ++++++++++++++++-------------- format/it/load/instrument.go | 4 ++-- instrument/pcm.go | 5 +++-- player/render/channel.go | 2 +- player/voice/pcm.go | 2 +- util/lerp.go | 2 +- voice/component/envelope_filter.go | 20 +++++++++-------- voice/component/envelope_pitch.go | 16 ++++++------- voice/filterapplier.go | 2 +- 13 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 filter/pitchfiltervalue.go diff --git a/filter/amigafilter.go b/filter/amigafilter.go index 703c48b..acf9df0 100644 --- a/filter/amigafilter.go +++ b/filter/amigafilter.go @@ -88,5 +88,5 @@ func (f *AmigaLPF) recalculate() { } // 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..2d0a28c 100644 --- a/filter/echofilter.go +++ b/filter/echofilter.go @@ -126,6 +126,6 @@ func (e *EchoFilter) recalculate() { } } -func (e *EchoFilter) UpdateEnv(val int8) { +func (e *EchoFilter) UpdateEnv(val uint8) { } diff --git a/filter/filter.go b/filter/filter.go index ddf2274..10f6e7f 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -8,7 +8,7 @@ import ( // Filter is an interface to a filter type Filter interface { Filter(volume.Matrix) volume.Matrix - UpdateEnv(int8) + UpdateEnv(uint8) Clone() Filter } diff --git a/filter/pitchfiltervalue.go b/filter/pitchfiltervalue.go new file mode 100644 index 0000000..30c7407 --- /dev/null +++ b/filter/pitchfiltervalue.go @@ -0,0 +1,11 @@ +package filter + +type PitchFiltValue uint8 + +func (p PitchFiltValue) AsFilter() uint8 { + return uint8(p) +} + +func (p PitchFiltValue) AsPitch() int8 { + return int8(p) +} diff --git a/format/it/filter/resonantfilter.go b/format/it/filter/resonantfilter.go index 5e7a035..34bb19f 100644 --- a/format/it/filter/resonantfilter.go +++ b/format/it/filter/resonantfilter.go @@ -47,7 +47,7 @@ func NewResonantFilter(cutoff uint8, resonance uint8, playbackRate period.Freque rf.cutoff.Set(uint8(c)) } - rf.recalculate(int8(c)) + rf.recalculate(c) return rf } @@ -77,6 +77,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 +94,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 +105,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 { @@ -136,14 +140,12 @@ func (f *ResonantFilter) recalculate(v int8) { 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 - } + 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 @@ -187,6 +189,6 @@ func (f *ResonantFilter) recalculate(v int8) { } // 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/it/load/instrument.go b/format/it/load/instrument.go index 433f7d4..3f51e2d 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -227,8 +227,8 @@ func convertPanEnvValue(v int8) panning.Position { return panning.MakeStereoPosition(float32(v), -64, 64) } -func convertPitchEnvValue(v int8) int8 { - return v +func convertPitchEnvValue(v int8) filter.PitchFiltValue { + return filter.PitchFiltValue(256-uint16(v)) / 2 } func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope, convert func(int8) T) error { diff --git a/instrument/pcm.go b/instrument/pcm.go index a9d43af..7c858d0 100644 --- a/instrument/pcm.go +++ b/instrument/pcm.go @@ -4,6 +4,7 @@ import ( "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/voice/envelope" "github.com/gotracker/playback/voice/fadeout" "github.com/gotracker/playback/voice/loop" @@ -20,8 +21,8 @@ type PCM struct { 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 + PitchFiltMode bool // true = filter, false = pitch + PitchFiltEnv envelope.Envelope[filter.PitchFiltValue] // this is either pitch or filter } func (PCM) GetKind() Kind { diff --git a/player/render/channel.go b/player/render/channel.go index eb862df..ab37008 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -38,7 +38,7 @@ func (oc *Channel) GetPremixVolume() volume.Volume { } // SetFilterEnvelopeValue updates the filter on the channel with the new envelope value -func (oc *Channel) SetFilterEnvelopeValue(envVal int8) { +func (oc *Channel) SetFilterEnvelopeValue(envVal uint8) { if oc.Filter != nil { oc.Filter.UpdateEnv(envVal) } diff --git a/player/voice/pcm.go b/player/voice/pcm.go index a31770d..378f128 100644 --- a/player/voice/pcm.go +++ b/player/voice/pcm.go @@ -349,7 +349,7 @@ func (v *pcmVoice[TPeriod]) IsFilterEnvelopeEnabled() bool { return v.filterEnv.IsEnabled() } -func (v *pcmVoice[TPeriod]) GetCurrentFilterEnvelope() int8 { +func (v *pcmVoice[TPeriod]) GetCurrentFilterEnvelope() uint8 { return v.filterEnv.GetCurrentValue() } diff --git a/util/lerp.go b/util/lerp.go index 2219a95..bca938f 100644 --- a/util/lerp.go +++ b/util/lerp.go @@ -12,5 +12,5 @@ func Lerp[T Lerpable](t float64, a, b T) T { } else if t >= 1 { return b } - return a + T(t*float64(b-a)) + return a + T(t*(float64(b)-(float64(a)))) } diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index cd7f5b6..087e8b6 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -1,6 +1,8 @@ package component import ( + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/envelope" ) @@ -8,13 +10,13 @@ import ( // FilterEnvelope is a filter frequency cutoff modulation envelope type FilterEnvelope struct { enabled bool - state envelope.State[int8] - value int8 + state envelope.State[filter.PitchFiltValue] + value uint8 keyOn bool prevKeyOn bool } -func (e *FilterEnvelope) Init(env *envelope.Envelope[int8]) { +func (e *FilterEnvelope) Init(env *envelope.Envelope[filter.PitchFiltValue]) { e.state.Init(env) e.Reset() } @@ -48,7 +50,7 @@ func (e *FilterEnvelope) IsEnabled() bool { } // GetCurrentValue returns the current cached envelope value -func (e *FilterEnvelope) GetCurrentValue() int8 { +func (e *FilterEnvelope) GetCurrentValue() uint8 { return e.value } @@ -81,15 +83,15 @@ func (e *FilterEnvelope) Advance(keyOn bool, prevKeyOn bool) voice.Callback { func (e *FilterEnvelope) update() { cur, next, t := e.state.GetCurrentValue(e.keyOn) - var y0 float32 + var y0 filter.PitchFiltValue if cur != nil { - y0 = float32(cur.Value()) + y0 = cur.Value() } - var y1 float32 + var y1 filter.PitchFiltValue if next != nil { - y1 = float32(next.Value()) + y1 = next.Value() } - e.value = int8(y0 + t*(y1-y0)) + e.value = util.Lerp(float64(t), y0.AsFilter(), y1.AsFilter()) } diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index 4780957..f8c3499 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -1,6 +1,7 @@ package component import ( + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" @@ -10,13 +11,13 @@ import ( // PitchEnvelope is an frequency modulation envelope type PitchEnvelope[TPeriod period.Period] struct { enabled bool - state envelope.State[int8] + state envelope.State[filter.PitchFiltValue] delta period.Delta keyOn bool prevKeyOn bool } -func (e *PitchEnvelope[TPeriod]) Init(env *envelope.Envelope[int8]) { +func (e *PitchEnvelope[TPeriod]) Init(env *envelope.Envelope[filter.PitchFiltValue]) { e.state.Init(env) e.Reset() } @@ -83,16 +84,15 @@ func (e *PitchEnvelope[TPeriod]) Advance(keyOn bool, prevKeyOn bool) voice.Callb func (e *PitchEnvelope[TPeriod]) update() { cur, next, t := e.state.GetCurrentValue(e.keyOn) - var y0 period.Delta + var y0 filter.PitchFiltValue if cur != nil { - y0 = period.Delta(cur.Value()) + y0 = cur.Value() } - var y1 period.Delta + var y1 filter.PitchFiltValue if next != nil { - y1 = period.Delta(next.Value()) + y1 = next.Value() } - d := -util.Lerp(float64(t), y0, y1) - e.delta = d + e.delta = -period.Delta(util.Lerp(float64(t), y0.AsPitch(), y1.AsPitch())) } diff --git a/voice/filterapplier.go b/voice/filterapplier.go index c2b7858..c1eee01 100755 --- a/voice/filterapplier.go +++ b/voice/filterapplier.go @@ -7,5 +7,5 @@ import ( // FilterApplier is an interface for applying a filter to a sample stream type FilterApplier interface { ApplyFilter(dry volume.Matrix) volume.Matrix - SetFilterEnvelopeValue(envVal int8) + SetFilterEnvelopeValue(envVal uint8) } From 79d3f38ee364342a24ba5c726932c9620a1df1ef Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 17:56:05 -0800 Subject: [PATCH 21/63] better fix for pitch filter --- filter/pitchfiltervalue.go | 2 +- format/it/load/instrument.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/filter/pitchfiltervalue.go b/filter/pitchfiltervalue.go index 30c7407..7214b31 100644 --- a/filter/pitchfiltervalue.go +++ b/filter/pitchfiltervalue.go @@ -7,5 +7,5 @@ func (p PitchFiltValue) AsFilter() uint8 { } func (p PitchFiltValue) AsPitch() int8 { - return int8(p) + return int8(p) - 32 } diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 3f51e2d..909d7b3 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -228,7 +228,7 @@ func convertPanEnvValue(v int8) panning.Position { } func convertPitchEnvValue(v int8) filter.PitchFiltValue { - return filter.PitchFiltValue(256-uint16(v)) / 2 + return filter.PitchFiltValue(v + 32) } func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope, convert func(int8) T) error { From 3bf62a2486bb5c3a33ef4fb4fddc251b553e9058 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 18:29:44 -0800 Subject: [PATCH 22/63] better calculation of filter value --- filter/pitchfiltervalue.go | 10 +--------- format/it/load/instrument.go | 2 +- voice/component/envelope_filter.go | 3 ++- voice/component/envelope_pitch.go | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/filter/pitchfiltervalue.go b/filter/pitchfiltervalue.go index 7214b31..b2e1f88 100644 --- a/filter/pitchfiltervalue.go +++ b/filter/pitchfiltervalue.go @@ -1,11 +1,3 @@ package filter -type PitchFiltValue uint8 - -func (p PitchFiltValue) AsFilter() uint8 { - return uint8(p) -} - -func (p PitchFiltValue) AsPitch() int8 { - return int8(p) - 32 -} +type PitchFiltValue = int8 diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 909d7b3..8209eb7 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -228,7 +228,7 @@ func convertPanEnvValue(v int8) panning.Position { } func convertPitchEnvValue(v int8) filter.PitchFiltValue { - return filter.PitchFiltValue(v + 32) + return filter.PitchFiltValue(v) } func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope, convert func(int8) T) error { diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 087e8b6..22ed770 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -93,5 +93,6 @@ func (e *FilterEnvelope) update() { y1 = next.Value() } - e.value = util.Lerp(float64(t), y0.AsFilter(), y1.AsFilter()) + v := util.Lerp(float64(t), y0, y1) + e.value = uint8(32 + v) } diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index f8c3499..96b6fac 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -94,5 +94,5 @@ func (e *PitchEnvelope[TPeriod]) update() { y1 = next.Value() } - e.delta = -period.Delta(util.Lerp(float64(t), y0.AsPitch(), y1.AsPitch())) + e.delta = -period.Delta(util.Lerp(float64(t), y0, y1)) } From 422c2906d16d8a0ca8f18db827181dff5b0d5f79 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 19:35:40 -0800 Subject: [PATCH 23/63] Fix for s3m/mod envelope crash --- voice/envelope/envelope.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/voice/envelope/envelope.go b/voice/envelope/envelope.go index a5f6faa..38f0890 100644 --- a/voice/envelope/envelope.go +++ b/voice/envelope/envelope.go @@ -48,6 +48,11 @@ func (e *State[T]) Envelope() *Envelope[T] { // Reset resets the envelope func (e *State[T]) Reset() { + if e.env == nil || !e.env.Enabled { + e.stopped = true + return + } + e.position = 0 pos, _, _ := e.calcLoopedPos(true) if pos < len(e.env.Values) { From da045fbd737a601cbeed574ed42fb5503c78f6f6 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 21:25:33 -0800 Subject: [PATCH 24/63] Support mod fine-panning - also stub out s3m surround support --- format/s3m/channel/effect_surroundon.go | 24 ++++++++++++++++++++++++ format/s3m/channel/effectfactory.go | 20 ++++++++++++++++++++ format/s3m/load/modconv/modconverter.go | 11 +++++++++++ 3 files changed, 55 insertions(+) create mode 100644 format/s3m/channel/effect_surroundon.go diff --git a/format/s3m/channel/effect_surroundon.go b/format/s3m/channel/effect_surroundon.go new file mode 100644 index 0000000..cfcf1a8 --- /dev/null +++ b/format/s3m/channel/effect_surroundon.go @@ -0,0 +1,24 @@ +package channel + +import ( + "fmt" + + "github.com/gotracker/playback" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" +) + +// SurroundOn defines a set surround on effect +type SurroundOn ChannelCommand // 'S91' + +// Start triggers on the first tick, but before the Tick() function is called +func (e SurroundOn) Start(cs S3MChannel, p playback.Playback) error { + cs.ResetRetriggerCount() + + // TODO: support for center rear panning + cs.SetPan(s3mPanning.DefaultPanning) + return nil +} + +func (e SurroundOn) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} diff --git a/format/s3m/channel/effectfactory.go b/format/s3m/channel/effectfactory.go index c5cbd94..9dd3f53 100644 --- a/format/s3m/channel/effectfactory.go +++ b/format/s3m/channel/effectfactory.go @@ -105,6 +105,8 @@ func specialEffect(mem *Memory, data 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 @@ -143,3 +145,21 @@ func volumeSlideFactory(mem *Memory, cd uint8, ce DataEffect) EffectS3M { return VolumeSlideDown(xy) //return UnhandledCommand{Command: cd, Info: xy} } + +func soundControlEffect(data Data) EffectS3M { + 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/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index f4c5c7d..5f03d2f 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -87,6 +87,17 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) case 0xC: // Set Volume u.What |= s3mfile.PatternFlagVolume u.Volume = s3mfile.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 { From 8c477b0cc158c2c5444aa8a6dbc5d757094c832c Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 23 Dec 2023 21:38:46 -0800 Subject: [PATCH 25/63] s3m playback cleanup --- format/s3m/playback/playback_command.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/format/s3m/playback/playback_command.go b/format/s3m/playback/playback_command.go index b87ff2b..565dd63 100644 --- a/format/s3m/playback/playback_command.go +++ b/format/s3m/playback/playback_command.go @@ -94,10 +94,9 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last } else if keyOff { nc.Release() nc.Fadeout() - cs.SetPeriod(0) } else if stop { cs.SetInstrument(nil) - cs.SetPeriod(0) + cs.NoteCut() } } return nil From e90e338e1007d628e084fdccde0f6a7509b197b7 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 24 Dec 2023 11:58:54 -0800 Subject: [PATCH 26/63] first pass on cleaning up interfaces --- channel.go | 23 +--- channelstate.go | 43 +++++++ format/it/channel/data.go | 2 +- format/it/channel/effect_arpeggio.go | 2 +- format/it/channel/effect_portatonote.go | 4 +- format/it/channel/effect_retrigvolslide.go | 2 +- format/it/channel/effect_sampleoffset.go | 7 +- .../it/channel/effect_setcoarsepanposition.go | 2 +- format/it/channel/effect_setfinetune.go | 2 +- format/it/channel/effect_setpanposition.go | 2 +- format/it/channel/util.go | 30 ++--- format/it/playback/playback.go | 5 +- format/it/playback/playback_command.go | 25 ++-- format/s3m/channel/data.go | 2 +- format/s3m/channel/effect_arpeggio.go | 2 +- format/s3m/channel/effect_portatonote.go | 5 +- format/s3m/channel/effect_retrigvolslide.go | 2 +- format/s3m/channel/effect_sampleoffset.go | 2 +- format/s3m/channel/effect_setfinetune.go | 2 +- format/s3m/channel/effect_setpanposition.go | 2 +- format/s3m/channel/effect_stereocontrol.go | 5 +- format/s3m/channel/effect_surroundon.go | 2 +- format/s3m/channel/util.go | 34 +++--- format/s3m/playback/playback.go | 8 +- format/s3m/playback/playback_command.go | 23 ++-- format/xm/channel/data.go | 2 +- format/xm/channel/effect_arpeggio.go | 2 +- format/xm/channel/effect_notecut.go | 2 +- format/xm/channel/effect_panslide.go | 5 +- format/xm/channel/effect_portatonote.go | 4 +- format/xm/channel/effect_retriggernote.go | 2 +- format/xm/channel/effect_retrigvolslide.go | 2 +- format/xm/channel/effect_sampleoffset.go | 2 +- .../xm/channel/effect_setcoarsepanposition.go | 2 +- format/xm/channel/effect_setfinetune.go | 2 +- format/xm/channel/effect_setpanposition.go | 2 +- format/xm/channel/effect_setvolume.go | 3 +- format/xm/channel/util.go | 31 ++--- format/xm/playback/playback.go | 5 +- format/xm/playback/playback_command.go | 27 ++--- player/state/active.go | 31 ++--- player/state/channel.go | 109 +++--------------- player/state/channel_transaction.go | 14 ++- player/state/playback.go | 28 ----- 44 files changed, 229 insertions(+), 284 deletions(-) create mode 100644 channelstate.go delete mode 100644 player/state/playback.go diff --git a/channel.go b/channel.go index 1b2376b..c80e592 100644 --- a/channel.go +++ b/channel.go @@ -1,8 +1,6 @@ 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/op" @@ -19,41 +17,25 @@ type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] ResetRetriggerCount() SetMemory(*TMemory) GetMemory() *TMemory - GetActiveVolume() volume.Volume - SetActiveVolume(volume.Volume) FreezePlayback() UnfreezePlayback() GetChannelData() TChannelData GetPortaTargetPeriod() TPeriod SetPortaTargetPeriod(TPeriod) - GetTargetPeriod() TPeriod - SetTargetPeriod(TPeriod) SetPeriodOverride(TPeriod) - GetPeriod() TPeriod - SetPeriod(TPeriod) SetPeriodDelta(period.Delta) GetPeriodDelta() period.Delta 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) @@ -68,7 +50,10 @@ type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] SetVolumeEnvelopeEnable(bool) SetPanningEnvelopeEnable(bool) SetPitchEnvelopeEnable(bool) - NoteCut() + + GetPreviousState() ChannelState[TPeriod] + GetActiveState() *ChannelState[TPeriod] + GetTargetState() *ChannelState[TPeriod] } type ChannelTargeter[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(out *op.ChannelTargets[TPeriod], d TChannelData, s song.Data, cs Channel[TPeriod, TMemory, TChannelData]) error diff --git a/channelstate.go b/channelstate.go new file mode 100644 index 0000000..4db29de --- /dev/null +++ b/channelstate.go @@ -0,0 +1,43 @@ +package playback + +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" +) + +// ChannelState is the information needed to make an instrument play +type ChannelState[TPeriod period.Period] struct { + Instrument *instrument.Instrument + Period TPeriod + vol volume.Volume + Pos sampling.Pos + Pan panning.Position +} + +// Reset sets the render state to defaults +func (s *ChannelState[TPeriod]) Reset() { + s.Instrument = nil + var empty TPeriod + s.Period = empty + s.Pos = sampling.Pos{} + s.Pan = panning.CenterAhead +} + +func (s *ChannelState[TPeriod]) GetVolume() volume.Volume { + return s.vol +} + +func (s *ChannelState[TPeriod]) SetVolume(vol volume.Volume) { + if vol != volume.VolumeUseInstVol { + s.vol = vol + } +} + +func (s *ChannelState[TPeriod]) NoteCut() { + var empty TPeriod + s.Period = empty +} diff --git a/format/it/channel/data.go b/format/it/channel/data.go index bfa75a5..85cea8a 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -167,7 +167,7 @@ func (d Data) ShortString() string { func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], d Data, s song.Data, cs playback.Channel[TPeriod, Memory, Data]) error { var n note.Note = note.EmptyNote{} - inst := cs.GetInstrument() + inst := cs.GetActiveState().Instrument prevInst := inst if d.HasNote() || d.HasInstrument() { diff --git a/format/it/channel/effect_arpeggio.go b/format/it/channel/effect_arpeggio.go index 2d2c194..4dbb2ce 100644 --- a/format/it/channel/effect_arpeggio.go +++ b/format/it/channel/effect_arpeggio.go @@ -15,7 +15,7 @@ type Arpeggio[TPeriod period.Period] DataEffect // 'J' func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - cs.SetPos(cs.GetTargetPos()) + cs.GetActiveState().Pos = cs.GetTargetState().Pos return nil } diff --git a/format/it/channel/effect_portatonote.go b/format/it/channel/effect_portatonote.go index 7c5d29c..947e2b1 100644 --- a/format/it/channel/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -17,7 +17,7 @@ func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetChannelData(); cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) + cs.SetPortaTargetPeriod(cs.GetTargetState().Period) cs.SetNotePlayTick(false, note.ActionContinue, 0) } return nil @@ -29,7 +29,7 @@ func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p xx := mem.PortaToNote(DataEffect(e)) // vibrato modifies current period for portamento - cur := cs.GetPeriod() + cur := cs.GetActiveState().Period if cur.IsInvalid() { return nil } diff --git a/format/it/channel/effect_retrigvolslide.go b/format/it/channel/effect_retrigvolslide.go index 040eae9..ae87e28 100644 --- a/format/it/channel/effect_retrigvolslide.go +++ b/format/it/channel/effect_retrigvolslide.go @@ -29,7 +29,7 @@ func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Da rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) if DataEffect(rt) >= x { - cs.SetPos(sampling.Pos{}) + cs.GetActiveState().Pos = sampling.Pos{} cs.ResetRetriggerCount() switch x { case 1: diff --git a/format/it/channel/effect_sampleoffset.go b/format/it/channel/effect_sampleoffset.go index 7e6e253..c19c63a 100644 --- a/format/it/channel/effect_sampleoffset.go +++ b/format/it/channel/effect_sampleoffset.go @@ -19,12 +19,13 @@ func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], xx := mem.SampleOffset(DataEffect(e)) pos := sampling.Pos{Pos: mem.HighOffset + int(xx)*0x100} + target := cs.GetTargetState() if mem.Shared.OldEffectMode { - if inst := cs.GetInstrument(); inst != nil && inst.GetLength().Pos < pos.Pos { - cs.SetTargetPos(pos) + if inst := cs.GetActiveState().Instrument; inst != nil && inst.GetLength().Pos < pos.Pos { + target.Pos = pos } } else { - cs.SetTargetPos(pos) + target.Pos = pos } return nil } diff --git a/format/it/channel/effect_setcoarsepanposition.go b/format/it/channel/effect_setcoarsepanposition.go index 695cd00..973af0d 100644 --- a/format/it/channel/effect_setcoarsepanposition.go +++ b/format/it/channel/effect_setcoarsepanposition.go @@ -21,7 +21,7 @@ func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory pan := itfile.PanValue(x << 2) - cs.SetPan(itPanning.FromItPanning(pan)) + cs.GetActiveState().Pan = itPanning.FromItPanning(pan) return nil } diff --git a/format/it/channel/effect_setfinetune.go b/format/it/channel/effect_setfinetune.go index a884f7b..51e2ec6 100644 --- a/format/it/channel/effect_setfinetune.go +++ b/format/it/channel/effect_setfinetune.go @@ -15,7 +15,7 @@ type SetFinetune[TPeriod period.Period] DataEffect // 'S2x' func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { x := DataEffect(e) & 0xf - inst := cs.GetTargetInst() + inst := cs.GetTargetState().Instrument if inst != nil { ft := (note.Finetune(x) - 8) * 4 inst.SetFinetune(ft) diff --git a/format/it/channel/effect_setpanposition.go b/format/it/channel/effect_setpanposition.go index 7ff4586..dc409cf 100644 --- a/format/it/channel/effect_setpanposition.go +++ b/format/it/channel/effect_setpanposition.go @@ -21,7 +21,7 @@ func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data pan := itfile.PanValue(x) - cs.SetPan(itPanning.FromItPanning(pan)) + cs.GetActiveState().Pan = itPanning.FromItPanning(pan) return nil } diff --git a/format/it/channel/util.go b/format/it/channel/util.go index 7d75ba5..4d671f9 100644 --- a/format/it/channel/util.go +++ b/format/it/channel/util.go @@ -31,7 +31,8 @@ type IT interface { } func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], delta float32, multiplier float32) error { - av := cs.GetActiveVolume() + active := cs.GetActiveState() + av := active.GetVolume() v := itVolume.ToItVolume(av) vol := int16((float32(v) + delta) * multiplier) if vol >= 0x40 { @@ -42,7 +43,7 @@ func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data } v = itfile.Volume(vol) nv := itVolume.FromItVolume(v) - cs.SetActiveVolume(nv) + active.SetVolume(nv) return nil } @@ -63,14 +64,15 @@ func doGlobalVolSlide(m IT, delta float32, multiplier float32) error { } func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } delta := int(amount * multiplier) cur = period.PortaUp(cur, delta) - cs.SetPeriod(cur) + active.Period = cur return nil } @@ -78,21 +80,22 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { return err } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { - cs.SetPeriod(target) + if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipLeftGreater { + active.Period = target } return nil } func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } delta := int(amount * multiplier) cur = period.PortaDown(cur, delta) - cs.SetPeriod(cur) + active.Period = cur return nil } @@ -100,8 +103,8 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memor if err := doPortaDown(cs, amount, multiplier); err != nil { return err } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { - cs.SetPeriod(target) + if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipRightGreater { + active.Period = target } return nil } @@ -142,7 +145,7 @@ func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneBDelta) } cs.SetOverrideSemitone(arpSemitoneTarget) - cs.SetTargetPos(cs.GetPos()) + cs.GetTargetState().Pos = cs.GetActiveState().Pos return nil } @@ -156,7 +159,8 @@ var ( ) func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data]) error { - vol := itVolume.ToItVolume(cs.GetActiveVolume()) + active := cs.GetActiveState() + vol := itVolume.ToItVolume(active.GetVolume()) if vol >= 0x10 && vol <= 0x50 { vol -= 0x10 if vol >= 64 { @@ -169,7 +173,7 @@ func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Mem } vv := itfile.Volume(v) - cs.SetActiveVolume(itVolume.FromItVolume(vv)) + active.SetVolume(itVolume.FromItVolume(vv)) } return nil } diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index 947c5aa..8b438a8 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -79,10 +79,11 @@ func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.Period cs.SetSongDataInterface(song) cs.SetRenderChannel(oc) cs.SetGlobalVolume(m.GetGlobalVolume()) - cs.SetActiveVolume(ch.InitialVolume) + active := cs.GetActiveState() + active.SetVolume(ch.InitialVolume) cs.SetChannelVolume(ch.ChannelVolume) cs.SetPanEnabled(true) - cs.SetPan(ch.InitialPanning) + active.Pan = ch.InitialPanning cs.SetMemory(&song.ChannelSettings[i].Memory) cs.SetStoredSemitone(note.UnchangedSemitone) } diff --git a/format/it/playback/playback_command.go b/format/it/playback/playback_command.go index b926115..15ac1ed 100644 --- a/format/it/playback/playback_command.go +++ b/format/it/playback/playback_command.go @@ -20,7 +20,7 @@ func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState return nil } - if inst := cs.GetTargetInst(); inst != nil { + if inst := cs.GetTargetState().Instrument; inst != nil { cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) ft := inst.GetFinetune() period := itPeriod.CalcSemitonePeriod[TPeriod](cs.Semitone, ft, inst.GetSampleRate()) @@ -69,25 +69,25 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod cs.TransitionActiveToPastState() } + active := cs.GetActiveState() + target := cs.GetTargetState() + wantAttack := false - targetPeriod := cs.GetTargetPeriod() - if !targetPeriod.IsInvalid() { - targetInst := cs.GetTargetInst() - if targetInst != nil { + if !target.Period.IsInvalid() { + if target.Instrument != nil { keyOn = true wantAttack = noteAction == note.ActionRetrigger } if cs.UseTargetPeriod { - cs.SetPeriod(targetPeriod) - cs.SetPortaTargetPeriod(targetPeriod) + active.Period = target.Period + cs.SetPortaTargetPeriod(target.Period) } - cs.SetInstrument(targetInst) - cs.SetPos(cs.GetTargetPos()) + cs.SetInstrument(target.Instrument) + active.Pos = target.Pos } - var invalidPeriod TPeriod if nc := cs.GetVoice(); nc != nil { switch noteAction { case note.ActionRetrigger: @@ -100,7 +100,8 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod nc.Release() case note.ActionCut: cs.SetInstrument(nil) - cs.SetPeriod(invalidPeriod) + var invalidPeriod TPeriod + active.Period = invalidPeriod } } @@ -111,7 +112,7 @@ func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TP if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() - cs.SetPeriod(arpeggioPeriod) + cs.GetActiveState().Period = arpeggioPeriod } return nil } diff --git a/format/s3m/channel/data.go b/format/s3m/channel/data.go index 9f632d8..949e1dd 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -129,7 +129,7 @@ func NoteFromS3MNote(sn s3mfile.Note) note.Note { func GetTargetsFromData(out *op.ChannelTargets[period.Amiga], d Data, s song.Data, cs playback.Channel[period.Amiga, Memory, Data]) error { var n note.Note = note.EmptyNote{} - inst := cs.GetInstrument() + inst := cs.GetActiveState().Instrument prevInst := inst if d.HasNote() || d.HasInstrument() { diff --git a/format/s3m/channel/effect_arpeggio.go b/format/s3m/channel/effect_arpeggio.go index b37e9c2..89fe8ab 100644 --- a/format/s3m/channel/effect_arpeggio.go +++ b/format/s3m/channel/effect_arpeggio.go @@ -13,7 +13,7 @@ type Arpeggio ChannelCommand // 'J' func (e Arpeggio) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - cs.SetPos(cs.GetTargetPos()) + cs.GetActiveState().Pos = cs.GetTargetState().Pos return nil } diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 2472ad2..596a02c 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -17,7 +17,7 @@ func (e PortaToNote) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetChannelData(); cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) + cs.SetPortaTargetPeriod(cs.GetTargetState().Period) cs.SetNotePlayTick(false, note.ActionContinue, 0) } return nil @@ -29,7 +29,8 @@ func (e PortaToNote) Tick(cs S3MChannel, p playback.Playback, currentTick int) e xx := mem.PortaToNote(DataEffect(e)) // vibrato modifies current period for portamento - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } diff --git a/format/s3m/channel/effect_retrigvolslide.go b/format/s3m/channel/effect_retrigvolslide.go index 037853b..10f76c1 100644 --- a/format/s3m/channel/effect_retrigvolslide.go +++ b/format/s3m/channel/effect_retrigvolslide.go @@ -28,7 +28,7 @@ func (e RetrigVolumeSlide) Tick(cs S3MChannel, p playback.Playback, currentTick rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) if DataEffect(rt) >= x { - cs.SetPos(sampling.Pos{}) + cs.GetActiveState().Pos = sampling.Pos{} cs.ResetRetriggerCount() switch x { case 1: diff --git a/format/s3m/channel/effect_sampleoffset.go b/format/s3m/channel/effect_sampleoffset.go index a20ae97..a01a903 100644 --- a/format/s3m/channel/effect_sampleoffset.go +++ b/format/s3m/channel/effect_sampleoffset.go @@ -16,7 +16,7 @@ func (e SampleOffset) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(DataEffect(e)) - cs.SetTargetPos(sampling.Pos{Pos: int(xx) * 0x100}) + cs.GetTargetState().Pos = sampling.Pos{Pos: int(xx) * 0x100} return nil } diff --git a/format/s3m/channel/effect_setfinetune.go b/format/s3m/channel/effect_setfinetune.go index 5ae891b..859f6f8 100644 --- a/format/s3m/channel/effect_setfinetune.go +++ b/format/s3m/channel/effect_setfinetune.go @@ -14,7 +14,7 @@ type SetFinetune ChannelCommand // 'S2x' func (e SetFinetune) PreStart(cs S3MChannel, p playback.Playback) error { x := DataEffect(e) & 0xf - inst := cs.GetTargetInst() + inst := cs.GetTargetState().Instrument if inst != nil { ft := (note.Finetune(x) - 8) * 4 inst.SetFinetune(ft) diff --git a/format/s3m/channel/effect_setpanposition.go b/format/s3m/channel/effect_setpanposition.go index 16725bb..fc4c27a 100644 --- a/format/s3m/channel/effect_setpanposition.go +++ b/format/s3m/channel/effect_setpanposition.go @@ -16,7 +16,7 @@ func (e SetPanPosition) Start(cs S3MChannel, p playback.Playback) error { x := uint8(e) & 0xf - cs.SetPan(s3mPanning.PanningFromS3M(x)) + cs.GetActiveState().Pan = s3mPanning.PanningFromS3M(x) return nil } diff --git a/format/s3m/channel/effect_stereocontrol.go b/format/s3m/channel/effect_stereocontrol.go index c953405..ed98a8d 100644 --- a/format/s3m/channel/effect_stereocontrol.go +++ b/format/s3m/channel/effect_stereocontrol.go @@ -16,10 +16,11 @@ func (e StereoControl) Start(cs S3MChannel, p playback.Playback) error { x := uint8(e) & 0xf + active := cs.GetActiveState() if x > 7 { - cs.SetPan(s3mPanning.PanningFromS3M(x - 8)) + active.Pan = s3mPanning.PanningFromS3M(x - 8) } else { - cs.SetPan(s3mPanning.PanningFromS3M(x + 8)) + active.Pan = s3mPanning.PanningFromS3M(x + 8) } return nil } diff --git a/format/s3m/channel/effect_surroundon.go b/format/s3m/channel/effect_surroundon.go index cfcf1a8..6db6659 100644 --- a/format/s3m/channel/effect_surroundon.go +++ b/format/s3m/channel/effect_surroundon.go @@ -15,7 +15,7 @@ func (e SurroundOn) Start(cs S3MChannel, p playback.Playback) error { cs.ResetRetriggerCount() // TODO: support for center rear panning - cs.SetPan(s3mPanning.DefaultPanning) + cs.GetActiveState().Pan = s3mPanning.DefaultPanning return nil } diff --git a/format/s3m/channel/util.go b/format/s3m/channel/util.go index 40b00aa..0f91629 100644 --- a/format/s3m/channel/util.go +++ b/format/s3m/channel/util.go @@ -34,9 +34,10 @@ type S3M interface { } func doVolSlide(cs S3MChannel, delta float32, multiplier float32) error { - av := cs.GetActiveVolume() + active := cs.GetActiveState() + av := active.GetVolume() v := s3mVolume.VolumeToS3M(av) - vol := int16((float32(v) + delta) * multiplier) + vol := int16(float32(v) + (delta * multiplier)) if vol >= 64 { vol = 63 } @@ -45,19 +46,20 @@ func doVolSlide(cs S3MChannel, delta float32, multiplier float32) error { } sv := s3mfile.Volume(vol) nv := s3mVolume.VolumeFromS3M(sv) - cs.SetActiveVolume(nv) + active.SetVolume(nv) return nil } func doPortaUp(cs S3MChannel, amount float32, multiplier float32) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } delta := int(amount * multiplier) cur = cur.PortaUp(delta) - cs.SetPeriod(cur) + active.Period = cur return nil } @@ -66,7 +68,8 @@ func doPortaUpToNote(cs S3MChannel, amount float32, multiplier float32, target p return nil } - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } @@ -77,24 +80,26 @@ func doPortaUpToNote(cs S3MChannel, amount float32, multiplier float32, target p if period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { cur = target } - cs.SetPeriod(cur) + active.Period = cur return nil } func doPortaDown(cs S3MChannel, amount float32, multiplier float32) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } delta := int(amount * multiplier) cur = cur.PortaDown(delta) - cs.SetPeriod(cur) + active.Period = cur return nil } func doPortaDownToNote(cs S3MChannel, amount float32, multiplier float32, target period.Amiga) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } @@ -105,7 +110,7 @@ func doPortaDownToNote(cs S3MChannel, amount float32, multiplier float32, target if period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { cur = target } - cs.SetPeriod(cur) + active.Period = cur return nil } @@ -145,7 +150,7 @@ func doArpeggio(cs S3MChannel, currentTick int, arpSemitoneADelta int8, arpSemit arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneBDelta) } cs.SetOverrideSemitone(arpSemitoneTarget) - cs.SetTargetPos(cs.GetPos()) + cs.GetTargetState().Pos = cs.GetActiveState().Pos return nil } @@ -159,11 +164,12 @@ var ( ) func doVolSlideTwoThirds(cs S3MChannel) error { - vol := s3mVolume.VolumeToS3M(cs.GetActiveVolume()) + active := cs.GetActiveState() + vol := s3mVolume.VolumeToS3M(active.GetVolume()) if vol >= 64 { vol = 63 } - cs.SetActiveVolume(s3mVolume.VolumeFromS3M(volSlideTwoThirdsTable[vol])) + active.SetVolume(s3mVolume.VolumeFromS3M(volSlideTwoThirdsTable[vol])) return nil } diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index c390323..8215840 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -78,14 +78,14 @@ func NewManager(song *layout.Song) (playback.Playback, error) { cs.SetSongDataInterface(song) cs.SetRenderChannel(oc) cs.SetGlobalVolume(m.GetGlobalVolume()) - cs.SetActiveVolume(ch.InitialVolume) + active := cs.GetActiveState() + active.SetVolume(ch.InitialVolume) if song.Head.Stereo { cs.SetPanEnabled(true) - cs.SetPan(ch.InitialPanning) + active.Pan = ch.InitialPanning } else { - cs.SetPanEnabled(true) - cs.SetPan(panning.CenterAhead) cs.SetPanEnabled(false) + active.Pan = panning.CenterAhead } cs.SetStoredSemitone(note.UnchangedSemitone) mem := &song.ChannelSettings[i].Memory diff --git a/format/s3m/playback/playback_command.go b/format/s3m/playback/playback_command.go index 565dd63..fb66c0f 100644 --- a/format/s3m/playback/playback_command.go +++ b/format/s3m/playback/playback_command.go @@ -19,7 +19,7 @@ func (o doNoteCalc) Process(p playback.Playback, cs *channelState) error { return nil } - if inst := cs.GetTargetInst(); inst != nil { + if inst := cs.GetTargetState().Instrument; inst != nil { cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) period := s3mPeriod.CalcSemitonePeriod(cs.Semitone, inst.GetFinetune(), inst.GetSampleRate()) o.UpdateFunc(period) @@ -61,7 +61,10 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last keyOff := false stop := false - if targetInst := cs.GetTargetInst(); targetInst != nil { + active := cs.GetActiveState() + target := cs.GetTargetState() + + if targetInst := target.Instrument; targetInst != nil { cs.SetInstrument(targetInst) keyOn = true } else { @@ -73,15 +76,15 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last nc.Release() nc.Fadeout() } - targetPeriod := cs.GetTargetPeriod() - cs.SetPeriod(targetPeriod) + targetPeriod := target.Period + active.Period = targetPeriod cs.SetPortaTargetPeriod(targetPeriod) } - cs.SetPos(cs.GetTargetPos()) + active.Pos = target.Pos - if inst := cs.GetInstrument(); inst != nil { - keyOff = inst.IsReleaseNote(n) - stop = inst.IsStopNote(n) + if active.Instrument != nil { + keyOff = active.Instrument.IsReleaseNote(n) + stop = active.Instrument.IsStopNote(n) } if nc := cs.GetVoice(); nc != nil { @@ -96,7 +99,7 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last nc.Fadeout() } else if stop { cs.SetInstrument(nil) - cs.NoteCut() + active.NoteCut() } } return nil @@ -106,7 +109,7 @@ func (m *manager) processVoiceUpdates(ch int, cs *channelState, currentTick int, if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() - cs.SetPeriod(arpeggioPeriod) + cs.GetActiveState().Period = arpeggioPeriod } return nil } diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index 23360b3..d38705b 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -165,7 +165,7 @@ func (d Data) ShortString() string { func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], d Data, s song.Data, cs playback.Channel[TPeriod, Memory, Data]) error { var n note.Note = note.EmptyNote{} - inst := cs.GetInstrument() + inst := cs.GetActiveState().Instrument prevInst := inst if d.HasNote() || d.HasInstrument() { diff --git a/format/xm/channel/effect_arpeggio.go b/format/xm/channel/effect_arpeggio.go index d41ce76..e7637f7 100644 --- a/format/xm/channel/effect_arpeggio.go +++ b/format/xm/channel/effect_arpeggio.go @@ -14,7 +14,7 @@ type Arpeggio[TPeriod period.Period] DataEffect // '0' func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { cs.ResetRetriggerCount() cs.UnfreezePlayback() - cs.SetPos(cs.GetTargetPos()) + cs.GetActiveState().Pos = cs.GetTargetState().Pos return nil } diff --git a/format/xm/channel/effect_notecut.go b/format/xm/channel/effect_notecut.go index 8e7f729..23435ec 100644 --- a/format/xm/channel/effect_notecut.go +++ b/format/xm/channel/effect_notecut.go @@ -21,7 +21,7 @@ func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p pla x := DataEffect(e) & 0xf if x != 0 && currentTick == int(x) { - cs.NoteCut() + cs.GetActiveState().NoteCut() } return nil } diff --git a/format/xm/channel/effect_panslide.go b/format/xm/channel/effect_panslide.go index 0f2aa3f..694c890 100644 --- a/format/xm/channel/effect_panslide.go +++ b/format/xm/channel/effect_panslide.go @@ -17,7 +17,8 @@ func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p p x := xx >> 4 y := xx & 0x0F - xp := DataEffect(xmPanning.PanningToXm(cs.GetPan())) + active := cs.GetActiveState() + xp := DataEffect(xmPanning.PanningToXm(active.Pan)) if x == 0 { // slide left y units if xp < y { @@ -33,7 +34,7 @@ func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p p xp += x } } - cs.SetPan(xmPanning.PanningFromXm(uint8(xp))) + active.Pan = xmPanning.PanningFromXm(uint8(xp)) return nil } diff --git a/format/xm/channel/effect_portatonote.go b/format/xm/channel/effect_portatonote.go index e001a7d..31005c1 100644 --- a/format/xm/channel/effect_portatonote.go +++ b/format/xm/channel/effect_portatonote.go @@ -17,7 +17,7 @@ func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], cs.ResetRetriggerCount() cs.UnfreezePlayback() if cmd := cs.GetChannelData(); cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetPeriod()) + cs.SetPortaTargetPeriod(cs.GetTargetState().Period) cs.SetNotePlayTick(false, note.ActionContinue, 0) } return nil @@ -32,7 +32,7 @@ func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p mem := cs.GetMemory() xx := mem.PortaToNote(DataEffect(e)) - current := cs.GetPeriod() + current := cs.GetActiveState().Period target := cs.GetPortaTargetPeriod() if period.ComparePeriods(current, target) == comparison.SpaceshipRightGreater { return doPortaUpToNote(cs, float32(xx), 4, target) // subtracts diff --git a/format/xm/channel/effect_retriggernote.go b/format/xm/channel/effect_retriggernote.go index e29e00c..7fbf082 100644 --- a/format/xm/channel/effect_retriggernote.go +++ b/format/xm/channel/effect_retriggernote.go @@ -28,7 +28,7 @@ func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) if DataEffect(rt) >= y { - cs.SetPos(sampling.Pos{}) + cs.GetActiveState().Pos = sampling.Pos{} cs.ResetRetriggerCount() } return nil diff --git a/format/xm/channel/effect_retrigvolslide.go b/format/xm/channel/effect_retrigvolslide.go index 950cdbf..11bf974 100644 --- a/format/xm/channel/effect_retrigvolslide.go +++ b/format/xm/channel/effect_retrigvolslide.go @@ -29,7 +29,7 @@ func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Da rt := cs.GetRetriggerCount() + 1 cs.SetRetriggerCount(rt) if DataEffect(rt) >= x { - cs.SetPos(sampling.Pos{}) + cs.GetActiveState().Pos = sampling.Pos{} cs.ResetRetriggerCount() switch x { case 1: diff --git a/format/xm/channel/effect_sampleoffset.go b/format/xm/channel/effect_sampleoffset.go index 7567c57..feb46c1 100644 --- a/format/xm/channel/effect_sampleoffset.go +++ b/format/xm/channel/effect_sampleoffset.go @@ -17,7 +17,7 @@ func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], cs.ResetRetriggerCount() mem := cs.GetMemory() xx := mem.SampleOffset(DataEffect(e)) - cs.SetTargetPos(sampling.Pos{Pos: int(xx) * 0x100}) + cs.GetTargetState().Pos = sampling.Pos{Pos: int(xx) * 0x100} return nil } diff --git a/format/xm/channel/effect_setcoarsepanposition.go b/format/xm/channel/effect_setcoarsepanposition.go index 27cdd2e..573f0d3 100644 --- a/format/xm/channel/effect_setcoarsepanposition.go +++ b/format/xm/channel/effect_setcoarsepanposition.go @@ -18,7 +18,7 @@ func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory xy := DataEffect(e) y := xy & 0x0F - cs.SetPan(xmPanning.PanningFromXm(uint8(y) << 4)) + cs.GetActiveState().Pan = xmPanning.PanningFromXm(uint8(y) << 4) return nil } diff --git a/format/xm/channel/effect_setfinetune.go b/format/xm/channel/effect_setfinetune.go index a4ed198..98cbe5c 100644 --- a/format/xm/channel/effect_setfinetune.go +++ b/format/xm/channel/effect_setfinetune.go @@ -15,7 +15,7 @@ type SetFinetune[TPeriod period.Period] DataEffect // 'E5x' func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { x := DataEffect(e) & 0xf - inst := cs.GetTargetInst() + inst := cs.GetTargetState().Instrument if inst != nil { ft := (note.Finetune(x) - 8) * 4 inst.SetFinetune(ft) diff --git a/format/xm/channel/effect_setpanposition.go b/format/xm/channel/effect_setpanposition.go index 9e43e91..957fb7d 100644 --- a/format/xm/channel/effect_setpanposition.go +++ b/format/xm/channel/effect_setpanposition.go @@ -17,7 +17,7 @@ func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data xx := uint8(e) - cs.SetPan(xmPanning.PanningFromXm(xx)) + cs.GetActiveState().Pan = xmPanning.PanningFromXm(xx) return nil } diff --git a/format/xm/channel/effect_setvolume.go b/format/xm/channel/effect_setvolume.go index 948ba90..02ef1e2 100644 --- a/format/xm/channel/effect_setvolume.go +++ b/format/xm/channel/effect_setvolume.go @@ -17,7 +17,8 @@ func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p xx := xmVolume.XmVolume(e) - cs.SetActiveVolume(xx.Volume()) + active := cs.GetActiveState() + active.SetVolume(xx.Volume()) return nil } diff --git a/format/xm/channel/util.go b/format/xm/channel/util.go index 2c8db29..3cecead 100644 --- a/format/xm/channel/util.go +++ b/format/xm/channel/util.go @@ -29,7 +29,8 @@ type XM interface { } func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], delta float32, multiplier float32) error { - av := cs.GetActiveVolume() + active := cs.GetActiveState() + av := active.GetVolume() v := xmVolume.ToVolumeXM(av) vol := int16((float32(v) + delta) * multiplier) if vol >= 0x40 { @@ -39,7 +40,7 @@ func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data vol = 0x00 } v = xmVolume.XmVolume(DataEffect(vol)) - cs.SetActiveVolume(v.Volume()) + active.SetVolume(v.Volume()) return nil } @@ -59,14 +60,15 @@ func doGlobalVolSlide(m XM, delta float32, multiplier float32) error { } func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } delta := int(amount * multiplier) cur = period.PortaUp(cur, delta) - cs.SetPeriod(cur) + active.Period = cur return nil } @@ -74,21 +76,23 @@ func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, if err := doPortaUp(cs, amount, multiplier); err != nil { return err } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { - cs.SetPeriod(target) + + if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipLeftGreater { + active.Period = target } return nil } func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - cur := cs.GetPeriod() + active := cs.GetActiveState() + cur := active.Period if cur.IsInvalid() { return nil } delta := int(amount * multiplier) cur = period.PortaDown(cur, delta) - cs.SetPeriod(cur) + active.Period = cur return nil } @@ -96,8 +100,8 @@ func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memor if err := doPortaDown(cs, amount, multiplier); err != nil { return err } - if cur := cs.GetPeriod(); period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { - cs.SetPeriod(target) + if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipRightGreater { + active.Period = target } return nil } @@ -138,7 +142,7 @@ func doArpeggio[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data arpSemitoneTarget = note.Semitone(int8(ns) + arpSemitoneBDelta) } cs.SetOverrideSemitone(arpSemitoneTarget) - cs.SetTargetPos(cs.GetPos()) + cs.GetTargetState().Pos = cs.GetActiveState().Pos return nil } @@ -152,7 +156,8 @@ var ( ) func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data]) error { - vol := xmVolume.ToVolumeXM(cs.GetActiveVolume()) + active := cs.GetActiveState() + vol := xmVolume.ToVolumeXM(active.GetVolume()) if vol >= 64 { vol = 63 } @@ -162,7 +167,7 @@ func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Mem v = 0x40 } - cs.SetActiveVolume(v.Volume()) + active.SetVolume(v.Volume()) return nil } diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index 7b46ba5..b32dd27 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -67,9 +67,10 @@ func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.Period cs.SetSongDataInterface(song) cs.SetRenderChannel(oc) cs.SetGlobalVolume(m.GetGlobalVolume()) - cs.SetActiveVolume(ch.InitialVolume) + active := cs.GetActiveState() + active.SetVolume(ch.InitialVolume) cs.SetPanEnabled(true) - cs.SetPan(ch.InitialPanning) + active.Pan = ch.InitialPanning cs.SetMemory(&song.ChannelSettings[i].Memory) cs.SetStoredSemitone(note.UnchangedSemitone) } diff --git a/format/xm/playback/playback_command.go b/format/xm/playback/playback_command.go index 8e6672c..db71a66 100644 --- a/format/xm/playback/playback_command.go +++ b/format/xm/playback/playback_command.go @@ -22,7 +22,7 @@ func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState return nil } - if inst := cs.GetTargetInst(); inst != nil { + if inst := cs.GetTargetState().Instrument; inst != nil { cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) period := xmPeriod.CalcSemitonePeriod[TPeriod](cs.Semitone, inst.GetFinetune(), inst.GetSampleRate()) o.UpdateFunc(period) @@ -63,9 +63,10 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod } stop := false noteAction := note.ActionContinue - targetPeriod := cs.GetTargetPeriod() - if targetTick, na := cs.WillTriggerOn(currentTick); !targetPeriod.IsInvalid() && targetTick { - if targetInst := cs.GetTargetInst(); targetInst != nil { + active := cs.GetActiveState() + target := cs.GetTargetState() + if targetTick, na := cs.WillTriggerOn(currentTick); !target.Period.IsInvalid() && targetTick { + if targetInst := target.Instrument; targetInst != nil { cs.SetInstrument(targetInst) keyOn = true noteAction = na @@ -80,14 +81,14 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod nc.Fadeout() } } - cs.SetPeriod(targetPeriod) - cs.SetPortaTargetPeriod(targetPeriod) + active.Period = target.Period + cs.SetPortaTargetPeriod(target.Period) } - cs.SetPos(cs.GetTargetPos()) + active.Pos = target.Pos } - if inst := cs.GetInstrument(); inst != nil { - keyOff = inst.IsReleaseNote(n) - stop = inst.IsStopNote(n) + if active.Instrument != nil { + keyOff = active.Instrument.IsReleaseNote(n) + stop = active.Instrument.IsStopNote(n) } var invalidPeriod TPeriod @@ -102,10 +103,10 @@ func (m *manager[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod if voice.IsVolumeEnvelopeEnabled(nc) { nc.Fadeout() } - cs.SetPeriod(invalidPeriod) + active.Period = invalidPeriod } else if stop { cs.SetInstrument(nil) - cs.SetPeriod(invalidPeriod) + active.Period = invalidPeriod } } return nil @@ -115,7 +116,7 @@ func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TP if cs.UsePeriodOverride { cs.UsePeriodOverride = false arpeggioPeriod := cs.GetPeriodOverride() - cs.SetPeriod(arpeggioPeriod) + cs.GetActiveState().Period = arpeggioPeriod } return nil } diff --git a/player/state/active.go b/player/state/active.go index 544844c..937d9ae 100644 --- a/player/state/active.go +++ b/player/state/active.go @@ -6,13 +6,14 @@ import ( "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback" "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice" ) // Active is the active state of a channel type Active[TPeriod period.Period] struct { - Playback[TPeriod] + playback.ChannelState[TPeriod] Voice voice.Voice PeriodDelta period.Delta } @@ -24,7 +25,7 @@ func (a *Active[TPeriod]) Reset() { a.Voice = nil } a.PeriodDelta = 0 - a.Playback.Reset() + a.ChannelState.Reset() } // Clone clones the active state so that various interfaces do not collide @@ -37,23 +38,6 @@ func (a *Active[TPeriod]) Clone() *Active[TPeriod] { return &c } -// Transitions the active state so that various interfaces do not collide -func (a *Active[TPeriod]) Transition() *Active[TPeriod] { - var c *Active[TPeriod] - if a.Voice != nil && !a.Voice.IsDone() { - c = &Active[TPeriod]{ - 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 @@ -86,7 +70,12 @@ func RenderStatesTogether[TPeriod period.Period](periodConverter period.PeriodCo } func (a *Active[TPeriod]) renderState(periodConverter period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { - if a.Period.IsInvalid() || a.Volume == 0 { + if a.Period.IsInvalid() { + return nil + } + + vol := a.GetVolume() + if vol == 0 { return nil } @@ -97,7 +86,7 @@ func (a *Active[TPeriod]) renderState(periodConverter period.PeriodConverter[TPe // Commit the playback settings to the note-control voice.SetPeriod(ncv, a.Period) - voice.SetVolume(ncv, a.Volume) + voice.SetVolume(ncv, vol) voice.SetPos(ncv, a.Pos) voice.SetPan(ncv, a.Pan) diff --git a/player/state/channel.go b/player/state/channel.go index 06775fe..6cd1aa4 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -2,8 +2,6 @@ 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" @@ -37,7 +35,7 @@ type SemitoneSetterFactory[TPeriod period.Period, TMemory any, TChannelData song // ChannelState is the state of a single channel type ChannelState[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { activeState Active[TPeriod] - targetState Playback[TPeriod] + targetState playback.ChannelState[TPeriod] prevState Active[TPeriod] ActiveEffects []playback.Effect @@ -80,7 +78,7 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) WillTriggerOn(tick int) // AdvanceRow will update the current state to make room for the next row's state data func (cs *ChannelState[TPeriod, TMemory, TChannelData]) AdvanceRow(txn ChannelDataTransaction[TPeriod, TMemory, TChannelData]) { cs.prevState = cs.activeState - cs.targetState = cs.activeState.Playback + cs.targetState = cs.activeState.ChannelState cs.Trigger.Reset() cs.RetriggerCount = 0 cs.activeState.PeriodDelta = 0 @@ -146,18 +144,6 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetMemory(mem *TMemory) cs.Memory = mem } -// GetActiveVolume returns the current active volume on the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetActiveVolume() volume.Volume { - return cs.activeState.Volume -} - -// SetActiveVolume sets the active volume on the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetActiveVolume(vol volume.Volume) { - if vol != volume.VolumeUseInstVol { - cs.activeState.Volume = vol - } -} - func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetSongDataInterface(s song.Data) { cs.s = s } @@ -202,16 +188,6 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPortaTargetPeriod(per } } -// GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTargetPeriod() TPeriod { - return cs.targetState.Period -} - -// SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetPeriod(period TPeriod) { - cs.targetState.Period = period -} - // GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriodOverride() TPeriod { return cs.periodOverride @@ -238,11 +214,6 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetVolumeActive(on bool) cs.volumeActive = on } -// GetInstrument returns the interface to the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetInstrument() *instrument.Instrument { - return cs.activeState.Instrument -} - // SetInstrument sets the interface to the active instrument func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetInstrument(inst *instrument.Instrument) { cs.activeState.Instrument = inst @@ -252,6 +223,8 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetInstrument(inst *inst } else { cs.activeState.Voice = voiceImpl.New[TPeriod](cs.PeriodConverter, inst, cs.RenderChannel) } + } else { + cs.activeState.Voice = nil } } @@ -260,21 +233,6 @@ func (cs *ChannelState[TPeriod, 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[TPeriod, 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[TPeriod, TMemory, TChannelData]) SetTargetInst(inst *instrument.Instrument) { - cs.targetState.Instrument = inst -} - -// GetPrevInst returns the interface to the last row's active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPrevInst() *instrument.Instrument { - return cs.prevState.Instrument -} - // GetPrevVoice returns the interface to the last row's active voice func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPrevVoice() voice.Voice { return cs.prevState.Voice @@ -285,36 +243,6 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetNoteSemitone() note.S return cs.StoredSemitone } -// GetTargetPos returns the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTargetPos() sampling.Pos { - return cs.targetState.Pos -} - -// SetTargetPos sets the soon-to-be-committed sample position of the instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetPos(pos sampling.Pos) { - cs.targetState.Pos = pos -} - -// GetPeriod returns the current sampler period of the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriod() TPeriod { - return cs.activeState.Period -} - -// SetPeriod sets the current sampler period of the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPeriod(period TPeriod) { - cs.activeState.Period = period -} - -// GetPos returns the sample position of the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPos() sampling.Pos { - return cs.activeState.Pos -} - -// SetPos sets the sample position of the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPos(pos sampling.Pos) { - cs.activeState.Pos = pos -} - // SetNotePlayTick sets the tick on which the note will retrigger func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetNotePlayTick(enabled bool, action note.Action, tick int) { if enabled { @@ -342,22 +270,12 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPanEnabled(on bool) { cs.PanEnabled = on } -// SetPan sets the active panning value of the channel -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) GetPan() panning.Position { - return cs.activeState.Pan -} - // SetTargetSemitone sets the target semitone for the channel func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetSemitone(st note.Semitone) { if cs.txn != nil { - cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, cs.SetTargetPeriod)) + cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, func(p TPeriod) { + cs.GetTargetState().Period = p + })) } } @@ -472,7 +390,14 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPitchEnvelopeEnable(e voice.EnablePitchEnvelope[TPeriod](cs.activeState.Voice, enabled) } -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) NoteCut() { - var empty TPeriod - cs.activeState.Period = empty +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPreviousState() playback.ChannelState[TPeriod] { + return cs.prevState.ChannelState +} + +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetActiveState() *playback.ChannelState[TPeriod] { + return &cs.activeState.ChannelState +} + +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTargetState() *playback.ChannelState[TPeriod] { + return &cs.targetState } diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go index bdd9c45..b1dfb0e 100644 --- a/player/state/channel_transaction.go +++ b/player/state/channel_transaction.go @@ -67,16 +67,17 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPreRow(p pl } func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { + target := cs.GetTargetState() if pos, ok := d.TargetPos.Get(); ok { - cs.SetTargetPos(pos) + target.Pos = pos } if inst, ok := d.TargetInst.Get(); ok { - cs.SetTargetInst(inst) + target.Instrument = inst } if period, ok := d.TargetPeriod.Get(); ok { - cs.SetTargetPeriod(period) + target.Period = period cs.SetPortaTargetPeriod(period) } @@ -89,7 +90,8 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitRow(p playb } if v, ok := d.TargetVolume.Get(); ok { - cs.SetActiveVolume(v) + cs.GetActiveState().SetVolume(v) + target.SetVolume(v) } na, targetTick := d.NoteAction.Get() @@ -97,7 +99,9 @@ func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitRow(p playb cs.SetNotePlayTick(targetTick, na, 0) if st, ok := d.NoteCalcST.Get(); ok { - d.AddNoteOp(semitoneSetterFactory(st, cs.SetTargetPeriod)) + d.AddNoteOp(semitoneSetterFactory(st, func(p TPeriod) { + target.Period = p + })) } return nil diff --git a/player/state/playback.go b/player/state/playback.go deleted file mode 100644 index 865cada..0000000 --- a/player/state/playback.go +++ /dev/null @@ -1,28 +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[TPeriod period.Period] struct { - Instrument *instrument.Instrument - Period TPeriod - Volume volume.Volume - Pos sampling.Pos - Pan panning.Position -} - -// Reset sets the render state to defaults -func (p *Playback[TPeriod]) Reset() { - p.Instrument = nil - var empty TPeriod - p.Period = empty - p.Pos = sampling.Pos{} - p.Pan = panning.CenterAhead -} From 4d2177e26ef437ef6a9ef13a9b2e47b7375eb459 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 24 Dec 2023 15:43:15 -0800 Subject: [PATCH 27/63] further cleanup --- channel.go | 2 + format/it/playback/playback.go | 14 ++- format/it/playback/playback_pattern.go | 6 +- format/it/playback/playback_render.go | 22 ++-- format/it/playback/playback_tracing.go | 137 --------------------- format/s3m/playback/playback.go | 14 ++- format/s3m/playback/playback_command.go | 12 +- format/s3m/playback/playback_pattern.go | 6 +- format/s3m/playback/playback_render.go | 22 ++-- format/xm/playback/playback.go | 14 ++- format/xm/playback/playback_pattern.go | 6 +- format/xm/playback/playback_render.go | 22 ++-- note/action.go | 19 +++ playback.go | 12 ++ player/state/channel.go | 13 ++ player/state/renderstate.go | 32 +++++ player/voice/config.go | 6 + player/voice/pcm.go | 34 +++--- player/voice/voice.go | 4 + tracing/channelstate.go | 30 +++++ tracing/tracing.go | 152 ++++++++++++++++++++++++ voice/component/sampler.go | 9 +- voice/component/vol0optimization.go | 45 +++++++ 23 files changed, 406 insertions(+), 227 deletions(-) delete mode 100644 format/it/playback/playback_tracing.go create mode 100644 player/state/renderstate.go create mode 100644 player/voice/config.go create mode 100644 tracing/channelstate.go create mode 100644 tracing/tracing.go create mode 100644 voice/component/vol0optimization.go diff --git a/channel.go b/channel.go index c80e592..06b52a4 100644 --- a/channel.go +++ b/channel.go @@ -50,6 +50,8 @@ type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] SetVolumeEnvelopeEnable(bool) SetPanningEnvelopeEnable(bool) SetPitchEnvelopeEnable(bool) + GetActiveEffects() []Effect + GetUseTargetPeriod() bool GetPreviousState() ChannelState[TPeriod] GetActiveState() *ChannelState[TPeriod] diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index 8b438a8..5fab1c8 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -24,11 +24,13 @@ import ( "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" "github.com/gotracker/playback/system" + "github.com/gotracker/playback/tracing" ) // manager is a playback manager for IT music type manager[TPeriod period.Period] struct { player.Tracker + tracing.Tracing[TPeriod, channel.Memory, channel.Data] song *layout.Song @@ -40,7 +42,7 @@ type manager[TPeriod period.Period] struct { postMixRowTxn *playpattern.RowUpdateTransaction premix *output.PremixData - rowRenderState *rowRenderState + rowRenderState *state.RowRenderState OnEffect func(playback.Effect) longChannelOutput bool enableNewNoteActions bool @@ -56,13 +58,17 @@ var _ playback.Channel[period.Amiga, channel.Memory, channel.Data] = (*state.Cha func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.PeriodConverter[TPeriod]) error { m.Tracker.BaseClockRate = it.GetBaseClock() m.song = song + m.Tracing.Playback = m + m.Tracing.ChannelGetter = func(c int) playback.Channel[TPeriod, channel.Memory, channel.Data] { + return m.GetChannel(c) + } m.PastNotes.SetMaxPerChannel(1) m.Tracker.PreTickable = m m.Tracker.Tickable = m m.Tracker.Premixable = m - m.Tracker.Traceable = m + m.Tracker.Traceable = &m.Tracing m.pattern.Reset() m.pattern.Orders = song.OrderList @@ -367,6 +373,10 @@ func (m *manager[TPeriod]) GetCurrentRow() index.Row { return m.pattern.GetCurrentRow() } +func (m *manager[TPeriod]) GetRenderState() playback.RowRenderState { + return m.rowRenderState +} + // GetName returns the current song's name func (m *manager[TPeriod]) GetName() string { return m.song.GetName() diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index 42b8bd3..dc7dbf0 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -69,7 +69,7 @@ func (m *manager[TPeriod]) processPatternRow() error { if m.rowRenderState == nil { panmixer := s.GetPanMixer() - m.rowRenderState = &rowRenderState{ + m.rowRenderState = &state.RowRenderState{ RenderDetails: state.RenderDetails{ Mix: s.Mixer(), SamplerSpeed: s.GetSamplerSpeed(), @@ -127,8 +127,8 @@ func (m *manager[TPeriod]) processPatternRow() error { m.rowRenderState.Duration = tickDuration m.rowRenderState.Samples = int(tickDuration.Seconds() * float64(s.SampleRate)) - m.rowRenderState.ticksThisRow = m.pattern.GetTicksThisRow() - m.rowRenderState.currentTick = 0 + m.rowRenderState.TicksThisRow = m.pattern.GetTicksThisRow() + m.rowRenderState.CurrentTick = 0 // run row processing, now that prestart has completed for channelNum := 0; channelNum < nch; channelNum++ { diff --git a/format/it/playback/playback_render.go b/format/it/playback/playback_render.go index 47a3ca0..90ed9d5 100644 --- a/format/it/playback/playback_render.go +++ b/format/it/playback/playback_render.go @@ -3,7 +3,6 @@ package playback import ( "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state" ) // OnPreTick runs the IT pre-tick processing @@ -23,7 +22,7 @@ func (m *manager[TPeriod]) OnTick() error { }() m.postMixRowTxn = postMixRowTxn - if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { + if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { if err := m.processPatternRow(); err != nil { return err } @@ -41,13 +40,13 @@ func (m *manager[TPeriod]) OnTick() error { finalData.Order = int(m.pattern.GetCurrentOrder()) finalData.Row = int(m.pattern.GetCurrentRow()) - finalData.Tick = m.rowRenderState.currentTick - if m.rowRenderState.currentTick == 0 { + finalData.Tick = m.rowRenderState.CurrentTick + if m.rowRenderState.CurrentTick == 0 { finalData.RowText = m.getRowText() } - m.rowRenderState.currentTick++ - if m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { + m.rowRenderState.CurrentTick++ + if m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { postMixRowTxn.AdvanceRow = true } @@ -64,16 +63,9 @@ func (m *manager[TPeriod]) GetPremixData() (*output.PremixData, error) { return m.premix, nil } -type rowRenderState struct { - state.RenderDetails - - ticksThisRow int - currentTick int -} - func (m *manager[TPeriod]) soundRenderTick(premix *output.PremixData) error { - tick := m.rowRenderState.currentTick - var lastTick = (tick+1 == m.rowRenderState.ticksThisRow) + tick := m.rowRenderState.CurrentTick + var lastTick = (tick+1 == m.rowRenderState.TicksThisRow) for ch := range m.channels { cs := &m.channels[ch] diff --git a/format/it/playback/playback_tracing.go b/format/it/playback/playback_tracing.go deleted file mode 100644 index 75de935..0000000 --- a/format/it/playback/playback_tracing.go +++ /dev/null @@ -1,137 +0,0 @@ -package playback - -import ( - "fmt" - "io" - "reflect" - "text/tabwriter" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/player" -) - -func (m *manager[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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 trackData string - effects := ch.GetActiveEffects() - if len(effects) == 0 { - effects = []playback.Effect{nil} - } - trackData = fmt.Sprint(ch.GetChannelData()) - for _, effect := range effects { - var ( - activeEffect string - activeEffectType string - ) - if effect != nil { - activeEffect = fmt.Sprint(effect) - activeEffectType = reflect.TypeOf(effect).Name() - } - 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/s3m/playback/playback.go b/format/s3m/playback/playback.go index 8215840..1647705 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -22,6 +22,7 @@ import ( "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" "github.com/gotracker/playback/system" + "github.com/gotracker/playback/tracing" ) type channelState = state.ChannelState[period.Amiga, channel.Memory, channel.Data] @@ -29,6 +30,7 @@ type channelState = state.ChannelState[period.Amiga, channel.Memory, channel.Dat // manager is a playback manager for S3M music type manager struct { player.Tracker + tracing.Tracing[period.Amiga, channel.Memory, channel.Data] song *layout.Song @@ -39,7 +41,7 @@ type manager struct { postMixRowTxn *playpattern.RowUpdateTransaction premix *output.PremixData - rowRenderState *rowRenderState + rowRenderState *state.RowRenderState OnEffect func(playback.Effect) chOrder [4][]*channelState @@ -58,8 +60,14 @@ func NewManager(song *layout.Song) (playback.Playback, error) { song: song, } + m.Tracing.Playback = &m + m.Tracing.ChannelGetter = func(c int) playback.Channel[period.Amiga, channel.Memory, channel.Data] { + return m.GetChannel(c) + } + m.Tracker.Tickable = &m m.Tracker.Premixable = &m + m.Tracker.Traceable = &m.Tracing m.pattern.Reset() m.pattern.Orders = song.OrderList @@ -344,6 +352,10 @@ func (m *manager) GetCurrentRow() index.Row { return m.pattern.GetCurrentRow() } +func (m *manager) GetRenderState() playback.RowRenderState { + return m.rowRenderState +} + // GetName returns the current song's name func (m *manager) GetName() string { return m.song.GetName() diff --git a/format/s3m/playback/playback_command.go b/format/s3m/playback/playback_command.go index fb66c0f..ed48fb4 100644 --- a/format/s3m/playback/playback_command.go +++ b/format/s3m/playback/playback_command.go @@ -72,10 +72,6 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last } if cs.UseTargetPeriod { - if nc := cs.GetVoice(); nc != nil { - nc.Release() - nc.Fadeout() - } targetPeriod := target.Period active.Period = targetPeriod cs.SetPortaTargetPeriod(targetPeriod) @@ -87,16 +83,16 @@ func (m *manager) processRowNote(ch int, cs *channelState, currentTick int, last stop = active.Instrument.IsStopNote(n) } - if nc := cs.GetVoice(); nc != nil { + if curVoice := cs.GetVoice(); curVoice != 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() + curVoice.Attack() mem := cs.GetMemory() mem.Retrigger() } else if keyOff { - nc.Release() - nc.Fadeout() + curVoice.Release() + curVoice.Fadeout() } else if stop { cs.SetInstrument(nil) active.NoteCut() diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index f11eca0..76958c1 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -70,7 +70,7 @@ func (m *manager) processPatternRow() error { if m.rowRenderState == nil { panmixer := s.GetPanMixer() - m.rowRenderState = &rowRenderState{ + m.rowRenderState = &state.RowRenderState{ RenderDetails: state.RenderDetails{ Mix: s.Mixer(), SamplerSpeed: s.GetSamplerSpeed(), @@ -128,8 +128,8 @@ func (m *manager) processPatternRow() error { m.rowRenderState.Duration = tickDuration m.rowRenderState.Samples = int(tickDuration.Seconds() * float64(s.SampleRate)) - m.rowRenderState.ticksThisRow = m.pattern.GetTicksThisRow() - m.rowRenderState.currentTick = 0 + m.rowRenderState.TicksThisRow = m.pattern.GetTicksThisRow() + m.rowRenderState.CurrentTick = 0 for _, order := range m.chOrder { for _, cs := range order { diff --git a/format/s3m/playback/playback_render.go b/format/s3m/playback/playback_render.go index f9c3335..e0387ef 100644 --- a/format/s3m/playback/playback_render.go +++ b/format/s3m/playback/playback_render.go @@ -3,7 +3,6 @@ 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 @@ -17,7 +16,7 @@ func (m *manager) OnTick() error { }() m.postMixRowTxn = postMixRowTxn - if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { + if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { if err := m.processPatternRow(); err != nil { return err } @@ -35,13 +34,13 @@ func (m *manager) OnTick() error { finalData.Order = int(m.pattern.GetCurrentOrder()) finalData.Row = int(m.pattern.GetCurrentRow()) - finalData.Tick = m.rowRenderState.currentTick - if m.rowRenderState.currentTick == 0 { + finalData.Tick = m.rowRenderState.CurrentTick + if m.rowRenderState.CurrentTick == 0 { finalData.RowText = m.getRowText() } - m.rowRenderState.currentTick++ - if m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { + m.rowRenderState.CurrentTick++ + if m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { postMixRowTxn.AdvanceRow = true } @@ -58,16 +57,9 @@ func (m *manager) GetPremixData() (*output.PremixData, error) { return m.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) + tick := m.rowRenderState.CurrentTick + var lastTick = (tick+1 == m.rowRenderState.TicksThisRow) for ch := range m.channels { cs := &m.channels[ch] diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index b32dd27..48a4858 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -21,11 +21,13 @@ import ( "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/tracing" ) // manager is a playback manager for XM music type manager[TPeriod period.Period] struct { player.Tracker + tracing.Tracing[TPeriod, channel.Memory, channel.Data] song *layout.Song @@ -36,7 +38,7 @@ type manager[TPeriod period.Period] struct { postMixRowTxn *playpattern.RowUpdateTransaction premix *output.PremixData - rowRenderState *rowRenderState + rowRenderState *state.RowRenderState OnEffect func(playback.Effect) } @@ -49,8 +51,14 @@ func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.Period m.Tracker.BaseClockRate = xmPeriod.XMBaseClock m.song = song + m.Tracing.Playback = m + m.Tracing.ChannelGetter = func(c int) playback.Channel[TPeriod, channel.Memory, channel.Data] { + return m.GetChannel(c) + } + m.Tracker.Tickable = m m.Tracker.Premixable = m + m.Tracker.Traceable = &m.Tracing m.pattern.Reset() m.pattern.Orders = song.OrderList @@ -337,6 +345,10 @@ func (m *manager[TPeriod]) GetCurrentRow() index.Row { return m.pattern.GetCurrentRow() } +func (m *manager[TPeriod]) GetRenderState() playback.RowRenderState { + return m.rowRenderState +} + // GetName returns the current song's name func (m *manager[TPeriod]) GetName() string { return m.song.GetName() diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index 02e4e67..b6e9da2 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -69,7 +69,7 @@ func (m *manager[TPeriod]) processPatternRow() error { if m.rowRenderState == nil { panmixer := s.GetPanMixer() - m.rowRenderState = &rowRenderState{ + m.rowRenderState = &state.RowRenderState{ RenderDetails: state.RenderDetails{ Mix: s.Mixer(), SamplerSpeed: s.GetSamplerSpeed(), @@ -127,8 +127,8 @@ func (m *manager[TPeriod]) processPatternRow() error { m.rowRenderState.Duration = tickDuration m.rowRenderState.Samples = int(tickDuration.Seconds() * float64(s.SampleRate)) - m.rowRenderState.ticksThisRow = m.pattern.GetTicksThisRow() - m.rowRenderState.currentTick = 0 + m.rowRenderState.TicksThisRow = m.pattern.GetTicksThisRow() + m.rowRenderState.CurrentTick = 0 // run row processing, now that prestart has completed for channelNum := 0; channelNum < nch; channelNum++ { diff --git a/format/xm/playback/playback_render.go b/format/xm/playback/playback_render.go index 44d6e4a..e8fe135 100644 --- a/format/xm/playback/playback_render.go +++ b/format/xm/playback/playback_render.go @@ -3,7 +3,6 @@ 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 @@ -17,7 +16,7 @@ func (m *manager[TPeriod]) OnTick() error { }() m.postMixRowTxn = postMixRowTxn - if m.rowRenderState == nil || m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { + if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { if err := m.processPatternRow(); err != nil { return err } @@ -35,13 +34,13 @@ func (m *manager[TPeriod]) OnTick() error { finalData.Order = int(m.pattern.GetCurrentOrder()) finalData.Row = int(m.pattern.GetCurrentRow()) - finalData.Tick = m.rowRenderState.currentTick - if m.rowRenderState.currentTick == 0 { + finalData.Tick = m.rowRenderState.CurrentTick + if m.rowRenderState.CurrentTick == 0 { finalData.RowText = m.getRowText() } - m.rowRenderState.currentTick++ - if m.rowRenderState.currentTick >= m.rowRenderState.ticksThisRow { + m.rowRenderState.CurrentTick++ + if m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { postMixRowTxn.AdvanceRow = true } @@ -58,16 +57,9 @@ func (m *manager[TPeriod]) GetPremixData() (*output.PremixData, error) { return m.premix, nil } -type rowRenderState struct { - state.RenderDetails - - ticksThisRow int - currentTick int -} - func (m *manager[TPeriod]) soundRenderTick(premix *output.PremixData) error { - tick := m.rowRenderState.currentTick - var lastTick = (tick+1 == m.rowRenderState.ticksThisRow) + tick := m.rowRenderState.CurrentTick + var lastTick = (tick+1 == m.rowRenderState.TicksThisRow) for ch := range m.channels { cs := &m.channels[ch] 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/playback.go b/playback.go index 05bd9b2..a70a26f 100644 --- a/playback.go +++ b/playback.go @@ -21,6 +21,7 @@ type Playback interface { GetOPL2Chip() render.OPL2Chip GetGlobalVolume() volume.Volume SetGlobalVolume(volume.Volume) + GetMixerVolume() volume.Volume Update(time.Duration, chan<- *output.PremixData) error Generate(time.Duration) (*output.PremixData, error) @@ -32,6 +33,7 @@ type Playback interface { SetNextOrder(index.Order) error SetNextRow(index.Row) error SetNextRowWithBacktrack(index.Row, bool) error + GetCurrentOrder() index.Order GetCurrentRow() index.Row Configure([]feature.Feature) error GetName() string @@ -41,5 +43,15 @@ type Playback interface { GetOnEffect() func(Effect) IgnoreUnknownEffect() bool + GetRenderState() RowRenderState + StartPatternTransaction() *pattern.RowUpdateTransaction } + +type RowRenderState interface { + GetSamplerSpeed() float32 + GetDuration() time.Duration + GetSamples() int + GetCurrentTick() int + GetTicksThisRow() int +} diff --git a/player/state/channel.go b/player/state/channel.go index 6cd1aa4..0e12200 100644 --- a/player/state/channel.go +++ b/player/state/channel.go @@ -216,16 +216,25 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetVolumeActive(on bool) // SetInstrument sets the interface to the active instrument func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetInstrument(inst *instrument.Instrument) { + prevVoice := cs.activeState.Voice + doClearPrev := false cs.activeState.Instrument = inst if inst != nil { if inst == cs.prevState.Instrument { cs.activeState.Voice = cs.prevState.Voice } else { + doClearPrev = true cs.activeState.Voice = voiceImpl.New[TPeriod](cs.PeriodConverter, inst, cs.RenderChannel) } } else { + doClearPrev = true cs.activeState.Voice = nil } + + if doClearPrev && prevVoice != nil { + prevVoice.Release() + prevVoice.Fadeout() + } } // GetVoice returns the active voice interface @@ -390,6 +399,10 @@ func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPitchEnvelopeEnable(e voice.EnablePitchEnvelope[TPeriod](cs.activeState.Voice, enabled) } +func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetUseTargetPeriod() bool { + return cs.UseTargetPeriod +} + func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPreviousState() playback.ChannelState[TPeriod] { return cs.prevState.ChannelState } diff --git a/player/state/renderstate.go b/player/state/renderstate.go new file mode 100644 index 0000000..8ad5667 --- /dev/null +++ b/player/state/renderstate.go @@ -0,0 +1,32 @@ +package state + +import ( + "time" +) + +type RowRenderState struct { + RenderDetails + + TicksThisRow int + CurrentTick int +} + +func (r RowRenderState) GetTicksThisRow() int { + return r.TicksThisRow +} + +func (r RowRenderState) GetCurrentTick() int { + return r.CurrentTick +} + +func (r RowRenderState) GetSamplerSpeed() float32 { + return r.RenderDetails.SamplerSpeed +} + +func (r RowRenderState) GetDuration() time.Duration { + return r.RenderDetails.Duration +} + +func (r RowRenderState) GetSamples() int { + return r.RenderDetails.Samples +} diff --git a/player/voice/config.go b/player/voice/config.go new file mode 100644 index 0000000..5198ce2 --- /dev/null +++ b/player/voice/config.go @@ -0,0 +1,6 @@ +package voice + +type Vol0OptimizationConfiguration struct { + Enabled bool + MaxTicksAt0 int +} diff --git a/player/voice/pcm.go b/player/voice/pcm.go index 378f128..3b8248c 100644 --- a/player/voice/pcm.go +++ b/player/voice/pcm.go @@ -31,14 +31,15 @@ type PCM[TPeriod period.Period] interface { // PCMConfiguration is the information needed to configure an PCM2 voice type PCMConfiguration[TPeriod period.Period] struct { - SampleRate period.Frequency - InitialVolume volume.Volume - InitialPeriod TPeriod - AutoVibrato voice.AutoVibrato - Data instrument.Data - OutputFilter voice.FilterApplier - VoiceFilter filter.Filter - PluginFilter filter.Filter + SampleRate period.Frequency + InitialVolume volume.Volume + InitialPeriod TPeriod + AutoVibrato voice.AutoVibrato + Data instrument.Data + OutputFilter voice.FilterApplier + VoiceFilter filter.Filter + PluginFilter filter.Filter + Vol0Optimization Vol0OptimizationConfiguration } // == the actual pcm voice == @@ -67,7 +68,7 @@ type pcmVoice[TPeriod period.Period] struct { pitchEnv component.PitchEnvelope[TPeriod] panEnv component.PanEnvelope filterEnv component.FilterEnvelope - vol0ticks int + vol0Opt component.Vol0Optimization done bool periodConverter period.PeriodConverter[TPeriod] @@ -122,6 +123,9 @@ func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPerio v.freq.ResetAutoVibratoAndSweep(config.AutoVibrato.Sweep) } + v.vol0Opt.SetEnabled(config.Vol0Optimization.Enabled) + v.vol0Opt.Init(config.Vol0Optimization.MaxTicksAt0) + return &v } @@ -129,7 +133,7 @@ func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPerio func (v *pcmVoice[TPeriod]) Attack() { v.keyOn = true - v.vol0ticks = 0 + v.vol0Opt.Reset() v.done = false v.amp.Attack() v.freq.ResetAutoVibrato() @@ -176,7 +180,7 @@ func (v *pcmVoice[TPeriod]) IsDone() bool { return v.amp.GetFadeoutVolume() <= 0 } - return v.vol0ticks >= 3 + return v.vol0Opt.IsDone() } // == SampleStream == @@ -413,11 +417,7 @@ func (v *pcmVoice[TPeriod]) Advance(tickDuration time.Duration) { v.voiceFilter.UpdateEnv(fval) } - if vol := v.GetFinalVolume(); vol <= 0 { - v.vol0ticks++ - } else { - v.vol0ticks = 0 - } + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) v.prevKeyOn = v.keyOn } @@ -451,7 +451,7 @@ func (v *pcmVoice[TPeriod]) Clone() voice.Voice { pitchEnv: v.pitchEnv.Clone(), panEnv: v.panEnv.Clone(), filterEnv: v.filterEnv.Clone(), - vol0ticks: 0, + vol0Opt: v.vol0Opt.Clone(), done: false, periodConverter: v.periodConverter, } diff --git a/player/voice/voice.go b/player/voice/voice.go index b8aaeec..1847f7b 100644 --- a/player/voice/voice.go +++ b/player/voice/voice.go @@ -31,6 +31,10 @@ func New[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], OutputFilter: output, VoiceFilter: voiceFilter, PluginFilter: pluginFilter, + Vol0Optimization: Vol0OptimizationConfiguration{ + Enabled: true, + MaxTicksAt0: 3, + }, }) case *instrument.OPL2: return NewOPL2[TPeriod](OPLConfiguration[TPeriod]{ diff --git a/tracing/channelstate.go b/tracing/channelstate.go new file mode 100644 index 0000000..e01f497 --- /dev/null +++ b/tracing/channelstate.go @@ -0,0 +1,30 @@ +package tracing + +import ( + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" +) + +func ChannelStateHeaders() []string { + return []string{ + "Instrument", + "Period", + "Volume", + "Position", + "Pan", + } +} + +func ChannelState[TPeriod period.Period](cs *playback.ChannelState[TPeriod]) []any { + var instStr string + if cs.Instrument != nil { + instStr = cs.Instrument.GetID().String() + } + return []any{ + instStr, + cs.Period, + cs.GetVolume(), + cs.Pos, + cs.Pan, + } +} diff --git a/tracing/tracing.go b/tracing/tracing.go new file mode 100644 index 0000000..99687f9 --- /dev/null +++ b/tracing/tracing.go @@ -0,0 +1,152 @@ +package tracing + +import ( + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/gotracker/playback" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player" + "github.com/gotracker/playback/song" +) + +type Tracing[TPeriod period.Period, TMemory any, TData song.ChannelData] struct { + playback.Playback + ChannelGetter func(c int) playback.Channel[TPeriod, TMemory, TData] +} + +func (m Tracing[TPeriod, TMemory, TData]) 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 Tracing[TPeriod, TMemory, TData]) outputGlobalTrace() func(w io.Writer) { + gs := player.NewTracingTable("=== global ===", + "globalVolume", + "mixerVolume", + "currentOrder", + "currentRow", + ) + gs.AddRow( + m.Playback.GetGlobalVolume(), + m.Playback.GetMixerVolume(), + m.Playback.GetCurrentOrder(), + m.Playback.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 Tracing[TPeriod, TMemory, TData]) outputRenderTrace() func(w io.Writer) { + r := m.Playback.GetRenderState() + if r == nil { + return func(w io.Writer) {} + } + + rs := player.NewTracingTable("=== rowRenderState ===", + "samplerSpeed", + "tickDuration", + "samplesPerTick", + "ticksThisRow", + "currentTick", + ) + rs.AddRow( + fmt.Sprint(r.GetSamplerSpeed()), + fmt.Sprint(r.GetDuration()), + fmt.Sprint(r.GetSamples()), + fmt.Sprint(r.GetTicksThisRow()), + fmt.Sprint(r.GetCurrentTick()), + ) + + 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 Tracing[TPeriod, TMemory, TData]) outputChannelsTrace() func(w io.Writer) { + cs := player.NewTracingTable("=== channels ===", + append( + []string{ + "Channel", + "ChannelVolume", + "ActiveEffect", + "TrackData", + "RetriggerCount", + "Semitone", + "UseTargetPeriod", + "NewNoteAction", + }, + ChannelStateHeaders()..., + )..., + ) + + for c := 0; c < m.Playback.GetNumChannels(); c++ { + ch := m.ChannelGetter(c) + if ch == nil { + continue + } + var trackData string + effects := ch.GetActiveEffects() + if len(effects) == 0 { + effects = []playback.Effect{nil} + } + trackData = fmt.Sprint(ch.GetChannelData()) + var activeEffect []string + for _, effect := range effects { + if effect != nil { + effectTypes := playback.GetEffectNames(effect) + activeEffect = append(activeEffect, strings.Join(effectTypes, ",")) + } + } + + cs.AddRow( + append( + []any{ + c + 1, + ch.GetChannelVolume(), + strings.Join(activeEffect, ","), + trackData, + ch.GetRetriggerCount(), + ch.GetNoteSemitone(), + ch.GetUseTargetPeriod(), + ch.GetNewNoteAction(), + }, + ChannelState[TPeriod](ch.GetActiveState())..., + )..., + ) + } + + 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/voice/component/sampler.go b/voice/component/sampler.go index f3b09ea..f0a2fe9 100644 --- a/voice/component/sampler.go +++ b/voice/component/sampler.go @@ -82,13 +82,8 @@ func (s *Sampler) GetSample(pos sampling.Pos) volume.Matrix { } 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 + if s.loopsEnabled { + return (s.keyOn && s.sustainLoop.Enabled()) || s.wholeLoop.Enabled() } return false } diff --git a/voice/component/vol0optimization.go b/voice/component/vol0optimization.go new file mode 100644 index 0000000..6612836 --- /dev/null +++ b/voice/component/vol0optimization.go @@ -0,0 +1,45 @@ +package component + +import "github.com/gotracker/gomixing/volume" + +type Vol0Optimization struct { + enabled bool + ticksAt0 int + maxTicksAt0 int +} + +func (c Vol0Optimization) Clone() Vol0Optimization { + return Vol0Optimization{ + enabled: c.enabled, + ticksAt0: 0, + maxTicksAt0: c.maxTicksAt0, + } +} + +func (c *Vol0Optimization) Init(maxTicksAt0 int) { + c.enabled = true + c.maxTicksAt0 = maxTicksAt0 + c.Reset() +} + +func (c *Vol0Optimization) SetEnabled(enabled bool) { + c.enabled = enabled +} + +func (c *Vol0Optimization) Reset() { + c.ticksAt0 = 0 +} + +func (c *Vol0Optimization) ObserveVolume(v volume.Volume) { + if c.enabled { + if v == 0 { + c.ticksAt0++ + } else { + c.ticksAt0 = 0 + } + } +} + +func (c Vol0Optimization) IsDone() bool { + return c.enabled && c.ticksAt0 >= c.maxTicksAt0 +} From 8e31e415e8ade18d8ec5d9d785b6a17971ccc4e8 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 24 Dec 2023 20:14:56 -0800 Subject: [PATCH 28/63] rework tracing to make issues more visible --- format/it/playback/playback_pattern.go | 21 ++++++++- format/it/playback/playback_render.go | 18 +++++-- format/s3m/playback/playback.go | 1 + format/s3m/playback/playback_pattern.go | 21 ++++++++- format/s3m/playback/playback_render.go | 19 ++++++-- format/xm/playback/playback.go | 1 + format/xm/playback/playback_pattern.go | 21 ++++++++- format/xm/playback/playback_render.go | 20 ++++++-- go.mod | 8 +++- go.sum | 11 +++++ player/tracker.go | 4 +- player/tracker_tracing.go | 49 +++++++++++++++++-- tracing/channelstate.go | 14 +++--- tracing/tracing.go | 62 ++++++++++++------------- 14 files changed, 207 insertions(+), 63 deletions(-) diff --git a/format/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go index dc7dbf0..35e2cfa 100644 --- a/format/it/playback/playback_pattern.go +++ b/format/it/playback/playback_pattern.go @@ -14,7 +14,7 @@ const ( tickBaseDuration = time.Duration(2500) * time.Millisecond ) -func (m *manager[TPeriod]) processPatternRow() error { +func (m *manager[TPeriod]) startProcessPatternRow() error { patIdx, err := m.pattern.GetCurrentPatternIdx() if err != nil { return err @@ -123,6 +123,25 @@ func (m *manager[TPeriod]) processPatternRow() error { return err } + return nil +} + +func (m *manager[TPeriod]) processPatternRow() error { + patIdx, err := m.pattern.GetCurrentPatternIdx() + if err != nil { + return err + } + rowIdx := m.pattern.GetCurrentRow() + + pat := m.song.GetPattern(patIdx) + row := pat.GetRow(rowIdx) + nch := row.GetNumChannels() + if actual := m.GetNumChannels(); nch > actual { + nch = actual + } + + s := m.GetSampler() + tickDuration := tickBaseDuration / time.Duration(m.pattern.GetTempo()) m.rowRenderState.Duration = tickDuration diff --git a/format/it/playback/playback_render.go b/format/it/playback/playback_render.go index 90ed9d5..7077250 100644 --- a/format/it/playback/playback_render.go +++ b/format/it/playback/playback_render.go @@ -8,19 +8,27 @@ import ( // OnPreTick runs the IT pre-tick processing func (m *manager[TPeriod]) OnPreTick() error { m.PastNotes.Update() + + m.premix = nil + + m.postMixRowTxn = m.pattern.StartTransaction() + + if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { + if err := m.startProcessPatternRow(); err != nil { + return err + } + } + return nil } // OnTick runs the IT tick processing func (m *manager[TPeriod]) OnTick() error { - m.premix = nil - - postMixRowTxn := m.pattern.StartTransaction() + postMixRowTxn := m.postMixRowTxn 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 { @@ -90,7 +98,7 @@ func (m *manager[TPeriod]) soundRenderTick(premix *output.PremixData) error { } /** unused in IT, so far -func (m *Manager) ensureOPL2() { +func (m *Manager[TPeriod]) ensureOPL2() { if opl2 := m.GetOPL2Chip(); opl2 == nil { if s := m.GetSampler(); s != nil { opl2 = render.NewOPL2Chip(uint32(s.SampleRate)) diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index 1647705..46297b3 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -65,6 +65,7 @@ func NewManager(song *layout.Song) (playback.Playback, error) { return m.GetChannel(c) } + m.Tracker.PreTickable = &m m.Tracker.Tickable = &m m.Tracker.Premixable = &m m.Tracker.Traceable = &m.Tracing diff --git a/format/s3m/playback/playback_pattern.go b/format/s3m/playback/playback_pattern.go index 76958c1..8c5e63b 100644 --- a/format/s3m/playback/playback_pattern.go +++ b/format/s3m/playback/playback_pattern.go @@ -15,7 +15,7 @@ const ( tickBaseDuration = time.Duration(2500) * time.Millisecond ) -func (m *manager) processPatternRow() error { +func (m *manager) startProcessPatternRow() error { patIdx, err := m.pattern.GetCurrentPatternIdx() if err != nil { return err @@ -124,6 +124,25 @@ func (m *manager) processPatternRow() error { return err } + return nil +} + +func (m *manager) processPatternRow() error { + patIdx, err := m.pattern.GetCurrentPatternIdx() + if err != nil { + return err + } + rowIdx := m.pattern.GetCurrentRow() + + pat := m.song.GetPattern(patIdx) + row := pat.GetRow(rowIdx) + nch := row.GetNumChannels() + if actual := m.GetNumChannels(); nch > actual { + nch = actual + } + + s := m.GetSampler() + tickDuration := tickBaseDuration / time.Duration(m.pattern.GetTempo()) m.rowRenderState.Duration = tickDuration diff --git a/format/s3m/playback/playback_render.go b/format/s3m/playback/playback_render.go index e0387ef..791eb66 100644 --- a/format/s3m/playback/playback_render.go +++ b/format/s3m/playback/playback_render.go @@ -5,16 +5,27 @@ import ( "github.com/gotracker/playback/player/render" ) -// OnTick runs the S3M tick processing -func (m *manager) OnTick() error { +// OnPreTick runs the S3M pre-tick processing +func (m *manager) OnPreTick() error { m.premix = nil + m.postMixRowTxn = m.pattern.StartTransaction() + + if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { + if err := m.startProcessPatternRow(); err != nil { + return err + } + } + + return nil +} - postMixRowTxn := m.pattern.StartTransaction() +// OnTick runs the S3M tick processing +func (m *manager) OnTick() error { + postMixRowTxn := m.postMixRowTxn 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 { diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index 48a4858..c05f9a4 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -56,6 +56,7 @@ func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.Period return m.GetChannel(c) } + m.Tracker.PreTickable = m m.Tracker.Tickable = m m.Tracker.Premixable = m m.Tracker.Traceable = &m.Tracing diff --git a/format/xm/playback/playback_pattern.go b/format/xm/playback/playback_pattern.go index b6e9da2..f897fd8 100644 --- a/format/xm/playback/playback_pattern.go +++ b/format/xm/playback/playback_pattern.go @@ -14,7 +14,7 @@ const ( tickBaseDuration = time.Duration(2500) * time.Millisecond ) -func (m *manager[TPeriod]) processPatternRow() error { +func (m *manager[TPeriod]) startProcessPatternRow() error { patIdx, err := m.pattern.GetCurrentPatternIdx() if err != nil { return err @@ -123,6 +123,25 @@ func (m *manager[TPeriod]) processPatternRow() error { return err } + return nil +} + +func (m *manager[TPeriod]) processPatternRow() error { + patIdx, err := m.pattern.GetCurrentPatternIdx() + if err != nil { + return err + } + rowIdx := m.pattern.GetCurrentRow() + + pat := m.song.GetPattern(patIdx) + row := pat.GetRow(rowIdx) + nch := row.GetNumChannels() + if actual := m.GetNumChannels(); nch > actual { + nch = actual + } + + s := m.GetSampler() + tickDuration := tickBaseDuration / time.Duration(m.pattern.GetTempo()) m.rowRenderState.Duration = tickDuration diff --git a/format/xm/playback/playback_render.go b/format/xm/playback/playback_render.go index e8fe135..7593e97 100644 --- a/format/xm/playback/playback_render.go +++ b/format/xm/playback/playback_render.go @@ -5,16 +5,28 @@ import ( "github.com/gotracker/playback/player/render" ) -// OnTick runs the XM tick processing -func (m *manager[TPeriod]) OnTick() error { +// OnPreTick runs the IT pre-tick processing +func (m *manager[TPeriod]) OnPreTick() error { m.premix = nil - postMixRowTxn := m.pattern.StartTransaction() + m.postMixRowTxn = m.pattern.StartTransaction() + + if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { + if err := m.startProcessPatternRow(); err != nil { + return err + } + } + + return nil +} + +// OnTick runs the IT tick processing +func (m *manager[TPeriod]) OnTick() error { + postMixRowTxn := m.postMixRowTxn 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 { diff --git a/go.mod b/go.mod index b58d5a2..56e78ab 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gotracker/playback go 1.21 require ( + github.com/fatih/color v1.16.0 github.com/gotracker/goaudiofile v1.0.15 github.com/gotracker/gomixing v1.3.0 github.com/gotracker/opl2 v1.0.1 @@ -11,4 +12,9 @@ require ( golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 ) -require github.com/pkg/errors v0.9.1 // indirect +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/sys v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index 9deebce..add20e9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/gotracker/goaudiofile v1.0.15 h1:90MtPQB1i99DSTradMol49VZlV4smJ5iLYGrdecfIps= github.com/gotracker/goaudiofile v1.0.15/go.mod h1:+biBmTEKcFRF4hCR1flCtqOA76p6OReIAwqqV+07N8Y= github.com/gotracker/gomixing v1.3.0 h1:L0pOTsjIppAbSoo+yYRVghrfF2dcAywUTsk6Ig2Z/IM= @@ -8,9 +10,18 @@ github.com/heucuva/comparison v1.0.0 h1:xxXNKS9GKHetQavOz35FitlAXWvmvM3U6M5IRIw7 github.com/heucuva/comparison v1.0.0/go.mod h1:5l0Va1uxFyy7S4DgdflnayxV2HStFwWI2rzbrlNNNMk= github.com/heucuva/optional v0.0.1 h1:tLbVBMQBKzQVfe43bHQFSxjhTzYcRK8frnTBG6FLksM= github.com/heucuva/optional v0.0.1/go.mod h1:2AtE/X9279wzrHLkCNvKl0xP7AiEIj3RijGKwbO8R3M= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/player/tracker.go b/player/tracker.go index a7715c7..9c6eed1 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -59,8 +59,6 @@ func (t *Tracker) Update(deltaTime time.Duration, out chan<- *output.PremixData) return err } - t.OutputTraces() - if premix != nil && len(premix.Data) > 0 { out <- premix } @@ -117,6 +115,8 @@ func (t *Tracker) renderTick() (*output.PremixData, error) { return nil, err } + t.OutputTraces() + if err := DoTick(t.Tickable); err != nil { return nil, err } diff --git a/player/tracker_tracing.go b/player/tracker_tracing.go index 8f1e869..1ead220 100644 --- a/player/tracker_tracing.go +++ b/player/tracker_tracing.go @@ -6,6 +6,9 @@ import ( "io" "strings" "sync" + "text/tabwriter" + + ansi "github.com/fatih/color" ) type tracingMsgFunc func() string @@ -49,9 +52,10 @@ type tracingColumn struct { } type TracingTable struct { - cols []*tracingColumn - name string - maxRows int + cols []*tracingColumn + rowColors []*ansi.Color + name string + maxRows int } func NewTracingTable(name string, headers ...string) TracingTable { @@ -67,9 +71,19 @@ func NewTracingTable(name string, headers ...string) TracingTable { } func (tt *TracingTable) AddRow(cols ...any) { + tt.AddRowColor(nil, cols...) +} + +func (tt *TracingTable) AddRowColor(c []ansi.Attribute, cols ...any) { + var cc *ansi.Color + if len(c) > 0 { + cc = ansi.Set(c...) + cc.EnableColor() + } + tt.rowColors = append(tt.rowColors, cc) for i, col := range cols { - c := tt.cols[i] - c.rows = append(c.rows, fmt.Sprint(col)) + tc := tt.cols[i] + tc.rows = append(tc.rows, fmt.Sprint(col)) } tt.maxRows++ } @@ -108,6 +122,31 @@ func (tt TracingTable) Fprintln(w io.Writer, colSep string, withRowNums bool) er return nil } +func (tt *TracingTable) WriteOut(w io.Writer) error { + var sb strings.Builder + tw := tabwriter.NewWriter(&sb, 1, 1, 1, ' ', 0) + if err := tt.Fprintln(tw, "\t", true); err != nil { + return err + } + if err := tw.Flush(); err != nil { + return err + } + + for r, txt := range strings.Split(sb.String(), "\n") { + var rc *ansi.Color + if rcr := r - 1; rcr >= 0 && rcr < len(tt.rowColors) { + rc = tt.rowColors[rcr] + } + if rc != nil { + txt = rc.Sprint(txt) + } + if _, err := fmt.Fprintln(w, txt); err != nil { + return err + } + } + return nil +} + type Traceable interface { OutputTraces(out chan<- func(w io.Writer)) } diff --git a/tracing/channelstate.go b/tracing/channelstate.go index e01f497..bf9a682 100644 --- a/tracing/channelstate.go +++ b/tracing/channelstate.go @@ -1,17 +1,19 @@ package tracing import ( + "fmt" + "github.com/gotracker/playback" "github.com/gotracker/playback/period" ) -func ChannelStateHeaders() []string { +func ChannelStateHeaders(preamble string) []string { return []string{ - "Instrument", - "Period", - "Volume", - "Position", - "Pan", + fmt.Sprintf("%s Instrument", preamble), + fmt.Sprintf("%s Period", preamble), + fmt.Sprintf("%s Volume", preamble), + fmt.Sprintf("%s Position", preamble), + fmt.Sprintf("%s Pan", preamble), } } diff --git a/tracing/tracing.go b/tracing/tracing.go index 99687f9..671c6bc 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -4,7 +4,8 @@ import ( "fmt" "io" "strings" - "text/tabwriter" + + ansi "github.com/fatih/color" "github.com/gotracker/playback" "github.com/gotracker/playback/period" @@ -49,11 +50,7 @@ func (m Tracing[TPeriod, TMemory, TData]) outputGlobalTrace() func(w io.Writer) return func(w io.Writer) { fmt.Fprintln(w) - - tw := tabwriter.NewWriter(w, 1, 1, 1, ' ', 0) - defer tw.Flush() - - gs.Fprintln(tw, "\t", false) + gs.WriteOut(w) } } @@ -80,17 +77,13 @@ func (m Tracing[TPeriod, TMemory, TData]) outputRenderTrace() func(w io.Writer) return func(w io.Writer) { fmt.Fprintln(w) - - tw := tabwriter.NewWriter(w, 1, 1, 1, ' ', 0) - defer tw.Flush() - - rs.Fprintln(tw, "\t", false) + rs.WriteOut(w) } } func (m Tracing[TPeriod, TMemory, TData]) outputChannelsTrace() func(w io.Writer) { cs := player.NewTracingTable("=== channels ===", - append( + append(append( []string{ "Channel", "ChannelVolume", @@ -101,7 +94,8 @@ func (m Tracing[TPeriod, TMemory, TData]) outputChannelsTrace() func(w io.Writer "UseTargetPeriod", "NewNoteAction", }, - ChannelStateHeaders()..., + ChannelStateHeaders("Previous")...), + ChannelStateHeaders("Active")..., )..., ) @@ -124,29 +118,31 @@ func (m Tracing[TPeriod, TMemory, TData]) outputChannelsTrace() func(w io.Writer } } - cs.AddRow( - append( - []any{ - c + 1, - ch.GetChannelVolume(), - strings.Join(activeEffect, ","), - trackData, - ch.GetRetriggerCount(), - ch.GetNoteSemitone(), - ch.GetUseTargetPeriod(), - ch.GetNewNoteAction(), - }, - ChannelState[TPeriod](ch.GetActiveState())..., - )..., - ) + prev := ch.GetPreviousState() + active := ch.GetActiveState() + + data := []any{ + c + 1, + ch.GetChannelVolume(), + strings.Join(activeEffect, ","), + trackData, + ch.GetRetriggerCount(), + ch.GetNoteSemitone(), + ch.GetUseTargetPeriod(), + ch.GetNewNoteAction(), + } + data = append(data, ChannelState[TPeriod](&prev)...) + data = append(data, ChannelState[TPeriod](active)...) + + if prev.Instrument != active.Instrument || any(prev.Period) != any(active.Period) || prev.GetVolume() != active.GetVolume() || prev.Pos != active.Pos || prev.Pan != active.Pan { + cs.AddRowColor([]ansi.Attribute{ansi.BgRed, ansi.FgHiWhite}, data...) + } else { + cs.AddRow(data...) + } } return func(w io.Writer) { fmt.Fprintln(w) - - tw := tabwriter.NewWriter(w, 1, 1, 1, ' ', 0) - defer tw.Flush() - - cs.Fprintln(tw, "\t", true) + cs.WriteOut(w) } } From 8155ef613600823b09a34ed56d11efa73ccf941e Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 12 Jan 2024 21:26:42 -0800 Subject: [PATCH 29/63] round 1 of playback cleanup --- channel.go | 27 +- channelstate.go | 29 +- effect.go | 124 ++-- format/common/loadformat.go | 7 +- format/it/channel/data.go | 70 ++- format/it/channel/effect_arpeggio.go | 28 +- .../it/channel/effect_channelvolumeslide.go | 66 +- .../it/channel/effect_extrafineportadown.go | 29 +- format/it/channel/effect_extrafineportaup.go | 28 +- format/it/channel/effect_finepatterndelay.go | 25 +- format/it/channel/effect_fineportadown.go | 30 +- format/it/channel/effect_fineportaup.go | 31 +- format/it/channel/effect_finevibrato.go | 34 +- format/it/channel/effect_finevolslidedown.go | 52 +- format/it/channel/effect_finevolslideup.go | 52 +- format/it/channel/effect_globalvolumeslide.go | 31 +- format/it/channel/effect_highoffset.go | 25 +- .../effect_newnoteactionnotecontinue.go | 19 +- .../it/channel/effect_newnoteactionnotecut.go | 19 +- .../channel/effect_newnoteactionnotefade.go | 19 +- .../it/channel/effect_newnoteactionnoteoff.go | 19 +- format/it/channel/effect_notecut.go | 26 +- format/it/channel/effect_notedelay.go | 21 +- format/it/channel/effect_orderjump.go | 19 +- format/it/channel/effect_panbrello.go | 43 ++ .../it/channel/effect_panningenvelopeoff.go | 19 +- format/it/channel/effect_panningenvelopeon.go | 19 +- format/it/channel/effect_pastnotecut.go | 19 +- format/it/channel/effect_pastnotefadeout.go | 19 +- format/it/channel/effect_pastnoteoff.go | 19 +- format/it/channel/effect_patterndelay.go | 22 +- format/it/channel/effect_patternloop.go | 38 +- format/it/channel/effect_pitchenvelopeoff.go | 19 +- format/it/channel/effect_pitchenvelopeon.go | 19 +- format/it/channel/effect_portadown.go | 27 +- format/it/channel/effect_portatonote.go | 50 +- format/it/channel/effect_portaup.go | 26 +- format/it/channel/effect_portavolslide.go | 10 +- format/it/channel/effect_retrigvolslide.go | 102 ++-- format/it/channel/effect_rowjump.go | 21 +- format/it/channel/effect_sampleoffset.go | 39 +- format/it/channel/effect_setchannelvolume.go | 32 +- .../it/channel/effect_setcoarsepanposition.go | 25 +- format/it/channel/effect_setfinetune.go | 32 +- format/it/channel/effect_setglobalvolume.go | 28 +- .../it/channel/effect_setpanbrellowaveform.go | 25 +- format/it/channel/effect_setpanposition.go | 25 +- format/it/channel/effect_setspeed.go | 26 +- format/it/channel/effect_settempo.go | 56 +- .../it/channel/effect_settremolowaveform.go | 25 +- .../it/channel/effect_setvibratowaveform.go | 25 +- format/it/channel/effect_tremolo.go | 34 +- format/it/channel/effect_tremor.go | 26 +- format/it/channel/effect_vibrato.go | 38 +- format/it/channel/effect_vibratovolslide.go | 10 +- format/it/channel/effect_volslidedown.go | 47 +- format/it/channel/effect_volslideup.go | 47 +- format/it/channel/effect_volumeenvelopeoff.go | 19 +- format/it/channel/effect_volumeenvelopeon.go | 19 +- format/it/channel/effectfactory.go | 22 +- format/it/channel/machine.go | 51 ++ format/it/channel/memory.go | 4 +- format/it/channel/sampleid.go | 2 +- format/it/channel/sharedmem.go | 2 - format/it/channel/unhandled.go | 45 +- format/it/channel/util.go | 181 ------ format/it/it.go | 8 + format/it/layout/channelsetting.go | 49 +- format/it/layout/header.go | 17 +- format/it/layout/noteinstrument.go | 4 +- format/it/layout/song.go | 166 +++++- format/it/load/instrument.go | 92 +-- format/it/load/itformat.go | 78 +-- format/it/panning/panning.go | 53 +- format/it/pattern/pattern.go | 6 +- format/it/pattern/row.go | 6 +- format/it/pattern/state.go | 52 +- format/it/period/util.go | 1 - format/it/playback/playback.go | 360 +---------- format/it/playback/playback_command.go | 186 ------ format/it/playback/playback_pattern.go | 178 ------ format/it/playback/playback_render.go | 113 ---- format/it/playback/playback_textoutput.go | 25 - format/it/settings/machine.go | 77 +++ format/it/settings/voicefactory.go | 20 + format/it/system/system.go | 14 +- format/it/voice/enveloper_filter.go | 40 ++ format/it/voice/enveloper_pan.go | 32 + format/it/voice/enveloper_pitch.go | 37 ++ format/it/voice/enveloper_volume.go | 32 + format/it/voice/modulator_amp.go | 62 ++ format/it/voice/modulator_freq.go | 32 + format/it/voice/modulator_pan.go | 50 ++ format/it/voice/render.go | 53 ++ format/it/voice/sampler.go | 72 +++ format/it/voice/tracing.go | 29 + format/it/voice/voice.go | 294 +++++++++ format/it/volume/finevolume.go | 58 ++ format/it/volume/volume.go | 47 +- format/s3m/channel/data.go | 37 +- format/s3m/channel/effect_arpeggio.go | 29 +- format/s3m/channel/effect_enablefilter.go | 27 +- .../s3m/channel/effect_extrafineportadown.go | 25 +- format/s3m/channel/effect_extrafineportaup.go | 25 +- format/s3m/channel/effect_finepatterndelay.go | 25 +- format/s3m/channel/effect_fineportadown.go | 25 +- format/s3m/channel/effect_fineportaup.go | 25 +- format/s3m/channel/effect_finevibrato.go | 32 +- format/s3m/channel/effect_finevolslidedown.go | 27 +- format/s3m/channel/effect_finevolslideup.go | 27 +- format/s3m/channel/effect_notecut.go | 27 +- format/s3m/channel/effect_notedelay.go | 23 +- format/s3m/channel/effect_orderjump.go | 21 +- format/s3m/channel/effect_patterndelay.go | 23 +- format/s3m/channel/effect_patternloop.go | 36 +- format/s3m/channel/effect_portadown.go | 33 +- format/s3m/channel/effect_portatonote.go | 53 +- format/s3m/channel/effect_portaup.go | 33 +- format/s3m/channel/effect_portavolslide.go | 10 +- format/s3m/channel/effect_retrigvolslide.go | 103 ++-- format/s3m/channel/effect_rowjump.go | 21 +- format/s3m/channel/effect_sampleoffset.go | 28 +- format/s3m/channel/effect_setfinetune.go | 29 +- format/s3m/channel/effect_setglobalvolume.go | 23 +- format/s3m/channel/effect_setpanposition.go | 21 +- format/s3m/channel/effect_setspeed.go | 27 +- format/s3m/channel/effect_settempo.go | 57 +- .../s3m/channel/effect_settremolowaveform.go | 26 +- .../s3m/channel/effect_setvibratowaveform.go | 26 +- format/s3m/channel/effect_stereocontrol.go | 22 +- format/s3m/channel/effect_surroundon.go | 17 +- format/s3m/channel/effect_tremolo.go | 28 +- format/s3m/channel/effect_tremor.go | 23 +- format/s3m/channel/effect_vibrato.go | 29 +- format/s3m/channel/effect_vibratovolslide.go | 10 +- format/s3m/channel/effect_volslidedown.go | 29 +- format/s3m/channel/effect_volslideup.go | 29 +- format/s3m/channel/effectfactory.go | 3 +- format/s3m/channel/machine.go | 79 +++ format/s3m/channel/memory.go | 15 +- format/s3m/channel/unhandled.go | 25 +- format/s3m/channel/util.go | 32 +- format/s3m/layout/channelsetting.go | 60 +- format/s3m/layout/header.go | 10 +- format/s3m/layout/song.go | 153 ++++- format/s3m/load/load.go | 4 +- format/s3m/load/modconv/modconverter.go | 5 +- format/s3m/load/s3mformat.go | 70 ++- format/s3m/panning/panning.go | 48 +- format/s3m/pattern/pattern.go | 3 +- format/s3m/pattern/row.go | 3 +- format/s3m/pattern/state.go | 3 +- format/s3m/playback/playback.go | 382 ++---------- format/s3m/playback/playback_command.go | 179 ------ format/s3m/playback/playback_pattern.go | 182 ------ format/s3m/playback/playback_render.go | 108 ---- format/s3m/playback/playback_textoutput.go | 25 - format/s3m/s3m.go | 6 + format/s3m/settings/machine.go | 54 ++ format/s3m/settings/voicefactory.go | 19 + format/s3m/system/system.go | 14 +- format/s3m/voice/render.go | 53 ++ format/s3m/voice/sampler.go | 69 +++ format/s3m/voice/tracing.go | 25 + format/s3m/voice/voice.go | 164 +++++ format/s3m/volume/finevolume.go | 53 ++ format/s3m/volume/volume.go | 59 +- format/xm/channel/data.go | 70 ++- format/xm/channel/effect_arpeggio.go | 25 +- .../xm/channel/effect_extrafineportadown.go | 31 +- format/xm/channel/effect_extrafineportaup.go | 31 +- format/xm/channel/effect_fineportadown.go | 31 +- format/xm/channel/effect_fineportaup.go | 31 +- format/xm/channel/effect_finevolslidedown.go | 30 +- format/xm/channel/effect_finevolslideup.go | 30 +- format/xm/channel/effect_globalvolumeslide.go | 34 +- format/xm/channel/effect_notecut.go | 27 +- format/xm/channel/effect_notedelay.go | 22 +- format/xm/channel/effect_orderjump.go | 19 +- format/xm/channel/effect_panslide.go | 33 +- format/xm/channel/effect_patterndelay.go | 22 +- format/xm/channel/effect_patternloop.go | 36 +- format/xm/channel/effect_portadown.go | 29 +- format/xm/channel/effect_portatonote.go | 46 +- format/xm/channel/effect_portaup.go | 29 +- format/xm/channel/effect_portavolslide.go | 10 +- format/xm/channel/effect_retriggernote.go | 33 +- format/xm/channel/effect_retrigvolslide.go | 102 ++-- format/xm/channel/effect_rowjump.go | 26 +- format/xm/channel/effect_sampleoffset.go | 26 +- .../xm/channel/effect_setcoarsepanposition.go | 22 +- .../xm/channel/effect_setenvelopeposition.go | 23 +- format/xm/channel/effect_setfinetune.go | 30 +- format/xm/channel/effect_setglobalvolume.go | 22 +- format/xm/channel/effect_setpanposition.go | 23 +- format/xm/channel/effect_setspeed.go | 29 +- format/xm/channel/effect_settempo.go | 35 +- .../xm/channel/effect_settremolowaveform.go | 24 +- .../xm/channel/effect_setvibratowaveform.go | 24 +- format/xm/channel/effect_setvolume.go | 20 +- format/xm/channel/effect_tremolo.go | 29 +- format/xm/channel/effect_tremor.go | 29 +- format/xm/channel/effect_vibrato.go | 30 +- format/xm/channel/effect_vibratovolslide.go | 10 +- format/xm/channel/effect_volslide.go | 31 +- format/xm/channel/effectfactory.go | 12 +- format/xm/channel/effectfactory_standard.go | 2 +- format/xm/channel/machine.go | 51 ++ format/xm/channel/unhandled.go | 42 +- format/xm/channel/util.go | 170 ------ format/xm/layout/channelsetting.go | 47 +- format/xm/layout/header.go | 17 +- format/xm/layout/song.go | 167 +++++- format/xm/load/xmformat.go | 127 ++-- format/xm/panning/panning.go | 58 +- format/xm/pattern/pattern.go | 6 +- format/xm/pattern/row.go | 6 +- format/xm/pattern/state.go | 52 +- format/xm/period/util.go | 67 +-- format/xm/playback/playback.go | 342 ++--------- format/xm/playback/playback_command.go | 190 ------ format/xm/playback/playback_pattern.go | 178 ------ format/xm/playback/playback_render.go | 111 ---- format/xm/playback/playback_textoutput.go | 25 - format/xm/settings/machine.go | 71 +++ format/xm/settings/voicefactory.go | 20 + format/xm/system/system.go | 21 +- format/xm/voice/enveloper_filter.go | 21 + format/xm/voice/enveloper_pan.go | 32 + format/xm/voice/enveloper_pitch.go | 25 + format/xm/voice/enveloper_volume.go | 32 + format/xm/voice/modulator_amp.go | 62 ++ format/xm/voice/modulator_freq.go | 32 + format/xm/voice/modulator_pan.go | 33 + format/xm/voice/render.go | 53 ++ format/xm/voice/sampler.go | 69 +++ format/xm/voice/tracing.go | 27 + format/xm/voice/voice.go | 241 ++++++++ format/xm/volume/voleffect.go | 2 +- format/xm/volume/volume.go | 51 +- format/xm/xm.go | 8 + index/pattern.go | 6 + instrument/instrument.go | 74 ++- instrument/pcm.go | 39 +- note/keyoct.go | 2 + oscillator/impulsetracker.go | 4 + oscillator/protracker.go | 6 +- period/amiga.go | 21 + period/amigaconverter.go | 82 ++- period/linear.go | 26 +- period/linearconverter.go | 73 ++- period/period.go | 17 + period/periodconverter.go | 13 + playback.go | 40 +- player/feature/startorderandrow.go | 8 + player/machine/channel.go | 59 ++ player/machine/channel_noteaction.go | 168 ++++++ player/machine/channel_notedecode.go | 124 ++++ player/machine/channel_tick.go | 169 ++++++ player/machine/instruction/instruction.go | 5 + player/machine/instruction/value.go | 5 + player/machine/machine.go | 243 ++++++++ player/machine/machine_channel.go | 562 ++++++++++++++++++ player/machine/machine_factory.go | 165 +++++ player/machine/machine_globals.go | 160 +++++ player/machine/machine_instruction.go | 71 +++ player/machine/machine_pastnote.go | 59 ++ player/machine/machine_singlerow.go | 49 ++ player/machine/machine_tick.go | 173 ++++++ player/machine/machine_tracing.go | 115 ++++ player/machine/newnoteinfo.go | 35 ++ player/machine/oscillator.go | 14 + player/machine/pastnote.go | 103 ++++ player/machine/settings/machinesettings.go | 26 + player/machine/settings/usersettings.go | 91 +++ player/machine/ticker.go | 141 +++++ player/op/channeltargets.go | 9 +- player/render/channel.go | 27 +- player/render/render.go | 8 +- player/state/active.go | 136 ----- player/state/channel.go | 416 ------------- player/state/channel_transaction.go | 182 ------ player/state/pastnotes.go | 127 ---- player/state/render/details.go | 15 + player/state/renderstate.go | 10 +- player/tracker.go | 140 ++--- player/tracker_tracing.go | 172 ------ player/voice/config.go | 6 - player/voice/opl2.go | 300 ---------- player/voice/pcm.go | 481 --------------- player/voice/transaction.go | 286 --------- player/voice/voice.go | 50 -- song/channeldata.go | 11 +- song/channelmemory.go | 5 + song/channelsettings.go | 52 ++ song/pattern.go | 17 +- song/periodcalculator.go | 22 + song/row.go | 19 +- song/song.go | 62 +- system/clockedsystem.go | 35 ++ system/system.go | 7 +- tracing/channelstate.go | 32 - tracing/entry.go | 68 +++ tracing/tick.go | 34 ++ tracing/tickchannel.go | 37 ++ tracing/tracechannel.go | 31 + tracing/tracer.go | 15 + tracing/tracing.go | 235 ++++---- tracing/valueupdate.go | 58 ++ voice/ampmodulator.go | 12 - .../settings.go} | 12 +- voice/component/envelope.go | 101 +++- voice/component/envelope_filter.go | 82 +-- voice/component/envelope_pan.go | 96 +-- voice/component/envelope_pitch.go | 86 +-- voice/component/envelope_volume.go | 94 +-- voice/component/modulator_amp.go | 125 ++-- voice/component/modulator_autovibrato.go | 94 +++ voice/component/modulator_fadeout.go | 82 +++ voice/component/modulator_freq.go | 113 +--- voice/component/modulator_key.go | 87 +++ voice/component/modulator_pan.go | 62 +- voice/component/modulator_pitchpan.go | 92 +++ voice/component/opl2.go | 61 +- voice/component/output.go | 5 +- voice/component/sampler.go | 125 ++-- voice/component/slimkeymodulator.go | 22 + voice/component/voicer.go | 19 + voice/component/vol0optimization.go | 37 +- voice/config.go | 34 ++ voice/envelope/envelope.go | 119 ++-- voice/envelope/instrumentenv.go | 29 +- voice/fadeout/fadeout.go | 13 + voice/{ => filter}/filterapplier.go | 6 +- voice/filterenveloper.go | 9 - voice/freqmodulator.go | 14 - voice/loop/disabled.go | 9 +- voice/oscillator/oscillator.go | 1 + voice/panenveloper.go | 13 - voice/panmodulator.go | 12 - voice/pitchenveloper.go | 13 - voice/positioner.go | 11 - voice/transaction.go | 51 -- voice/types/pandelta.go | 3 + voice/types/panning.go | 34 ++ voice/types/period.go | 9 + voice/types/volume.go | 29 + voice/types/volumedelta.go | 3 + voice/voice.go | 327 ++++------ voice/voicefactory.go | 5 + voice/vol0optimization/settings.go | 6 + voice/volumeenveloper.go | 13 - 352 files changed, 10740 insertions(+), 8653 deletions(-) create mode 100644 format/it/channel/effect_panbrello.go create mode 100644 format/it/channel/machine.go delete mode 100644 format/it/playback/playback_command.go delete mode 100644 format/it/playback/playback_pattern.go delete mode 100644 format/it/playback/playback_render.go delete mode 100644 format/it/playback/playback_textoutput.go create mode 100644 format/it/settings/machine.go create mode 100644 format/it/settings/voicefactory.go create mode 100644 format/it/voice/enveloper_filter.go create mode 100644 format/it/voice/enveloper_pan.go create mode 100644 format/it/voice/enveloper_pitch.go create mode 100644 format/it/voice/enveloper_volume.go create mode 100644 format/it/voice/modulator_amp.go create mode 100644 format/it/voice/modulator_freq.go create mode 100644 format/it/voice/modulator_pan.go create mode 100644 format/it/voice/render.go create mode 100644 format/it/voice/sampler.go create mode 100644 format/it/voice/tracing.go create mode 100644 format/it/voice/voice.go create mode 100644 format/it/volume/finevolume.go create mode 100644 format/s3m/channel/machine.go delete mode 100644 format/s3m/playback/playback_command.go delete mode 100644 format/s3m/playback/playback_pattern.go delete mode 100644 format/s3m/playback/playback_render.go delete mode 100644 format/s3m/playback/playback_textoutput.go create mode 100644 format/s3m/settings/machine.go create mode 100644 format/s3m/settings/voicefactory.go create mode 100644 format/s3m/voice/render.go create mode 100644 format/s3m/voice/sampler.go create mode 100644 format/s3m/voice/tracing.go create mode 100644 format/s3m/voice/voice.go create mode 100644 format/s3m/volume/finevolume.go create mode 100644 format/xm/channel/machine.go delete mode 100644 format/xm/playback/playback_command.go delete mode 100644 format/xm/playback/playback_pattern.go delete mode 100644 format/xm/playback/playback_render.go delete mode 100644 format/xm/playback/playback_textoutput.go create mode 100644 format/xm/settings/machine.go create mode 100644 format/xm/settings/voicefactory.go create mode 100644 format/xm/voice/enveloper_filter.go create mode 100644 format/xm/voice/enveloper_pan.go create mode 100644 format/xm/voice/enveloper_pitch.go create mode 100644 format/xm/voice/enveloper_volume.go create mode 100644 format/xm/voice/modulator_amp.go create mode 100644 format/xm/voice/modulator_freq.go create mode 100644 format/xm/voice/modulator_pan.go create mode 100644 format/xm/voice/render.go create mode 100644 format/xm/voice/sampler.go create mode 100644 format/xm/voice/tracing.go create mode 100644 format/xm/voice/voice.go create mode 100644 player/feature/startorderandrow.go create mode 100644 player/machine/channel.go create mode 100644 player/machine/channel_noteaction.go create mode 100644 player/machine/channel_notedecode.go create mode 100644 player/machine/channel_tick.go create mode 100644 player/machine/instruction/instruction.go create mode 100644 player/machine/instruction/value.go create mode 100644 player/machine/machine.go create mode 100644 player/machine/machine_channel.go create mode 100644 player/machine/machine_factory.go create mode 100644 player/machine/machine_globals.go create mode 100644 player/machine/machine_instruction.go create mode 100644 player/machine/machine_pastnote.go create mode 100644 player/machine/machine_singlerow.go create mode 100644 player/machine/machine_tick.go create mode 100644 player/machine/machine_tracing.go create mode 100644 player/machine/newnoteinfo.go create mode 100644 player/machine/oscillator.go create mode 100644 player/machine/pastnote.go create mode 100644 player/machine/settings/machinesettings.go create mode 100644 player/machine/settings/usersettings.go create mode 100644 player/machine/ticker.go delete mode 100644 player/state/active.go delete mode 100644 player/state/channel.go delete mode 100644 player/state/channel_transaction.go delete mode 100644 player/state/pastnotes.go create mode 100644 player/state/render/details.go delete mode 100644 player/tracker_tracing.go delete mode 100644 player/voice/config.go delete mode 100644 player/voice/opl2.go delete mode 100644 player/voice/pcm.go delete mode 100644 player/voice/transaction.go delete mode 100644 player/voice/voice.go create mode 100644 song/channelmemory.go create mode 100644 song/channelsettings.go create mode 100644 song/periodcalculator.go delete mode 100644 tracing/channelstate.go create mode 100644 tracing/entry.go create mode 100644 tracing/tick.go create mode 100644 tracing/tickchannel.go create mode 100644 tracing/tracechannel.go create mode 100644 tracing/tracer.go create mode 100644 tracing/valueupdate.go delete mode 100755 voice/ampmodulator.go rename voice/{autovibrato.go => autovibrato/settings.go} (61%) mode change 100755 => 100644 create mode 100644 voice/component/modulator_autovibrato.go create mode 100644 voice/component/modulator_fadeout.go create mode 100644 voice/component/modulator_key.go create mode 100644 voice/component/modulator_pitchpan.go create mode 100644 voice/component/slimkeymodulator.go create mode 100644 voice/component/voicer.go create mode 100644 voice/config.go rename voice/{ => filter}/filterapplier.go (53%) mode change 100755 => 100644 delete mode 100755 voice/filterenveloper.go delete mode 100755 voice/freqmodulator.go delete mode 100755 voice/panenveloper.go delete mode 100755 voice/panmodulator.go delete mode 100755 voice/pitchenveloper.go delete mode 100644 voice/positioner.go delete mode 100755 voice/transaction.go create mode 100644 voice/types/pandelta.go create mode 100644 voice/types/panning.go create mode 100644 voice/types/period.go create mode 100644 voice/types/volume.go create mode 100644 voice/types/volumedelta.go create mode 100644 voice/voicefactory.go create mode 100644 voice/vol0optimization/settings.go delete mode 100755 voice/volumeenveloper.go diff --git a/channel.go b/channel.go index 06b52a4..a09c4ef 100644 --- a/channel.go +++ b/channel.go @@ -1,7 +1,6 @@ package playback import ( - "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/player/render" @@ -13,10 +12,10 @@ import ( ) // Channel is an interface for channel state -type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { +type Channel[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { ResetRetriggerCount() - SetMemory(*TMemory) - GetMemory() *TMemory + SetMemory(TMemory) + GetMemory() TMemory FreezePlayback() UnfreezePlayback() GetChannelData() TChannelData @@ -25,7 +24,7 @@ type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] SetPeriodOverride(TPeriod) SetPeriodDelta(period.Delta) GetPeriodDelta() period.Delta - SetInstrument(*instrument.Instrument) + SetInstrument(*instrument.Instrument[TMixingVolume, TVolume, TPanning]) GetVoice() voice.Voice GetPrevVoice() voice.Voice GetNoteSemitone() note.Semitone @@ -36,12 +35,12 @@ type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] GetRetriggerCount() uint8 SetRetriggerCount(uint8) SetPanEnabled(bool) - SetRenderChannel(*render.Channel) - GetRenderChannel() *render.Channel + SetRenderChannel(*render.Channel[TGlobalVolume, TMixingVolume, TPanning]) + GetRenderChannel() *render.Channel[TGlobalVolume, TMixingVolume, TPanning] SetVolumeActive(bool) - SetGlobalVolume(volume.Volume) - SetChannelVolume(volume.Volume) - GetChannelVolume() volume.Volume + SetGlobalVolume(TGlobalVolume) + SetChannelVolume(TMixingVolume) + GetChannelVolume() TMixingVolume SetEnvelopePosition(int) TransitionActiveToPastState() SetNewNoteAction(note.Action) @@ -53,9 +52,9 @@ type Channel[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] GetActiveEffects() []Effect GetUseTargetPeriod() bool - GetPreviousState() ChannelState[TPeriod] - GetActiveState() *ChannelState[TPeriod] - GetTargetState() *ChannelState[TPeriod] + GetPreviousState() ChannelState[TPeriod, TVolume, TPanning] + GetActiveState() *ChannelState[TPeriod, TVolume, TPanning] + GetTargetState() *ChannelState[TPeriod, TVolume, TPanning] } -type ChannelTargeter[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(out *op.ChannelTargets[TPeriod], d TChannelData, s song.Data, cs Channel[TPeriod, TMemory, TChannelData]) error +type ChannelTargeter[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] func(out *op.ChannelTargets[TPeriod, TVolume, TPanning], d TChannelData, s song.Data, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error diff --git a/channelstate.go b/channelstate.go index 4db29de..f1f04e1 100644 --- a/channelstate.go +++ b/channelstate.go @@ -1,43 +1,42 @@ package playback 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" + "github.com/gotracker/playback/voice/types" ) // ChannelState is the information needed to make an instrument play -type ChannelState[TPeriod period.Period] struct { - Instrument *instrument.Instrument +type ChannelState[TPeriod types.Period, TVolume types.Volume, TPanning types.Panning] struct { + Instrument instrument.InstrumentIntf Period TPeriod - vol volume.Volume + vol TVolume Pos sampling.Pos - Pan panning.Position + Pan TPanning } // Reset sets the render state to defaults -func (s *ChannelState[TPeriod]) Reset() { +func (s *ChannelState[TPeriod, TVolume, TPanning]) Reset() { s.Instrument = nil - var empty TPeriod - s.Period = empty + var emptyPeriod TPeriod + s.Period = emptyPeriod s.Pos = sampling.Pos{} - s.Pan = panning.CenterAhead + var emptyPan TPanning + s.Pan = emptyPan } -func (s *ChannelState[TPeriod]) GetVolume() volume.Volume { +func (s *ChannelState[TPeriod, TVolume, TPanning]) GetVolume() TVolume { return s.vol } -func (s *ChannelState[TPeriod]) SetVolume(vol volume.Volume) { - if vol != volume.VolumeUseInstVol { +func (s *ChannelState[TPeriod, TVolume, TPanning]) SetVolume(vol TVolume) { + if !vol.IsUseInstrumentVol() { s.vol = vol } } -func (s *ChannelState[TPeriod]) NoteCut() { +func (s *ChannelState[TPeriod, TVolume, TPanning]) NoteCut() { var empty TPeriod s.Period = empty } diff --git a/effect.go b/effect.go index 6ce554a..89cdb1d 100644 --- a/effect.go +++ b/effect.go @@ -4,20 +4,23 @@ 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 Effecter[TMemory any] interface { - GetEffects(*TMemory, period.Period) []Effect +type Effecter[TMemory song.ChannelMemory] interface { + GetEffects(TMemory, period.Period) []Effect } -func GetEffects[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](mem *TMemory, d TChannelData) []Effect { +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 @@ -30,8 +33,8 @@ type EffectNamer interface { Names() []string } -type effectPreStartIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - PreStart(Channel[TPeriod, TMemory, TChannelData], Playback) error +type effectPreStartIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { + PreStart(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback) error } func GetEffectNames(e Effect) []string { @@ -44,8 +47,8 @@ func GetEffectNames(e Effect) []string { } // EffectPreStart triggers when the effect enters onto the channel state -func EffectPreStart[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { - if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory, TChannelData]); ok { +func EffectPreStart[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { + if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { if err := eff.PreStart(cs, p); err != nil { return err } @@ -53,13 +56,13 @@ func EffectPreStart[TPeriod period.Period, TMemory any, TChannelData song.Channe return nil } -type effectStartIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - Start(Channel[TPeriod, TMemory, TChannelData], Playback) error +type effectStartIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { + Start(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback) error } // EffectStart triggers on the first tick, but before the Tick() function is called -func EffectStart[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { - if eff, ok := e.(effectStartIntf[TPeriod, TMemory, TChannelData]); ok { +func EffectStart[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { + if eff, ok := e.(effectStartIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { if err := eff.Start(cs, p); err != nil { return err } @@ -67,27 +70,27 @@ func EffectStart[TPeriod period.Period, TMemory any, TChannelData song.ChannelDa return nil } -type effectTickIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - Tick(Channel[TPeriod, TMemory, TChannelData], Playback, int) error +type effectTickIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { + OldTick(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback, int) error } // EffectTick is called on every tick -func EffectTick[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback, currentTick int) error { - if eff, ok := e.(effectTickIntf[TPeriod, TMemory, TChannelData]); ok { - if err := eff.Tick(cs, p, currentTick); err != nil { +func EffectTick[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, currentTick int) error { + if eff, ok := e.(effectTickIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { + if err := eff.OldTick(cs, p, currentTick); err != nil { return err } } return nil } -type effectStopIntf[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - Stop(Channel[TPeriod, TMemory, TChannelData], Playback, int) error +type effectStopIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { + Stop(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback, int) error } // EffectStop is called on the last tick of the row, but after the Tick() function is called -func EffectStop[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback, lastTick int) error { - if eff, ok := e.(effectStopIntf[TPeriod, TMemory, TChannelData]); ok { +func EffectStop[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, lastTick int) error { + if eff, ok := e.(effectStopIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { if err := eff.Stop(cs, p, lastTick); err != nil { return err } @@ -96,14 +99,14 @@ func EffectStop[TPeriod period.Period, TMemory any, TChannelData song.ChannelDat } // CombinedEffect specifies multiple simultaneous effects into one -type CombinedEffect[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { +type CombinedEffect[TPeriod period.Period, TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume]] struct { Effects []Effect } // PreStart triggers when the effect enters onto the channel state -func (e CombinedEffect[TPeriod, TMemory, TChannelData]) PreStart(cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) PreStart(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { for _, effect := range e.Effects { - if err := EffectPreStart(effect, cs, p); err != nil { + if err := EffectPreStart[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p); err != nil { return err } } @@ -111,9 +114,9 @@ func (e CombinedEffect[TPeriod, TMemory, TChannelData]) PreStart(cs Channel[TPer } // Start triggers on the first tick, but before the Tick() function is called -func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Start(cs Channel[TPeriod, TMemory, TChannelData], p Playback) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) Start(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { for _, effect := range e.Effects { - if err := EffectStart(effect, cs, p); err != nil { + if err := EffectStart[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p); err != nil { return err } } @@ -121,9 +124,9 @@ func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Start(cs Channel[TPeriod } // Tick is called on every tick -func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Tick(cs Channel[TPeriod, TMemory, TChannelData], p Playback, currentTick int) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) OldTick(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, currentTick int) error { for _, effect := range e.Effects { - if err := EffectTick(effect, cs, p, currentTick); err != nil { + if err := EffectTick[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p, currentTick); err != nil { return err } } @@ -131,9 +134,9 @@ func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Tick(cs Channel[TPeriod, } // Stop is called on the last tick of the row, but after the Tick() function is called -func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Stop(cs Channel[TPeriod, TMemory, TChannelData], p Playback, lastTick int) error { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) Stop(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, lastTick int) error { for _, effect := range e.Effects { - if err := EffectStop(effect, cs, p, lastTick); err != nil { + if err := EffectStop[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p, lastTick); err != nil { return err } } @@ -141,7 +144,7 @@ func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Stop(cs Channel[TPeriod, } // String returns the string for the effect list -func (e CombinedEffect[TPeriod, TMemory, TChannelData]) String() string { +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) String() string { for _, eff := range e.Effects { s := fmt.Sprint(eff) if s != "" { @@ -151,7 +154,7 @@ func (e CombinedEffect[TPeriod, TMemory, TChannelData]) String() string { return "" } -func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Names() []string { +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)...) @@ -160,23 +163,74 @@ func (e CombinedEffect[TPeriod, TMemory, TChannelData]) Names() []string { } // DoEffect runs the standard tick lifetime of an effect -func DoEffect[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](e Effect, cs Channel[TPeriod, TMemory, TChannelData], p Playback, currentTick int, lastTick bool) error { +func DoEffect[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, currentTick int, lastTick bool) error { if e == nil { return nil } if currentTick == 0 { - if err := EffectStart(e, cs, p); err != nil { + if err := EffectStart[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](e, cs, p); err != nil { return err } } - if err := EffectTick(e, cs, p, currentTick); err != nil { + if err := EffectTick[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](e, cs, p, currentTick); err != nil { return err } if lastTick { - if err := EffectStop(e, cs, p, currentTick); err != nil { + if err := EffectStop[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](e, cs, p, currentTick); err != nil { return err } } return nil } + +//////// + +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 := m.DoInstructionRowStart(ch, effect); err != nil { + return err + } + } + return nil +} + +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) PreTick(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error { + for _, effect := range e.Effects { + if err := m.DoInstructionPreTick(ch, effect); err != nil { + return err + } + } + return nil +} + +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 := m.DoInstructionTick(ch, effect); err != nil { + return err + } + } + return nil +} + +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) PostTick(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error { + for _, effect := range e.Effects { + if err := m.DoInstructionPostTick(ch, effect); err != nil { + return err + } + } + return nil +} + +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 := m.DoInstructionRowEnd(ch, effect); err != nil { + return err + } + } + return nil +} + +func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) TraceData() string { + return e.String() +} diff --git a/format/common/loadformat.go b/format/common/loadformat.go index a9950c6..94726a7 100644 --- a/format/common/loadformat.go +++ b/format/common/loadformat.go @@ -5,13 +5,14 @@ import ( "github.com/gotracker/playback" "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 any] func(*TSong) (playback.Playback, error) +type ManagerFactory func(song.Data) (playback.Playback, error) -func Load[TSong any](r io.Reader, reader ReaderFunc[TSong], factory ManagerFactory[TSong], features []feature.Feature) (playback.Playback, error) { +func Load(r io.Reader, reader ReaderFunc, factory ManagerFactory, features []feature.Feature) (playback.Playback, error) { song, err := reader(r, features) if err != nil { return nil, err diff --git a/format/it/channel/data.go b/format/it/channel/data.go index 85cea8a..21d1bab 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -10,10 +10,14 @@ import ( "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/index" "github.com/gotracker/playback/instrument" "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/player/op" "github.com/gotracker/playback/song" ) @@ -35,7 +39,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 @@ -45,22 +49,22 @@ 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 { +func (d Data[TPeriod]) GetInstrument(stmem note.Semitone) instrument.ID { st := stmem if d.HasNote() { n := d.GetNote() @@ -75,7 +79,7 @@ func (d Data) GetInstrument(stmem note.Semitone) instrument.ID { } // 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 } @@ -85,12 +89,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 } @@ -103,27 +111,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 (d Data) GetEffects(mem *Memory, periodType period.Period) []playback.Effect { - switch periodType.(type) { - case period.Linear: - if e := EffectFactory[period.Linear](mem, d); e != nil { - return []playback.Effect{e} - } - case period.Amiga: - if e := EffectFactory[period.Amiga](mem, d); e != nil { - return []playback.Effect{e} - } - default: - panic("unhandled period type") +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) getNoteString(n note.Note) string { +func (Data[TPeriod]) getNoteString(n note.Note) string { switch note.Type(n) { case note.SpecialTypeRelease: return "===" @@ -136,7 +135,7 @@ func (Data) getNoteString(n note.Note) string { } } -func (d Data) String() string { +func (d Data[TPeriod]) String() string { pieces := []string{ "...", // note "..", // inst @@ -158,14 +157,29 @@ 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 GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], d Data, s song.Data, cs playback.Channel[TPeriod, Memory, Data]) error { +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 +} + +func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod, itVolume.Volume, itPanning.Panning], d Data[TPeriod], s song.Data, cs playback.Channel[TPeriod, *Memory, Data[TPeriod], itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { var n note.Note = note.EmptyNote{} inst := cs.GetActiveState().Instrument prevInst := inst @@ -200,7 +214,7 @@ func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], out.TargetPos.Set(sampling.Pos{}) if inst != nil { if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) + out.TargetVolume.Set(itVolume.ToItVolume(inst.GetDefaultVolumeGeneric())) } out.NoteAction.Set(note.ActionRetrigger) out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) @@ -225,9 +239,9 @@ func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], if d.HasVolume() { v := d.GetVolume() - if v == volume.VolumeUseInstVol { + if v.IsUseInstrumentVol() { if inst != nil { - v = inst.GetDefaultVolume() + v = itVolume.ToItVolume(inst.GetDefaultVolumeGeneric()) } } out.TargetVolume.Set(v) diff --git a/format/it/channel/effect_arpeggio.go b/format/it/channel/effect_arpeggio.go index 4dbb2ce..84e4998 100644 --- a/format/it/channel/effect_arpeggio.go +++ b/format/it/channel/effect_arpeggio.go @@ -3,29 +3,29 @@ 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Arpeggio defines an arpeggio effect type Arpeggio[TPeriod period.Period] DataEffect // 'J' -// Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - cs.GetActiveState().Pos = cs.GetTargetState().Pos - return nil +func (e Arpeggio[TPeriod]) String() string { + return fmt.Sprintf("J%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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(cs, currentTick, int8(x), int8(y)) + return doArpeggio(ch, m, tick, int8(x), int8(y)) } -func (e Arpeggio[TPeriod]) String() string { - return fmt.Sprintf("J%0.2x", DataEffect(e)) +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 index 22783cb..2e28374 100644 --- a/format/it/channel/effect_channelvolumeslide.go +++ b/format/it/channel/effect_channelvolumeslide.go @@ -3,66 +3,38 @@ package channel import ( "fmt" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e ChannelVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - mem := cs.GetMemory() - x, y := mem.ChannelVolumeSlide(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 +func (e ChannelVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("N%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e ChannelVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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: - vol := cs.GetChannelVolume() + (volume.Volume(x) / 64) - if vol > 1 { - vol = 1 - } - cs.SetChannelVolume(vol) + // slide up + return m.SlideChannelMixingVolume(ch, 1, float32(x)) 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 + // slide down + return m.SlideChannelMixingVolume(ch, 1, -float32(y)) + default: + return nil } - return nil } -func (e ChannelVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("N%0.2x", DataEffect(e)) +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 index bb6f4fa..c3107f6 100644 --- a/format/it/channel/effect_extrafineportadown.go +++ b/format/it/channel/effect_extrafineportadown.go @@ -3,24 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +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 + } - mem := cs.GetMemory() y := mem.PortaDown(DataEffect(e)) & 0x0F - return doPortaDown(cs, float32(y), 1) + if tick != 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(y)*1) } -func (e ExtraFinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 782cb21..6c87bd3 100644 --- a/format/it/channel/effect_extrafineportaup.go +++ b/format/it/channel/effect_extrafineportaup.go @@ -3,24 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +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 + } - mem := cs.GetMemory() y := mem.PortaUp(DataEffect(e)) & 0x0F - return doPortaUp(cs, float32(y), 1) + if tick != 0 { + return nil + } + return m.DoChannelPortaUp(ch, period.Delta(y)*1) } -func (e ExtraFinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +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 index 224ce06..9d1f59c 100644 --- a/format/it/channel/effect_finepatterndelay.go +++ b/format/it/channel/effect_finepatterndelay.go @@ -3,26 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e FinePatternDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - m := p.(IT) - if err := m.AddRowTicks(int(x)); err != nil { - return err - } - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 88d941c..4787218 100644 --- a/format/it/channel/effect_fineportadown.go +++ b/format/it/channel/effect_fineportadown.go @@ -3,24 +3,36 @@ package channel import ( "fmt" - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +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 + } - mem := cs.GetMemory() y := mem.PortaDown(DataEffect(e)) & 0x0F - return doPortaDown(cs, float32(y), 4) + return m.DoChannelPortaDown(ch, period.Delta(y)*system.SlideFinesPerSemitone) } -func (e FinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 3a6587c..9bab721 100644 --- a/format/it/channel/effect_fineportaup.go +++ b/format/it/channel/effect_fineportaup.go @@ -3,24 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +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 + } - mem := cs.GetMemory() - y := mem.PortaUp(DataEffect(e)) & 0x0F + y := mem.PortaUp(DataEffect(e)) - return doPortaUp(cs, float32(y), 4) + if tick != 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(y)*4) } -func (e FinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +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 index bc95b32..409d293 100644 --- a/format/it/channel/effect_finevibrato.go +++ b/format/it/channel/effect_finevibrato.go @@ -3,30 +3,36 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e FineVibrato[TPeriod]) String() string { + return fmt.Sprintf("U%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e FineVibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick != 0 { - return doVibrato(cs, currentTick, x, y, 1) + + if tick == 0 { + return nil } - 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]) String() string { - return fmt.Sprintf("U%0.2x", DataEffect(e)) +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 index 84edcb8..fee402f 100644 --- a/format/it/channel/effect_finevolslidedown.go +++ b/format/it/channel/effect_finevolslidedown.go @@ -3,32 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e FineVolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e FineVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - _, y := mem.VolumeSlide(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 + } - if y != 0x0F && currentTick == 0 { - return doVolSlide(cs, -float32(y), 1.0) + _, y := mem.VolumeSlide(DataEffect(e)) + if y != 0x0F && tick == 0 { + return m.SlideChannelVolume(ch, 1.0, -float32(y)) } return nil } -func (e FineVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e FineVolumeSlideDown[TPeriod]) TraceData() string { + return e.String() } //==================================================== @@ -36,14 +39,23 @@ func (e FineVolumeSlideDown[TPeriod]) String() string { // VolChanFineVolumeSlideDown defines a fine volume slide down effect (from the volume channel) type VolChanFineVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - mem := cs.GetMemory() - y := mem.VolChanVolumeSlide(DataEffect(e)) +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 + } - return doVolSlide(cs, -float32(y), 1.0) + _, y := mem.VolumeSlide(DataEffect(e)) + if tick == 0 { + return m.SlideChannelVolume(ch, 1.0, -float32(y)) + } + return nil } -func (e VolChanFineVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("dF%x", DataEffect(e)) +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 index bffc446..057ffbf 100644 --- a/format/it/channel/effect_finevolslideup.go +++ b/format/it/channel/effect_finevolslideup.go @@ -3,32 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e FineVolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e FineVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, _ := mem.VolumeSlide(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 + } - if x != 0x0F && currentTick == 0 { - return doVolSlide(cs, float32(x), 1.0) + x, _ := mem.VolumeSlide(DataEffect(e)) + if x != 0x0F && tick == 0 { + return m.SlideChannelVolume(ch, 1.0, float32(x)) } return nil } -func (e FineVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e FineVolumeSlideUp[TPeriod]) TraceData() string { + return e.String() } //==================================================== @@ -36,14 +39,23 @@ func (e FineVolumeSlideUp[TPeriod]) String() string { // VolChanFineVolumeSlideUp defines a fine volume slide up effect (from the volume channel) type VolChanFineVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolChanFineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - mem := cs.GetMemory() - x := mem.VolChanVolumeSlide(DataEffect(e)) +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 + } - return doVolSlide(cs, float32(x), 1.0) + x, _ := mem.VolumeSlide(DataEffect(e)) + if tick == 0 { + return m.SlideChannelVolume(ch, 1.0, float32(x)) + } + return nil } -func (e VolChanFineVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("d%xF", DataEffect(e)) +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 index c916089..2fcd512 100644 --- a/format/it/channel/effect_globalvolumeslide.go +++ b/format/it/channel/effect_globalvolumeslide.go @@ -3,40 +3,41 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e GlobalVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("W%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick == 0 { + if tick == 0 { return nil } - m := p.(IT) - if x == 0 { // global vol slide down - return doGlobalVolSlide(m, -float32(y), 1.0) + return m.SlideGlobalVolume(1, -float32(y)) } else if y == 0 { // global vol slide up - return doGlobalVolSlide(m, float32(y), 1.0) + return m.SlideGlobalVolume(1, float32(x)) } return nil } -func (e GlobalVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("W%0.2x", DataEffect(e)) +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 index 7980a3c..52f9365 100644 --- a/format/it/channel/effect_highoffset.go +++ b/format/it/channel/effect_highoffset.go @@ -3,24 +3,29 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e HighOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() - - xx := DataEffect(e) +func (e HighOffset[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - mem.HighOffset = int(xx) * 0x10000 +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index a523d61..998da0b 100644 --- a/format/it/channel/effect_newnoteactionnotecontinue.go +++ b/format/it/channel/effect_newnoteactionnotecontinue.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteContinue[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionContinue) - return nil -} - 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 index b99b7f9..7d46776 100644 --- a/format/it/channel/effect_newnoteactionnotecut.go +++ b/format/it/channel/effect_newnoteactionnotecut.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionCut) - return nil -} - 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 index e7bff81..c9d0768 100644 --- a/format/it/channel/effect_newnoteactionnotefade.go +++ b/format/it/channel/effect_newnoteactionnotefade.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionFadeout) - return nil -} - 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 index ff96af1..7fc2dde 100644 --- a/format/it/channel/effect_newnoteactionnoteoff.go +++ b/format/it/channel/effect_newnoteactionnoteoff.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e NewNoteActionNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.SetNewNoteAction(note.ActionRelease) - return nil -} - 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 index a02ba66..420ee7b 100644 --- a/format/it/channel/effect_notecut.go +++ b/format/it/channel/effect_notecut.go @@ -3,29 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e NoteCut[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - x := DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.FreezePlayback() - } - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index c454f61..e9e7284 100644 --- a/format/it/channel/effect_notedelay.go +++ b/format/it/channel/effect_notedelay.go @@ -3,26 +3,25 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) - return nil +func (e NoteDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 3f29cf4..1b101a6 100644 --- a/format/it/channel/effect_orderjump.go +++ b/format/it/channel/effect_orderjump.go @@ -3,25 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e OrderJump[TPeriod]) String() string { + return fmt.Sprintf("B%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { - return p.SetNextOrder(index.Order(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]) String() string { - return fmt.Sprintf("B%0.2x", DataEffect(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 index c99e4da..4a62056 100644 --- a/format/it/channel/effect_panningenvelopeoff.go +++ b/format/it/channel/effect_panningenvelopeoff.go @@ -3,21 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e PanningEnvelopeOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.SetPanningEnvelopeEnable(false) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 8e57c6d..51090ec 100644 --- a/format/it/channel/effect_panningenvelopeon.go +++ b/format/it/channel/effect_panningenvelopeon.go @@ -3,21 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PanningEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e PanningEnvelopeOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.SetPanningEnvelopeEnable(true) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 4276d77..e52f066 100644 --- a/format/it/channel/effect_pastnotecut.go +++ b/format/it/channel/effect_pastnotecut.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.DoPastNoteEffect(note.ActionCut) - return nil -} - 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 index c7100ca..0ab9b01 100644 --- a/format/it/channel/effect_pastnotefadeout.go +++ b/format/it/channel/effect_pastnotefadeout.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteFade[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.DoPastNoteEffect(note.ActionFadeout) - return nil -} - 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 index 777e2bf..295636c 100644 --- a/format/it/channel/effect_pastnoteoff.go +++ b/format/it/channel/effect_pastnoteoff.go @@ -3,20 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PastNoteOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.DoPastNoteEffect(note.ActionRelease) - return nil -} - 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 index 8ae5878..71d6820 100644 --- a/format/it/channel/effect_patterndelay.go +++ b/format/it/channel/effect_patterndelay.go @@ -3,25 +3,25 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - m := p.(IT) - return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) +func (e PatternDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 645fbfe..7c8bafb 100644 --- a/format/it/channel/effect_patternloop.go +++ b/format/it/channel/effect_patternloop.go @@ -3,42 +3,32 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e PatternLoop[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e PatternLoop[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { - x := uint8(e) & 0xF +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 - mem := cs.GetMemory() - pl := mem.GetPatternLoop() if x == 0 { - // set loop - pl.Start = p.GetCurrentRow() + // set loop start + return m.SetPatternLoopStart(ch) } 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) - } + // set loop end + count + return m.SetPatternLoops(ch, int(x)) } - return nil } -func (e PatternLoop[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index b4851b4..d0cb531 100644 --- a/format/it/channel/effect_pitchenvelopeoff.go +++ b/format/it/channel/effect_pitchenvelopeoff.go @@ -3,21 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e PitchEnvelopeOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.SetPitchEnvelopeEnable(false) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index d77bcbf..89973c3 100644 --- a/format/it/channel/effect_pitchenvelopeon.go +++ b/format/it/channel/effect_pitchenvelopeon.go @@ -3,21 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PitchEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e PitchEnvelopeOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.SetPitchEnvelopeEnable(true) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 63274a3..4ab15d1 100644 --- a/format/it/channel/effect_portadown.go +++ b/format/it/channel/effect_portadown.go @@ -3,28 +3,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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e PortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 doPortaDown(cs, float32(xx), 4) + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) } -func (e PortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 947e2b1..7948ce5 100644 --- a/format/it/channel/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -3,48 +3,38 @@ package channel import ( "fmt" - "github.com/gotracker/playback" - "github.com/gotracker/playback/note" + 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/heucuva/comparison" + "github.com/gotracker/playback/player/machine" ) // PortaToNote defines a portamento-to-note effect type PortaToNote[TPeriod period.Period] DataEffect // 'G' -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - if cmd := cs.GetChannelData(); cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetState().Period) - cs.SetNotePlayTick(false, note.ActionContinue, 0) - } - return nil +func (e PortaToNote[TPeriod]) String() string { + return fmt.Sprintf("G%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - xx := mem.PortaToNote(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) +} - // vibrato modifies current period for portamento - cur := cs.GetActiveState().Period - if cur.IsInvalid() { - return nil +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 } - cur = period.AddDelta(cur, 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) // subtracts - } else { - return doPortaDownToNote(cs, float32(xx), 4, ptp) // adds - } + + 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]) String() string { - return fmt.Sprintf("G%0.2x", DataEffect(e)) +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 index 43a2602..eba24e2 100644 --- a/format/it/channel/effect_portaup.go +++ b/format/it/channel/effect_portaup.go @@ -3,28 +3,30 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e PortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 doPortaUp(cs, float32(xx), 4) + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) } -func (e PortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +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 index 18746bb..62fc9ad 100644 --- a/format/it/channel/effect_portavolslide.go +++ b/format/it/channel/effect_portavolslide.go @@ -4,12 +4,14 @@ 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, Memory, Data] + playback.CombinedEffect[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning, *Memory, Data[TPeriod]] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object @@ -21,5 +23,9 @@ func NewPortaVolumeSlide[TPeriod period.Period](mem *Memory, cd Command, val Dat } func (e PortaVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("L%0.2x", e.Effects[0].(DataEffect)) + 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 index ae87e28..d29a01e 100644 --- a/format/it/channel/effect_retrigvolslide.go +++ b/format/it/channel/effect_retrigvolslide.go @@ -3,69 +3,71 @@ package channel import ( "fmt" - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RetrigVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("Q%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, y := mem.RetrigVolumeSlide(DataEffect(e)) - if y == 0 { +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 } - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if DataEffect(rt) >= x { - cs.GetActiveState().Pos = 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) - } + 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]) String() string { - return fmt.Sprintf("Q%0.2x", DataEffect(e)) +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 index 759c2fd..a4eed4f 100644 --- a/format/it/channel/effect_rowjump.go +++ b/format/it/channel/effect_rowjump.go @@ -3,27 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RowJump[TPeriod]) String() string { + return fmt.Sprintf("C%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { - r := DataEffect(e) - rowIdx := index.Row(r) - return p.SetNextRow(rowIdx) +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]) String() string { - return fmt.Sprintf("C%0.2x", DataEffect(e)) +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 index c19c63a..2ff2d01 100644 --- a/format/it/channel/effect_sampleoffset.go +++ b/format/it/channel/effect_sampleoffset.go @@ -5,31 +5,44 @@ import ( "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() +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} - target := cs.GetTargetState() if mem.Shared.OldEffectMode { - if inst := cs.GetActiveState().Instrument; inst != nil && inst.GetLength().Pos < pos.Pos { - target.Pos = pos + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err + } + if inst == nil || pos.Pos >= inst.GetLength().Pos { + return nil } - } else { - target.Pos = pos } - return nil + return m.SetChannelPos(ch, pos) } -func (e SampleOffset[TPeriod]) String() string { - return fmt.Sprintf("O%0.2x", DataEffect(e)) +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 index 44e233e..54c8771 100644 --- a/format/it/channel/effect_setchannelvolume.go +++ b/format/it/channel/effect_setchannelvolume.go @@ -3,33 +3,25 @@ package channel import ( "fmt" - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetChannelVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xx := DataEffect(e) - - cv := itfile.Volume(xx) - - vol := volume.Volume(cv.Value()) - if vol > 1 { - vol = 1 - } +func (e SetChannelVolume[TPeriod]) String() string { + return fmt.Sprintf("M%0.2x", DataEffect(e)) +} - cs.SetChannelVolume(vol) - return nil +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]) String() string { - return fmt.Sprintf("M%0.2x", DataEffect(e)) +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 index 973af0d..34f550e 100644 --- a/format/it/channel/effect_setcoarsepanposition.go +++ b/format/it/channel/effect_setcoarsepanposition.go @@ -3,28 +3,25 @@ package channel import ( "fmt" - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - - "github.com/gotracker/playback" 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf - - pan := itfile.PanValue(x << 2) +func (e SetCoarsePanPosition[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.GetActiveState().Pan = itPanning.FromItPanning(pan) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 51e2ec6..75b5894 100644 --- a/format/it/channel/effect_setfinetune.go +++ b/format/it/channel/effect_setfinetune.go @@ -3,32 +3,34 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - x := DataEffect(e) & 0xf +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 := cs.GetTargetState().Instrument - if inst != nil { - ft := (note.Finetune(x) - 8) * 4 - inst.SetFinetune(ft) + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err } - return nil -} -// Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() + ft := (note.Finetune(x) - 8) * 4 + inst.SetFinetune(ft) return nil } -func (e SetFinetune[TPeriod]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index bb14853..0805a16 100644 --- a/format/it/channel/effect_setglobalvolume.go +++ b/format/it/channel/effect_setglobalvolume.go @@ -3,31 +3,25 @@ package channel import ( "fmt" - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback" + 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' -// PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - v := volume.Volume(DataEffect(e)) / 0x80 - if v > 1 { - v = 1 - } - cs.SetChannelVolume(v) - return nil +func (e SetGlobalVolume[TPeriod]) String() string { + return fmt.Sprintf("V%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("V%0.2x", DataEffect(e)) +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 index 9e292d1..ada1f8f 100644 --- a/format/it/channel/effect_setpanbrellowaveform.go +++ b/format/it/channel/effect_setpanbrellowaveform.go @@ -3,27 +3,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" "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" ) // SetPanbrelloWaveform defines a set panbrello waveform effect type SetPanbrelloWaveform[TPeriod period.Period] DataEffect // 'S5x' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanbrelloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetPanbrelloWaveform[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - panb := mem.PanbrelloOscillator() - panb.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index dc409cf..87980ff 100644 --- a/format/it/channel/effect_setpanposition.go +++ b/format/it/channel/effect_setpanposition.go @@ -3,28 +3,25 @@ package channel import ( "fmt" - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - - "github.com/gotracker/playback" 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) - - pan := itfile.PanValue(x) +func (e SetPanPosition[TPeriod]) String() string { + return fmt.Sprintf("X%0.2x", DataEffect(e)) +} - cs.GetActiveState().Pan = itPanning.FromItPanning(pan) - return nil +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]) String() string { - return fmt.Sprintf("X%0.2x", DataEffect(e)) +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 index 067914d..5092fda 100644 --- a/format/it/channel/effect_setspeed.go +++ b/format/it/channel/effect_setspeed.go @@ -3,30 +3,24 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - if e != 0 { - m := p.(IT) - if err := m.SetTicks(int(e)); err != nil { - return err - } - } - return nil +func (e SetSpeed[TPeriod]) String() string { + return fmt.Sprintf("A%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("A%0.2x", DataEffect(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 index 38c99df..05def43 100644 --- a/format/it/channel/effect_settempo.go +++ b/format/it/channel/effect_settempo.go @@ -3,58 +3,48 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - if e > 0x20 { - m := p.(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[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e SetTempo[TPeriod]) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - m := p.(IT) +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 tempo - if currentTick != 0 { - mem := cs.GetMemory() - val := int(mem.TempoDecrease(DataEffect(e & 0x0F))) - if err := m.DecreaseTempo(val); err != nil { + 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 tempo - if currentTick != 0 { - mem := cs.GetMemory() - val := int(mem.TempoIncrease(DataEffect(e & 0x0F))) - if err := m.IncreaseTempo(val); err != nil { + 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 err := m.SetTempo(int(e)); err != nil { - return err + if tick == 0 { + return m.SetBPM(int(e)) } } return nil } -func (e SetTempo[TPeriod]) String() string { - return fmt.Sprintf("T%0.2x", DataEffect(e)) +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 index 11ef243..7a74c63 100644 --- a/format/it/channel/effect_settremolowaveform.go +++ b/format/it/channel/effect_settremolowaveform.go @@ -3,27 +3,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" "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" ) // SetTremoloWaveform defines a set tremolo waveform effect type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'S4x' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetTremoloWaveform[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - trem := mem.TremoloOscillator() - trem.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index c30194e..b18931d 100644 --- a/format/it/channel/effect_setvibratowaveform.go +++ b/format/it/channel/effect_setvibratowaveform.go @@ -3,27 +3,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" "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" ) // SetVibratoWaveform defines a set vibrato waveform effect type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'S3x' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetVibratoWaveform[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - vib := mem.VibratoOscillator() - vib.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e SetVibratoWaveform[TPeriod]) TraceData() string { + return e.String() } diff --git a/format/it/channel/effect_tremolo.go b/format/it/channel/effect_tremolo.go index 632c441..db7b40e 100644 --- a/format/it/channel/effect_tremolo.go +++ b/format/it/channel/effect_tremolo.go @@ -3,31 +3,39 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e Tremolo[TPeriod]) String() string { + return fmt.Sprintf("R%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick != 0 { - return doTremolo(cs, currentTick, x, y, 4) + if tick == 0 { + return nil } - 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]) String() string { - return fmt.Sprintf("R%0.2x", DataEffect(e)) +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 index 2e03157..1408545 100644 --- a/format/it/channel/effect_tremor.go +++ b/format/it/channel/effect_tremor.go @@ -3,26 +3,30 @@ 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Tremor defines a tremor effect type Tremor[TPeriod period.Period] DataEffect // 'I' -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e Tremor[TPeriod]) String() string { + return fmt.Sprintf("I%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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(cs, currentTick, int(x)+1, int(y)+1) + return doTremor(ch, m, int(x)+1, int(y)+1) } -func (e Tremor[TPeriod]) String() string { - return fmt.Sprintf("I%0.2x", DataEffect(e)) +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 index 7b26db1..c37f3ad 100644 --- a/format/it/channel/effect_vibrato.go +++ b/format/it/channel/effect_vibrato.go @@ -3,34 +3,40 @@ 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Vibrato defines a vibrato effect type Vibrato[TPeriod period.Period] DataEffect // 'H' -// Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e Vibrato[TPeriod]) String() string { + return fmt.Sprintf("H%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick != 0 { - return doVibrato(cs, currentTick, x, y, 8) + if tick == 0 { + return nil } - } else { - return doVibrato(cs, currentTick, x, y, 4) + mul = 8 } - return nil + 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]) String() string { - return fmt.Sprintf("H%0.2x", DataEffect(e)) +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 index 323bace..02504c1 100644 --- a/format/it/channel/effect_vibratovolslide.go +++ b/format/it/channel/effect_vibratovolslide.go @@ -4,12 +4,14 @@ 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, Memory, Data] + playback.CombinedEffect[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning, *Memory, Data[TPeriod]] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object @@ -21,5 +23,9 @@ func NewVibratoVolumeSlide[TPeriod period.Period](mem *Memory, cd Command, val D } func (e VibratoVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("K%0.2x", e.Effects[0].(DataEffect)) + 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 index a613f52..315e823 100644 --- a/format/it/channel/effect_volslidedown.go +++ b/format/it/channel/effect_volslidedown.go @@ -3,29 +3,32 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e VolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e VolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - _, y := mem.VolumeSlide(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 + } - return doVolSlide(cs, -float32(y), 1.0) + _, y := mem.VolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1.0, -float32(y)) } -func (e VolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e VolumeSlideDown[TPeriod]) TraceData() string { + return e.String() } //==================================================== @@ -33,14 +36,20 @@ func (e VolumeSlideDown[TPeriod]) String() string { // VolChanVolumeSlideDown defines a volume slide down effect (from the volume channel) type VolChanVolumeSlideDown[TPeriod period.Period] DataEffect // 'd' -// Tick is called on every tick -func (e VolChanVolumeSlideDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - y := mem.VolChanVolumeSlide(DataEffect(e)) +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 + } - return doVolSlide(cs, -float32(y), 1.0) + y := mem.VolChanVolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1, -float32(y)) } -func (e VolChanVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("d0%x", DataEffect(e)) +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 index 764f52d..3120827 100644 --- a/format/it/channel/effect_volslideup.go +++ b/format/it/channel/effect_volslideup.go @@ -3,29 +3,32 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e VolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e VolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x, _ := mem.VolumeSlide(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 + } - return doVolSlide(cs, float32(x), 1.0) + x, _ := mem.VolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1, float32(x)) } -func (e VolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e VolumeSlideUp[TPeriod]) TraceData() string { + return e.String() } //==================================================== @@ -33,14 +36,20 @@ func (e VolumeSlideUp[TPeriod]) String() string { // VolChanVolumeSlideUp defines a volume slide up effect (from the volume channel) type VolChanVolumeSlideUp[TPeriod period.Period] DataEffect // 'd' -// Tick is called on every tick -func (e VolChanVolumeSlideUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() - x := mem.VolChanVolumeSlide(DataEffect(e)) +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 + } - return doVolSlide(cs, float32(x), 1.0) + x := mem.VolChanVolumeSlide(DataEffect(e)) + return m.SlideChannelVolume(ch, 1, float32(x)) } -func (e VolChanVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("d%x0", DataEffect(e)) +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 index c5da35e..a9ddee3 100644 --- a/format/it/channel/effect_volumeenvelopeoff.go +++ b/format/it/channel/effect_volumeenvelopeoff.go @@ -3,21 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOff[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e VolumeEnvelopeOff[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.SetVolumeEnvelopeEnable(false) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index 49f63e6..8c689cd 100644 --- a/format/it/channel/effect_volumeenvelopeon.go +++ b/format/it/channel/effect_volumeenvelopeon.go @@ -3,21 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeEnvelopeOn[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e VolumeEnvelopeOn[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.SetVolumeEnvelopeEnable(true) - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e VolumeEnvelopeOn[TPeriod]) TraceData() string { + return e.String() } diff --git a/format/it/channel/effectfactory.go b/format/it/channel/effectfactory.go index b59181e..44be180 100644 --- a/format/it/channel/effectfactory.go +++ b/format/it/channel/effectfactory.go @@ -4,6 +4,8 @@ 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" ) @@ -12,7 +14,7 @@ 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, Memory, Data] + playback.CombinedEffect[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning, *Memory, Data[TPeriod]] eff EffectIT } @@ -23,13 +25,17 @@ func (e VolEff[TPeriod]) String() string { 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) EffectIT { +func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData[itVolume.Volume]) EffectIT { if data == nil { return nil } - d, _ := data.(Data) + d, _ := data.(Data[TPeriod]) if !d.What.HasCommand() && !d.What.HasVolPan() { return nil @@ -58,7 +64,7 @@ func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData) Ef } } -func standardEffectFactory[TPeriod period.Period](mem *Memory, data Data) EffectIT { +func standardEffectFactory[TPeriod period.Period](mem *Memory, data Data[TPeriod]) EffectIT { switch data.Effect + '@' { case '@': // unused return nil @@ -125,7 +131,7 @@ func standardEffectFactory[TPeriod period.Period](mem *Memory, data Data) Effect case 'X': // Set Pan Position return SetPanPosition[TPeriod](data.EffectParameter) case 'Y': // Panbrello - //return Panbrello[TPeriod](data.EffectParameter) + return Panbrello[TPeriod](data.EffectParameter) case 'Z': // MIDI Macro return nil // TODO: MIDIMacro default: @@ -133,7 +139,7 @@ func standardEffectFactory[TPeriod period.Period](mem *Memory, data Data) Effect return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func specialEffect[TPeriod period.Period](data Data) EffectIT { +func specialEffect[TPeriod period.Period](data Data[TPeriod]) EffectIT { switch data.EffectParameter >> 4 { case 0x0: // unused return nil @@ -172,7 +178,7 @@ func specialEffect[TPeriod period.Period](data Data) EffectIT { return UnhandledCommand[TPeriod]{Command: data.Effect, Info: data.EffectParameter} } -func specialNoteEffects[TPeriod period.Period](data Data) EffectIT { +func specialNoteEffects[TPeriod period.Period](data Data[TPeriod]) EffectIT { switch data.EffectParameter & 0xf { case 0x0: // Past Note Cut return PastNoteCut[TPeriod](data.EffectParameter) @@ -225,7 +231,7 @@ func volumeSlideFactory[TPeriod period.Period](mem *Memory, cd Command, ce DataE return nil } -func soundControlEffect[TPeriod period.Period](data Data) EffectIT { +func soundControlEffect[TPeriod period.Period](data Data[TPeriod]) EffectIT { switch data.EffectParameter & 0xF { case 0x0: // Surround Off case 0x1: // Surround On 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..a6508ed 100644 --- a/format/it/channel/memory.go +++ b/format/it/channel/memory.go @@ -133,8 +133,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 diff --git a/format/it/channel/sampleid.go b/format/it/channel/sampleid.go index b529a24..87c2423 100644 --- a/format/it/channel/sampleid.go +++ b/format/it/channel/sampleid.go @@ -18,5 +18,5 @@ func (s SampleID) IsEmpty() bool { } func (s SampleID) String() string { - return fmt.Sprint(s.InstID) + return fmt.Sprintf("%d(%v)", s.InstID, s.Semitone) } 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 index 4b9a265..c4f27d2 100644 --- a/format/it/channel/unhandled.go +++ b/format/it/channel/unhandled.go @@ -3,8 +3,11 @@ 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // UnhandledCommand is an unhandled command @@ -13,14 +16,6 @@ type UnhandledCommand[TPeriod period.Period] struct { Info DataEffect } -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m 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[TPeriod]) String() string { return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) } @@ -31,19 +26,24 @@ func (e UnhandledCommand[TPeriod]) Names() []string { } } -// UnhandledVolCommand is an unhandled volume command -type UnhandledVolCommand[TPeriod period.Period] struct { - Vol uint8 -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m IT) error { +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: volCmd:%0.2X", e.Vol)) + 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) } @@ -53,3 +53,14 @@ func (e UnhandledVolCommand[TPeriod]) Names() []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/channel/util.go b/format/it/channel/util.go index 4d671f9..65d7286 100644 --- a/format/it/channel/util.go +++ b/format/it/channel/util.go @@ -1,154 +1,5 @@ package channel -import ( - itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/oscillator" - - itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" -) - -// 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 -} - -func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], delta float32, multiplier float32) error { - active := cs.GetActiveState() - av := active.GetVolume() - 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) - active.SetVolume(nv) - return nil -} - -func doGlobalVolSlide(m 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 doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = period.PortaUp(cur, delta) - active.Period = cur - return nil -} - -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { - if err := doPortaUp[TPeriod](cs, amount, multiplier); err != nil { - return err - } - if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipLeftGreater { - active.Period = target - } - return nil -} - -func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = period.PortaDown(cur, delta) - active.Period = cur - return nil -} - -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { - if err := doPortaDown(cs, amount, multiplier); err != nil { - return err - } - if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipRightGreater { - active.Period = target - } - return nil -} - -func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { - mem := cs.GetMemory() - vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) - d := period.Delta(vib) - cs.SetPeriodDelta(d) - return nil -} - -func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, 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[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, 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.GetTargetState().Pos = cs.GetActiveState().Pos - return nil -} - var ( volSlideTwoThirdsTable = [...]DataEffect{ 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9, @@ -157,35 +8,3 @@ var ( 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, } ) - -func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data]) error { - active := cs.GetActiveState() - vol := itVolume.ToItVolume(active.GetVolume()) - if vol >= 0x10 && vol <= 0x50 { - vol -= 0x10 - if vol >= 64 { - vol = 63 - } - - v := volSlideTwoThirdsTable[vol] - if v >= 0x40 { - v = 0x40 - } - - vv := itfile.Volume(v) - active.SetVolume(itVolume.FromItVolume(vv)) - } - return nil -} - -func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return doVolSlide(cs, delta, 1.0) -} - -func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { - delta := o.GetWave(float32(depth) * multiplier) - o.Advance(int(speed)) - return delta -} diff --git a/format/it/it.go b/format/it/it.go index bc74995..9cdbbe9 100644 --- a/format/it/it.go +++ b/format/it/it.go @@ -6,7 +6,10 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/it/load" + "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/util" ) @@ -31,3 +34,8 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { return load.IT(r, features) } + +func init() { + machine.RegisterMachine(settings.GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(settings.GetMachineSettings[period.Linear]()) +} diff --git a/format/it/layout/channelsetting.go b/format/it/layout/channelsetting.go index 925128e..6b7cfd6 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -1,17 +1,56 @@ package layout import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" "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/song" ) // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled bool OutputChannelNum int - InitialVolume volume.Volume - ChannelVolume volume.Volume - InitialPanning panning.Position + InitialVolume itVolume.Volume + ChannelVolume itVolume.FineVolume + InitialPanning itPanning.Panning Memory channel.Memory } + +var _ song.ChannelSettings = (*ChannelSetting)(nil) + +func (c ChannelSetting) GetEnabled() bool { + return c.Enabled +} + +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 { + return c.InitialPanning +} + +func (c ChannelSetting) GetMemory() song.ChannelMemory { + return &c.Memory +} + +func (c ChannelSetting) GetPanEnabled() bool { + return true +} + +func (c ChannelSetting) GetDefaultFilterName() string { + return "" +} + +func (c ChannelSetting) IsDefaultFilterEnabled() bool { + return false +} 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..2e716c0 100644 --- a/format/it/layout/noteinstrument.go +++ b/format/it/layout/noteinstrument.go @@ -1,6 +1,8 @@ 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" ) @@ -8,5 +10,5 @@ import ( // NoteInstrument is the note remapping and instrument pair type NoteInstrument struct { NoteRemap note.Semitone - Inst *instrument.Instrument + Inst *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] } diff --git a/format/it/layout/song.go b/format/it/layout/song.go index 8e207a4..5eb1e72 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -1,56 +1,161 @@ package layout import ( + "reflect" + "time" + + "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" "github.com/gotracker/playback/format/it/pattern" + itPeriod "github.com/gotracker/playback/format/it/period" + 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/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/system" ) -// Song is the full definition of the song data of an Song file -type Song struct { +// Song is the full definition of the song data of an IT file +type Song[TPeriod period.Period] struct { + System system.System Head Header - Instruments map[uint8]*instrument.Instrument + Instruments map[uint8]*instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument - Patterns []pattern.Pattern + Patterns []pattern.Pattern[TPeriod] ChannelSettings []ChannelSetting OrderList []index.Pattern FilterPlugins map[int]filter.Factory } +func (s Song[TPeriod]) GetPeriodType() reflect.Type { + if s.Head.LinearFreqSlides { + var p period.Linear + return reflect.TypeOf(p) + } else { + var p period.Amiga + return reflect.TypeOf(p) + } +} + +func (Song[TPeriod]) GetGlobalVolumeType() reflect.Type { + var gv itVolume.FineVolume + return reflect.TypeOf(gv) +} + +func (Song[TPeriod]) GetChannelMixingVolumeType() reflect.Type { + var cmv itVolume.FineVolume + return reflect.TypeOf(cmv) +} + +func (Song[TPeriod]) GetChannelVolumeType() reflect.Type { + var cv itVolume.Volume + return reflect.TypeOf(cv) +} + +func (Song[TPeriod]) GetChannelPanningType() reflect.Type { + var cp itPanning.Panning + return reflect.TypeOf(cp) +} + // GetOrderList returns the list of all pattern orders for the song -func (s Song) GetOrderList() []index.Pattern { +func (s Song[TPeriod]) GetOrderList() []index.Pattern { return s.OrderList } -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) song.Pattern[channel.Data] { +// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song +func (s Song[TPeriod]) GetInitialBPM() int { + return s.Head.InitialTempo +} + +// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song +func (s Song[TPeriod]) GetInitialTempo() int { + return s.Head.InitialSpeed +} + +// GetGlobalVolumeGeneric returns the initial global volume for the song +func (s Song[TPeriod]) GetGlobalVolumeGeneric() volume.Volume { + return s.Head.GlobalVolume.ToVolume() +} + +// GetGlobalVolume returns the initial global volume for the song +func (s Song[TPeriod]) GetGlobalVolume() itVolume.FineVolume { + return s.Head.GlobalVolume +} + +// GetMixingVolumeGeneric returns the initial mixing volume for the song +func (s Song[TPeriod]) GetMixingVolumeGeneric() volume.Volume { + return s.Head.MixingVolume.ToVolume() +} + +// GetMixingVolume returns the initial mixing volume for the song +func (s Song[TPeriod]) GetMixingVolume() itVolume.FineVolume { + return s.Head.MixingVolume +} + +const durationPerBpm = time.Duration(2500) * time.Millisecond + +// GetTickDuration calculates the duration of a tick at a particular BPM +func (s Song[TPeriod]) 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 Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern[channel.Data[TPeriod], itVolume.Volume], error) { if int(patNum) >= len(s.Patterns) { - return nil + return nil, song.ErrStopSong } - return s.Patterns[patNum] + return s.Patterns[patNum].Pattern, nil } -// IsChannelEnabled returns true if the channel at index `channelNum` is enabled -func (s Song) IsChannelEnabled(channelNum int) bool { - return s.ChannelSettings[channelNum].Enabled +// GetPatternIntf returns an interface to a specific pattern indexed by `patNum` +func (s Song[TPeriod]) GetPatternIntf(patNum index.Pattern) (song.PatternIntf, error) { + return s.GetPattern(patNum) +} + +// GetPatternIntfByOrder returns the pattern specified by the order index provided +func (s Song[TPeriod]) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, 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.GetPatternIntf(pat) } -// GetRenderChannel returns the output channel for the channel at index `channelNum` -func (s Song) GetRenderChannel(channelNum int) int { - return s.ChannelSettings[channelNum].OutputChannelNum +// GetNumChannels returns the number of channels the song has +func (s Song[TPeriod]) GetNumChannels() int { + return len(s.ChannelSettings) +} + +// GetChannelSettings returns the channel settings at index `channelNum` +func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + return s.ChannelSettings[channelNum] } // NumInstruments returns the number of instruments in the song -func (s Song) NumInstruments() int { +func (s Song[TPeriod]) NumInstruments() int { return len(s.Instruments) } // IsValidInstrumentID returns true if the instrument exists -func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { +func (s Song[TPeriod]) IsValidInstrumentID(instNum instrument.ID) bool { if instNum.IsEmpty() { return false } @@ -63,7 +168,7 @@ func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { } // GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instNum instrument.ID) (*instrument.Instrument, note.Semitone) { +func (s Song[TPeriod]) GetInstrument(instNum instrument.ID) (instrument.InstrumentIntf, note.Semitone) { if instNum.IsEmpty() { return nil, note.UnchangedSemitone } @@ -79,6 +184,29 @@ func (s Song) GetInstrument(instNum instrument.ID) (*instrument.Instrument, note } // GetName returns the name of the song -func (s Song) GetName() string { +func (s Song[TPeriod]) GetName() string { return s.Head.Name } + +func (s Song[TPeriod]) GetPeriodCalculator() song.PeriodCalculatorIntf { + if s.Head.LinearFreqSlides { + return itPeriod.LinearConverter + } + return itPeriod.AmigaConverter +} + +func (s Song[TPeriod]) GetInitialOrder() index.Order { + return s.Head.InitialOrder +} + +func (s Song[TPeriod]) GetRowRenderStringer(row song.RowIntf, channels int, longFormat bool) render.RowStringer { + rt := render.NewRowText[channel.Data[TPeriod]](channels, longFormat) + rowData := make(song.Row[channel.Data[TPeriod], itVolume.Volume], channels) + copy(rowData, row.(song.Row[channel.Data[TPeriod], itVolume.Volume])) + rt.Channels = rowData + return rt +} + +func (s Song[TPeriod]) GetSystem() system.System { + return s.System +} diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 8209eb7..2834ec1 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -8,27 +8,30 @@ import ( "math" itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "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/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/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 + Inst *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] NR []noteRemap } @@ -57,19 +60,18 @@ 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.EnvPoint[itVolume.Volume], 0), }, } - ii := instrument.Instrument{ + ii := instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ Inst: &id, } @@ -100,12 +102,13 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData } for i := range inst.VolumeEnvelope { - var out envelope.EnvPoint[volume.Volume] + var out envelope.EnvPoint[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 +120,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) } @@ -156,16 +160,20 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf } 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: instrument.PitchPan{ + Enabled: inst.PitchPanSeparation != 0, + Center: note.Semitone(inst.PitchPanCenter), + Separation: float32(inst.PitchPanSeparation) / 8, + }, } - ii := instrument.Instrument{ - Static: instrument.StaticValues{ + ii := instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + Static: instrument.StaticValues[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ FilterFactory: channelFilterFactory, PluginFilter: pluginFilterFactory, }, @@ -186,6 +194,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 +206,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,19 +221,19 @@ 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) filter.PitchFiltValue { @@ -260,16 +269,22 @@ func convertEnvelope[T any](outEnv *envelope.Envelope[T], inEnv *itfile.Envelope 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.EnvPoint[T] + out.Length = ticks + out.Pos = int(in1.Tick) + out.Y = y + outEnv.Values[i] = out } else { - ticks = math.MaxInt64 + outEnv.Values[i] = envelope.EnvPoint[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) @@ -340,12 +355,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(ii *instrument.Instrument[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), @@ -446,7 +460,7 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrumen ii.Static.Filename = si.Header.GetFilename() ii.Static.Name = si.Header.GetName() ii.SampleRate = period.Frequency(si.Header.C5Speed) - ii.Static.AutoVibrato = voice.AutoVibrato{ + ii.Static.AutoVibrato = autovibrato.AutoVibratoSettings{ Enabled: (si.Header.VibratoDepth != 0 && si.Header.VibratoSpeed != 0 && si.Header.VibratoSweep != 0), Sweep: 255, WaveformSelection: itAutoVibratoWSToProtrackerWS(si.Header.VibratoType), @@ -456,7 +470,7 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument, id *instrumen return oscillatorImpl.NewImpulseTrackerOscillator(1) }, } - ii.Static.Volume = volume.Volume(si.Header.Volume.Value()) + ii.Static.Volume = itVolume.Volume(si.Header.Volume) if ii.SampleRate == 0 { ii.SampleRate = 8363.0 @@ -474,7 +488,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 ed5143c..b594003 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -10,17 +10,20 @@ 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/it/channel" "github.com/gotracker/playback/format/it/layout" itPanning "github.com/gotracker/playback/format/it/panning" "github.com/gotracker/playback/format/it/pattern" + 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/period" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/song" ) func moduleHeaderToHeader(fh *itfile.ModuleHeader) (*layout.Header, error) { @@ -28,28 +31,29 @@ 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), + LinearFreqSlides: fh.Flags.IsLinearSlides(), } 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, int, error) { - pat := make(pattern.Pattern, pkt.Rows) +func convertItPattern[TPeriod period.Period](pkt itfile.PackedPattern, channels int) (*pattern.Pattern[TPeriod], int, error) { + pat := make(song.Pattern[channel.Data[TPeriod], itVolume.Volume], pkt.Rows) channelMem := make([]itfile.ChannelData, channels) maxCh := uint8(0) pos := 0 for rowNum := 0; rowNum < int(pkt.Rows); rowNum++ { - row := make([]channel.Data, channels) + row := make([]channel.Data[TPeriod], channels) pat[rowNum] = row channelLoop: for { @@ -64,7 +68,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, @@ -80,10 +84,18 @@ func convertItPattern(pkt itfile.PackedPattern, channels int) (pattern.Pattern, } } - return pat, int(maxCh), nil + return &pattern.Pattern[TPeriod]{Pattern: 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 @@ -93,11 +105,12 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So oldEffectMode := f.Head.Flags.IsOldEffects() efgLinkMode := f.Head.Flags.IsEFGLinking() - song := layout.Song{ + songData := &layout.Song[TPeriod]{ + System: itSystem.ITSystem, Head: *h, - Instruments: make(map[uint8]*instrument.Instrument), + Instruments: make(map[uint8]*instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]), InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument), - Patterns: make([]pattern.Pattern, len(f.Patterns)), + Patterns: make([]pattern.Pattern[TPeriod], len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.OrderCount)), FilterPlugins: make(map[int]filter.Factory), } @@ -107,14 +120,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() { @@ -132,40 +145,38 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So } for _, ci := range instMap { - addSampleWithNoteMapToSong(&song, ci.Inst, ci.NR, instNum) + addSampleWithNoteMapToSong(songData, ci.Inst, ci.NR, instNum) } case *itfile.IMPIInstrument: - instMap, err := convertITInstrumentToInstrument(ii, f.Samples, convSettings, song.FilterPlugins, features) + instMap, err := convertITInstrumentToInstrument(ii, 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, ci.Inst, ci.NR, instNum) } } } } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern, 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, @@ -176,9 +187,9 @@ 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]), + InitialVolume: itVolume.Volume(itVolume.DefaultItVolume), + ChannelVolume: min(itVolume.FineVolume(f.Head.ChannelVol[chNum]*2), itVolume.MaxItFineVolume), + InitialPanning: itPanning.Panning(f.Head.ChannelPan[chNum]), Memory: channel.Memory{ Shared: &sharedMem, }, @@ -189,9 +200,8 @@ func convertItFileToSong(f *itfile.File, features []feature.Feature) (*layout.So channels[chNum] = cs } - song.ChannelSettings = channels - - return &song, nil + songData.ChannelSettings = channels + return songData, nil } func decodeFilter(f *itblock.FX) (filter.Factory, error) { @@ -215,7 +225,7 @@ 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], sample *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning], sts []noteRemap, instNum int) { if sample == nil { return } @@ -242,7 +252,7 @@ func addSampleWithNoteMapToSong(song *layout.Song, sample *instrument.Instrument } } -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/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 index 96c4958..4027b7b 100644 --- a/format/it/pattern/pattern.go +++ b/format/it/pattern/pattern.go @@ -2,7 +2,11 @@ package pattern import ( "github.com/gotracker/playback/format/it/channel" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" ) -type Pattern = song.Pattern[channel.Data] +type Pattern[TPeriod period.Period] struct { + song.Pattern[channel.Data[TPeriod], itVolume.Volume] +} diff --git a/format/it/pattern/row.go b/format/it/pattern/row.go index 8af113e..740df7c 100644 --- a/format/it/pattern/row.go +++ b/format/it/pattern/row.go @@ -2,7 +2,11 @@ package pattern import ( "github.com/gotracker/playback/format/it/channel" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" ) -type Row = song.Row[channel.Data] +type Row[TPeriod period.Period] struct { + song.Row[channel.Data[TPeriod], itVolume.Volume] +} diff --git a/format/it/pattern/state.go b/format/it/pattern/state.go index 1d3f4d0..e272584 100644 --- a/format/it/pattern/state.go +++ b/format/it/pattern/state.go @@ -4,8 +4,10 @@ import ( "errors" "github.com/gotracker/playback/format/it/channel" + itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/song" formatutil "github.com/gotracker/playback/util" @@ -13,7 +15,7 @@ import ( ) // State is the current pattern state -type State struct { +type State[TPeriod period.Period] struct { currentOrder index.Order currentRow index.Row ticks int @@ -27,22 +29,22 @@ type State struct { loopDetect formatutil.LoopDetect // when SongLoopEnabled is false, this is used to detect song loops loopCount int - Patterns []Pattern + Patterns []Pattern[TPeriod] Orders []index.Pattern } // GetTempo returns the tempo of the current state -func (state *State) GetTempo() int { +func (state *State[TPeriod]) GetTempo() int { return state.tempo } // GetSpeed returns the row speed of the current state -func (state *State) GetSpeed() int { +func (state *State[TPeriod]) GetSpeed() int { return state.ticks } // GetTicksThisRow returns the number of ticks in the current row -func (state *State) GetTicksThisRow() int { +func (state *State[TPeriod]) GetTicksThisRow() int { rowLoops := 1 if patternDelay, ok := state.patternDelay.Get(); ok { rowLoops = patternDelay @@ -54,7 +56,7 @@ func (state *State) GetTicksThisRow() int { } // GetPatNum returns the current pattern number -func (state *State) GetPatNum() index.Pattern { +func (state *State[TPeriod]) GetPatNum() index.Pattern { if int(state.currentOrder) >= len(state.Orders) { return index.InvalidPattern } @@ -62,7 +64,7 @@ func (state *State) GetPatNum() index.Pattern { } // GetNumRows returns the number of rows in the current pattern -func (state *State) GetNumRows() (int, error) { +func (state *State[TPeriod]) GetNumRows() (int, error) { rows, err := state.GetRows() if err != nil { return 0, err @@ -74,31 +76,31 @@ func (state *State) GetNumRows() (int, error) { } // WantsStop returns true when the current pattern wants to end the song -func (state *State) WantsStop() bool { +func (state *State[TPeriod]) WantsStop() bool { return state.GetPatNum() == index.InvalidPattern } // setCurrentOrder sets the current order index -func (state *State) setCurrentOrder(order index.Order) { +func (state *State[TPeriod]) setCurrentOrder(order index.Order) { state.currentOrder = order } -func (state *State) advanceOrder() { +func (state *State[TPeriod]) advanceOrder() { state.setCurrentOrder(state.currentOrder + 1) } // GetCurrentOrder returns the current order -func (state *State) GetCurrentOrder() index.Order { +func (state *State[TPeriod]) GetCurrentOrder() index.Order { return state.currentOrder } // GetNumOrders returns the number of orders in the song -func (state *State) GetNumOrders() int { +func (state *State[TPeriod]) GetNumOrders() int { return len(state.Orders) } // GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { +func (state *State[TPeriod]) GetCurrentPatternIdx() (index.Pattern, error) { ordLen := len(state.Orders) if ordLen == 0 { @@ -137,12 +139,12 @@ func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { } // GetCurrentRow returns the current row -func (state *State) GetCurrentRow() index.Row { +func (state *State[TPeriod]) GetCurrentRow() index.Row { return state.currentRow } // setCurrentRow sets the current row -func (state *State) setCurrentRow(row index.Row) error { +func (state *State[TPeriod]) setCurrentRow(row index.Row) error { state.currentRow = row rows, err := state.GetNumRows() if err != nil { @@ -157,7 +159,7 @@ func (state *State) setCurrentRow(row index.Row) error { } // Observe will attempt to detect a song loop -func (state *State) Observe() error { +func (state *State[TPeriod]) 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) { @@ -176,7 +178,7 @@ func (state *State) Observe() error { } // nextOrder travels to the next pattern in the order list -func (state *State) nextOrder(resetRow ...bool) error { +func (state *State[TPeriod]) nextOrder(resetRow ...bool) error { state.advanceOrder() state.patternDelay.Reset() state.finePatternDelay = 0 @@ -191,8 +193,8 @@ func (state *State) nextOrder(resetRow ...bool) error { } // Reset resets a pattern state back to zeroes -func (state *State) Reset() { - *state = State{ +func (state *State[TPeriod]) Reset() { + *state = State[TPeriod]{ SongLoop: feature.SongLoop{ Count: 0, }, @@ -205,7 +207,7 @@ func (state *State) Reset() { // 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 { +func (state *State[TPeriod]) nextRow() error { state.patternDelay.Reset() state.finePatternDelay = 0 @@ -234,7 +236,7 @@ func (state *State) nextRow() error { } // GetRows returns all the rows in the pattern -func (state *State) GetRows() (*song.Pattern[channel.Data], error) { +func (state *State[TPeriod]) GetRows() (*song.Pattern[channel.Data[TPeriod], itVolume.Volume], error) { nextRow: for loops := 0; loops < len(state.Patterns); loops++ { var patNum = state.GetPatNum() @@ -251,21 +253,21 @@ nextRow: return nil, nil } pat := &state.Patterns[patNum] - return pat, nil + return &pat.Pattern, nil } } return nil, nil } // NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State) NeedResetPatternLoops() bool { +func (state *State[TPeriod]) 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 { +func (state *State[TPeriod]) commitTransaction(txn *pattern.RowUpdateTransaction) error { tempo, tempoSet := txn.Tempo.Get() tempoDelta, tempoDeltaSet := txn.TempoDelta.Get() if tempoSet || tempoDeltaSet { @@ -330,7 +332,7 @@ func (state *State) commitTransaction(txn *pattern.RowUpdateTransaction) error { } // StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { +func (state *State[TPeriod]) StartTransaction() *pattern.RowUpdateTransaction { txn := pattern.RowUpdateTransaction{ CommitTransaction: state.commitTransaction, } diff --git a/format/it/period/util.go b/format/it/period/util.go index a940f8b..2904a3e 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -21,7 +21,6 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet case *period.Linear: nft := int(semi)*system.SlideFinesPerNote + int(ft) p.Finetune = note.Finetune(nft) - p.CommonRate = c5SampleRate case *period.Amiga: key := int(semi.Key()) octave := uint32(semi.Octave()) diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index 5fab1c8..e3dceb6 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -4,392 +4,94 @@ import ( "errors" "fmt" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/it/channel" itFeature "github.com/gotracker/playback/format/it/feature" "github.com/gotracker/playback/format/it/layout" - "github.com/gotracker/playback/format/it/pattern" - itPeriod "github.com/gotracker/playback/format/it/period" itSystem "github.com/gotracker/playback/format/it/system" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/output" - playpattern "github.com/gotracker/playback/pattern" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" "github.com/gotracker/playback/system" - "github.com/gotracker/playback/tracing" ) // manager is a playback manager for IT music type manager[TPeriod period.Period] struct { player.Tracker - tracing.Tracing[TPeriod, channel.Memory, channel.Data] - - song *layout.Song - - channels []state.ChannelState[TPeriod, channel.Memory, channel.Data] - PastNotes state.PastNotesProcessor[TPeriod] - pattern pattern.State - - preMixRowTxn *playpattern.RowUpdateTransaction - postMixRowTxn *playpattern.RowUpdateTransaction - premix *output.PremixData - - rowRenderState *state.RowRenderState - OnEffect func(playback.Effect) - longChannelOutput bool - enableNewNoteActions bool + song *layout.Song[TPeriod] } -var it system.System = itSystem.ITSystem - var _ playback.Playback = (*manager[period.Linear])(nil) -var _ playback.Playback = (*manager[period.Amiga])(nil) -var _ playback.Channel[period.Linear, channel.Memory, channel.Data] = (*state.ChannelState[period.Linear, channel.Memory, channel.Data])(nil) -var _ playback.Channel[period.Amiga, channel.Memory, channel.Data] = (*state.ChannelState[period.Amiga, channel.Memory, channel.Data])(nil) - -func (m *manager[TPeriod]) init(song *layout.Song, periodConverter period.PeriodConverter[TPeriod]) error { - m.Tracker.BaseClockRate = it.GetBaseClock() - m.song = song - m.Tracing.Playback = m - m.Tracing.ChannelGetter = func(c int) playback.Channel[TPeriod, channel.Memory, channel.Data] { - return m.GetChannel(c) - } - - m.PastNotes.SetMaxPerChannel(1) - - m.Tracker.PreTickable = m - m.Tracker.Tickable = m - m.Tracker.Premixable = m - m.Tracker.Traceable = &m.Tracing - m.pattern.Reset() - m.pattern.Orders = song.OrderList - m.pattern.Patterns = song.Patterns +var it system.ClockableSystem = itSystem.ITSystem - m.SetGlobalVolume(song.Head.GlobalVolume) - m.SetMixerVolume(song.Head.MixingVolume) - - m.SetNumChannels(len(song.ChannelSettings), periodConverter) - for i, ch := range song.ChannelSettings { - oc := m.GetRenderChannel(ch.OutputChannelNum, m.channelInit) - - cs := m.GetChannel(i) - cs.SetSongDataInterface(song) - cs.SetRenderChannel(oc) - cs.SetGlobalVolume(m.GetGlobalVolume()) - active := cs.GetActiveState() - active.SetVolume(ch.InitialVolume) - cs.SetChannelVolume(ch.ChannelVolume) - cs.SetPanEnabled(true) - active.Pan = ch.InitialPanning - cs.SetMemory(&song.ChannelSettings[i].Memory) - cs.SetStoredSemitone(note.UnchangedSemitone) - } - - txn := m.pattern.StartTransaction() - - txn.Ticks.Set(song.Head.InitialSpeed) - txn.Tempo.Set(song.Head.InitialTempo) - - if err := txn.Commit(); err != nil { - return err - } +func (m *manager[TPeriod]) init(s *layout.Song[TPeriod]) error { + m.Tracker.BaseClockRate = it.GetBaseClock() + m.song = s return nil } // NewManager creates a new manager for an IT song -func NewManager(song *layout.Song) (playback.Playback, error) { - if song == nil { +func NewManager(songData song.Data) (playback.Playback, error) { + if songData == nil { return nil, errors.New("song cannot be nil") } - var linearFreqSlides bool - if len(song.ChannelSettings) != 0 { - linearFreqSlides = song.ChannelSettings[0].Memory.Shared.LinearFreqSlides - } - - if linearFreqSlides { + switch s := songData.(type) { + case *layout.Song[period.Linear]: var m manager[period.Linear] - if err := m.init(song, itPeriod.LinearConverter); err != nil { - return nil, fmt.Errorf("could not initialize it linear manager: %w", err) + if err := m.init(s); err != nil { + return nil, fmt.Errorf("could not initialize it manager: %w", err) } return &m, nil - } else { + case *layout.Song[period.Amiga]: var m manager[period.Amiga] - if err := m.init(song, itPeriod.AmigaConverter); err != nil { - return nil, fmt.Errorf("could not initialize it amiga manager: %w", err) + if err := m.init(s); err != nil { + return nil, fmt.Errorf("could not initialize it manager: %w", err) } return &m, nil + default: + return nil, errors.New("unsupported it song data") } } -// StartPatternTransaction returns a new row update transaction for the pattern system -func (m *manager[TPeriod]) StartPatternTransaction() *playpattern.RowUpdateTransaction { - return m.pattern.StartTransaction() -} - -// GetNumChannels returns the number of channels -func (m *manager[TPeriod]) GetNumChannels() int { - return len(m.channels) -} - -func (m *manager[TPeriod]) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[TPeriod]) state.NoteOp[TPeriod, channel.Memory, channel.Data] { - return doNoteCalc[TPeriod]{ - Semitone: st, - UpdateFunc: fn, - } -} - -// SetNumChannels updates the song to have the specified number of channels and resets their states -func (m *manager[TPeriod]) SetNumChannels(num int, periodConverter period.PeriodConverter[TPeriod]) { - m.channels = make([]state.ChannelState[TPeriod, channel.Memory, channel.Data], num) - m.PastNotes.SetMax(channel.MaxTotalChannels - num) - - for ch := range m.channels { - cs := &m.channels[ch] - cs.ResetStates() - cs.PeriodConverter = periodConverter - cs.SemitoneSetterFactory = m.semitoneSetterFactory - - cs.PortaTargetPeriod.Reset() - cs.Trigger.Reset() - cs.RetriggerCount = 0 - _ = cs.SetData(channel.Data{}) - ocNum := m.song.GetRenderChannel(ch) - cs.RenderChannel = m.GetRenderChannel(ocNum, m.channelInit) - - if m.enableNewNoteActions { - cs.PastNotes = &m.PastNotes - } - } -} - -func (m *manager[TPeriod]) channelInit(ch int) *render.Channel { - return &render.Channel{ - ChannelNum: ch, - Filter: nil, - GetSampleRate: m.GetSampleRate, - SetGlobalVolume: m.SetGlobalVolume, - GetOPL2Chip: m.GetOPL2Chip, - ChannelVolume: volume.Volume(1), - } -} - -// SetNextOrder sets the next order index -func (m *manager[TPeriod]) SetNextOrder(order index.Order) error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.SetNextOrder(order) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetNextOrder(order) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetNextRow sets the next row index -func (m *manager[TPeriod]) SetNextRow(row index.Row) error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.SetNextRow(row) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetNextRow(row) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetNextRowWithBacktrack will set the next row index and backtracing allowance -func (m *manager[TPeriod]) SetNextRowWithBacktrack(row index.Row, allowBacktrack bool) error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.SetNextRowWithBacktrack(row, allowBacktrack) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetNextRowWithBacktrack(row, allowBacktrack) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// BreakOrder breaks to the next pattern in the order -func (m *manager[TPeriod]) BreakOrder() error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.BreakOrder = true - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.BreakOrder = true - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetTempo sets the desired tempo for the song -func (m *manager[TPeriod]) SetTempo(tempo int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.Tempo.Set(tempo) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.Tempo.Set(tempo) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// DecreaseTempo reduces the tempo by the `delta` value -func (m *manager[TPeriod]) DecreaseTempo(delta int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.AccTempoDelta(-delta) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.AccTempoDelta(-delta) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// IncreaseTempo increases the tempo by the `delta` value -func (m *manager[TPeriod]) IncreaseTempo(delta int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.AccTempoDelta(delta) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.AccTempoDelta(delta) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - // Configure sets specified features func (m *manager[TPeriod]) Configure(features []feature.Feature) error { - if err := m.Tracker.Configure(features); err != nil { + us, err := m.Tracker.Configure(features) + if err != nil { return err } for _, feat := range features { switch f := feat.(type) { case feature.SongLoop: - m.pattern.SongLoop = f + us.SongLoop = f + case feature.StartOrderAndRow: + us.StartOrderAndRow = f case feature.PlayUntilOrderAndRow: - m.pattern.PlayUntilOrderAndRow = f + us.PlayUntilOrderAndRow = f case itFeature.LongChannelOutput: - m.longChannelOutput = f.Enabled + m.Tracker.LongChannelOutput = f.Enabled + us.LongChannelOutput = f.Enabled case itFeature.NewNoteActions: - m.enableNewNoteActions = f.Enabled - for ch := range m.channels { - cs := &m.channels[ch] - if m.enableNewNoteActions { - cs.PastNotes = &m.PastNotes - } else { - cs.PastNotes = nil - } - } + us.EnableNewNoteActions = f.Enabled case feature.SetDefaultTempo: - txn := m.pattern.StartTransaction() - txn.Ticks.Set(f.Tempo) - if err := txn.Commit(); err != nil { - return err - } + us.StartTempo = f.Tempo case feature.SetDefaultBPM: - txn := m.pattern.StartTransaction() - txn.Tempo.Set(f.BPM) - if err := txn.Commit(); err != nil { - return err - } + us.StartBPM = f.BPM } } - return nil -} - -// CanOrderLoop returns true if the song is allowed to order loop -func (m *manager[TPeriod]) CanOrderLoop() bool { - return (m.pattern.SongLoop.Count != 0) -} - -// GetSongData gets the song data object -func (m *manager[TPeriod]) GetSongData() song.Data { - return m.song + return m.SetupMachine(m.song, us) } -// GetChannel returns the channel interface for the specified channel number -func (m *manager[TPeriod]) GetChannel(ch int) *state.ChannelState[TPeriod, channel.Memory, channel.Data] { - return &m.channels[ch] -} - -// GetCurrentOrder returns the current order -func (m *manager[TPeriod]) GetCurrentOrder() index.Order { - return m.pattern.GetCurrentOrder() -} - -// GetNumOrders returns the number of orders in the song func (m *manager[TPeriod]) GetNumOrders() int { - return m.pattern.GetNumOrders() + return len(m.song.GetOrderList()) } -// GetCurrentRow returns the current row -func (m *manager[TPeriod]) GetCurrentRow() index.Row { - return m.pattern.GetCurrentRow() -} - -func (m *manager[TPeriod]) GetRenderState() playback.RowRenderState { - return m.rowRenderState +func (m *manager[TPeriod]) CanOrderLoop() bool { + return true } -// GetName returns the current song's name func (m *manager[TPeriod]) GetName() string { return m.song.GetName() } - -// SetOnEffect sets the callback for an effect being generated for a channel -func (m *manager[TPeriod]) SetOnEffect(fn func(playback.Effect)) { - m.OnEffect = fn -} - -func (m *manager[TPeriod]) GetOnEffect() func(playback.Effect) { - return m.OnEffect -} - -func (m *manager[TPeriod]) SetEnvelopePosition(v int) { -} diff --git a/format/it/playback/playback_command.go b/format/it/playback/playback_command.go deleted file mode 100644 index 15ac1ed..0000000 --- a/format/it/playback/playback_command.go +++ /dev/null @@ -1,186 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback" - "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/format/it/channel" - itPeriod "github.com/gotracker/playback/format/it/period" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/state" -) - -type doNoteCalc[TPeriod period.Period] struct { - Semitone note.Semitone - UpdateFunc state.PeriodUpdateFunc[TPeriod] -} - -func (o doNoteCalc[TPeriod]) Process(p playback.Playback, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data]) error { - if o.UpdateFunc == nil { - return nil - } - - if inst := cs.GetTargetState().Instrument; inst != nil { - cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) - ft := inst.GetFinetune() - period := itPeriod.CalcSemitonePeriod[TPeriod](cs.Semitone, ft, inst.GetSampleRate()) - o.UpdateFunc(period) - } - return nil -} - -func (m *manager[TPeriod]) processEffect(ch int, cs *state.ChannelState[TPeriod, 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 - } - } - cs.SetGlobalVolume(m.GetGlobalVolume()) - - 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[TPeriod]) processRowNote(ch int, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data], currentTick int, lastTick bool) error { - targetTick, noteAction := cs.WillTriggerOn(currentTick) - if !targetTick { - return nil - } - - keyOn := false - if nc := cs.GetVoice(); nc != nil { - keyOn = nc.IsKeyOn() - } - - if noteAction == note.ActionRetrigger { - cs.TransitionActiveToPastState() - } - - active := cs.GetActiveState() - target := cs.GetTargetState() - - wantAttack := false - if !target.Period.IsInvalid() { - if target.Instrument != nil { - keyOn = true - wantAttack = noteAction == note.ActionRetrigger - } - - if cs.UseTargetPeriod { - active.Period = target.Period - cs.SetPortaTargetPeriod(target.Period) - } - - cs.SetInstrument(target.Instrument) - active.Pos = target.Pos - } - - if nc := cs.GetVoice(); nc != nil { - switch noteAction { - case note.ActionRetrigger: - if keyOn && wantAttack { - nc.Attack() - mem := cs.GetMemory() - mem.Retrigger() - } - case note.ActionRelease: - nc.Release() - case note.ActionCut: - cs.SetInstrument(nil) - var invalidPeriod TPeriod - active.Period = invalidPeriod - } - } - - return nil -} - -func (m *manager[TPeriod]) processVoiceUpdates(ch int, cs *state.ChannelState[TPeriod, channel.Memory, channel.Data], currentTick int, lastTick bool) error { - if cs.UsePeriodOverride { - cs.UsePeriodOverride = false - arpeggioPeriod := cs.GetPeriodOverride() - cs.GetActiveState().Period = arpeggioPeriod - } - return nil -} - -// SetFilterEnable activates or deactivates the amiga low-pass filter on the instruments -func (m *manager[TPeriod]) 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(period.Frequency(itPeriod.DefaultC5SampleRate), m.GetSampleRate()) - } - } else { - o.Filter = nil - } - } - } -} - -// SetTicks sets the number of ticks the row expects to play for -func (m *manager[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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/it/playback/playback_pattern.go b/format/it/playback/playback_pattern.go deleted file mode 100644 index 35e2cfa..0000000 --- a/format/it/playback/playback_pattern.go +++ /dev/null @@ -1,178 +0,0 @@ -package playback - -import ( - "errors" - "time" - - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/player/state" - "github.com/gotracker/playback/song" -) - -const ( - tickBaseDuration = time.Duration(2500) * time.Millisecond -) - -func (m *manager[TPeriod]) startProcessPatternRow() 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 - } - } - - myCurrentRow := m.pattern.GetCurrentRow() - - row := pat.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 = &state.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(state.NewChannelDataTxn[TPeriod, channel.Memory](channel.GetTargetsFromData[TPeriod])) - if resetMemory { - mem := cs.GetMemory() - mem.StartOrder() - } - } - - // generate effects and run prestart - nch := row.GetNumChannels() - if actual := m.GetNumChannels(); nch > actual { - nch = actual - } - - for channelNum := 0; channelNum < nch; channelNum++ { - cdata := row.GetChannel(index.Channel(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 - } - - return nil -} - -func (m *manager[TPeriod]) processPatternRow() error { - patIdx, err := m.pattern.GetCurrentPatternIdx() - if err != nil { - return err - } - rowIdx := m.pattern.GetCurrentRow() - - pat := m.song.GetPattern(patIdx) - row := pat.GetRow(rowIdx) - nch := row.GetNumChannels() - if actual := m.GetNumChannels(); nch > actual { - nch = actual - } - - s := m.GetSampler() - - 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 := 0; channelNum < nch; channelNum++ { - cs := &m.channels[channelNum] - - if err := m.processRowForChannel(cs); err != nil { - return err - } - } - - return nil -} - -func (m *manager[TPeriod]) processRowForChannel(cs *state.ChannelState[TPeriod, 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 7077250..0000000 --- a/format/it/playback/playback_render.go +++ /dev/null @@ -1,113 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/player/render" -) - -// OnPreTick runs the IT pre-tick processing -func (m *manager[TPeriod]) OnPreTick() error { - m.PastNotes.Update() - - m.premix = nil - - m.postMixRowTxn = m.pattern.StartTransaction() - - if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { - if err := m.startProcessPatternRow(); err != nil { - return err - } - } - - return nil -} - -// OnTick runs the IT tick processing -func (m *manager[TPeriod]) OnTick() error { - postMixRowTxn := m.postMixRowTxn - defer func() { - postMixRowTxn.Cancel() - m.postMixRowTxn = nil - }() - - if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { - if err := m.processPatternRow(); err != nil { - return err - } - } - - var finalData render.RowRender - premix := &output.PremixData{ - Userdata: &finalData, - SamplesLen: m.rowRenderState.Samples, - } - - if err := m.soundRenderTick(premix); err != nil { - return 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 err - } - - m.premix = premix - return nil -} - -// GetPremixData gets the current premix data from the manager -func (m *manager[TPeriod]) GetPremixData() (*output.PremixData, error) { - return m.premix, nil -} - -func (m *manager[TPeriod]) 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[TPeriod]) 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 ea0248c..0000000 --- a/format/it/playback/playback_textoutput.go +++ /dev/null @@ -1,25 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/format/it/channel" - "github.com/gotracker/playback/player/render" -) - -func (m *manager[TPeriod]) 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 - } - - rowText.Channels[ch] = cs.GetChannelData() - } - return &rowText -} diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go new file mode 100644 index 0000000..0144d8f --- /dev/null +++ b/format/it/settings/machine.go @@ -0,0 +1,77 @@ +package settings + +import ( + "fmt" + + "github.com/gotracker/playback/filter" + 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/oscillator" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/settings" + voiceOscillator "github.com/gotracker/playback/voice/oscillator" +) + +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: filterFactory, + GetVibratoFactory: vibratoFactory, + GetTremoloFactory: tremoloFactory, + GetPanbrelloFactory: panbrelloFactory, + VoiceFactory: amigaVoiceFactory, + } + + linearMachine = settings.MachineSettings[period.Linear, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + PeriodConverter: itPeriod.LinearConverter, + GetFilterFactory: filterFactory, + GetVibratoFactory: vibratoFactory, + GetTremoloFactory: tremoloFactory, + GetPanbrelloFactory: panbrelloFactory, + VoiceFactory: linearVoiceFactory, + } +) + +func filterFactory(name string) (settings.FilterFactoryFunc, error) { + switch name { + case "amigalpf": + return func(instrument, playback period.Frequency) (filter.Filter, error) { + lpf := filter.NewAmigaLPF(instrument, playback) + return lpf, nil + }, nil + + default: + return nil, fmt.Errorf("unsupported filter: %q", name) + } +} + +func vibratoFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewImpulseTrackerOscillator(4), nil +} + +func tremoloFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewImpulseTrackerOscillator(4), nil +} + +func panbrelloFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewImpulseTrackerOscillator(1), nil +} + +func init() { + machine.RegisterMachine(GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(GetMachineSettings[period.Linear]()) +} diff --git a/format/it/settings/voicefactory.go b/format/it/settings/voicefactory.go new file mode 100644 index 0000000..eefef9f --- /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() voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { + return itVoice.New(GetMachineSettings[TPeriod]()) +} + +var ( + amigaVoiceFactory voiceFactory[period.Amiga] + linearVoiceFactory voiceFactory[period.Linear] +) diff --git a/format/it/system/system.go b/format/it/system/system.go index 51faa5c..f44ad27 100644 --- a/format/it/system/system.go +++ b/format/it/system/system.go @@ -21,8 +21,14 @@ const ( C5SlideFines = 5 * SlideFinesPerOctave ) -var ITSystem system.System = system.ClockedSystem{ - BaseClock: ITBaseClock, - BaseFinetunes: C5SlideFines, - FinetunesPerOctave: SlideFinesPerOctave, +var semitonePeriodTable = [...]float32{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, + 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..9b00352 --- /dev/null +++ b/format/it/voice/enveloper_filter.go @@ -0,0 +1,40 @@ +package voice + +// == FilterEnveloper == + +func (v *itVoice[TPeriod]) 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 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) { + if !v.pitchAndFilterEnvShared || v.filterEnvActive { + if doneCB := v.filterEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB(v) + } + } +} + +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..8594551 --- /dev/null +++ b/format/it/voice/enveloper_pan.go @@ -0,0 +1,32 @@ +package voice + +import ( + itPanning "github.com/gotracker/playback/format/it/panning" +) + +// == PanEnveloper == + +func (v *itVoice[TPeriod]) EnablePanEnvelope(enabled bool) { + 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) { + if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB(v) + } +} + +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..7567833 --- /dev/null +++ b/format/it/voice/enveloper_pitch.go @@ -0,0 +1,37 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == PitchEnveloper == + +func (v *itVoice[TPeriod]) EnablePitchEnvelope(enabled bool) { + 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) { + if !v.pitchAndFilterEnvShared || !v.filterEnvActive { + if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB(v) + } + } +} + +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..931a45f --- /dev/null +++ b/format/it/voice/enveloper_volume.go @@ -0,0 +1,32 @@ +package voice + +import ( + itVolume "github.com/gotracker/playback/format/it/volume" +) + +// == VolumeEnveloper == + +func (v *itVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) { + 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) { + if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB(v) + } +} + +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..97c6be8 --- /dev/null +++ b/format/it/voice/modulator_amp.go @@ -0,0 +1,62 @@ +package voice + +import ( + "github.com/gotracker/gomixing/volume" + itVolume "github.com/gotracker/playback/format/it/volume" + "github.com/gotracker/playback/voice/types" +) + +// == AmpModulator == + +func (v *itVoice[TPeriod]) SetActive(on bool) { + v.amp.SetActive(on) +} + +func (v itVoice[TPeriod]) IsActive() bool { + return v.amp.IsActive() +} + +func (v *itVoice[TPeriod]) SetMixingVolume(vol itVolume.FineVolume) { + if !vol.IsUseInstrumentVol() { + v.amp.SetMixingVolume(vol) + } +} + +func (v itVoice[TPeriod]) GetMixingVolume() itVolume.FineVolume { + return v.amp.GetMixingVolume() +} + +func (v *itVoice[TPeriod]) SetVolume(vol itVolume.Volume) { + if vol.IsUseInstrumentVol() { + vol = v.voicer.GetDefaultVolume() + } + v.amp.SetVolume(vol) +} + +func (v itVoice[TPeriod]) GetVolume() itVolume.Volume { + return v.amp.GetVolume() +} + +func (v *itVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) { + 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 { + vol := v.amp.GetFinalVolume() + if v.IsVolumeEnvelopeEnabled() { + vol *= v.GetCurrentVolumeEnvelope().ToVolume() + } + return vol * v.fadeout.GetFinalVolume() +} diff --git a/format/it/voice/modulator_freq.go b/format/it/voice/modulator_freq.go new file mode 100644 index 0000000..0577a6e --- /dev/null +++ b/format/it/voice/modulator_freq.go @@ -0,0 +1,32 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == FreqModulator == + +func (v *itVoice[TPeriod]) SetPeriod(period TPeriod) { + v.freq.SetPeriod(period) +} + +func (v *itVoice[TPeriod]) GetPeriod() TPeriod { + return v.freq.GetPeriod() +} + +func (v *itVoice[TPeriod]) SetPeriodDelta(delta period.Delta) { + v.freq.SetPeriodDelta(delta) +} + +func (v *itVoice[TPeriod]) GetPeriodDelta() period.Delta { + return v.freq.GetPeriodDelta() +} + +func (v *itVoice[TPeriod]) GetFinalPeriod() TPeriod { + p := v.freq.GetFinalPeriod() + if v.IsPitchEnvelopeEnabled() { + delta := v.GetCurrentPitchEnvelope() + p = period.AddDelta(p, delta) + } + return p +} diff --git a/format/it/voice/modulator_pan.go b/format/it/voice/modulator_pan.go new file mode 100644 index 0000000..a66e421 --- /dev/null +++ b/format/it/voice/modulator_pan.go @@ -0,0 +1,50 @@ +package voice + +import ( + 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) { + v.pan.SetPan(pan) +} + +func (v itVoice[TPeriod]) GetPan() itPanning.Panning { + return v.pan.GetPan() +} + +func (v *itVoice[TPeriod]) SetPanDelta(d types.PanDelta) { + 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) { + v.pitchPan.SetPitch(st) +} + +func (v *itVoice[TPeriod]) EnablePitchPan(enabled bool) { + v.pitchPan.EnablePitchPan(enabled) +} + +func (v itVoice[TPeriod]) IsPitchPanEnabled() bool { + return v.pitchPan.IsPitchPanEnabled() +} + +func (v itVoice[TPeriod]) GetFinalPan() itPanning.Panning { + if !v.IsPanEnvelopeEnabled() { + return v.pan.GetFinalPan() + } + + envPan := v.panEnv.GetCurrentValue() + return v.pitchPan.GetSeparatedPan(envPan) +} diff --git a/format/it/voice/render.go b/format/it/voice/render.go new file mode 100644 index 0000000..7d79de0 --- /dev/null +++ b/format/it/voice/render.go @@ -0,0 +1,53 @@ +package voice + +import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/volume" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/state/render" +) + +func (v *itVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *playerRender.Channel[itVolume.FineVolume, itVolume.FineVolume, itPanning.Panning]) (*mixing.Data, error) { + if v.IsDone() { + return nil, nil + } + + if !v.IsActive() { + return nil, nil + } + + sampler, err := v.GetSampler(details.SamplerSpeed, renderChannel) + if err != nil || sampler == nil { + return nil, err + } + + // ... so grab the new value now. + pan := v.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.ToPosition(), + Volume: volume.Volume(1.0), + Pos: 0, + SamplesLen: details.Samples, + } + + if err := v.SetPos(sampler.GetPosition()); err != nil { + return nil, err + } + + return data, nil +} diff --git a/format/it/voice/sampler.go b/format/it/voice/sampler.go new file mode 100644 index 0000000..9cf8187 --- /dev/null +++ b/format/it/voice/sampler.go @@ -0,0 +1,72 @@ +package voice + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + itPanning "github.com/gotracker/playback/format/it/panning" + itVolume "github.com/gotracker/playback/format/it/volume" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/voice/component" +) + +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.config.VoiceFilter != nil { + wet = v.config.VoiceFilter.Filter(wet) + } + if v.config.PluginFilter != nil { + wet = v.config.PluginFilter.Filter(wet) + } + return wet +} + +func (v *itVoice[TPeriod]) GetSampler(samplerRate float32, renderChannel *playerRender.Channel[itVolume.FineVolume, itVolume.FineVolume, itPanning.Panning]) (sampling.Sampler, error) { + o := component.OutputFilter{ + Input: v, + Output: renderChannel, + } + + pos, err := v.GetPos() + if err != nil { + return nil, err + } + + p := v.GetFinalPeriod() + + samplerAdd := float32(v.ms.PeriodConverter.GetSamplerAdd(p, float64(v.config.SampleRate)*float64(samplerRate))) + s := sampling.NewSampler(&o, pos, samplerAdd) + + return s, nil +} 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..024e836 --- /dev/null +++ b/format/it/voice/voice.go @@ -0,0 +1,294 @@ +package voice + +import ( + 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/player/machine/settings" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" +) + +type Period interface { + period.Period +} + +type itVoice[TPeriod Period] struct { + ms *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + config voice.InstrumentConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + + pitchAndFilterEnvShared bool + filterEnvActive bool // if pitchAndFilterEnvShared is true, this dictates which is active initially - true=filter, false=pitch + fadeoutMode fadeout.Mode + + component.KeyModulator + + voicer component.Voicer[TPeriod, 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 +} + +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](ms *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { + v := &itVoice[TPeriod]{ + ms: ms, + 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: itVolume.MaxItFineVolume, + DefaultVolume: itVolume.Volume(itVolume.MaxItVolume), + }) + + v.pan.Setup(component.PanModulatorSettings[itPanning.Panning]{ + InitialPan: itPanning.DefaultPanning, + }) + + 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() + } +} + +func (v *itVoice[TPeriod]) doRelease() { + v.volEnv.Release() + v.pitchEnv.Release() + v.panEnv.Release() + v.filterEnv.Release() + if v.voicer != nil { + v.voicer.Release() + } +} + +func (v *itVoice[TPeriod]) doFadeout() { + if v.voicer != nil { + v.voicer.Fadeout() + } + + v.fadeout.Fadeout() +} + +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]) Setup(config voice.InstrumentConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) { + v.config = config + v.fadeout.Setup(component.FadeoutModulatorSettings{ + GetEnabled: v.getFadeoutEnabled, + Amount: config.FadeOut.Amount, + }) + v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) + v.autoVibrato.Setup(config.AutoVibrato) + v.pitchPan.Setup(component.PitchPanModulatorSettings[itPanning.Panning]{ + PitchPanEnable: config.PitchPan.Enabled, + PitchPanCenter: config.PitchPan.Center, + PitchPanSeparation: config.PitchPan.Separation, + }) + volEnvSettings := component.EnvelopeSettings[itVolume.Volume, itVolume.Volume]{ + Envelope: config.VolEnv, + } + if config.VolEnvFinishFadesOut { + volEnvSettings.OnFinished = func(v voice.Voice) { + v.Fadeout() + } + } + v.volEnv.Setup(volEnvSettings) + v.pitchEnv.Setup(component.EnvelopeSettings[int8, period.Delta]{ + Envelope: config.PitchFiltEnv, + }) + v.panEnv.Setup(component.EnvelopeSettings[itPanning.Panning, itPanning.Panning]{ + Envelope: config.PanEnv, + }) + v.filterEnv.Setup(component.EnvelopeSettings[int8, uint8]{ + Envelope: config.PitchFiltEnv, + }) + v.vol0Opt.Setup(config.Vol0Optimization) + v.KeyModulator.Release() + v.Reset() +} + +func (v *itVoice[TPeriod]) Reset() { + v.filterEnvActive = v.config.PitchFiltMode + v.fadeoutMode = v.config.FadeOut.Mode + + v.fadeout.Reset() + + v.volEnv.Reset() + v.pitchEnv.Reset() + v.panEnv.Reset() + v.filterEnv.Reset() + + v.autoVibrato.Reset() + + v.volEnv.Reset() + v.pitchEnv.Reset() + v.panEnv.Reset() + v.filterEnv.Reset() + v.vol0Opt.Reset() +} + +func (v *itVoice[TPeriod]) Stop() { + v.voicer = nil +} + +func (v *itVoice[TPeriod]) IsDone() bool { + if v.voicer == nil { + return true + } + + if v.fadeout.IsActive() { + return v.fadeout.GetVolume() <= 0 + } + + return v.vol0Opt.IsDone() +} + +func (v *itVoice[TPeriod]) Advance() { + 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.config.VoiceFilter != nil && v.IsFilterEnvelopeEnabled() { + fval := v.GetCurrentFilterEnvelope() + v.config.VoiceFilter.UpdateEnv(fval) + } + + // has to be after the mod/env updates + v.KeyModulator.DeferredUpdate() + + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + v.KeyModulator.Advance() +} + +func (v *itVoice[TPeriod]) Clone() voice.Voice { + vv := itVoice[TPeriod]{ + ms: v.ms, + config: v.config, + pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, + filterEnvActive: v.filterEnvActive, + fadeoutMode: v.fadeoutMode, + amp: v.amp.Clone(), + freq: v.freq.Clone(), + pan: v.pan.Clone(), + volEnv: v.volEnv.Clone(), + pitchEnv: v.pitchEnv.Clone(), + panEnv: v.panEnv.Clone(), + filterEnv: v.filterEnv.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.config.VoiceFilter != nil { + vv.config.VoiceFilter = v.config.VoiceFilter.Clone() + } + + if v.config.PluginFilter != nil { + vv.config.PluginFilter = v.config.PluginFilter.Clone() + } + + return &vv +} + +func (v *itVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, defVol itVolume.Volume) { + var s component.Sampler[TPeriod, itVolume.Volume] + s.Setup(component.SamplerSettings[TPeriod, itVolume.Volume]{ + Sample: samp, + DefaultVolume: defVol, + WholeLoop: wholeLoop, + SustainLoop: sustainLoop, + }) + v.voicer = &s +} 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..26f3827 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(0x7f) 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/s3m/channel/data.go b/format/s3m/channel/data.go index 949e1dd..ea587d8 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -9,10 +9,14 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" + 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/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/player/machine/instruction" "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/song" ) @@ -25,7 +29,7 @@ type Data struct { What s3mfile.PatternFlags Note s3mfile.Note Instrument InstID - Volume s3mfile.Volume + Volume s3mVolume.Volume Command uint8 Info DataEffect } @@ -55,9 +59,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 @@ -109,6 +117,21 @@ 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 +} + // NoteFromS3MNote converts an S3M file note into a player note func NoteFromS3MNote(sn s3mfile.Note) note.Note { switch { @@ -127,7 +150,7 @@ func NoteFromS3MNote(sn s3mfile.Note) note.Note { return note.InvalidNote{} } -func GetTargetsFromData(out *op.ChannelTargets[period.Amiga], d Data, s song.Data, cs playback.Channel[period.Amiga, Memory, Data]) error { +func GetTargetsFromData(out *op.ChannelTargets[period.Amiga, s3mVolume.Volume, s3mPanning.Panning], d Data, s song.Data, cs playback.Channel[period.Amiga, *Memory, Data, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { var n note.Note = note.EmptyNote{} inst := cs.GetActiveState().Instrument prevInst := inst @@ -162,7 +185,7 @@ func GetTargetsFromData(out *op.ChannelTargets[period.Amiga], d Data, s song.Dat out.TargetPos.Set(sampling.Pos{}) if inst != nil { if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) + out.TargetVolume.Set(s3mVolume.VolumeToS3M(inst.GetDefaultVolumeGeneric())) } out.NoteAction.Set(note.ActionRetrigger) out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) @@ -187,9 +210,9 @@ func GetTargetsFromData(out *op.ChannelTargets[period.Amiga], d Data, s song.Dat if d.HasVolume() { v := d.GetVolume() - if v == volume.VolumeUseInstVol { + if v.IsUseInstrumentVol() { if inst != nil { - v = inst.GetDefaultVolume() + v = s3mVolume.VolumeToS3M(inst.GetDefaultVolumeGeneric()) } } out.TargetVolume.Set(v) diff --git a/format/s3m/channel/effect_arpeggio.go b/format/s3m/channel/effect_arpeggio.go index 89fe8ab..3ce0fad 100644 --- a/format/s3m/channel/effect_arpeggio.go +++ b/format/s3m/channel/effect_arpeggio.go @@ -3,27 +3,30 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - cs.GetActiveState().Pos = cs.GetTargetState().Pos - return nil +func (e Arpeggio) String() string { + return fmt.Sprintf("J%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Arpeggio) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +func (e Arpeggio) 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 doArpeggio(cs, currentTick, int8(x), int8(y)) + return doArpeggio(ch, m, tick, int8(x), int8(y)) } -func (e Arpeggio) String() string { - return fmt.Sprintf("J%0.2x", DataEffect(e)) +func (e Arpeggio) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_enablefilter.go b/format/s3m/channel/effect_enablefilter.go index b48da13..c7bffc1 100644 --- a/format/s3m/channel/effect_enablefilter.go +++ b/format/s3m/channel/effect_enablefilter.go @@ -3,24 +3,29 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() +func (e EnableFilter) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - x := DataEffect(e) & 0xf - on := x != 0 +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 + } - pb := p.(S3M) - pb.SetFilterEnable(on) - return nil + x := DataEffect(e) & 0xf + return m.SetFilterOnAllChannelsByFilterName("amigalpf", x != 0) } -func (e EnableFilter) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e EnableFilter) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_extrafineportadown.go b/format/s3m/channel/effect_extrafineportadown.go index 044e073..112e020 100644 --- a/format/s3m/channel/effect_extrafineportadown.go +++ b/format/s3m/channel/effect_extrafineportadown.go @@ -3,22 +3,29 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e ExtraFinePortaDown) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - y := DataEffect(e) & 0x0F +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 + } - return doPortaDown(cs, float32(y), 1) + y := DataEffect(e) & 0x0F + return doPortaDown(ch, m, float32(y), 1) } -func (e ExtraFinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +func (e ExtraFinePortaDown) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_extrafineportaup.go b/format/s3m/channel/effect_extrafineportaup.go index 66fc8b6..c6fcfe1 100644 --- a/format/s3m/channel/effect_extrafineportaup.go +++ b/format/s3m/channel/effect_extrafineportaup.go @@ -3,22 +3,29 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e ExtraFinePortaUp) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} - y := DataEffect(e) & 0x0F +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 + } - return doPortaUp(cs, float32(y), 1) + y := DataEffect(e) & 0x0F + return doPortaUp(ch, m, float32(y), 1) } -func (e ExtraFinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +func (e ExtraFinePortaUp) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_finepatterndelay.go b/format/s3m/channel/effect_finepatterndelay.go index cf9ebf8..4f8b20c 100644 --- a/format/s3m/channel/effect_finepatterndelay.go +++ b/format/s3m/channel/effect_finepatterndelay.go @@ -3,22 +3,29 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() +func (e FinePatternDelay) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - x := DataEffect(e) & 0xf +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 + } - m := p.(S3M) - return m.AddRowTicks(int(x)) + x := DataEffect(e) & 0xf + return m.AddExtraTicks(int(x)) } -func (e FinePatternDelay) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e FinePatternDelay) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_fineportadown.go b/format/s3m/channel/effect_fineportadown.go index e48a932..ee9b3fb 100644 --- a/format/s3m/channel/effect_fineportadown.go +++ b/format/s3m/channel/effect_fineportadown.go @@ -3,22 +3,29 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e FinePortaDown) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - y := DataEffect(e) & 0x0F +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 + } - return doPortaDown(cs, float32(y), 4) + y := DataEffect(e) & 0x0F + return doPortaDown(ch, m, float32(y), 4) } -func (e FinePortaDown) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +func (e FinePortaDown) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_fineportaup.go b/format/s3m/channel/effect_fineportaup.go index 61843d8..79a3c24 100644 --- a/format/s3m/channel/effect_fineportaup.go +++ b/format/s3m/channel/effect_fineportaup.go @@ -3,22 +3,29 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e FinePortaUp) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} - y := DataEffect(e) & 0x0F +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 + } - return doPortaUp(cs, float32(y), 4) + y := DataEffect(e) & 0x0F + return doPortaUp(ch, m, float32(y), 4) } -func (e FinePortaUp) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +func (e FinePortaUp) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_finevibrato.go b/format/s3m/channel/effect_finevibrato.go index 9172457..278cb8a 100644 --- a/format/s3m/channel/effect_finevibrato.go +++ b/format/s3m/channel/effect_finevibrato.go @@ -3,30 +3,36 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e FineVibrato) String() string { + return fmt.Sprintf("U%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e FineVibrato) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick != 0 || mem.Shared.ModCompatibility { - return doVibrato(cs, currentTick, x, y, 1) + 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) String() string { - return fmt.Sprintf("U%0.2x", DataEffect(e)) +func (e FineVibrato) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_finevolslidedown.go b/format/s3m/channel/effect_finevolslidedown.go index f178881..c7b0acf 100644 --- a/format/s3m/channel/effect_finevolslidedown.go +++ b/format/s3m/channel/effect_finevolslidedown.go @@ -3,28 +3,33 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e FineVolumeSlideDown) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e FineVolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { +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 && currentTick == 0 { - return doVolSlide(cs, -float32(y), 1.0) + if y != 0x0F { + return m.SlideChannelVolume(ch, 1, -float32(y)) } return nil } -func (e FineVolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e FineVolumeSlideDown) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_finevolslideup.go b/format/s3m/channel/effect_finevolslideup.go index b3aa4cf..96a8eda 100644 --- a/format/s3m/channel/effect_finevolslideup.go +++ b/format/s3m/channel/effect_finevolslideup.go @@ -3,28 +3,33 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e FineVolumeSlideUp) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e FineVolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { +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 && currentTick == 0 { - return doVolSlide(cs, float32(x), 1.0) + if x != 0x0F { + return m.SlideChannelVolume(ch, 1, float32(x)) } return nil } -func (e FineVolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e FineVolumeSlideUp) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_notecut.go b/format/s3m/channel/effect_notecut.go index faf4cd6..53eb5fa 100644 --- a/format/s3m/channel/effect_notecut.go +++ b/format/s3m/channel/effect_notecut.go @@ -3,28 +3,33 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e NoteCut) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e NoteCut) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { +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 x != 0 && currentTick == int(x) { - cs.FreezePlayback() + if tick == int(x) { + return m.ChannelStop(ch) } return nil } -func (e NoteCut) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e NoteCut) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_notedelay.go b/format/s3m/channel/effect_notedelay.go index 1620704..2a87cdf 100644 --- a/format/s3m/channel/effect_notedelay.go +++ b/format/s3m/channel/effect_notedelay.go @@ -3,25 +3,26 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e NoteDelay) PreStart(cs S3MChannel, p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) - return nil +func (e NoteDelay) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e NoteDelay) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_orderjump.go b/format/s3m/channel/effect_orderjump.go index b80dc98..67c503c 100644 --- a/format/s3m/channel/effect_orderjump.go +++ b/format/s3m/channel/effect_orderjump.go @@ -3,24 +3,25 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e OrderJump) String() string { + return fmt.Sprintf("B%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e OrderJump) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { - return p.SetNextOrder(index.Order(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) String() string { - return fmt.Sprintf("B%0.2x", DataEffect(e)) +func (e OrderJump) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_patterndelay.go b/format/s3m/channel/effect_patterndelay.go index fbbd8fe..cbf0820 100644 --- a/format/s3m/channel/effect_patterndelay.go +++ b/format/s3m/channel/effect_patterndelay.go @@ -3,24 +3,25 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - m := p.(S3M) - return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) +func (e PatternDelay) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e PatternDelay) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_patternloop.go b/format/s3m/channel/effect_patternloop.go index f3dc736..3595560 100644 --- a/format/s3m/channel/effect_patternloop.go +++ b/format/s3m/channel/effect_patternloop.go @@ -3,41 +3,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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e PatternLoop) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e PatternLoop) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { - x := DataEffect(e) & 0xF +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 - mem := cs.GetMemory() - pl := mem.GetPatternLoop() if x == 0 { - // set loop - pl.Start = p.GetCurrentRow() + m.SetPatternLoopStart(ch) } 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) - } + m.SetPatternLoops(ch, int(x)) } return nil } -func (e PatternLoop) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e PatternLoop) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go index 05d38e1..8a1f558 100644 --- a/format/s3m/channel/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -3,30 +3,35 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e PortaDown) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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.LastNonZero(DataEffect(e)) - if currentTick != 0 { - return doPortaDown(cs, float32(xx), 4) + if tick == 0 { + return nil } - return nil + + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) } -func (e PortaDown) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +func (e PortaDown) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 596a02c..5fd7368 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -3,50 +3,39 @@ package channel import ( "fmt" - "github.com/gotracker/playback" - "github.com/gotracker/playback/note" + 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/heucuva/comparison" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - if cmd := cs.GetChannelData(); cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetState().Period) - cs.SetNotePlayTick(false, note.ActionContinue, 0) - } - return nil +func (e PortaToNote) String() string { + return fmt.Sprintf("G%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaToNote) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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.PortaToNote(DataEffect(e)) - // vibrato modifies current period for portamento - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { + if tick == 0 { return nil } - d := cs.GetPeriodDelta() - cur = period.AddDelta(cur, d) - 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 + + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) } -func (e PortaToNote) String() string { - return fmt.Sprintf("G%0.2x", DataEffect(e)) +func (e PortaToNote) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go index f3ca33f..9484352 100644 --- a/format/s3m/channel/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -3,30 +3,35 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e PortaUp) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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.LastNonZero(DataEffect(e)) - if currentTick != 0 { - return doPortaUp(cs, float32(xx), 4) + if tick == 0 { + return nil } - return nil + + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) } -func (e PortaUp) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +func (e PortaUp) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_portavolslide.go b/format/s3m/channel/effect_portavolslide.go index 294b3df..04b51af 100644 --- a/format/s3m/channel/effect_portavolslide.go +++ b/format/s3m/channel/effect_portavolslide.go @@ -4,12 +4,14 @@ 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, Memory, Data] + playback.CombinedEffect[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning, *Memory, Data] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object @@ -21,5 +23,9 @@ func NewPortaVolumeSlide(mem *Memory, cd uint8, val DataEffect) PortaVolumeSlide } func (e PortaVolumeSlide) String() string { - return fmt.Sprintf("L%0.2x", e.Effects[0].(DataEffect)) + 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 index 10f76c1..f174bd7 100644 --- a/format/s3m/channel/effect_retrigvolslide.go +++ b/format/s3m/channel/effect_retrigvolslide.go @@ -3,68 +3,71 @@ package channel import ( "fmt" - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RetrigVolumeSlide) String() string { + return fmt.Sprintf("Q%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e RetrigVolumeSlide) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - x := DataEffect(e) >> 4 - y := DataEffect(e) & 0x0F - if y == 0 { +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 } - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if DataEffect(rt) >= x { - cs.GetActiveState().Pos = 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) - } + 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) String() string { - return fmt.Sprintf("Q%0.2x", DataEffect(e)) +func (e RetrigVolumeSlide) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_rowjump.go b/format/s3m/channel/effect_rowjump.go index 63a0e3c..1940553 100644 --- a/format/s3m/channel/effect_rowjump.go +++ b/format/s3m/channel/effect_rowjump.go @@ -3,27 +3,28 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RowJump) String() string { + return fmt.Sprintf("C%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e RowJump) Stop(cs S3MChannel, p playback.Playback, lastTick int) error { +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 p.SetNextRow(rowIdx) + + return m.SetRow(rowIdx, true) } -func (e RowJump) String() string { - return fmt.Sprintf("C%0.2x", DataEffect(e)) +func (e RowJump) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_sampleoffset.go b/format/s3m/channel/effect_sampleoffset.go index a01a903..1f40c43 100644 --- a/format/s3m/channel/effect_sampleoffset.go +++ b/format/s3m/channel/effect_sampleoffset.go @@ -4,22 +4,30 @@ import ( "fmt" "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() +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)) - cs.GetTargetState().Pos = sampling.Pos{Pos: int(xx) * 0x100} - return nil + return m.SetChannelPos(ch, sampling.Pos{Pos: int(xx) * 0x100}) } -func (e SampleOffset) String() string { - return fmt.Sprintf("O%0.2x", DataEffect(e)) +func (e SampleOffset) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_setfinetune.go b/format/s3m/channel/effect_setfinetune.go index 859f6f8..fce3862 100644 --- a/format/s3m/channel/effect_setfinetune.go +++ b/format/s3m/channel/effect_setfinetune.go @@ -3,18 +3,29 @@ 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/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 ChannelCommand // 'S2x' -// PreStart triggers when the effect enters onto the channel state -func (e SetFinetune) PreStart(cs S3MChannel, p playback.Playback) error { +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 := cs.GetTargetState().Instrument + inst, err := m.GetChannelInstrument(ch) + if err != nil { + return err + } + if inst != nil { ft := (note.Finetune(x) - 8) * 4 inst.SetFinetune(ft) @@ -22,12 +33,6 @@ func (e SetFinetune) PreStart(cs S3MChannel, p playback.Playback) error { return nil } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetFinetune) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e SetFinetune) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_setglobalvolume.go b/format/s3m/channel/effect_setglobalvolume.go index 86c24ba..aa59f08 100644 --- a/format/s3m/channel/effect_setglobalvolume.go +++ b/format/s3m/channel/effect_setglobalvolume.go @@ -3,27 +3,24 @@ package channel import ( "fmt" - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - - "github.com/gotracker/playback" + 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' -// PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume) PreStart(cs S3MChannel, p playback.Playback) error { - p.SetGlobalVolume(s3mVolume.VolumeFromS3M(s3mfile.Volume(DataEffect(e)))) - return nil +func (e SetGlobalVolume) String() string { + return fmt.Sprintf("V%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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) String() string { - return fmt.Sprintf("V%0.2x", 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 index fc4c27a..89d9294 100644 --- a/format/s3m/channel/effect_setpanposition.go +++ b/format/s3m/channel/effect_setpanposition.go @@ -3,23 +3,24 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - - x := uint8(e) & 0xf +func (e SetPanPosition) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - cs.GetActiveState().Pan = s3mPanning.PanningFromS3M(x) - return nil +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) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e SetPanPosition) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_setspeed.go b/format/s3m/channel/effect_setspeed.go index 3fabfe3..8302ace 100644 --- a/format/s3m/channel/effect_setspeed.go +++ b/format/s3m/channel/effect_setspeed.go @@ -3,29 +3,24 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - if e != 0 { - m := p.(S3M) - if err := m.SetTicks(int(e)); err != nil { - return err - } - } - return nil +func (e SetSpeed) String() string { + return fmt.Sprintf("A%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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) String() string { - return fmt.Sprintf("A%0.2x", DataEffect(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 index 6a13926..0247411 100644 --- a/format/s3m/channel/effect_settempo.go +++ b/format/s3m/channel/effect_settempo.go @@ -3,57 +3,56 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - if e > 0x20 { - m := p.(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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e SetTempo) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e SetTempo) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - m := p.(S3M) +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 currentTick != 0 { - mem := cs.GetMemory() + 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.DecreaseTempo(val); err != nil { + if err := m.SlideBPM(-val); err != nil { return err } } case 1: // increase tempo - if currentTick != 0 { - mem := cs.GetMemory() + 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.IncreaseTempo(val); err != nil { + if err := m.SlideBPM(val); err != nil { return err } } default: - if err := m.SetTempo(int(e)); err != nil { - return err + if tick == 0 { + if err := m.SetBPM(int(e)); err != nil { + return err + } } } return nil } -func (e SetTempo) String() string { - return fmt.Sprintf("T%0.2x", DataEffect(e)) +func (e SetTempo) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_settremolowaveform.go b/format/s3m/channel/effect_settremolowaveform.go index fe14d9f..bffc57a 100644 --- a/format/s3m/channel/effect_settremolowaveform.go +++ b/format/s3m/channel/effect_settremolowaveform.go @@ -3,26 +3,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" "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetTremoloWaveform) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - trem := mem.TremoloOscillator() - trem.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e SetTremoloWaveform) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_setvibratowaveform.go b/format/s3m/channel/effect_setvibratowaveform.go index 9383fc2..c8b5309 100644 --- a/format/s3m/channel/effect_setvibratowaveform.go +++ b/format/s3m/channel/effect_setvibratowaveform.go @@ -3,26 +3,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" "github.com/gotracker/playback/voice/oscillator" - - "github.com/gotracker/playback" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetVibratoWaveform) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - vib := mem.VibratoOscillator() - vib.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e SetVibratoWaveform) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_stereocontrol.go b/format/s3m/channel/effect_stereocontrol.go index ed98a8d..c05329c 100644 --- a/format/s3m/channel/effect_stereocontrol.go +++ b/format/s3m/channel/effect_stereocontrol.go @@ -3,28 +3,30 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() +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 - active := cs.GetActiveState() if x > 7 { - active.Pan = s3mPanning.PanningFromS3M(x - 8) + return m.SetChannelPan(ch, s3mPanning.Panning(x-8)) } else { - active.Pan = s3mPanning.PanningFromS3M(x + 8) + return m.SetChannelPan(ch, s3mPanning.Panning(x+8)) } - return nil } -func (e StereoControl) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e StereoControl) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_surroundon.go b/format/s3m/channel/effect_surroundon.go index 6db6659..55789d5 100644 --- a/format/s3m/channel/effect_surroundon.go +++ b/format/s3m/channel/effect_surroundon.go @@ -3,22 +3,25 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // SurroundOn defines a set surround on effect type SurroundOn ChannelCommand // 'S91' -// Start triggers on the first tick, but before the Tick() function is called -func (e SurroundOn) Start(cs S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() +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 center rear panning - cs.GetActiveState().Pan = s3mPanning.DefaultPanning return nil } -func (e SurroundOn) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +func (e SurroundOn) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_tremolo.go b/format/s3m/channel/effect_tremolo.go index 75071c6..fc083ce 100644 --- a/format/s3m/channel/effect_tremolo.go +++ b/format/s3m/channel/effect_tremolo.go @@ -4,6 +4,12 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" + "github.com/gotracker/playback/voice/types" ) // Tremolo defines a tremolo effect @@ -15,17 +21,25 @@ func (e Tremolo) Start(cs S3MChannel, p playback.Playback) error { return nil } -// Tick is called on every tick -func (e Tremolo) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick != 0 || mem.Shared.ModCompatibility { - return doTremolo(cs, currentTick, DataEffect(x), DataEffect(y), 4) + 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) String() string { - return fmt.Sprintf("R%0.2x", DataEffect(e)) +func (e Tremolo) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_tremor.go b/format/s3m/channel/effect_tremor.go index d378bfe..bffec0e 100644 --- a/format/s3m/channel/effect_tremor.go +++ b/format/s3m/channel/effect_tremor.go @@ -4,6 +4,11 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Tremor defines a tremor effect @@ -15,13 +20,19 @@ func (e Tremor) Start(cs S3MChannel, p playback.Playback) error { return nil } -// Tick is called on every tick -func (e Tremor) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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(cs, currentTick, int(x)+1, int(y)+1) + return doTremor(ch, m, int(x)+1, int(y)+1) } -func (e Tremor) String() string { - return fmt.Sprintf("I%0.2x", DataEffect(e)) +func (e Tremor) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_vibrato.go b/format/s3m/channel/effect_vibrato.go index 6fb7e9c..7163a30 100644 --- a/format/s3m/channel/effect_vibrato.go +++ b/format/s3m/channel/effect_vibrato.go @@ -4,6 +4,11 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Vibrato defines a vibrato effect @@ -16,17 +21,25 @@ func (e Vibrato) Start(cs S3MChannel, p playback.Playback) error { return nil } -// Tick is called on every tick -func (e Vibrato) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 dos not update on tick 0, but MOD does. - if currentTick != 0 || mem.Shared.ModCompatibility { - return doVibrato(cs, currentTick, x, y, 4) + // 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) String() string { - return fmt.Sprintf("H%0.2x", DataEffect(e)) +func (e Vibrato) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_vibratovolslide.go b/format/s3m/channel/effect_vibratovolslide.go index 3972f03..8a9e377 100644 --- a/format/s3m/channel/effect_vibratovolslide.go +++ b/format/s3m/channel/effect_vibratovolslide.go @@ -4,12 +4,14 @@ 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, Memory, Data] + playback.CombinedEffect[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning, *Memory, Data] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object @@ -21,5 +23,9 @@ func NewVibratoVolumeSlide(mem *Memory, cd uint8, val DataEffect) VibratoVolumeS } func (e VibratoVolumeSlide) String() string { - return fmt.Sprintf("K%0.2x", e.Effects[0].(DataEffect)) + 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 index ff744f6..3effd34 100644 --- a/format/s3m/channel/effect_volslidedown.go +++ b/format/s3m/channel/effect_volslidedown.go @@ -3,29 +3,32 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e VolumeSlideDown) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e VolumeSlideDown) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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.VolSlideEveryFrame || currentTick != 0 { - return doVolSlide(cs, -float32(y), 1.0) + if mem.Shared.VolSlideEveryFrame || tick != 0 { + return m.SlideChannelVolume(ch, 1, -float32(y)) } return nil } -func (e VolumeSlideDown) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e VolumeSlideDown) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effect_volslideup.go b/format/s3m/channel/effect_volslideup.go index 6b761a6..4b6b7c5 100644 --- a/format/s3m/channel/effect_volslideup.go +++ b/format/s3m/channel/effect_volslideup.go @@ -3,29 +3,32 @@ 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/index" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e VolumeSlideUp) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e VolumeSlideUp) Tick(cs S3MChannel, p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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.VolSlideEveryFrame || currentTick != 0 { - return doVolSlide(cs, float32(x), 1.0) + if mem.Shared.VolSlideEveryFrame || tick != 0 { + return m.SlideChannelVolume(ch, 1, float32(x)) } return nil } -func (e VolumeSlideUp) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +func (e VolumeSlideUp) TraceData() string { + return e.String() } diff --git a/format/s3m/channel/effectfactory.go b/format/s3m/channel/effectfactory.go index 9dd3f53..73d3211 100644 --- a/format/s3m/channel/effectfactory.go +++ b/format/s3m/channel/effectfactory.go @@ -1,13 +1,14 @@ package channel import ( + s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/song" ) type ChannelCommand DataEffect // Factory produces an effect for the provided channel pattern data -func EffectFactory(mem *Memory, data song.ChannelData) EffectS3M { +func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) EffectS3M { if data == nil { return nil } diff --git a/format/s3m/channel/machine.go b/format/s3m/channel/machine.go new file mode 100644 index 0000000..f81c032 --- /dev/null +++ b/format/s3m/channel/machine.go @@ -0,0 +1,79 @@ +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 doPortaUp(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], amount float32, multiplier float32) error { + cur, err := m.GetChannelPeriod(ch) + if err != nil { + return err + } + if cur.IsInvalid() { + return nil + } + + delta := int(amount * multiplier) + cur = cur.PortaUp(delta) + return m.SetChannelPeriod(ch, cur) +} + +func doPortaDown(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], amount float32, multiplier float32) error { + cur, err := m.GetChannelPeriod(ch) + if err != nil { + return err + } + if cur.IsInvalid() { + return nil + } + + delta := int(amount * multiplier) + cur = cur.PortaDown(delta) + return m.SetChannelPeriod(ch, cur) +} + +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 cd03448..235d9f9 100644 --- a/format/s3m/channel/memory.go +++ b/format/s3m/channel/memory.go @@ -1,7 +1,6 @@ package channel import ( - "github.com/gotracker/playback/note" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback/memory" @@ -12,8 +11,7 @@ import ( // Memory is the storage object for custom effect/command values type Memory struct { - semitone note.Semitone - inst InstID + inst InstID portaToNote memory.Value[DataEffect] vibratoSpeed memory.Value[DataEffect] @@ -33,16 +31,6 @@ type Memory struct { Shared *SharedMemory } -// Semitone gets or sets the most recent non-zero value (or input) for the semitone -func (m *Memory) Semitone(input note.Semitone) note.Semitone { - if input == 0 { - return m.semitone - } - - m.semitone = input - return input -} - // Inst gets or sets the most recent non-zero value (or input) for inst func (m *Memory) Inst(input InstID) InstID { if input == 0 { @@ -136,7 +124,6 @@ func (m *Memory) GetPatternLoop() *formatutil.PatternLoop { // StartOrder is called when the first order's row at tick 0 is started func (m *Memory) StartOrder() { if m.Shared.ResetMemoryAtStartOfOrder0 { - m.semitone = 0 m.inst = 0 m.portaToNote.Reset() m.vibratoSpeed.Reset() diff --git a/format/s3m/channel/unhandled.go b/format/s3m/channel/unhandled.go index 44d8056..db233a2 100644 --- a/format/s3m/channel/unhandled.go +++ b/format/s3m/channel/unhandled.go @@ -2,6 +2,12 @@ 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 @@ -10,14 +16,6 @@ type UnhandledCommand struct { Info DataEffect } -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand) PreStart(cs S3MChannel, m 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) } @@ -27,3 +25,14 @@ func (e UnhandledCommand) Names() []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/channel/util.go b/format/s3m/channel/util.go index 0f91629..594f0e3 100644 --- a/format/s3m/channel/util.go +++ b/format/s3m/channel/util.go @@ -1,11 +1,11 @@ package channel import ( - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" "github.com/gotracker/gomixing/volume" "github.com/heucuva/comparison" "github.com/gotracker/playback" + 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" @@ -14,7 +14,7 @@ import ( ) type EffectS3M = playback.Effect -type S3MChannel = playback.Channel[period.Amiga, Memory, Data] +type S3MChannel = playback.Channel[period.Amiga, *Memory, Data, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] // S3M is an interface to S3M effect operations type S3M interface { @@ -33,10 +33,9 @@ type S3M interface { IgnoreUnknownEffect() bool // Unhandled } -func doVolSlide(cs S3MChannel, delta float32, multiplier float32) error { +func oldDoVolSlide(cs S3MChannel, delta float32, multiplier float32) error { active := cs.GetActiveState() - av := active.GetVolume() - v := s3mVolume.VolumeToS3M(av) + v := active.GetVolume() vol := int16(float32(v) + (delta * multiplier)) if vol >= 64 { vol = 63 @@ -44,13 +43,12 @@ func doVolSlide(cs S3MChannel, delta float32, multiplier float32) error { if vol < 0 { vol = 0 } - sv := s3mfile.Volume(vol) - nv := s3mVolume.VolumeFromS3M(sv) - active.SetVolume(nv) + sv := s3mVolume.Volume(vol) + active.SetVolume(sv) return nil } -func doPortaUp(cs S3MChannel, amount float32, multiplier float32) error { +func oldDoPortaUp(cs S3MChannel, amount float32, multiplier float32) error { active := cs.GetActiveState() cur := active.Period if cur.IsInvalid() { @@ -84,7 +82,7 @@ func doPortaUpToNote(cs S3MChannel, amount float32, multiplier float32, target p return nil } -func doPortaDown(cs S3MChannel, amount float32, multiplier float32) error { +func oldDoPortaDown(cs S3MChannel, amount float32, multiplier float32) error { active := cs.GetActiveState() cur := active.Period if cur.IsInvalid() { @@ -114,7 +112,7 @@ func doPortaDownToNote(cs S3MChannel, amount float32, multiplier float32, target return nil } -func doVibrato(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { +func oldDoVibrato(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() delta := calculateWaveTable(cs, currentTick, DataEffect(speed), DataEffect(depth), multiplier, mem.VibratoOscillator()) d := period.Delta(delta) @@ -122,7 +120,7 @@ func doVibrato(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffec return nil } -func doTremor(cs S3MChannel, currentTick int, onTicks int, offTicks int) error { +func oldDoTremor(cs S3MChannel, currentTick int, onTicks int, offTicks int) error { mem := cs.GetMemory() tremor := mem.TremorMem() if tremor.IsActive() { @@ -138,7 +136,7 @@ func doTremor(cs S3MChannel, currentTick int, onTicks int, offTicks int) error { return nil } -func doArpeggio(cs S3MChannel, currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { +func oldDoArpeggio(cs S3MChannel, currentTick int, arpSemitoneADelta int8, arpSemitoneBDelta int8) error { ns := cs.GetNoteSemitone() var arpSemitoneTarget note.Semitone switch currentTick % 3 { @@ -155,7 +153,7 @@ func doArpeggio(cs S3MChannel, currentTick int, arpSemitoneADelta int8, arpSemit } var ( - volSlideTwoThirdsTable = [...]s3mfile.Volume{ + volSlideTwoThirdsTable = [...]s3mVolume.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, @@ -165,18 +163,18 @@ var ( func doVolSlideTwoThirds(cs S3MChannel) error { active := cs.GetActiveState() - vol := s3mVolume.VolumeToS3M(active.GetVolume()) + vol := active.GetVolume() if vol >= 64 { vol = 63 } - active.SetVolume(s3mVolume.VolumeFromS3M(volSlideTwoThirdsTable[vol])) + active.SetVolume(volSlideTwoThirdsTable[vol]) return nil } func doTremolo(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { mem := cs.GetMemory() delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return doVolSlide(cs, delta, 1.0) + return oldDoVolSlide(cs, delta, 1.0) } func calculateWaveTable(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { diff --git a/format/s3m/layout/channelsetting.go b/format/s3m/layout/channelsetting.go index f844173..c082d03 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -2,17 +2,61 @@ 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/format/s3m/channel" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/song" ) // ChannelSetting is settings specific to a single channel type ChannelSetting struct { - Enabled bool - OutputChannelNum int - Category s3mfile.ChannelCategory - InitialVolume volume.Volume - InitialPanning panning.Position - Memory channel.Memory + Enabled bool + OutputChannelNum int + Category s3mfile.ChannelCategory + InitialVolume s3mVolume.Volume + PanEnabled bool + InitialPanning s3mPanning.Panning + Memory channel.Memory + DefaultFilterName string +} + +var _ song.ChannelSettings = (*ChannelSetting)(nil) + +func (c ChannelSetting) GetEnabled() bool { + return c.Enabled +} + +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) GetPanEnabled() bool { + return c.PanEnabled +} + +func (c ChannelSetting) GetDefaultFilterName() string { + return c.DefaultFilterName +} + +func (c ChannelSetting) IsDefaultFilterEnabled() bool { + return len(c.DefaultFilterName) > 0 } diff --git a/format/s3m/layout/header.go b/format/s3m/layout/header.go index a3b6e73..ea56509 100644 --- a/format/s3m/layout/header.go +++ b/format/s3m/layout/header.go @@ -1,13 +1,17 @@ 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 + GlobalVolume s3mVolume.Volume + MixingVolume s3mVolume.FineVolume Stereo bool + InitialOrder index.Order } diff --git a/format/s3m/layout/song.go b/format/s3m/layout/song.go index 0ec8844..aef1af1 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -1,44 +1,142 @@ package layout import ( + "reflect" + "time" + + "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/format/s3m/channel" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" "github.com/gotracker/playback/format/s3m/pattern" + 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/instrument" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/system" ) // Song is the full definition of the song data of an Song file type Song struct { + System system.System Head Header - Instruments []*instrument.Instrument + Instruments []*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] Patterns []pattern.Pattern ChannelSettings []ChannelSetting + NumChannels int OrderList []index.Pattern } +func (Song) GetPeriodType() reflect.Type { + var p period.Amiga + return reflect.TypeOf(p) +} + +func (s Song) GetGlobalVolumeType() reflect.Type { + return reflect.TypeOf(s.Head.GlobalVolume) +} + +func (s Song) GetChannelMixingVolumeType() reflect.Type { + return reflect.TypeOf(s.Head.MixingVolume) +} + +func (s Song) GetChannelVolumeType() reflect.Type { + var cs ChannelSetting + return reflect.TypeOf(cs.InitialVolume) +} + +func (s Song) GetChannelPanningType() reflect.Type { + var cs ChannelSetting + return reflect.TypeOf(cs.InitialPanning) +} + // GetOrderList returns the list of all pattern orders for the song func (s Song) GetOrderList() []index.Pattern { return s.OrderList } -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) *song.Pattern[channel.Data] { +// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song +func (s Song) GetInitialBPM() int { + return s.Head.InitialTempo +} + +// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song +func (s Song) GetInitialTempo() int { + return s.Head.InitialSpeed +} + +// GetGlobalVolumeGeneric returns the initial global volume for the song +func (s Song) GetGlobalVolumeGeneric() volume.Volume { + return s.Head.GlobalVolume.ToVolume() +} + +// GetGlobalVolume returns the initial global volume for the song +func (s Song) GetGlobalVolume() s3mVolume.Volume { + return s.Head.GlobalVolume +} + +// GetMixingVolumeGeneric returns the initial mixing volume for the song +func (s Song) GetMixingVolumeGeneric() volume.Volume { + return s.Head.MixingVolume.ToVolume() +} + +// GetMixingVolume returns the initial mixing volume for the song +func (s Song) GetMixingVolume() s3mVolume.FineVolume { + return s.Head.MixingVolume +} + +const durationPerBpm = time.Duration(2500) * time.Millisecond + +// GetTickDuration calculates the duration of a tick at a particular BPM +func (s Song) 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 Song) GetPattern(patNum index.Pattern) (song.Pattern[channel.Data, s3mVolume.Volume], error) { if int(patNum) >= len(s.Patterns) { - return nil + return nil, song.ErrStopSong + } + return s.Patterns[patNum], nil +} + +// GetPattern returns an interface to a specific pattern indexed by `patNum` +func (s Song) GetPatternIntf(patNum index.Pattern) (song.PatternIntf, error) { + return s.GetPattern(patNum) +} + +// GetPatternByOrder returns the pattern specified by the order index provided +func (s Song) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, 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.Patterns[patNum] + + return s.GetPatternIntf(pat) } -// IsChannelEnabled returns true if the channel at index `channelNum` is enabled -func (s Song) IsChannelEnabled(channelNum int) bool { - return s.ChannelSettings[channelNum].Enabled +// GetNumChannels returns the number of channels the song has +func (s Song) GetNumChannels() int { + return s.NumChannels } -// GetRenderChannel returns the output channel for the channel at index `channelNum` -func (s Song) GetRenderChannel(channelNum int) int { - return s.ChannelSettings[channelNum].OutputChannelNum +// GetChannelSettings returns the channel settings at index `channelNum` +func (s Song) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + return s.ChannelSettings[channelNum] } // NumInstruments returns the number of instruments in the song @@ -60,7 +158,7 @@ func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { } // GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instID instrument.ID) (*instrument.Instrument, note.Semitone) { +func (s Song) GetInstrument(instID instrument.ID) (instrument.InstrumentIntf, note.Semitone) { if instID.IsEmpty() { return nil, note.UnchangedSemitone } @@ -76,3 +174,34 @@ func (s Song) GetInstrument(instID instrument.ID) (*instrument.Instrument, note. func (s Song) GetName() string { return s.Head.Name } + +func (s Song) GetPeriodCalculator() song.PeriodCalculatorIntf { + return s3mPeriod.AmigaConverter +} + +func (s Song) GetInitialOrder() index.Order { + return s.Head.InitialOrder +} + +func (s Song) GetRowRenderStringer(row song.RowIntf, channels int, longFormat bool) render.RowStringer { + nch := min(s.NumChannels, channels) + rt := render.NewRowText[channel.Data](nch, longFormat) + rowData := make(pattern.Row, 0, nch) + pr := row.(pattern.Row) + nprch := min(pr.GetNumChannels(), nch) + for i := 0; i < nprch; i++ { + if !s.ChannelSettings[i].Enabled { + continue + } + rowData = append(rowData, pr[i]) + } + for len(rowData) < nch { + rowData = append(rowData, channel.Data{}) + } + rt.Channels = rowData + return rt +} + +func (s Song) GetSystem() system.System { + return s.System +} diff --git a/format/s3m/load/load.go b/format/s3m/load/load.go index 8051631..1cdc298 100644 --- a/format/s3m/load/load.go +++ b/format/s3m/load/load.go @@ -5,13 +5,13 @@ import ( "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 diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index 5f03d2f..e368602 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -10,6 +10,7 @@ import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" ) func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) { @@ -29,7 +30,7 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) What: s3mfile.PatternFlags(c & 0x1F), Note: s3mfile.EmptyNote, Instrument: channel.InstID(sampleNumber), - Volume: s3mfile.EmptyVolume, + Volume: s3mVolume.Volume(s3mfile.EmptyVolume), Command: uint8(0), Info: channel.DataEffect(0), } @@ -86,7 +87,7 @@ 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 diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index a7f88a5..f4cd0a8 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -7,18 +7,19 @@ import ( "io" s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" "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/pattern" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "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" @@ -32,7 +33,7 @@ func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { Name: fh.GetName(), InitialSpeed: int(fh.InitialSpeed), InitialTempo: int(fh.InitialTempo), - GlobalVolume: s3mVolume.VolumeFromS3M(fh.GlobalVolume), + GlobalVolume: s3mVolume.Volume(fh.GlobalVolume), Stereo: (fh.MixingVolume & 0x80) != 0, } @@ -40,29 +41,29 @@ func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { 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[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { + sample := instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Static: instrument.StaticValues[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ Filename: scrs.Head.GetFilename(), Name: si.GetSampleName(), - Volume: s3mVolume.VolumeFromS3M(si.Volume), + Volume: s3mVolume.Volume(si.Volume), }, SampleRate: period.Frequency(si.C2Spd.Lo), } 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[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { + sample := instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Static: instrument.StaticValues[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ Filename: scrs.Head.GetFilename(), Name: si.GetSampleName(), - Volume: s3mVolume.VolumeFromS3M(si.Volume), + Volume: s3mVolume.Volume(si.Volume), }, SampleRate: period.Frequency(si.C2Spd.Lo), } @@ -80,10 +81,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,12 +117,12 @@ 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[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { + inst := instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + Static: instrument.StaticValues[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ Filename: scrs.Head.GetFilename(), Name: si.GetSampleName(), - Volume: s3mVolume.VolumeFromS3M(si.Volume), + Volume: s3mVolume.Volume(si.Volume), }, SampleRate: period.Frequency(si.C2Spd.Lo), } @@ -165,7 +164,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[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { if scrs == nil { return nil, errors.New("scrs is nil") } @@ -216,7 +215,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 @@ -257,8 +256,9 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } song := layout.Song{ + System: s3mSystem.S3MSystem, Head: *h, - Instruments: make([]*instrument.Instrument, len(f.InstrumentPointers)), + Instruments: make([]*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.InstrumentPointers)), OrderList: make([]index.Pattern, len(f.OrderList)), } @@ -283,7 +283,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, song.OrderList[i] = index.Pattern(o) } - song.Instruments = make([]*instrument.Instrument, len(f.Instruments)) + song.Instruments = make([]*instrument.Instrument[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,15 +296,15 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, song.Instruments[instNum] = sample } - lastEnabledChannel := 0 + maxPatternChannel := 0 song.Patterns = make([]pattern.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 } @@ -321,25 +321,32 @@ 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 := 0 for chNum, ch := range f.ChannelSettings { chn := ch.GetChannel() cs := layout.ChannelSetting{ Enabled: ch.IsEnabled(), Category: chn.GetChannelCategory(), OutputChannelNum: int(ch.GetChannel() & 0x07), - InitialVolume: s3mVolume.DefaultVolume, + InitialVolume: s3mVolume.Volume(s3mfile.DefaultVolume), + PanEnabled: h.Stereo, InitialPanning: s3mPanning.DefaultPanning, Memory: channel.Memory{ Shared: &sharedMem, }, + DefaultFilterName: "", + } + + if sbFilterEnable { + cs.DefaultFilterName = "amigalpf" } cs.Memory.ResetOscillators() 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: @@ -357,12 +364,13 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } } - song.ChannelSettings = channels[:lastEnabledChannel+1] + song.NumChannels = lastEnabledChannel + 1 + song.ChannelSettings = channels[:maxPatternChannel+1] return &song, 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/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 index 7d25c82..6d5ab79 100644 --- a/format/s3m/pattern/pattern.go +++ b/format/s3m/pattern/pattern.go @@ -2,7 +2,8 @@ package pattern import ( "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/song" ) -type Pattern = song.Pattern[channel.Data] +type Pattern = song.Pattern[channel.Data, s3mVolume.Volume] diff --git a/format/s3m/pattern/row.go b/format/s3m/pattern/row.go index c64c038..0396bf1 100644 --- a/format/s3m/pattern/row.go +++ b/format/s3m/pattern/row.go @@ -2,7 +2,8 @@ package pattern import ( "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/song" ) -type Row = song.Row[channel.Data] +type Row = song.Row[channel.Data, s3mVolume.Volume] diff --git a/format/s3m/pattern/state.go b/format/s3m/pattern/state.go index 8f4cb8f..b5a7081 100644 --- a/format/s3m/pattern/state.go +++ b/format/s3m/pattern/state.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/gotracker/playback/format/s3m/channel" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/pattern" "github.com/gotracker/playback/player/feature" @@ -235,7 +236,7 @@ func (state *State) nextRow() error { } // GetRows returns all the rows in the pattern -func (state *State) GetRows() (*song.Pattern[channel.Data], error) { +func (state *State) GetRows() (*song.Pattern[channel.Data, s3mVolume.Volume], error) { nextRow: for loops := 0; loops < len(state.Patterns); loops++ { var patNum = state.GetPatNum() diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index 46297b3..3f8dcb0 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -1,390 +1,114 @@ package playback import ( - s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/channel" "github.com/gotracker/playback/format/s3m/layout" - "github.com/gotracker/playback/format/s3m/pattern" - s3mPeriod "github.com/gotracker/playback/format/s3m/period" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" s3mSystem "github.com/gotracker/playback/format/s3m/system" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/output" - playpattern "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/period" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/player" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state" "github.com/gotracker/playback/song" "github.com/gotracker/playback/system" - "github.com/gotracker/playback/tracing" ) -type channelState = state.ChannelState[period.Amiga, channel.Memory, channel.Data] - // manager is a playback manager for S3M music type manager struct { player.Tracker - tracing.Tracing[period.Amiga, channel.Memory, channel.Data] - song *layout.Song - - channels []channelState - pattern pattern.State - - preMixRowTxn *playpattern.RowUpdateTransaction - postMixRowTxn *playpattern.RowUpdateTransaction - premix *output.PremixData - - rowRenderState *state.RowRenderState - OnEffect func(playback.Effect) - - chOrder [4][]*channelState } var _ playback.Playback = (*manager)(nil) -var _ playback.Channel[period.Amiga, channel.Memory, channel.Data] = (*state.ChannelState[period.Amiga, channel.Memory, channel.Data])(nil) -var s3m system.System = s3mSystem.S3MSystem +var s3m system.ClockableSystem = s3mSystem.S3MSystem // NewManager creates a new manager for an S3M song -func NewManager(song *layout.Song) (playback.Playback, error) { +func NewManager(songData song.Data) (playback.Playback, error) { + s := songData.(*layout.Song) + m := manager{ Tracker: player.Tracker{ - BaseClockRate: s3m.GetBaseClock(), + BaseClockRate: s3m.GetBaseClock(), + LongChannelOutput: true, }, - song: song, - } - - m.Tracing.Playback = &m - m.Tracing.ChannelGetter = func(c int) playback.Channel[period.Amiga, channel.Memory, channel.Data] { - return m.GetChannel(c) - } - - m.Tracker.PreTickable = &m - m.Tracker.Tickable = &m - m.Tracker.Premixable = &m - m.Tracker.Traceable = &m.Tracing - - m.pattern.Reset() - m.pattern.Orders = song.OrderList - m.pattern.Patterns = song.Patterns - - m.SetGlobalVolume(song.Head.GlobalVolume) - m.SetMixerVolume(song.Head.MixingVolume) - - m.SetNumChannels(len(song.ChannelSettings)) - lowpassEnabled := false - for i, ch := range song.ChannelSettings { - oc := m.GetRenderChannel(ch.OutputChannelNum, m.channelInit) - - cs := m.GetChannel(i) - cs.PeriodConverter = s3mPeriod.AmigaConverter - cs.SetSongDataInterface(song) - cs.SetRenderChannel(oc) - cs.SetGlobalVolume(m.GetGlobalVolume()) - active := cs.GetActiveState() - active.SetVolume(ch.InitialVolume) - if song.Head.Stereo { - cs.SetPanEnabled(true) - active.Pan = ch.InitialPanning - } else { - cs.SetPanEnabled(false) - active.Pan = panning.CenterAhead - } - cs.SetStoredSemitone(note.UnchangedSemitone) - mem := &song.ChannelSettings[i].Memory - cs.SetMemory(mem) - if mem.Shared.LowPassFilterEnable { - lowpassEnabled = true - } - - // weirdly, S3M processes channels in channel category order - // so we have to make a list with the order we're expecting - switch s3mfile.ChannelCategory(ch.Category) { - case s3mfile.ChannelCategoryUnknown: - // do nothing - default: - cIdx := int(ch.Category) - 1 - m.chOrder[cIdx] = append(m.chOrder[cIdx], cs) - } - } - - if lowpassEnabled { - m.SetFilterEnable(true) - } - - txn := m.pattern.StartTransaction() - - txn.Ticks.Set(song.Head.InitialSpeed) - txn.Tempo.Set(song.Head.InitialTempo) - - if err := txn.Commit(); err != nil { - return nil, err + song: s, } return &m, nil } -func (m *manager) channelInit(ch int) *render.Channel { - return &render.Channel{ - ChannelNum: ch, - Filter: nil, - GetSampleRate: m.GetSampleRate, - SetGlobalVolume: m.SetGlobalVolume, - GetOPL2Chip: m.GetOPL2Chip, - ChannelVolume: volume.Volume(1), - } -} - -// StartPatternTransaction returns a new row update transaction for the pattern system -func (m *manager) StartPatternTransaction() *playpattern.RowUpdateTransaction { - return m.pattern.StartTransaction() -} - -// GetNumChannels returns the number of channels -func (m *manager) GetNumChannels() int { - return len(m.channels) -} - -func (m *manager) semitoneSetterFactory(st note.Semitone, fn state.PeriodUpdateFunc[period.Amiga]) state.NoteOp[period.Amiga, channel.Memory, channel.Data] { - return doNoteCalc{ - Semitone: st, - UpdateFunc: fn, - } -} - -// SetNumChannels updates the song to have the specified number of channels and resets their states -func (m *manager) SetNumChannels(num int) { - m.channels = make([]channelState, num) - - for ch := range m.channels { - cs := &m.channels[ch] - cs.ResetStates() - cs.SemitoneSetterFactory = m.semitoneSetterFactory - - cs.PortaTargetPeriod.Reset() - cs.Trigger.Reset() - cs.RetriggerCount = 0 - _ = cs.SetData(channel.Data{}) - ocNum := m.song.GetRenderChannel(ch) - cs.RenderChannel = m.GetRenderChannel(ocNum, m.channelInit) - } -} - -// SetNextOrder sets the next order index -func (m *manager) SetNextOrder(order index.Order) error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.SetNextOrder(order) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetNextOrder(order) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetNextRow sets the next row index -func (m *manager) SetNextRow(row index.Row) error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.SetNextRow(row) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetNextRow(row) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetNextRowWithBacktrack will set the next row index and backtracing allowance -func (m *manager) SetNextRowWithBacktrack(row index.Row, allowBacktrack bool) error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.SetNextRowWithBacktrack(row, allowBacktrack) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.SetNextRowWithBacktrack(row, allowBacktrack) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// BreakOrder breaks to the next pattern in the order -func (m *manager) BreakOrder() error { - if m.postMixRowTxn != nil { - m.postMixRowTxn.BreakOrder = true - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.BreakOrder = true - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// SetTempo sets the desired tempo for the song -func (m *manager) SetTempo(tempo int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.Tempo.Set(tempo) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.Tempo.Set(tempo) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// DecreaseTempo reduces the tempo by the `delta` value -func (m *manager) DecreaseTempo(delta int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.AccTempoDelta(-delta) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.AccTempoDelta(-delta) - if err := rowTxn.Commit(); err != nil { - return err - } - } - - return nil -} - -// IncreaseTempo increases the tempo by the `delta` value -func (m *manager) IncreaseTempo(delta int) error { - if m.preMixRowTxn != nil { - m.preMixRowTxn.AccTempoDelta(delta) - } else { - rowTxn := m.pattern.StartTransaction() - defer rowTxn.Cancel() - - rowTxn.AccTempoDelta(delta) - if err := rowTxn.Commit(); err != nil { - return err - } +func (m *manager) channelInit(ch int) render.ChannelIntf { + return &render.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]{ + ChannelNum: ch, + Filter: nil, + GetSampleRate: m.GetSampleRate, + SetGlobalVolume: func(v s3mVolume.Volume) error { + m.SetGlobalVolume(v.ToVolume()) + return nil + }, + GetOPL2Chip: m.GetOPL2Chip, + ChannelVolume: s3mVolume.MaxFineVolume, } - - return nil } // Configure sets specified features func (m *manager) Configure(features []feature.Feature) error { - if err := m.Tracker.Configure(features); err != nil { + us, err := m.Tracker.Configure(features) + if err != nil { return err } for _, feat := range features { switch f := feat.(type) { case feature.SongLoop: - m.pattern.SongLoop = f + us.SongLoop = f case feature.PlayUntilOrderAndRow: - m.pattern.PlayUntilOrderAndRow = f + us.PlayUntilOrderAndRow = f case feature.SetDefaultTempo: - txn := m.pattern.StartTransaction() - txn.Ticks.Set(f.Tempo) - if err := txn.Commit(); err != nil { - return err - } + us.StartTempo = f.Tempo case feature.SetDefaultBPM: - txn := m.pattern.StartTransaction() - txn.Tempo.Set(f.BPM) - if err := txn.Commit(); err != nil { - return err - } + us.StartBPM = f.BPM } } - return nil + return m.SetupMachine(m.song, us) } -// CanOrderLoop returns true if the song is allowed to order loop -func (m *manager) CanOrderLoop() bool { - return (m.pattern.SongLoop.Count != 0) -} +// SetupSampler configures the internal sampler +func (m *manager) SetupSampler(samplesPerSecond int, channels int) error { + if err := m.Tracker.SetupSampler(samplesPerSecond, channels); err != nil { + return err + } -// GetSongData gets the song data object -func (m *manager) GetSongData() song.Data { - return m.song -} + var oplLen int + //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Melody)-1]) + //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Drums)-1]) -// GetChannel returns the channel interface for the specified channel number -func (m *manager) GetChannel(ch int) *channelState { - return &m.channels[ch] + if oplLen > 0 { + m.ensureOPL2() + } + return nil } -// GetCurrentOrder returns the current order -func (m *manager) GetCurrentOrder() index.Order { - return m.pattern.GetCurrentOrder() +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) + } + } } -// GetNumOrders returns the number of orders in the song func (m *manager) GetNumOrders() int { - return m.pattern.GetNumOrders() -} - -// GetCurrentRow returns the current row -func (m *manager) GetCurrentRow() index.Row { - return m.pattern.GetCurrentRow() + return len(m.song.GetOrderList()) } -func (m *manager) GetRenderState() playback.RowRenderState { - return m.rowRenderState +func (m *manager) CanOrderLoop() bool { + return true } -// GetName returns the current song's name func (m *manager) GetName() string { return m.song.GetName() } - -// SetOnEffect sets the callback for an effect being generated for a channel -func (m *manager) SetOnEffect(fn func(playback.Effect)) { - m.OnEffect = fn -} - -func (m *manager) GetOnEffect() func(playback.Effect) { - return m.OnEffect -} - -func (m *manager) SetEnvelopePosition(v int) { -} - -// SetupSampler configures the internal sampler -func (m *manager) SetupSampler(samplesPerSecond int, channels int) error { - if err := m.Tracker.SetupSampler(samplesPerSecond, channels); err != nil { - return err - } - - oplLen := len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Melody)-1]) - oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Drums)-1]) - - if oplLen > 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 ed48fb4..0000000 --- a/format/s3m/playback/playback_command.go +++ /dev/null @@ -1,179 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback" - "github.com/gotracker/playback/filter" - s3mPeriod "github.com/gotracker/playback/format/s3m/period" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/state" -) - -type doNoteCalc struct { - Semitone note.Semitone - UpdateFunc state.PeriodUpdateFunc[period.Amiga] -} - -func (o doNoteCalc) Process(p playback.Playback, cs *channelState) error { - if o.UpdateFunc == nil { - return nil - } - - if inst := cs.GetTargetState().Instrument; inst != nil { - cs.Semitone = note.Semitone(int(o.Semitone) + int(inst.GetSemitoneShift())) - period := s3mPeriod.CalcSemitonePeriod(cs.Semitone, inst.GetFinetune(), inst.GetSampleRate()) - o.UpdateFunc(period) - } - return nil -} - -func (m *manager) processEffect(ch int, cs *channelState, 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 *channelState, currentTick int, lastTick bool) error { - triggerTick, noteAction := cs.WillTriggerOn(currentTick) - if !triggerTick { - return nil - } - n := cs.GetChannelData().GetNote() - keyOn := false - keyOff := false - stop := false - - active := cs.GetActiveState() - target := cs.GetTargetState() - - if targetInst := target.Instrument; targetInst != nil { - cs.SetInstrument(targetInst) - keyOn = true - } else { - cs.SetInstrument(nil) - } - - if cs.UseTargetPeriod { - targetPeriod := target.Period - active.Period = targetPeriod - cs.SetPortaTargetPeriod(targetPeriod) - } - active.Pos = target.Pos - - if active.Instrument != nil { - keyOff = active.Instrument.IsReleaseNote(n) - stop = active.Instrument.IsStopNote(n) - } - - if curVoice := cs.GetVoice(); curVoice != 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()) - curVoice.Attack() - mem := cs.GetMemory() - mem.Retrigger() - } else if keyOff { - curVoice.Release() - curVoice.Fadeout() - } else if stop { - cs.SetInstrument(nil) - active.NoteCut() - } - } - return nil -} - -func (m *manager) processVoiceUpdates(ch int, cs *channelState, currentTick int, lastTick bool) error { - if cs.UsePeriodOverride { - cs.UsePeriodOverride = false - arpeggioPeriod := cs.GetPeriodOverride() - cs.GetActiveState().Period = 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.DefaultC4SampleRate, 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 8c5e63b..0000000 --- a/format/s3m/playback/playback_pattern.go +++ /dev/null @@ -1,182 +0,0 @@ -package playback - -import ( - "errors" - "time" - - "github.com/gotracker/playback/format/s3m/channel" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/state" - "github.com/gotracker/playback/song" -) - -const ( - tickBaseDuration = time.Duration(2500) * time.Millisecond -) - -func (m *manager) startProcessPatternRow() 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 - } - } - - myCurrentRow := m.pattern.GetCurrentRow() - - row := pat.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 = &state.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(state.NewChannelDataTxn[period.Amiga, channel.Memory, channel.Data](channel.GetTargetsFromData)) - if resetMemory { - mem := cs.GetMemory() - mem.StartOrder() - } - } - - // generate effects and run prestart - nch := row.GetNumChannels() - if actual := m.GetNumChannels(); nch > actual { - nch = actual - } - - for channelNum := 0; channelNum < nch; channelNum++ { - cdata := row.GetChannel(index.Channel(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 - } - - return nil -} - -func (m *manager) processPatternRow() error { - patIdx, err := m.pattern.GetCurrentPatternIdx() - if err != nil { - return err - } - rowIdx := m.pattern.GetCurrentRow() - - pat := m.song.GetPattern(patIdx) - row := pat.GetRow(rowIdx) - nch := row.GetNumChannels() - if actual := m.GetNumChannels(); nch > actual { - nch = actual - } - - s := m.GetSampler() - - 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 *channelState) 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 791eb66..0000000 --- a/format/s3m/playback/playback_render.go +++ /dev/null @@ -1,108 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/player/render" -) - -// OnPreTick runs the S3M pre-tick processing -func (m *manager) OnPreTick() error { - m.premix = nil - m.postMixRowTxn = m.pattern.StartTransaction() - - if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { - if err := m.startProcessPatternRow(); err != nil { - return err - } - } - - return nil -} - -// OnTick runs the S3M tick processing -func (m *manager) OnTick() error { - postMixRowTxn := m.postMixRowTxn - defer func() { - postMixRowTxn.Cancel() - m.postMixRowTxn = nil - }() - - if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { - if err := m.processPatternRow(); err != nil { - return err - } - } - - var finalData render.RowRender - premix := &output.PremixData{ - Userdata: &finalData, - SamplesLen: m.rowRenderState.Samples, - } - - if err := m.soundRenderTick(premix); err != nil { - return 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 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 -} - -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 00ac663..0000000 --- a/format/s3m/playback/playback_textoutput.go +++ /dev/null @@ -1,25 +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 - } - - rowText.Channels[ch] = cs.GetChannelData() - } - return &rowText -} diff --git a/format/s3m/s3m.go b/format/s3m/s3m.go index d752c54..b12c217 100644 --- a/format/s3m/s3m.go +++ b/format/s3m/s3m.go @@ -6,7 +6,9 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/load" + "github.com/gotracker/playback/format/s3m/settings" "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/util" ) @@ -31,3 +33,7 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { return load.S3M(r, features) } + +func init() { + machine.RegisterMachine(settings.GetMachineSettings()) +} diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go new file mode 100644 index 0000000..6de4f64 --- /dev/null +++ b/format/s3m/settings/machine.go @@ -0,0 +1,54 @@ +package settings + +import ( + "fmt" + + "github.com/gotracker/playback/filter" + 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/oscillator" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" + voiceOscillator "github.com/gotracker/playback/voice/oscillator" +) + +func GetMachineSettings() *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + return &amigaSettings +} + +var ( + amigaSettings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + PeriodConverter: s3mPeriod.AmigaConverter, + GetFilterFactory: filterFactory, + GetVibratoFactory: vibratoFactory, + GetTremoloFactory: tremoloFactory, + GetPanbrelloFactory: panbrelloFactory, + VoiceFactory: amigaVoiceFactory, + } +) + +func filterFactory(name string) (settings.FilterFactoryFunc, error) { + switch name { + case "amigalpf": + return func(instrument, playback period.Frequency) (filter.Filter, error) { + lpf := filter.NewAmigaLPF(instrument, playback) + return lpf, nil + }, nil + + default: + return nil, fmt.Errorf("unsupported filter: %q", name) + } +} + +func vibratoFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewProtrackerOscillator(), nil +} + +func tremoloFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewProtrackerOscillator(), nil +} + +func panbrelloFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewProtrackerOscillator(), nil +} diff --git a/format/s3m/settings/voicefactory.go b/format/s3m/settings/voicefactory.go new file mode 100644 index 0000000..1d14878 --- /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() voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + return s3mVoice.New(GetMachineSettings()) +} + +var ( + amigaVoiceFactory voiceFactory +) diff --git a/format/s3m/system/system.go b/format/s3m/system/system.go index c718c78..4e03878 100644 --- a/format/s3m/system/system.go +++ b/format/s3m/system/system.go @@ -26,8 +26,14 @@ const ( C4SlideFines = 4 * SlideFinesPerOctave ) -var S3MSystem system.System = system.ClockedSystem{ - BaseClock: S3MBaseClock, - BaseFinetunes: C4SlideFines, - FinetunesPerOctave: SlideFinesPerOctave, +var semitonePeriodTable = [...]float32{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, + CommonRate: DefaultC4SampleRate, + SemitonePeriods: semitonePeriodTable, } diff --git a/format/s3m/voice/render.go b/format/s3m/voice/render.go new file mode 100644 index 0000000..a81a195 --- /dev/null +++ b/format/s3m/voice/render.go @@ -0,0 +1,53 @@ +package voice + +import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/volume" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/state/render" +) + +func (v *s3mVoice) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *playerRender.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]) (*mixing.Data, error) { + if v.IsDone() { + return nil, nil + } + + if !v.IsActive() { + return nil, nil + } + + sampler, err := v.GetSampler(details.SamplerSpeed, renderChannel) + if err != nil || sampler == nil { + return nil, err + } + + // ... so grab the new value now. + pan := v.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.ToPosition(), + Volume: volume.Volume(1.0), + Pos: 0, + SamplesLen: details.Samples, + } + + if err := v.SetPos(sampler.GetPosition()); err != nil { + return nil, err + } + + return data, nil +} diff --git a/format/s3m/voice/sampler.go b/format/s3m/voice/sampler.go new file mode 100644 index 0000000..6847a13 --- /dev/null +++ b/format/s3m/voice/sampler.go @@ -0,0 +1,69 @@ +package voice + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/voice/component" +) + +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 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.config.VoiceFilter != nil { + wet = v.config.VoiceFilter.Filter(wet) + } + return wet +} + +func (v *s3mVoice) GetSampler(samplerRate float32, renderChannel *playerRender.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]) (sampling.Sampler, error) { + o := component.OutputFilter{ + Input: v, + Output: renderChannel, + } + + pos, err := v.GetPos() + if err != nil { + return nil, err + } + + p := v.GetFinalPeriod() + + samplerAdd := float32(v.ms.PeriodConverter.GetSamplerAdd(p, float64(v.config.SampleRate)*float64(samplerRate))) + s := sampling.NewSampler(&o, pos, samplerAdd) + + return s, nil +} 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..1c4d965 --- /dev/null +++ b/format/s3m/voice/voice.go @@ -0,0 +1,164 @@ +package voice + +import ( + s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mVolume "github.com/gotracker/playback/format/s3m/volume" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" +) + +type Period interface { + period.Period +} + +type s3mVoice struct { + ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + config voice.InstrumentConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + + component.KeyModulator + + voicer component.Voicer[period.Amiga, s3mVolume.Volume] + component.AmpModulator[s3mVolume.FineVolume, s3mVolume.Volume] + component.FreqModulator[period.Amiga] + component.PanModulator[s3mPanning.Panning] + vol0Opt component.Vol0Optimization +} + +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(ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + v := &s3mVoice{ + ms: ms, + } + + 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: s3mVolume.MaxFineVolume, + DefaultVolume: s3mVolume.MaxVolume, + }) + + v.PanModulator.Setup(component.PanModulatorSettings[s3mPanning.Panning]{ + InitialPan: s3mPanning.DefaultPanning, + }) + + return v +} + +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) Setup(config voice.InstrumentConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) { + v.config = config + + v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{}) + v.vol0Opt.Setup(config.Vol0Optimization) + v.KeyModulator.Release() + v.Reset() +} + +func (v *s3mVoice) Reset() { + v.vol0Opt.Reset() +} + +func (v *s3mVoice) Stop() { + v.voicer = nil +} + +func (v *s3mVoice) IsDone() bool { + if v.voicer == nil { + return true + } + + return v.vol0Opt.IsDone() +} + +func (v *s3mVoice) Advance() { + // has to be after the mod/env updates + v.KeyModulator.DeferredUpdate() + + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + v.KeyModulator.Advance() +} + +func (v *s3mVoice) Clone() voice.Voice { + vv := s3mVoice{ + ms: v.ms, + config: v.config, + AmpModulator: v.AmpModulator.Clone(), + FreqModulator: v.FreqModulator.Clone(), + PanModulator: v.PanModulator.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.config.VoiceFilter != nil { + vv.config.VoiceFilter = v.config.VoiceFilter.Clone() + } + + return &vv +} + +func (v *s3mVoice) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, defVol s3mVolume.Volume) { + var s component.Sampler[period.Amiga, s3mVolume.Volume] + s.Setup(component.SamplerSettings[period.Amiga, s3mVolume.Volume]{ + Sample: samp, + DefaultVolume: defVol, + WholeLoop: wholeLoop, + SustainLoop: sustainLoop, + }) + v.voicer = &s +} diff --git a/format/s3m/volume/finevolume.go b/format/s3m/volume/finevolume.go new file mode 100644 index 0000000..6bed4eb --- /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(0x80) + +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 > 0x7f && 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..5e77c79 100644 --- a/format/s3m/volume/volume.go +++ b/format/s3m/volume/volume.go @@ -1,20 +1,67 @@ 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 ( // DefaultVolume is the default volume value for most everything in S3M format - DefaultVolume = VolumeFromS3M(s3mfile.DefaultVolume) + DefaultVolume = VolumeFromS3M(Volume(s3mfile.DefaultVolume)) +) + +type Volume s3mfile.Volume + +var ( + _ types.VolumeMaxer[Volume] = Volume(0) + _ types.VolumeDeltaer[Volume] = Volume(0) ) +const volCoeff = volume.Volume(1) / volume.Volume(64) + +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 > 64 && 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 @@ -27,12 +74,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 * 64.0) } } diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index d38705b..c1897f8 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -10,10 +10,14 @@ import ( "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/index" "github.com/gotracker/playback/instrument" "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/player/op" "github.com/gotracker/playback/song" ) @@ -35,7 +39,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 @@ -45,22 +49,22 @@ 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 { +func (d Data[TPeriod]) GetInstrument(stmem note.Semitone) instrument.ID { st := stmem if d.HasNote() { n := d.GetNote() @@ -75,7 +79,7 @@ func (d Data) GetInstrument(stmem note.Semitone) instrument.ID { } // 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 } @@ -84,12 +88,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 } @@ -102,27 +110,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 (d Data) GetEffects(mem *Memory, periodType period.Period) []playback.Effect { - switch periodType.(type) { - case period.Linear: - if e := EffectFactory[period.Linear](mem, d); e != nil { - return []playback.Effect{e} - } - case period.Amiga: - if e := EffectFactory[period.Amiga](mem, d); e != nil { - return []playback.Effect{e} - } - default: - panic("unhandled period type") +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) getNoteString(n note.Note) string { +func (Data[TPeriod]) getNoteString(n note.Note) string { switch note.Type(n) { case note.SpecialTypeRelease: return "== " @@ -133,7 +132,7 @@ func (Data) getNoteString(n note.Note) string { } } -func (d Data) String() string { +func (d Data[TPeriod]) String() string { pieces := []string{ "...", // note " ", // inst @@ -156,14 +155,29 @@ 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 GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], d Data, s song.Data, cs playback.Channel[TPeriod, Memory, Data]) error { +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 +} + +func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod, xmVolume.XmVolume, xmPanning.Panning], d Data[TPeriod], s song.Data, cs playback.Channel[TPeriod, *Memory, Data[TPeriod], xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { var n note.Note = note.EmptyNote{} inst := cs.GetActiveState().Instrument prevInst := inst @@ -198,7 +212,7 @@ func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], out.TargetPos.Set(sampling.Pos{}) if inst != nil { if wantRetriggerVol { - out.TargetVolume.Set(inst.GetDefaultVolume()) + out.TargetVolume.Set(xmVolume.ToVolumeXM(inst.GetDefaultVolumeGeneric())) } out.NoteAction.Set(note.ActionRetrigger) out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) @@ -223,9 +237,9 @@ func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod], if d.HasVolume() { v := d.GetVolume() - if v == volume.VolumeUseInstVol { + if v.IsUseInstrumentVol() { if inst != nil { - v = inst.GetDefaultVolume() + v = xmVolume.ToVolumeXM(inst.GetDefaultVolumeGeneric()) } } out.TargetVolume.Set(v) diff --git a/format/xm/channel/effect_arpeggio.go b/format/xm/channel/effect_arpeggio.go index e7637f7..4c7451b 100644 --- a/format/xm/channel/effect_arpeggio.go +++ b/format/xm/channel/effect_arpeggio.go @@ -3,33 +3,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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Arpeggio defines an arpeggio effect type Arpeggio[TPeriod period.Period] DataEffect // '0' -// Start triggers on the first tick, but before the Tick() function is called -func (e Arpeggio[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - cs.GetActiveState().Pos = cs.GetTargetState().Pos - return nil +func (e Arpeggio[TPeriod]) String() string { + return fmt.Sprintf("0%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Arpeggio[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { +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 := int8(xy >> 4) - y := int8(xy & 0x0f) - return doArpeggio(cs, currentTick, x, y) + x, y := int8(xy>>4), int8(xy&0x0f) + return doArpeggio[TPeriod](ch, m, tick, x, y) } -func (e Arpeggio[TPeriod]) String() string { - return fmt.Sprintf("0%0.2x", DataEffect(e)) +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 index 238bf68..a6d1d2f 100644 --- a/format/xm/channel/effect_extrafineportadown.go +++ b/format/xm/channel/effect_extrafineportadown.go @@ -3,25 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e ExtraFinePortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - xx := mem.ExtraFinePortaDown(DataEffect(e)) - y := xx & 0x0F +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 - return doPortaDown(cs, float32(y), 1) + if tick != 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(y)*1) } -func (e ExtraFinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index c507a48..99cab71 100644 --- a/format/xm/channel/effect_extrafineportaup.go +++ b/format/xm/channel/effect_extrafineportaup.go @@ -3,25 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e ExtraFinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e ExtraFinePortaUp[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - xx := mem.ExtraFinePortaUp(DataEffect(e)) - y := xx & 0x0F +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 - return doPortaUp(cs, float32(y), 1) + if tick != 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(y)*1) } -func (e ExtraFinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(e)) +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 index 90600b0..8b9f3c5 100644 --- a/format/xm/channel/effect_fineportadown.go +++ b/format/xm/channel/effect_fineportadown.go @@ -3,25 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e FinePortaDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - xy := mem.FinePortaDown(DataEffect(e)) - y := xy & 0x0F +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 - return doPortaDown(cs, float32(y), 4) + if tick != 0 { + return nil + } + + return m.DoChannelPortaDown(ch, period.Delta(y)*4) } -func (e FinePortaDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index a1cc6c2..b1e380c 100644 --- a/format/xm/channel/effect_fineportaup.go +++ b/format/xm/channel/effect_fineportaup.go @@ -3,25 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FinePortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() +func (e FinePortaUp[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - xy := mem.FinePortaUp(DataEffect(e)) - y := xy & 0x0F +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 - return doPortaUp(cs, float32(y), 4) + if tick != 0 { + return nil + } + + return m.DoChannelPortaUp(ch, period.Delta(y)*4) } -func (e FinePortaUp[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 73fb653..d354a60 100644 --- a/format/xm/channel/effect_finevolslidedown.go +++ b/format/xm/channel/effect_finevolslidedown.go @@ -3,24 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e FineVolumeSlideDown[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - xy := mem.FineVolumeSlideDown(DataEffect(e)) - y := DataEffect(xy & 0x0F) +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 - return doVolSlide(cs, -float32(y), 1.0) + if tick != 0 { + return nil + } + + return m.SlideChannelVolume(ch, 1.0, -float32(y)) } -func (e FineVolumeSlideDown[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 424f8b1..a802503 100644 --- a/format/xm/channel/effect_finevolslideup.go +++ b/format/xm/channel/effect_finevolslideup.go @@ -3,24 +3,34 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e FineVolumeSlideUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e FineVolumeSlideUp[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - xy := mem.FineVolumeSlideUp(DataEffect(e)) - y := DataEffect(xy & 0x0F) +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 - return doVolSlide(cs, float32(y), 1.0) + if tick != 0 { + return nil + } + + return m.SlideChannelVolume(ch, 1.0, float32(y)) } -func (e FineVolumeSlideUp[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 9f71c6f..3c4125e 100644 --- a/format/xm/channel/effect_globalvolumeslide.go +++ b/format/xm/channel/effect_globalvolumeslide.go @@ -3,40 +3,40 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e GlobalVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e GlobalVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("H%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e GlobalVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick == 0 { + if tick == 0 { return nil } - m := p.(XM) - if x == 0 { - // global vol slide down - return doGlobalVolSlide(m, -float32(y), 1.0) + return m.SlideGlobalVolume(1, -float32(y)) } else if y == 0 { - // global vol slide up - return doGlobalVolSlide(m, float32(y), 1.0) + return m.SlideGlobalVolume(1, float32(y)) } return nil } -func (e GlobalVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("H%0.2x", DataEffect(e)) +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 index 23435ec..a1c86f6 100644 --- a/format/xm/channel/effect_notecut.go +++ b/format/xm/channel/effect_notecut.go @@ -3,29 +3,26 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteCut[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e NoteCut[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e NoteCut[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - x := DataEffect(e) & 0xf - - if x != 0 && currentTick == int(x) { - cs.GetActiveState().NoteCut() - } - return nil +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]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 527db11..19d8fa7 100644 --- a/format/xm/channel/effect_notedelay.go +++ b/format/xm/channel/effect_notedelay.go @@ -3,26 +3,26 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e NoteDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.SetNotePlayTick(true, note.ActionRetrigger, int(DataEffect(e)&0x0F)) - return nil +func (e NoteDelay[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e NoteDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 3f29cf4..9a622c0 100644 --- a/format/xm/channel/effect_orderjump.go +++ b/format/xm/channel/effect_orderjump.go @@ -3,25 +3,24 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e OrderJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e OrderJump[TPeriod]) String() string { + return fmt.Sprintf("B%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e OrderJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { - return p.SetNextOrder(index.Order(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]) String() string { - return fmt.Sprintf("B%0.2x", DataEffect(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 index 694c890..34e08c8 100644 --- a/format/xm/channel/effect_panslide.go +++ b/format/xm/channel/effect_panslide.go @@ -3,41 +3,38 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PanSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { +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 := xx >> 4 - y := xx & 0x0F + x, y := xx>>4, xx&0x0F - active := cs.GetActiveState() - xp := DataEffect(xmPanning.PanningToXm(active.Pan)) if x == 0 { // slide left y units - if xp < y { - xp = 0 - } else { - xp -= y + if err := m.SlideChannelPan(ch, 1, -float32(y)); err != nil { + return err } } else if y == 0 { // slide right x units - if xp > 0xFF-x { - xp = 0xFF - } else { - xp += x + if err := m.SlideChannelPan(ch, 1, float32(x)); err != nil { + return err } } - active.Pan = xmPanning.PanningFromXm(uint8(xp)) return nil } -func (e PanSlide[TPeriod]) String() string { - return fmt.Sprintf("P%0.2x", DataEffect(e)) +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 index 3953504..0dec3c3 100644 --- a/format/xm/channel/effect_patterndelay.go +++ b/format/xm/channel/effect_patterndelay.go @@ -3,25 +3,25 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e PatternDelay[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - m := p.(XM) - return m.SetPatternDelay(int(DataEffect(e) & 0x0F)) +func (e PatternDelay[TPeriod]) String() string { + return fmt.Sprintf("S%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternDelay[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("S%0.2x", DataEffect(e)) +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 index c272497..bdff5f4 100644 --- a/format/xm/channel/effect_patternloop.go +++ b/format/xm/channel/effect_patternloop.go @@ -3,38 +3,32 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PatternLoop[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e PatternLoop[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - x := DataEffect(e) & 0xF +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 - mem := cs.GetMemory() - pl := mem.GetPatternLoop() if x == 0 { - // set loop - pl.Start = p.GetCurrentRow() + // set loop start + return m.SetPatternLoopStart(ch) } 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) - } + // set loop end + count + return m.SetPatternLoops(ch, int(x)) } - return nil } -func (e PatternLoop[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 974fc75..6e0e58b 100644 --- a/format/xm/channel/effect_portadown.go +++ b/format/xm/channel/effect_portadown.go @@ -3,32 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaDown[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e PortaDown[TPeriod]) String() string { + return fmt.Sprintf("2%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaDown[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick == 0 { + if tick == 0 { return nil } - return doPortaDown(cs, float32(xx), 4) + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) } -func (e PortaDown[TPeriod]) String() string { - return fmt.Sprintf("2%0.2x", DataEffect(e)) +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 index 31005c1..c840190 100644 --- a/format/xm/channel/effect_portatonote.go +++ b/format/xm/channel/effect_portatonote.go @@ -3,44 +3,38 @@ package channel import ( "fmt" - "github.com/gotracker/playback" - "github.com/gotracker/playback/note" + 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/heucuva/comparison" + "github.com/gotracker/playback/player/machine" ) // PortaToNote defines a portamento-to-note effect type PortaToNote[TPeriod period.Period] DataEffect // '3' -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaToNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - if cmd := cs.GetChannelData(); cmd.HasNote() { - cs.SetPortaTargetPeriod(cs.GetTargetState().Period) - cs.SetNotePlayTick(false, note.ActionContinue, 0) - } - return nil +func (e PortaToNote[TPeriod]) String() string { + return fmt.Sprintf("3%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaToNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - if currentTick == 0 { +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 := cs.GetMemory() - xx := mem.PortaToNote(DataEffect(e)) - - current := cs.GetActiveState().Period - target := cs.GetPortaTargetPeriod() - if period.ComparePeriods(current, target) == comparison.SpaceshipRightGreater { - return doPortaUpToNote(cs, float32(xx), 4, target) // subtracts - } else { - return doPortaDownToNote(cs, float32(xx), 4, target) // adds + 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]) String() string { - return fmt.Sprintf("3%0.2x", DataEffect(e)) +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 index 5e35063..ad88406 100644 --- a/format/xm/channel/effect_portaup.go +++ b/format/xm/channel/effect_portaup.go @@ -3,32 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e PortaUp[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e PortaUp[TPeriod]) String() string { + return fmt.Sprintf("1%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e PortaUp[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick == 0 { + if tick == 0 { return nil } - return doPortaUp(cs, float32(xx), 4) + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) } -func (e PortaUp[TPeriod]) String() string { - return fmt.Sprintf("1%0.2x", DataEffect(e)) +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 index 576031f..2cae5f8 100644 --- a/format/xm/channel/effect_portavolslide.go +++ b/format/xm/channel/effect_portavolslide.go @@ -4,12 +4,14 @@ 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, Memory, Data] + playback.CombinedEffect[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning, *Memory, Data[TPeriod]] } // NewPortaVolumeSlide creates a new PortaVolumeSlide object @@ -20,5 +22,9 @@ func NewPortaVolumeSlide[TPeriod period.Period](val DataEffect) PortaVolumeSlide } func (e PortaVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("5%0.2x", DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) + 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 index 7fbf082..129ee79 100644 --- a/format/xm/channel/effect_retriggernote.go +++ b/format/xm/channel/effect_retriggernote.go @@ -3,37 +3,26 @@ package channel import ( "fmt" - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e RetriggerNote[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RetriggerNote[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e RetriggerNote[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { +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 - if y == 0 { - return nil - } - - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if DataEffect(rt) >= y { - cs.GetActiveState().Pos = sampling.Pos{} - cs.ResetRetriggerCount() - } - return nil + return m.SetChannelNoteAction(ch, note.ActionRetrigger, int(y)) } -func (e RetriggerNote[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 11bf974..ebf85fb 100644 --- a/format/xm/channel/effect_retrigvolslide.go +++ b/format/xm/channel/effect_retrigvolslide.go @@ -3,69 +3,71 @@ package channel import ( "fmt" - "github.com/gotracker/gomixing/sampling" - - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e RetrigVolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RetrigVolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("R%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e RetrigVolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - x := DataEffect(e) >> 4 - y := DataEffect(e) & 0x0F - if y == 0 { +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 } - rt := cs.GetRetriggerCount() + 1 - cs.SetRetriggerCount(rt) - if DataEffect(rt) >= x { - cs.GetActiveState().Pos = 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) - } + 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]) String() string { - return fmt.Sprintf("R%0.2x", DataEffect(e)) +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 index d7d57e2..da086b6 100644 --- a/format/xm/channel/effect_rowjump.go +++ b/format/xm/channel/effect_rowjump.go @@ -3,32 +3,28 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e RowJump[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e RowJump[TPeriod]) String() string { + return fmt.Sprintf("D%0.2x", DataEffect(e)) } -// Stop is called on the last tick of the row, but after the Tick() function is called -func (e RowJump[TPeriod]) Stop(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, lastTick int) error { +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 := xy >> 4 - y := xy & 0x0f + x, y := xy>>4, xy&0x0f row := index.Row(x*10 + y) - if err := p.BreakOrder(); err != nil { - return err - } - return p.SetNextRow(row) + + return m.SetRow(row, true) } -func (e RowJump[TPeriod]) String() string { - return fmt.Sprintf("D%0.2x", DataEffect(e)) +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 index feb46c1..5b9f2bd 100644 --- a/format/xm/channel/effect_sampleoffset.go +++ b/format/xm/channel/effect_sampleoffset.go @@ -5,22 +5,30 @@ import ( "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/playback" + 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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SampleOffset[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - mem := cs.GetMemory() +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)) - cs.GetTargetState().Pos = sampling.Pos{Pos: int(xx) * 0x100} - return nil + return m.SetChannelPos(ch, sampling.Pos{Pos: int(xx) * 0x100}) } -func (e SampleOffset[TPeriod]) String() string { - return fmt.Sprintf("9%0.2x", DataEffect(e)) +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 index 573f0d3..fbecf34 100644 --- a/format/xm/channel/effect_setcoarsepanposition.go +++ b/format/xm/channel/effect_setcoarsepanposition.go @@ -3,25 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetCoarsePanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - xy := DataEffect(e) - y := xy & 0x0F +func (e SetCoarsePanPosition[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - cs.GetActiveState().Pan = xmPanning.PanningFromXm(uint8(y) << 4) - return nil +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]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 7fc46fb..c1f811a 100644 --- a/format/xm/channel/effect_setenvelopeposition.go +++ b/format/xm/channel/effect_setenvelopeposition.go @@ -3,23 +3,28 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetEnvelopePosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e SetEnvelopePosition[TPeriod]) String() string { + return fmt.Sprintf("L%0.2x", DataEffect(e)) +} - xx := 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 + } - cs.SetEnvelopePosition(int(xx)) - return nil + return m.SetChannelEnvelopePositions(ch, int(e)) } -func (e SetEnvelopePosition[TPeriod]) String() string { - return fmt.Sprintf("L%0.2x", DataEffect(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 index 98cbe5c..dc94044 100644 --- a/format/xm/channel/effect_setfinetune.go +++ b/format/xm/channel/effect_setfinetune.go @@ -3,32 +3,34 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetFinetune[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - x := DataEffect(e) & 0xf +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 + } - inst := cs.GetTargetState().Instrument if inst != nil { - ft := (note.Finetune(x) - 8) * 4 + ft := (note.Finetune(e&0x0F) - 8) * 4 inst.SetFinetune(ft) } return nil } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetFinetune[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - -func (e SetFinetune[TPeriod]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 2564ec6..c3effba 100644 --- a/format/xm/channel/effect_setglobalvolume.go +++ b/format/xm/channel/effect_setglobalvolume.go @@ -3,27 +3,25 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetGlobalVolume[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - v := xmVolume.XmVolume(e) - p.SetGlobalVolume(v.Volume()) - return nil +func (e SetGlobalVolume[TPeriod]) String() string { + return fmt.Sprintf("G%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetGlobalVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("G%0.2x", DataEffect(e)) +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 index 957fb7d..d6e7802 100644 --- a/format/xm/channel/effect_setpanposition.go +++ b/format/xm/channel/effect_setpanposition.go @@ -3,24 +3,29 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetPanPosition[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +func (e SetPanPosition[TPeriod]) String() string { + return fmt.Sprintf("8%0.2x", DataEffect(e)) +} - xx := uint8(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 + } - cs.GetActiveState().Pan = xmPanning.PanningFromXm(xx) - return nil + xx := uint8(e) + return m.SetChannelPan(ch, xmPanning.Panning(xx)) } -func (e SetPanPosition[TPeriod]) String() string { - return fmt.Sprintf("8%0.2x", DataEffect(e)) +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 index 5d2b489..f679b9b 100644 --- a/format/xm/channel/effect_setspeed.go +++ b/format/xm/channel/effect_setspeed.go @@ -3,30 +3,27 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetSpeed[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - if e != 0 { - m := p.(XM) - if err := m.SetTicks(int(e)); err != nil { - return err - } - } - return nil +func (e SetSpeed[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) } -// Start triggers on the first tick, but before the Tick() function is called -func (e SetSpeed[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +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]) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(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 index e5eb2b3..556aad4 100644 --- a/format/xm/channel/effect_settempo.go +++ b/format/xm/channel/effect_settempo.go @@ -3,36 +3,27 @@ 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/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' -// PreStart triggers when the effect enters onto the channel state -func (e SetTempo[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - if e > 0x20 { - m := p.(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[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e SetTempo[TPeriod]) String() string { + return fmt.Sprintf("F%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e SetTempo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - m := p.(XM) - return m.SetTempo(int(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]) String() string { - return fmt.Sprintf("F%0.2x", DataEffect(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 index aa8e6a8..60c9d5d 100644 --- a/format/xm/channel/effect_settremolowaveform.go +++ b/format/xm/channel/effect_settremolowaveform.go @@ -3,27 +3,25 @@ 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" - - "github.com/gotracker/playback" ) // SetTremoloWaveform defines a set tremolo waveform effect type SetTremoloWaveform[TPeriod period.Period] DataEffect // 'E7x' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetTremoloWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetTremoloWaveform[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - trem := mem.TremoloOscillator() - trem.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 7429d69..5bb0ddc 100644 --- a/format/xm/channel/effect_setvibratowaveform.go +++ b/format/xm/channel/effect_setvibratowaveform.go @@ -3,27 +3,25 @@ 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" - - "github.com/gotracker/playback" ) // SetVibratoWaveform defines a set vibrato waveform effect type SetVibratoWaveform[TPeriod period.Period] DataEffect // 'E4x' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVibratoWaveform[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - - x := DataEffect(e) & 0xf +func (e SetVibratoWaveform[TPeriod]) String() string { + return fmt.Sprintf("E%0.2x", DataEffect(e)) +} - mem := cs.GetMemory() - vib := mem.VibratoOscillator() - vib.SetWaveform(oscillator.WaveTableSelect(x)) - return nil +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]) String() string { - return fmt.Sprintf("E%0.2x", DataEffect(e)) +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 index 02ef1e2..e36fcab 100644 --- a/format/xm/channel/effect_setvolume.go +++ b/format/xm/channel/effect_setvolume.go @@ -3,25 +3,25 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e SetVolume[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() +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) - - active := cs.GetActiveState() - active.SetVolume(xx.Volume()) - return nil + return m.SetChannelVolume(ch, xx) } -func (e SetVolume[TPeriod]) String() string { - return fmt.Sprintf("C%0.2x", DataEffect(e)) +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 index ae1985f..fe1ee92 100644 --- a/format/xm/channel/effect_tremolo.go +++ b/format/xm/channel/effect_tremolo.go @@ -3,28 +3,35 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremolo[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e Tremolo[TPeriod]) String() string { + return fmt.Sprintf("7%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Tremolo[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 doTremolo(cs, currentTick, x, y, 4) + 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]) String() string { - return fmt.Sprintf("7%0.2x", DataEffect(e)) +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 index 8e76075..c8478dc 100644 --- a/format/xm/channel/effect_tremor.go +++ b/format/xm/channel/effect_tremor.go @@ -3,29 +3,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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Tremor defines a tremor effect type Tremor[TPeriod period.Period] DataEffect // 'T' -// Start triggers on the first tick, but before the Tick() function is called -func (e Tremor[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e Tremor[TPeriod]) String() string { + return fmt.Sprintf("T%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Tremor[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - if currentTick != 0 { - mem := cs.GetMemory() - x, y := mem.Tremor(DataEffect(e)) - return doTremor(cs, currentTick, int(x)+1, int(y)+1) +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 } - return nil + + x, y := mem.Tremor(DataEffect(e)) + return doTremor(ch, m, int(x)+1, int(y)+1) } -func (e Tremor[TPeriod]) String() string { - return fmt.Sprintf("T%0.2x", DataEffect(e)) +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 index 85f91a0..918a96c 100644 --- a/format/xm/channel/effect_vibrato.go +++ b/format/xm/channel/effect_vibrato.go @@ -3,29 +3,35 @@ 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // Vibrato defines a vibrato effect type Vibrato[TPeriod period.Period] DataEffect // '4' -// Start triggers on the first tick, but before the Tick() function is called -func (e Vibrato[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil +func (e Vibrato[TPeriod]) String() string { + return fmt.Sprintf("4%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e Vibrato[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 doVibrato(cs, currentTick, x, y, 4) + 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]) String() string { - return fmt.Sprintf("4%0.2x", DataEffect(e)) +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 index 7b52194..24afcf5 100644 --- a/format/xm/channel/effect_vibratovolslide.go +++ b/format/xm/channel/effect_vibratovolslide.go @@ -4,12 +4,14 @@ 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, Memory, Data] + playback.CombinedEffect[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning, *Memory, Data[TPeriod]] } // NewVibratoVolumeSlide creates a new VibratoVolumeSlide object @@ -20,5 +22,9 @@ func NewVibratoVolumeSlide[TPeriod period.Period](val DataEffect) VibratoVolumeS } func (e VibratoVolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("6%0.2x", DataEffect(e.Effects[0].(VolumeSlide[TPeriod]))) + 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 index 732a96d..23fc81d 100644 --- a/format/xm/channel/effect_volslide.go +++ b/format/xm/channel/effect_volslide.go @@ -3,38 +3,43 @@ 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/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' -// Start triggers on the first tick, but before the Tick() function is called -func (e VolumeSlide[TPeriod]) Start(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback) error { - cs.ResetRetriggerCount() - return nil +func (e VolumeSlide[TPeriod]) String() string { + return fmt.Sprintf("A%0.2x", DataEffect(e)) } -// Tick is called on every tick -func (e VolumeSlide[TPeriod]) Tick(cs playback.Channel[TPeriod, Memory, Data], p playback.Playback, currentTick int) error { - mem := cs.GetMemory() +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 currentTick == 0 { + if tick == 0 { return nil } if x == 0 { // vol slide down - return doVolSlide(cs, -float32(y), 1.0) + return m.SlideChannelVolume(ch, 1, -float32(y)) } else if y == 0 { // vol slide up - return doVolSlide(cs, float32(y), 1.0) + return m.SlideChannelVolume(ch, 1, float32(x)) } + return nil } -func (e VolumeSlide[TPeriod]) String() string { - return fmt.Sprintf("A%0.2x", DataEffect(e)) +func (e VolumeSlide[TPeriod]) TraceData() string { + return e.String() } diff --git a/format/xm/channel/effectfactory.go b/format/xm/channel/effectfactory.go index 8f9d12d..e8854d7 100644 --- a/format/xm/channel/effectfactory.go +++ b/format/xm/channel/effectfactory.go @@ -4,6 +4,8 @@ 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" ) @@ -12,7 +14,7 @@ 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, Memory, Data] + playback.CombinedEffect[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning, *Memory, Data[TPeriod]] eff EffectXM } @@ -31,13 +33,17 @@ func (e VolEff[TPeriod]) Names() []string { 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) EffectXM { +func EffectFactory[TPeriod period.Period](mem *Memory, data song.ChannelData[xmVolume.XmVolume]) EffectXM { if data == nil { return nil } - d, _ := data.(Data) + d, _ := data.(Data[TPeriod]) if !d.HasCommand() { return nil } diff --git a/format/xm/channel/effectfactory_standard.go b/format/xm/channel/effectfactory_standard.go index 9821ac7..4c419cc 100644 --- a/format/xm/channel/effectfactory_standard.go +++ b/format/xm/channel/effectfactory_standard.go @@ -4,7 +4,7 @@ import ( "github.com/gotracker/playback/period" ) -func standardEffectFactory[TPeriod period.Period](mem *Memory, cd Data) EffectXM { +func standardEffectFactory[TPeriod period.Period](mem *Memory, cd Data[TPeriod]) EffectXM { if !cd.What.HasEffect() && !cd.What.HasEffectParameter() { return nil } 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/unhandled.go b/format/xm/channel/unhandled.go index e60ddc8..35c0fe2 100644 --- a/format/xm/channel/unhandled.go +++ b/format/xm/channel/unhandled.go @@ -3,9 +3,11 @@ 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine" ) // UnhandledCommand is an unhandled command @@ -14,14 +16,6 @@ type UnhandledCommand[TPeriod period.Period] struct { Info DataEffect } -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m XM) error { - if !m.IgnoreUnknownEffect() { - panic("unhandled command") - } - return nil -} - func (e UnhandledCommand[TPeriod]) String() string { return fmt.Sprintf("%c%0.2x", e.Command.ToRune(), e.Info) } @@ -32,19 +26,24 @@ func (e UnhandledCommand[TPeriod]) Names() []string { } } -// UnhandledVolCommand is an unhandled volume command -type UnhandledVolCommand[TPeriod period.Period] struct { - Vol xmVolume.VolEffect -} - -// PreStart triggers when the effect enters onto the channel state -func (e UnhandledVolCommand[TPeriod]) PreStart(cs playback.Channel[TPeriod, Memory, Data], m XM) error { +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) } @@ -54,3 +53,14 @@ func (e UnhandledVolCommand[TPeriod]) Names() []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 index 3cecead..449b10b 100644 --- a/format/xm/channel/util.go +++ b/format/xm/channel/util.go @@ -1,151 +1,9 @@ package channel import ( - "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/oscillator" - xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/note" - "github.com/heucuva/comparison" ) -// 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 -} - -func doVolSlide[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], delta float32, multiplier float32) error { - active := cs.GetActiveState() - av := active.GetVolume() - v := xmVolume.ToVolumeXM(av) - vol := int16((float32(v) + delta) * multiplier) - if vol >= 0x40 { - vol = 0x40 - } - if vol < 0x00 { - vol = 0x00 - } - v = xmVolume.XmVolume(DataEffect(vol)) - active.SetVolume(v.Volume()) - return nil -} - -func doGlobalVolSlide(m 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(DataEffect(vol)) - m.SetGlobalVolume(v.Volume()) - return nil -} - -func doPortaUp[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = period.PortaUp(cur, delta) - active.Period = cur - return nil -} - -func doPortaUpToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { - if err := doPortaUp(cs, amount, multiplier); err != nil { - return err - } - - if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipLeftGreater { - active.Period = target - } - return nil -} - -func doPortaDown[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = period.PortaDown(cur, delta) - active.Period = cur - return nil -} - -func doPortaDownToNote[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], amount float32, multiplier float32, target TPeriod) error { - if err := doPortaDown(cs, amount, multiplier); err != nil { - return err - } - if active := cs.GetActiveState(); period.ComparePeriods(active.Period, target) == comparison.SpaceshipRightGreater { - active.Period = target - } - return nil -} - -func doVibrato[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { - mem := cs.GetMemory() - vib := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.VibratoOscillator()) - delta := period.Delta(vib) - cs.SetPeriodDelta(delta) - return nil -} - -func doTremor[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, 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[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, 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.GetTargetState().Pos = cs.GetActiveState().Pos - return nil -} - var ( volSlideTwoThirdsTable = [...]xmVolume.XmVolume{ 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9, @@ -154,31 +12,3 @@ var ( 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 36, 37, 38, 38, 39, } ) - -func doVolSlideTwoThirds[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data]) error { - active := cs.GetActiveState() - vol := xmVolume.ToVolumeXM(active.GetVolume()) - if vol >= 64 { - vol = 63 - } - - v := volSlideTwoThirdsTable[vol] - if v >= 0x40 { - v = 0x40 - } - - active.SetVolume(v.Volume()) - return nil -} - -func doTremolo[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return doVolSlide(cs, delta, 1.0) -} - -func calculateWaveTable[TPeriod period.Period](cs playback.Channel[TPeriod, Memory, Data], currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { - delta := o.GetWave(float32(depth) * multiplier) - o.Advance(int(speed)) - return delta -} diff --git a/format/xm/layout/channelsetting.go b/format/xm/layout/channelsetting.go index c0f8f96..f563cf4 100644 --- a/format/xm/layout/channelsetting.go +++ b/format/xm/layout/channelsetting.go @@ -1,16 +1,55 @@ package layout import ( - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" "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/song" ) // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled 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) GetEnabled() bool { + return c.Enabled +} + +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) GetPanEnabled() bool { + return true +} + +func (c ChannelSetting) GetDefaultFilterName() string { + return "" +} + +func (c ChannelSetting) IsDefaultFilterEnabled() bool { + return false +} 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/song.go b/format/xm/layout/song.go index 22e9d8d..673a9dd 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -1,54 +1,158 @@ package layout import ( + "reflect" + "time" + + "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/format/xm/channel" + xmPanning "github.com/gotracker/playback/format/xm/panning" "github.com/gotracker/playback/format/xm/pattern" + xmPeriod "github.com/gotracker/playback/format/xm/period" + 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/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/system" ) -// Song is the full definition of the song data of an Song file -type Song struct { +// Song is the full definition of the song data of an XM file +type Song[TPeriod period.Period] struct { + System system.System Head Header - Instruments map[uint8]*instrument.Instrument - InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument - Patterns []pattern.Pattern + Instruments map[uint8]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + Patterns []pattern.Pattern[TPeriod] ChannelSettings []ChannelSetting OrderList []index.Pattern } +func (s Song[TPeriod]) GetPeriodType() reflect.Type { + if s.Head.LinearFreqSlides { + var p period.Linear + return reflect.TypeOf(p) + } else { + var p period.Amiga + return reflect.TypeOf(p) + } +} + +func (Song[TPeriod]) GetGlobalVolumeType() reflect.Type { + var gv xmVolume.XmVolume + return reflect.TypeOf(gv) +} + +func (Song[TPeriod]) GetChannelMixingVolumeType() reflect.Type { + var cmv xmVolume.XmVolume + return reflect.TypeOf(cmv) +} + +func (Song[TPeriod]) GetChannelVolumeType() reflect.Type { + var cv xmVolume.XmVolume + return reflect.TypeOf(cv) +} + +func (Song[TPeriod]) GetChannelPanningType() reflect.Type { + var cp xmPanning.Panning + return reflect.TypeOf(cp) +} + // GetOrderList returns the list of all pattern orders for the song -func (s Song) GetOrderList() []index.Pattern { +func (s Song[TPeriod]) GetOrderList() []index.Pattern { return s.OrderList } -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) *song.Pattern[channel.Data] { +// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song +func (s Song[TPeriod]) GetInitialBPM() int { + return s.Head.InitialTempo +} + +// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song +func (s Song[TPeriod]) GetInitialTempo() int { + return s.Head.InitialSpeed +} + +// GetGlobalVolumeGeneric returns the initial global volume for the song +func (s Song[TPeriod]) GetGlobalVolumeGeneric() volume.Volume { + return s.Head.GlobalVolume.ToVolume() +} + +// GetGlobalVolume returns the initial global volume for the song +func (s Song[TPeriod]) GetGlobalVolume() xmVolume.XmVolume { + return s.Head.GlobalVolume +} + +// GetMixingVolumeGeneric returns the initial mixing volume for the song +func (s Song[TPeriod]) GetMixingVolumeGeneric() volume.Volume { + return s.Head.MixingVolume.ToVolume() +} + +// GetMixingVolume returns the initial mixing volume for the song +func (s Song[TPeriod]) GetMixingVolume() xmVolume.XmVolume { + return s.Head.MixingVolume +} + +const durationPerBpm = time.Duration(2500) * time.Millisecond + +// GetTickDuration calculates the duration of a tick at a particular BPM +func (s Song[TPeriod]) 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 Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume], error) { if int(patNum) >= len(s.Patterns) { - return nil + return nil, song.ErrStopSong } - return &s.Patterns[patNum] + return s.Patterns[patNum].Pattern, nil } -// IsChannelEnabled returns true if the channel at index `channelNum` is enabled -func (s Song) IsChannelEnabled(channelNum int) bool { - return s.ChannelSettings[channelNum].Enabled +// GetPatternIntf returns an interface to a specific pattern indexed by `patNum` +func (s Song[TPeriod]) GetPatternIntf(patNum index.Pattern) (song.PatternIntf, error) { + return s.GetPattern(patNum) +} + +// GetPatternIntfByOrder returns the pattern specified by the order index provided +func (s Song[TPeriod]) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, 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.GetPatternIntf(pat) } -// GetRenderChannel returns the output channel for the channel at index `channelNum` -func (s Song) GetRenderChannel(channelNum int) int { - return s.ChannelSettings[channelNum].OutputChannelNum +// GetNumChannels returns the number of channels the song has +func (s Song[TPeriod]) GetNumChannels() int { + return len(s.ChannelSettings) +} + +// GetChannelSettings returns the channel settings at index `channelNum` +func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.ChannelSettings { + return s.ChannelSettings[channelNum] } // NumInstruments returns the number of instruments in the song -func (s Song) NumInstruments() int { +func (s Song[TPeriod]) NumInstruments() int { return len(s.Instruments) } // IsValidInstrumentID returns true if the instrument exists -func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { +func (s Song[TPeriod]) IsValidInstrumentID(instNum instrument.ID) bool { if instNum.IsEmpty() { return false } @@ -61,7 +165,7 @@ func (s Song) IsValidInstrumentID(instNum instrument.ID) bool { } // GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instNum instrument.ID) (*instrument.Instrument, note.Semitone) { +func (s Song[TPeriod]) GetInstrument(instNum instrument.ID) (instrument.InstrumentIntf, note.Semitone) { if instNum.IsEmpty() { return nil, note.UnchangedSemitone } @@ -77,6 +181,29 @@ func (s Song) GetInstrument(instNum instrument.ID) (*instrument.Instrument, note } // GetName returns the name of the song -func (s Song) GetName() string { +func (s Song[TPeriod]) GetName() string { return s.Head.Name } + +func (s Song[TPeriod]) GetPeriodCalculator() song.PeriodCalculatorIntf { + if s.Head.LinearFreqSlides { + return xmPeriod.LinearConverter + } + return xmPeriod.AmigaConverter +} + +func (s Song[TPeriod]) GetInitialOrder() index.Order { + return s.Head.InitialOrder +} + +func (s Song[TPeriod]) GetRowRenderStringer(row song.RowIntf, channels int, longFormat bool) render.RowStringer { + rt := render.NewRowText[channel.Data[TPeriod]](channels, longFormat) + rowData := make(song.Row[channel.Data[TPeriod], xmVolume.XmVolume], channels) + copy(rowData, row.(song.Row[channel.Data[TPeriod], xmVolume.XmVolume])) + rt.Channels = rowData + return rt +} + +func (s Song[TPeriod]) GetSystem() system.System { + return s.System +} diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index 7240011..aadefab 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -6,14 +6,15 @@ import ( "math" xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" + "github.com/heucuva/optional" "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/format/xm/layout" xmPanning "github.com/gotracker/playback/format/xm/panning" "github.com/gotracker/playback/format/xm/pattern" xmPeriod "github.com/gotracker/playback/format/xm/period" + xmSystem "github.com/gotracker/playback/format/xm/system" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" @@ -21,7 +22,8 @@ import ( "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/voice" + "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" @@ -33,11 +35,12 @@ func moduleHeaderToHeader(fh *xmfile.ModuleHeader) (*layout.Header, error) { 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, + Name: fh.GetName(), + InitialSpeed: int(fh.DefaultSpeed), + InitialTempo: int(fh.DefaultTempo), + GlobalVolume: xmVolume.DefaultXmVolume, + MixingVolume: xmVolume.DefaultXmMixingVolume, + LinearFreqSlides: fh.Flags.IsLinearSlides(), } return &head, nil } @@ -59,23 +62,20 @@ func xmAutoVibratoWSToProtrackerWS(vibtype uint8) uint8 { } } -func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument, map[int][]note.Semitone, error) { +func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument[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[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[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + Static: instrument.StaticValues[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ Filename: si.GetName(), Name: inst.GetName(), - Volume: v.Volume(), + Volume: v, RelativeNoteNumber: si.RelativeNoteNumber, - AutoVibrato: voice.AutoVibrato{ + AutoVibrato: autovibrato.AutoVibratoSettings{ Enabled: (inst.VibratoDepth != 0 && inst.VibratoRate != 0), Sweep: int(inst.VibratoSweep), WaveformSelection: xmAutoVibratoWSToProtrackerWS(inst.VibratoType), @@ -123,18 +123,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 +146,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.EnvPoint[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 +154,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 +172,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.EnvPoint[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,21 +184,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 { - if linearFrequencySlides { - sample.SampleRate = xmPeriod.CalcFinetuneC4SampleRate[period.Linear](xmPeriod.LinearConverter, xmPeriod.DefaultC4SampleRate, note.Finetune(si.Finetune)) - } else { - sample.SampleRate = xmPeriod.CalcFinetuneC4SampleRate[period.Amiga](xmPeriod.AmigaConverter, xmPeriod.DefaultC4SampleRate, note.Finetune(si.Finetune)) - } - } - if sample.SampleRate == 0 { - sample.SampleRate = period.Frequency(xmPeriod.DefaultC4SampleRate) - } + n := note.Semitone(xmSystem.C4Note + si.RelativeNoteNumber) + sample.SampleRate = xmPeriod.CalcFinetuneC4SampleRate(xmSystem.DefaultC4SampleRate, n, note.Finetune(si.Finetune)) if si.Flags.IsStereo() { numChannels = 2 } @@ -247,7 +246,7 @@ 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(ih *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], map[int][]note.Semitone, error) { if ih == nil { return nil, nil, errors.New("instrument is nil") } @@ -255,16 +254,16 @@ func convertXMInstrumentToInstrument(ih *xmfile.InstrumentHeader, linearFrequenc return xmInstrumentToInstrument(ih, linearFrequencySlides, features) } -func convertXmPattern(pkt xmfile.Pattern) (pattern.Pattern, int) { - pat := make(pattern.Pattern, len(pkt.Data)) +func convertXmPattern[TPeriod period.Period](pkt xmfile.Pattern) (*pattern.Pattern[TPeriod], int) { + pat := make(song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume], len(pkt.Data)) maxCh := uint8(0) for rowNum, drow := range pkt.Data { - row := make(pattern.Row, len(drow)) + row := make(song.Row[channel.Data[TPeriod], xmVolume.XmVolume], len(drow)) pat[rowNum] = row for channelNum, chn := range drow { - cd := channel.Data{ + cd := channel.Data[TPeriod]{ What: chn.Flags, Note: chn.Note, Instrument: chn.Instrument, @@ -279,10 +278,18 @@ func convertXmPattern(pkt xmfile.Pattern) (pattern.Pattern, int) { } } - return pat, int(maxCh) + return &pattern.Pattern[TPeriod]{Pattern: pat}, int(maxCh) +} + +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 convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.Song, error) { +func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features []feature.Feature) (*layout.Song[TPeriod], error) { h, err := moduleHeaderToHeader(&f.Head) if err != nil { return nil, err @@ -290,16 +297,17 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So linearFrequencySlides := f.Head.Flags.IsLinearSlides() - song := layout.Song{ + s := layout.Song[TPeriod]{ + System: xmSystem.XMSystem, Head: *h, - Instruments: make(map[uint8]*instrument.Instrument), - InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument), - Patterns: make([]pattern.Pattern, len(f.Patterns)), + Instruments: make(map[uint8]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), + InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), + Patterns: make([]pattern.Pattern[TPeriod], len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.SongLength)), } 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 { @@ -315,7 +323,7 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So InstID: uint8(instNum + 1), } sample.Static.ID = id - song.Instruments[id.InstID] = sample + s.Instruments[id.InstID] = sample } for i, sts := range noteMap { sample := samples[i] @@ -323,10 +331,10 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So if !ok { continue } - inm, ok := song.InstrumentNoteMap[id.InstID] + inm, ok := s.InstrumentNoteMap[id.InstID] if !ok { - inm = make(map[note.Semitone]*instrument.Instrument) - song.InstrumentNoteMap[id.InstID] = inm + inm = make(map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) + s.InstrumentNoteMap[id.InstID] = inm } for _, st := range sts { inm[st] = samples[i] @@ -335,16 +343,15 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So } lastEnabledChannel := 0 - song.Patterns = make([]pattern.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { - pattern, maxCh := convertXmPattern(pkt) + pattern, maxCh := convertXmPattern[TPeriod](pkt) if pattern == nil { continue } if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - song.Patterns[patNum] = pattern + s.Patterns[patNum] = *pattern } sharedMem := channel.SharedMemory{ @@ -356,7 +363,7 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So for chNum := range channels { cs := layout.ChannelSetting{ Enabled: true, - InitialVolume: xmVolume.DefaultVolume, + InitialVolume: xmVolume.DefaultXmVolume, InitialPanning: xmPanning.DefaultPanning, Memory: channel.Memory{ Shared: &sharedMem, @@ -368,12 +375,12 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (*layout.So 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/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 index 4aa50db..1fc26b1 100644 --- a/format/xm/pattern/pattern.go +++ b/format/xm/pattern/pattern.go @@ -2,7 +2,11 @@ package pattern import ( "github.com/gotracker/playback/format/xm/channel" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" ) -type Pattern = song.Pattern[channel.Data] +type Pattern[TPeriod period.Period] struct { + song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume] +} diff --git a/format/xm/pattern/row.go b/format/xm/pattern/row.go index 23396d4..8521c07 100644 --- a/format/xm/pattern/row.go +++ b/format/xm/pattern/row.go @@ -2,7 +2,11 @@ package pattern import ( "github.com/gotracker/playback/format/xm/channel" + xmVolume "github.com/gotracker/playback/format/xm/volume" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" ) -type Row = song.Row[channel.Data] +type Row[TPeriod period.Period] struct { + song.Row[channel.Data[TPeriod], xmVolume.XmVolume] +} diff --git a/format/xm/pattern/state.go b/format/xm/pattern/state.go index e45e4da..f5ca817 100644 --- a/format/xm/pattern/state.go +++ b/format/xm/pattern/state.go @@ -4,8 +4,10 @@ import ( "errors" "github.com/gotracker/playback/format/xm/channel" + xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/pattern" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/song" formatutil "github.com/gotracker/playback/util" @@ -13,7 +15,7 @@ import ( ) // State is the current pattern state -type State struct { +type State[TPeriod period.Period] struct { currentOrder index.Order currentRow index.Row ticks int @@ -27,22 +29,22 @@ type State struct { loopDetect formatutil.LoopDetect // when SongLoopEnabled is false, this is used to detect song loops loopCount int - Patterns []Pattern + Patterns []Pattern[TPeriod] Orders []index.Pattern } // GetTempo returns the tempo of the current state -func (state *State) GetTempo() int { +func (state *State[TPeriod]) GetTempo() int { return state.tempo } // GetSpeed returns the row speed of the current state -func (state *State) GetSpeed() int { +func (state *State[TPeriod]) GetSpeed() int { return state.ticks } // GetTicksThisRow returns the number of ticks in the current row -func (state *State) GetTicksThisRow() int { +func (state *State[TPeriod]) GetTicksThisRow() int { rowLoops := 1 if patternDelay, ok := state.patternDelay.Get(); ok { rowLoops = patternDelay @@ -54,7 +56,7 @@ func (state *State) GetTicksThisRow() int { } // GetPatNum returns the current pattern number -func (state *State) GetPatNum() index.Pattern { +func (state *State[TPeriod]) GetPatNum() index.Pattern { if int(state.currentOrder) >= len(state.Orders) { return index.InvalidPattern } @@ -62,7 +64,7 @@ func (state *State) GetPatNum() index.Pattern { } // GetNumRows returns the number of rows in the current pattern -func (state *State) GetNumRows() (int, error) { +func (state *State[TPeriod]) GetNumRows() (int, error) { rows, err := state.GetRows() if err != nil { return 0, err @@ -74,31 +76,31 @@ func (state *State) GetNumRows() (int, error) { } // WantsStop returns true when the current pattern wants to end the song -func (state *State) WantsStop() bool { +func (state *State[TPeriod]) WantsStop() bool { return state.GetPatNum() == index.InvalidPattern } // setCurrentOrder sets the current order index -func (state *State) setCurrentOrder(order index.Order) { +func (state *State[TPeriod]) setCurrentOrder(order index.Order) { state.currentOrder = order } -func (state *State) advanceOrder() { +func (state *State[TPeriod]) advanceOrder() { state.setCurrentOrder(state.currentOrder + 1) } // GetCurrentOrder returns the current order -func (state *State) GetCurrentOrder() index.Order { +func (state *State[TPeriod]) GetCurrentOrder() index.Order { return state.currentOrder } // GetNumOrders returns the number of orders in the song -func (state *State) GetNumOrders() int { +func (state *State[TPeriod]) GetNumOrders() int { return len(state.Orders) } // GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { +func (state *State[TPeriod]) GetCurrentPatternIdx() (index.Pattern, error) { ordLen := len(state.Orders) if ordLen == 0 { @@ -137,12 +139,12 @@ func (state *State) GetCurrentPatternIdx() (index.Pattern, error) { } // GetCurrentRow returns the current row -func (state *State) GetCurrentRow() index.Row { +func (state *State[TPeriod]) GetCurrentRow() index.Row { return state.currentRow } // setCurrentRow sets the current row -func (state *State) setCurrentRow(row index.Row) error { +func (state *State[TPeriod]) setCurrentRow(row index.Row) error { state.currentRow = row rows, err := state.GetNumRows() if err != nil { @@ -157,7 +159,7 @@ func (state *State) setCurrentRow(row index.Row) error { } // Observe will attempt to detect a song loop -func (state *State) Observe() error { +func (state *State[TPeriod]) 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) { @@ -176,7 +178,7 @@ func (state *State) Observe() error { } // nextOrder travels to the next pattern in the order list -func (state *State) nextOrder(resetRow ...bool) error { +func (state *State[TPeriod]) nextOrder(resetRow ...bool) error { state.advanceOrder() state.patternDelay.Reset() state.finePatternDelay = 0 @@ -191,8 +193,8 @@ func (state *State) nextOrder(resetRow ...bool) error { } // Reset resets a pattern state back to zeroes -func (state *State) Reset() { - *state = State{ +func (state *State[TPeriod]) Reset() { + *state = State[TPeriod]{ SongLoop: feature.SongLoop{ Count: 0, }, @@ -205,7 +207,7 @@ func (state *State) Reset() { // 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 { +func (state *State[TPeriod]) nextRow() error { state.patternDelay.Reset() state.finePatternDelay = 0 @@ -234,7 +236,7 @@ func (state *State) nextRow() error { } // GetRows returns all the rows in the pattern -func (state *State) GetRows() (*song.Pattern[channel.Data], error) { +func (state *State[TPeriod]) GetRows() (*song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume], error) { nextRow: for loops := 0; loops < len(state.Patterns); loops++ { var patNum = state.GetPatNum() @@ -251,21 +253,21 @@ nextRow: return nil, nil } pat := &state.Patterns[patNum] - return pat, nil + return &pat.Pattern, nil } } return nil, nil } // NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State) NeedResetPatternLoops() bool { +func (state *State[TPeriod]) 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 { +func (state *State[TPeriod]) commitTransaction(txn *pattern.RowUpdateTransaction) error { tempo, tempoSet := txn.Tempo.Get() tempoDelta, tempoDeltaSet := txn.TempoDelta.Get() if tempoSet || tempoDeltaSet { @@ -330,7 +332,7 @@ func (state *State) commitTransaction(txn *pattern.RowUpdateTransaction) error { } // StartTransaction starts a row update transaction -func (state *State) StartTransaction() *pattern.RowUpdateTransaction { +func (state *State[TPeriod]) StartTransaction() *pattern.RowUpdateTransaction { txn := pattern.RowUpdateTransaction{ CommitTransaction: state.commitTransaction, } diff --git a/format/xm/period/util.go b/format/xm/period/util.go index bf36ee3..1e17661 100644 --- a/format/xm/period/util.go +++ b/format/xm/period/util.go @@ -1,31 +1,14 @@ package period import ( + "math" + + xmSystem "github.com/gotracker/playback/format/xm/system" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/system" ) -const ( - // DefaultC4SampleRate is the default c4 sample rate for XM samples - DefaultC4SampleRate = 8363 - c2Period = 1712 - - floatDefaultC4SampleRate = float32(DefaultC4SampleRate) - - // XMBaseClock is the base clock speed of xm files - XMBaseClock period.Frequency = DefaultC4SampleRate * c2Period - - NotesPerOctave = 12 - SlideFinesPerSemitone = 4 - SemitonesPerNote = 16 - SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote - SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave - - C4SlideFines = 4 * SlideFinesPerOctave -) - -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[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c4SampleRate period.Frequency) TPeriod { if semi == note.UnchangedSemitone { @@ -35,26 +18,34 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet var result TPeriod switch p := any(&result).(type) { case *period.Linear: - nft := int(semi)*SlideFinesPerNote + int(ft) + nft := int(semi)*xmSystem.SlideFinesPerNote + int(ft)/2 p.Finetune = note.Finetune(nft) - p.CommonRate = c4SampleRate case *period.Amiga: - key := int(semi.Key()) - octave := uint32(semi.Octave()) - - if key >= len(semitonePeriodTable) { + stp, ok := xmSystem.XMSystem.GetSemitonePeriod(semi.Key()) + if !ok { return result } if c4SampleRate == 0 { - c4SampleRate = period.Frequency(DefaultC4SampleRate) + c4SampleRate = period.Frequency(xmSystem.DefaultC4SampleRate) } - if ft != 0 { - c4SampleRate = CalcFinetuneC4SampleRate[period.Amiga](AmigaConverter, c4SampleRate, ft) + per := max(float64(xmSystem.C4Note)*xmSystem.SlideFinesPerNote-float64(ft)/2, 0) + + exp := (per / xmSystem.SlideFinesPerOctave) - xmSystem.C4Octave + float64(semi.Octave()) + + pow := math.Pow(2.0, exp) + sampleRate := math.Floor(float64(c4SampleRate) * pow) + + if sampleRate == 0 { + return result } - *p = period.Amiga(float64(floatDefaultC4SampleRate*semitonePeriodTable[key]) / float64(uint32(c4SampleRate)< actual { - nch = actual - } - - for channelNum := 0; channelNum < nch; channelNum++ { - cdata := row.GetChannel(index.Channel(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 - } - - return nil -} - -func (m *manager[TPeriod]) processPatternRow() error { - patIdx, err := m.pattern.GetCurrentPatternIdx() - if err != nil { - return err - } - rowIdx := m.pattern.GetCurrentRow() - - pat := m.song.GetPattern(patIdx) - row := pat.GetRow(rowIdx) - nch := row.GetNumChannels() - if actual := m.GetNumChannels(); nch > actual { - nch = actual - } - - s := m.GetSampler() - - 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 := 0; channelNum < nch; channelNum++ { - cs := &m.channels[channelNum] - - if err := m.processRowForChannel(cs); err != nil { - return err - } - } - - return nil -} - -func (m *manager[TPeriod]) processRowForChannel(cs *state.ChannelState[TPeriod, 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 7593e97..0000000 --- a/format/xm/playback/playback_render.go +++ /dev/null @@ -1,111 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/output" - "github.com/gotracker/playback/player/render" -) - -// OnPreTick runs the IT pre-tick processing -func (m *manager[TPeriod]) OnPreTick() error { - m.premix = nil - - m.postMixRowTxn = m.pattern.StartTransaction() - - if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { - if err := m.startProcessPatternRow(); err != nil { - return err - } - } - - return nil -} - -// OnTick runs the IT tick processing -func (m *manager[TPeriod]) OnTick() error { - postMixRowTxn := m.postMixRowTxn - defer func() { - postMixRowTxn.Cancel() - m.postMixRowTxn = nil - }() - - if m.rowRenderState == nil || m.rowRenderState.CurrentTick >= m.rowRenderState.TicksThisRow { - if err := m.processPatternRow(); err != nil { - return err - } - } - - var finalData render.RowRender - premix := &output.PremixData{ - Userdata: &finalData, - SamplesLen: m.rowRenderState.Samples, - } - - if err := m.soundRenderTick(premix); err != nil { - return 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 err - } - - m.premix = premix - return nil -} - -// GetPremixData gets the current premix data from the manager -func (m *manager[TPeriod]) GetPremixData() (*output.PremixData, error) { - return m.premix, nil -} - -func (m *manager[TPeriod]) 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[TPeriod]) 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 cdadafa..0000000 --- a/format/xm/playback/playback_textoutput.go +++ /dev/null @@ -1,25 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/format/xm/channel" - "github.com/gotracker/playback/player/render" -) - -func (m *manager[TPeriod]) 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 - } - - rowText.Channels[ch] = cs.GetChannelData() - } - return &rowText -} diff --git a/format/xm/settings/machine.go b/format/xm/settings/machine.go new file mode 100644 index 0000000..6af3fca --- /dev/null +++ b/format/xm/settings/machine.go @@ -0,0 +1,71 @@ +package settings + +import ( + "fmt" + + "github.com/gotracker/playback/filter" + 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/oscillator" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" + voiceOscillator "github.com/gotracker/playback/voice/oscillator" +) + +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: filterFactory, + GetVibratoFactory: vibratoFactory, + GetTremoloFactory: tremoloFactory, + GetPanbrelloFactory: panbrelloFactory, + VoiceFactory: amigaVoiceFactory, + } + + linearMachine = settings.MachineSettings[period.Linear, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + PeriodConverter: xmPeriod.LinearConverter, + GetFilterFactory: filterFactory, + GetVibratoFactory: vibratoFactory, + GetTremoloFactory: tremoloFactory, + GetPanbrelloFactory: panbrelloFactory, + VoiceFactory: linearVoiceFactory, + } +) + +func filterFactory(name string) (settings.FilterFactoryFunc, error) { + switch name { + case "amigalpf": + return func(instrument, playback period.Frequency) (filter.Filter, error) { + lpf := filter.NewAmigaLPF(instrument, playback) + return lpf, nil + }, nil + + default: + return nil, fmt.Errorf("unsupported filter: %q", name) + } +} + +func vibratoFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewImpulseTrackerOscillator(4), nil +} + +func tremoloFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewImpulseTrackerOscillator(4), nil +} + +func panbrelloFactory() (voiceOscillator.Oscillator, error) { + return oscillator.NewImpulseTrackerOscillator(1), nil +} diff --git a/format/xm/settings/voicefactory.go b/format/xm/settings/voicefactory.go new file mode 100644 index 0000000..0fecd3c --- /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() voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { + return xmVoice.New(GetMachineSettings[TPeriod]()) +} + +var ( + amigaVoiceFactory voiceFactory[period.Amiga] + linearVoiceFactory voiceFactory[period.Linear] +) diff --git a/format/xm/system/system.go b/format/xm/system/system.go index efdf8e4..eff5659 100644 --- a/format/xm/system/system.go +++ b/format/xm/system/system.go @@ -8,7 +8,10 @@ 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 = 1712 + C4Period = 856 + + C4Octave = 4 + C4Note = C4Octave * NotesPerOctave floatDefaultC4SampleRate = float32(DefaultC4SampleRate) @@ -20,11 +23,17 @@ const ( SemitonesPerNote = 16 SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave - C4SlideFines = 4 * SlideFinesPerOctave + C4SlideFines = C4Note * SlideFinesPerNote ) -var XMSystem system.System = system.ClockedSystem{ - BaseClock: XMBaseClock, - BaseFinetunes: C4SlideFines, - FinetunesPerOctave: SlideFinesPerOctave, +var semitonePeriodTable = [...]float32{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, + 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..6620c06 --- /dev/null +++ b/format/xm/voice/enveloper_pan.go @@ -0,0 +1,32 @@ +package voice + +import ( + xmPanning "github.com/gotracker/playback/format/xm/panning" +) + +// == PanEnveloper == + +func (v *xmVoice[TPeriod]) EnablePanEnvelope(enabled bool) { + 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) { + if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB(v) + } +} + +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..84845fe --- /dev/null +++ b/format/xm/voice/enveloper_volume.go @@ -0,0 +1,32 @@ +package voice + +import ( + xmVolume "github.com/gotracker/playback/format/xm/volume" +) + +// == VolumeEnveloper == + +func (v *xmVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) { + 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) { + if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB(v) + } +} + +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..b9b91f0 --- /dev/null +++ b/format/xm/voice/modulator_amp.go @@ -0,0 +1,62 @@ +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) { + v.amp.SetActive(on) +} + +func (v xmVoice[TPeriod]) IsActive() bool { + return v.amp.IsActive() +} + +func (v *xmVoice[TPeriod]) SetMixingVolume(vol xmVolume.XmVolume) { + if !vol.IsUseInstrumentVol() { + v.amp.SetMixingVolume(vol) + } +} + +func (v xmVoice[TPeriod]) GetMixingVolume() xmVolume.XmVolume { + return v.amp.GetMixingVolume() +} + +func (v *xmVoice[TPeriod]) SetVolume(vol xmVolume.XmVolume) { + if vol.IsUseInstrumentVol() { + vol = v.voicer.GetDefaultVolume() + } + v.amp.SetVolume(vol) +} + +func (v xmVoice[TPeriod]) GetVolume() xmVolume.XmVolume { + return v.amp.GetVolume() +} + +func (v *xmVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) { + 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..f1d3760 --- /dev/null +++ b/format/xm/voice/modulator_freq.go @@ -0,0 +1,32 @@ +package voice + +import ( + "github.com/gotracker/playback/period" +) + +// == FreqModulator == + +func (v *xmVoice[TPeriod]) SetPeriod(period TPeriod) { + v.freq.SetPeriod(period) +} + +func (v *xmVoice[TPeriod]) GetPeriod() TPeriod { + return v.freq.GetPeriod() +} + +func (v *xmVoice[TPeriod]) SetPeriodDelta(delta period.Delta) { + v.freq.SetPeriodDelta(delta) +} + +func (v *xmVoice[TPeriod]) GetPeriodDelta() period.Delta { + return v.freq.GetPeriodDelta() +} + +func (v *xmVoice[TPeriod]) GetFinalPeriod() TPeriod { + p := v.freq.GetFinalPeriod() + if v.IsPitchEnvelopeEnabled() { + delta := v.GetCurrentPitchEnvelope() + p = period.AddDelta(p, delta) + } + return p +} diff --git a/format/xm/voice/modulator_pan.go b/format/xm/voice/modulator_pan.go new file mode 100644 index 0000000..6beaa82 --- /dev/null +++ b/format/xm/voice/modulator_pan.go @@ -0,0 +1,33 @@ +package voice + +import ( + xmPanning "github.com/gotracker/playback/format/xm/panning" + "github.com/gotracker/playback/voice/types" +) + +// == PanModulator == + +func (v *xmVoice[TPeriod]) SetPan(pan xmPanning.Panning) { + v.pan.SetPan(pan) +} + +func (v xmVoice[TPeriod]) GetPan() xmPanning.Panning { + return v.pan.GetPan() +} + +func (v *xmVoice[TPeriod]) SetPanDelta(d types.PanDelta) { + v.pan.SetPanDelta(d) +} + +func (v xmVoice[TPeriod]) GetPanDelta() types.PanDelta { + return v.pan.GetPanDelta() +} + +func (v xmVoice[TPeriod]) GetFinalPan() xmPanning.Panning { + if !v.IsPanEnvelopeEnabled() { + return v.pan.GetFinalPan() + } + + envPan := v.panEnv.GetCurrentValue() + return envPan +} diff --git a/format/xm/voice/render.go b/format/xm/voice/render.go new file mode 100644 index 0000000..d50cb3d --- /dev/null +++ b/format/xm/voice/render.go @@ -0,0 +1,53 @@ +package voice + +import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/volume" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/state/render" +) + +func (v *xmVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *playerRender.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) (*mixing.Data, error) { + if v.IsDone() { + return nil, nil + } + + if !v.IsActive() { + return nil, nil + } + + sampler, err := v.GetSampler(details.SamplerSpeed, renderChannel) + if err != nil || sampler == nil { + return nil, err + } + + // ... so grab the new value now. + pan := v.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.ToPosition(), + Volume: volume.Volume(1.0), + Pos: 0, + SamplesLen: details.Samples, + } + + if err := v.SetPos(sampler.GetPosition()); err != nil { + return nil, err + } + + return data, nil +} diff --git a/format/xm/voice/sampler.go b/format/xm/voice/sampler.go new file mode 100644 index 0000000..0c5d730 --- /dev/null +++ b/format/xm/voice/sampler.go @@ -0,0 +1,69 @@ +package voice + +import ( + "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/gomixing/volume" + xmPanning "github.com/gotracker/playback/format/xm/panning" + xmVolume "github.com/gotracker/playback/format/xm/volume" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/voice/component" +) + +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 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.config.VoiceFilter != nil { + wet = v.config.VoiceFilter.Filter(wet) + } + return wet +} + +func (v *xmVoice[TPeriod]) GetSampler(samplerRate float32, renderChannel *playerRender.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) (sampling.Sampler, error) { + o := component.OutputFilter{ + Input: v, + Output: renderChannel, + } + + pos, err := v.GetPos() + if err != nil { + return nil, err + } + + p := v.GetFinalPeriod() + + samplerAdd := float32(v.ms.PeriodConverter.GetSamplerAdd(p, float64(v.config.SampleRate)*float64(samplerRate))) + s := sampling.NewSampler(&o, pos, samplerAdd) + + return s, nil +} 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..b216912 --- /dev/null +++ b/format/xm/voice/voice.go @@ -0,0 +1,241 @@ +package voice + +import ( + 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/player/machine/settings" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" +) + +type Period interface { + period.Period +} + +type xmVoice[TPeriod Period] struct { + ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + config voice.InstrumentConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + + fadeoutMode fadeout.Mode + + component.KeyModulator + + voicer component.Voicer[TPeriod, 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 +} + +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](ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { + v := &xmVoice[TPeriod]{ + ms: ms, + } + + 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: xmVolume.DefaultXmMixingVolume, + DefaultVolume: xmVolume.DefaultXmVolume, + }) + + v.pan.Setup(component.PanModulatorSettings[xmPanning.Panning]{ + InitialPan: xmPanning.DefaultPanning, + }) + + 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() + } + + v.fadeout.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]) Setup(config voice.InstrumentConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) { + v.config = config + v.fadeout.Setup(component.FadeoutModulatorSettings{ + GetEnabled: v.getFadeoutEnabled, + Amount: config.FadeOut.Amount, + }) + v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) + v.autoVibrato.Setup(config.AutoVibrato) + volEnvSettings := component.EnvelopeSettings[xmVolume.XmVolume, xmVolume.XmVolume]{ + Envelope: config.VolEnv, + } + if config.VolEnvFinishFadesOut { + volEnvSettings.OnFinished = func(v voice.Voice) { + v.Fadeout() + } + } + v.volEnv.Setup(volEnvSettings) + v.panEnv.Setup(component.EnvelopeSettings[xmPanning.Panning, xmPanning.Panning]{ + Envelope: config.PanEnv, + }) + v.vol0Opt.Setup(config.Vol0Optimization) + v.KeyModulator.Release() + v.Reset() +} + +func (v *xmVoice[TPeriod]) Reset() { + v.fadeoutMode = v.config.FadeOut.Mode + + v.fadeout.Reset() + + v.volEnv.Reset() + v.panEnv.Reset() + + v.autoVibrato.Reset() + + v.volEnv.Reset() + v.panEnv.Reset() + v.vol0Opt.Reset() +} + +func (v *xmVoice[TPeriod]) Stop() { + v.voicer = nil +} + +func (v *xmVoice[TPeriod]) IsDone() bool { + if v.voicer == nil { + return true + } + + if v.fadeout.IsActive() { + return v.fadeout.GetVolume() <= 0 + } + + return v.vol0Opt.IsDone() +} + +func (v *xmVoice[TPeriod]) Advance() { + 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.vol0Opt.ObserveVolume(v.GetFinalVolume()) + v.KeyModulator.Advance() +} + +func (v *xmVoice[TPeriod]) Clone() voice.Voice { + vv := xmVoice[TPeriod]{ + ms: v.ms, + config: v.config, + fadeoutMode: v.fadeoutMode, + amp: v.amp.Clone(), + freq: v.freq.Clone(), + pan: v.pan.Clone(), + volEnv: v.volEnv.Clone(), + panEnv: v.panEnv.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.config.VoiceFilter != nil { + vv.config.VoiceFilter = v.config.VoiceFilter.Clone() + } + + return &vv +} + +func (v *xmVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, defVol xmVolume.XmVolume) { + var s component.Sampler[TPeriod, xmVolume.XmVolume] + s.Setup(component.SamplerSettings[TPeriod, xmVolume.XmVolume]{ + Sample: samp, + DefaultVolume: defVol, + WholeLoop: wholeLoop, + SustainLoop: sustainLoop, + }) + v.voicer = &s +} 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..ba5ba4b 100644 --- a/format/xm/xm.go +++ b/format/xm/xm.go @@ -6,7 +6,10 @@ import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/load" + "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/util" ) @@ -31,3 +34,8 @@ func (f format) Load(filename string, features []feature.Feature) (playback.Play func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playback.Playback, error) { return load.XM(r, features) } + +func init() { + machine.RegisterMachine(settings.GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(settings.GetMachineSettings[period.Linear]()) +} 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 4b28271..56e917c 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -8,17 +8,40 @@ import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/types" ) +type InstrumentIntf interface { + IsInvalid() bool + GetSampleRate() period.Frequency + SetSampleRate(sampleRate period.Frequency) + GetLength() sampling.Pos + SetFinetune(ft note.Finetune) + GetFinetune() note.Finetune + GetID() ID + GetSemitoneShift() int8 + GetKind() Kind + GetNewNoteAction() note.Action + GetData() Data + GetFilterFactory() filter.Factory + GetPluginFilterFactory() filter.Factory + GetAutoVibrato() autovibrato.AutoVibratoSettings + 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[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { 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.AutoVibratoSettings NewNoteAction note.Action Finetune note.Finetune FilterFactory filter.Factory @@ -26,46 +49,51 @@ type StaticValues struct { } // Instrument is the mildly-decoded instrument/sample header -type Instrument struct { - Static StaticValues +type Instrument[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { + Static StaticValues[TMixingVolume, TVolume, TPanning] Inst Data SampleRate period.Frequency Finetune optional.Value[note.Finetune] } // IsInvalid always returns false (valid) -func (inst Instrument) IsInvalid() bool { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) IsInvalid() bool { return false } // GetSampleRate returns the central-note sample rate value for the instrument // This may get mutated if a finetune effect is processed -func (inst Instrument) GetSampleRate() period.Frequency { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetSampleRate() period.Frequency { return inst.SampleRate } // SetSampleRate sets the central-note sample rate value for the instrument -func (inst *Instrument) SetSampleRate(sampleRate period.Frequency) { +func (inst *Instrument[TMixingVolume, TVolume, TPanning]) SetSampleRate(sampleRate period.Frequency) { inst.SampleRate = sampleRate } // GetDefaultVolume returns the default volume value for the instrument -func (inst Instrument) GetDefaultVolume() volume.Volume { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetDefaultVolume() TVolume { return inst.Static.Volume } +// GetDefaultVolumeGeneric returns the default volume value for the instrument +func (inst Instrument[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[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[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[TMixingVolume, TVolume, TPanning]) GetFinetune() note.Finetune { if ft, ok := inst.Finetune.Get(); ok { return ft } @@ -73,47 +101,47 @@ func (inst Instrument) GetFinetune() note.Finetune { } // GetID returns the instrument number (1-based) -func (inst Instrument) GetID() ID { +func (inst Instrument[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[TMixingVolume, TVolume, TPanning]) GetSemitoneShift() int8 { return inst.Static.RelativeNoteNumber } // GetKind returns the kind of the instrument -func (inst Instrument) GetKind() Kind { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetKind() Kind { return inst.Inst.GetKind() } // GetNewNoteAction returns the NewNoteAction associated to the instrument -func (inst Instrument) GetNewNoteAction() note.Action { +func (inst Instrument[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[TMixingVolume, TVolume, TPanning]) GetData() Data { return inst.Inst } // GetFilterFactory returns the factory for the channel filter -func (inst Instrument) GetFilterFactory() filter.Factory { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetFilterFactory() filter.Factory { return inst.Static.FilterFactory } // GetPluginFilterFactory returns the factory for the channel plugin filter -func (inst Instrument) GetPluginFilterFactory() filter.Factory { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetPluginFilterFactory() filter.Factory { return inst.Static.PluginFilter } // GetAutoVibrato returns the settings for the autovibrato system -func (inst Instrument) GetAutoVibrato() voice.AutoVibrato { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetAutoVibrato() autovibrato.AutoVibratoSettings { 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[TMixingVolume, TVolume, TPanning]) IsReleaseNote(n note.Note) bool { switch n.Type() { case note.SpecialTypeStopOrRelease: return inst.GetKind() == KindOPL2 @@ -122,7 +150,7 @@ func (inst Instrument) IsReleaseNote(n note.Note) bool { } // IsStopNote returns true if the note is a stop (Note-Cut) -func (inst Instrument) IsStopNote(n note.Note) bool { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) IsStopNote(n note.Note) bool { switch n.Type() { case note.SpecialTypeStopOrRelease: return inst.GetKind() == KindPCM diff --git a/instrument/pcm.go b/instrument/pcm.go index 7c858d0..1781451 100644 --- a/instrument/pcm.go +++ b/instrument/pcm.go @@ -1,34 +1,43 @@ package instrument import ( - "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/note" "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/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[filter.PitchFiltValue] // 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 + VolEnv envelope.Envelope[TVolume] + VolEnvFinishFadesOut bool + PanEnv envelope.Envelope[TPanning] + PitchFiltMode bool // true = filter, false = pitch + PitchFiltEnv envelope.Envelope[filter.PitchFiltValue] // this is either pitch or filter } -func (PCM) GetKind() Kind { +type PitchPan struct { + Enabled bool + Center note.Semitone + Separation float32 +} + +func (PCM[TMixingVolume, TVolume, TPanning]) 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/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/oscillator/impulsetracker.go b/oscillator/impulsetracker.go index 8dc508f..d9609ff 100644 --- a/oscillator/impulsetracker.go +++ b/oscillator/impulsetracker.go @@ -114,6 +114,10 @@ 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() { o.reset(false) diff --git a/oscillator/protracker.go b/oscillator/protracker.go index c074fcb..526c298 100644 --- a/oscillator/protracker.go +++ b/oscillator/protracker.go @@ -42,7 +42,7 @@ func (o protrackerOscillator) Clone() oscillator.Oscillator { } // 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: @@ -81,6 +81,10 @@ 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() { o.reset(false) diff --git a/period/amiga.go b/period/amiga.go index 3735d5a..d7d2d19 100644 --- a/period/amiga.go +++ b/period/amiga.go @@ -33,6 +33,24 @@ func (p Amiga) PortaUp(amount int) Amiga { return p.Add(Delta(amount)) } +func (p Amiga) PortaTo(amount int, target Amiga) Amiga { + 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 Amiga) IsInvalid() bool { return p == 0 } @@ -65,5 +83,8 @@ func (p Amiga) GetFrequency() Frequency { } 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 index 10fc1f8..298ea72 100644 --- a/period/amigaconverter.go +++ b/period/amigaconverter.go @@ -1,11 +1,16 @@ package period -import "github.com/gotracker/playback/system" +import ( + "errors" + + "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.System + System system.ClockableSystem } var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) @@ -19,9 +24,78 @@ func (c AmigaConverter) GetFrequency(p Amiga) Frequency { } // GetSamplerAdd returns the number of samples to advance an instrument by given the period -func (AmigaConverter) GetSamplerAdd(p Amiga, samplerSpeed float64) float64 { +func (c AmigaConverter) GetSamplerAdd(p Amiga, samplerSpeed float64) float64 { if p.IsInvalid() { return 0 } - return samplerSpeed / float64(p) + return samplerSpeed / (float64(p) * float64(c.System.GetCommonRate())) +} + +func (c AmigaConverter) GetPeriod(n note.Note) Amiga { + switch n.Type() { + case note.SpecialTypeEmpty, note.SpecialTypeRelease, note.SpecialTypeStop, note.SpecialTypeStopOrRelease: + return Amiga(0) + case note.SpecialTypeNormal: + semi := note.Semitone(n.(note.Normal)) + octave := uint32(semi.Octave()) + + keyPeriod, valid := c.System.GetSemitonePeriod(semi.Key()) + if !valid { + return Amiga(0) + } + + return Amiga(float64(keyPeriod) / float64(uint32(1)<= len(m.channels) { + return nil, fmt.Errorf("invalid channel index: %d", ch) + } + + return &m.channels[ch], nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IsChannelEnabled(ch index.Channel) bool { + if int(ch) >= len(m.channels) { + return false + } + + return m.songData.GetChannelSettings(ch).GetEnabled() +} + +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.RowIntf, error) { + pat, err := m.songData.GetPatternIntfByOrder(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.GetRowIntf(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.RowIntf) error { + rowChannels := min(m.songData.GetNumChannels(), rowData.GetNumChannels()) + for i := range m.channels { + ch := index.Channel(i) + + c := &m.channels[ch] + c.instructions = nil + + if !c.enabled || i >= rowChannels { + continue + } + + d := rowData.GetChannelIntf(ch) + + if d != nil { + if err := c.decodeNote(m, d); err != nil { + return 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 fmt.Errorf("channel[%d] instruction error: %w", ch, err) + } + + c.instructions = insts + } + } + } + + return 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..9a4457c --- /dev/null +++ b/player/machine/machine_channel.go @@ -0,0 +1,562 @@ +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 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]) 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[TMixingVolume, TVolume, TPanning], error) { + return withChannelReturningValue(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (*instrument.Instrument[TMixingVolume, TVolume, TPanning], error) { + return c.target.Inst, nil + }) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelInstrumentByID(ch index.Channel, i instrument.ID) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + var oldID instrument.ID + if c.target.Inst != nil { + oldID = c.target.Inst.GetID() + } + + traceChannelValueChangeWithComment(m, ch, "target.Inst", oldID, i, "SetChannelInstrumentByID") + inst, _ := m.songData.GetInstrument(i) + var ok bool + c.target.Inst, ok = inst.(*instrument.Instrument[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]) SetChannelMute(ch index.Channel, enabled bool) error { + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + traceChannelValueChangeWithComment(m, ch, "mute", c.mute, enabled, "SetChannelMute") + c.mute = enabled + 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 + total := uint8(count) + + traceChannelValueChangeWithComment(m, ch, "patternLoopEnabled", pl.Enabled, true, "SetPatternLoops") + pl.Enabled = true + + traceChannelValueChangeWithComment(m, ch, "patternLoopEnd", pl.End, m.ticker.current.row, "SetPatternLoops") + pl.End = m.ticker.current.row + + traceChannelValueChangeWithComment(m, ch, "patternLoopTotal", pl.Total, total, "SetPatternLoops") + pl.Total = total + + traceChannelValueChangeWithComment(m, ch, "patternLoopCount", pl.Count, uint8(0), "SetPatternLoops") + pl.Count = 0 + + 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, "portaPeriod", c.portaPeriod, p, "StartChannelPortaToNote") + c.portaPeriod = p + } + + traceChannelOptionalValueResetWithComment(m, ch, "newNote.Pos", c.newNote.Pos, "StartChannelPortaToNote") + c.newNote.Pos.Reset() + + 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 { + p := freqMod.GetPeriod() + tp := period.PortaTo(p, int(delta), c.portaPeriod) + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaToNote") + 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 := period.PortaDown[TPeriod](p, int(delta)) + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaDown") + 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 := period.PortaUp[TPeriod](p, int(delta)) + + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaUp") + 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 { + m.doPastNoteAction(ch, 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..ca69eb7 --- /dev/null +++ b/player/machine/machine_factory.go @@ -0,0 +1,165 @@ +package machine + +import ( + "fmt" + "reflect" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/player/machine/settings" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/render" + "github.com/gotracker/playback/voice/types" +) + +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), + } + factoryRegistry[tl] = func(songData song.Data, us settings.UserSettings) (MachineTicker, error) { + var m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + m.ms = ms + m.songData = songData + m.us = us + + order := songData.GetInitialOrder() + if o, set := us.StartOrderAndRow.Order.Get(); set { + order = index.Order(o) + } + + var row index.Row + if r, set := us.StartOrderAndRow.Row.Get(); set { + row = index.Row(r) + } + + sys := songData.GetSystem() + + bpm := songData.GetInitialBPM() + if us.StartBPM != 0 { + bpm = us.StartBPM + } + + tempo := songData.GetInitialTempo() + if us.StartTempo != 0 { + tempo = us.StartTempo + } + + 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) + // make at least 64 output channels + m.outputChannels = make([]playerRender.Channel[TGlobalVolume, TMixingVolume, TPanning], channels) + for i := 0; i < channels; i++ { + ch := index.Channel(i) + cs := songData.GetChannelSettings(ch) + + rc := &m.outputChannels[ch] + rc.ChannelNum = i + rc.Filter = nil + rc.GetSampleRate = m.getSampleRate + rc.SetGlobalVolume = m.SetGlobalVolume + rc.GetOPL2Chip = func() render.OPL2Chip { + // TODO: add OPL2 back in + return nil + } + rc.ChannelVolume = types.GetMaxVolume[TMixingVolume]() + rc.LastGlobalVolume = types.GetMaxVolume[TGlobalVolume]() // this is the channel's version of the GlobalVolume + + c := &m.channels[ch] + c.enabled = cs.GetEnabled() + c.pn.MaxPastNotes = sys.GetMaxPastNotesPerChannel() + c.cv = m.ms.VoiceFactory.NewVoice() + c.memory = cs.GetMemory() + c.target.ActionTick.Reset() + + c.nna = note.ActionCut + var err error + 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 + } + if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { + freqMod.SetPeriod(m.ms.PeriodConverter.GetPeriod(note.StopNote{})) + } + cmv, err := song.GetChannelMixingVolume[TMixingVolume](cs) + if err != nil { + return nil, fmt.Errorf("channel[%d]: %w", i, err) + } + 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{ + Order: order, + Row: 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..761f4f2 --- /dev/null +++ b/player/machine/machine_globals.go @@ -0,0 +1,160 @@ +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 { + traceOptionalValueChangeWithComment(m, "nextRow", m.ticker.next.row, r, "SetRow") + m.ticker.next.row.Set(r) + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetFilterOnAllChannelsByFilterName(name string, enabled bool) error { + filtFactory, err := m.ms.GetFilterFactory(name) + if err != nil { + return err + } + + for i := range m.channels { + c := &m.channels[i] + if enabled { + filt, err := filtFactory(m.songData.GetSystem().GetCommonRate(), m.getSampleRate()) + if err != nil { + return err + } + c.filter = filt + } else { + c.filter = nil + } + } + + return nil +} diff --git a/player/machine/machine_instruction.go b/player/machine/machine_instruction.go new file mode 100644 index 0000000..b354032 --- /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 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 instructionPreTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + PreTick(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionPreTick(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionPreTick[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.PreTick(ch, m, m.ticker.current.tick) +} + +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 instructionPostTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + instruction.Instruction + PostTick(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionPostTick(ch index.Channel, i instruction.Instruction) error { + ii, ok := i.(instructionPostTick[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + if !ok { + return nil + } + return ii.PostTick(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) +} diff --git a/player/machine/machine_pastnote.go b/player/machine/machine_pastnote.go new file mode 100644 index 0000000..afd3b4a --- /dev/null +++ b/player/machine/machine_pastnote.go @@ -0,0 +1,59 @@ +package machine + +import ( + "sort" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/voice" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canPastNote() bool { + return m.songData.GetSystem().GetMaxPastNotesPerChannel() > 0 +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(pn voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { + type pastNoteAges struct { + i int + age int + } + + ages := make([]pastNoteAges, 0, len(m.outputChannels)) + // first pass, just try to add it + for i := range m.channels { + c := &m.channels[i] + + if c.pn.CanAddPastNote() { + c.pn.AddPastNote(pn, m.age) + return + } + + ages = append(ages, pastNoteAges{ + i: i, + age: c.pn.GetAge(), + }) + } + + if len(ages) == 0 { + // impossible, but just in case + pn.Stop() + return + } + + // second pass, find the oldest + sort.Slice(ages, func(i, j int) bool { + return ages[i].age < ages[j].age + }) + + // jam it in + oldest := ages[0].i + m.channels[oldest].pn.AddPastNote(pn, m.age) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPastNoteAction(ch index.Channel, na note.Action) { + if int(ch) >= len(m.outputChannels) { + return + } + + m.channels[ch].pn.DoPastNoteAction(na) +} 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..16d3b79 --- /dev/null +++ b/player/machine/machine_tick.go @@ -0,0 +1,173 @@ +package machine + +import ( + "fmt" + + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/output" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/sampler" + stateRender "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/voice" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(s *sampler.Sampler) (*output.PremixData, error) { + m.getSampleRate = func() period.Frequency { + return period.Frequency(s.SampleRate) + } + + for i := range m.channels { + if err := m.channels[i].PreTick(index.Channel(i), m); err != nil { + return nil, err + } + } + + 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 + } + + if err := runTick(&m.ticker, m); err != nil { + return nil, err + } + + details := stateRender.Details{ + Mix: s.Mixer(), + Panmixer: s.GetPanMixer(), + SamplerSpeed: s.GetSamplerSpeed(), + Samples: premix.SamplesLen, + Duration: tickDuration, + } + + centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) + + for i := range m.channels { + ch := index.Channel(i) + c := &m.channels[ch] + var mixData []mixing.Data + + if pos, set := c.target.Pos.Get(); set { + if samp, ok := c.cv.(voice.Sampler); ok { + samp.SetPos(pos) + } + c.target.Pos.Reset() + } + + rc := &m.outputChannels[ch] + + c.cv.DumpState(ch, m.us.Tracer) + data, err := c.cv.Render(centerAheadPan, details, rc) + if err != nil { + return nil, err + } + c.cv.Advance() + if data != nil { + mixData = append(mixData, *data) + } + + pnData, err := c.pn.Render(centerAheadPan, details, rc) + if err != nil { + return nil, err + } + if len(pnData) > 0 { + mixData = append(mixData, pnData...) + } + + if len(mixData) > 0 { + premix.Data = append(premix.Data, mixData) + } + } + + 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, + }, + }) + } + + m.age++ + return &premix, nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTick() error { + for i := range m.channels { + c := &m.channels[i] + if err := c.Tick(index.Channel(i), m); err != nil { + return err + } + + c.pn.UpdatePastNotes() + } + + for i := range m.channels { + c := &m.channels[i] + if err := c.PostTick(index.Channel(i), m); err != nil { + return err + } + } + return 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 + } + + for ch := range m.channels { + if err := m.channels[ch].RowStart(index.Channel(ch), m); err != nil { + return err + } + } + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowEnd() error { + for i := range m.channels { + c := &m.channels[i] + if err := c.RowEnd(index.Channel(i), m); err != nil { + return err + } + } + + return nil +} diff --git a/player/machine/machine_tracing.go b/player/machine/machine_tracing.go new file mode 100644 index 0000000..499e2e4 --- /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], comment string) { + 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, comment string) { + m.us.TraceWithComment(name, comment) +} + +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], comment string) { + if v, set := before.Get(); set { + m.us.TraceValueChangeWithComment(name, v, nil, comment) + } +} + +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, comment string) { + if v, set := before.Get(); set { + traceValueChangeWithComment(m, name, v, after, comment) + return + } + + m.us.TraceValueChangeWithComment(name, nil, after, comment) +} + +func traceValueChangeWithComment[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name string, before, after T, comment string) { + if reflect.DeepEqual(before, after) { + return + } + + m.us.TraceValueChangeWithComment(name, before, after, comment) +} + +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, comment string) { + m.us.TraceChannelWithComment(ch, name, comment) +} + +func traceChannelOptionalValueClear[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], comment string) { + if v, set := before.Get(); set { + m.us.TraceChannelValueChangeWithComment(ch, name, v, nil, comment) + } +} + +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, comment string) { + if v, set := before.Get(); set { + traceChannelValueChangeWithComment(m, ch, name, v, after, comment) + return + } + + m.us.TraceChannelValueChangeWithComment(ch, name, nil, after, comment) +} + +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, comment string) { + if reflect.DeepEqual(before, after) { + return + } + + m.us.TraceChannelValueChangeWithComment(ch, name, before, after, comment) +} diff --git a/player/machine/newnoteinfo.go b/player/machine/newnoteinfo.go new file mode 100644 index 0000000..bf58093 --- /dev/null +++ b/player/machine/newnoteinfo.go @@ -0,0 +1,35 @@ +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[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]) 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..2e7e4d4 --- /dev/null +++ b/player/machine/pastnote.go @@ -0,0 +1,103 @@ +package machine + +import ( + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/note" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/voice" +) + +type pastNote[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + age int +} + +type pastNotes[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + MaxPastNotes int + pn []pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] +} + +func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) CanAddPastNote() bool { + return len(o.pn) < o.MaxPastNotes +} + +func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) AddPastNote(v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], age int) { + o.pn = append(o.pn, pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ + v: v, + age: age, + }) + if over := len(o.pn) - o.MaxPastNotes; over > 0 { + for _, n := range o.pn[0:over] { + n.v.Stop() + } + o.pn = o.pn[over:] + } +} + +func (o pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetAge() int { + if len(o.pn) == 0 { + return 0 + } + + return o.pn[0].age +} + +func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) UpdatePastNotes() { + var updated []pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + for _, pn := range o.pn { + if pn.v.IsDone() { + pn.v.Stop() + continue + } + updated = append(updated, pn) + } + o.pn = updated +} + +func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoPastNoteAction(na note.Action) { + for _, pn := range o.pn { + switch na { + case note.ActionCut: + pn.v.Stop() + case note.ActionRelease: + pn.v.Release() + case note.ActionFadeout: + pn.v.Release() + pn.v.Fadeout() + case note.ActionRetrigger: + pn.v.Release() + pn.v.Attack() + + case note.ActionContinue: + fallthrough + default: + // nothing + } + } +} + +func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Render(centerAheadPan volume.Matrix, details render.Details, rc *playerRender.Channel[TGlobalVolume, TMixingVolume, TPanning]) ([]mixing.Data, error) { + var mixData []mixing.Data + + for _, pn := range p.pn { + if pn.v.IsDone() { + continue + } + + if ampMod, ok := pn.v.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok && !ampMod.IsActive() { + continue + } + + data, err := pn.v.Render(centerAheadPan, details, rc) + if err != nil { + return nil, err + } + + if data != nil { + mixData = append(mixData, *data) + } + } + return mixData, nil +} diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go new file mode 100644 index 0000000..fc0cd4b --- /dev/null +++ b/player/machine/settings/machinesettings.go @@ -0,0 +1,26 @@ +package settings + +import ( + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/period" + "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, playback period.Frequency) (filter.Filter, error) + +type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + PeriodConverter song.PeriodCalculator[TPeriod] + GetFilterFactory func(name string) (FilterFactoryFunc, error) + GetVibratoFactory func() (oscillator.Oscillator, error) + GetTremoloFactory func() (oscillator.Oscillator, error) + GetPanbrelloFactory func() (oscillator.Oscillator, error) + VoiceFactory voice.VoiceFactory[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] +} diff --git a/player/machine/settings/usersettings.go b/player/machine/settings/usersettings.go new file mode 100644 index 0000000..c6576d7 --- /dev/null +++ b/player/machine/settings/usersettings.go @@ -0,0 +1,91 @@ +package settings + +import ( + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/player/feature" + "github.com/gotracker/playback/tracing" +) + +type UserSettings struct { + Tracer tracing.Tracer + SongLoop feature.SongLoop + StartOrderAndRow feature.StartOrderAndRow + PlayUntilOrderAndRow feature.PlayUntilOrderAndRow + LongChannelOutput bool + IgnoreUnknownEffect bool + EnableNewNoteActions bool + StartTempo int + StartBPM int +} + +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, comment string) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceWithComment(op, comment) +} + +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, comment string) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceValueChangeWithComment(op, prev, new, comment) +} + +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, comment string) { + if s.Tracer == nil { + return + } + + s.TraceChannelWithComment(ch, op, comment) +} + +func (s UserSettings) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { + if s.Tracer == nil { + return + } + + s.TraceChannelValueChange(ch, op, prev, new) +} + +func (s UserSettings) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, comment string) { + if s.Tracer == nil { + return + } + + s.Tracer.TraceChannelValueChangeWithComment(ch, op, prev, new, comment) +} diff --git a/player/machine/ticker.go b/player/machine/ticker.go new file mode 100644 index 0000000..38e3bfc --- /dev/null +++ b/player/machine/ticker.go @@ -0,0 +1,141 @@ +package machine + +import ( + "errors" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/song" + "github.com/heucuva/optional" +) + +type ticker struct { + current struct { + tick int + row index.Row + order index.Order + } + next struct { + row optional.Value[index.Row] + order optional.Value[index.Order] + } +} + +type tickerSettings struct { + Order index.Order + Row index.Row +} + +func initTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](t *ticker, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], setup tickerSettings) error { + t.current.tick = 0 + t.current.row = 0 + t.current.order = 0 + t.next.row.Set(setup.Row) + t.next.order.Set(setup.Order) + + row, order, err := advanceRowOrder(t, m) + if err != nil { + return err + } + + t.current.row = row + t.current.order = order + m.us.SetTracingTick(t.current.order, t.current.row, t.current.tick) + + 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 + done := false + if tick >= m.tempo { + tick = 0 + rowAdvanced = true + } + + if rowAdvanced { + if err := m.onRowEnd(); err != nil { + return err + } + + var err error + row, order, err = advanceRowOrder(t, m) + if err != nil && !errors.Is(err, song.ErrStopSong) { + 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 { + return song.ErrStopSong + } else 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]) (index.Row, index.Order, error) { + row := int(t.current.row) + order := int(t.current.order) + + desiredRow, rowSet := t.next.row.Get() + desiredOrder, orderSet := t.next.order.Get() + + if rowSet && orderSet { + row = int(desiredRow) + order = int(desiredOrder) + } else if rowSet { + row = int(desiredRow) + order++ + } else if orderSet { + order = int(desiredOrder) + row = 0 + } else { + row++ + } + + t.next.row.Reset() + t.next.order.Reset() + + orderScanMax := len(m.songData.GetOrderList()) + orderScanIter := 0 +orderScan: + if orderScanIter >= orderScanMax { + return index.Row(row), index.Order(order), song.ErrStopSong + } + + pat, err := m.songData.GetPatternIntfByOrder(index.Order(order)) + if err != nil { + if errors.Is(err, index.ErrNextPattern) { + order++ + orderScanIter++ + // don't update row here + goto orderScan + } + return index.Row(row), index.Order(order), err + } + + if row >= pat.NumRows() { + order++ + orderScanIter++ + row = 0 + goto orderScan + } + + return index.Row(row), index.Order(order), nil +} diff --git a/player/op/channeltargets.go b/player/op/channeltargets.go index b0d1ea0..2597b68 100644 --- a/player/op/channeltargets.go +++ b/player/op/channeltargets.go @@ -2,21 +2,20 @@ package op import ( "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/types" "github.com/heucuva/optional" ) -type ChannelTargets[TPeriod period.Period] struct { +type ChannelTargets[TPeriod types.Period, TVolume types.Volume, TPanning types.Panning] struct { NoteAction optional.Value[note.Action] NoteCalcST optional.Value[note.Semitone] TargetPos optional.Value[sampling.Pos] - TargetInst optional.Value[*instrument.Instrument] + TargetInst optional.Value[instrument.InstrumentIntf] TargetPeriod optional.Value[TPeriod] TargetStoredSemitone optional.Value[note.Semitone] TargetNewNoteAction optional.Value[note.Action] - TargetVolume optional.Value[volume.Volume] + TargetVolume optional.Value[TVolume] } diff --git a/player/render/channel.go b/player/render/channel.go index ab37008..86daaeb 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -1,26 +1,33 @@ package render import ( + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/song" + channelfilter "github.com/gotracker/playback/voice/filter" "github.com/gotracker/playback/voice/render" - - "github.com/gotracker/gomixing/volume" ) +type ChannelIntf interface { + channelfilter.Applier + GetPremixVolume() volume.Volume +} + // Channel is the important bits to make output to a particular downmixing channel work -type Channel struct { +type Channel[TGlobalVolume, TMixingVolume song.Volume, TPanning song.Panning] struct { ChannelNum int Filter filter.Filter GetSampleRate func() period.Frequency - SetGlobalVolume func(volume.Volume) + SetGlobalVolume func(TGlobalVolume) error GetOPL2Chip func() render.OPL2Chip - ChannelVolume volume.Volume - LastGlobalVolume volume.Volume // this is the channel's version of the GlobalVolume + ChannelVolume TMixingVolume + LastGlobalVolume TGlobalVolume // this is the channel's version of the GlobalVolume } // ApplyFilter will apply the channel filter, if there is one. -func (oc *Channel) ApplyFilter(dry volume.Matrix) volume.Matrix { +func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) ApplyFilter(dry volume.Matrix) volume.Matrix { if dry.Channels == 0 { return volume.Matrix{} } @@ -33,12 +40,12 @@ func (oc *Channel) ApplyFilter(dry volume.Matrix) volume.Matrix { } // GetPremixVolume returns the premix volume of the output channel -func (oc *Channel) GetPremixVolume() volume.Volume { - return oc.LastGlobalVolume * oc.ChannelVolume +func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) GetPremixVolume() volume.Volume { + return oc.LastGlobalVolume.ToVolume() * oc.ChannelVolume.ToVolume() } // SetFilterEnvelopeValue updates the filter on the channel with the new envelope value -func (oc *Channel) SetFilterEnvelopeValue(envVal uint8) { +func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) SetFilterEnvelopeValue(envVal uint8) { if oc.Filter != nil { oc.Filter.UpdateEnv(envVal) } diff --git a/player/render/render.go b/player/render/render.go index 5349b1c..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,9 +40,7 @@ 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 type RowRender struct { diff --git a/player/state/active.go b/player/state/active.go deleted file mode 100644 index 937d9ae..0000000 --- a/player/state/active.go +++ /dev/null @@ -1,136 +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" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice" -) - -// Active is the active state of a channel -type Active[TPeriod period.Period] struct { - playback.ChannelState[TPeriod] - Voice voice.Voice - PeriodDelta period.Delta -} - -// Reset sets the active state to defaults -func (a *Active[TPeriod]) Reset() { - if v := a.Voice; v != nil { - v.Release() - a.Voice = nil - } - a.PeriodDelta = 0 - a.ChannelState.Reset() -} - -// Clone clones the active state so that various interfaces do not collide -func (a *Active[TPeriod]) Clone() *Active[TPeriod] { - var c Active[TPeriod] = *a - if a.Voice != nil { - c.Voice = a.Voice.Clone() - } - - 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[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], activeState *Active[TPeriod], pastNotes []*Active[TPeriod], details RenderDetails) []mixing.Data { - var mixData []mixing.Data - - centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) - - if activeState != nil { - if data := activeState.renderState(periodConverter, centerAheadPan, details); data != nil { - mixData = append(mixData, *data) - } - } - - for _, pn := range pastNotes { - if pn != nil { - if data := pn.renderState(periodConverter, centerAheadPan, details); data != nil { - mixData = append(mixData, *data) - } - } - } - - return mixData -} - -func (a *Active[TPeriod]) renderState(periodConverter period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details RenderDetails) *mixing.Data { - if a.Period.IsInvalid() { - return nil - } - - vol := a.GetVolume() - if vol == 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, vol) - voice.SetPos(ncv, a.Pos) - voice.SetPan(ncv, a.Pan) - - voice.SetPeriodDelta[TPeriod](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. - p := voice.GetFinalPeriod[TPeriod](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(periodConverter.GetSamplerAdd(p, 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 0e12200..0000000 --- a/player/state/channel.go +++ /dev/null @@ -1,416 +0,0 @@ -package state - -import ( - "github.com/gotracker/gomixing/mixing" - "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[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData]) error -} - -type NoteOp[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - Process(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData]) error -} - -type PeriodUpdateFunc[TPeriod period.Period] func(TPeriod) - -type SemitoneSetterFactory[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(note.Semitone, PeriodUpdateFunc[TPeriod]) NoteOp[TPeriod, TMemory, TChannelData] - -// ChannelState is the state of a single channel -type ChannelState[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { - activeState Active[TPeriod] - targetState playback.ChannelState[TPeriod] - prevState Active[TPeriod] - - ActiveEffects []playback.Effect - - s song.Data - txn ChannelDataTransaction[TPeriod, TMemory, TChannelData] - prevTxn ChannelDataTransaction[TPeriod, TMemory, TChannelData] - - SemitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData] - - StoredSemitone note.Semitone // from pattern, unmodified, current note - PortaTargetPeriod optional.Value[TPeriod] - Trigger optional.Value[NoteTrigger] - RetriggerCount uint8 - Memory *TMemory - freezePlayback bool - Semitone note.Semitone // from TargetSemitone, modified further, used in period calculations - UseTargetPeriod bool - periodOverride TPeriod - UsePeriodOverride bool - volumeActive bool - PanEnabled bool - NewNoteAction note.Action - - PastNotes *PastNotesProcessor[TPeriod] - RenderChannel *render.Channel - - PeriodConverter period.PeriodConverter[TPeriod] -} - -// WillTriggerOn returns true if a note will trigger on the tick specified -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) AdvanceRow(txn ChannelDataTransaction[TPeriod, TMemory, TChannelData]) { - cs.prevState = cs.activeState - cs.targetState = cs.activeState.ChannelState - 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[TPeriod, TMemory, TChannelData]) RenderRowTick(details RenderDetails, pastNotes []*Active[TPeriod]) ([]mixing.Data, error) { - if cs.PlaybackFrozen() { - return nil, nil - } - - mixData := RenderStatesTogether(cs.PeriodConverter, &cs.activeState, pastNotes, details) - - return mixData, nil -} - -// ResetStates resets the channel's internal states -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) ResetStates() { - cs.activeState.Reset() - cs.targetState.Reset() - cs.prevState.Reset() -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetActiveEffects() []playback.Effect { - return cs.ActiveEffects -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetActiveEffects(effects []playback.Effect) { - cs.ActiveEffects = effects -} - -// FreezePlayback suspends mixer progression on the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) FreezePlayback() { - cs.freezePlayback = true -} - -// UnfreezePlayback resumes mixer progression on the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) UnfreezePlayback() { - cs.freezePlayback = false -} - -// PlaybackFrozen returns true if the mixer progression for the channel is suspended -func (cs ChannelState[TPeriod, TMemory, TChannelData]) PlaybackFrozen() bool { - return cs.freezePlayback -} - -// ResetRetriggerCount sets the retrigger count to 0 -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) ResetRetriggerCount() { - cs.RetriggerCount = 0 -} - -// GetMemory returns the interface to the custom effect memory module -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetMemory() *TMemory { - return cs.Memory -} - -// SetMemory sets the custom effect memory interface -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetMemory(mem *TMemory) { - cs.Memory = mem -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetSongDataInterface(s song.Data) { - cs.s = s -} - -// GetChannelData returns the interface to the current channel song pattern data -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetChannelData() TChannelData { - if cs.txn == nil { - var empty TChannelData - return empty - } - - return cs.txn.GetChannelData() -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetData(cdata TChannelData) error { - if cs.txn == nil { - return nil - } - - return cs.txn.SetData(cdata, cs.s, cs) -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTxn() ChannelDataTransaction[TPeriod, TMemory, TChannelData] { - return cs.txn -} - -// GetPortaTargetPeriod returns the current target portamento (to note) sampler period -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPortaTargetPeriod() TPeriod { - if p, ok := cs.PortaTargetPeriod.Get(); ok { - return p - } - var empty TPeriod - return empty -} - -// SetPortaTargetPeriod sets the current target portamento (to note) sampler period -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPortaTargetPeriod(period TPeriod) { - if !period.IsInvalid() { - cs.PortaTargetPeriod.Set(period) - } else { - cs.PortaTargetPeriod.Reset() - } -} - -// GetTargetPeriod returns the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriodOverride() TPeriod { - return cs.periodOverride -} - -// SetTargetPeriod sets the soon-to-be-committed sampler period (when the note retriggers) -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPeriodOverride(period TPeriod) { - cs.periodOverride = period - cs.UsePeriodOverride = true -} - -// SetPeriodDelta sets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetPeriodDelta(delta period.Delta) { - cs.activeState.PeriodDelta = delta -} - -// GetPeriodDelta gets the vibrato (ephemeral) delta sampler period -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPeriodDelta() period.Delta { - return cs.activeState.PeriodDelta -} - -// SetVolumeActive enables or disables the sample of the instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetVolumeActive(on bool) { - cs.volumeActive = on -} - -// SetInstrument sets the interface to the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetInstrument(inst *instrument.Instrument) { - prevVoice := cs.activeState.Voice - doClearPrev := false - cs.activeState.Instrument = inst - if inst != nil { - if inst == cs.prevState.Instrument { - cs.activeState.Voice = cs.prevState.Voice - } else { - doClearPrev = true - cs.activeState.Voice = voiceImpl.New[TPeriod](cs.PeriodConverter, inst, cs.RenderChannel) - } - } else { - doClearPrev = true - cs.activeState.Voice = nil - } - - if doClearPrev && prevVoice != nil { - prevVoice.Release() - prevVoice.Fadeout() - } -} - -// GetVoice returns the active voice interface -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetVoice() voice.Voice { - return cs.activeState.Voice -} - -// GetPrevVoice returns the interface to the last row's active voice -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPrevVoice() voice.Voice { - return cs.prevState.Voice -} - -// GetNoteSemitone returns the note semitone for the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetNoteSemitone() note.Semitone { - return cs.StoredSemitone -} - -// SetNotePlayTick sets the tick on which the note will retrigger -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) GetRetriggerCount() uint8 { - return cs.RetriggerCount -} - -// SetRetriggerCount sets the current count of the retrigger counter -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) SetPanEnabled(on bool) { - cs.PanEnabled = on -} - -// SetTargetSemitone sets the target semitone for the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetTargetSemitone(st note.Semitone) { - if cs.txn != nil { - cs.txn.AddNoteOp(cs.SemitoneSetterFactory(st, func(p TPeriod) { - cs.GetTargetState().Period = p - })) - } -} - -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) SetStoredSemitone(st note.Semitone) { - cs.StoredSemitone = st -} - -// SetRenderChannel sets the output channel for the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetRenderChannel(outputCh *render.Channel) { - cs.RenderChannel = outputCh -} - -// GetRenderChannel returns the output channel for the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetRenderChannel() *render.Channel { - return cs.RenderChannel -} - -// SetGlobalVolume sets the last-known global volume on the channel -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) SetChannelVolume(cv volume.Volume) { - cs.RenderChannel.ChannelVolume = cv -} - -// GetChannelVolume gets the channel volume on the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetChannelVolume() volume.Volume { - return cs.RenderChannel.ChannelVolume -} - -// SetEnvelopePosition sets the envelope position for the active instrument -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetEnvelopePosition(ticks int) { - if nc := cs.GetVoice(); nc != nil { - voice.SetVolumeEnvelopePosition(nc, ticks) - voice.SetPanEnvelopePosition(nc, ticks) - voice.SetPitchEnvelopePosition[TPeriod](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[TPeriod, 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[TPeriod, 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[TPeriod, TMemory, TChannelData]) SetNewNoteAction(nna note.Action) { - cs.NewNoteAction = nna -} - -// GetNewNoteAction gets the New-Note Action on the channel -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetNewNoteAction() note.Action { - return cs.NewNoteAction -} - -// SetVolumeEnvelopeEnable sets the enable flag on the active volume envelope -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) SetVolumeEnvelopeEnable(enabled bool) { - voice.EnableVolumeEnvelope(cs.activeState.Voice, enabled) -} - -// SetPanningEnvelopeEnable sets the enable flag on the active panning envelope -func (cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) SetPitchEnvelopeEnable(enabled bool) { - voice.EnablePitchEnvelope[TPeriod](cs.activeState.Voice, enabled) -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetUseTargetPeriod() bool { - return cs.UseTargetPeriod -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetPreviousState() playback.ChannelState[TPeriod] { - return cs.prevState.ChannelState -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetActiveState() *playback.ChannelState[TPeriod] { - return &cs.activeState.ChannelState -} - -func (cs *ChannelState[TPeriod, TMemory, TChannelData]) GetTargetState() *playback.ChannelState[TPeriod] { - return &cs.targetState -} diff --git a/player/state/channel_transaction.go b/player/state/channel_transaction.go deleted file mode 100644 index b1dfb0e..0000000 --- a/player/state/channel_transaction.go +++ /dev/null @@ -1,182 +0,0 @@ -package state - -import ( - "github.com/gotracker/playback" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/op" - "github.com/gotracker/playback/song" -) - -type ChannelDataTransaction[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] interface { - GetChannelData() TChannelData - SetData(data TChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory, TChannelData]) error - - CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - - CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error - - AddVolOp(op VolOp[TPeriod, TMemory, TChannelData]) - AddNoteOp(op NoteOp[TPeriod, TMemory, TChannelData]) -} - -type ChannelDataConverter[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] func(out *op.ChannelTargets[TPeriod], data TChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory, TChannelData]) error - -type ChannelDataTxnHelper[TPeriod period.Period, TMemory any, TChannelData song.ChannelData] struct { - Data TChannelData - Targeter playback.ChannelTargeter[TPeriod, TMemory, TChannelData] - - op.ChannelTargets[TPeriod] - - VolOps []VolOp[TPeriod, TMemory, TChannelData] - NoteOps []NoteOp[TPeriod, TMemory, TChannelData] -} - -func NewChannelDataTxn[TPeriod period.Period, TMemory any, TChannelData song.ChannelData](targeter playback.ChannelTargeter[TPeriod, TMemory, TChannelData]) ChannelDataTransaction[TPeriod, TMemory, TChannelData] { - return &ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]{ - Targeter: targeter, - } -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) GetChannelData() TChannelData { - return d.Data -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) SetData(cd TChannelData, s song.Data, cs *ChannelState[TPeriod, TMemory, TChannelData]) error { - d.Data = cd - return d.Targeter(&d.ChannelTargets, cd, s, cs) -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPreRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { - effects := playback.GetEffects[TPeriod, TMemory, TChannelData](cs.GetMemory(), d.Data) - cs.SetActiveEffects(effects) - onEff := p.GetOnEffect() - for _, e := range effects { - if onEff != nil { - onEff(e) - } - if err := playback.EffectPreStart[TPeriod, TMemory](e, cs, p); err != nil { - return err - } - } - - return nil -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { - target := cs.GetTargetState() - if pos, ok := d.TargetPos.Get(); ok { - target.Pos = pos - } - - if inst, ok := d.TargetInst.Get(); ok { - target.Instrument = inst - } - - if period, ok := d.TargetPeriod.Get(); ok { - target.Period = period - cs.SetPortaTargetPeriod(period) - } - - if st, ok := d.TargetStoredSemitone.Get(); ok { - cs.SetStoredSemitone(st) - } - - if nna, ok := d.TargetNewNoteAction.Get(); ok { - cs.SetNewNoteAction(nna) - } - - if v, ok := d.TargetVolume.Get(); ok { - cs.GetActiveState().SetVolume(v) - target.SetVolume(v) - } - - na, targetTick := d.NoteAction.Get() - cs.UseTargetPeriod = targetTick - cs.SetNotePlayTick(targetTick, na, 0) - - if st, ok := d.NoteCalcST.Get(); ok { - d.AddNoteOp(semitoneSetterFactory(st, func(p TPeriod) { - target.Period = p - })) - } - - return nil -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPostRow(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { - return nil -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPreTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, 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[TPeriod, TMemory, TChannelData]) CommitTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, TMemory, TChannelData]) error { - for _, e := range cs.GetActiveEffects() { - if err := playback.DoEffect[TPeriod, TMemory](e, cs, p, currentTick, lastTick); err != nil { - return err - } - } - - return nil -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) CommitPostTick(p playback.Playback, cs *ChannelState[TPeriod, TMemory, TChannelData], currentTick int, lastTick bool, semitoneSetterFactory SemitoneSetterFactory[TPeriod, 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[TPeriod, TMemory, TChannelData]) AddVolOp(op VolOp[TPeriod, TMemory, TChannelData]) { - d.VolOps = append(d.VolOps, op) -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) ProcessVolOps(p playback.Playback, cs *ChannelState[TPeriod, 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[TPeriod, TMemory, TChannelData]) AddNoteOp(op NoteOp[TPeriod, TMemory, TChannelData]) { - d.NoteOps = append(d.NoteOps, op) -} - -func (d *ChannelDataTxnHelper[TPeriod, TMemory, TChannelData]) ProcessNoteOps(p playback.Playback, cs *ChannelState[TPeriod, 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 e1c5a7e..0000000 --- a/player/state/pastnotes.go +++ /dev/null @@ -1,127 +0,0 @@ -package state - -import ( - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" - "github.com/heucuva/optional" -) - -type pastNote[TPeriod period.Period] struct { - ch int - activeState *Active[TPeriod] -} - -func (pn *pastNote[TPeriod]) IsValid() bool { - return pn.activeState.Voice != nil && !pn.activeState.Voice.IsDone() -} - -type PastNotesProcessor[TPeriod period.Period] struct { - order []pastNote[TPeriod] - max optional.Value[int] - maxPerCh optional.Value[int] -} - -func (p *PastNotesProcessor[TPeriod]) Add(ch int, data *Active[TPeriod]) { - 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[TPeriod]{ - ch: ch, - activeState: data, - } - - p.order = append(p.order, cl) -} - -func (p *PastNotesProcessor[TPeriod]) 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[TPeriod]) Update() { - var nl []pastNote[TPeriod] - for _, o := range p.order { - if !o.IsValid() { - o.activeState.Reset() - continue - } - - nl = append(nl, o) - } - p.order = nl -} - -func (p *PastNotesProcessor[TPeriod]) GetNotesForChannel(ch int) []*Active[TPeriod] { - var pastNotes []*Active[TPeriod] - 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[TPeriod]) SetMax(max int) { - p.max.Set(max) -} - -func (p *PastNotesProcessor[TPeriod]) ClearMax() { - p.max.Reset() -} - -func (p *PastNotesProcessor[TPeriod]) SetMaxPerChannel(max int) { - p.maxPerCh.Set(max) -} - -func (p *PastNotesProcessor[TPeriod]) ClearMaxPerChannel() { - p.maxPerCh.Reset() -} diff --git a/player/state/render/details.go b/player/state/render/details.go new file mode 100644 index 0000000..7fea696 --- /dev/null +++ b/player/state/render/details.go @@ -0,0 +1,15 @@ +package render + +import ( + "time" + + "github.com/gotracker/gomixing/mixing" +) + +type Details struct { + Mix *mixing.Mixer + Panmixer mixing.PanMixer + SamplerSpeed float32 + Samples int + Duration time.Duration +} diff --git a/player/state/renderstate.go b/player/state/renderstate.go index 8ad5667..83c4412 100644 --- a/player/state/renderstate.go +++ b/player/state/renderstate.go @@ -2,10 +2,12 @@ package state import ( "time" + + "github.com/gotracker/playback/player/state/render" ) type RowRenderState struct { - RenderDetails + render.Details TicksThisRow int CurrentTick int @@ -20,13 +22,13 @@ func (r RowRenderState) GetCurrentTick() int { } func (r RowRenderState) GetSamplerSpeed() float32 { - return r.RenderDetails.SamplerSpeed + return r.Details.SamplerSpeed } func (r RowRenderState) GetDuration() time.Duration { - return r.RenderDetails.Duration + return r.Details.Duration } func (r RowRenderState) GetSamples() int { - return r.RenderDetails.Samples + return r.Details.Samples } diff --git a/player/tracker.go b/player/tracker.go index 9c6eed1..1b62d81 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -2,7 +2,6 @@ package player import ( "errors" - "os" "time" "github.com/gotracker/gomixing/mixing" @@ -11,8 +10,12 @@ import ( "github.com/gotracker/playback/output" "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/player/render" "github.com/gotracker/playback/player/sampler" + "github.com/gotracker/playback/song" + "github.com/gotracker/playback/tracing" voiceRender "github.com/gotracker/playback/voice/render" ) @@ -24,11 +27,7 @@ type Premixable interface { // Tracker is an extensible music tracker type Tracker struct { BaseClockRate period.Frequency - PreTickable PreTickable - Tickable Tickable - PostTickable PostTickable Premixable Premixable - Traceable Traceable s *sampler.Sampler opl2 voiceRender.OPL2Chip @@ -37,19 +36,23 @@ type Tracker struct { mixerVolume volume.Volume ignoreUnknownEffect feature.IgnoreUnknownEffect - tracingFile *os.File - tracingState tracingState - outputChannels map[int]*render.Channel + outputChannels map[int]render.ChannelIntf + + tracing.Tracing + + M machine.MachineTicker + LongChannelOutput bool +} + +func (t *Tracker) SetupMachine(s song.Data, us settings.UserSettings) error { + var err error + t.M, err = machine.NewMachine(s, us) + return err } func (t *Tracker) Close() { - if t.tracingState.c != nil { - close(t.tracingState.c) - } - if t.tracingFile != nil { - t.tracingFile.Close() - } - t.tracingState.wg.Wait() + t.Trace("Close") + t.Tracing.Close() } // Update runs processing on the tracker, producing premixed sound data @@ -66,68 +69,16 @@ func (t *Tracker) Update(deltaTime time.Duration, out chan<- *output.PremixData) 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) + defer t.OutputTraces() + + var premix *output.PremixData + var err error + if t.M != nil { + premix, err = t.M.Tick(t.s) + if err != nil { + return nil, err } - 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 := DoPreTick(t.PreTickable); err != nil { - return nil, err - } - - t.OutputTraces() - - if err := DoTick(t.Tickable); err != nil { - return nil, err - } - - if err := DoPostTick(t.PostTickable); err != nil { - return nil, err - } - - premix, err := t.Premixable.GetPremixData() - if err != nil { - return nil, err } if t.opl2 != nil { @@ -148,9 +99,29 @@ func (t *Tracker) renderTick() (*output.PremixData, error) { mv := premix.MixerVolume premix.MixerVolume /= (mv + 1) } + return premix, nil } +// GetRenderChannel returns the output channel for the provided index `ch` +func (t *Tracker) GetRenderChannel(ch int, init func(ch int) render.ChannelIntf) render.ChannelIntf { + if t.outputChannels == nil { + t.outputChannels = make(map[int]render.ChannelIntf) + } + + 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) 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) @@ -205,6 +176,7 @@ func (t *Tracker) GetGlobalVolume() volume.Volume { // SetGlobalVolume sets the global volume to the specified `vol` value func (t *Tracker) SetGlobalVolume(vol volume.Volume) { + t.TraceValueChange("SetGlobalVolume", t.globalVolume, vol) t.globalVolume = vol } @@ -215,6 +187,7 @@ func (t *Tracker) GetMixerVolume() volume.Volume { // SetMixerVolume sets the mixer volume to the specified `vol` value func (t *Tracker) SetMixerVolume(vol volume.Volume) { + t.TraceValueChange("SetMixerVolume", t.mixerVolume, vol) t.mixerVolume = vol } @@ -224,18 +197,23 @@ func (t *Tracker) IgnoreUnknownEffect() bool { } // Configure sets specified features -func (t *Tracker) Configure(features []feature.Feature) error { +func (t *Tracker) Configure(features []feature.Feature) (settings.UserSettings, error) { + us := settings.UserSettings{ + LongChannelOutput: true, + EnableNewNoteActions: true, + } + for _, feat := range features { switch f := feat.(type) { case feature.IgnoreUnknownEffect: t.ignoreUnknownEffect = f + us.IgnoreUnknownEffect = f.Enabled case feature.EnableTracing: - var err error - t.tracingFile, err = os.Create(f.Filename) - if err != nil { - return err + if err := t.EnableTracing(f.Filename); err != nil { + return us, err } + us.Tracer = &t.Tracing } } - return nil + return us, nil } diff --git a/player/tracker_tracing.go b/player/tracker_tracing.go deleted file mode 100644 index 1ead220..0000000 --- a/player/tracker_tracing.go +++ /dev/null @@ -1,172 +0,0 @@ -package player - -import ( - "errors" - "fmt" - "io" - "strings" - "sync" - "text/tabwriter" - - ansi "github.com/fatih/color" -) - -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 - rowColors []*ansi.Color - 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) { - tt.AddRowColor(nil, cols...) -} - -func (tt *TracingTable) AddRowColor(c []ansi.Attribute, cols ...any) { - var cc *ansi.Color - if len(c) > 0 { - cc = ansi.Set(c...) - cc.EnableColor() - } - tt.rowColors = append(tt.rowColors, cc) - for i, col := range cols { - tc := tt.cols[i] - tc.rows = append(tc.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 -} - -func (tt *TracingTable) WriteOut(w io.Writer) error { - var sb strings.Builder - tw := tabwriter.NewWriter(&sb, 1, 1, 1, ' ', 0) - if err := tt.Fprintln(tw, "\t", true); err != nil { - return err - } - if err := tw.Flush(); err != nil { - return err - } - - for r, txt := range strings.Split(sb.String(), "\n") { - var rc *ansi.Color - if rcr := r - 1; rcr >= 0 && rcr < len(tt.rowColors) { - rc = tt.rowColors[rcr] - } - if rc != nil { - txt = rc.Sprint(txt) - } - if _, err := fmt.Fprintln(w, txt); 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/config.go b/player/voice/config.go deleted file mode 100644 index 5198ce2..0000000 --- a/player/voice/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package voice - -type Vol0OptimizationConfiguration struct { - Enabled bool - MaxTicksAt0 int -} diff --git a/player/voice/opl2.go b/player/voice/opl2.go deleted file mode 100644 index fa1f180..0000000 --- a/player/voice/opl2.go +++ /dev/null @@ -1,300 +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[TPeriod period.Period] interface { - voice.Voice - voice.FreqModulator[TPeriod] - voice.AmpModulator - voice.VolumeEnveloper - voice.PitchEnveloper[TPeriod] -} - -// OPL2Registers is a set of OPL operator configurations -type OPL2Registers component.OPL2Registers - -// OPLConfiguration is the information needed to configure an OPL2 voice -type OPLConfiguration[TPeriod period.Period] struct { - Chip render.OPL2Chip - Channel int - SampleRate period.Frequency - InitialVolume volume.Volume - InitialPeriod TPeriod - AutoVibrato voice.AutoVibrato - Data instrument.Data -} - -// == the actual opl2 voice == - -type opl2Voice[TPeriod period.Period] struct { - sampleRate period.Frequency - initialVolume volume.Volume - - active bool - keyOn bool - prevKeyOn bool - - fadeoutMode fadeout.Mode - - o component.OPL2[TPeriod] - amp component.AmpModulator - freq component.FreqModulator[TPeriod] - volEnv component.VolumeEnvelope - pitchEnv component.PitchEnvelope[TPeriod] -} - -// NewOPL2 creates a new OPL2 voice -func NewOPL2[TPeriod period.Period](config OPLConfiguration[TPeriod]) voice.Voice { - v := opl2Voice[TPeriod]{ - sampleRate: config.SampleRate, - 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() - v.pitchEnv.SetEnabled(false) - v.pitchEnv.Reset() - 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.SampleRate) - 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.ResetAutoVibratoAndSweep(config.AutoVibrato.Sweep) - } - - return &v -} - -// == Controller == - -func (v *opl2Voice[TPeriod]) Attack() { - v.keyOn = true - v.amp.Attack() - v.freq.ResetAutoVibrato() - v.SetVolumeEnvelopePosition(0) - v.SetPitchEnvelopePosition(0) - -} - -func (v *opl2Voice[TPeriod]) Release() { - v.keyOn = false - v.amp.Release() - v.o.Release() -} - -func (v *opl2Voice[TPeriod]) Fadeout() { - switch v.fadeoutMode { - case fadeout.ModeAlwaysActive: - v.amp.Fadeout() - case fadeout.ModeOnlyIfVolEnvActive: - if v.IsVolumeEnvelopeEnabled() { - v.amp.Fadeout() - } - } -} - -func (v *opl2Voice[TPeriod]) IsKeyOn() bool { - return v.keyOn -} - -func (v *opl2Voice[TPeriod]) IsFadeout() bool { - return v.amp.IsFadeoutEnabled() -} - -func (v *opl2Voice[TPeriod]) IsDone() bool { - if !v.amp.IsFadeoutEnabled() { - return false - } - return v.amp.GetFadeoutVolume() <= 0 -} - -// == FreqModulator == - -func (v *opl2Voice[TPeriod]) SetPeriod(period TPeriod) { - v.freq.SetPeriod(period) -} - -func (v *opl2Voice[TPeriod]) GetPeriod() TPeriod { - return v.freq.GetPeriod() -} - -func (v *opl2Voice[TPeriod]) SetPeriodDelta(delta period.Delta) { - v.freq.SetDelta(delta) -} - -func (v *opl2Voice[TPeriod]) GetPeriodDelta() period.Delta { - return v.freq.GetDelta() -} - -func (v *opl2Voice[TPeriod]) GetFinalPeriod() TPeriod { - p := v.freq.GetFinalPeriod() - if v.IsPitchEnvelopeEnabled() { - d := v.GetCurrentPitchEnvelope() - p = period.AddDelta(p, d) - } - return p -} - -// == AmpModulator == - -func (v *opl2Voice[TPeriod]) SetVolume(vol volume.Volume) { - if vol == volume.VolumeUseInstVol { - vol = v.initialVolume - } - v.amp.SetVolume(vol) -} - -func (v *opl2Voice[TPeriod]) GetVolume() volume.Volume { - return v.amp.GetVolume() -} - -func (v *opl2Voice[TPeriod]) GetFinalVolume() volume.Volume { - vol := v.amp.GetFinalVolume() - if v.IsVolumeEnvelopeEnabled() { - vol *= v.GetCurrentVolumeEnvelope() - } - return vol -} - -// == VolumeEnveloper == - -func (v *opl2Voice[TPeriod]) EnableVolumeEnvelope(enabled bool) { - v.volEnv.SetEnabled(enabled) -} - -func (v *opl2Voice[TPeriod]) IsVolumeEnvelopeEnabled() bool { - return v.volEnv.IsEnabled() -} - -func (v *opl2Voice[TPeriod]) GetCurrentVolumeEnvelope() volume.Volume { - if v.volEnv.IsEnabled() { - return v.volEnv.GetCurrentValue() - } - return 1 -} - -func (v *opl2Voice[TPeriod]) SetVolumeEnvelopePosition(pos int) { - if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == PitchEnveloper == - -func (v *opl2Voice[TPeriod]) EnablePitchEnvelope(enabled bool) { - v.pitchEnv.SetEnabled(enabled) -} - -func (v *opl2Voice[TPeriod]) IsPitchEnvelopeEnabled() bool { - return v.pitchEnv.IsEnabled() -} - -func (v *opl2Voice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { - if v.pitchEnv.IsEnabled() { - return v.pitchEnv.GetCurrentValue() - } - var empty period.Delta - return empty -} - -func (v *opl2Voice[TPeriod]) SetPitchEnvelopePosition(pos int) { - if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == required function interfaces == - -func (v *opl2Voice[TPeriod]) 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[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { - return volume.Matrix{} -} - -func (v *opl2Voice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { - return nil -} - -func (v *opl2Voice[TPeriod]) Clone() voice.Voice { - o := *v - return &o -} - -func (v *opl2Voice[TPeriod]) StartTransaction() voice.Transaction[TPeriod] { - t := txn[TPeriod]{ - Voice: v, - } - return &t -} - -func (v *opl2Voice[TPeriod]) SetActive(active bool) { - v.active = active -} - -func (v *opl2Voice[TPeriod]) IsActive() bool { - return v.active -} diff --git a/player/voice/pcm.go b/player/voice/pcm.go deleted file mode 100644 index 3b8248c..0000000 --- a/player/voice/pcm.go +++ /dev/null @@ -1,481 +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[TPeriod period.Period] interface { - voice.Voice - voice.Positioner - voice.FreqModulator[TPeriod] - voice.AmpModulator - voice.PanModulator - voice.VolumeEnveloper - voice.PitchEnveloper[TPeriod] - voice.PanEnveloper - voice.FilterEnveloper -} - -// PCMConfiguration is the information needed to configure an PCM2 voice -type PCMConfiguration[TPeriod period.Period] struct { - SampleRate period.Frequency - InitialVolume volume.Volume - InitialPeriod TPeriod - AutoVibrato voice.AutoVibrato - Data instrument.Data - OutputFilter voice.FilterApplier - VoiceFilter filter.Filter - PluginFilter filter.Filter - Vol0Optimization Vol0OptimizationConfiguration -} - -// == the actual pcm voice == - -type pcmVoice[TPeriod period.Period] struct { - sampleRate 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[TPeriod] - pan component.PanModulator - volEnv component.VolumeEnvelope - pitchEnv component.PitchEnvelope[TPeriod] - panEnv component.PanEnvelope - filterEnv component.FilterEnvelope - vol0Opt component.Vol0Optimization - done bool - - periodConverter period.PeriodConverter[TPeriod] -} - -// NewPCM creates a new PCM voice -func NewPCM[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], config PCMConfiguration[TPeriod]) voice.Voice { - v := pcmVoice[TPeriod]{ - sampleRate: config.SampleRate, - initialVolume: config.InitialVolume, - outputFilter: config.OutputFilter, - voiceFilter: config.VoiceFilter, - pluginFilter: config.PluginFilter, - active: true, - periodConverter: periodConverter, - } - - 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.fadeoutMode = d.FadeOut.Mode - //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.Init(&d.VolEnv) - v.pitchEnv.SetEnabled(d.PitchFiltEnv.Enabled) - v.pitchEnv.Init(&d.PitchFiltEnv) - v.panEnv.SetEnabled(d.PanEnv.Enabled) - v.panEnv.Init(&d.PanEnv) - v.filterEnv.SetEnabled(d.PitchFiltEnv.Enabled) - v.filterEnv.Init(&d.PitchFiltEnv) - v.channels = d.Sample.Channels() - } - - switch v.fadeoutMode { - case fadeout.ModeAlwaysActive: - v.amp.SetFadeoutEnabled(true) - case fadeout.ModeOnlyIfVolEnvActive: - v.amp.SetFadeoutEnabled(v.volEnv.IsEnabled()) - default: - } - - 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.ResetAutoVibratoAndSweep(config.AutoVibrato.Sweep) - } - - v.vol0Opt.SetEnabled(config.Vol0Optimization.Enabled) - v.vol0Opt.Init(config.Vol0Optimization.MaxTicksAt0) - - return &v -} - -// == Controller == - -func (v *pcmVoice[TPeriod]) Attack() { - v.keyOn = true - v.vol0Opt.Reset() - 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[TPeriod]) Release() { - v.keyOn = false - v.amp.Release() - v.sampler.Release() -} - -func (v *pcmVoice[TPeriod]) 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[TPeriod]) IsKeyOn() bool { - return v.keyOn -} - -func (v *pcmVoice[TPeriod]) IsFadeout() bool { - return v.amp.IsFadeoutEnabled() -} - -func (v *pcmVoice[TPeriod]) IsDone() bool { - if v.done { - return true - } - - if v.amp.IsFadeoutEnabled() { - return v.amp.GetFadeoutVolume() <= 0 - } - - return v.vol0Opt.IsDone() -} - -// == SampleStream == - -func (v *pcmVoice[TPeriod]) 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[TPeriod]) SetPos(pos sampling.Pos) { - v.sampler.SetPos(pos) -} - -func (v *pcmVoice[TPeriod]) GetPos() sampling.Pos { - return v.sampler.GetPos() -} - -// == FreqModulator == - -func (v *pcmVoice[TPeriod]) SetPeriod(period TPeriod) { - v.freq.SetPeriod(period) -} - -func (v *pcmVoice[TPeriod]) GetPeriod() TPeriod { - return v.freq.GetPeriod() -} - -func (v *pcmVoice[TPeriod]) SetPeriodDelta(delta period.Delta) { - v.freq.SetDelta(delta) -} - -func (v *pcmVoice[TPeriod]) GetPeriodDelta() period.Delta { - return v.freq.GetDelta() -} - -func (v *pcmVoice[TPeriod]) GetFinalPeriod() TPeriod { - p := v.freq.GetFinalPeriod() - if v.IsPitchEnvelopeEnabled() { - delta := v.GetCurrentPitchEnvelope() - p = period.AddDelta(p, delta) - } - return p -} - -// == AmpModulator == - -func (v *pcmVoice[TPeriod]) SetVolume(vol volume.Volume) { - if vol == volume.VolumeUseInstVol { - vol = v.initialVolume - } - v.amp.SetVolume(vol) -} - -func (v *pcmVoice[TPeriod]) GetVolume() volume.Volume { - return v.amp.GetVolume() -} - -func (v *pcmVoice[TPeriod]) GetFinalVolume() volume.Volume { - vol := v.amp.GetFinalVolume() - if v.IsVolumeEnvelopeEnabled() { - vol *= v.GetCurrentVolumeEnvelope() - } - return vol -} - -// == PanModulator == - -func (v *pcmVoice[TPeriod]) SetPan(pan panning.Position) { - v.pan.SetPan(pan) -} - -func (v *pcmVoice[TPeriod]) GetPan() panning.Position { - return v.pan.GetPan() -} - -func (v *pcmVoice[TPeriod]) GetFinalPan() panning.Position { - p := v.pan.GetFinalPan() - if v.IsPanEnvelopeEnabled() { - p = pan.CalculateCombinedPanning(p, v.panEnv.GetCurrentValue()) - } - return p -} - -// == VolumeEnveloper == - -func (v *pcmVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) { - v.volEnv.SetEnabled(enabled) -} - -func (v *pcmVoice[TPeriod]) IsVolumeEnvelopeEnabled() bool { - return v.volEnv.IsEnabled() -} - -func (v *pcmVoice[TPeriod]) GetCurrentVolumeEnvelope() volume.Volume { - if v.volEnv.IsEnabled() { - return v.volEnv.GetCurrentValue() - } - return 1 -} - -func (v *pcmVoice[TPeriod]) SetVolumeEnvelopePosition(pos int) { - if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == PitchEnveloper == - -func (v *pcmVoice[TPeriod]) EnablePitchEnvelope(enabled bool) { - v.pitchEnv.SetEnabled(enabled) -} - -func (v *pcmVoice[TPeriod]) IsPitchEnvelopeEnabled() bool { - if v.pitchAndFilterEnvShared && v.filterEnvActive { - return false - } - return v.pitchEnv.IsEnabled() -} - -func (v *pcmVoice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { - if v.pitchEnv.IsEnabled() { - return v.pitchEnv.GetCurrentValue() - } - var empty period.Delta - return empty -} - -func (v *pcmVoice[TPeriod]) SetPitchEnvelopePosition(pos int) { - if !v.pitchAndFilterEnvShared || !v.filterEnvActive { - if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } - } -} - -// == FilterEnveloper == - -func (v *pcmVoice[TPeriod]) 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[TPeriod]) IsFilterEnvelopeEnabled() bool { - if v.pitchAndFilterEnvShared && !v.filterEnvActive { - return false - } - return v.filterEnv.IsEnabled() -} - -func (v *pcmVoice[TPeriod]) GetCurrentFilterEnvelope() uint8 { - return v.filterEnv.GetCurrentValue() -} - -func (v *pcmVoice[TPeriod]) SetFilterEnvelopePosition(pos int) { - if !v.pitchAndFilterEnvShared || v.filterEnvActive { - if doneCB := v.filterEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } - } -} - -// == PanEnveloper == - -func (v *pcmVoice[TPeriod]) EnablePanEnvelope(enabled bool) { - v.panEnv.SetEnabled(enabled) -} - -func (v *pcmVoice[TPeriod]) IsPanEnvelopeEnabled() bool { - return v.panEnv.IsEnabled() -} - -func (v *pcmVoice[TPeriod]) GetCurrentPanEnvelope() panning.Position { - return v.panEnv.GetCurrentValue() -} - -func (v *pcmVoice[TPeriod]) SetPanEnvelopePosition(pos int) { - if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { - doneCB(v) - } -} - -// == required function interfaces == - -func (v *pcmVoice[TPeriod]) Advance(tickDuration time.Duration) { - 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) - } - - v.vol0Opt.ObserveVolume(v.GetFinalVolume()) - v.prevKeyOn = v.keyOn -} - -func (v *pcmVoice[TPeriod]) GetSampler(samplerRate float32) sampling.Sampler { - p := v.GetFinalPeriod() - samplerAdd := float32(v.periodConverter.GetSamplerAdd(p, float64(samplerRate))) - o := component.OutputFilter{ - Input: v, - Output: v.outputFilter, - } - return sampling.NewSampler(&o, v.GetPos(), samplerAdd) -} - -func (v *pcmVoice[TPeriod]) Clone() voice.Voice { - p := pcmVoice[TPeriod]{ - sampleRate: v.sampleRate, - initialVolume: v.initialVolume, - outputFilter: v.outputFilter, - fadeoutMode: v.fadeoutMode, - channels: v.channels, - active: true, - keyOn: false, - prevKeyOn: false, - pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, - filterEnvActive: v.filterEnvActive, - sampler: v.sampler.Clone(), - amp: v.amp, - freq: v.freq, - pan: v.pan, - volEnv: v.volEnv.Clone(), - pitchEnv: v.pitchEnv.Clone(), - panEnv: v.panEnv.Clone(), - filterEnv: v.filterEnv.Clone(), - vol0Opt: v.vol0Opt.Clone(), - done: false, - periodConverter: v.periodConverter, - } - - if v.voiceFilter != nil { - p.voiceFilter = v.voiceFilter.Clone() - } - if v.pluginFilter != nil { - p.pluginFilter = v.pluginFilter.Clone() - } - return &p -} - -func (v *pcmVoice[TPeriod]) StartTransaction() voice.Transaction[TPeriod] { - t := txn[TPeriod]{ - Voice: v, - } - return &t -} - -func (v *pcmVoice[TPeriod]) SetActive(active bool) { - v.active = active -} - -func (v *pcmVoice[TPeriod]) IsActive() bool { - return v.active -} diff --git a/player/voice/transaction.go b/player/voice/transaction.go deleted file mode 100644 index 197cf13..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[TPeriod period.Period] struct { - cancelled bool - Voice voice.Voice - - active optional.Value[bool] - playing optional.Value[playingMode] - fadeout optional.Value[struct{}] - period optional.Value[TPeriod] - 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[TPeriod]) SetActive(active bool) { - t.active.Set(active) -} - -func (t *txn[TPeriod]) IsPendingActive() (bool, bool) { - return t.active.Get() -} - -func (t *txn[TPeriod]) IsCurrentlyActive() bool { - return t.Voice.IsActive() -} - -// Attack sets the playing mode to Attack -func (t *txn[TPeriod]) Attack() { - t.playing.Set(playingModeAttack) -} - -// Release sets the playing mode to Release -func (t *txn[TPeriod]) Release() { - t.playing.Set(playingModeRelease) -} - -// Fadeout activates the voice's fade-out function -func (t *txn[TPeriod]) Fadeout() { - t.fadeout.Set(struct{}{}) -} - -// SetPeriod sets the period -func (t *txn[TPeriod]) SetPeriod(period TPeriod) { - t.period.Set(period) -} - -func (t *txn[TPeriod]) GetPendingPeriod() (TPeriod, bool) { - return t.period.Get() -} - -func (t *txn[TPeriod]) GetCurrentPeriod() TPeriod { - return voice.GetPeriod[TPeriod](t.Voice) -} - -// SetPeriodDelta sets the period delta -func (t *txn[TPeriod]) SetPeriodDelta(delta period.Delta) { - t.periodDelta.Set(delta) -} - -func (t *txn[TPeriod]) GetPendingPeriodDelta() (period.Delta, bool) { - return t.periodDelta.Get() -} - -func (t *txn[TPeriod]) GetCurrentPeriodDelta() period.Delta { - return voice.GetPeriodDelta[TPeriod](t.Voice) -} - -// SetVolume sets the volume -func (t *txn[TPeriod]) SetVolume(vol volume.Volume) { - t.vol.Set(vol) -} - -func (t *txn[TPeriod]) GetPendingVolume() (volume.Volume, bool) { - return t.vol.Get() -} - -func (t *txn[TPeriod]) GetCurrentVolume() volume.Volume { - return voice.GetVolume(t.Voice) -} - -// SetPos sets the position -func (t *txn[TPeriod]) SetPos(pos sampling.Pos) { - t.pos.Set(pos) -} - -func (t *txn[TPeriod]) GetPendingPos() (sampling.Pos, bool) { - return t.pos.Get() -} - -func (t *txn[TPeriod]) GetCurrentPos() sampling.Pos { - return voice.GetPos(t.Voice) -} - -// SetPan sets the panning position -func (t *txn[TPeriod]) SetPan(pan panning.Position) { - t.pan.Set(pan) -} - -func (t *txn[TPeriod]) GetPendingPan() (panning.Position, bool) { - return t.pan.Get() -} - -func (t *txn[TPeriod]) GetCurrentPan() panning.Position { - return voice.GetPan(t.Voice) -} - -// SetVolumeEnvelopePosition sets the volume envelope position -func (t *txn[TPeriod]) SetVolumeEnvelopePosition(pos int) { - t.volEnv.pos.Set(pos) -} - -// EnableVolumeEnvelope sets the volume envelope enable flag -func (t *txn[TPeriod]) EnableVolumeEnvelope(enabled bool) { - t.volEnv.enabled.Set(enabled) -} - -func (t *txn[TPeriod]) IsPendingVolumeEnvelopeEnabled() (bool, bool) { - return t.volEnv.enabled.Get() -} - -func (t *txn[TPeriod]) IsCurrentVolumeEnvelopeEnabled() bool { - return voice.IsVolumeEnvelopeEnabled(t.Voice) -} - -// SetPitchEnvelopePosition sets the pitch envelope position -func (t *txn[TPeriod]) SetPitchEnvelopePosition(pos int) { - t.pitchEnv.pos.Set(pos) -} - -// EnablePitchEnvelope sets the pitch envelope enable flag -func (t *txn[TPeriod]) EnablePitchEnvelope(enabled bool) { - t.pitchEnv.enabled.Set(enabled) -} - -// SetPanEnvelopePosition sets the panning envelope position -func (t *txn[TPeriod]) SetPanEnvelopePosition(pos int) { - t.panEnv.pos.Set(pos) -} - -// EnablePanEnvelope sets the pan envelope enable flag -func (t *txn[TPeriod]) EnablePanEnvelope(enabled bool) { - t.panEnv.enabled.Set(enabled) -} - -// SetFilterEnvelopePosition sets the pitch envelope position -func (t *txn[TPeriod]) SetFilterEnvelopePosition(pos int) { - t.filterEnv.pos.Set(pos) -} - -// EnableFilterEnvelope sets the filter envelope enable flag -func (t *txn[TPeriod]) EnableFilterEnvelope(enabled bool) { - t.filterEnv.enabled.Set(enabled) -} - -// SetAllEnvelopePositions sets all the envelope positions to the same value -func (t *txn[TPeriod]) 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[TPeriod]) Cancel() { - t.cancelled = true -} - -// Commit commits the transaction by applying pending updates -func (t *txn[TPeriod]) 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[TPeriod](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[TPeriod](t.Voice, pos) - } - - if enabled, ok := t.pitchEnv.enabled.Get(); ok { - voice.EnablePitchEnvelope[TPeriod](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[TPeriod]) GetVoice() voice.Voice { - return t.Voice -} - -func (t *txn[TPeriod]) Clone() voice.Transaction[TPeriod] { - c := *t - return &c -} diff --git a/player/voice/voice.go b/player/voice/voice.go deleted file mode 100644 index 1847f7b..0000000 --- a/player/voice/voice.go +++ /dev/null @@ -1,50 +0,0 @@ -package voice - -import ( - "github.com/gotracker/playback/period" - "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[TPeriod period.Period](periodConverter period.PeriodConverter[TPeriod], 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.SampleRate, output.GetSampleRate()) - } - if factory := inst.GetPluginFilterFactory(); factory != nil { - pluginFilter = factory(inst.SampleRate, output.GetSampleRate()) - } - return NewPCM[TPeriod](periodConverter, PCMConfiguration[TPeriod]{ - SampleRate: inst.GetSampleRate(), - InitialVolume: inst.GetDefaultVolume(), - AutoVibrato: inst.GetAutoVibrato(), - Data: data, - OutputFilter: output, - VoiceFilter: voiceFilter, - PluginFilter: pluginFilter, - Vol0Optimization: Vol0OptimizationConfiguration{ - Enabled: true, - MaxTicksAt0: 3, - }, - }) - case *instrument.OPL2: - return NewOPL2[TPeriod](OPLConfiguration[TPeriod]{ - Chip: output.GetOPL2Chip(), - Channel: output.ChannelNum, - SampleRate: inst.GetSampleRate(), - InitialVolume: inst.GetDefaultVolume(), - AutoVibrato: inst.GetAutoVibrato(), - Data: data, - }) - } - return nil -} diff --git a/song/channeldata.go b/song/channeldata.go index b43bd9e..e722e47 100644 --- a/song/channeldata.go +++ b/song/channeldata.go @@ -4,13 +4,12 @@ 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 @@ -18,7 +17,7 @@ type ChannelData interface { GetInstrument(note.Semitone) instrument.ID HasVolume() bool - GetVolume() volume.Volume + GetVolumeGeneric() volume.Volume HasCommand() bool @@ -27,3 +26,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..bdcb364 --- /dev/null +++ b/song/channelmemory.go @@ -0,0 +1,5 @@ +package song + +type ChannelMemory interface { + Retrigger() +} diff --git a/song/channelsettings.go b/song/channelsettings.go new file mode 100644 index 0000000..76ea524 --- /dev/null +++ b/song/channelsettings.go @@ -0,0 +1,52 @@ +package song + +import ( + "errors" +) + +type ChannelSettings interface { + GetEnabled() bool + GetOutputChannelNum() int + GetMemory() ChannelMemory + GetPanEnabled() bool + GetDefaultFilterName() string + IsDefaultFilterEnabled() bool +} + +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 16e108b..f6dcbb3 100644 --- a/song/pattern.go +++ b/song/pattern.go @@ -11,18 +11,27 @@ var ( ErrStopSong = errors.New("stop song") ) +type PatternIntf interface { + GetRowIntf(row index.Row) RowIntf + NumRows() int +} + // Pattern is structure containing the pattern data -type Pattern[TChannelData ChannelData] []Row[TChannelData] +type Pattern[TChannelData ChannelData[TVolume], TVolume Volume] []Row[TChannelData, TVolume] // GetRow returns a single row of channel data -func (p Pattern[TChannelData]) GetRow(row index.Row) Row[TChannelData] { +func (p Pattern[TChannelData, TVolume]) GetRow(row index.Row) Row[TChannelData, TVolume] { + return p[row] +} + +func (p Pattern[TChannelData, TVolume]) GetRowIntf(row index.Row) RowIntf { return p[row] } // NumRows returns the number of rows contained within the pattern -func (p Pattern[TChannelData]) NumRows() int { +func (p Pattern[TChannelData, TVolume]) NumRows() int { return len(p) } // Patterns is an array of pattern interfaces -type Patterns[TChannelData ChannelData] []Pattern[TChannelData] +type Patterns[TChannelData ChannelData[TVolume], TVolume Volume] []Pattern[TChannelData, TVolume] diff --git a/song/periodcalculator.go b/song/periodcalculator.go new file mode 100644 index 0000000..7a2941c --- /dev/null +++ b/song/periodcalculator.go @@ -0,0 +1,22 @@ +package song + +import ( + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" +) + +type PeriodCalculatorIntf interface { + GetPeriodGeneric(note.Note) period.Period + PortaToNoteGeneric(period.Period, period.Delta, period.Period) (period.Period, error) + PortaDownGeneric(period.Period, period.Delta) (period.Period, error) + PortaUpGeneric(period.Period, period.Delta) (period.Period, error) +} + +type PeriodCalculator[TPeriod period.Period] interface { + GetPeriod(note.Note) TPeriod + PortaToNote(TPeriod, period.Delta, TPeriod) (TPeriod, error) + PortaDown(TPeriod, period.Delta) (TPeriod, error) + PortaUp(TPeriod, period.Delta) (TPeriod, error) + GetSamplerAdd(TPeriod, float64) float64 + GetFrequency(TPeriod) period.Frequency +} diff --git a/song/row.go b/song/row.go index b6bc192..1181488 100644 --- a/song/row.go +++ b/song/row.go @@ -2,13 +2,26 @@ package song import "github.com/gotracker/playback/index" +type RowIntf interface { + GetNumChannels() int + GetChannelIntf(index.Channel) ChannelDataIntf +} + // Row is a structure containing a single row -type Row[TChannelData ChannelData] []TChannelData +type Row[TChannelData ChannelData[TVolume], TVolume Volume] []TChannelData -func (r Row[TChannelData]) GetNumChannels() int { +func (r Row[TChannelData, TVolume]) GetNumChannels() int { return len(r) } -func (r Row[TChannelData]) GetChannel(ch index.Channel) TChannelData { +func (r Row[TChannelData, TVolume]) GetChannel(ch index.Channel) TChannelData { return r[ch] } + +func (r Row[TChannelData, TVolume]) GetChannelIntf(ch index.Channel) ChannelDataIntf { + return r[ch] +} + +type RowStringer interface { + String(options ...any) string +} diff --git a/song/song.go b/song/song.go index ec6ff2d..fac57e8 100644 --- a/song/song.go +++ b/song/song.go @@ -1,22 +1,74 @@ 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(instrument.ID) (instrument.InstrumentIntf, note.Semitone) GetName() string + GetPatternIntfByOrder(index.Order) (PatternIntf, error) + GetPatternIntf(index.Pattern) (PatternIntf, error) + GetPeriodCalculator() PeriodCalculatorIntf + GetInitialOrder() index.Order + GetRowRenderStringer(RowIntf, int, bool) RowStringer + GetSystem() system.System +} + +type ( + Volume = types.Volume + Panning = types.Panning +) + +type globalVolumeGetter[TGlobalVolume Volume] interface { + GetGlobalVolume() TGlobalVolume } -type PatternData[TChannelData ChannelData] 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 index 405876f..0f02459 100644 --- a/system/clockedsystem.go +++ b/system/clockedsystem.go @@ -4,10 +4,30 @@ import ( "github.com/gotracker/playback/note" ) +type ClockableSystem interface { + System + GetBaseClock() Frequency + GetBaseFinetunes() note.Finetune + GetFinetunesPerOctave() note.Finetune + GetFinetunesPerSemitone() note.Finetune + GetSemitonePeriod(note.Key) (float32, bool) +} + type ClockedSystem struct { + MaxPastNotesPerChannel int + BaseClock Frequency BaseFinetunes note.Finetune FinetunesPerOctave note.Finetune + FinetunesPerNote note.Finetune + CommonRate Frequency + SemitonePeriods [note.NumKeys]float32 +} + +var _ ClockableSystem = (*ClockedSystem)(nil) + +func (s ClockedSystem) GetMaxPastNotesPerChannel() int { + return s.MaxPastNotesPerChannel } func (s ClockedSystem) GetBaseClock() Frequency { @@ -21,3 +41,18 @@ func (s ClockedSystem) GetBaseFinetunes() note.Finetune { func (s ClockedSystem) GetFinetunesPerOctave() note.Finetune { return s.FinetunesPerOctave } + +func (s ClockedSystem) GetFinetunesPerSemitone() note.Finetune { + return s.FinetunesPerNote +} + +func (s ClockedSystem) GetCommonRate() Frequency { + return s.CommonRate +} + +func (s ClockedSystem) GetSemitonePeriod(k note.Key) (float32, bool) { + if int(k) < note.NumKeys { + return s.SemitonePeriods[int(k)], true + } + return 0, false +} diff --git a/system/system.go b/system/system.go index b12ba64..5641f92 100644 --- a/system/system.go +++ b/system/system.go @@ -1,9 +1,6 @@ package system -import "github.com/gotracker/playback/note" - type System interface { - GetBaseClock() Frequency - GetBaseFinetunes() note.Finetune - GetFinetunesPerOctave() note.Finetune + GetMaxPastNotesPerChannel() int + GetCommonRate() Frequency } diff --git a/tracing/channelstate.go b/tracing/channelstate.go deleted file mode 100644 index bf9a682..0000000 --- a/tracing/channelstate.go +++ /dev/null @@ -1,32 +0,0 @@ -package tracing - -import ( - "fmt" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/period" -) - -func ChannelStateHeaders(preamble string) []string { - return []string{ - fmt.Sprintf("%s Instrument", preamble), - fmt.Sprintf("%s Period", preamble), - fmt.Sprintf("%s Volume", preamble), - fmt.Sprintf("%s Position", preamble), - fmt.Sprintf("%s Pan", preamble), - } -} - -func ChannelState[TPeriod period.Period](cs *playback.ChannelState[TPeriod]) []any { - var instStr string - if cs.Instrument != nil { - instStr = cs.Instrument.GetID().String() - } - return []any{ - instStr, - cs.Period, - cs.GetVolume(), - cs.Pos, - cs.Pan, - } -} diff --git a/tracing/entry.go b/tracing/entry.go new file mode 100644 index 0000000..9c2af1f --- /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 *Tracing) trace(tick Tick, op string) { + t.traceWithComment(tick, op, "") +} + +type emptyPayload struct{} + +func (emptyPayload) String() string { + return "" +} + +var empty emptyPayload + +func (t *Tracing) traceWithComment(tick Tick, op, comment string) { + if t.tracingFile == nil { + return + } + traceWithPayload(t, tick, op, comment, empty) +} + +func traceWithPayload[TPrefix Ticker, TPayload fmt.Stringer](t *Tracing, 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..695b16c --- /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 *Tracing) traceChannel(tick Tick, ch index.Channel, op string) { + t.traceChannelWithComment(tick, ch, op, "") +} + +func (t *Tracing) traceChannelWithComment(tick Tick, ch index.Channel, op string, comment string) { + if t.tracingFile == 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..30092c0 --- /dev/null +++ b/tracing/tracer.go @@ -0,0 +1,15 @@ +package tracing + +import "github.com/gotracker/playback/index" + +type Tracer interface { + SetTracingTick(order index.Order, row index.Row, tick int) + Trace(op string) + TraceWithComment(op, comment string) + TraceValueChange(op string, prev, new any) + TraceValueChangeWithComment(op string, prev, new any, comment string) + TraceChannel(ch index.Channel, op string) + TraceChannelWithComment(ch index.Channel, op, comment string) + TraceChannelValueChange(ch index.Channel, op string, prev, new any) + TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, comment string) +} diff --git a/tracing/tracing.go b/tracing/tracing.go index 671c6bc..f30093d 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -3,146 +3,141 @@ package tracing import ( "fmt" "io" - "strings" + "log" + "os" + "sync" - ansi "github.com/fatih/color" - - "github.com/gotracker/playback" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player" - "github.com/gotracker/playback/song" + "github.com/gotracker/playback/index" ) -type Tracing[TPeriod period.Period, TMemory any, TData song.ChannelData] struct { - playback.Playback - ChannelGetter func(c int) playback.Channel[TPeriod, TMemory, TData] +type Tracing struct { + tracingFile *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 } -func (m Tracing[TPeriod, TMemory, TData]) 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) - } +type entryIntf interface { + GetTick() Tick + Prefix() string + fmt.Stringer +} - fmt.Fprintln(w) - } +type tracingMsgFunc func() string + +type tracingChannelState struct { + traces []tracingMsgFunc } -func (m Tracing[TPeriod, TMemory, TData]) outputGlobalTrace() func(w io.Writer) { - gs := player.NewTracingTable("=== global ===", - "globalVolume", - "mixerVolume", - "currentOrder", - "currentRow", - ) - gs.AddRow( - m.Playback.GetGlobalVolume(), - m.Playback.GetMixerVolume(), - m.Playback.GetCurrentOrder(), - m.Playback.GetCurrentRow(), - ) - - return func(w io.Writer) { - fmt.Fprintln(w) - gs.WriteOut(w) +func (t *Tracing) EnableTracing(filename string) error { + var err error + t.tracingFile, err = os.Create(filename) + if err != nil { + return err } + + return nil } -func (m Tracing[TPeriod, TMemory, TData]) outputRenderTrace() func(w io.Writer) { - r := m.Playback.GetRenderState() - if r == nil { - return func(w io.Writer) {} +func (t *Tracing) Close() { + if t.c != nil { + close(t.c) } - - rs := player.NewTracingTable("=== rowRenderState ===", - "samplerSpeed", - "tickDuration", - "samplesPerTick", - "ticksThisRow", - "currentTick", - ) - rs.AddRow( - fmt.Sprint(r.GetSamplerSpeed()), - fmt.Sprint(r.GetDuration()), - fmt.Sprint(r.GetSamples()), - fmt.Sprint(r.GetTicksThisRow()), - fmt.Sprint(r.GetCurrentTick()), - ) - - return func(w io.Writer) { - fmt.Fprintln(w) - rs.WriteOut(w) + if t.tracingFile != nil { + t.tracingFile.Close() } + t.wg.Wait() } -func (m Tracing[TPeriod, TMemory, TData]) outputChannelsTrace() func(w io.Writer) { - cs := player.NewTracingTable("=== channels ===", - append(append( - []string{ - "Channel", - "ChannelVolume", - "ActiveEffect", - "TrackData", - "RetriggerCount", - "Semitone", - "UseTargetPeriod", - "NewNoteAction", - }, - ChannelStateHeaders("Previous")...), - ChannelStateHeaders("Active")..., - )..., - ) - - for c := 0; c < m.Playback.GetNumChannels(); c++ { - ch := m.ChannelGetter(c) - if ch == nil { - continue - } - var trackData string - effects := ch.GetActiveEffects() - if len(effects) == 0 { - effects = []playback.Effect{nil} - } - trackData = fmt.Sprint(ch.GetChannelData()) - var activeEffect []string - for _, effect := range effects { - if effect != nil { - effectTypes := playback.GetEffectNames(effect) - activeEffect = append(activeEffect, strings.Join(effectTypes, ",")) +func (t *Tracing) OutputTraces() { + if t.tracingFile == nil { + return + } + + t.mu.Lock() + defer t.mu.Unlock() + + var updates []entryIntf + updates, t.updates = t.updates, nil + + go func() { + logger := log.New(t.tracingFile, "", 0) + for _, u := range updates { + if tick := u.GetTick(); !tick.Equals(t.prevTick) { + fmt.Fprintln(t.tracingFile) + t.prevTick = tick } - } - prev := ch.GetPreviousState() - active := ch.GetActiveState() - - data := []any{ - c + 1, - ch.GetChannelVolume(), - strings.Join(activeEffect, ","), - trackData, - ch.GetRetriggerCount(), - ch.GetNoteSemitone(), - ch.GetUseTargetPeriod(), - ch.GetNewNoteAction(), + logger.Println("[" + u.Prefix() + "] " + u.String()) } - data = append(data, ChannelState[TPeriod](&prev)...) - data = append(data, ChannelState[TPeriod](active)...) + }() +} - if prev.Instrument != active.Instrument || any(prev.Period) != any(active.Period) || prev.GetVolume() != active.GetVolume() || prev.Pos != active.Pos || prev.Pan != active.Pan { - cs.AddRowColor([]ansi.Attribute{ansi.BgRed, ansi.FgHiWhite}, data...) - } else { - cs.AddRow(data...) - } +func (t *Tracing) 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 *Tracing) GetTracingTick() Tick { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.tick +} + +func (t *Tracing) Trace(op string) { + t.TraceWithComment(op, "") +} + +func (t *Tracing) TraceWithComment(op, comment string) { + traceWithPayload(t, t.GetTracingTick(), op, comment, empty) +} + +func (t *Tracing) TraceValueChange(op string, prev, new any) { + t.TraceValueChangeWithComment(op, prev, new, "") +} + +func (t *Tracing) TraceValueChangeWithComment(op string, prev, new any, comment string) { + traceWithPayload(t, t.GetTracingTick(), op, comment, valueUpdate{ + old: prev, + new: new, + }) +} + +func (t *Tracing) TraceChannel(ch index.Channel, op string) { + t.TraceChannelWithComment(ch, op, "") +} + +func (t *Tracing) TraceChannelWithComment(ch index.Channel, op, comment string) { + tc := tickChannel{ + tick: t.GetTracingTick(), + ch: ch, } + traceWithPayload(t, tc, op, comment, empty) +} + +func (t *Tracing) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { + t.TraceChannelValueChangeWithComment(ch, op, prev, new, "") +} - return func(w io.Writer) { - fmt.Fprintln(w) - cs.WriteOut(w) +func (t *Tracing) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, comment string) { + tc := tickChannel{ + tick: t.GetTracingTick(), + ch: ch, } + traceWithPayload(t, tc, op, comment, valueUpdate{ + old: prev, + new: new, + }) } diff --git a/tracing/valueupdate.go b/tracing/valueupdate.go new file mode 100644 index 0000000..06fe6f8 --- /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 *Tracing) traceValueChange(tick Tick, op string, prev, new any) { + t.traceValueChangeWithComment(tick, op, prev, new, "") +} + +func (t *Tracing) traceValueChangeWithComment(tick Tick, op string, prev, new any, comment string) { + if t.tracingFile == nil { + return + } + if reflect.DeepEqual(prev, new) { + return + } + traceWithPayload(t, tick, op, comment, valueUpdate{ + old: prev, + new: new, + }) +} + +func (t *Tracing) traceChannelValueChange(tick Tick, ch index.Channel, op string, prev, new any) { + t.traceChannelValueChangeWithComment(tick, ch, op, prev, new, "") +} + +func (t *Tracing) traceChannelValueChangeWithComment(tick Tick, ch index.Channel, op string, prev, new any, comment string) { + if t.tracingFile == 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/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/settings.go old mode 100755 new mode 100644 similarity index 61% rename from voice/autovibrato.go rename to voice/autovibrato/settings.go index 08770ad..58aa7b0 --- a/voice/autovibrato.go +++ b/voice/autovibrato/settings.go @@ -1,11 +1,9 @@ -package voice +package autovibrato -import ( - "github.com/gotracker/playback/voice/oscillator" -) +import "github.com/gotracker/playback/voice/oscillator" -// AutoVibrato is the setting and memory for the auto-vibrato system -type AutoVibrato struct { +// AutoVibratoSettings is the setting and memory for the auto-vibrato system +type AutoVibratoSettings struct { Enabled bool Sweep int WaveformSelection uint8 @@ -15,7 +13,7 @@ type AutoVibrato struct { } // Generate creates an AutoVibrato waveform oscillator and configures it with the inital values -func (a *AutoVibrato) Generate() oscillator.Oscillator { +func (a AutoVibratoSettings) Generate() oscillator.Oscillator { if a.Factory == nil { return nil } diff --git a/voice/component/envelope.go b/voice/component/envelope.go index 909563b..92a3deb 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -1,10 +1,109 @@ 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" +) + // Envelope is an envelope component interface type Envelope interface { //Init(env *envelope.Envelope) SetEnabled(enabled bool) IsEnabled() bool Reset() - Advance(keyOn bool, prevKeyOn bool) + Advance() +} + +// baseEnvelope is a basic modulation envelope +type baseEnvelope[TIn, TOut any] struct { + settings EnvelopeSettings[TIn, TOut] + updater func() TOut + enabled bool + state envelope.State[TIn] + 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() TOut) { + e.settings = settings + e.updater = update + e.Reset() +} + +func (e baseEnvelope[TIn, TOut]) Clone(update func() TOut) baseEnvelope[TIn, TOut] { + m := e + m.state = e.state.Clone() + return m +} + +// Reset resets the state to defaults based on the envelope provided +func (e *baseEnvelope[TIn, TOut]) Reset() { + e.enabled = e.settings.Enabled + e.state.Init(&e.settings.Envelope) + if e.enabled { + e.value = e.updater() + } +} + +// SetEnabled sets the enabled flag for the envelope +func (e *baseEnvelope[TIn, TOut]) SetEnabled(enabled bool) { + e.enabled = enabled +} + +// IsEnabled returns the enabled flag for the envelope +func (e baseEnvelope[TIn, TOut]) IsEnabled() bool { + return e.enabled +} + +func (e baseEnvelope[TIn, TOut]) IsDone() bool { + return e.state.Stopped() +} + +// 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 { + e.state.Reset() + // 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(); doneCB != nil { + return doneCB + } + } + return nil +} + +func (e baseEnvelope[TIn, TOut]) GetEnvelopePosition() int { + return e.state.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.state.Advance(e.keyOn); done { + doneCB = e.settings.OnFinished + } + e.value = e.updater() + return doneCB +} + +func (e baseEnvelope[TIn, TOut]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} pos{%v} value{%v}", + e.enabled, + e.state.Pos(), + e.value, + ), comment) } diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 22ed770..45a64bf 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -3,96 +3,36 @@ package component import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/util" - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" ) // FilterEnvelope is a filter frequency cutoff modulation envelope type FilterEnvelope struct { - enabled bool - state envelope.State[filter.PitchFiltValue] - value uint8 - keyOn bool - prevKeyOn bool + baseEnvelope[filter.PitchFiltValue, uint8] } -func (e *FilterEnvelope) Init(env *envelope.Envelope[filter.PitchFiltValue]) { - e.state.Init(env) - e.Reset() +func (e *FilterEnvelope) Setup(settings EnvelopeSettings[filter.PitchFiltValue, uint8]) { + e.baseEnvelope.Setup(settings, e.calc) } func (e FilterEnvelope) Clone() FilterEnvelope { - return FilterEnvelope{ - enabled: e.enabled, - state: e.state.Clone(), - value: e.value, - keyOn: false, - prevKeyOn: false, - } -} - -// Reset resets the state to defaults based on the envelope provided -func (e *FilterEnvelope) Reset() { - e.state.Reset() - e.keyOn = false - e.prevKeyOn = false - e.update() -} - -// SetEnabled sets the enabled flag for the envelope -func (e *FilterEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled -} - -// 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() uint8 { - 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 - e.state.Reset() - // 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 + var m FilterEnvelope + m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + return m } -func (e *FilterEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) +func (e *FilterEnvelope) calc() uint8 { + cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) var y0 filter.PitchFiltValue if cur != nil { - y0 = cur.Value() + y0 = cur.Y } var y1 filter.PitchFiltValue if next != nil { - y1 = next.Value() + y1 = next.Y } v := util.Lerp(float64(t), y0, y1) - e.value = uint8(32 + v) + return uint8(32 + v) } diff --git a/voice/component/envelope_pan.go b/voice/component/envelope_pan.go index 98308e8..a10ab18 100755 --- a/voice/component/envelope_pan.go +++ b/voice/component/envelope_pan.go @@ -1,99 +1,37 @@ package component import ( - "github.com/gotracker/gomixing/panning" - - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/util" + "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 -} - -func (e *PanEnvelope) Init(env *envelope.Envelope[panning.Position]) { - e.state.Init(env) - e.Reset() -} - -func (e PanEnvelope) Clone() PanEnvelope { - return PanEnvelope{ - enabled: e.enabled, - state: e.state.Clone(), - pan: e.pan, - keyOn: false, - prevKeyOn: false, - } -} - -// Reset resets the state to defaults based on the envelope provided -func (e *PanEnvelope) Reset() { - e.state.Reset() - e.keyOn = false - e.prevKeyOn = false - e.update() +type PanEnvelope[TPanning types.Panning] struct { + baseEnvelope[TPanning, TPanning] } -// SetEnabled sets the enabled flag for the envelope -func (e *PanEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled +func (e *PanEnvelope[TPanning]) Setup(settings EnvelopeSettings[TPanning, TPanning]) { + e.baseEnvelope.Setup(settings, e.calc) } -// 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 -} - -// SetEnvelopePosition sets the current position in the envelope -func (e *PanEnvelope) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - e.state.Reset() - // 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 *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() PanEnvelope[TPanning] { + var m PanEnvelope[TPanning] + m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + return m } -func (e *PanEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) +func (e *PanEnvelope[TPanning]) calc() TPanning { + cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) - y0 := panning.CenterAhead + var y0 TPanning if cur != nil { - y0 = cur.Value() + y0 = cur.Y } - y1 := panning.CenterAhead + var y1 TPanning if next != nil { - y1 = next.Value() + y1 = next.Y } - // 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) + return util.Lerp(float64(t), y0, y1) } diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index 96b6fac..46b3c35 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -4,95 +4,35 @@ import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" "github.com/gotracker/playback/util" - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" ) // PitchEnvelope is an frequency modulation envelope -type PitchEnvelope[TPeriod period.Period] struct { - enabled bool - state envelope.State[filter.PitchFiltValue] - delta period.Delta - keyOn bool - prevKeyOn bool +type PitchEnvelope struct { + baseEnvelope[filter.PitchFiltValue, period.Delta] } -func (e *PitchEnvelope[TPeriod]) Init(env *envelope.Envelope[filter.PitchFiltValue]) { - e.state.Init(env) - e.Reset() +func (e *PitchEnvelope) Setup(settings EnvelopeSettings[filter.PitchFiltValue, period.Delta]) { + e.baseEnvelope.Setup(settings, e.calc) } -func (e PitchEnvelope[TPeriod]) Clone() PitchEnvelope[TPeriod] { - return PitchEnvelope[TPeriod]{ - enabled: e.enabled, - state: e.state.Clone(), - delta: e.delta, - keyOn: false, - prevKeyOn: false, - } -} - -// Reset resets the state to defaults based on the envelope provided -func (e *PitchEnvelope[TPeriod]) Reset() { - e.state.Reset() - e.keyOn = false - e.prevKeyOn = false - e.update() -} - -// SetEnabled sets the enabled flag for the envelope -func (e *PitchEnvelope[TPeriod]) SetEnabled(enabled bool) { - e.enabled = enabled -} - -// IsEnabled returns the enabled flag for the envelope -func (e *PitchEnvelope[TPeriod]) IsEnabled() bool { - return e.enabled -} - -// GetCurrentValue returns the current cached envelope value -func (e *PitchEnvelope[TPeriod]) GetCurrentValue() period.Delta { - return e.delta -} - -// SetEnvelopePosition sets the current position in the envelope -func (e *PitchEnvelope[TPeriod]) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - e.state.Reset() - // 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[TPeriod]) 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) Clone() PitchEnvelope { + var m PitchEnvelope + m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + return m } -func (e *PitchEnvelope[TPeriod]) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) +func (e *PitchEnvelope) calc() period.Delta { + cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) var y0 filter.PitchFiltValue if cur != nil { - y0 = cur.Value() + y0 = cur.Y } var y1 filter.PitchFiltValue if next != nil { - y1 = next.Value() + y1 = next.Y } - e.delta = -period.Delta(util.Lerp(float64(t), y0, y1)) + return -period.Delta(util.Lerp(float64(t), y0, y1)) } diff --git a/voice/component/envelope_volume.go b/voice/component/envelope_volume.go index 66e2481..1240aad 100755 --- a/voice/component/envelope_volume.go +++ b/voice/component/envelope_volume.go @@ -1,97 +1,37 @@ package component import ( - "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/util" + "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 -} - -func (e *VolumeEnvelope) Init(env *envelope.Envelope[volume.Volume]) { - e.state.Init(env) - e.Reset() -} - -func (e VolumeEnvelope) Clone() VolumeEnvelope { - return VolumeEnvelope{ - enabled: e.enabled, - state: e.state.Clone(), - vol: e.vol, - keyOn: false, - prevKeyOn: false, - } -} - -// Reset resets the state to defaults based on the envelope provided -func (e *VolumeEnvelope) Reset() { - e.state.Reset() - e.keyOn = false - e.prevKeyOn = false - e.update() +type VolumeEnvelope[TVolume types.Volume] struct { + baseEnvelope[TVolume, TVolume] } -// SetEnabled sets the enabled flag for the envelope -func (e *VolumeEnvelope) SetEnabled(enabled bool) { - e.enabled = enabled +func (e *VolumeEnvelope[TVolume]) Setup(settings EnvelopeSettings[TVolume, TVolume]) { + e.baseEnvelope.Setup(settings, e.calc) } -// 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 -} - -// SetEnvelopePosition sets the current position in the envelope -func (e *VolumeEnvelope) SetEnvelopePosition(pos int) voice.Callback { - keyOn := e.keyOn - prevKeyOn := e.prevKeyOn - e.state.Reset() - // 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 *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() VolumeEnvelope[TVolume] { + var m VolumeEnvelope[TVolume] + m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + return m } -func (e *VolumeEnvelope) update() { - cur, next, t := e.state.GetCurrentValue(e.keyOn) +func (e *VolumeEnvelope[TVolume]) calc() TVolume { + cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) - var y0 volume.Volume + var y0 TVolume if cur != nil { - y0 = cur.Value() + y0 = cur.Y } - var y1 volume.Volume + var y1 TVolume if next != nil { - y1 = next.Value() + y1 = next.Y } - e.vol = y0 + volume.Volume(t)*(y1-y0) + return util.Lerp(float64(t), y0, y1) } diff --git a/voice/component/modulator_amp.go b/voice/component/modulator_amp.go index 7d5de20..9b41f16 100755 --- a/voice/component/modulator_amp.go +++ b/voice/component/modulator_amp.go @@ -1,102 +1,109 @@ 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" ) // 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] + active bool + vol TVolume + delta types.VolumeDelta + mixing 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 + DefaultMixingVolume TMixingVolume + DefaultVolume TVolume } -// Attack disables the fadeout and resets its volume -func (a *AmpModulator) Attack() { - a.fadeoutEnabled = false - a.fadeoutVol = volume.Volume(1) +func (a *AmpModulator[TMixingVolume, TVolume]) Setup(settings AmpModulatorSettings[TMixingVolume, TVolume]) { + a.settings = settings + a.active = settings.Active + a.vol = settings.DefaultVolume + a.delta = 0 + a.mixing = settings.DefaultMixingVolume a.updateFinal() } -// 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 +func (a *AmpModulator[TMixingVolume, TVolume]) SetActive(active bool) { + a.active = active 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]) IsActive() bool { + return a.active } -// GetVolume returns the current volume (before fadeout calculation) -func (a *AmpModulator) GetVolume() volume.Volume { - return a.vol +// SetMixingVolume configures the mixing volume of the modulator +func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolume(mixing TMixingVolume) { + if !mixing.IsUseInstrumentVol() { + a.mixing = mixing + a.updateFinal() + } } -// SetFadeoutEnabled sets the status of the fadeout enablement flag -func (a *AmpModulator) SetFadeoutEnabled(enabled bool) { - a.fadeoutEnabled = enabled - a.updateFinal() +// GetMixingVolume returns the current mixing volume of the modulator +func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolume() TMixingVolume { + return a.mixing } -// 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] +// SetVolume sets the current volume (before fadeout calculation) +func (a *AmpModulator[TMixingVolume, TVolume]) SetVolume(vol TVolume) { + if vol.IsUseInstrumentVol() { + vol = a.settings.DefaultVolume } + a.vol = vol 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.vol } -// GetFadeoutVolume returns the value of the fadeout volume -func (a *AmpModulator) GetFadeoutVolume() volume.Volume { - return a.fadeoutVol +func (a *AmpModulator[TMixingVolume, TVolume]) SetVolumeDelta(d types.VolumeDelta) { + a.delta = d + a.updateFinal() +} + +func (a AmpModulator[TMixingVolume, TVolume]) GetVolumeDelta() types.VolumeDelta { + return a.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 { +func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() { + if !a.active { + a.final = 0 return } - a.fadeoutVol -= a.fadeoutAmt - switch { - case a.fadeoutVol < 0: - a.fadeoutVol = 0 - case a.fadeoutVol > 1: - a.fadeoutVol = 1 - } - a.updateFinal() + v := types.AddVolumeDelta(a.vol, a.delta) + a.final = a.mixing.ToVolume() * v.ToVolume() } -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} vol{%v} mixing{%v} final{%v}", + a.active, + a.vol, + a.mixing, + a.final, + ), comment) } diff --git a/voice/component/modulator_autovibrato.go b/voice/component/modulator_autovibrato.go new file mode 100644 index 0000000..b78bce7 --- /dev/null +++ b/voice/component/modulator_autovibrato.go @@ -0,0 +1,94 @@ +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" +) + +// AutoVibratoModulator is a frequency (pitch) modulator +type AutoVibratoModulator[TPeriod period.Period] struct { + settings autovibrato.AutoVibratoSettings + enabled bool + autoVibrato oscillator.Oscillator + age int // current age of oscillator (in ticks) +} + +func (f *AutoVibratoModulator[TPeriod]) Setup(settings autovibrato.AutoVibratoSettings) { + f.settings = settings + 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() { + f.enabled = f.settings.Enabled + f.autoVibrato = f.settings.Generate() + f.ResetAutoVibrato() +} + +// SetEnabled sets the status of the AutoVibrato enablement flag +func (f *AutoVibratoModulator[TPeriod]) SetEnabled(enabled bool) { + f.enabled = enabled +} + +// ConfigureAutoVibrato sets the AutoVibrato oscillator settings +func (f *AutoVibratoModulator[TPeriod]) ConfigureAutoVibrato() { + f.autoVibrato = f.settings.Generate() +} + +// ResetAutoVibrato resets the current AutoVibrato +func (f *AutoVibratoModulator[TPeriod]) ResetAutoVibrato() { + if f.autoVibrato != nil { + f.autoVibrato.HardReset() + } + + f.age = 0 +} + +// IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag +func (f *AutoVibratoModulator[TPeriod]) IsAutoVibratoEnabled() bool { + return f.enabled +} + +// GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) +func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) TPeriod { + if !f.enabled { + return in + } + + depth := f.settings.Depth + if f.settings.Sweep > f.age { + depth *= float32(f.age) / float32(f.settings.Sweep) + } + avDelta := f.autoVibrato.GetWave(depth) + d := period.Delta(avDelta) + return period.AddDelta(in, d) +} + +// Advance advances the autoVibrato value by 1 tick +func (f *AutoVibratoModulator[TPeriod]) Advance() { + if !f.enabled { + return + } + + f.autoVibrato.Advance(f.settings.Rate) + f.age++ +} + +func (f AutoVibratoModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} age{%v}", + f.enabled, + f.age, + ), comment) +} diff --git a/voice/component/modulator_fadeout.go b/voice/component/modulator_fadeout.go new file mode 100644 index 0000000..aa9180c --- /dev/null +++ b/voice/component/modulator_fadeout.go @@ -0,0 +1,82 @@ +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 + active bool + vol volume.Volume +} + +type FadeoutModulatorSettings struct { + GetEnabled func() bool + Amount volume.Volume +} + +func (a *FadeoutModulator) Setup(settings FadeoutModulatorSettings) { + a.settings = settings + a.Reset() +} + +func (a FadeoutModulator) Clone() FadeoutModulator { + m := a + return m +} + +// Reset disables the fadeout and resets its volume +func (a *FadeoutModulator) Reset() { + a.active = false + a.vol = volume.Volume(1) +} + +// Fadeout activates the fadeout +func (a *FadeoutModulator) Fadeout() { + a.active = a.settings.Amount != 0 +} + +// SetActive sets the status of the fadeout active flag +func (a *FadeoutModulator) SetActive(active bool) { + a.active = active +} + +// IsEnabled returns the status of the fadeout enablement flag +func (a FadeoutModulator) IsActive() bool { + if a.settings.GetEnabled == nil { + return false + } + + return a.settings.GetEnabled() && a.active +} + +// GetVolume returns the value of the fadeout volume +func (a FadeoutModulator) GetVolume() volume.Volume { + return a.vol +} + +func (a FadeoutModulator) GetFinalVolume() volume.Volume { + if !a.IsActive() { + return volume.Volume(1) + } + return a.vol +} + +// Advance advances the fadeout value by 1 tick +func (a *FadeoutModulator) Advance() { + if a.IsActive() && a.vol > 0 { + a.vol = min(max(a.vol-a.settings.Amount, 0), 1) + } +} + +func (a FadeoutModulator) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} vol{%v}", + a.active, + a.vol, + ), comment) +} diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index bd36cdd..9bf5761 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -1,109 +1,60 @@ 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[TPeriod period.Period] struct { - period TPeriod - 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) -} - -func (a FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { - return FreqModulator[TPeriod]{ - period: a.period, - delta: a.delta, - autoVibratoEnabled: a.autoVibratoEnabled, - autoVibrato: a.autoVibrato.Clone(), - autoVibratoDepth: a.autoVibratoDepth, - autoVibratoRate: a.autoVibratoRate, - autoVibratoSweep: a.autoVibratoSweep, - autoVibratoAge: 0, - } -} - -// SetPeriod sets the current period (before AutoVibrato and Delta calculation) -func (a *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { - a.period = period -} - -// GetPeriod returns the current period (before AutoVibrato and Delta calculation) -func (a *FreqModulator[TPeriod]) GetPeriod() TPeriod { - return a.period + settings FreqModulatorSettings[TPeriod] + period TPeriod + delta period.Delta } -// SetDelta sets the current period delta (before AutoVibrato calculation) -func (a *FreqModulator[TPeriod]) SetDelta(delta period.Delta) { - a.delta = delta +type FreqModulatorSettings[TPeriod period.Period] struct { } -// GetDelta returns the current period delta (before AutoVibrato calculation) -func (a *FreqModulator[TPeriod]) GetDelta() period.Delta { - return a.delta +func (f *FreqModulator[TPeriod]) Setup(settings FreqModulatorSettings[TPeriod]) { + f.settings = settings } -// SetAutoVibratoEnabled sets the status of the AutoVibrato enablement flag -func (a *FreqModulator[TPeriod]) SetAutoVibratoEnabled(enabled bool) { - a.autoVibratoEnabled = enabled +func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { + m := f + return m } -// ConfigureAutoVibrato sets the AutoVibrato oscillator settings -func (a *FreqModulator[TPeriod]) ConfigureAutoVibrato(av voice.AutoVibrato) { - a.autoVibrato = av.Generate() - a.autoVibratoRate = int(av.Rate) - a.autoVibratoDepth = av.Depth +// SetPeriod sets the current period (before AutoVibrato and Delta calculation) +func (f *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { + f.period = period } -// ResetAutoVibrato resets the current AutoVibrato -func (a *FreqModulator[TPeriod]) ResetAutoVibrato() { - if a.autoVibrato != nil { - a.autoVibrato.HardReset() - } - - a.autoVibratoAge = 0 +// GetPeriod returns the current period (before AutoVibrato and Delta calculation) +func (f *FreqModulator[TPeriod]) GetPeriod() TPeriod { + return f.period } -func (a *FreqModulator[TPeriod]) ResetAutoVibratoAndSweep(sweep int) { - a.ResetAutoVibrato() - a.autoVibratoSweep = sweep +// SetPeriodDelta sets the current period delta (before AutoVibrato calculation) +func (f *FreqModulator[TPeriod]) SetPeriodDelta(delta period.Delta) { + f.delta = delta } -// IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag -func (a *FreqModulator[TPeriod]) IsAutoVibratoEnabled() bool { - return a.autoVibratoEnabled +// GetDelta returns the current period delta (before AutoVibrato calculation) +func (f *FreqModulator[TPeriod]) GetPeriodDelta() period.Delta { + return f.delta } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) -func (a *FreqModulator[TPeriod]) GetFinalPeriod() TPeriod { - p := period.AddDelta(a.period, 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) - d := period.Delta(avDelta) - p = period.AddDelta(p, d) - } - - return p +func (f *FreqModulator[TPeriod]) GetFinalPeriod() TPeriod { + return period.AddDelta(f.period, f.delta) } -// Advance advances the autoVibrato value by 1 tick -func (a *FreqModulator[TPeriod]) Advance() { - if !a.autoVibratoEnabled { - return - } - - a.autoVibrato.Advance(a.autoVibratoRate) - a.autoVibratoAge++ +func (f FreqModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("period{%v} delta{%v}", + f.period, + f.delta, + ), comment) } diff --git a/voice/component/modulator_key.go b/voice/component/modulator_key.go new file mode 100644 index 0000000..1643de0 --- /dev/null +++ b/voice/component/modulator_key.go @@ -0,0 +1,87 @@ +package component + +import ( + "fmt" + + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/tracing" +) + +type KeyModulator struct { + settings KeyModulatorSettings + slimKeyModulator + fadeout 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) DeferredUpdate() { + if k.keyOn == k.prevKeyOn { + return + } + + if k.keyOn { + if k.settings.DeferredAttack != nil { + k.settings.DeferredAttack() + } + } else { + if k.settings.DeferredRelease != nil { + k.settings.DeferredRelease() + } + } +} + +func (k *KeyModulator) Attack() { + k.slimKeyModulator.Attack() + k.fadeout = false + + if k.settings.Attack != nil && k.keyOn != k.prevKeyOn { + k.settings.Attack() + } +} + +func (k *KeyModulator) Release() { + k.slimKeyModulator.Release() + + if k.settings.Release != nil && k.keyOn != k.prevKeyOn { + 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) +} diff --git a/voice/component/modulator_pan.go b/voice/component/modulator_pan.go index eb95c7c..32fa5f0 100755 --- a/voice/component/modulator_pan.go +++ b/voice/component/modulator_pan.go @@ -1,29 +1,71 @@ package component import ( - "github.com/gotracker/gomixing/panning" + "fmt" + + "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] + pan TPanning + delta types.PanDelta + final TPanning +} + +type PanModulatorSettings[TPanning types.Panning] struct { + InitialPan TPanning +} + +func (p *PanModulator[TPanning]) Setup(settings PanModulatorSettings[TPanning]) { + p.settings = settings + p.pan = settings.InitialPan + p.delta = 0 + 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) { + p.pan = pan + p.updateFinal() } // GetPan returns the current panning -func (p *PanModulator) GetPan() panning.Position { +func (p PanModulator[TPanning]) GetPan() TPanning { return p.pan } +// SetPanDelta sets the current panning delta +func (p *PanModulator[TPanning]) SetPanDelta(d types.PanDelta) { + p.delta = d + p.updateFinal() +} + +// GetPanDelta returns the current panning delta +func (p PanModulator[TPanning]) GetPanDelta() types.PanDelta { + return p.delta +} + // GetFinalPan returns the current panning -func (p *PanModulator) GetFinalPan() panning.Position { - return p.pan +func (p PanModulator[TPanning]) GetFinalPan() TPanning { + 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.pan, + p.delta, + ), comment) } -// Advance advances the fadeout value by 1 tick -func (p *PanModulator) Advance() { +func (p *PanModulator[TPanning]) updateFinal() { + p.final = types.AddPanningDelta(p.pan, p.delta) } diff --git a/voice/component/modulator_pitchpan.go b/voice/component/modulator_pitchpan.go new file mode 100644 index 0000000..751d5ee --- /dev/null +++ b/voice/component/modulator_pitchpan.go @@ -0,0 +1,92 @@ +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] + pitchPanEnabled bool + pitch note.Semitone + 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.pitchPanEnabled = settings.PitchPanEnable + p.pitch = settings.PitchPanCenter + p.Reset() +} + +func (p PitchPanModulator[TPanning]) Clone() PitchPanModulator[TPanning] { + m := p + return m +} + +func (p *PitchPanModulator[TPanning]) Reset() { + p.updatePitchPan() +} + +// SetPitch updates the pan separation modulated by the provided pitch +func (p *PitchPanModulator[TPanning]) SetPitch(st note.Semitone) { + p.pitch = st + p.updatePitchPan() +} + +// IsPitchPanEnabled returns the enablement of the pitch-pan separation function +func (p PitchPanModulator[TPanning]) IsPitchPanEnabled() bool { + return p.pitchPanEnabled +} + +// EnablePitchPan enables the pitch-pan separation function +func (p *PitchPanModulator[TPanning]) EnablePitchPan(enabled bool) { + p.pitchPanEnabled = enabled + 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.pitchPanEnabled || 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() { + if !p.pitchPanEnabled { + return + } + + p.panSep = (float32(p.pitch) - float32(p.settings.PitchPanCenter)) * p.settings.PitchPanSeparation +} + +func (p PitchPanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("pitchPanEnabled{%v} pitch{%v} panSep{%v}", + p.pitchPanEnabled, + p.pitch, + p.panSep, + ), comment) +} diff --git a/voice/component/opl2.go b/voice/component/opl2.go index b1b7390..24c0796 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/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/tracing" "github.com/gotracker/playback/voice/render" + "github.com/gotracker/playback/voice/types" ) // OPL2Operator is a block of values specific to configuring an OPL operator (modulator or carrier) @@ -25,28 +28,41 @@ type OPL2Registers struct { } // OPL2 is an OPL2 component -type OPL2[TPeriod period.Period] struct { +type OPL2[TPeriod types.Period, TVolume types.Volume] struct { chip render.OPL2Chip channel int reg OPL2Registers baseFreq period.Frequency periodConverter period.PeriodConverter[TPeriod] + defaultVolume TVolume keyOn bool } // Setup sets up the OPL2 component -func (o *OPL2[TPeriod]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency) { +func (o *OPL2[TPeriod, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency, defaultVolume TVolume) { o.chip = chip o.channel = channel o.reg = reg o.baseFreq = baseFreq + o.defaultVolume = defaultVolume o.keyOn = false } -// Attack activates the key-on bit -func (o *OPL2[TPeriod]) Attack() { - o.keyOn = true +func (o *OPL2[TPeriod, TVolume]) Attack() { + // does nothing +} + +func (o *OPL2[TPeriod, TVolume]) Release() { + // does nothing +} +func (o *OPL2[TPeriod, TVolume]) Fadeout() { + // does nothing +} + +// DeferredAttack activates the key-on bit +func (o *OPL2[TPeriod, TVolume]) DeferredAttack() { + o.keyOn = true // calculate the register addressing information index := uint32(o.channel) mod := o.getChannelIndex(o.channel) @@ -69,8 +85,8 @@ func (o *OPL2[TPeriod]) Attack() { ch.WriteReg(0xC0|index, o.reg.RegC0) } -// Release deactivates the key-on bit -func (o *OPL2[TPeriod]) Release() { +// DeferredRelease deactivates the key-on bit +func (o *OPL2[TPeriod, TVolume]) DeferredRelease() { o.keyOn = false // calculate the register addressing information @@ -82,7 +98,7 @@ func (o *OPL2[TPeriod]) Release() { } // Advance advances the playback -func (o *OPL2[TPeriod]) Advance(carVol volume.Volume, period TPeriod) { +func (o *OPL2[TPeriod, TVolume]) Advance(carVol volume.Volume, period TPeriod) { // calculate the register addressing information index := uint32(o.channel) mod := o.getChannelIndex(o.channel) @@ -112,17 +128,32 @@ func (o *OPL2[TPeriod]) Advance(carVol volume.Volume, period TPeriod) { ch.WriteReg(0xB0|index, regB0) } +func (o OPL2[TPeriod, TVolume]) Clone() Voicer[TPeriod, TVolume] { + var out OPL2[TPeriod, TVolume] + out.Setup(o.chip, o.channel, o.reg, o.baseFreq, o.defaultVolume) + out.periodConverter = o.periodConverter + return &out +} + +func (o OPL2[TPeriod, TVolume]) GetDefaultVolume() TVolume { + return o.defaultVolume +} + +func (o OPL2[TPeriod, 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[TPeriod]) getChannelIndex(channelIdx int) uint32 { +func (o *OPL2[TPeriod, TVolume]) getChannelIndex(channelIdx int) uint32 { return twoOperatorMelodic[channelIdx%18] } -func (o *OPL2[TPeriod]) calc40(reg40 uint8, vol volume.Volume) uint8 { +func (o *OPL2[TPeriod, TVolume]) calc40(reg40 uint8, vol volume.Volume) uint8 { oVol := volume.Volume(63-uint16(reg40&0x3f)) / 63 totalVol := oVol * vol * 63 if totalVol > 63 { @@ -135,21 +166,21 @@ func (o *OPL2[TPeriod]) calc40(reg40 uint8, vol volume.Volume) uint8 { return result } -func (o *OPL2[TPeriod]) periodToFreqBlock(p TPeriod, baseFreq period.Frequency) (uint16, uint8) { +func (o *OPL2[TPeriod, TVolume]) periodToFreqBlock(p TPeriod, baseFreq period.Frequency) (uint16, uint8) { modFreq := o.periodConverter.GetFrequency(p) freq := float64(baseFreq) * float64(modFreq) / 261625 return o.freqToFnumBlock(freq) } -func (o *OPL2[TPeriod]) freqBlockToRegA0B0(freq uint16, block uint8) (uint8, uint8) { +func (o *OPL2[TPeriod, 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[TPeriod]) freqToFnumBlock(freq float64) (uint16, uint8) { +func (o *OPL2[TPeriod, TVolume]) freqToFnumBlock(freq float64) (uint16, uint8) { if freq > 6208.431 { return 0, 0 } @@ -176,3 +207,7 @@ func (o *OPL2[TPeriod]) freqToFnumBlock(freq float64) (uint16, uint8) { return fnum, block } + +func (o OPL2[TPeriod, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + // TODO +} diff --git a/voice/component/output.go b/voice/component/output.go index 3ead058..1a69a90 100644 --- a/voice/component/output.go +++ b/voice/component/output.go @@ -3,14 +3,13 @@ package component import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - - "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/filter" ) // OutputFilter applies a filter to a sample stream type OutputFilter struct { Input sampling.SampleStream - Output voice.FilterApplier + Output filter.Applier } // GetSample operates the filter diff --git a/voice/component/sampler.go b/voice/component/sampler.go index f0a2fe9..7d9a926 100644 --- a/voice/component/sampler.go +++ b/voice/component/sampler.go @@ -1,69 +1,91 @@ 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 +type Sampler[TPeriod types.Period, TVolume types.Volume] struct { + settings SamplerSettings[TPeriod, TVolume] pos sampling.Pos - keyOn bool loopsEnabled bool - wholeLoop loop.Loop - sustainLoop loop.Loop -} - -func (s Sampler) Clone() Sampler { - return Sampler{ - sample: s.sample, - pos: sampling.Pos{}, - keyOn: false, - loopsEnabled: s.loopsEnabled, - wholeLoop: s.wholeLoop, - sustainLoop: s.sustainLoop, - } + + slimKeyModulator +} + +type SamplerSettings[TPeriod types.Period, TVolume types.Volume] struct { + Sample pcm.Sample + DefaultVolume TVolume + WholeLoop loop.Loop + SustainLoop loop.Loop +} + +func (s Sampler[TPeriod, TVolume]) Clone() Voicer[TPeriod, TVolume] { + m := s + return &m } // 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, TVolume]) Setup(settings SamplerSettings[TPeriod, TVolume]) { + s.settings = settings } // SetPos sets the current position of the sampler in the pcm data (and loops) -func (s *Sampler) SetPos(pos sampling.Pos) { +func (s *Sampler[TPeriod, TVolume]) SetPos(pos sampling.Pos) { s.pos = pos } // GetPos returns the current position of the sampler in the pcm data (and loops) -func (s *Sampler) GetPos() sampling.Pos { +func (s Sampler[TPeriod, TVolume]) GetPos() sampling.Pos { return s.pos } // Attack sets the key-on value (for loop processing) -func (s *Sampler) Attack() { - s.keyOn = true +func (s *Sampler[TPeriod, TVolume]) Attack() { + s.slimKeyModulator.Attack() s.loopsEnabled = true } // Release releases the key-on value (for loop processing) -func (s *Sampler) Release() { - s.keyOn = false +func (s *Sampler[TPeriod, TVolume]) Release() { + s.slimKeyModulator.Release() } // Fadeout disables the loops (for loop processing) -func (s *Sampler) Fadeout() { +func (s *Sampler[TPeriod, TVolume]) Fadeout() { s.loopsEnabled = false } +func (s *Sampler[TPeriod, TVolume]) DeferredAttack() { + // does nothing +} + +func (s *Sampler[TPeriod, TVolume]) DeferredRelease() { + // does nothing +} + +func (s Sampler[TPeriod, TVolume]) GetDefaultVolume() TVolume { + return s.settings.DefaultVolume +} + +func (s Sampler[TPeriod, 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, TVolume]) GetSample(pos sampling.Pos) volume.Matrix { v0 := s.getConvertedSample(pos.Pos) if v0.Channels == 0 { if s.canLoop() { @@ -81,31 +103,52 @@ func (s *Sampler) GetSample(pos sampling.Pos) volume.Matrix { return v0.Lerp(v1, pos.Frac) } -func (s *Sampler) canLoop() bool { +func (s Sampler[TPeriod, TVolume]) canLoop() bool { if s.loopsEnabled { - return (s.keyOn && s.sustainLoop.Enabled()) || s.wholeLoop.Enabled() + 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, 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.maxTicksAt0 + return c.enabled && c.ticksAt0 >= c.settings.MaxTicksAt0 +} + +func (c Vol0Optimization) DumpState(ch index.Channel, t tracing.Tracer, comment string) { + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} ticksAt0{%v}", + c.enabled, + c.ticksAt0, + ), comment) } diff --git a/voice/config.go b/voice/config.go new file mode 100644 index 0000000..9297aa2 --- /dev/null +++ b/voice/config.go @@ -0,0 +1,34 @@ +package voice + +import ( + "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/instrument" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice/autovibrato" + "github.com/gotracker/playback/voice/envelope" + "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/voice/types" + "github.com/gotracker/playback/voice/vol0optimization" +) + +type ( + Period = types.Period + Volume = types.Volume + Panning = types.Panning +) + +type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + SampleRate period.Frequency + AutoVibrato autovibrato.AutoVibratoSettings + Data instrument.Data + VoiceFilter filter.Filter + PluginFilter filter.Filter + Vol0Optimization vol0optimization.Vol0OptimizationSettings + FadeOut fadeout.Settings + PitchPan instrument.PitchPan + VolEnv envelope.Envelope[TVolume] + VolEnvFinishFadesOut bool + PanEnv envelope.Envelope[TPanning] + PitchFiltMode bool // true = filter, false = pitch + PitchFiltEnv envelope.Envelope[filter.PitchFiltValue] // this is either pitch or filter +} diff --git a/voice/envelope/envelope.go b/voice/envelope/envelope.go index 38f0890..548f0a9 100644 --- a/voice/envelope/envelope.go +++ b/voice/envelope/envelope.go @@ -1,15 +1,16 @@ package envelope import ( + "math" + "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] + tick int + stopped bool + env *Envelope[T] } func (e *State[T]) Init(env *Envelope[T]) { @@ -24,10 +25,9 @@ func (e *State[T]) Init(env *Envelope[T]) { func (e State[T]) Clone() State[T] { return State[T]{ - position: 0, - length: e.length, - stopped: false, - env: e.env, + tick: e.tick, + stopped: e.stopped, + env: e.env, } } @@ -53,65 +53,62 @@ func (e *State[T]) Reset() { return } - e.position = 0 - pos, _, _ := e.calcLoopedPos(true) - if pos < len(e.env.Values) { - e.length = e.env.Values[pos].Length() - } + e.tick = 0 + e.stopped = false } -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 +func (e *State[T]) Pos() int { + return e.tick } // GetCurrentValue returns the current value -func (e *State[T]) GetCurrentValue(keyOn bool) (*EnvPoint[T], *EnvPoint[T], float32) { +func (e *State[T]) GetCurrentValue(keyOn, prevKeyOn bool) (*EnvPoint[T], *EnvPoint[T], float64) { if e.stopped { return nil, nil, 0 } - pos, npos, looped := e.calcLoopedPos(keyOn) - if pos >= len(e.env.Values) { + nPoints := len(e.env.Values) + + if nPoints == 0 { return nil, nil, 0 } - if npos >= len(e.env.Values) { - npos = pos + curTick, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.tick, e.env.Length, prevKeyOn) + nextTick, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.tick+1, e.env.Length, keyOn) + + var cur EnvPoint[T] + for _, it := range e.env.Values { + if it.Pos > curTick { + break + } + cur = it } - 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) - } + var next EnvPoint[T] + foundNext := false + for _, it := range e.env.Values { + if it.Pos > nextTick { + next = it + foundNext = true + break } - t = 1 - (l / float32(tl)) } - switch { - case t < 0: - t = 0 - case t > 1: - t = 1 + + if !foundNext { + return &cur, &cur, 0 + } + + t := float64(0) + if cur.Length > 0 { + if tl := curTick - cur.Pos; tl > 0 { + t = max(min((float64(tl)/float64(cur.Length)), 1), 0) + } } return &cur, &next, t } // Advance advances the state by 1 tick -func (e *State[T]) Advance(keyOn bool, prevKeyOn bool) bool { +func (e *State[T]) Advance(keyOn bool) bool { if e.stopped { return false } @@ -126,26 +123,28 @@ func (e *State[T]) Advance(keyOn bool, prevKeyOn bool) bool { } } -loopAdvance: - e.length-- - if e.length > 0 { - return false + nPoints := len(e.env.Values) + + if nPoints == 0 { + e.stopped = true + return true } - if keyOn != prevKeyOn && prevKeyOn { - p, _, _ := e.calcLoopedPos(prevKeyOn) - e.position = p + + e.tick++ + curTick, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.tick, e.env.Length, keyOn) + + found := false + for _, i := range e.env.Values { + if i.Pos >= curTick && i.Length != math.MaxInt { + found = true + break + } } - e.position++ - pos, _, _ := e.calcLoopedPos(keyOn) - if pos >= len(e.env.Values) { + if !found { e.stopped = true return true } - e.length = e.env.Values[pos].Length() - if e.length <= 0 { - goto loopAdvance - } return false } diff --git a/voice/envelope/instrumentenv.go b/voice/envelope/instrumentenv.go index 89104b4..a18a2d9 100755 --- a/voice/envelope/instrumentenv.go +++ b/voice/envelope/instrumentenv.go @@ -1,34 +1,21 @@ 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 + Enabled bool + Loop loop.Loop + Sustain loop.Loop + Length int + Values []EnvPoint[T] } // 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 + 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/filterapplier.go b/voice/filter/filterapplier.go old mode 100755 new mode 100644 similarity index 53% rename from voice/filterapplier.go rename to voice/filter/filterapplier.go index c1eee01..e91e197 --- a/voice/filterapplier.go +++ b/voice/filter/filterapplier.go @@ -1,11 +1,11 @@ -package voice +package filter import ( "github.com/gotracker/gomixing/volume" ) -// FilterApplier is an interface for applying a filter to a sample stream -type FilterApplier interface { +// 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/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 0cbc6a0..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[TPeriod period.Period] interface { - SetPeriod(period TPeriod) - GetPeriod() TPeriod - SetPeriodDelta(delta period.Delta) - GetPeriodDelta() period.Delta - GetFinalPeriod() TPeriod -} 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/oscillator/oscillator.go b/voice/oscillator/oscillator.go index 5c3c9b4..19fe5d4 100755 --- a/voice/oscillator/oscillator.go +++ b/voice/oscillator/oscillator.go @@ -9,6 +9,7 @@ type Oscillator interface { GetWave(depth float32) float32 Advance(speed int) SetWaveform(table WaveTableSelect) + 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/pitchenveloper.go b/voice/pitchenveloper.go deleted file mode 100755 index f1d31a2..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[TPeriod period.Period] interface { - EnablePitchEnvelope(enabled bool) - IsPitchEnvelopeEnabled() bool - GetCurrentPitchEnvelope() period.Delta - SetPitchEnvelopePosition(pos int) -} 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/transaction.go b/voice/transaction.go deleted file mode 100755 index 8f87b8b..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[TPeriod period.Period] interface { - Cancel() - Commit() - GetVoice() Voice - Clone() Transaction[TPeriod] - - SetActive(active bool) - IsPendingActive() (bool, bool) - IsCurrentlyActive() bool - - Attack() - Release() - Fadeout() - SetPeriod(period TPeriod) - GetPendingPeriod() (TPeriod, bool) - GetCurrentPeriod() TPeriod - 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/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 510e4f4..ffa5758 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -1,247 +1,132 @@ package voice import ( - "time" - - "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/pcm" + "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 -} + DumpState(ch index.Channel, t tracing.Tracer) -type VoiceTransactioner[TPeriod period.Period] interface { - StartTransaction() Transaction[TPeriod] -} + // Configuration + Reset() -// Controller is the instrument actuation control interface -type Controller interface { + // Actions Attack() Release() Fadeout() - IsKeyOn() bool - IsFadeout() bool - IsDone() bool - SetActive(active bool) - IsActive() bool -} - -// == 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 == + Stop() -// SetPeriod sets the period into the frequency modulator, if the interface for it exists on the voice -func SetPeriod[TPeriod period.Period](v Voice, period TPeriod) { - if fm, ok := v.(FreqModulator[TPeriod]); ok { - fm.SetPeriod(period) - } -} - -// GetPeriod gets the period from the frequency modulator, if the interface for it exists on the voice -func GetPeriod[TPeriod period.Period](v Voice) TPeriod { - if fm, ok := v.(FreqModulator[TPeriod]); ok { - return fm.GetPeriod() - } - var empty TPeriod - return empty -} - -// SetPeriodDelta sets the period delta into the frequency modulator, if the interface for it exists on the voice -func SetPeriodDelta[TPeriod period.Period](v Voice, delta period.Delta) { - if fm, ok := v.(FreqModulator[TPeriod]); ok { - fm.SetPeriodDelta(delta) - } -} - -// GetPeriodDelta returns the period delta from the frequency modulator, if the interface for it exists on the voice -func GetPeriodDelta[TPeriod period.Period](v Voice) period.Delta { - if fm, ok := v.(FreqModulator[TPeriod]); ok { - return fm.GetPeriodDelta() - } - var empty period.Delta - return empty -} + // State Machine Update + Advance() -// GetFinalPeriod returns the final period from the frequency modulator, if the interface for it exists on the voice -func GetFinalPeriod[TPeriod period.Period](v Voice) TPeriod { - if fm, ok := v.(FreqModulator[TPeriod]); ok { - return fm.GetFinalPeriod() - } - var empty TPeriod - return empty -} - -// == 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) -} - -// 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) -} - -// == PanModulator == - -// 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) - } -} - -// 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 -} - -// 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 -} - -// == 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) - } -} - -// 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 -} - -// 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) - } -} - -// == PanEnveloper == - -// 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) - } -} - -// 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) - } -} - -// == PitchEnveloper == - -// EnablePitchEnvelope sets the pitch envelope enable flag, if the interface for it exists on the voice -func EnablePitchEnvelope[TPeriod period.Period](v Voice, enabled bool) { - if pe, ok := v.(PitchEnveloper[TPeriod]); ok { - pe.EnablePitchEnvelope(enabled) - } -} - -// SetPitchEnvelopePosition sets the pitch envelope position, if the interface for it exists on the voice -func SetPitchEnvelopePosition[TPeriod period.Period](v Voice, pos int) { - if pe, ok := v.(PitchEnveloper[TPeriod]); ok { - pe.SetPitchEnvelopePosition(pos) - } + // General Parameters + IsDone() bool } -// == FilterEnveloper == +type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + Voice -// 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) - } -} + // Configuration + Setup(config InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + SetPCM(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop, defaultVolume 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) - } + // Render + Render(panningMatrix volume.Matrix, details render.Details, renderChannel *playerRender.Channel[TGlobalVolume, TMixingVolume, TPanning]) (*mixing.Data, error) } -// 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 AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { + // Amp/Volume Parameters + IsActive() bool + SetActive(active bool) + GetMixingVolume() TMixingVolume + SetMixingVolume(v TMixingVolume) + GetVolume() TVolume + SetVolume(v TVolume) + GetVolumeDelta() types.VolumeDelta + SetVolumeDelta(d types.VolumeDelta) + GetFinalVolume() volume.Volume } -// == Envelopes == - -// SetEnvelopePosition sets the envelope position(s) on the voice -func SetAllEnvelopePositions[TPeriod period.Period](v Voice, pos int) { - SetVolumeEnvelopePosition(v, pos) - SetPanEnvelopePosition(v, pos) - SetPitchEnvelopePosition[TPeriod](v, pos) - SetFilterEnvelopePosition(v, pos) +type FadeoutModulator interface { + IsFadeout() bool + GetFadeoutVolume() volume.Volume +} + +type FreqModulator[TPeriod Period] interface { + // Frequency/Pitch Parameters + GetPeriod() TPeriod + SetPeriod(p TPeriod) + GetPeriodDelta() period.Delta + SetPeriodDelta(delta period.Delta) + GetFinalPeriod() TPeriod +} + +type Sampler interface { + // Sampler Parameters + SetPos(pos sampling.Pos) error + GetPos() (sampling.Pos, error) +} + +type PanModulator[TPanning Panning] interface { + // Pan Parameters + GetPan() TPanning + SetPan(pan TPanning) + GetPanDelta() types.PanDelta + SetPanDelta(d types.PanDelta) + GetFinalPan() TPanning +} + +type PitchPanModulator[TPanning Panning] interface { + SetPitchPanNote(st note.Semitone) + IsPitchPanEnabled() bool + EnablePitchPan(enabled bool) + GetPanSeparation() float32 +} + +type VolumeEnvelope[TGlobalVolume, TMixingVolume, TVolume Volume] interface { + // Amp/Volume Envelope Parameters + IsVolumeEnvelopeEnabled() bool + EnableVolumeEnvelope(enabled bool) + GetVolumeEnvelopePosition() int + SetVolumeEnvelopePosition(pos int) + GetCurrentVolumeEnvelope() TVolume +} + +type PitchEnvelope[TPeriod Period] interface { + // Frequency/Pitch Envelope Parameters + IsPitchEnvelopeEnabled() bool + EnablePitchEnvelope(enabled bool) + GetPitchEnvelopePosition() int + SetPitchEnvelopePosition(pos int) + GetCurrentPitchEnvelope() period.Delta +} + +type PanEnvelope[TPanning Panning] interface { + // Pan Envelope Parameters + IsPanEnvelopeEnabled() bool + EnablePanEnvelope(enabled bool) + GetPanEnvelopePosition() int + SetPanEnvelopePosition(pos int) + GetCurrentPanEnvelope() TPanning +} + +type FilterEnvelope interface { + // Filter Envelope Parameters + IsFilterEnvelopeEnabled() bool + EnableFilterEnvelope(enabled bool) + GetFilterEnvelopePosition() int + SetFilterEnvelopePosition(pos int) + GetCurrentFilterEnvelope() uint8 } diff --git a/voice/voicefactory.go b/voice/voicefactory.go new file mode 100644 index 0000000..0e2dac4 --- /dev/null +++ b/voice/voicefactory.go @@ -0,0 +1,5 @@ +package voice + +type VoiceFactory[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { + NewVoice() RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] +} diff --git a/voice/vol0optimization/settings.go b/voice/vol0optimization/settings.go new file mode 100644 index 0000000..913ceea --- /dev/null +++ b/voice/vol0optimization/settings.go @@ -0,0 +1,6 @@ +package vol0optimization + +type Vol0OptimizationSettings struct { + Enabled bool + MaxTicksAt0 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) -} From c7491291a57b01c071e7470dd832f1ca9ea12b5f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 13 Jan 2024 12:08:33 -0800 Subject: [PATCH 30/63] round 2 of playback cleanup --- channel.go | 3 - effect.go | 16 +-- format/it/channel/data.go | 73 ---------- format/it/channel/util.go | 10 -- format/it/load/instrument.go | 10 +- format/it/voice/render.go | 5 +- format/s3m/channel/data.go | 91 ------------- format/s3m/channel/effect_tremolo.go | 7 - format/s3m/channel/effect_tremor.go | 7 - format/s3m/channel/effect_vibrato.go | 8 -- format/s3m/channel/effectfactory.go | 9 +- format/s3m/channel/memory.go | 48 +------ format/s3m/channel/util.go | 189 ++------------------------ format/s3m/load/s3mformat.go | 2 - format/s3m/voice/render.go | 5 +- format/xm/channel/data.go | 73 ---------- format/xm/load/xmformat.go | 4 +- format/xm/voice/render.go | 5 +- player/machine/channel_noteaction.go | 2 +- player/machine/channel_tick.go | 31 ++--- player/machine/machine.go | 4 +- player/machine/machine_instruction.go | 40 +++--- player/machine/machine_tick.go | 45 ++++-- player/machine/pastnote.go | 5 +- player/machine/ticker.go | 87 ++++++++++-- player/op/channeltargets.go | 21 --- player/posttick.go | 14 -- player/pretick.go | 14 -- player/{state => }/render/details.go | 0 player/state/renderstate.go | 34 ----- player/tick.go | 14 -- player/tracker.go | 16 +-- song/channelmemory.go | 1 + voice/component/envelope.go | 149 +++++++++++++++++--- voice/component/envelope_filter.go | 16 +-- voice/component/envelope_pan.go | 16 +-- voice/component/envelope_pitch.go | 16 +-- voice/component/envelope_volume.go | 16 +-- voice/envelope/envelope.go | 150 +------------------- voice/envelope/instrumentenv.go | 21 --- voice/envelope/point.go | 8 ++ voice/voice.go | 5 +- 42 files changed, 349 insertions(+), 941 deletions(-) delete mode 100644 format/it/channel/util.go delete mode 100644 player/op/channeltargets.go delete mode 100644 player/posttick.go delete mode 100644 player/pretick.go rename player/{state => }/render/details.go (100%) delete mode 100644 player/state/renderstate.go delete mode 100644 player/tick.go delete mode 100755 voice/envelope/instrumentenv.go create mode 100644 voice/envelope/point.go diff --git a/channel.go b/channel.go index a09c4ef..3d92f44 100644 --- a/channel.go +++ b/channel.go @@ -2,7 +2,6 @@ package playback import ( "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" @@ -56,5 +55,3 @@ type Channel[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData son GetActiveState() *ChannelState[TPeriod, TVolume, TPanning] GetTargetState() *ChannelState[TPeriod, TVolume, TPanning] } - -type ChannelTargeter[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] func(out *op.ChannelTargets[TPeriod, TVolume, TPanning], d TChannelData, s song.Data, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error diff --git a/effect.go b/effect.go index 89cdb1d..791c348 100644 --- a/effect.go +++ b/effect.go @@ -186,18 +186,18 @@ func DoEffect[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData so //////// -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) RowStart(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { +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.DoInstructionRowStart(ch, effect); err != nil { + if err := m.DoInstructionOrderStart(ch, effect); err != nil { return err } } return nil } -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) PreTick(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) 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 := m.DoInstructionPreTick(ch, effect); err != nil { + if err := m.DoInstructionRowStart(ch, effect); err != nil { return err } } @@ -213,18 +213,18 @@ func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, return nil } -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) PostTick(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick 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 := m.DoInstructionPostTick(ch, effect); err != nil { + if err := m.DoInstructionRowEnd(ch, effect); err != nil { return err } } return nil } -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) RowEnd(ch index.Channel, m machine.Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) 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 := m.DoInstructionRowEnd(ch, effect); err != nil { + if err := m.DoInstructionOrderEnd(ch, effect); err != nil { return err } } diff --git a/format/it/channel/data.go b/format/it/channel/data.go index 21d1bab..321e5ba 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -5,7 +5,6 @@ import ( "strings" itfile "github.com/gotracker/goaudiofile/music/tracked/it" - "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" @@ -18,7 +17,6 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/player/machine/instruction" - "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/song" ) @@ -178,74 +176,3 @@ func (d Data[TPeriod]) ToInstructions(m machine.Machine[TPeriod, itVolume.FineVo return instructions, nil } - -func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod, itVolume.Volume, itPanning.Panning], d Data[TPeriod], s song.Data, cs playback.Channel[TPeriod, *Memory, Data[TPeriod], itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) error { - var n note.Note = note.EmptyNote{} - inst := cs.GetActiveState().Instrument - prevInst := inst - - if d.HasNote() || d.HasInstrument() { - instID := d.GetInstrument(cs.GetNoteSemitone()) - n = d.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = prevInst - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - if !note.IsEmpty(n) && inst == nil { - inst = prevInst - } - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(itVolume.ToItVolume(inst.GetDefaultVolumeGeneric())) - } - out.NoteAction.Set(note.ActionRetrigger) - out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) - } - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Reset() - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else if !note.IsEmpty(n) { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } else { - out.NoteAction.Set(note.ActionCut) - } - } - - if d.HasVolume() { - v := d.GetVolume() - if v.IsUseInstrumentVol() { - if inst != nil { - v = itVolume.ToItVolume(inst.GetDefaultVolumeGeneric()) - } - } - out.TargetVolume.Set(v) - } - - return nil -} diff --git a/format/it/channel/util.go b/format/it/channel/util.go deleted file mode 100644 index 65d7286..0000000 --- a/format/it/channel/util.go +++ /dev/null @@ -1,10 +0,0 @@ -package channel - -var ( - volSlideTwoThirdsTable = [...]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, - } -) diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 2834ec1..50887c9 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -67,7 +67,7 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData }, VolEnv: envelope.Envelope[itVolume.Volume]{ Enabled: (inst.Flags & itfile.IMPIOldFlagUseVolumeEnvelope) != 0, - Values: make([]envelope.EnvPoint[itVolume.Volume], 0), + Values: make([]envelope.Point[itVolume.Volume], 0), }, } @@ -102,7 +102,7 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData } for i := range inst.VolumeEnvelope { - var out envelope.EnvPoint[itVolume.Volume] + var out envelope.Point[itVolume.Volume] in1 := inst.VolumeEnvelope[i] vol := itVolume.Volume(uint8(in1)) if vol > itVolume.Volume(itVolume.MaxItVolume) { @@ -265,20 +265,20 @@ 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) if i+1 < len(outEnv.Values) { in2 := inEnv.NodePoints[i+1] ticks := int(in2.Tick) - int(in1.Tick) - var out envelope.EnvPoint[T] + var out envelope.Point[T] out.Length = ticks out.Pos = int(in1.Tick) out.Y = y outEnv.Values[i] = out } else { - outEnv.Values[i] = envelope.EnvPoint[T]{ + outEnv.Values[i] = envelope.Point[T]{ Pos: int(in1.Tick), Length: math.MaxInt, Y: y, diff --git a/format/it/voice/render.go b/format/it/voice/render.go index 7d79de0..5e2c4d0 100644 --- a/format/it/voice/render.go +++ b/format/it/voice/render.go @@ -5,11 +5,10 @@ import ( "github.com/gotracker/gomixing/volume" itPanning "github.com/gotracker/playback/format/it/panning" itVolume "github.com/gotracker/playback/format/it/volume" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/player/render" ) -func (v *itVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *playerRender.Channel[itVolume.FineVolume, itVolume.FineVolume, itPanning.Panning]) (*mixing.Data, error) { +func (v *itVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *render.Channel[itVolume.FineVolume, itVolume.FineVolume, itPanning.Panning]) (*mixing.Data, error) { if v.IsDone() { return nil, nil } diff --git a/format/s3m/channel/data.go b/format/s3m/channel/data.go index ea587d8..76ac98f 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -5,7 +5,6 @@ import ( "strings" s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" @@ -17,7 +16,6 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/player/machine/instruction" - "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/song" ) @@ -131,92 +129,3 @@ func (d Data) ToInstructions(m machine.Machine[period.Amiga, s3mVolume.Volume, s return instructions, nil } - -// NoteFromS3MNote converts an S3M file note into a player note -func NoteFromS3MNote(sn s3mfile.Note) note.Note { - switch { - case sn == s3mfile.EmptyNote: - return note.EmptyNote{} - case sn == s3mfile.StopNote: - return note.StopOrReleaseNote{} - default: - k := uint8(sn.Key()) & 0x0f - o := uint8(sn.Octave()) & 0x0f - if k < 12 && o < 10 { - s := note.Semitone(o*12 + k) - return note.Normal(s) - } - } - return note.InvalidNote{} -} - -func GetTargetsFromData(out *op.ChannelTargets[period.Amiga, s3mVolume.Volume, s3mPanning.Panning], d Data, s song.Data, cs playback.Channel[period.Amiga, *Memory, Data, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { - var n note.Note = note.EmptyNote{} - inst := cs.GetActiveState().Instrument - prevInst := inst - - if d.HasNote() || d.HasInstrument() { - instID := d.GetInstrument(cs.GetNoteSemitone()) - n = d.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = prevInst - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - if !note.IsEmpty(n) && inst == nil { - inst = prevInst - } - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(s3mVolume.VolumeToS3M(inst.GetDefaultVolumeGeneric())) - } - out.NoteAction.Set(note.ActionRetrigger) - out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) - } - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Reset() - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else if !note.IsEmpty(n) { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } else { - out.NoteAction.Set(note.ActionCut) - } - } - - if d.HasVolume() { - v := d.GetVolume() - if v.IsUseInstrumentVol() { - if inst != nil { - v = s3mVolume.VolumeToS3M(inst.GetDefaultVolumeGeneric()) - } - } - out.TargetVolume.Set(v) - } - - return nil -} diff --git a/format/s3m/channel/effect_tremolo.go b/format/s3m/channel/effect_tremolo.go index fc083ce..90e6297 100644 --- a/format/s3m/channel/effect_tremolo.go +++ b/format/s3m/channel/effect_tremolo.go @@ -3,7 +3,6 @@ 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/index" @@ -15,12 +14,6 @@ import ( // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - func (e Tremolo) String() string { return fmt.Sprintf("R%0.2x", DataEffect(e)) } diff --git a/format/s3m/channel/effect_tremor.go b/format/s3m/channel/effect_tremor.go index bffec0e..54a8982 100644 --- a/format/s3m/channel/effect_tremor.go +++ b/format/s3m/channel/effect_tremor.go @@ -3,7 +3,6 @@ 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/index" @@ -14,12 +13,6 @@ import ( // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - return nil -} - func (e Tremor) String() string { return fmt.Sprintf("I%0.2x", DataEffect(e)) } diff --git a/format/s3m/channel/effect_vibrato.go b/format/s3m/channel/effect_vibrato.go index 7163a30..5d42914 100644 --- a/format/s3m/channel/effect_vibrato.go +++ b/format/s3m/channel/effect_vibrato.go @@ -3,7 +3,6 @@ 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/index" @@ -14,13 +13,6 @@ import ( // 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 S3MChannel, p playback.Playback) error { - cs.ResetRetriggerCount() - cs.UnfreezePlayback() - return nil -} - func (e Vibrato) String() string { return fmt.Sprintf("H%0.2x", DataEffect(e)) } diff --git a/format/s3m/channel/effectfactory.go b/format/s3m/channel/effectfactory.go index 73d3211..dd15eca 100644 --- a/format/s3m/channel/effectfactory.go +++ b/format/s3m/channel/effectfactory.go @@ -1,6 +1,7 @@ package channel import ( + "github.com/gotracker/playback" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/song" ) @@ -8,7 +9,7 @@ import ( type ChannelCommand DataEffect // Factory produces an effect for the provided channel pattern data -func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) EffectS3M { +func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) playback.Effect { if data == nil { return nil } @@ -85,7 +86,7 @@ func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) EffectS return UnhandledCommand{Command: d.Command, Info: d.Info} } -func specialEffect(mem *Memory, data 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 @@ -124,7 +125,7 @@ func specialEffect(mem *Memory, data Data) EffectS3M { return UnhandledCommand{Command: data.Command, Info: data.Info} } -func volumeSlideFactory(mem *Memory, cd uint8, ce DataEffect) EffectS3M { +func volumeSlideFactory(mem *Memory, cd uint8, ce DataEffect) playback.Effect { xy := mem.LastNonZero(ce) x := DataEffect(xy >> 4) y := DataEffect(xy & 0x0F) @@ -147,7 +148,7 @@ func volumeSlideFactory(mem *Memory, cd uint8, ce DataEffect) EffectS3M { //return UnhandledCommand{Command: cd, Info: xy} } -func soundControlEffect(data Data) EffectS3M { +func soundControlEffect(data Data) playback.Effect { switch data.Info & 0xF { case 0x0: // Surround Off case 0x1: // Surround On diff --git a/format/s3m/channel/memory.go b/format/s3m/channel/memory.go index 235d9f9..d87c247 100644 --- a/format/s3m/channel/memory.go +++ b/format/s3m/channel/memory.go @@ -1,18 +1,12 @@ 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 { - inst InstID - portaToNote memory.Value[DataEffect] vibratoSpeed memory.Value[DataEffect] vibratoDepth memory.Value[DataEffect] @@ -23,30 +17,11 @@ 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 } -// Inst gets or sets the most recent non-zero value (or input) for inst -func (m *Memory) Inst(input InstID) InstID { - if input == 0 { - return m.inst - } - - m.inst = input - return input -} - -// 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) @@ -99,32 +74,13 @@ 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() { if m.Shared.ResetMemoryAtStartOfOrder0 { - m.inst = 0 m.portaToNote.Reset() m.vibratoSpeed.Reset() m.vibratoDepth.Reset() diff --git a/format/s3m/channel/util.go b/format/s3m/channel/util.go index 594f0e3..9309a43 100644 --- a/format/s3m/channel/util.go +++ b/format/s3m/channel/util.go @@ -1,184 +1,25 @@ package channel import ( - "github.com/gotracker/gomixing/volume" - "github.com/heucuva/comparison" + s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/playback" - 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/voice/oscillator" ) -type EffectS3M = playback.Effect -type S3MChannel = playback.Channel[period.Amiga, *Memory, Data, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] - -// 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 -} - -func oldDoVolSlide(cs S3MChannel, delta float32, multiplier float32) error { - active := cs.GetActiveState() - v := active.GetVolume() - vol := int16(float32(v) + (delta * multiplier)) - if vol >= 64 { - vol = 63 - } - if vol < 0 { - vol = 0 - } - sv := s3mVolume.Volume(vol) - active.SetVolume(sv) - return nil -} - -func oldDoPortaUp(cs S3MChannel, amount float32, multiplier float32) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = cur.PortaUp(delta) - active.Period = cur - return nil -} - -func doPortaUpToNote(cs S3MChannel, amount float32, multiplier float32, target period.Amiga) error { - if target.IsInvalid() { - return nil - } - - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - d := period.Delta(delta) - cur = cur.Add(d) - if period.ComparePeriods(cur, target) == comparison.SpaceshipLeftGreater { - cur = target - } - active.Period = cur - return nil -} - -func oldDoPortaDown(cs S3MChannel, amount float32, multiplier float32) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = cur.PortaDown(delta) - active.Period = cur - return nil -} - -func doPortaDownToNote(cs S3MChannel, amount float32, multiplier float32, target period.Amiga) error { - active := cs.GetActiveState() - cur := active.Period - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - d := period.Delta(-delta) - cur = cur.Add(d) - if period.ComparePeriods(cur, target) == comparison.SpaceshipRightGreater { - cur = target - } - active.Period = cur - return nil -} - -func oldDoVibrato(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, DataEffect(speed), DataEffect(depth), multiplier, mem.VibratoOscillator()) - d := period.Delta(delta) - cs.SetPeriodDelta(d) - return nil -} - -func oldDoTremor(cs S3MChannel, 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() +// NoteFromS3MNote converts an S3M file note into a player note +func NoteFromS3MNote(sn s3mfile.Note) note.Note { + switch { + case sn == s3mfile.EmptyNote: + return note.EmptyNote{} + case sn == s3mfile.StopNote: + return note.StopOrReleaseNote{} + default: + k := uint8(sn.Key()) & 0x0f + o := uint8(sn.Octave()) & 0x0f + if k < 12 && o < 10 { + s := note.Semitone(o*12 + k) + return note.Normal(s) } } - cs.SetVolumeActive(tremor.IsActive()) - return nil -} - -func oldDoArpeggio(cs S3MChannel, 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.GetTargetState().Pos = cs.GetActiveState().Pos - return nil -} - -var ( - volSlideTwoThirdsTable = [...]s3mVolume.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 S3MChannel) error { - active := cs.GetActiveState() - vol := active.GetVolume() - if vol >= 64 { - vol = 63 - } - active.SetVolume(volSlideTwoThirdsTable[vol]) - return nil -} - -func doTremolo(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32) error { - mem := cs.GetMemory() - delta := calculateWaveTable(cs, currentTick, speed, depth, multiplier, mem.TremoloOscillator()) - return oldDoVolSlide(cs, delta, 1.0) -} - -func calculateWaveTable(cs S3MChannel, currentTick int, speed DataEffect, depth DataEffect, multiplier float32, o oscillator.Oscillator) float32 { - delta := o.GetWave(float32(depth)) * multiplier - o.Advance(int(speed)) - return delta + return note.InvalidNote{} } diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index f4cd0a8..7dd7d74 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -342,8 +342,6 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, cs.DefaultFilterName = "amigalpf" } - cs.Memory.ResetOscillators() - pf := f.Panning[chNum] if pf.IsValid() { cs.InitialPanning = s3mPanning.Panning(pf.Value()) diff --git a/format/s3m/voice/render.go b/format/s3m/voice/render.go index a81a195..1fc670e 100644 --- a/format/s3m/voice/render.go +++ b/format/s3m/voice/render.go @@ -5,11 +5,10 @@ import ( "github.com/gotracker/gomixing/volume" s3mPanning "github.com/gotracker/playback/format/s3m/panning" s3mVolume "github.com/gotracker/playback/format/s3m/volume" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/player/render" ) -func (v *s3mVoice) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *playerRender.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]) (*mixing.Data, error) { +func (v *s3mVoice) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *render.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]) (*mixing.Data, error) { if v.IsDone() { return nil, nil } diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index c1897f8..167b0c0 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -5,7 +5,6 @@ import ( "strings" xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" - "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback" @@ -18,7 +17,6 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/player/machine/instruction" - "github.com/gotracker/playback/player/op" "github.com/gotracker/playback/song" ) @@ -176,74 +174,3 @@ func (d Data[TPeriod]) ToInstructions(m machine.Machine[TPeriod, xmVolume.XmVolu return instructions, nil } - -func GetTargetsFromData[TPeriod period.Period](out *op.ChannelTargets[TPeriod, xmVolume.XmVolume, xmPanning.Panning], d Data[TPeriod], s song.Data, cs playback.Channel[TPeriod, *Memory, Data[TPeriod], xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) error { - var n note.Note = note.EmptyNote{} - inst := cs.GetActiveState().Instrument - prevInst := inst - - if d.HasNote() || d.HasInstrument() { - instID := d.GetInstrument(cs.GetNoteSemitone()) - n = d.GetNote() - var ( - wantRetrigger bool - wantRetriggerVol bool - ) - if instID.IsEmpty() { - // use current - inst = prevInst - wantRetrigger = true - } else if !s.IsValidInstrumentID(instID) { - out.TargetInst.Set(nil) - n = note.InvalidNote{} - } else { - var str note.Semitone - inst, str = s.GetInstrument(instID) - n = note.CoalesceNoteSemitone(n, str) - if !note.IsEmpty(n) && inst == nil { - inst = prevInst - } - wantRetrigger = true - wantRetriggerVol = true - } - - if wantRetrigger { - out.TargetInst.Set(inst) - out.TargetPos.Set(sampling.Pos{}) - if inst != nil { - if wantRetriggerVol { - out.TargetVolume.Set(xmVolume.ToVolumeXM(inst.GetDefaultVolumeGeneric())) - } - out.NoteAction.Set(note.ActionRetrigger) - out.TargetNewNoteAction.Set(inst.GetNewNoteAction()) - } - } - } - - if note.IsInvalid(n) { - out.TargetPeriod.Reset() - out.NoteAction.Set(note.ActionCut) - } else if note.IsRelease(n) { - out.NoteAction.Set(note.ActionRelease) - } else if !note.IsEmpty(n) { - if nn, ok := n.(note.Normal); ok { - st := note.Semitone(nn) - out.TargetStoredSemitone.Set(st) - out.NoteCalcST.Set(st) - } else { - out.NoteAction.Set(note.ActionCut) - } - } - - if d.HasVolume() { - v := d.GetVolume() - if v.IsUseInstrumentVol() { - if inst != nil { - v = xmVolume.ToVolumeXM(inst.GetDefaultVolumeGeneric()) - } - } - out.TargetVolume.Set(v) - } - - return nil -} diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index aadefab..804214b 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -146,7 +146,7 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid volEnvSustainMode = loop.ModeNormal } - ii.VolEnv.Values = make([]envelope.EnvPoint[xmVolume.XmVolume], 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) @@ -172,7 +172,7 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid panEnvSustainMode = loop.ModeNormal } - ii.PanEnv.Values = make([]envelope.EnvPoint[xmPanning.Panning], 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 diff --git a/format/xm/voice/render.go b/format/xm/voice/render.go index d50cb3d..45b3f6d 100644 --- a/format/xm/voice/render.go +++ b/format/xm/voice/render.go @@ -5,11 +5,10 @@ import ( "github.com/gotracker/gomixing/volume" xmPanning "github.com/gotracker/playback/format/xm/panning" xmVolume "github.com/gotracker/playback/format/xm/volume" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/player/render" ) -func (v *xmVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *playerRender.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) (*mixing.Data, error) { +func (v *xmVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *render.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) (*mixing.Data, error) { if v.IsDone() { return nil, nil } diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index e4dc50a..c9b3bc9 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -9,7 +9,7 @@ import ( "github.com/gotracker/playback/voice/vol0optimization" ) -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { +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 diff --git a/player/machine/channel_tick.go b/player/machine/channel_tick.go index 851226a..6eb30cd 100644 --- a/player/machine/channel_tick.go +++ b/player/machine/channel_tick.go @@ -9,6 +9,11 @@ import ( "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 { + c.memory.StartOrder() + 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() @@ -102,19 +107,6 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowS return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) PreTick(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { - for _, i := range c.instructions { - if err := m.DoInstructionPreTick(ch, i); err != nil { - return err - } - } - - if err := c.doNoteAction(ch, m); err != nil { - return err - } - 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 { @@ -125,15 +117,6 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) PostTick(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { - for _, i := range c.instructions { - if err := m.DoInstructionPostTick(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 { @@ -167,3 +150,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowE } 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/machine.go b/player/machine/machine.go index 7156c98..a25dd52 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -107,11 +107,11 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann SetChannelPitchEnvelopeEnable(ch index.Channel, enabled bool) error // Instructions + DoInstructionOrderStart(ch index.Channel, i instruction.Instruction) error DoInstructionRowStart(ch index.Channel, i instruction.Instruction) error - DoInstructionPreTick(ch index.Channel, i instruction.Instruction) error DoInstructionTick(ch index.Channel, i instruction.Instruction) error - DoInstructionPostTick(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 { diff --git a/player/machine/machine_instruction.go b/player/machine/machine_instruction.go index b354032..828e562 100644 --- a/player/machine/machine_instruction.go +++ b/player/machine/machine_instruction.go @@ -5,30 +5,30 @@ import ( "github.com/gotracker/playback/player/machine/instruction" ) -type instructionRowStart[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { +type instructionOrderStart[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { instruction.Instruction - RowStart(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error + OrderStart(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]) +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.RowStart(ch, m) + return ii.OrderStart(ch, m) } -type instructionPreTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { +type instructionRowStart[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { instruction.Instruction - PreTick(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error + RowStart(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionPreTick(ch index.Channel, i instruction.Instruction) error { - ii, ok := i.(instructionPreTick[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) +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.PreTick(ch, m, m.ticker.current.tick) + return ii.RowStart(ch, m) } type instructionTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { @@ -44,28 +44,28 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoIn return ii.Tick(ch, m, m.ticker.current.tick) } -type instructionPostTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { +type instructionRowEnd[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { instruction.Instruction - PostTick(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], tick int) error + RowEnd(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoInstructionPostTick(ch index.Channel, i instruction.Instruction) error { - ii, ok := i.(instructionPostTick[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) +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.PostTick(ch, m, m.ticker.current.tick) + return ii.RowEnd(ch, m) } -type instructionRowEnd[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { +type instructionOrderEnd[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { instruction.Instruction - RowEnd(ch index.Channel, m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error + OrderEnd(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]) +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.RowEnd(ch, m) + return ii.OrderEnd(ch, m) } diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index 16d3b79..190be66 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -1,6 +1,7 @@ package machine import ( + "errors" "fmt" "github.com/gotracker/gomixing/mixing" @@ -11,7 +12,7 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" - stateRender "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" ) @@ -21,7 +22,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } for i := range m.channels { - if err := m.channels[i].PreTick(index.Channel(i), m); err != nil { + if err := m.channels[i].DoNoteAction(index.Channel(i), m); err != nil { return nil, err } } @@ -48,11 +49,14 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick renderRow.RowText = m.rowStringer } - if err := runTick(&m.ticker, m); err != nil { - return nil, err + tickErr := runTick(&m.ticker, m) + if tickErr != nil { + if !errors.Is(tickErr, song.ErrStopSong) { + return nil, tickErr + } } - details := stateRender.Details{ + details := render.Details{ Mix: s.Mixer(), Panmixer: s.GetPanMixer(), SamplerSpeed: s.GetSamplerSpeed(), @@ -79,7 +83,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick c.cv.DumpState(ch, m.us.Tracer) data, err := c.cv.Render(centerAheadPan, details, rc) if err != nil { - return nil, err + return nil, errors.Join(tickErr, err) } c.cv.Advance() if data != nil { @@ -88,7 +92,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick pnData, err := c.pn.Render(centerAheadPan, details, rc) if err != nil { - return nil, err + return nil, errors.Join(tickErr, err) } if len(pnData) > 0 { mixData = append(mixData, pnData...) @@ -112,7 +116,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } m.age++ - return &premix, nil + return &premix, tickErr } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTick() error { @@ -125,12 +129,16 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTi c.pn.UpdatePastNotes() } - for i := range m.channels { - c := &m.channels[i] - if err := c.PostTick(index.Channel(i), m); err != nil { + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onOrderStart() error { + for ch := range m.channels { + if err := m.channels[ch].OrderStart(index.Channel(ch), m); err != nil { return err } } + return nil } @@ -162,9 +170,18 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRo } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowEnd() error { - for i := range m.channels { - c := &m.channels[i] - if err := c.RowEnd(index.Channel(i), m); err != nil { + for ch := range m.channels { + if err := m.channels[ch].RowEnd(index.Channel(ch), m); err != nil { + return err + } + } + + return nil +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onOrderEnd() error { + for ch := range m.channels { + if err := m.channels[ch].OrderEnd(index.Channel(ch), m); err != nil { return err } } diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go index 2e7e4d4..6703d43 100644 --- a/player/machine/pastnote.go +++ b/player/machine/pastnote.go @@ -4,8 +4,7 @@ import ( "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/note" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state/render" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/voice" ) @@ -78,7 +77,7 @@ func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Do } } -func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Render(centerAheadPan volume.Matrix, details render.Details, rc *playerRender.Channel[TGlobalVolume, TMixingVolume, TPanning]) ([]mixing.Data, error) { +func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Render(centerAheadPan volume.Matrix, details render.Details, rc *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) ([]mixing.Data, error) { var mixData []mixing.Data for _, pn := range p.pn { diff --git a/player/machine/ticker.go b/player/machine/ticker.go index 38e3bfc..7f792f7 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -32,15 +32,24 @@ func initTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPan t.next.row.Set(setup.Row) t.next.order.Set(setup.Order) - row, order, err := advanceRowOrder(t, m) + nextRow, nextOrder, err := advanceRowOrder(t, m) if err != nil { return err } - t.current.row = row - t.current.order = order + if row, set := nextRow.Get(); set { + t.current.row = row + } + if order, set := nextOrder.Get(); set { + t.current.order = order + } + m.us.SetTracingTick(t.current.order, t.current.row, t.current.tick) + if err := m.onOrderStart(); err != nil { + return err + } + return m.onRowStart() } @@ -53,6 +62,7 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann row := t.current.row order := t.current.order rowAdvanced := false + orderAdvanced := false done := false if tick >= m.tempo { tick = 0 @@ -64,10 +74,26 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann return err } - var err error - row, order, err = advanceRowOrder(t, m) - if err != nil && !errors.Is(err, song.ErrStopSong) { - 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 + } } } @@ -81,7 +107,14 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann if done { return song.ErrStopSong - } else if rowAdvanced { + } + + if orderAdvanced { + if err := m.onOrderStart(); err != nil { + return err + } + } + if rowAdvanced { if err := m.onRowStart(); err != nil { return err } @@ -89,24 +122,33 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann return nil } -func advanceRowOrder[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](t *ticker, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) (index.Row, index.Order, error) { +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) + rowUpdated = true order = int(desiredOrder) + orderUpdated = true } else if rowSet { row = int(desiredRow) + rowUpdated = true order++ + orderUpdated = true } else if orderSet { order = int(desiredOrder) + orderUpdated = true row = 0 + rowUpdated = true } else { row++ + rowUpdated = true } t.next.row.Reset() @@ -116,26 +158,47 @@ func advanceRowOrder[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum orderScanIter := 0 orderScan: if orderScanIter >= orderScanMax { - return index.Row(row), index.Order(order), song.ErrStopSong + var ( + emptyRow optional.Value[index.Row] + emptyOrder optional.Value[index.Order] + ) + return emptyRow, emptyOrder, song.ErrStopSong } pat, err := m.songData.GetPatternIntfByOrder(index.Order(order)) if err != nil { if errors.Is(err, index.ErrNextPattern) { order++ + orderUpdated = true orderScanIter++ // don't update row here goto orderScan } - return index.Row(row), index.Order(order), err + 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 } - return index.Row(row), index.Order(order), nil + 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/op/channeltargets.go b/player/op/channeltargets.go deleted file mode 100644 index 2597b68..0000000 --- a/player/op/channeltargets.go +++ /dev/null @@ -1,21 +0,0 @@ -package op - -import ( - "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/voice/types" - "github.com/heucuva/optional" -) - -type ChannelTargets[TPeriod types.Period, TVolume types.Volume, TPanning types.Panning] struct { - NoteAction optional.Value[note.Action] - NoteCalcST optional.Value[note.Semitone] - - TargetPos optional.Value[sampling.Pos] - TargetInst optional.Value[instrument.InstrumentIntf] - TargetPeriod optional.Value[TPeriod] - TargetStoredSemitone optional.Value[note.Semitone] - TargetNewNoteAction optional.Value[note.Action] - TargetVolume optional.Value[TVolume] -} diff --git a/player/posttick.go b/player/posttick.go deleted file mode 100644 index e147d57..0000000 --- a/player/posttick.go +++ /dev/null @@ -1,14 +0,0 @@ -package player - -// PostTickable is an interface which exposes the OnPostTick call -type PostTickable interface { - OnPostTick() error -} - -// DoPostTick calls the OnPostTick() function on the interface, if possible -func DoPostTick(t PostTickable) error { - if t != nil { - return t.OnPostTick() - } - return nil -} diff --git a/player/pretick.go b/player/pretick.go deleted file mode 100644 index 109d8e1..0000000 --- a/player/pretick.go +++ /dev/null @@ -1,14 +0,0 @@ -package player - -// PreTickable is an interface which exposes the OnPreTick call -type PreTickable interface { - OnPreTick() error -} - -// DoPreTick calls the OnPreTick() function on the interface, if possible -func DoPreTick(t PreTickable) error { - if t != nil { - return t.OnPreTick() - } - return nil -} diff --git a/player/state/render/details.go b/player/render/details.go similarity index 100% rename from player/state/render/details.go rename to player/render/details.go diff --git a/player/state/renderstate.go b/player/state/renderstate.go deleted file mode 100644 index 83c4412..0000000 --- a/player/state/renderstate.go +++ /dev/null @@ -1,34 +0,0 @@ -package state - -import ( - "time" - - "github.com/gotracker/playback/player/state/render" -) - -type RowRenderState struct { - render.Details - - TicksThisRow int - CurrentTick int -} - -func (r RowRenderState) GetTicksThisRow() int { - return r.TicksThisRow -} - -func (r RowRenderState) GetCurrentTick() int { - return r.CurrentTick -} - -func (r RowRenderState) GetSamplerSpeed() float32 { - return r.Details.SamplerSpeed -} - -func (r RowRenderState) GetDuration() time.Duration { - return r.Details.Duration -} - -func (r RowRenderState) GetSamples() int { - return r.Details.Samples -} 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 index 1b62d81..27cdf98 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -58,25 +58,23 @@ func (t *Tracker) Close() { // 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 - } - if premix != nil && len(premix.Data) > 0 { out <- premix } - return nil + return err } func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) { defer t.OutputTraces() - var premix *output.PremixData - var err error + var ( + premix *output.PremixData + err error + ) if t.M != nil { premix, err = t.M.Tick(t.s) - if err != nil { + if err != nil && !errors.Is(err, song.ErrStopSong) { return nil, err } } @@ -100,7 +98,7 @@ func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) premix.MixerVolume /= (mv + 1) } - return premix, nil + return premix, err } // GetRenderChannel returns the output channel for the provided index `ch` diff --git a/song/channelmemory.go b/song/channelmemory.go index bdcb364..1f9d26e 100644 --- a/song/channelmemory.go +++ b/song/channelmemory.go @@ -2,4 +2,5 @@ package song type ChannelMemory interface { Retrigger() + StartOrder() } diff --git a/voice/component/envelope.go b/voice/component/envelope.go index 92a3deb..d7fd3cc 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -2,11 +2,13 @@ package component import ( "fmt" + "math" "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 @@ -21,9 +23,10 @@ type Envelope interface { // baseEnvelope is a basic modulation envelope type baseEnvelope[TIn, TOut any] struct { settings EnvelopeSettings[TIn, TOut] - updater func() TOut + updater func(TIn, TIn, float64) TOut enabled bool - state envelope.State[TIn] + pos int + stopped bool value TOut slimKeyModulator @@ -34,25 +37,22 @@ type EnvelopeSettings[TIn, TOut any] struct { OnFinished voice.Callback } -func (e *baseEnvelope[TIn, TOut]) Setup(settings EnvelopeSettings[TIn, TOut], update func() TOut) { +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() TOut) baseEnvelope[TIn, TOut] { +func (e baseEnvelope[TIn, TOut]) Clone(update func(TIn, TIn, float64) TOut) baseEnvelope[TIn, TOut] { m := e - m.state = e.state.Clone() return m } // Reset resets the state to defaults based on the envelope provided func (e *baseEnvelope[TIn, TOut]) Reset() { e.enabled = e.settings.Enabled - e.state.Init(&e.settings.Envelope) - if e.enabled { - e.value = e.updater() - } + e.stateReset() + e.updateValue() } // SetEnabled sets the enabled flag for the envelope @@ -66,7 +66,7 @@ func (e baseEnvelope[TIn, TOut]) IsEnabled() bool { } func (e baseEnvelope[TIn, TOut]) IsDone() bool { - return e.state.Stopped() + return e.stopped } // GetCurrentValue returns the current cached envelope value @@ -76,7 +76,7 @@ func (e baseEnvelope[TIn, TOut]) GetCurrentValue() TOut { // SetEnvelopePosition sets the current position in the envelope func (e *baseEnvelope[TIn, TOut]) SetEnvelopePosition(pos int) voice.Callback { - e.state.Reset() + e.stateReset() // 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(); doneCB != nil { @@ -87,23 +87,140 @@ func (e *baseEnvelope[TIn, TOut]) SetEnvelopePosition(pos int) voice.Callback { } func (e baseEnvelope[TIn, TOut]) GetEnvelopePosition() int { - return e.state.Pos() + return e.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.state.Advance(e.keyOn); done { + if done := e.stateAdvance(e.keyOn); done { doneCB = e.settings.OnFinished } - e.value = e.updater() + e.updateValue() return doneCB } func (e baseEnvelope[TIn, TOut]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} pos{%v} value{%v}", + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} pos{%v} stopped{%v} value{%v}", e.enabled, - e.state.Pos(), + e.pos, + e.stopped, e.value, ), comment) } + +func (e *baseEnvelope[TIn, TOut]) updateValue() { + if !e.enabled { + return + } + + curVal, nextVal, t := e.getCurrentPoints() + + var y0 TIn + if curVal != nil { + y0 = curVal.Y + } + + var y1 TIn + if nextVal != nil { + y1 = nextVal.Y + } + + e.value = e.updater(y0, y1, t) +} + +func (e *baseEnvelope[TIn, TOut]) stateReset() { + if !e.settings.Envelope.Enabled { + e.stopped = true + return + } + + e.pos = 0 + e.stopped = false +} + +func (e *baseEnvelope[TIn, TOut]) getCurrentPoints() (*envelope.Point[TIn], *envelope.Point[TIn], float64) { + if e.stopped { + return nil, nil, 0 + } + + nPoints := len(e.settings.Envelope.Values) + + if nPoints == 0 { + return nil, nil, 0 + } + + curTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.pos, e.settings.Envelope.Length, e.prevKeyOn) + nextTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.pos+1, e.settings.Envelope.Length, e.keyOn) + + var cur envelope.Point[TIn] + for _, it := range e.settings.Envelope.Values { + if it.Pos > curTick { + break + } + cur = it + } + + var next envelope.Point[TIn] + foundNext := false + for _, it := range e.settings.Envelope.Values { + if it.Pos > nextTick { + next = it + foundNext = true + break + } + } + + if !foundNext { + return &cur, &cur, 0 + } + + t := float64(0) + if cur.Length > 0 { + if tl := curTick - cur.Pos; tl > 0 { + t = max(min((float64(tl)/float64(cur.Length)), 1), 0) + } + } + return &cur, &next, t +} + +func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { + if e.stopped { + 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.stopped = true + return true + } + + e.pos++ + curTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.pos, e.settings.Envelope.Length, keyOn) + + found := false + for _, i := range e.settings.Envelope.Values { + if i.Pos >= curTick && i.Length != math.MaxInt { + found = true + break + } + } + + if !found { + e.stopped = true + return true + } + + return false +} diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 45a64bf..9def967 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -20,19 +20,7 @@ func (e FilterEnvelope) Clone() FilterEnvelope { return m } -func (e *FilterEnvelope) calc() uint8 { - cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) - - var y0 filter.PitchFiltValue - if cur != nil { - y0 = cur.Y - } - - var y1 filter.PitchFiltValue - if next != nil { - y1 = next.Y - } - - v := util.Lerp(float64(t), y0, y1) +func (e *FilterEnvelope) calc(y0, y1 filter.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 a10ab18..17761af 100755 --- a/voice/component/envelope_pan.go +++ b/voice/component/envelope_pan.go @@ -20,18 +20,6 @@ func (e PanEnvelope[TPanning]) Clone() PanEnvelope[TPanning] { return m } -func (e *PanEnvelope[TPanning]) calc() TPanning { - cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) - - var y0 TPanning - if cur != nil { - y0 = cur.Y - } - - var y1 TPanning - if next != nil { - y1 = next.Y - } - - return util.Lerp(float64(t), y0, y1) +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 46b3c35..3f1787a 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -21,18 +21,6 @@ func (e PitchEnvelope) Clone() PitchEnvelope { return m } -func (e *PitchEnvelope) calc() period.Delta { - cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) - - var y0 filter.PitchFiltValue - if cur != nil { - y0 = cur.Y - } - - var y1 filter.PitchFiltValue - if next != nil { - y1 = next.Y - } - - return -period.Delta(util.Lerp(float64(t), y0, y1)) +func (e *PitchEnvelope) calc(y0, y1 filter.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 1240aad..b54040e 100755 --- a/voice/component/envelope_volume.go +++ b/voice/component/envelope_volume.go @@ -20,18 +20,6 @@ func (e VolumeEnvelope[TVolume]) Clone() VolumeEnvelope[TVolume] { return m } -func (e *VolumeEnvelope[TVolume]) calc() TVolume { - cur, next, t := e.state.GetCurrentValue(e.keyOn, e.prevKeyOn) - - var y0 TVolume - if cur != nil { - y0 = cur.Y - } - - var y1 TVolume - if next != nil { - y1 = next.Y - } - - return util.Lerp(float64(t), y0, y1) +func (e *VolumeEnvelope[TVolume]) calc(y0, y1 TVolume, t float64) TVolume { + return util.Lerp(t, y0, y1) } diff --git a/voice/envelope/envelope.go b/voice/envelope/envelope.go index 548f0a9..6b3d596 100644 --- a/voice/envelope/envelope.go +++ b/voice/envelope/envelope.go @@ -1,150 +1,14 @@ package envelope import ( - "math" - "github.com/gotracker/playback/voice/loop" ) -// State is the state information about an envelope -type State[T any] struct { - tick int - stopped bool - env *Envelope[T] -} - -func (e *State[T]) Init(env *Envelope[T]) { - e.env = env - if e.env == nil || !e.env.Enabled { - e.stopped = true - return - } - - e.Reset() -} - -func (e State[T]) Clone() State[T] { - return State[T]{ - tick: e.tick, - stopped: e.stopped, - env: e.env, - } -} - -// 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() { - if e.env == nil || !e.env.Enabled { - e.stopped = true - return - } - - e.tick = 0 - e.stopped = false -} - -func (e *State[T]) Pos() int { - return e.tick -} - -// GetCurrentValue returns the current value -func (e *State[T]) GetCurrentValue(keyOn, prevKeyOn bool) (*EnvPoint[T], *EnvPoint[T], float64) { - if e.stopped { - return nil, nil, 0 - } - - nPoints := len(e.env.Values) - - if nPoints == 0 { - return nil, nil, 0 - } - - curTick, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.tick, e.env.Length, prevKeyOn) - nextTick, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.tick+1, e.env.Length, keyOn) - - var cur EnvPoint[T] - for _, it := range e.env.Values { - if it.Pos > curTick { - break - } - cur = it - } - - var next EnvPoint[T] - foundNext := false - for _, it := range e.env.Values { - if it.Pos > nextTick { - next = it - foundNext = true - break - } - } - - if !foundNext { - return &cur, &cur, 0 - } - - t := float64(0) - if cur.Length > 0 { - if tl := curTick - cur.Pos; tl > 0 { - t = max(min((float64(tl)/float64(cur.Length)), 1), 0) - } - } - return &cur, &next, t -} - -// Advance advances the state by 1 tick -func (e *State[T]) Advance(keyOn 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 - } - } - - nPoints := len(e.env.Values) - - if nPoints == 0 { - e.stopped = true - return true - } - - e.tick++ - curTick, _ := loop.CalcLoopPos(e.env.Loop, e.env.Sustain, e.tick, e.env.Length, keyOn) - - found := false - for _, i := range e.env.Values { - if i.Pos >= curTick && i.Length != math.MaxInt { - found = true - break - } - } - - if !found { - e.stopped = true - return true - } - - 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 a18a2d9..0000000 --- a/voice/envelope/instrumentenv.go +++ /dev/null @@ -1,21 +0,0 @@ -package envelope - -import ( - "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 - Length int - Values []EnvPoint[T] -} - -// EnvPoint is a point for the envelope -type EnvPoint[T any] struct { - Pos int - Length int - Y T -} 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/voice.go b/voice/voice.go index ffa5758..e181ec8 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -7,8 +7,7 @@ import ( "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/player/state/render" + render "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/tracing" "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/pcm" @@ -43,7 +42,7 @@ type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, T SetPCM(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop, defaultVolume TVolume) // Render - Render(panningMatrix volume.Matrix, details render.Details, renderChannel *playerRender.Channel[TGlobalVolume, TMixingVolume, TPanning]) (*mixing.Data, error) + Render(panningMatrix volume.Matrix, details render.Details, renderChannel *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) (*mixing.Data, error) } type AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { From 25a00e8ddb6b4f10af5a2de85acf4ee065400ef7 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 13 Jan 2024 13:06:09 -0800 Subject: [PATCH 31/63] round 3 of playback cleanup --- format/it/playback/playback.go | 35 +---- format/it/settings/machine.go | 2 + format/s3m/playback/playback.go | 56 -------- format/s3m/settings/machine.go | 1 + format/xm/playback/playback.go | 50 +------- format/xm/settings/machine.go | 2 + player/machine/machine.go | 21 +++ player/machine/machine_factory.go | 6 +- player/machine/machine_globals.go | 5 +- player/machine/machine_opl2.go | 59 +++++++++ player/machine/machine_tick.go | 23 +++- player/machine/settings/machinesettings.go | 1 + player/render/channel.go | 4 +- player/render/opl2intf.go | 14 +- player/sampler/sampler.go | 12 +- player/tracker.go | 142 ++++----------------- system/clockedsystem.go | 8 ++ system/system.go | 1 + voice/component/opl2.go | 2 +- voice/render/opl2chip.go | 12 -- 20 files changed, 174 insertions(+), 282 deletions(-) create mode 100644 player/machine/machine_opl2.go delete mode 100755 voice/render/opl2chip.go diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index e3dceb6..a0c36ec 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -2,17 +2,14 @@ package playback import ( "errors" - "fmt" "github.com/gotracker/playback" itFeature "github.com/gotracker/playback/format/it/feature" "github.com/gotracker/playback/format/it/layout" - itSystem "github.com/gotracker/playback/format/it/system" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/system" ) // manager is a playback manager for IT music @@ -23,15 +20,6 @@ type manager[TPeriod period.Period] struct { var _ playback.Playback = (*manager[period.Linear])(nil) -var it system.ClockableSystem = itSystem.ITSystem - -func (m *manager[TPeriod]) init(s *layout.Song[TPeriod]) error { - m.Tracker.BaseClockRate = it.GetBaseClock() - m.song = s - - return nil -} - // NewManager creates a new manager for an IT song func NewManager(songData song.Data) (playback.Playback, error) { if songData == nil { @@ -40,15 +28,13 @@ func NewManager(songData song.Data) (playback.Playback, error) { switch s := songData.(type) { case *layout.Song[period.Linear]: - var m manager[period.Linear] - if err := m.init(s); err != nil { - return nil, fmt.Errorf("could not initialize it manager: %w", err) + m := manager[period.Linear]{ + song: s, } return &m, nil case *layout.Song[period.Amiga]: - var m manager[period.Amiga] - if err := m.init(s); err != nil { - return nil, fmt.Errorf("could not initialize it manager: %w", err) + m := manager[period.Amiga]{ + song: s, } return &m, nil default: @@ -71,7 +57,6 @@ func (m *manager[TPeriod]) Configure(features []feature.Feature) error { case feature.PlayUntilOrderAndRow: us.PlayUntilOrderAndRow = f case itFeature.LongChannelOutput: - m.Tracker.LongChannelOutput = f.Enabled us.LongChannelOutput = f.Enabled case itFeature.NewNoteActions: us.EnableNewNoteActions = f.Enabled @@ -83,15 +68,3 @@ func (m *manager[TPeriod]) Configure(features []feature.Feature) error { } return m.SetupMachine(m.song, us) } - -func (m *manager[TPeriod]) GetNumOrders() int { - return len(m.song.GetOrderList()) -} - -func (m *manager[TPeriod]) CanOrderLoop() bool { - return true -} - -func (m *manager[TPeriod]) GetName() string { - return m.song.GetName() -} diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go index 0144d8f..6663de1 100644 --- a/format/it/settings/machine.go +++ b/format/it/settings/machine.go @@ -34,6 +34,7 @@ var ( GetTremoloFactory: tremoloFactory, GetPanbrelloFactory: panbrelloFactory, VoiceFactory: amigaVoiceFactory, + OPL2Enabled: false, } linearMachine = settings.MachineSettings[period.Linear, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ @@ -43,6 +44,7 @@ var ( GetTremoloFactory: tremoloFactory, GetPanbrelloFactory: panbrelloFactory, VoiceFactory: linearVoiceFactory, + OPL2Enabled: false, } ) diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index 3f8dcb0..2afeff5 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -3,14 +3,9 @@ package playback import ( "github.com/gotracker/playback" "github.com/gotracker/playback/format/s3m/layout" - s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/player" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/system" ) // manager is a playback manager for S3M music @@ -20,37 +15,18 @@ type manager struct { } var _ playback.Playback = (*manager)(nil) -var s3m system.ClockableSystem = s3mSystem.S3MSystem // NewManager creates a new manager for an S3M song func NewManager(songData song.Data) (playback.Playback, error) { s := songData.(*layout.Song) m := manager{ - Tracker: player.Tracker{ - BaseClockRate: s3m.GetBaseClock(), - LongChannelOutput: true, - }, song: s, } return &m, nil } -func (m *manager) channelInit(ch int) render.ChannelIntf { - return &render.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]{ - ChannelNum: ch, - Filter: nil, - GetSampleRate: m.GetSampleRate, - SetGlobalVolume: func(v s3mVolume.Volume) error { - m.SetGlobalVolume(v.ToVolume()) - return nil - }, - GetOPL2Chip: m.GetOPL2Chip, - ChannelVolume: s3mVolume.MaxFineVolume, - } -} - // Configure sets specified features func (m *manager) Configure(features []feature.Feature) error { us, err := m.Tracker.Configure(features) @@ -78,37 +54,5 @@ func (m *manager) SetupSampler(samplesPerSecond int, channels int) error { return err } - var oplLen int - //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Melody)-1]) - //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Drums)-1]) - - if oplLen > 0 { - m.ensureOPL2() - } 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) - } - } -} - -func (m *manager) GetNumOrders() int { - return len(m.song.GetOrderList()) -} - -func (m *manager) CanOrderLoop() bool { - return true -} - -func (m *manager) GetName() string { - return m.song.GetName() -} diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go index 6de4f64..55c9259 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -25,6 +25,7 @@ var ( GetTremoloFactory: tremoloFactory, GetPanbrelloFactory: panbrelloFactory, VoiceFactory: amigaVoiceFactory, + OPL2Enabled: true, } ) diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index e0a5595..56a06bf 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -2,18 +2,13 @@ package playback import ( "errors" - "fmt" "github.com/gotracker/playback" "github.com/gotracker/playback/format/xm/layout" - xmPanning "github.com/gotracker/playback/format/xm/panning" - xmSystem "github.com/gotracker/playback/format/xm/system" - xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player" "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" ) @@ -24,15 +19,6 @@ type manager[TPeriod period.Period] struct { } var _ playback.Playback = (*manager[period.Linear])(nil) -var _ playback.Playback = (*manager[period.Amiga])(nil) - -func (m *manager[TPeriod]) init(s *layout.Song[TPeriod]) error { - m.Tracker.BaseClockRate = xmSystem.XMBaseClock - m.Tracker.LongChannelOutput = true - m.song = s - - return nil -} // NewManager creates a new manager for an XM song func NewManager(songData song.Data) (playback.Playback, error) { @@ -42,16 +28,14 @@ func NewManager(songData song.Data) (playback.Playback, error) { switch s := songData.(type) { case *layout.Song[period.Linear]: - var m manager[period.Linear] - if err := m.init(s); err != nil { - return nil, fmt.Errorf("could not initialize xm manager: %w", err) + m := manager[period.Linear]{ + song: s, } return &m, nil case *layout.Song[period.Amiga]: - var m manager[period.Amiga] - if err := m.init(s); err != nil { - return nil, fmt.Errorf("could not initialize xm manager: %w", err) + m := manager[period.Amiga]{ + song: s, } return &m, nil @@ -60,20 +44,6 @@ func NewManager(songData song.Data) (playback.Playback, error) { } } -func (m *manager[TPeriod]) channelInit(ch int) render.ChannelIntf { - return &render.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ - ChannelNum: ch, - Filter: nil, - GetSampleRate: m.GetSampleRate, - SetGlobalVolume: func(xv xmVolume.XmVolume) error { - m.SetGlobalVolume(xv.ToVolume()) - return nil - }, - GetOPL2Chip: m.GetOPL2Chip, - ChannelVolume: xmVolume.DefaultXmVolume, - } -} - // Configure sets specified features func (m *manager[TPeriod]) Configure(features []feature.Feature) error { us, err := m.Tracker.Configure(features) @@ -94,15 +64,3 @@ func (m *manager[TPeriod]) Configure(features []feature.Feature) error { } return m.SetupMachine(m.song, us) } - -func (m *manager[TPeriod]) GetNumOrders() int { - return len(m.song.GetOrderList()) -} - -func (m *manager[TPeriod]) CanOrderLoop() bool { - return true -} - -func (m *manager[TPeriod]) GetName() string { - return m.song.GetName() -} diff --git a/format/xm/settings/machine.go b/format/xm/settings/machine.go index 6af3fca..15727f6 100644 --- a/format/xm/settings/machine.go +++ b/format/xm/settings/machine.go @@ -33,6 +33,7 @@ var ( GetTremoloFactory: tremoloFactory, GetPanbrelloFactory: panbrelloFactory, VoiceFactory: amigaVoiceFactory, + OPL2Enabled: false, } linearMachine = settings.MachineSettings[period.Linear, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ @@ -42,6 +43,7 @@ var ( GetTremoloFactory: tremoloFactory, GetPanbrelloFactory: panbrelloFactory, VoiceFactory: linearVoiceFactory, + OPL2Enabled: false, } ) diff --git a/player/machine/machine.go b/player/machine/machine.go index a25dd52..a882b81 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -36,7 +36,15 @@ 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) (*output.PremixData, error) } @@ -125,6 +133,7 @@ type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann songData song.Data ms *settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] us settings.UserSettings + opl2 render.OPL2Chip rowStringer render.RowStringer @@ -163,6 +172,18 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Igno 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.SongLoop.Count != 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) diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index ca69eb7..a0b4bf4 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -7,10 +7,9 @@ import ( "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" "github.com/gotracker/playback/player/machine/settings" - playerRender "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/render" "github.com/gotracker/playback/voice/types" ) @@ -98,7 +97,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum m.channels = make([]channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], channels) // make at least 64 output channels - m.outputChannels = make([]playerRender.Channel[TGlobalVolume, TMixingVolume, TPanning], channels) + m.outputChannels = make([]render.Channel[TGlobalVolume, TMixingVolume, TPanning], channels) for i := 0; i < channels; i++ { ch := index.Channel(i) cs := songData.GetChannelSettings(ch) @@ -107,7 +106,6 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc.ChannelNum = i rc.Filter = nil rc.GetSampleRate = m.getSampleRate - rc.SetGlobalVolume = m.SetGlobalVolume rc.GetOPL2Chip = func() render.OPL2Chip { // TODO: add OPL2 back in return nil diff --git a/player/machine/machine_globals.go b/player/machine/machine_globals.go index 761f4f2..f05b2f8 100644 --- a/player/machine/machine_globals.go +++ b/player/machine/machine_globals.go @@ -143,10 +143,13 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetF return err } + cr := m.songData.GetSystem().GetCommonRate() + sr := m.getSampleRate() + for i := range m.channels { c := &m.channels[i] if enabled { - filt, err := filtFactory(m.songData.GetSystem().GetCommonRate(), m.getSampleRate()) + filt, err := filtFactory(cr, sr) if err != nil { return err } diff --git a/player/machine/machine_opl2.go b/player/machine/machine_opl2.go new file mode 100644 index 0000000..401815e --- /dev/null +++ b/player/machine/machine_opl2.go @@ -0,0 +1,59 @@ +package machine + +import ( + "errors" + + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/sampler" +) + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setupOPL2(s *sampler.Sampler) error { + if m.opl2 != nil || !m.ms.OPL2Enabled { + return nil + } + + if s != nil { + return errors.New("sampler is nil") + } + + var oplLen int + //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Melody)-1]) + //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Drums)-1]) + + if oplLen > 0 { + o := render.NewOPL2Chip(uint32(s.SampleRate)) + 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 + } + + 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_tick.go b/player/machine/machine_tick.go index 190be66..4924041 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -56,10 +56,12 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } } + sys := m.songData.GetSystem() + details := render.Details{ Mix: s.Mixer(), Panmixer: s.GetPanMixer(), - SamplerSpeed: s.GetSamplerSpeed(), + SamplerSpeed: sys.GetSamplerSpeed(period.Frequency(s.SampleRate)), Samples: premix.SamplesLen, Duration: tickDuration, } @@ -103,6 +105,25 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } } + if m.opl2 != nil { + rr := [1]mixing.Data{} + if err := m.renderOPL2Tick(&rr[0], s.Mixer(), premix.SamplesLen); err != nil { + return nil, errors.Join(tickErr, 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{ diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go index fc0cd4b..dca8d3e 100644 --- a/player/machine/settings/machinesettings.go +++ b/player/machine/settings/machinesettings.go @@ -23,4 +23,5 @@ type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum GetTremoloFactory func() (oscillator.Oscillator, error) GetPanbrelloFactory func() (oscillator.Oscillator, error) VoiceFactory voice.VoiceFactory[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + OPL2Enabled bool } diff --git a/player/render/channel.go b/player/render/channel.go index 86daaeb..7276690 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -7,7 +7,6 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" channelfilter "github.com/gotracker/playback/voice/filter" - "github.com/gotracker/playback/voice/render" ) type ChannelIntf interface { @@ -20,8 +19,7 @@ type Channel[TGlobalVolume, TMixingVolume song.Volume, TPanning song.Panning] st ChannelNum int Filter filter.Filter GetSampleRate func() period.Frequency - SetGlobalVolume func(TGlobalVolume) error - GetOPL2Chip func() render.OPL2Chip + GetOPL2Chip func() OPL2Chip ChannelVolume TMixingVolume LastGlobalVolume TGlobalVolume // this is the channel's version of the GlobalVolume } diff --git a/player/render/opl2intf.go b/player/render/opl2intf.go index a960bd2..cd0235f 100644 --- a/player/render/opl2intf.go +++ b/player/render/opl2intf.go @@ -2,10 +2,20 @@ package render import ( "github.com/gotracker/opl2" - "github.com/gotracker/playback/voice/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 +} + // NewOPL2Chip generates a new OPL2 chip -func NewOPL2Chip(rate uint32) render.OPL2Chip { +func NewOPL2Chip(rate uint32) OPL2Chip { return opl2.NewChip(rate, false) } diff --git a/player/sampler/sampler.go b/player/sampler/sampler.go index 89aa3ed..fe07fdf 100644 --- a/player/sampler/sampler.go +++ b/player/sampler/sampler.go @@ -14,10 +14,9 @@ type Sampler struct { } // 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) *Sampler { s := Sampler{ - SampleRate: samplesPerSec, - BaseClockRate: baseClockRate, + SampleRate: samplesPerSec, mixer: mixing.Mixer{ Channels: channels, }, @@ -25,13 +24,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/tracker.go b/player/tracker.go index 27cdf98..6d5cc34 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -4,19 +4,14 @@ import ( "errors" "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/machine" "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/tracing" - voiceRender "github.com/gotracker/playback/voice/render" ) // Premixable is an interface to getting the premix data from the tracker @@ -26,22 +21,10 @@ type Premixable interface { // Tracker is an extensible music tracker type Tracker struct { - BaseClockRate period.Frequency - Premixable Premixable - - s *sampler.Sampler - opl2 voiceRender.OPL2Chip - - globalVolume volume.Volume - mixerVolume volume.Volume - - ignoreUnknownEffect feature.IgnoreUnknownEffect - outputChannels map[int]render.ChannelIntf + M machine.MachineTicker + s *sampler.Sampler tracing.Tracing - - M machine.MachineTicker - LongChannelOutput bool } func (t *Tracker) SetupMachine(s song.Data, us settings.UserSettings) error { @@ -66,95 +49,28 @@ func (t *Tracker) Update(deltaTime time.Duration, out chan<- *output.PremixData) } func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) { - defer t.OutputTraces() - - var ( - premix *output.PremixData - err error - ) - if t.M != nil { - premix, err = t.M.Tick(t.s) - if err != nil && !errors.Is(err, song.ErrStopSong) { - 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) + if t.M == nil { + return nil, nil } - return premix, err -} + defer t.OutputTraces() -// GetRenderChannel returns the output channel for the provided index `ch` -func (t *Tracker) GetRenderChannel(ch int, init func(ch int) render.ChannelIntf) render.ChannelIntf { - if t.outputChannels == nil { - t.outputChannels = make(map[int]render.ChannelIntf) + premix, err := t.M.Tick(t.s) + if err != nil && !errors.Is(err, song.ErrStopSong) { + return nil, err } - if oc, ok := t.outputChannels[ch]; ok { - return oc - } - oc := init(ch) - t.outputChannels[ch] = oc - return oc + return premix, err } // GetSampleRate returns the sample rate of the sampler func (t *Tracker) GetSampleRate() period.Frequency { - return period.Frequency(t.GetSampler().SampleRate) -} - -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 + return period.Frequency(t.s.SampleRate) } // SetupSampler configures the internal sampler func (t *Tracker) SetupSampler(samplesPerSecond int, channels int) error { - t.s = sampler.NewSampler(samplesPerSecond, channels, t.BaseClockRate) + t.s = sampler.NewSampler(samplesPerSecond, channels) if t.s == nil { return errors.New("NewSampler() returned nil") } @@ -167,31 +83,28 @@ func (t *Tracker) GetSampler() *sampler.Sampler { return t.s } -// GetGlobalVolume returns the global volume value -func (t *Tracker) GetGlobalVolume() volume.Volume { - return t.globalVolume -} +func (t *Tracker) GetNumOrders() int { + if t.M == nil { + return 0 + } -// SetGlobalVolume sets the global volume to the specified `vol` value -func (t *Tracker) SetGlobalVolume(vol volume.Volume) { - t.TraceValueChange("SetGlobalVolume", t.globalVolume, vol) - t.globalVolume = vol + return t.M.GetNumOrders() } -// GetMixerVolume returns the mixer volume value -func (t *Tracker) GetMixerVolume() volume.Volume { - return t.mixerVolume -} +func (t *Tracker) CanOrderLoop() bool { + if t.M == nil { + return false + } -// SetMixerVolume sets the mixer volume to the specified `vol` value -func (t *Tracker) SetMixerVolume(vol volume.Volume) { - t.TraceValueChange("SetMixerVolume", t.mixerVolume, vol) - t.mixerVolume = vol + return t.M.CanOrderLoop() } -// IgnoreUnknownEffect returns true if the tracker wants unknown effects to be ignored -func (t *Tracker) IgnoreUnknownEffect() bool { - return t.ignoreUnknownEffect.Enabled +func (t *Tracker) GetName() string { + if t.M == nil { + return "" + } + + return t.M.GetName() } // Configure sets specified features @@ -204,7 +117,6 @@ func (t *Tracker) Configure(features []feature.Feature) (settings.UserSettings, for _, feat := range features { switch f := feat.(type) { case feature.IgnoreUnknownEffect: - t.ignoreUnknownEffect = f us.IgnoreUnknownEffect = f.Enabled case feature.EnableTracing: if err := t.EnableTracing(f.Filename); err != nil { diff --git a/system/clockedsystem.go b/system/clockedsystem.go index 0f02459..05dbdba 100644 --- a/system/clockedsystem.go +++ b/system/clockedsystem.go @@ -11,6 +11,7 @@ type ClockableSystem interface { GetFinetunesPerOctave() note.Finetune GetFinetunesPerSemitone() note.Finetune GetSemitonePeriod(note.Key) (float32, bool) + GetSamplerSpeed(sampleRate Frequency) float32 } type ClockedSystem struct { @@ -50,6 +51,13 @@ func (s ClockedSystem) GetCommonRate() Frequency { return s.CommonRate } +func (s ClockedSystem) GetSamplerSpeed(sampleRate Frequency) float32 { + if sampleRate == 0 { + panic("sampleRate is 0") + } + return float32(s.BaseClock) / float32(sampleRate) +} + func (s ClockedSystem) GetSemitonePeriod(k note.Key) (float32, bool) { if int(k) < note.NumKeys { return s.SemitonePeriods[int(k)], true diff --git a/system/system.go b/system/system.go index 5641f92..296116f 100644 --- a/system/system.go +++ b/system/system.go @@ -3,4 +3,5 @@ package system type System interface { GetMaxPastNotesPerChannel() int GetCommonRate() Frequency + GetSamplerSpeed(sampleRate Frequency) float32 } diff --git a/voice/component/opl2.go b/voice/component/opl2.go index 24c0796..bfbe784 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -6,8 +6,8 @@ import ( "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/tracing" - "github.com/gotracker/playback/voice/render" "github.com/gotracker/playback/voice/types" ) 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 -} From c2830632b84ae5587fbd925dc76ff0f56c491a22 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 13 Jan 2024 14:51:17 -0800 Subject: [PATCH 32/63] round 4 of playback cleanup - s3m filter fix - s3m portamento fixes - mod loader fixes - fix up rendering pipeline - fix voice configurations --- filter/amigafilter.go | 61 +++++----- filter/echofilter.go | 33 +++--- filter/filter.go | 3 +- format/it/filter/resonantfilter.go | 39 +++++-- format/it/layout/channelsetting.go | 10 +- format/it/load/instrument.go | 4 +- format/it/settings/machine.go | 4 +- format/it/settings/voicefactory.go | 4 +- format/it/voice/voice.go | 12 +- format/s3m/channel/effect_finevibrato.go | 3 +- format/s3m/channel/effect_portadown.go | 3 +- format/s3m/channel/effect_portatonote.go | 3 +- format/s3m/channel/effect_portaup.go | 3 +- format/s3m/channel/effect_vibrato.go | 3 +- format/s3m/layout/channelsetting.go | 10 +- format/s3m/load/modconv/modconverter.go | 1 + format/s3m/settings/machine.go | 4 +- format/s3m/settings/voicefactory.go | 4 +- format/s3m/voice/voice.go | 11 +- format/xm/layout/channelsetting.go | 10 +- format/xm/settings/machine.go | 4 +- format/xm/settings/voicefactory.go | 4 +- format/xm/voice/voice.go | 12 +- player/machine/channel_noteaction.go | 36 +++--- player/machine/machine.go | 4 +- player/machine/machine_factory.go | 49 ++++++-- player/machine/machine_globals.go | 3 +- player/machine/machine_render.go | 127 +++++++++++++++++++++ player/machine/machine_tick.go | 119 +------------------ player/machine/settings/machinesettings.go | 2 +- player/render/channel.go | 13 +-- song/channelsettings.go | 5 +- voice/component/modulator_pan.go | 13 ++- voice/config.go | 9 +- voice/voicefactory.go | 2 +- 35 files changed, 365 insertions(+), 262 deletions(-) create mode 100644 player/machine/machine_render.go diff --git a/filter/amigafilter.go b/filter/amigafilter.go index acf9df0..1470ba1 100644 --- a/filter/amigafilter.go +++ b/filter/amigafilter.go @@ -23,13 +23,10 @@ type AmigaLPF struct { } // NewAmigaLPF creates a new AmigaLPF -func NewAmigaLPF(instrument, playback period.Frequency) *AmigaLPF { - lpf := AmigaLPF{ - playbackRate: playback, - } - lpf.recalculate() - - return &lpf +func NewAmigaLPF(instrument period.Frequency) *AmigaLPF { + var f AmigaLPF + f.SetPlaybackRate(instrument) + return &f } func (f *AmigaLPF) Clone() Filter { @@ -41,39 +38,17 @@ func (f *AmigaLPF) Clone() Filter { 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 - } - return wet -} - -func (f *AmigaLPF) recalculate() { +func (f *AmigaLPF) SetPlaybackRate(playback period.Frequency) { 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 +62,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, channelData{}) + } + 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 uint8) { } diff --git a/filter/echofilter.go b/filter/echofilter.go index 2d0a28c..5de19af 100644 --- a/filter/echofilter.go +++ b/filter/echofilter.go @@ -22,12 +22,10 @@ type EchoFilterFactory struct { } func (e *EchoFilterFactory) Factory() Factory { - return func(instrument, playback period.Frequency) Filter { + return func(instrument period.Frequency) Filter { echo := EchoFilter{ EchoFilterSettings: e.EchoFilterSettings, - sampleRate: playback, } - echo.recalculate() return &echo } } @@ -41,19 +39,29 @@ type delayInfo struct { type EchoFilter struct { EchoFilterSettings - sampleRate period.Frequency initialFeedback volume.Volume writePos int delay [2]delayInfo // L,R } +func (e *EchoFilter) SetPlaybackRate(playback period.Frequency) { + 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 { copy(clone.delay[i].buf, e.delay[i].buf) } @@ -113,19 +121,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 uint8) { } diff --git a/filter/filter.go b/filter/filter.go index 10f6e7f..4114912 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -8,9 +8,10 @@ import ( // Filter is an interface to a filter type Filter interface { Filter(volume.Matrix) volume.Matrix + SetPlaybackRate(playbackRate period.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 period.Frequency) Filter diff --git a/format/it/filter/resonantfilter.go b/format/it/filter/resonantfilter.go index 34bb19f..2389e7f 100644 --- a/format/it/filter/resonantfilter.go +++ b/format/it/filter/resonantfilter.go @@ -25,15 +25,17 @@ 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 } // 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, +func NewResonantFilter(cutoff uint8, resonance uint8, extendedFilterRange bool, highpass bool) filter.Filter { + rf := ResonantFilter{ highpass: highpass, extendedFilterRange: extendedFilterRange, } @@ -47,8 +49,22 @@ func NewResonantFilter(cutoff uint8, resonance uint8, playbackRate period.Freque rf.cutoff.Set(uint8(c)) } - rf.recalculate(c) - return rf + return &rf +} + +func (f *ResonantFilter) SetPlaybackRate(playback period.Frequency) { + 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.Filter { @@ -138,8 +154,7 @@ func (f *ResonantFilter) recalculate(v uint8) { const dampingFactorDivisor = ((24.0 / 128.0) / 20.0) dampingFactor := math.Pow(10.0, -float64(resonance)*dampingFactorDivisor) - f2 := float64(f.playbackRate) / 2.0 - freq := f2 + freq := f.f2 fcComputedCutoff := float64(computedCutoff) freq = 110.0 * math.Pow(2.0, 0.25+(fcComputedCutoff/filterRange)) if freq < 120.0 { @@ -147,15 +162,15 @@ func (f *ResonantFilter) recalculate(v uint8) { } else if freq > 20000 { freq = 20000 } - if freq > f2 { - freq = f2 + if freq > f.f2 { + 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 { @@ -164,7 +179,7 @@ func (f *ResonantFilter) recalculate(v uint8) { d = (2.0*dampingFactor - d) / r e = 1.0 / (r * r) } else { - r := float64(f.playbackRate) / fc + r := f.fr / fc d = dampingFactor*r + dampingFactor - 1.0 e = r * r diff --git a/format/it/layout/channelsetting.go b/format/it/layout/channelsetting.go index 6b7cfd6..af88dba 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -5,6 +5,7 @@ import ( itPanning "github.com/gotracker/playback/format/it/panning" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/vol0optimization" ) // ChannelSetting is settings specific to a single channel @@ -43,7 +44,7 @@ func (c ChannelSetting) GetMemory() song.ChannelMemory { return &c.Memory } -func (c ChannelSetting) GetPanEnabled() bool { +func (c ChannelSetting) IsPanEnabled() bool { return true } @@ -54,3 +55,10 @@ func (c ChannelSetting) GetDefaultFilterName() string { func (c ChannelSetting) IsDefaultFilterEnabled() bool { return false } + +func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { + return vol0optimization.Vol0OptimizationSettings{ + Enabled: true, + MaxTicksAt0: 3, + } +} diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 50887c9..8736612 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -148,8 +148,8 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf pluginFilterFactory filter.Factory ) if inst.InitialFilterResonance != 0 { - channelFilterFactory = func(instrument, playback period.Frequency) filter.Filter { - return itfilter.NewResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, playback, convSettings.extendedFilterRange, convSettings.useHighPassFilter) + channelFilterFactory = func(instrument period.Frequency) filter.Filter { + return itfilter.NewResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, convSettings.extendedFilterRange, convSettings.useHighPassFilter) } } diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go index 6663de1..d0df42b 100644 --- a/format/it/settings/machine.go +++ b/format/it/settings/machine.go @@ -51,8 +51,8 @@ var ( func filterFactory(name string) (settings.FilterFactoryFunc, error) { switch name { case "amigalpf": - return func(instrument, playback period.Frequency) (filter.Filter, error) { - lpf := filter.NewAmigaLPF(instrument, playback) + return func(instrument period.Frequency) (filter.Filter, error) { + lpf := filter.NewAmigaLPF(instrument) return lpf, nil }, nil diff --git a/format/it/settings/voicefactory.go b/format/it/settings/voicefactory.go index eefef9f..1a8c804 100644 --- a/format/it/settings/voicefactory.go +++ b/format/it/settings/voicefactory.go @@ -10,8 +10,8 @@ import ( type voiceFactory[TPeriod period.Period] struct{} -func (voiceFactory[TPeriod]) NewVoice() voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { - return itVoice.New(GetMachineSettings[TPeriod]()) +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, GetMachineSettings[TPeriod]()) } var ( diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 024e836..456f0d8 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -53,7 +53,7 @@ var ( _ voice.FilterEnvelope = (*itVoice[period.Linear])(nil) ) -func New[TPeriod Period](ms *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { +func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], ms *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { v := &itVoice[TPeriod]{ ms: ms, pitchAndFilterEnvShared: true, @@ -69,14 +69,17 @@ func New[TPeriod Period](ms *settings.MachineSettings[TPeriod, itVolume.FineVolu v.amp.Setup(component.AmpModulatorSettings[itVolume.FineVolume, itVolume.Volume]{ Active: true, - DefaultMixingVolume: itVolume.MaxItFineVolume, - DefaultVolume: itVolume.Volume(itVolume.MaxItVolume), + DefaultMixingVolume: config.InitialMixing, + DefaultVolume: config.InitialVolume, }) v.pan.Setup(component.PanModulatorSettings[itPanning.Panning]{ - InitialPan: itPanning.DefaultPanning, + Enabled: config.PanEnabled, + InitialPan: config.InitialPan, }) + v.vol0Opt.Setup(config.Vol0Optimization) + return v } @@ -164,7 +167,6 @@ func (v *itVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, itVolume v.filterEnv.Setup(component.EnvelopeSettings[int8, uint8]{ Envelope: config.PitchFiltEnv, }) - v.vol0Opt.Setup(config.Vol0Optimization) v.KeyModulator.Release() v.Reset() } diff --git a/format/s3m/channel/effect_finevibrato.go b/format/s3m/channel/effect_finevibrato.go index 278cb8a..fb31deb 100644 --- a/format/s3m/channel/effect_finevibrato.go +++ b/format/s3m/channel/effect_finevibrato.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -27,7 +28,7 @@ func (e FineVibrato) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV // 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 m.SetChannelPeriodDelta(ch, period.Delta(value)*s3mSystem.SlideFinesPerSemitone) }) } return nil diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go index 8a1f558..62e76dc 100644 --- a/format/s3m/channel/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -29,7 +30,7 @@ func (e PortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVol return nil } - return m.DoChannelPortaDown(ch, period.Delta(xx)*4) + return m.DoChannelPortaDown(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) } func (e PortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 5fd7368..2424c63 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -33,7 +34,7 @@ func (e PortaToNote) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV return nil } - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) } func (e PortaToNote) TraceData() string { diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go index 9484352..6e89d21 100644 --- a/format/s3m/channel/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -29,7 +30,7 @@ func (e PortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum return nil } - return m.DoChannelPortaUp(ch, period.Delta(xx)*4) + return m.DoChannelPortaUp(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) } func (e PortaUp) TraceData() string { diff --git a/format/s3m/channel/effect_vibrato.go b/format/s3m/channel/effect_vibrato.go index 5d42914..3a4527a 100644 --- a/format/s3m/channel/effect_vibrato.go +++ b/format/s3m/channel/effect_vibrato.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -26,7 +27,7 @@ func (e Vibrato) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum // 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 m.SetChannelPeriodDelta(ch, period.Delta(value)*s3mSystem.SlideFinesPerSemitone) }) } return nil diff --git a/format/s3m/layout/channelsetting.go b/format/s3m/layout/channelsetting.go index c082d03..8bcf0a6 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -6,6 +6,7 @@ import ( s3mPanning "github.com/gotracker/playback/format/s3m/panning" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/vol0optimization" ) // ChannelSetting is settings specific to a single channel @@ -49,7 +50,7 @@ func (c ChannelSetting) GetMemory() song.ChannelMemory { return &c.Memory } -func (c ChannelSetting) GetPanEnabled() bool { +func (c ChannelSetting) IsPanEnabled() bool { return c.PanEnabled } @@ -60,3 +61,10 @@ func (c ChannelSetting) GetDefaultFilterName() string { func (c ChannelSetting) IsDefaultFilterEnabled() bool { return len(c.DefaultFilterName) > 0 } + +func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { + return vol0optimization.Vol0OptimizationSettings{ + Enabled: c.Memory.Shared.ZeroVolOptimization, + MaxTicksAt0: 3, + } +} diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index e368602..10dc533 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -311,6 +311,7 @@ func Read(r io.Reader) (*s3mfile.File, error) { OrderCount: uint16(mf.Head.SongLen), InstrumentCount: 31, PatternCount: uint16(len(mf.Patterns)), + Flags: 0x0004 | 0x0010 | 0x0020, // amigaSlides | amigaLimits | sbFilterEnable GlobalVolume: s3mfile.DefaultVolume, InitialSpeed: 6, InitialTempo: 125, diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go index 55c9259..2d9f38e 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -32,8 +32,8 @@ var ( func filterFactory(name string) (settings.FilterFactoryFunc, error) { switch name { case "amigalpf": - return func(instrument, playback period.Frequency) (filter.Filter, error) { - lpf := filter.NewAmigaLPF(instrument, playback) + return func(instrument period.Frequency) (filter.Filter, error) { + lpf := filter.NewAmigaLPF(instrument) return lpf, nil }, nil diff --git a/format/s3m/settings/voicefactory.go b/format/s3m/settings/voicefactory.go index 1d14878..f1f6216 100644 --- a/format/s3m/settings/voicefactory.go +++ b/format/s3m/settings/voicefactory.go @@ -10,8 +10,8 @@ import ( type voiceFactory struct{} -func (voiceFactory) NewVoice() voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { - return s3mVoice.New(GetMachineSettings()) +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, GetMachineSettings()) } var ( diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index 1c4d965..dacefc8 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -35,7 +35,7 @@ var ( _ voice.PanModulator[s3mPanning.Panning] = (*s3mVoice)(nil) ) -func New(ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { +func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { v := &s3mVoice{ ms: ms, } @@ -50,14 +50,16 @@ func New(ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume. v.AmpModulator.Setup(component.AmpModulatorSettings[s3mVolume.FineVolume, s3mVolume.Volume]{ Active: true, - DefaultMixingVolume: s3mVolume.MaxFineVolume, - DefaultVolume: s3mVolume.MaxVolume, + DefaultMixingVolume: config.InitialMixing, + DefaultVolume: config.InitialVolume, }) v.PanModulator.Setup(component.PanModulatorSettings[s3mPanning.Panning]{ - InitialPan: s3mPanning.DefaultPanning, + Enabled: config.PanEnabled, + InitialPan: config.InitialPan, }) + v.vol0Opt.Setup(config.Vol0Optimization) return v } @@ -94,7 +96,6 @@ func (v *s3mVoice) Setup(config voice.InstrumentConfig[period.Amiga, s3mVolume.V v.config = config v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{}) - v.vol0Opt.Setup(config.Vol0Optimization) v.KeyModulator.Release() v.Reset() } diff --git a/format/xm/layout/channelsetting.go b/format/xm/layout/channelsetting.go index f563cf4..e06a7da 100644 --- a/format/xm/layout/channelsetting.go +++ b/format/xm/layout/channelsetting.go @@ -5,6 +5,7 @@ import ( xmPanning "github.com/gotracker/playback/format/xm/panning" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/vol0optimization" ) // ChannelSetting is settings specific to a single channel @@ -42,7 +43,7 @@ func (c ChannelSetting) GetMemory() song.ChannelMemory { return &c.Memory } -func (c ChannelSetting) GetPanEnabled() bool { +func (c ChannelSetting) IsPanEnabled() bool { return true } @@ -53,3 +54,10 @@ func (c ChannelSetting) GetDefaultFilterName() string { func (c ChannelSetting) IsDefaultFilterEnabled() bool { return false } + +func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { + return vol0optimization.Vol0OptimizationSettings{ + Enabled: true, + MaxTicksAt0: 3, + } +} diff --git a/format/xm/settings/machine.go b/format/xm/settings/machine.go index 15727f6..07a45fd 100644 --- a/format/xm/settings/machine.go +++ b/format/xm/settings/machine.go @@ -50,8 +50,8 @@ var ( func filterFactory(name string) (settings.FilterFactoryFunc, error) { switch name { case "amigalpf": - return func(instrument, playback period.Frequency) (filter.Filter, error) { - lpf := filter.NewAmigaLPF(instrument, playback) + return func(instrument period.Frequency) (filter.Filter, error) { + lpf := filter.NewAmigaLPF(instrument) return lpf, nil }, nil diff --git a/format/xm/settings/voicefactory.go b/format/xm/settings/voicefactory.go index 0fecd3c..a33a918 100644 --- a/format/xm/settings/voicefactory.go +++ b/format/xm/settings/voicefactory.go @@ -10,8 +10,8 @@ import ( type voiceFactory[TPeriod period.Period] struct{} -func (voiceFactory[TPeriod]) NewVoice() voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { - return xmVoice.New(GetMachineSettings[TPeriod]()) +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, GetMachineSettings[TPeriod]()) } var ( diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index b216912..19c28a6 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -45,7 +45,7 @@ var ( _ voice.PanEnvelope[xmPanning.Panning] = (*xmVoice[period.Linear])(nil) ) -func New[TPeriod Period](ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { +func New[TPeriod Period](config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { v := &xmVoice[TPeriod]{ ms: ms, } @@ -60,14 +60,17 @@ func New[TPeriod Period](ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume v.amp.Setup(component.AmpModulatorSettings[xmVolume.XmVolume, xmVolume.XmVolume]{ Active: true, - DefaultMixingVolume: xmVolume.DefaultXmMixingVolume, - DefaultVolume: xmVolume.DefaultXmVolume, + DefaultMixingVolume: config.InitialMixing, + DefaultVolume: config.InitialVolume, }) v.pan.Setup(component.PanModulatorSettings[xmPanning.Panning]{ - InitialPan: xmPanning.DefaultPanning, + Enabled: config.PanEnabled, + InitialPan: config.InitialPan, }) + v.vol0Opt.Setup(config.Vol0Optimization) + return v } @@ -140,7 +143,6 @@ func (v *xmVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, xmVolume v.panEnv.Setup(component.EnvelopeSettings[xmPanning.Panning, xmPanning.Panning]{ Envelope: config.PanEnv, }) - v.vol0Opt.Setup(config.Vol0Optimization) v.KeyModulator.Release() v.Reset() } diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index c9b3bc9..a07fa24 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -5,11 +5,11 @@ import ( "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" - "github.com/gotracker/playback/voice/vol0optimization" ) -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { na, set := c.target.ActionTick.Get() if !set { // assume continue @@ -66,10 +66,12 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo case note.ActionRetrigger: c.cv.Release() - if err := c.doSetupInstrument(ch, m); err != nil { + if err := c.doSetupInstrument(ch, m, outputRate); err != nil { return err } + c.memory.Retrigger() + for _, o := range c.osc { o.Reset() } @@ -92,7 +94,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupInstrument(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupInstrument(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { inst := c.target.Inst prevInst := c.prev.Inst if inst != nil { @@ -100,7 +102,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe switch inst.GetKind() { case instrument.KindPCM: d := inst.GetData().(*instrument.PCM[TMixingVolume, TVolume, TPanning]) - if err := c.doSetupPCM(ch, m, inst, d); err != nil { + if err := c.doSetupPCM(ch, m, inst, d, outputRate); err != nil { return err } @@ -122,30 +124,26 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupPCM(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.PCM[TMixingVolume, TVolume, TPanning]) error { - outputRate := m.getSampleRate() - +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupPCM(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.PCM[TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { var ( voiceFilter filter.Filter pluginFilter filter.Filter ) if factory := inst.GetFilterFactory(); factory != nil { - voiceFilter = factory(inst.SampleRate, outputRate) + voiceFilter = factory(inst.SampleRate) + voiceFilter.SetPlaybackRate(outputRate) } if factory := inst.GetPluginFilterFactory(); factory != nil { - pluginFilter = factory(inst.SampleRate, outputRate) + pluginFilter = factory(inst.SampleRate) + pluginFilter.SetPlaybackRate(outputRate) } c.cv.Setup(voice.InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ - SampleRate: inst.GetSampleRate(), - AutoVibrato: inst.GetAutoVibrato(), - Data: d, - VoiceFilter: voiceFilter, - PluginFilter: pluginFilter, - Vol0Optimization: vol0optimization.Vol0OptimizationSettings{ - Enabled: true, - MaxTicksAt0: 3, - }, + SampleRate: inst.GetSampleRate(), + AutoVibrato: inst.GetAutoVibrato(), + Data: d, + VoiceFilter: voiceFilter, + PluginFilter: pluginFilter, FadeOut: d.FadeOut, PitchPan: d.PitchPan, VolEnv: d.VolEnv, diff --git a/player/machine/machine.go b/player/machine/machine.go index a882b81..32f83dd 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -135,10 +135,8 @@ type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann us settings.UserSettings opl2 render.OPL2Chip - rowStringer render.RowStringer - + rowStringer render.RowStringer outputChannels []render.Channel[TGlobalVolume, TMixingVolume, TPanning] - getSampleRate func() period.Frequency } func NewMachine(songData song.Data, us settings.UserSettings) (MachineTicker, error) { diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index a0b4bf4..86b34fc 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -4,13 +4,13 @@ import ( "fmt" "reflect" + "github.com/gotracker/gomixing/volume" "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" - "github.com/gotracker/playback/voice/types" ) type typeLookup struct { @@ -105,23 +105,56 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc := &m.outputChannels[ch] rc.ChannelNum = i rc.Filter = nil - rc.GetSampleRate = m.getSampleRate + + if cs.IsDefaultFilterEnabled() { + name := cs.GetDefaultFilterName() + filtFactory, err := ms.GetFilterFactory(name) + if err != nil { + return nil, err + } + + filt, err := filtFactory(sys.GetCommonRate()) + if err != nil { + return nil, err + } + + rc.Filter = filt + } rc.GetOPL2Chip = func() render.OPL2Chip { - // TODO: add OPL2 back in - return nil + return m.opl2 } - rc.ChannelVolume = types.GetMaxVolume[TMixingVolume]() - rc.LastGlobalVolume = types.GetMaxVolume[TGlobalVolume]() // this is the channel's version of the GlobalVolume + + 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.GetEnabled() c.pn.MaxPastNotes = sys.GetMaxPastNotesPerChannel() - c.cv = m.ms.VoiceFactory.NewVoice() + c.cv = m.ms.VoiceFactory.NewVoice(voice.VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ + InitialVolume: initialVolume, + InitialMixing: initialMixing, + PanEnabled: cs.IsPanEnabled(), + InitialPan: initialPan, + Vol0Optimization: cs.GetVol0OptimizationSettings(), + }) c.memory = cs.GetMemory() c.target.ActionTick.Reset() c.nna = note.ActionCut - var err error if c.osc[OscillatorVibrato], err = ms.GetVibratoFactory(); err != nil { return nil, err } diff --git a/player/machine/machine_globals.go b/player/machine/machine_globals.go index f05b2f8..83d0e1e 100644 --- a/player/machine/machine_globals.go +++ b/player/machine/machine_globals.go @@ -144,12 +144,11 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetF } cr := m.songData.GetSystem().GetCommonRate() - sr := m.getSampleRate() for i := range m.channels { c := &m.channels[i] if enabled { - filt, err := filtFactory(cr, sr) + filt, err := filtFactory(cr) if err != nil { return err } diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go new file mode 100644 index 0000000..9ecf98a --- /dev/null +++ b/player/machine/machine_render.go @@ -0,0 +1,127 @@ +package machine + +import ( + "fmt" + + "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/gomixing/panning" + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" + "github.com/gotracker/playback/output" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/player/sampler" + "github.com/gotracker/playback/system" + "github.com/gotracker/playback/voice" +) + +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 + } + + sys := m.songData.GetSystem() + + details := render.Details{ + Mix: s.Mixer(), + Panmixer: s.GetPanMixer(), + SamplerSpeed: sys.GetSamplerSpeed(period.Frequency(s.SampleRate)), + Samples: premix.SamplesLen, + Duration: tickDuration, + } + + centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) + + for i := range m.channels { + ch := index.Channel(i) + c := &m.channels[ch] + var mixData []mixing.Data + + if pos, set := c.target.Pos.Get(); set { + if samp, ok := c.cv.(voice.Sampler); ok { + samp.SetPos(pos) + } + c.target.Pos.Reset() + } + + rc := &m.outputChannels[ch] + if filt := rc.Filter; filt != nil { + filt.SetPlaybackRate(system.Frequency(s.SampleRate)) + } + + rc.GlobalVolume = m.gv.ToVolume() + + c.cv.DumpState(ch, m.us.Tracer) + data, err := c.cv.Render(centerAheadPan, details, rc) + if err != nil { + return nil, err + } + c.cv.Advance() + if data != nil { + mixData = append(mixData, *data) + } + + pnData, err := c.pn.Render(centerAheadPan, details, rc) + if err != nil { + return nil, err + } + if len(pnData) > 0 { + mixData = append(mixData, pnData...) + } + + 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_tick.go b/player/machine/machine_tick.go index 4924041..c42c239 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -2,51 +2,24 @@ package machine import ( "errors" - "fmt" - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/panning" - "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/output" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/system" ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(s *sampler.Sampler) (*output.PremixData, error) { - m.getSampleRate = func() period.Frequency { - return period.Frequency(s.SampleRate) - } - for i := range m.channels { - if err := m.channels[i].DoNoteAction(index.Channel(i), m); err != nil { + if err := m.channels[i].DoNoteAction(index.Channel(i), m, system.Frequency(s.SampleRate)); err != nil { return nil, err } } - 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 + premix, err := m.render(s) + if err != nil { + return premix, err } tickErr := runTick(&m.ticker, m) @@ -56,88 +29,8 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } } - sys := m.songData.GetSystem() - - details := render.Details{ - Mix: s.Mixer(), - Panmixer: s.GetPanMixer(), - SamplerSpeed: sys.GetSamplerSpeed(period.Frequency(s.SampleRate)), - Samples: premix.SamplesLen, - Duration: tickDuration, - } - - centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) - - for i := range m.channels { - ch := index.Channel(i) - c := &m.channels[ch] - var mixData []mixing.Data - - if pos, set := c.target.Pos.Get(); set { - if samp, ok := c.cv.(voice.Sampler); ok { - samp.SetPos(pos) - } - c.target.Pos.Reset() - } - - rc := &m.outputChannels[ch] - - c.cv.DumpState(ch, m.us.Tracer) - data, err := c.cv.Render(centerAheadPan, details, rc) - if err != nil { - return nil, errors.Join(tickErr, err) - } - c.cv.Advance() - if data != nil { - mixData = append(mixData, *data) - } - - pnData, err := c.pn.Render(centerAheadPan, details, rc) - if err != nil { - return nil, errors.Join(tickErr, err) - } - if len(pnData) > 0 { - mixData = append(mixData, pnData...) - } - - 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, errors.Join(tickErr, 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, - }, - }) - } - m.age++ - return &premix, tickErr + return premix, errors.Join(tickErr, err) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTick() error { diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go index dca8d3e..395d0aa 100644 --- a/player/machine/settings/machinesettings.go +++ b/player/machine/settings/machinesettings.go @@ -14,7 +14,7 @@ type ( Panning = voice.Panning ) -type FilterFactoryFunc func(instrument, playback period.Frequency) (filter.Filter, error) +type FilterFactoryFunc func(instrument period.Frequency) (filter.Filter, error) type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { PeriodConverter song.PeriodCalculator[TPeriod] diff --git a/player/render/channel.go b/player/render/channel.go index 7276690..8989962 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -4,7 +4,6 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/period" "github.com/gotracker/playback/song" channelfilter "github.com/gotracker/playback/voice/filter" ) @@ -16,12 +15,10 @@ type ChannelIntf interface { // Channel is the important bits to make output to a particular downmixing channel work type Channel[TGlobalVolume, TMixingVolume song.Volume, TPanning song.Panning] struct { - ChannelNum int - Filter filter.Filter - GetSampleRate func() period.Frequency - GetOPL2Chip func() OPL2Chip - ChannelVolume TMixingVolume - LastGlobalVolume TGlobalVolume // this is the channel's version of the GlobalVolume + ChannelNum int + Filter filter.Filter + GetOPL2Chip func() OPL2Chip + GlobalVolume volume.Volume // this is the channel's version of the GlobalVolume } // ApplyFilter will apply the channel filter, if there is one. @@ -39,7 +36,7 @@ func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) ApplyFilter(dry volum // GetPremixVolume returns the premix volume of the output channel func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) GetPremixVolume() volume.Volume { - return oc.LastGlobalVolume.ToVolume() * oc.ChannelVolume.ToVolume() + return oc.GlobalVolume } // SetFilterEnvelopeValue updates the filter on the channel with the new envelope value diff --git a/song/channelsettings.go b/song/channelsettings.go index 76ea524..f779dbf 100644 --- a/song/channelsettings.go +++ b/song/channelsettings.go @@ -2,15 +2,18 @@ package song import ( "errors" + + "github.com/gotracker/playback/voice/vol0optimization" ) type ChannelSettings interface { GetEnabled() bool GetOutputChannelNum() int GetMemory() ChannelMemory - GetPanEnabled() bool + IsPanEnabled() bool GetDefaultFilterName() string IsDefaultFilterEnabled() bool + GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings } type channelInitialVolumeGetter[TVolume Volume] interface { diff --git a/voice/component/modulator_pan.go b/voice/component/modulator_pan.go index 32fa5f0..c796cef 100755 --- a/voice/component/modulator_pan.go +++ b/voice/component/modulator_pan.go @@ -17,6 +17,7 @@ type PanModulator[TPanning types.Panning] struct { } type PanModulatorSettings[TPanning types.Panning] struct { + Enabled bool InitialPan TPanning } @@ -34,8 +35,10 @@ func (p PanModulator[TPanning]) Clone() PanModulator[TPanning] { // SetPan sets the current panning func (p *PanModulator[TPanning]) SetPan(pan TPanning) { - p.pan = pan - p.updateFinal() + if p.settings.Enabled { + p.pan = pan + p.updateFinal() + } } // GetPan returns the current panning @@ -45,8 +48,10 @@ func (p PanModulator[TPanning]) GetPan() TPanning { // SetPanDelta sets the current panning delta func (p *PanModulator[TPanning]) SetPanDelta(d types.PanDelta) { - p.delta = d - p.updateFinal() + if p.settings.Enabled { + p.delta = d + p.updateFinal() + } } // GetPanDelta returns the current panning delta diff --git a/voice/config.go b/voice/config.go index 9297aa2..7b74fe9 100644 --- a/voice/config.go +++ b/voice/config.go @@ -23,7 +23,6 @@ type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volu Data instrument.Data VoiceFilter filter.Filter PluginFilter filter.Filter - Vol0Optimization vol0optimization.Vol0OptimizationSettings FadeOut fadeout.Settings PitchPan instrument.PitchPan VolEnv envelope.Envelope[TVolume] @@ -32,3 +31,11 @@ type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volu PitchFiltMode bool // true = filter, false = pitch PitchFiltEnv envelope.Envelope[filter.PitchFiltValue] // this is either pitch or filter } + +type VoiceConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + InitialVolume TVolume + InitialMixing TMixingVolume + PanEnabled bool + InitialPan TPanning + Vol0Optimization vol0optimization.Vol0OptimizationSettings +} diff --git a/voice/voicefactory.go b/voice/voicefactory.go index 0e2dac4..a300fde 100644 --- a/voice/voicefactory.go +++ b/voice/voicefactory.go @@ -1,5 +1,5 @@ package voice type VoiceFactory[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { - NewVoice() RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + NewVoice(settings VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] } From b16c3b1df2b60837c2feb751b6340f6c6d3b72ec Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 13 Jan 2024 17:15:34 -0800 Subject: [PATCH 33/63] Fix song looping, starting order, and pattern loop - also fix echo filter clone --- filter/echofilter.go | 1 + format/it/channel/memory.go | 7 - format/it/layout/channelsetting.go | 11 +- format/it/load/itformat.go | 6 + format/it/pattern/state.go | 341 -------------------------- format/s3m/layout/header.go | 1 - format/s3m/load/s3mformat.go | 6 +- format/s3m/pattern/state.go | 339 ------------------------- format/xm/channel/memory.go | 7 - format/xm/load/xmformat.go | 1 + format/xm/pattern/state.go | 341 -------------------------- player/machine/channel.go | 9 +- player/machine/channel_patternloop.go | 36 +++ player/machine/channel_tick.go | 8 +- player/machine/machine_channel.go | 11 +- player/machine/machine_factory.go | 6 +- player/machine/machine_globals.go | 8 +- player/machine/ticker.go | 70 ++++-- util/loopdetect.go | 46 ---- util/patternloop.go | 27 -- 20 files changed, 133 insertions(+), 1149 deletions(-) delete mode 100644 format/it/pattern/state.go delete mode 100644 format/s3m/pattern/state.go delete mode 100644 format/xm/pattern/state.go create mode 100644 player/machine/channel_patternloop.go delete mode 100644 util/loopdetect.go delete mode 100644 util/patternloop.go diff --git a/filter/echofilter.go b/filter/echofilter.go index 5de19af..2f05579 100644 --- a/filter/echofilter.go +++ b/filter/echofilter.go @@ -63,6 +63,7 @@ func (e *EchoFilter) Clone() Filter { writePos: e.writePos, } 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 diff --git a/format/it/channel/memory.go b/format/it/channel/memory.go index a6508ed..d602a3f 100644 --- a/format/it/channel/memory.go +++ b/format/it/channel/memory.go @@ -6,7 +6,6 @@ import ( "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 @@ -33,7 +32,6 @@ type Memory struct { vibratoOscillator oscillator.Oscillator tremoloOscillator oscillator.Oscillator panbrelloOscillator oscillator.Oscillator - patternLoop formatutil.PatternLoop HighOffset int Shared *SharedMemory @@ -164,11 +162,6 @@ func (m *Memory) Retrigger() { } } -// 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() { if m.Shared.ResetMemoryAtStartOfOrder0 { diff --git a/format/it/layout/channelsetting.go b/format/it/layout/channelsetting.go index af88dba..9a62201 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -14,8 +14,10 @@ type ChannelSetting struct { OutputChannelNum int InitialVolume itVolume.Volume ChannelVolume itVolume.FineVolume + PanEnabled bool InitialPanning itPanning.Panning Memory channel.Memory + Vol0OptEnabled bool } var _ song.ChannelSettings = (*ChannelSetting)(nil) @@ -37,7 +39,10 @@ func (c ChannelSetting) GetMixingVolume() itVolume.FineVolume { } func (c ChannelSetting) GetInitialPanning() itPanning.Panning { - return c.InitialPanning + if c.PanEnabled { + return c.InitialPanning + } + return itPanning.DefaultPanning } func (c ChannelSetting) GetMemory() song.ChannelMemory { @@ -45,7 +50,7 @@ func (c ChannelSetting) GetMemory() song.ChannelMemory { } func (c ChannelSetting) IsPanEnabled() bool { - return true + return c.PanEnabled } func (c ChannelSetting) GetDefaultFilterName() string { @@ -58,7 +63,7 @@ func (c ChannelSetting) IsDefaultFilterEnabled() bool { func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { return vol0optimization.Vol0OptimizationSettings{ - Enabled: true, + Enabled: c.Vol0OptEnabled, MaxTicksAt0: 3, } } diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index b594003..4babac2 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -35,7 +35,9 @@ func moduleHeaderToHeader(fh *itfile.ModuleHeader) (*layout.Header, error) { 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: @@ -104,6 +106,8 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] linearFrequencySlides := f.Head.Flags.IsLinearSlides() oldEffectMode := f.Head.Flags.IsOldEffects() efgLinkMode := f.Head.Flags.IsEFGLinking() + stereoMode := f.Head.Flags.IsStereo() + vol0Enabled := f.Head.Flags.IsVol0Optimizations() songData := &layout.Song[TPeriod]{ System: itSystem.ITSystem, @@ -189,10 +193,12 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] Enabled: true, 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() diff --git a/format/it/pattern/state.go b/format/it/pattern/state.go deleted file mode 100644 index e272584..0000000 --- a/format/it/pattern/state.go +++ /dev/null @@ -1,341 +0,0 @@ -package pattern - -import ( - "errors" - - "github.com/gotracker/playback/format/it/channel" - itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/period" - "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[TPeriod period.Period] 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[TPeriod] - Orders []index.Pattern -} - -// GetTempo returns the tempo of the current state -func (state *State[TPeriod]) GetTempo() int { - return state.tempo -} - -// GetSpeed returns the row speed of the current state -func (state *State[TPeriod]) GetSpeed() int { - return state.ticks -} - -// GetTicksThisRow returns the number of ticks in the current row -func (state *State[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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[TPeriod]) WantsStop() bool { - return state.GetPatNum() == index.InvalidPattern -} - -// setCurrentOrder sets the current order index -func (state *State[TPeriod]) setCurrentOrder(order index.Order) { - state.currentOrder = order -} - -func (state *State[TPeriod]) advanceOrder() { - state.setCurrentOrder(state.currentOrder + 1) -} - -// GetCurrentOrder returns the current order -func (state *State[TPeriod]) GetCurrentOrder() index.Order { - return state.currentOrder -} - -// GetNumOrders returns the number of orders in the song -func (state *State[TPeriod]) GetNumOrders() int { - return len(state.Orders) -} - -// GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State[TPeriod]) 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[TPeriod]) GetCurrentRow() index.Row { - return state.currentRow -} - -// setCurrentRow sets the current row -func (state *State[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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[TPeriod]) Reset() { - *state = State[TPeriod]{ - 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[TPeriod]) 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[TPeriod]) GetRows() (*song.Pattern[channel.Data[TPeriod], itVolume.Volume], 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 - } - pat := &state.Patterns[patNum] - return &pat.Pattern, nil - } - } - return nil, nil -} - -// NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State[TPeriod]) 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[TPeriod]) 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[TPeriod]) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn -} diff --git a/format/s3m/layout/header.go b/format/s3m/layout/header.go index ea56509..7a3fe6e 100644 --- a/format/s3m/layout/header.go +++ b/format/s3m/layout/header.go @@ -12,6 +12,5 @@ type Header struct { InitialTempo int GlobalVolume s3mVolume.Volume MixingVolume s3mVolume.FineVolume - Stereo bool InitialOrder index.Order } diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index 7dd7d74..6550d89 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -34,7 +34,8 @@ func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { InitialSpeed: int(fh.InitialSpeed), InitialTempo: int(fh.InitialTempo), GlobalVolume: s3mVolume.Volume(fh.GlobalVolume), - Stereo: (fh.MixingVolume & 0x80) != 0, + MixingVolume: s3mVolume.FineVolume(fh.MixingVolume &^ 0x80), + InitialOrder: 0, } z := uint32(fh.MixingVolume & 0x7f) @@ -267,6 +268,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, signedSamples = true } + stereoMode := (f.Head.MixingVolume & 0x80) != 0 st2Vibrato := (f.Head.Flags & 0x0001) != 0 st2Tempo := (f.Head.Flags & 0x0002) != 0 amigaSlides := (f.Head.Flags & 0x0004) != 0 @@ -330,7 +332,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, Category: chn.GetChannelCategory(), OutputChannelNum: int(ch.GetChannel() & 0x07), InitialVolume: s3mVolume.Volume(s3mfile.DefaultVolume), - PanEnabled: h.Stereo, + PanEnabled: stereoMode, InitialPanning: s3mPanning.DefaultPanning, Memory: channel.Memory{ Shared: &sharedMem, diff --git a/format/s3m/pattern/state.go b/format/s3m/pattern/state.go deleted file mode 100644 index b5a7081..0000000 --- a/format/s3m/pattern/state.go +++ /dev/null @@ -1,339 +0,0 @@ -package pattern - -import ( - "errors" - - "github.com/gotracker/playback/format/s3m/channel" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" - "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 - 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.Pattern[channel.Data, s3mVolume.Volume], 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 - } - pat := &state.Patterns[patNum] - return pat, 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/xm/channel/memory.go b/format/xm/channel/memory.go index a37de68..6bdb5aa 100644 --- a/format/xm/channel/memory.go +++ b/format/xm/channel/memory.go @@ -6,7 +6,6 @@ import ( "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 @@ -33,7 +32,6 @@ type Memory struct { tremorMem tremor.Tremor vibratoOscillator oscillator.Oscillator tremoloOscillator oscillator.Oscillator - patternLoop formatutil.PatternLoop Shared *SharedMemory } @@ -156,11 +154,6 @@ func (m *Memory) Retrigger() { } } -// 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() { if m.Shared.ResetMemoryAtStartOfOrder0 { diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index 804214b..d8d6179 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -41,6 +41,7 @@ func moduleHeaderToHeader(fh *xmfile.ModuleHeader) (*layout.Header, error) { GlobalVolume: xmVolume.DefaultXmVolume, MixingVolume: xmVolume.DefaultXmMixingVolume, LinearFreqSlides: fh.Flags.IsLinearSlides(), + InitialOrder: 0, } return &head, nil } diff --git a/format/xm/pattern/state.go b/format/xm/pattern/state.go deleted file mode 100644 index f5ca817..0000000 --- a/format/xm/pattern/state.go +++ /dev/null @@ -1,341 +0,0 @@ -package pattern - -import ( - "errors" - - "github.com/gotracker/playback/format/xm/channel" - xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/pattern" - "github.com/gotracker/playback/period" - "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[TPeriod period.Period] 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[TPeriod] - Orders []index.Pattern -} - -// GetTempo returns the tempo of the current state -func (state *State[TPeriod]) GetTempo() int { - return state.tempo -} - -// GetSpeed returns the row speed of the current state -func (state *State[TPeriod]) GetSpeed() int { - return state.ticks -} - -// GetTicksThisRow returns the number of ticks in the current row -func (state *State[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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[TPeriod]) WantsStop() bool { - return state.GetPatNum() == index.InvalidPattern -} - -// setCurrentOrder sets the current order index -func (state *State[TPeriod]) setCurrentOrder(order index.Order) { - state.currentOrder = order -} - -func (state *State[TPeriod]) advanceOrder() { - state.setCurrentOrder(state.currentOrder + 1) -} - -// GetCurrentOrder returns the current order -func (state *State[TPeriod]) GetCurrentOrder() index.Order { - return state.currentOrder -} - -// GetNumOrders returns the number of orders in the song -func (state *State[TPeriod]) GetNumOrders() int { - return len(state.Orders) -} - -// GetCurrentPatternIdx returns the current pattern index, derived from the order list -func (state *State[TPeriod]) 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[TPeriod]) GetCurrentRow() index.Row { - return state.currentRow -} - -// setCurrentRow sets the current row -func (state *State[TPeriod]) 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[TPeriod]) 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[TPeriod]) 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[TPeriod]) Reset() { - *state = State[TPeriod]{ - 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[TPeriod]) 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[TPeriod]) GetRows() (*song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume], 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 - } - pat := &state.Patterns[patNum] - return &pat.Pattern, nil - } - } - return nil, nil -} - -// NeedResetPatternLoops returns the state of the resetPatternLoops variable (and resets it) -func (state *State[TPeriod]) 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[TPeriod]) 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[TPeriod]) StartTransaction() *pattern.RowUpdateTransaction { - txn := pattern.RowUpdateTransaction{ - CommitTransaction: state.commitTransaction, - } - - return &txn -} diff --git a/player/machine/channel.go b/player/machine/channel.go index 1080b66..06506b7 100644 --- a/player/machine/channel.go +++ b/player/machine/channel.go @@ -3,12 +3,12 @@ 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/util" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/oscillator" "github.com/heucuva/optional" @@ -18,7 +18,12 @@ type channel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann enabled bool memory song.ChannelMemory osc [NumOscillators]oscillator.Oscillator - patternLoop util.PatternLoop + patternLoop struct { + Start index.Row + End index.Row + Total int + Count int + } prev struct { Period TPeriod diff --git a/player/machine/channel_patternloop.go b/player/machine/channel_patternloop.go new file mode 100644 index 0000000..7723874 --- /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 index 6eb30cd..ba91f76 100644 --- a/player/machine/channel_tick.go +++ b/player/machine/channel_tick.go @@ -11,6 +11,7 @@ import ( func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) OrderStart(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { c.memory.StartOrder() + c.resetPatternLoop() return nil } @@ -143,11 +144,10 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowE c.prev.Period = p } - if c.patternLoop.Enabled { - if nr, ok := c.patternLoop.ContinueLoop(m.ticker.current.row); ok { - return m.SetRow(nr, false) - } + if err := c.doPatternLoop(ch, m); err != nil { + return nil } + return nil } diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 9a4457c..17380e4 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -297,19 +297,12 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetP return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { pl := &c.patternLoop - total := uint8(count) - - traceChannelValueChangeWithComment(m, ch, "patternLoopEnabled", pl.Enabled, true, "SetPatternLoops") - pl.Enabled = true traceChannelValueChangeWithComment(m, ch, "patternLoopEnd", pl.End, m.ticker.current.row, "SetPatternLoops") pl.End = m.ticker.current.row - traceChannelValueChangeWithComment(m, ch, "patternLoopTotal", pl.Total, total, "SetPatternLoops") - pl.Total = total - - traceChannelValueChangeWithComment(m, ch, "patternLoopCount", pl.Count, uint8(0), "SetPatternLoops") - pl.Count = 0 + traceChannelValueChangeWithComment(m, ch, "patternLoopTotal", pl.Total, count, "SetPatternLoops") + pl.Total = count return nil }) diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 86b34fc..6a39629 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -185,8 +185,10 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum } if err := initTick(&m.ticker, &m, tickerSettings{ - Order: order, - Row: row, + InitialOrder: order, + InitialRow: row, + SongLoopStartingOrder: 0, + SongLoopCount: us.SongLoop.Count, }); err != nil { return nil, err } diff --git a/player/machine/machine_globals.go b/player/machine/machine_globals.go index 83d0e1e..9ae3d9a 100644 --- a/player/machine/machine_globals.go +++ b/player/machine/machine_globals.go @@ -131,8 +131,12 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetO } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetRow(r index.Row, breakOrder bool) error { - traceOptionalValueChangeWithComment(m, "nextRow", m.ticker.next.row, r, "SetRow") - m.ticker.next.row.Set(r) + rb := tickerRowBreak{ + row: r, + breakOrder: breakOrder, + } + traceOptionalValueChangeWithComment(m, "nextRow", m.ticker.next.row, rb, "SetRow") + m.ticker.next.row.Set(rb) return nil } diff --git a/player/machine/ticker.go b/player/machine/ticker.go index 7f792f7..1c4a986 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -9,28 +9,44 @@ import ( ) type ticker struct { - current struct { + settings tickerSettings + current struct { tick int row index.Row order index.Order } next struct { - row optional.Value[index.Row] + 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 { - Order index.Order - Row index.Row + InitialOrder index.Order + InitialRow index.Row + SongLoopStartingOrder index.Order + SongLoopCount int } -func initTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](t *ticker, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], setup tickerSettings) error { +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(setup.Row) - t.next.order.Set(setup.Order) + 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 { @@ -44,6 +60,12 @@ func initTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPan 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 { @@ -132,15 +154,17 @@ func advanceRowOrder[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum desiredOrder, orderSet := t.next.order.Get() if rowSet && orderSet { - row = int(desiredRow) + row = int(desiredRow.row) rowUpdated = true order = int(desiredOrder) orderUpdated = true } else if rowSet { - row = int(desiredRow) + row = int(desiredRow.row) rowUpdated = true - order++ - orderUpdated = true + if desiredRow.breakOrder { + order++ + orderUpdated = true + } } else if orderSet { order = int(desiredOrder) orderUpdated = true @@ -158,11 +182,8 @@ func advanceRowOrder[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum orderScanIter := 0 orderScan: if orderScanIter >= orderScanMax { - var ( - emptyRow optional.Value[index.Row] - emptyOrder optional.Value[index.Order] - ) - return emptyRow, emptyOrder, song.ErrStopSong + order = int(t.settings.SongLoopStartingOrder) + orderUpdated = true } pat, err := m.songData.GetPatternIntfByOrder(index.Order(order)) @@ -190,6 +211,23 @@ orderScan: goto orderScan } + if orderUpdated && 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 = 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)) 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 -} From b832b7dfe5ce5593a7a425c331c6176e319c1ae7 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 13 Jan 2024 23:19:46 -0800 Subject: [PATCH 34/63] past note fixes and envelope cleanup --- format/it/voice/modulator_amp.go | 6 +- format/it/voice/modulator_freq.go | 17 ++- format/it/voice/modulator_pan.go | 7 +- format/it/voice/voice.go | 103 +++++++++++++----- format/s3m/voice/voice.go | 13 ++- format/xm/voice/voice.go | 29 ++++-- player/machine/channel_noteaction.go | 12 ++- player/machine/channel_notedecode.go | 8 +- player/machine/channel_tick.go | 2 +- player/machine/machine_factory.go | 3 - player/machine/machine_pastnote.go | 50 ++++++--- player/machine/machine_render.go | 2 +- player/machine/newnoteinfo.go | 11 ++ player/machine/pastnote.go | 23 ++++- voice/component/envelope.go | 126 +++++++++++------------ voice/component/envelope_filter.go | 5 +- voice/component/envelope_pan.go | 5 +- voice/component/envelope_pitch.go | 5 +- voice/component/envelope_volume.go | 5 +- voice/component/modulator_amp.go | 60 ++++++----- voice/component/modulator_autovibrato.go | 33 +++--- voice/component/modulator_fadeout.go | 46 ++++----- voice/component/modulator_freq.go | 40 +++++-- voice/component/modulator_key.go | 15 ++- voice/component/modulator_pan.go | 32 +++--- voice/component/modulator_pitchpan.go | 33 +++--- voice/component/opl2.go | 36 +++---- voice/component/sampler.go | 81 +++++++++------ voice/component/voicer.go | 4 +- voice/component/vol0optimization.go | 33 +++--- voice/voice.go | 2 +- 31 files changed, 520 insertions(+), 327 deletions(-) diff --git a/format/it/voice/modulator_amp.go b/format/it/voice/modulator_amp.go index 97c6be8..e793b35 100644 --- a/format/it/voice/modulator_amp.go +++ b/format/it/voice/modulator_amp.go @@ -54,9 +54,5 @@ func (v itVoice[TPeriod]) GetFadeoutVolume() volume.Volume { } func (v itVoice[TPeriod]) GetFinalVolume() volume.Volume { - vol := v.amp.GetFinalVolume() - if v.IsVolumeEnvelopeEnabled() { - vol *= v.GetCurrentVolumeEnvelope().ToVolume() - } - return vol * v.fadeout.GetFinalVolume() + return v.finalVol } diff --git a/format/it/voice/modulator_freq.go b/format/it/voice/modulator_freq.go index 0577a6e..6c2313b 100644 --- a/format/it/voice/modulator_freq.go +++ b/format/it/voice/modulator_freq.go @@ -7,10 +7,12 @@ import ( // == FreqModulator == func (v *itVoice[TPeriod]) SetPeriod(period TPeriod) { - v.freq.SetPeriod(period) + if !period.IsInvalid() { + v.freq.SetPeriod(period) + } } -func (v *itVoice[TPeriod]) GetPeriod() TPeriod { +func (v itVoice[TPeriod]) GetPeriod() TPeriod { return v.freq.GetPeriod() } @@ -18,15 +20,10 @@ func (v *itVoice[TPeriod]) SetPeriodDelta(delta period.Delta) { v.freq.SetPeriodDelta(delta) } -func (v *itVoice[TPeriod]) GetPeriodDelta() period.Delta { +func (v itVoice[TPeriod]) GetPeriodDelta() period.Delta { return v.freq.GetPeriodDelta() } -func (v *itVoice[TPeriod]) GetFinalPeriod() TPeriod { - p := v.freq.GetFinalPeriod() - if v.IsPitchEnvelopeEnabled() { - delta := v.GetCurrentPitchEnvelope() - p = period.AddDelta(p, delta) - } - return p +func (v itVoice[TPeriod]) GetFinalPeriod() TPeriod { + return v.finalPeriod } diff --git a/format/it/voice/modulator_pan.go b/format/it/voice/modulator_pan.go index a66e421..ffa0ff3 100644 --- a/format/it/voice/modulator_pan.go +++ b/format/it/voice/modulator_pan.go @@ -41,10 +41,5 @@ func (v itVoice[TPeriod]) IsPitchPanEnabled() bool { } func (v itVoice[TPeriod]) GetFinalPan() itPanning.Panning { - if !v.IsPanEnvelopeEnabled() { - return v.pan.GetFinalPan() - } - - envPan := v.panEnv.GetCurrentValue() - return v.pitchPan.GetSeparatedPan(envPan) + return v.finalPan } diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 456f0d8..72e0786 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -1,6 +1,7 @@ package voice import ( + "github.com/gotracker/gomixing/volume" itPanning "github.com/gotracker/playback/format/it/panning" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/period" @@ -26,7 +27,7 @@ type itVoice[TPeriod Period] struct { component.KeyModulator - voicer component.Voicer[TPeriod, itVolume.Volume] + voicer component.Voicer[TPeriod, itVolume.FineVolume, itVolume.Volume] amp component.AmpModulator[itVolume.FineVolume, itVolume.Volume] fadeout component.FadeoutModulator freq component.FreqModulator[TPeriod] @@ -38,6 +39,11 @@ type itVoice[TPeriod Period] struct { panEnv component.PanEnvelope[itPanning.Panning] filterEnv component.FilterEnvelope vol0Opt component.Vol0Optimization + + // finals + finalVol volume.Volume + finalPeriod TPeriod + finalPan itPanning.Panning } var ( @@ -73,6 +79,8 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, DefaultVolume: config.InitialVolume, }) + v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) + v.pan.Setup(component.PanModulatorSettings[itPanning.Panning]{ Enabled: config.PanEnabled, InitialPan: config.InitialPan, @@ -100,6 +108,7 @@ func (v *itVoice[TPeriod]) doAttack() { if v.voicer != nil { v.voicer.Attack() } + v.updateFinal() } func (v *itVoice[TPeriod]) doRelease() { @@ -110,14 +119,14 @@ func (v *itVoice[TPeriod]) doRelease() { if v.voicer != nil { v.voicer.Release() } + v.updateFinal() } func (v *itVoice[TPeriod]) doFadeout() { if v.voicer != nil { v.voicer.Fadeout() } - - v.fadeout.Fadeout() + v.updateFinal() } func (v *itVoice[TPeriod]) doDeferredAttack() { @@ -138,17 +147,23 @@ func (v itVoice[TPeriod]) getFadeoutEnabled() bool { func (v *itVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) { v.config = config + v.filterEnvActive = v.config.PitchFiltMode + v.fadeoutMode = v.config.FadeOut.Mode + v.fadeout.Setup(component.FadeoutModulatorSettings{ - GetEnabled: v.getFadeoutEnabled, - Amount: config.FadeOut.Amount, + Enabled: v.config.FadeOut.Mode != fadeout.ModeDisabled, + GetActive: v.getFadeoutEnabled, + Amount: config.FadeOut.Amount, }) - v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) + v.autoVibrato.Setup(config.AutoVibrato) + v.pitchPan.Setup(component.PitchPanModulatorSettings[itPanning.Panning]{ PitchPanEnable: config.PitchPan.Enabled, PitchPanCenter: config.PitchPan.Center, PitchPanSeparation: config.PitchPan.Separation, }) + volEnvSettings := component.EnvelopeSettings[itVolume.Volume, itVolume.Volume]{ Envelope: config.VolEnv, } @@ -158,41 +173,41 @@ func (v *itVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, itVolume } } v.volEnv.Setup(volEnvSettings) + v.pitchEnv.Setup(component.EnvelopeSettings[int8, period.Delta]{ Envelope: config.PitchFiltEnv, }) + v.panEnv.Setup(component.EnvelopeSettings[itPanning.Panning, itPanning.Panning]{ Envelope: config.PanEnv, }) + v.filterEnv.Setup(component.EnvelopeSettings[int8, uint8]{ Envelope: config.PitchFiltEnv, }) + v.KeyModulator.Release() v.Reset() } func (v *itVoice[TPeriod]) Reset() { - v.filterEnvActive = v.config.PitchFiltMode - v.fadeoutMode = v.config.FadeOut.Mode - + v.amp.Reset() v.fadeout.Reset() - - v.volEnv.Reset() - v.pitchEnv.Reset() - v.panEnv.Reset() - v.filterEnv.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.voicer = nil + v.updateFinal() } func (v *itVoice[TPeriod]) IsDone() bool { @@ -242,6 +257,8 @@ func (v *itVoice[TPeriod]) Advance() { v.vol0Opt.ObserveVolume(v.GetFinalVolume()) v.KeyModulator.Advance() + + v.updateFinal() } func (v *itVoice[TPeriod]) Clone() voice.Voice { @@ -252,15 +269,21 @@ func (v *itVoice[TPeriod]) Clone() voice.Voice { filterEnvActive: v.filterEnvActive, fadeoutMode: v.fadeoutMode, amp: v.amp.Clone(), + fadeout: v.fadeout.Clone(), freq: v.freq.Clone(), + autoVibrato: v.autoVibrato.Clone(), pan: v.pan.Clone(), - volEnv: v.volEnv.Clone(), - pitchEnv: v.pitchEnv.Clone(), - panEnv: v.panEnv.Clone(), - filterEnv: v.filterEnv.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, @@ -284,13 +307,47 @@ func (v *itVoice[TPeriod]) Clone() voice.Voice { return &vv } -func (v *itVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, defVol itVolume.Volume) { - var s component.Sampler[TPeriod, itVolume.Volume] - s.Setup(component.SamplerSettings[TPeriod, itVolume.Volume]{ +func (v *itVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol itVolume.FineVolume, defVol itVolume.Volume) { + var s component.Sampler[TPeriod, itVolume.FineVolume, itVolume.Volume] + s.Setup(component.SamplerSettings[TPeriod, itVolume.FineVolume, itVolume.Volume]{ Sample: samp, DefaultVolume: defVol, + MixVolume: mixVol, WholeLoop: wholeLoop, SustainLoop: sustainLoop, }) v.voicer = &s } + +func (v *itVoice[TPeriod]) updateFinal() { + if v.IsDone() { + v.finalVol = 0 + return + } + + // 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 := v.freq.GetFinalPeriod() + if v.IsPitchEnvelopeEnabled() { + delta := v.GetCurrentPitchEnvelope() + p = period.AddDelta(p, delta) + } + v.finalPeriod = p + + // panning + if !v.IsPanEnvelopeEnabled() { + v.finalPan = v.pan.GetFinalPan() + } else { + envPan := v.panEnv.GetCurrentValue() + v.finalPan = v.pitchPan.GetSeparatedPan(envPan) + } +} diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index dacefc8..de1d769 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -21,7 +21,7 @@ type s3mVoice struct { component.KeyModulator - voicer component.Voicer[period.Amiga, s3mVolume.Volume] + voicer component.Voicer[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] component.AmpModulator[s3mVolume.FineVolume, s3mVolume.Volume] component.FreqModulator[period.Amiga] component.PanModulator[s3mPanning.Panning] @@ -150,14 +150,19 @@ func (v *s3mVoice) Clone() voice.Voice { vv.config.VoiceFilter = v.config.VoiceFilter.Clone() } + if v.config.PluginFilter != nil { + vv.config.PluginFilter = v.config.PluginFilter.Clone() + } + return &vv } -func (v *s3mVoice) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, defVol s3mVolume.Volume) { - var s component.Sampler[period.Amiga, s3mVolume.Volume] - s.Setup(component.SamplerSettings[period.Amiga, s3mVolume.Volume]{ +func (v *s3mVoice) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol s3mVolume.FineVolume, defVol s3mVolume.Volume) { + var s component.Sampler[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] + s.Setup(component.SamplerSettings[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume]{ Sample: samp, DefaultVolume: defVol, + MixVolume: mixVol, WholeLoop: wholeLoop, SustainLoop: sustainLoop, }) diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 19c28a6..1685f44 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -24,7 +24,7 @@ type xmVoice[TPeriod Period] struct { component.KeyModulator - voicer component.Voicer[TPeriod, xmVolume.XmVolume] + voicer component.Voicer[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] amp component.AmpModulator[xmVolume.XmVolume, xmVolume.XmVolume] fadeout component.FadeoutModulator freq component.FreqModulator[TPeriod] @@ -103,8 +103,6 @@ func (v *xmVoice[TPeriod]) doFadeout() { if v.voicer != nil { v.voicer.Fadeout() } - - v.fadeout.Fadeout() } func (v *xmVoice[TPeriod]) doDeferredAttack() { @@ -126,8 +124,9 @@ func (v xmVoice[TPeriod]) getFadeoutEnabled() bool { func (v *xmVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) { v.config = config v.fadeout.Setup(component.FadeoutModulatorSettings{ - GetEnabled: v.getFadeoutEnabled, - Amount: config.FadeOut.Amount, + Enabled: v.config.FadeOut.Mode != fadeout.ModeDisabled, + GetActive: v.getFadeoutEnabled, + Amount: config.FadeOut.Amount, }) v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) v.autoVibrato.Setup(config.AutoVibrato) @@ -205,13 +204,18 @@ func (v *xmVoice[TPeriod]) Clone() voice.Voice { config: v.config, fadeoutMode: v.fadeoutMode, amp: v.amp.Clone(), + fadeout: v.fadeout.Clone(), freq: v.freq.Clone(), + autoVibrato: v.autoVibrato.Clone(), pan: v.pan.Clone(), - volEnv: v.volEnv.Clone(), - panEnv: v.panEnv.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, @@ -228,14 +232,19 @@ func (v *xmVoice[TPeriod]) Clone() voice.Voice { vv.config.VoiceFilter = v.config.VoiceFilter.Clone() } + if v.config.PluginFilter != nil { + vv.config.PluginFilter = v.config.PluginFilter.Clone() + } + return &vv } -func (v *xmVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, defVol xmVolume.XmVolume) { - var s component.Sampler[TPeriod, xmVolume.XmVolume] - s.Setup(component.SamplerSettings[TPeriod, xmVolume.XmVolume]{ +func (v *xmVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol, defVol xmVolume.XmVolume) { + var s component.Sampler[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] + s.Setup(component.SamplerSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume]{ Sample: samp, DefaultVolume: defVol, + MixVolume: mixVol, WholeLoop: wholeLoop, SustainLoop: sustainLoop, }) diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index a07fa24..f4f9ce2 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -7,6 +7,7 @@ import ( "github.com/gotracker/playback/note" "github.com/gotracker/playback/system" "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/types" ) func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { @@ -46,7 +47,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo // nothing } - m.addPastNote(pn.(voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning])) + m.addPastNote(ch, pn.(voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning])) } if pitchPanMod, ok := c.cv.(voice.PitchPanModulator[TPanning]); ok { @@ -153,7 +154,14 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe PitchFiltEnv: d.PitchFiltEnv, }) - c.cv.SetPCM(d.Sample, d.Loop, d.SustainLoop, inst.GetDefaultVolume()) + var mixVol TMixingVolume + if mv, set := d.MixingVolume.Get(); set { + mixVol = mv + } else { + mixVol = types.GetMaxVolume[TMixingVolume]() + } + + c.cv.SetPCM(d.Sample, d.Loop, d.SustainLoop, mixVol, inst.GetDefaultVolume()) return nil } diff --git a/player/machine/channel_notedecode.go b/player/machine/channel_notedecode.go index 14501e3..da97835 100644 --- a/player/machine/channel_notedecode.go +++ b/player/machine/channel_notedecode.go @@ -68,8 +68,9 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco } if n != nil { - p := m.ConvertToPeriod(n) - changeNote.Period.Set(p) + if p := m.ConvertToPeriod(n); !p.IsInvalid() { + changeNote.Period.Set(p) + } } if na != note.ActionContinue { @@ -110,9 +111,6 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco switch inst.GetKind() { case instrument.KindPCM: if d, ok := inst.GetData().(*instrument.PCM[TMixingVolume, TVolume, TPanning]); ok { - if mv, set := d.MixingVolume.Get(); set { - changeNote.MixVol.Set(mv) - } if pan, set := d.Panning.Get(); set { changeNote.Pan.Set(pan) } diff --git a/player/machine/channel_tick.go b/player/machine/channel_tick.go index ba91f76..5d235ee 100644 --- a/player/machine/channel_tick.go +++ b/player/machine/channel_tick.go @@ -33,7 +33,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowS } info := c.newNote - c.newNote = NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning]{} + c.newNote.Reset() if tp, set := info.Period.Get(); set { if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 6a39629..7115bba 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -164,9 +164,6 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum if c.osc[OscillatorPanbrello], err = ms.GetPanbrelloFactory(); err != nil { return nil, err } - if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { - freqMod.SetPeriod(m.ms.PeriodConverter.GetPeriod(note.StopNote{})) - } cmv, err := song.GetChannelMixingVolume[TMixingVolume](cs) if err != nil { return nil, fmt.Errorf("channel[%d]: %w", i, err) diff --git a/player/machine/machine_pastnote.go b/player/machine/machine_pastnote.go index afd3b4a..44ea23c 100644 --- a/player/machine/machine_pastnote.go +++ b/player/machine/machine_pastnote.go @@ -12,26 +12,50 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canP return m.songData.GetSystem().GetMaxPastNotesPerChannel() > 0 } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(pn voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(ch index.Channel, pn voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { type pastNoteAges struct { i int age int } + // first pass, try to add it to the channel listed + { + c := &m.channels[ch] + + if c.pn.CanAddPastNote() { + c.pn.AddPastNote(ch, pn, m.age) + return + } + } + ages := make([]pastNoteAges, 0, len(m.outputChannels)) - // first pass, just try to add it + // second pass, try to bump the oldest listed for the channel for i := range m.channels { c := &m.channels[i] - if c.pn.CanAddPastNote() { - c.pn.AddPastNote(pn, m.age) - return + if c.pn.HasPastNoteForChannel(ch) { + ages = append(ages, pastNoteAges{ + i: i, + age: c.pn.GetAge(), + }) } + } + + // optional third pass, if no entries with existing past notes, then find any possible + if len(ages) == 0 { + for i := range m.channels { + c := &m.channels[i] + + if c.pn.CanAddPastNote() { + c.pn.AddPastNote(ch, pn, m.age) + return + } - ages = append(ages, pastNoteAges{ - i: i, - age: c.pn.GetAge(), - }) + ages = append(ages, pastNoteAges{ + i: i, + age: c.pn.GetAge(), + }) + } } if len(ages) == 0 { @@ -40,14 +64,14 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addP return } - // second pass, find the oldest + // find the oldest sort.Slice(ages, func(i, j int) bool { return ages[i].age < ages[j].age }) // jam it in oldest := ages[0].i - m.channels[oldest].pn.AddPastNote(pn, m.age) + m.channels[oldest].pn.AddPastNote(ch, pn, m.age) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPastNoteAction(ch index.Channel, na note.Action) { @@ -55,5 +79,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPa return } - m.channels[ch].pn.DoPastNoteAction(na) + for i := range m.channels { + m.channels[i].pn.DoPastNoteAction(ch, na) + } } diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 9ecf98a..1b06718 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -79,7 +79,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend mixData = append(mixData, *data) } - pnData, err := c.pn.Render(centerAheadPan, details, rc) + pnData, err := c.pn.RenderAndAdvance(centerAheadPan, details, rc) if err != nil { return nil, err } diff --git a/player/machine/newnoteinfo.go b/player/machine/newnoteinfo.go index bf58093..e74c7ab 100644 --- a/player/machine/newnoteinfo.go +++ b/player/machine/newnoteinfo.go @@ -23,6 +23,17 @@ type NewNoteInfo[TPeriod Period, TMixingVolume, TVolume Volume, TPanning Panning 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() || diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go index 6703d43..187ca21 100644 --- a/player/machine/pastnote.go +++ b/player/machine/pastnote.go @@ -3,6 +3,7 @@ package machine import ( "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/voice" @@ -10,6 +11,7 @@ import ( type pastNote[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + ch index.Channel age int } @@ -22,9 +24,10 @@ func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Ca return len(o.pn) < o.MaxPastNotes } -func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) AddPastNote(v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], age int) { +func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) AddPastNote(ch index.Channel, v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], age int) { o.pn = append(o.pn, pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ v: v, + ch: ch, age: age, }) if over := len(o.pn) - o.MaxPastNotes; over > 0 { @@ -55,8 +58,21 @@ func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Up o.pn = updated } -func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoPastNoteAction(na note.Action) { +func (o pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) HasPastNoteForChannel(ch index.Channel) bool { for _, pn := range o.pn { + if pn.ch == ch { + return true + } + } + return false +} + +func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoPastNoteAction(ch index.Channel, na note.Action) { + for _, pn := range o.pn { + if pn.ch != ch { + continue + } + switch na { case note.ActionCut: pn.v.Stop() @@ -77,7 +93,7 @@ func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Do } } -func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Render(centerAheadPan volume.Matrix, details render.Details, rc *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) ([]mixing.Data, error) { +func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RenderAndAdvance(centerAheadPan volume.Matrix, details render.Details, rc *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) ([]mixing.Data, error) { var mixData []mixing.Data for _, pn := range p.pn { @@ -93,6 +109,7 @@ func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Re if err != nil { return nil, err } + pn.v.Advance() if data != nil { mixData = append(mixData, *data) diff --git a/voice/component/envelope.go b/voice/component/envelope.go index d7fd3cc..d942127 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -2,7 +2,6 @@ package component import ( "fmt" - "math" "github.com/gotracker/playback/index" "github.com/gotracker/playback/tracing" @@ -24,10 +23,13 @@ type Envelope interface { type baseEnvelope[TIn, TOut any] struct { settings EnvelopeSettings[TIn, TOut] updater func(TIn, TIn, float64) TOut - enabled bool - pos int - stopped bool - value TOut + unkeyed struct{} + keyed struct { + active bool + pos int + done bool + } + value TOut slimKeyModulator } @@ -43,30 +45,35 @@ func (e *baseEnvelope[TIn, TOut]) Setup(settings EnvelopeSettings[TIn, TOut], up e.Reset() } -func (e baseEnvelope[TIn, TOut]) Clone(update func(TIn, TIn, float64) TOut) baseEnvelope[TIn, TOut] { +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() { - e.enabled = e.settings.Enabled + e.keyed.active = e.settings.Enabled e.stateReset() - e.updateValue() +} + +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) { - e.enabled = enabled + e.keyed.active = enabled } // IsEnabled returns the enabled flag for the envelope func (e baseEnvelope[TIn, TOut]) IsEnabled() bool { - return e.enabled + return e.keyed.active } func (e baseEnvelope[TIn, TOut]) IsDone() bool { - return e.stopped + return e.keyed.done } // GetCurrentValue returns the current cached envelope value @@ -76,6 +83,9 @@ func (e baseEnvelope[TIn, TOut]) GetCurrentValue() TOut { // SetEnvelopePosition sets the current position in the envelope func (e *baseEnvelope[TIn, TOut]) SetEnvelopePosition(pos int) voice.Callback { + prev := e.keyed.active + e.keyed.active = true + e.keyed.done = false e.stateReset() // TODO: this is gross, but currently the most optimal way to find the correct position for i := 0; i < pos; i++ { @@ -83,11 +93,12 @@ func (e *baseEnvelope[TIn, TOut]) SetEnvelopePosition(pos int) voice.Callback { return doneCB } } + e.keyed.active = prev return nil } func (e baseEnvelope[TIn, TOut]) GetEnvelopePosition() int { - return e.pos + return e.keyed.pos } // Advance advances the envelope state 1 tick and calculates the current envelope value @@ -96,96 +107,84 @@ func (e *baseEnvelope[TIn, TOut]) Advance() voice.Callback { if done := e.stateAdvance(e.keyOn); done { doneCB = e.settings.OnFinished } - e.updateValue() return doneCB } func (e baseEnvelope[TIn, TOut]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} pos{%v} stopped{%v} value{%v}", - e.enabled, - e.pos, - e.stopped, + 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]) updateValue() { - if !e.enabled { - return - } - - curVal, nextVal, t := e.getCurrentPoints() - - var y0 TIn - if curVal != nil { - y0 = curVal.Y - } - - var y1 TIn - if nextVal != nil { - y1 = nextVal.Y - } - - e.value = e.updater(y0, y1, t) -} - func (e *baseEnvelope[TIn, TOut]) stateReset() { if !e.settings.Envelope.Enabled { - e.stopped = true + e.keyed.done = true return } - e.pos = 0 - e.stopped = false + e.keyed.pos = 0 + e.keyed.done = false + e.updateValue() } -func (e *baseEnvelope[TIn, TOut]) getCurrentPoints() (*envelope.Point[TIn], *envelope.Point[TIn], float64) { - if e.stopped { - return nil, nil, 0 +func (e *baseEnvelope[TIn, TOut]) updateValue() { + if !e.keyed.active || e.keyed.done { + return } nPoints := len(e.settings.Envelope.Values) if nPoints == 0 { - return nil, nil, 0 + return } - curTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.pos, e.settings.Envelope.Length, e.prevKeyOn) - nextTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.pos+1, e.settings.Envelope.Length, e.keyOn) + 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) - var cur envelope.Point[TIn] - for _, it := range e.settings.Envelope.Values { + curPoint := -1 + for i, it := range e.settings.Envelope.Values { if it.Pos > curTick { + curPoint = i - 1 break } - cur = it + } + var cur envelope.Point[TIn] + if curPoint >= 0 && curPoint < nPoints { + cur = e.settings.Values[curPoint] + } else { + cur = e.settings.Values[nPoints-1] } - var next envelope.Point[TIn] - foundNext := false - for _, it := range e.settings.Envelope.Values { + nextPoint := -1 + for i, it := range e.settings.Envelope.Values { if it.Pos > nextTick { - next = it - foundNext = true + nextPoint = i break } } - if !foundNext { - return &cur, &cur, 0 + if nextPoint < 0 || nextPoint >= nPoints { + e.value = e.updater(cur.Y, cur.Y, 0) + return } + 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) } } - return &cur, &next, t + + e.value = e.updater(cur.Y, next.Y, t) } func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { - if e.stopped { + if e.keyed.done { return false } @@ -202,25 +201,26 @@ func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { nPoints := len(e.settings.Envelope.Values) if nPoints == 0 { - e.stopped = true + e.keyed.done = true return true } - e.pos++ - curTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.pos, e.settings.Envelope.Length, keyOn) + e.keyed.pos++ + curTick, _ := 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 && i.Length != math.MaxInt { + if i.Pos >= curTick { found = true break } } if !found { - e.stopped = true + e.keyed.done = true return true } + e.updateValue() return false } diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 9def967..3d8bd92 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -3,6 +3,7 @@ package component import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/util" + "github.com/gotracker/playback/voice" ) // FilterEnvelope is a filter frequency cutoff modulation envelope @@ -14,9 +15,9 @@ func (e *FilterEnvelope) Setup(settings EnvelopeSettings[filter.PitchFiltValue, e.baseEnvelope.Setup(settings, e.calc) } -func (e FilterEnvelope) Clone() FilterEnvelope { +func (e FilterEnvelope) Clone(onFinished voice.Callback) FilterEnvelope { var m FilterEnvelope - m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) return m } diff --git a/voice/component/envelope_pan.go b/voice/component/envelope_pan.go index 17761af..ff2d8c5 100755 --- a/voice/component/envelope_pan.go +++ b/voice/component/envelope_pan.go @@ -2,6 +2,7 @@ package component import ( "github.com/gotracker/playback/util" + "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/types" ) @@ -14,9 +15,9 @@ func (e *PanEnvelope[TPanning]) Setup(settings EnvelopeSettings[TPanning, TPanni e.baseEnvelope.Setup(settings, e.calc) } -func (e PanEnvelope[TPanning]) Clone() PanEnvelope[TPanning] { +func (e PanEnvelope[TPanning]) Clone(onFinished voice.Callback) PanEnvelope[TPanning] { var m PanEnvelope[TPanning] - m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) return m } diff --git a/voice/component/envelope_pitch.go b/voice/component/envelope_pitch.go index 3f1787a..724a1ff 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -4,6 +4,7 @@ import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" "github.com/gotracker/playback/util" + "github.com/gotracker/playback/voice" ) // PitchEnvelope is an frequency modulation envelope @@ -15,9 +16,9 @@ func (e *PitchEnvelope) Setup(settings EnvelopeSettings[filter.PitchFiltValue, p e.baseEnvelope.Setup(settings, e.calc) } -func (e PitchEnvelope) Clone() PitchEnvelope { +func (e PitchEnvelope) Clone(onFinished voice.Callback) PitchEnvelope { var m PitchEnvelope - m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) return m } diff --git a/voice/component/envelope_volume.go b/voice/component/envelope_volume.go index b54040e..fcb3859 100755 --- a/voice/component/envelope_volume.go +++ b/voice/component/envelope_volume.go @@ -2,6 +2,7 @@ package component import ( "github.com/gotracker/playback/util" + "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/types" ) @@ -14,9 +15,9 @@ func (e *VolumeEnvelope[TVolume]) Setup(settings EnvelopeSettings[TVolume, TVolu e.baseEnvelope.Setup(settings, e.calc) } -func (e VolumeEnvelope[TVolume]) Clone() VolumeEnvelope[TVolume] { +func (e VolumeEnvelope[TVolume]) Clone(onFinished voice.Callback) VolumeEnvelope[TVolume] { var m VolumeEnvelope[TVolume] - m.baseEnvelope = e.baseEnvelope.Clone(m.calc) + m.baseEnvelope = e.baseEnvelope.Clone(m.calc, onFinished) return m } diff --git a/voice/component/modulator_amp.go b/voice/component/modulator_amp.go index 9b41f16..67225ea 100755 --- a/voice/component/modulator_amp.go +++ b/voice/component/modulator_amp.go @@ -12,11 +12,16 @@ import ( // AmpModulator is an amplitude (volume) modulator type AmpModulator[TMixingVolume, TVolume types.Volume] struct { settings AmpModulatorSettings[TMixingVolume, TVolume] - active bool - vol TVolume - delta types.VolumeDelta - mixing TMixingVolume - final volume.Volume // = active? * mixing * vol + + unkeyed struct { + active bool + vol TVolume + mixing TMixingVolume + } + keyed struct { + delta types.VolumeDelta + } + final volume.Volume // = active? * mixing * vol } type AmpModulatorSettings[TMixingVolume, TVolume types.Volume] struct { @@ -27,11 +32,10 @@ type AmpModulatorSettings[TMixingVolume, TVolume types.Volume] struct { func (a *AmpModulator[TMixingVolume, TVolume]) Setup(settings AmpModulatorSettings[TMixingVolume, TVolume]) { a.settings = settings - a.active = settings.Active - a.vol = settings.DefaultVolume - a.delta = 0 - a.mixing = settings.DefaultMixingVolume - a.updateFinal() + a.unkeyed.active = settings.Active + a.unkeyed.vol = settings.DefaultVolume + a.unkeyed.mixing = settings.DefaultMixingVolume + a.Reset() } func (a AmpModulator[TMixingVolume, TVolume]) Clone() AmpModulator[TMixingVolume, TVolume] { @@ -39,26 +43,31 @@ func (a AmpModulator[TMixingVolume, TVolume]) Clone() AmpModulator[TMixingVolume return m } +func (a *AmpModulator[TMixingVolume, TVolume]) Reset() { + a.keyed.delta = 0 + a.updateFinal() +} + func (a *AmpModulator[TMixingVolume, TVolume]) SetActive(active bool) { - a.active = active + a.unkeyed.active = active a.updateFinal() } func (a AmpModulator[TMixingVolume, TVolume]) IsActive() bool { - return a.active + return a.unkeyed.active } // SetMixingVolume configures the mixing volume of the modulator func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolume(mixing TMixingVolume) { if !mixing.IsUseInstrumentVol() { - a.mixing = mixing + a.unkeyed.mixing = mixing a.updateFinal() } } // GetMixingVolume returns the current mixing volume of the modulator func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolume() TMixingVolume { - return a.mixing + return a.unkeyed.mixing } // SetVolume sets the current volume (before fadeout calculation) @@ -66,22 +75,22 @@ func (a *AmpModulator[TMixingVolume, TVolume]) SetVolume(vol TVolume) { if vol.IsUseInstrumentVol() { vol = a.settings.DefaultVolume } - a.vol = vol + a.unkeyed.vol = vol a.updateFinal() } // GetVolume returns the current volume (before fadeout calculation) func (a AmpModulator[TMixingVolume, TVolume]) GetVolume() TVolume { - return a.vol + return a.unkeyed.vol } func (a *AmpModulator[TMixingVolume, TVolume]) SetVolumeDelta(d types.VolumeDelta) { - a.delta = d + a.keyed.delta = d a.updateFinal() } func (a AmpModulator[TMixingVolume, TVolume]) GetVolumeDelta() types.VolumeDelta { - return a.delta + return a.keyed.delta } // GetFinalVolume returns the current volume (after fadeout calculation) @@ -90,20 +99,21 @@ func (a AmpModulator[TMixingVolume, TVolume]) GetFinalVolume() volume.Volume { } func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() { - if !a.active { + if !a.unkeyed.active { a.final = 0 return } - v := types.AddVolumeDelta(a.vol, a.delta) - a.final = a.mixing.ToVolume() * v.ToVolume() + v := types.AddVolumeDelta(a.unkeyed.vol, a.keyed.delta) + a.final = a.unkeyed.mixing.ToVolume() * v.ToVolume() } func (a AmpModulator[TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} vol{%v} mixing{%v} final{%v}", - a.active, - a.vol, - a.mixing, + t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} vol{%v} mixing{%v} delta{%v} final{%v}", + a.unkeyed.active, + a.unkeyed.vol, + a.unkeyed.mixing, + a.keyed.delta, a.final, ), comment) } diff --git a/voice/component/modulator_autovibrato.go b/voice/component/modulator_autovibrato.go index b78bce7..71d57de 100644 --- a/voice/component/modulator_autovibrato.go +++ b/voice/component/modulator_autovibrato.go @@ -12,14 +12,19 @@ import ( // AutoVibratoModulator is a frequency (pitch) modulator type AutoVibratoModulator[TPeriod period.Period] struct { - settings autovibrato.AutoVibratoSettings - enabled bool + settings autovibrato.AutoVibratoSettings + unkeyed struct { + enabled bool + } + keyed struct { + age int // current age of oscillator (in ticks) + } autoVibrato oscillator.Oscillator - age int // current age of oscillator (in ticks) } func (f *AutoVibratoModulator[TPeriod]) Setup(settings autovibrato.AutoVibratoSettings) { f.settings = settings + f.unkeyed.enabled = f.settings.Enabled f.Reset() } @@ -32,14 +37,14 @@ func (f AutoVibratoModulator[TPeriod]) Clone() AutoVibratoModulator[TPeriod] { } func (f *AutoVibratoModulator[TPeriod]) Reset() { - f.enabled = f.settings.Enabled + f.keyed.age = 0 f.autoVibrato = f.settings.Generate() f.ResetAutoVibrato() } // SetEnabled sets the status of the AutoVibrato enablement flag func (f *AutoVibratoModulator[TPeriod]) SetEnabled(enabled bool) { - f.enabled = enabled + f.unkeyed.enabled = enabled } // ConfigureAutoVibrato sets the AutoVibrato oscillator settings @@ -53,23 +58,23 @@ func (f *AutoVibratoModulator[TPeriod]) ResetAutoVibrato() { f.autoVibrato.HardReset() } - f.age = 0 + f.keyed.age = 0 } // IsAutoVibratoEnabled returns the status of the AutoVibrato enablement flag func (f *AutoVibratoModulator[TPeriod]) IsAutoVibratoEnabled() bool { - return f.enabled + return f.unkeyed.enabled } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) TPeriod { - if !f.enabled { + if !f.unkeyed.enabled { return in } depth := f.settings.Depth - if f.settings.Sweep > f.age { - depth *= float32(f.age) / float32(f.settings.Sweep) + 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) @@ -78,17 +83,17 @@ func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) TPeriod { // Advance advances the autoVibrato value by 1 tick func (f *AutoVibratoModulator[TPeriod]) Advance() { - if !f.enabled { + if !f.unkeyed.enabled { return } f.autoVibrato.Advance(f.settings.Rate) - f.age++ + 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.enabled, - f.age, + f.unkeyed.enabled, + f.keyed.age, ), comment) } diff --git a/voice/component/modulator_fadeout.go b/voice/component/modulator_fadeout.go index aa9180c..a188732 100644 --- a/voice/component/modulator_fadeout.go +++ b/voice/component/modulator_fadeout.go @@ -11,17 +11,23 @@ import ( // FadeoutModulator is an amplitude (volume) modulator type FadeoutModulator struct { settings FadeoutModulatorSettings - active bool - vol volume.Volume + unkeyed struct { + enabled bool + } + keyed struct { + vol volume.Volume + } } type FadeoutModulatorSettings struct { - GetEnabled func() bool - Amount volume.Volume + Enabled bool + GetActive func() bool + Amount volume.Volume } func (a *FadeoutModulator) Setup(settings FadeoutModulatorSettings) { a.settings = settings + a.unkeyed.enabled = settings.Enabled a.Reset() } @@ -32,51 +38,45 @@ func (a FadeoutModulator) Clone() FadeoutModulator { // Reset disables the fadeout and resets its volume func (a *FadeoutModulator) Reset() { - a.active = false - a.vol = volume.Volume(1) -} - -// Fadeout activates the fadeout -func (a *FadeoutModulator) Fadeout() { - a.active = a.settings.Amount != 0 + a.keyed.vol = volume.Volume(1) } -// SetActive sets the status of the fadeout active flag -func (a *FadeoutModulator) SetActive(active bool) { - a.active = active +// SetEnabled sets the status of the fadeout enable flag +func (a *FadeoutModulator) SetEnabled(enabled bool) { + a.unkeyed.enabled = enabled } // IsEnabled returns the status of the fadeout enablement flag func (a FadeoutModulator) IsActive() bool { - if a.settings.GetEnabled == nil { + if !a.unkeyed.enabled || a.settings.GetActive == nil { return false } - return a.settings.GetEnabled() && a.active + return a.settings.GetActive() } // GetVolume returns the value of the fadeout volume func (a FadeoutModulator) GetVolume() volume.Volume { - return a.vol + return a.keyed.vol } func (a FadeoutModulator) GetFinalVolume() volume.Volume { if !a.IsActive() { return volume.Volume(1) } - return a.vol + return a.keyed.vol } // Advance advances the fadeout value by 1 tick func (a *FadeoutModulator) Advance() { - if a.IsActive() && a.vol > 0 { - a.vol = min(max(a.vol-a.settings.Amount, 0), 1) + 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("active{%v} vol{%v}", - a.active, - a.vol, + 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 9bf5761..7c1d8d0 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -11,8 +11,13 @@ import ( // FreqModulator is a frequency (pitch) modulator type FreqModulator[TPeriod period.Period] struct { settings FreqModulatorSettings[TPeriod] - period TPeriod - delta period.Delta + unkeyed struct { + period TPeriod + } + keyed struct { + delta period.Delta + } + final TPeriod } type FreqModulatorSettings[TPeriod period.Period] struct { @@ -20,6 +25,14 @@ type FreqModulatorSettings[TPeriod period.Period] struct { func (f *FreqModulator[TPeriod]) Setup(settings FreqModulatorSettings[TPeriod]) { f.settings = settings + var empty TPeriod + f.unkeyed.period = empty + f.Reset() +} + +func (f *FreqModulator[TPeriod]) Reset() { + f.keyed.delta = 0 + f.updateFinal() } func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { @@ -29,32 +42,39 @@ func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { // SetPeriod sets the current period (before AutoVibrato and Delta calculation) func (f *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { - f.period = period + f.unkeyed.period = period + f.updateFinal() } // GetPeriod returns the current period (before AutoVibrato and Delta calculation) func (f *FreqModulator[TPeriod]) GetPeriod() TPeriod { - return f.period + return f.unkeyed.period } // SetPeriodDelta sets the current period delta (before AutoVibrato calculation) func (f *FreqModulator[TPeriod]) SetPeriodDelta(delta period.Delta) { - f.delta = delta + f.keyed.delta = delta + f.updateFinal() } // GetDelta returns the current period delta (before AutoVibrato calculation) func (f *FreqModulator[TPeriod]) GetPeriodDelta() period.Delta { - return f.delta + return f.keyed.delta } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) func (f *FreqModulator[TPeriod]) GetFinalPeriod() TPeriod { - return period.AddDelta(f.period, f.delta) + return f.final } func (f FreqModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("period{%v} delta{%v}", - f.period, - f.delta, + t.TraceChannelWithComment(ch, fmt.Sprintf("period{%v} delta{%v} final{%v}", + f.unkeyed.period, + f.keyed.delta, + f.final, ), comment) } + +func (f *FreqModulator[TPeriod]) updateFinal() { + f.final = period.AddDelta(f.unkeyed.period, f.keyed.delta) +} diff --git a/voice/component/modulator_key.go b/voice/component/modulator_key.go index 1643de0..f8b8345 100644 --- a/voice/component/modulator_key.go +++ b/voice/component/modulator_key.go @@ -45,7 +45,7 @@ func (k *KeyModulator) Attack() { k.slimKeyModulator.Attack() k.fadeout = false - if k.settings.Attack != nil && k.keyOn != k.prevKeyOn { + if k.settings.Attack != nil { k.settings.Attack() } } @@ -53,7 +53,7 @@ func (k *KeyModulator) Attack() { func (k *KeyModulator) Release() { k.slimKeyModulator.Release() - if k.settings.Release != nil && k.keyOn != k.prevKeyOn { + if k.settings.Release != nil { k.settings.Release() } } @@ -85,3 +85,14 @@ func (k KeyModulator) DumpState(ch index.Channel, t tracing.Tracer, comment stri 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 c796cef..30fcef7 100755 --- a/voice/component/modulator_pan.go +++ b/voice/component/modulator_pan.go @@ -11,9 +11,13 @@ import ( // PanModulator is an pan (spatial) modulator type PanModulator[TPanning types.Panning] struct { settings PanModulatorSettings[TPanning] - pan TPanning - delta types.PanDelta - final TPanning + unkeyed struct { + pan TPanning + } + keyed struct { + delta types.PanDelta + } + final TPanning } type PanModulatorSettings[TPanning types.Panning] struct { @@ -23,8 +27,12 @@ type PanModulatorSettings[TPanning types.Panning] struct { func (p *PanModulator[TPanning]) Setup(settings PanModulatorSettings[TPanning]) { p.settings = settings - p.pan = settings.InitialPan - p.delta = 0 + p.unkeyed.pan = settings.InitialPan + p.Reset() +} + +func (p *PanModulator[TPanning]) Reset() { + p.keyed.delta = 0 p.updateFinal() } @@ -36,27 +44,27 @@ func (p PanModulator[TPanning]) Clone() PanModulator[TPanning] { // SetPan sets the current panning func (p *PanModulator[TPanning]) SetPan(pan TPanning) { if p.settings.Enabled { - p.pan = pan + p.unkeyed.pan = pan p.updateFinal() } } // GetPan returns the current panning func (p PanModulator[TPanning]) GetPan() TPanning { - return p.pan + return p.unkeyed.pan } // SetPanDelta sets the current panning delta func (p *PanModulator[TPanning]) SetPanDelta(d types.PanDelta) { if p.settings.Enabled { - p.delta = d + p.keyed.delta = d p.updateFinal() } } // GetPanDelta returns the current panning delta func (p PanModulator[TPanning]) GetPanDelta() types.PanDelta { - return p.delta + return p.keyed.delta } // GetFinalPan returns the current panning @@ -66,11 +74,11 @@ func (p PanModulator[TPanning]) GetFinalPan() TPanning { func (p PanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { t.TraceChannelWithComment(ch, fmt.Sprintf("pan{%v} delta{%v}", - p.pan, - p.delta, + p.unkeyed.pan, + p.keyed.delta, ), comment) } func (p *PanModulator[TPanning]) updateFinal() { - p.final = types.AddPanningDelta(p.pan, p.delta) + p.final = types.AddPanningDelta(p.unkeyed.pan, p.keyed.delta) } diff --git a/voice/component/modulator_pitchpan.go b/voice/component/modulator_pitchpan.go index 751d5ee..021dd82 100644 --- a/voice/component/modulator_pitchpan.go +++ b/voice/component/modulator_pitchpan.go @@ -11,10 +11,13 @@ import ( // PitchPanModulator is an pan (spatial) modulator type PitchPanModulator[TPanning types.Panning] struct { - settings PitchPanModulatorSettings[TPanning] - pitchPanEnabled bool - pitch note.Semitone - panSep float32 + settings PitchPanModulatorSettings[TPanning] + unkeyed struct { + enabled bool + pitch note.Semitone + } + keyed struct{} + panSep float32 } type PitchPanModulatorSettings[TPanning types.Panning] struct { @@ -25,8 +28,8 @@ type PitchPanModulatorSettings[TPanning types.Panning] struct { func (p *PitchPanModulator[TPanning]) Setup(settings PitchPanModulatorSettings[TPanning]) { p.settings = settings - p.pitchPanEnabled = settings.PitchPanEnable - p.pitch = settings.PitchPanCenter + p.unkeyed.enabled = settings.PitchPanEnable + p.unkeyed.pitch = settings.PitchPanCenter p.Reset() } @@ -41,18 +44,18 @@ func (p *PitchPanModulator[TPanning]) Reset() { // SetPitch updates the pan separation modulated by the provided pitch func (p *PitchPanModulator[TPanning]) SetPitch(st note.Semitone) { - p.pitch = st + p.unkeyed.pitch = st p.updatePitchPan() } // IsPitchPanEnabled returns the enablement of the pitch-pan separation function func (p PitchPanModulator[TPanning]) IsPitchPanEnabled() bool { - return p.pitchPanEnabled + return p.unkeyed.enabled } // EnablePitchPan enables the pitch-pan separation function func (p *PitchPanModulator[TPanning]) EnablePitchPan(enabled bool) { - p.pitchPanEnabled = enabled + p.unkeyed.enabled = enabled p.updatePitchPan() } @@ -62,7 +65,7 @@ func (p PitchPanModulator[TPanning]) GetPanSeparation() float32 { } func (p PitchPanModulator[TPanning]) GetSeparatedPan(pan TPanning) TPanning { - if !p.pitchPanEnabled || p.panSep == 0 { + if !p.unkeyed.enabled || p.panSep == 0 { return pan } @@ -76,17 +79,17 @@ func (p *PitchPanModulator[TPanning]) Advance() { } func (p *PitchPanModulator[TPanning]) updatePitchPan() { - if !p.pitchPanEnabled { + if !p.unkeyed.enabled { return } - p.panSep = (float32(p.pitch) - float32(p.settings.PitchPanCenter)) * p.settings.PitchPanSeparation + p.panSep = (float32(p.unkeyed.pitch) - float32(p.settings.PitchPanCenter)) * p.settings.PitchPanSeparation } func (p PitchPanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("pitchPanEnabled{%v} pitch{%v} panSep{%v}", - p.pitchPanEnabled, - p.pitch, + 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 bfbe784..15145c2 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -28,7 +28,7 @@ type OPL2Registers struct { } // OPL2 is an OPL2 component -type OPL2[TPeriod types.Period, TVolume types.Volume] struct { +type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { chip render.OPL2Chip channel int reg OPL2Registers @@ -39,7 +39,7 @@ type OPL2[TPeriod types.Period, TVolume types.Volume] struct { } // Setup sets up the OPL2 component -func (o *OPL2[TPeriod, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency, defaultVolume TVolume) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency, defaultVolume TVolume) { o.chip = chip o.channel = channel o.reg = reg @@ -48,20 +48,20 @@ func (o *OPL2[TPeriod, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OP o.keyOn = false } -func (o *OPL2[TPeriod, TVolume]) Attack() { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Attack() { // does nothing } -func (o *OPL2[TPeriod, TVolume]) Release() { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Release() { // does nothing } -func (o *OPL2[TPeriod, TVolume]) Fadeout() { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Fadeout() { // does nothing } // DeferredAttack activates the key-on bit -func (o *OPL2[TPeriod, TVolume]) DeferredAttack() { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) DeferredAttack() { o.keyOn = true // calculate the register addressing information index := uint32(o.channel) @@ -86,7 +86,7 @@ func (o *OPL2[TPeriod, TVolume]) DeferredAttack() { } // DeferredRelease deactivates the key-on bit -func (o *OPL2[TPeriod, TVolume]) DeferredRelease() { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) DeferredRelease() { o.keyOn = false // calculate the register addressing information @@ -98,7 +98,7 @@ func (o *OPL2[TPeriod, TVolume]) DeferredRelease() { } // Advance advances the playback -func (o *OPL2[TPeriod, TVolume]) Advance(carVol volume.Volume, period TPeriod) { +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) @@ -128,18 +128,18 @@ func (o *OPL2[TPeriod, TVolume]) Advance(carVol volume.Volume, period TPeriod) { ch.WriteReg(0xB0|index, regB0) } -func (o OPL2[TPeriod, TVolume]) Clone() Voicer[TPeriod, TVolume] { - var out OPL2[TPeriod, TVolume] +func (o OPL2[TPeriod, TMixingVolume, TVolume]) Clone() Voicer[TPeriod, TMixingVolume, TVolume] { + var out OPL2[TPeriod, TMixingVolume, TVolume] out.Setup(o.chip, o.channel, o.reg, o.baseFreq, o.defaultVolume) out.periodConverter = o.periodConverter return &out } -func (o OPL2[TPeriod, TVolume]) GetDefaultVolume() TVolume { +func (o OPL2[TPeriod, TMixingVolume, TVolume]) GetDefaultVolume() TVolume { return o.defaultVolume } -func (o OPL2[TPeriod, TVolume]) GetNumChannels() int { +func (o OPL2[TPeriod, TMixingVolume, TVolume]) GetNumChannels() int { return 1 } @@ -149,11 +149,11 @@ var twoOperatorMelodic = [...]uint32{ 0x100, 0x101, 0x102, 0x108, 0x109, 0x10A, 0x110, 0x111, 0x112, } -func (o *OPL2[TPeriod, TVolume]) getChannelIndex(channelIdx int) uint32 { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) getChannelIndex(channelIdx int) uint32 { return twoOperatorMelodic[channelIdx%18] } -func (o *OPL2[TPeriod, TVolume]) 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 { @@ -166,21 +166,21 @@ func (o *OPL2[TPeriod, TVolume]) calc40(reg40 uint8, vol volume.Volume) uint8 { return result } -func (o *OPL2[TPeriod, TVolume]) periodToFreqBlock(p TPeriod, baseFreq period.Frequency) (uint16, uint8) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) periodToFreqBlock(p TPeriod, baseFreq period.Frequency) (uint16, uint8) { modFreq := o.periodConverter.GetFrequency(p) freq := float64(baseFreq) * float64(modFreq) / 261625 return o.freqToFnumBlock(freq) } -func (o *OPL2[TPeriod, TVolume]) 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[TPeriod, TVolume]) freqToFnumBlock(freq float64) (uint16, uint8) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) freqToFnumBlock(freq float64) (uint16, uint8) { if freq > 6208.431 { return 0, 0 } @@ -208,6 +208,6 @@ func (o *OPL2[TPeriod, TVolume]) freqToFnumBlock(freq float64) (uint16, uint8) { return fnum, block } -func (o OPL2[TPeriod, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { +func (o OPL2[TPeriod, TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { // TODO } diff --git a/voice/component/sampler.go b/voice/component/sampler.go index 7d9a926..d4f360b 100644 --- a/voice/component/sampler.go +++ b/voice/component/sampler.go @@ -14,70 +14,84 @@ import ( ) // Sampler is a sampler component -type Sampler[TPeriod types.Period, TVolume types.Volume] struct { - settings SamplerSettings[TPeriod, TVolume] - pos sampling.Pos - loopsEnabled bool +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, TVolume types.Volume] struct { +type SamplerSettings[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { Sample pcm.Sample DefaultVolume TVolume + MixVolume TMixingVolume WholeLoop loop.Loop SustainLoop loop.Loop } -func (s Sampler[TPeriod, TVolume]) Clone() Voicer[TPeriod, TVolume] { - m := s - return &m -} - // Setup sets up the sampler -func (s *Sampler[TPeriod, TVolume]) Setup(settings SamplerSettings[TPeriod, TVolume]) { +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[TPeriod, TVolume]) 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[TPeriod, TVolume]) 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[TPeriod, TVolume]) Attack() { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Attack() { s.slimKeyModulator.Attack() - s.loopsEnabled = true + s.keyed.loopsEnabled = true } // Release releases the key-on value (for loop processing) -func (s *Sampler[TPeriod, TVolume]) Release() { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Release() { s.slimKeyModulator.Release() } // Fadeout disables the loops (for loop processing) -func (s *Sampler[TPeriod, TVolume]) Fadeout() { - s.loopsEnabled = false +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) Fadeout() { + s.keyed.loopsEnabled = false } -func (s *Sampler[TPeriod, TVolume]) DeferredAttack() { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) DeferredAttack() { // does nothing } -func (s *Sampler[TPeriod, TVolume]) DeferredRelease() { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) DeferredRelease() { // does nothing } -func (s Sampler[TPeriod, TVolume]) GetDefaultVolume() TVolume { +func (s Sampler[TPeriod, TMixingVolume, TVolume]) GetDefaultVolume() TVolume { return s.settings.DefaultVolume } -func (s Sampler[TPeriod, TVolume]) GetNumChannels() int { +func (s Sampler[TPeriod, TMixingVolume, TVolume]) GetNumChannels() int { if s.settings.Sample == nil { return 0 } @@ -85,7 +99,7 @@ func (s Sampler[TPeriod, TVolume]) GetNumChannels() int { } // GetSample returns a multi-channel sample at the specified position -func (s *Sampler[TPeriod, TVolume]) 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() { @@ -96,21 +110,22 @@ func (s *Sampler[TPeriod, TVolume]) 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[TPeriod, TVolume]) canLoop() bool { - if s.loopsEnabled { +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[TPeriod, TVolume]) getConvertedSample(pos int) volume.Matrix { +func (s *Sampler[TPeriod, TMixingVolume, TVolume]) getConvertedSample(pos int) volume.Matrix { if s.settings.Sample == nil { return volume.Matrix{} } @@ -146,9 +161,9 @@ func (s *Sampler[TPeriod, TVolume]) getConvertedSample(pos int) volume.Matrix { return data.Apply(atten) } -func (s Sampler[TPeriod, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { +func (s Sampler[TPeriod, TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { t.TraceChannelWithComment(ch, fmt.Sprintf("pos{%v} loopsEnabled{%v}", - s.pos, - s.loopsEnabled, + s.unkeyed.pos, + s.keyed.loopsEnabled, ), comment) } diff --git a/voice/component/voicer.go b/voice/component/voicer.go index df9df7d..90bc287 100644 --- a/voice/component/voicer.go +++ b/voice/component/voicer.go @@ -6,8 +6,8 @@ import ( "github.com/gotracker/playback/voice/types" ) -type Voicer[TPeriod types.Period, TVolume types.Volume] interface { - Clone() Voicer[TPeriod, TVolume] +type Voicer[TPeriod types.Period, TMixingVolume, TVolume types.Volume] interface { + Clone() Voicer[TPeriod, TMixingVolume, TVolume] GetDefaultVolume() TVolume GetNumChannels() int Attack() diff --git a/voice/component/vol0optimization.go b/voice/component/vol0optimization.go index ba2776b..393f6f4 100644 --- a/voice/component/vol0optimization.go +++ b/voice/component/vol0optimization.go @@ -12,49 +12,50 @@ import ( type Vol0Optimization struct { settings vol0optimization.Vol0OptimizationSettings - enabled bool - ticksAt0 int + unkeyed struct { + enabled bool + } + keyed struct { + ticksAt0 int + } } func (c Vol0Optimization) Clone() Vol0Optimization { - return Vol0Optimization{ - settings: c.settings, - enabled: c.enabled, - ticksAt0: 0, - } + m := c + return m } func (c *Vol0Optimization) Setup(settings vol0optimization.Vol0OptimizationSettings) { c.settings = settings - c.enabled = settings.Enabled + c.unkeyed.enabled = settings.Enabled c.Reset() } func (c *Vol0Optimization) SetEnabled(enabled bool) { - c.enabled = enabled + c.unkeyed.enabled = enabled } func (c *Vol0Optimization) Reset() { - c.ticksAt0 = 0 + c.keyed.ticksAt0 = 0 } func (c *Vol0Optimization) ObserveVolume(v volume.Volume) { - if c.enabled { + if c.unkeyed.enabled { if v == 0 { - c.ticksAt0++ + c.keyed.ticksAt0++ } else { - c.ticksAt0 = 0 + c.keyed.ticksAt0 = 0 } } } func (c Vol0Optimization) IsDone() bool { - return c.enabled && c.ticksAt0 >= c.settings.MaxTicksAt0 + return c.unkeyed.enabled && c.keyed.ticksAt0 >= c.settings.MaxTicksAt0 } func (c Vol0Optimization) DumpState(ch index.Channel, t tracing.Tracer, comment string) { t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} ticksAt0{%v}", - c.enabled, - c.ticksAt0, + c.unkeyed.enabled, + c.keyed.ticksAt0, ), comment) } diff --git a/voice/voice.go b/voice/voice.go index e181ec8..8ee16fd 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -39,7 +39,7 @@ type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, T // Configuration Setup(config InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) - SetPCM(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop, defaultVolume TVolume) + SetPCM(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop, mixVolume TMixingVolume, defaultVolume TVolume) // Render Render(panningMatrix volume.Matrix, details render.Details, renderChannel *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) (*mixing.Data, error) From 93083b83ac622540351e5898cecc404625895639 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 13 Jan 2024 23:44:28 -0800 Subject: [PATCH 35/63] fix for past notes being overlimited --- player/machine/machine.go | 3 +- player/machine/machine_factory.go | 1 - player/machine/machine_pastnote.go | 55 +++++++++++++----------------- player/machine/pastnote.go | 20 +++-------- 4 files changed, 31 insertions(+), 48 deletions(-) diff --git a/player/machine/machine.go b/player/machine/machine.go index 32f83dd..5ea8779 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -125,7 +125,8 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { globals[TGlobalVolume] singleRow - channels []channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + channels []channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + pastNotes []index.Channel ticker ticker age int diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 7115bba..1dccf72 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -143,7 +143,6 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum c := &m.channels[ch] c.enabled = cs.GetEnabled() - c.pn.MaxPastNotes = sys.GetMaxPastNotesPerChannel() c.cv = m.ms.VoiceFactory.NewVoice(voice.VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ InitialVolume: initialVolume, InitialMixing: initialMixing, diff --git a/player/machine/machine_pastnote.go b/player/machine/machine_pastnote.go index 44ea23c..078830f 100644 --- a/player/machine/machine_pastnote.go +++ b/player/machine/machine_pastnote.go @@ -14,46 +14,29 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canP func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(ch index.Channel, pn voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { type pastNoteAges struct { - i int - age int + holder int + entry pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] } // first pass, try to add it to the channel listed - { + maxNotes := len(m.channels) * m.songData.GetSystem().GetMaxPastNotesPerChannel() + if len(m.pastNotes) < maxNotes { c := &m.channels[ch] - if c.pn.CanAddPastNote() { - c.pn.AddPastNote(ch, pn, m.age) - return - } + m.pastNotes = append(m.pastNotes, ch) + c.pn.AddPastNote(ch, pn, m.age) + return } ages := make([]pastNoteAges, 0, len(m.outputChannels)) - // second pass, try to bump the oldest listed for the channel + // second pass, try to bump the oldest for i := range m.channels { c := &m.channels[i] - if c.pn.HasPastNoteForChannel(ch) { - ages = append(ages, pastNoteAges{ - i: i, - age: c.pn.GetAge(), - }) - } - } - - // optional third pass, if no entries with existing past notes, then find any possible - if len(ages) == 0 { - for i := range m.channels { - c := &m.channels[i] - - if c.pn.CanAddPastNote() { - c.pn.AddPastNote(ch, pn, m.age) - return - } - + if oldest := c.pn.GetOldest(); oldest != nil { ages = append(ages, pastNoteAges{ - i: i, - age: c.pn.GetAge(), + holder: i, + entry: *oldest, }) } } @@ -66,12 +49,22 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addP // find the oldest sort.Slice(ages, func(i, j int) bool { - return ages[i].age < ages[j].age + return ages[i].entry.age < ages[j].entry.age }) // jam it in - oldest := ages[0].i - m.channels[oldest].pn.AddPastNote(ch, pn, m.age) + oldestHolder := ages[0].holder + m.channels[oldestHolder].pn.AddPastNote(ch, pn, m.age) + m.pastNotes = append(m.pastNotes, ch) + + if over := len(m.pastNotes) - maxNotes; over > 0 { + for _, n := range m.pastNotes[0:over] { + if oldest := m.channels[n].pn.GetOldest(); oldest != nil { + oldest.v.Stop() + } + } + m.pastNotes = m.pastNotes[over:] + } } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPastNoteAction(ch index.Channel, na note.Action) { diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go index 187ca21..6fd3fd8 100644 --- a/player/machine/pastnote.go +++ b/player/machine/pastnote.go @@ -16,12 +16,7 @@ type pastNote[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPan } type pastNotes[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { - MaxPastNotes int - pn []pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] -} - -func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) CanAddPastNote() bool { - return len(o.pn) < o.MaxPastNotes + pn []pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] } func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) AddPastNote(ch index.Channel, v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], age int) { @@ -30,20 +25,15 @@ func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Ad ch: ch, age: age, }) - if over := len(o.pn) - o.MaxPastNotes; over > 0 { - for _, n := range o.pn[0:over] { - n.v.Stop() - } - o.pn = o.pn[over:] - } } -func (o pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetAge() int { +func (o pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetOldest() *pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] { if len(o.pn) == 0 { - return 0 + return nil } - return o.pn[0].age + oldest := o.pn[0] + return &oldest } func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) UpdatePastNotes() { From 7070a705b16bdd20e6269ae2f945e1e2a35f35c5 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 14 Jan 2024 12:30:32 -0800 Subject: [PATCH 36/63] past note cleanup, voice cleanup --- channel.go | 57 ---------- effect.go | 120 ---------------------- filter/amigafilter.go | 5 + filter/echofilter.go | 6 ++ format/it/filter/resonantfilter.go | 33 ++++-- format/it/playback/playback.go | 10 -- format/it/settings/voicefactory.go | 2 +- format/it/voice/modulator_pan.go | 3 +- format/it/voice/render.go | 52 ---------- format/it/voice/sampler.go | 27 +---- format/it/voice/voice.go | 15 +-- format/s3m/channel/effect_finevibrato.go | 3 +- format/s3m/channel/effect_portadown.go | 3 +- format/s3m/channel/effect_portatonote.go | 3 +- format/s3m/channel/effect_portaup.go | 3 +- format/s3m/channel/effect_vibrato.go | 3 +- format/s3m/settings/voicefactory.go | 2 +- format/s3m/voice/render.go | 52 ---------- format/s3m/voice/sampler.go | 34 ++---- format/s3m/voice/voice.go | 19 ++-- format/xm/settings/voicefactory.go | 2 +- format/xm/voice/modulator_pan.go | 5 +- format/xm/voice/render.go | 52 ---------- format/xm/voice/sampler.go | 34 ++---- format/xm/voice/voice.go | 13 +-- player/machine/channel.go | 5 +- player/machine/channel_noteaction.go | 54 +++++----- player/machine/channel_pastnote.go | 76 ++++++++++++++ player/machine/channel_tick.go | 8 ++ player/machine/machine.go | 9 +- player/machine/machine_channel.go | 10 +- player/machine/machine_factory.go | 17 +-- player/machine/machine_pastnote.go | 78 -------------- player/machine/machine_render.go | 56 +++++----- player/machine/machine_tick.go | 2 +- player/machine/pastnote.go | 104 ++----------------- player/machine/ticker.go | 3 + player/render/channel.go | 67 ++++++++---- player/tracker.go | 10 ++ song/periodcalculator.go | 2 + voice/component/envelope.go | 7 +- voice/component/modulator_freq.go | 4 + voice/component/modulator_pan.go | 7 +- voice/config.go | 1 - {player/render => voice/mixer}/details.go | 4 +- voice/{component => mixer}/output.go | 9 +- voice/render.go | 72 +++++++++++++ voice/voice.go | 23 +++-- 48 files changed, 433 insertions(+), 753 deletions(-) delete mode 100644 channel.go delete mode 100644 format/it/voice/render.go delete mode 100644 format/s3m/voice/render.go delete mode 100644 format/xm/voice/render.go create mode 100644 player/machine/channel_pastnote.go delete mode 100644 player/machine/machine_pastnote.go rename {player/render => voice/mixer}/details.go (71%) rename voice/{component => mixer}/output.go (63%) create mode 100644 voice/render.go diff --git a/channel.go b/channel.go deleted file mode 100644 index 3d92f44..0000000 --- a/channel.go +++ /dev/null @@ -1,57 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/song" - "github.com/gotracker/playback/voice" - - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/note" -) - -// Channel is an interface for channel state -type Channel[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { - ResetRetriggerCount() - SetMemory(TMemory) - GetMemory() TMemory - FreezePlayback() - UnfreezePlayback() - GetChannelData() TChannelData - GetPortaTargetPeriod() TPeriod - SetPortaTargetPeriod(TPeriod) - SetPeriodOverride(TPeriod) - SetPeriodDelta(period.Delta) - GetPeriodDelta() period.Delta - SetInstrument(*instrument.Instrument[TMixingVolume, TVolume, TPanning]) - GetVoice() voice.Voice - GetPrevVoice() voice.Voice - GetNoteSemitone() note.Semitone - SetStoredSemitone(note.Semitone) - SetTargetSemitone(note.Semitone) - SetOverrideSemitone(note.Semitone) - SetNotePlayTick(bool, note.Action, int) - GetRetriggerCount() uint8 - SetRetriggerCount(uint8) - SetPanEnabled(bool) - SetRenderChannel(*render.Channel[TGlobalVolume, TMixingVolume, TPanning]) - GetRenderChannel() *render.Channel[TGlobalVolume, TMixingVolume, TPanning] - SetVolumeActive(bool) - SetGlobalVolume(TGlobalVolume) - SetChannelVolume(TMixingVolume) - GetChannelVolume() TMixingVolume - SetEnvelopePosition(int) - TransitionActiveToPastState() - SetNewNoteAction(note.Action) - GetNewNoteAction() note.Action - DoPastNoteEffect(action note.Action) - SetVolumeEnvelopeEnable(bool) - SetPanningEnvelopeEnable(bool) - SetPitchEnvelopeEnable(bool) - GetActiveEffects() []Effect - GetUseTargetPeriod() bool - - GetPreviousState() ChannelState[TPeriod, TVolume, TPanning] - GetActiveState() *ChannelState[TPeriod, TVolume, TPanning] - GetTargetState() *ChannelState[TPeriod, TVolume, TPanning] -} diff --git a/effect.go b/effect.go index 791c348..4cb28fb 100644 --- a/effect.go +++ b/effect.go @@ -33,10 +33,6 @@ type EffectNamer interface { Names() []string } -type effectPreStartIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { - PreStart(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback) error -} - func GetEffectNames(e Effect) []string { if namer, ok := e.(EffectNamer); ok { return namer.Names() @@ -46,103 +42,11 @@ func GetEffectNames(e Effect) []string { } } -// EffectPreStart triggers when the effect enters onto the channel state -func EffectPreStart[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { - if eff, ok := e.(effectPreStartIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { - if err := eff.PreStart(cs, p); err != nil { - return err - } - } - return nil -} - -type effectStartIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { - Start(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback) error -} - -// EffectStart triggers on the first tick, but before the Tick() function is called -func EffectStart[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { - if eff, ok := e.(effectStartIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { - if err := eff.Start(cs, p); err != nil { - return err - } - } - return nil -} - -type effectTickIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { - OldTick(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback, int) error -} - -// EffectTick is called on every tick -func EffectTick[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, currentTick int) error { - if eff, ok := e.(effectTickIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { - if err := eff.OldTick(cs, p, currentTick); err != nil { - return err - } - } - return nil -} - -type effectStopIntf[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning] interface { - Stop(Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], Playback, int) error -} - -// EffectStop is called on the last tick of the row, but after the Tick() function is called -func EffectStop[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, lastTick int) error { - if eff, ok := e.(effectStopIntf[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { - if err := eff.Stop(cs, p, lastTick); err != nil { - return err - } - } - return nil -} - // 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 } -// PreStart triggers when the effect enters onto the channel state -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) PreStart(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { - for _, effect := range e.Effects { - if err := EffectPreStart[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p); err != nil { - return err - } - } - return nil -} - -// Start triggers on the first tick, but before the Tick() function is called -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) Start(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback) error { - for _, effect := range e.Effects { - if err := EffectStart[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p); err != nil { - return err - } - } - return nil -} - -// Tick is called on every tick -func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) OldTick(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, currentTick int) error { - for _, effect := range e.Effects { - if err := EffectTick[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p, currentTick); 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[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, TMemory, TChannelData]) Stop(cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, lastTick int) error { - for _, effect := range e.Effects { - if err := EffectStop[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](effect, cs, p, lastTick); err != nil { - return err - } - } - return nil -} - // 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 { @@ -162,30 +66,6 @@ func (e CombinedEffect[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning, return names } -// DoEffect runs the standard tick lifetime of an effect -func DoEffect[TPeriod period.Period, TMemory song.ChannelMemory, TChannelData song.ChannelData[TVolume], TGlobalVolume, TMixingVolume, TVolume song.Volume, TPanning song.Panning](e Effect, cs Channel[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume, TPanning], p Playback, currentTick int, lastTick bool) error { - if e == nil { - return nil - } - - if currentTick == 0 { - if err := EffectStart[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](e, cs, p); err != nil { - return err - } - } - if err := EffectTick[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](e, cs, p, currentTick); err != nil { - return err - } - if lastTick { - if err := EffectStop[TPeriod, TMemory, TChannelData, TGlobalVolume, TMixingVolume, TVolume](e, cs, p, currentTick); err != nil { - return err - } - } - return 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 { diff --git a/filter/amigafilter.go b/filter/amigafilter.go index 1470ba1..9480246 100644 --- a/filter/amigafilter.go +++ b/filter/amigafilter.go @@ -39,6 +39,11 @@ func (f *AmigaLPF) Clone() Filter { } func (f *AmigaLPF) SetPlaybackRate(playback period.Frequency) { + if f.playbackRate == playback { + return + } + f.playbackRate = playback + freq := 3275.0 f2 := float64(playback) / 2.0 diff --git a/filter/echofilter.go b/filter/echofilter.go index 2f05579..9ecb561 100644 --- a/filter/echofilter.go +++ b/filter/echofilter.go @@ -42,9 +42,15 @@ type EchoFilter struct { initialFeedback volume.Volume writePos int delay [2]delayInfo // L,R + playbackRate period.Frequency } func (e *EchoFilter) SetPlaybackRate(playback period.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) diff --git a/format/it/filter/resonantfilter.go b/format/it/filter/resonantfilter.go index 2389e7f..73899ca 100644 --- a/format/it/filter/resonantfilter.go +++ b/format/it/filter/resonantfilter.go @@ -28,9 +28,10 @@ type ResonantFilter struct { highpass bool extendedFilterRange bool - f2 float64 - fr float64 - efr float64 + f2 float64 + fr float64 + efr float64 + playbackRate period.Frequency } // NewResonantFilter creates a new resonant filter with the provided cutoff and resonance values @@ -53,6 +54,11 @@ func NewResonantFilter(cutoff uint8, resonance uint8, extendedFilterRange bool, } func (f *ResonantFilter) SetPlaybackRate(playback period.Frequency) { + if f.playbackRate == playback { + return + } + f.playbackRate = playback + f.f2 = float64(playback) / 2.0 f.fr = float64(playback) @@ -162,7 +168,7 @@ func (f *ResonantFilter) recalculate(v uint8) { } else if freq > 20000 { freq = 20000 } - if freq > f.f2 { + if freq > f.f2 && f.f2 >= 120.0 { freq = f.f2 } @@ -176,8 +182,13 @@ func (f *ResonantFilter) recalculate(v uint8) { 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 := f.fr / fc @@ -198,6 +209,16 @@ func (f *ResonantFilter) recalculate(v uint8) { } } + 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) diff --git a/format/it/playback/playback.go b/format/it/playback/playback.go index a0c36ec..c656d8b 100644 --- a/format/it/playback/playback.go +++ b/format/it/playback/playback.go @@ -50,20 +50,10 @@ func (m *manager[TPeriod]) Configure(features []feature.Feature) error { } for _, feat := range features { switch f := feat.(type) { - case feature.SongLoop: - us.SongLoop = f - case feature.StartOrderAndRow: - us.StartOrderAndRow = f - case feature.PlayUntilOrderAndRow: - us.PlayUntilOrderAndRow = f case itFeature.LongChannelOutput: us.LongChannelOutput = f.Enabled case itFeature.NewNoteActions: us.EnableNewNoteActions = f.Enabled - case feature.SetDefaultTempo: - us.StartTempo = f.Tempo - case feature.SetDefaultBPM: - us.StartBPM = f.BPM } } return m.SetupMachine(m.song, us) diff --git a/format/it/settings/voicefactory.go b/format/it/settings/voicefactory.go index 1a8c804..b9ab078 100644 --- a/format/it/settings/voicefactory.go +++ b/format/it/settings/voicefactory.go @@ -11,7 +11,7 @@ import ( 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, GetMachineSettings[TPeriod]()) + return itVoice.New(config) } var ( diff --git a/format/it/voice/modulator_pan.go b/format/it/voice/modulator_pan.go index ffa0ff3..4e9451d 100644 --- a/format/it/voice/modulator_pan.go +++ b/format/it/voice/modulator_pan.go @@ -1,6 +1,7 @@ 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" @@ -40,6 +41,6 @@ func (v itVoice[TPeriod]) IsPitchPanEnabled() bool { return v.pitchPan.IsPitchPanEnabled() } -func (v itVoice[TPeriod]) GetFinalPan() itPanning.Panning { +func (v itVoice[TPeriod]) GetFinalPan() panning.Position { return v.finalPan } diff --git a/format/it/voice/render.go b/format/it/voice/render.go deleted file mode 100644 index 5e2c4d0..0000000 --- a/format/it/voice/render.go +++ /dev/null @@ -1,52 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/volume" - itPanning "github.com/gotracker/playback/format/it/panning" - itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/player/render" -) - -func (v *itVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *render.Channel[itVolume.FineVolume, itVolume.FineVolume, itPanning.Panning]) (*mixing.Data, error) { - if v.IsDone() { - return nil, nil - } - - if !v.IsActive() { - return nil, nil - } - - sampler, err := v.GetSampler(details.SamplerSpeed, renderChannel) - if err != nil || sampler == nil { - return nil, err - } - - // ... so grab the new value now. - pan := v.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.ToPosition(), - Volume: volume.Volume(1.0), - Pos: 0, - SamplesLen: details.Samples, - } - - if err := v.SetPos(sampler.GetPosition()); err != nil { - return nil, err - } - - return data, nil -} diff --git a/format/it/voice/sampler.go b/format/it/voice/sampler.go index 9cf8187..81a5bff 100644 --- a/format/it/voice/sampler.go +++ b/format/it/voice/sampler.go @@ -3,10 +3,7 @@ package voice import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - itPanning "github.com/gotracker/playback/format/it/panning" - itVolume "github.com/gotracker/playback/format/it/volume" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/period" ) type voicerPos interface { @@ -46,27 +43,9 @@ func (v *itVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { if v.config.VoiceFilter != nil { wet = v.config.VoiceFilter.Filter(wet) } - if v.config.PluginFilter != nil { - wet = v.config.PluginFilter.Filter(wet) - } return wet } -func (v *itVoice[TPeriod]) GetSampler(samplerRate float32, renderChannel *playerRender.Channel[itVolume.FineVolume, itVolume.FineVolume, itPanning.Panning]) (sampling.Sampler, error) { - o := component.OutputFilter{ - Input: v, - Output: renderChannel, - } - - pos, err := v.GetPos() - if err != nil { - return nil, err - } - - p := v.GetFinalPeriod() - - samplerAdd := float32(v.ms.PeriodConverter.GetSamplerAdd(p, float64(v.config.SampleRate)*float64(samplerRate))) - s := sampling.NewSampler(&o, pos, samplerAdd) - - return s, nil +func (v itVoice[TPeriod]) GetSampleRate() period.Frequency { + return v.config.SampleRate } diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 72e0786..7b1f39d 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -1,11 +1,11 @@ package voice import ( + "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" 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/player/machine/settings" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" @@ -18,7 +18,6 @@ type Period interface { } type itVoice[TPeriod Period] struct { - ms *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] config voice.InstrumentConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] pitchAndFilterEnvShared bool @@ -43,7 +42,7 @@ type itVoice[TPeriod Period] struct { // finals finalVol volume.Volume finalPeriod TPeriod - finalPan itPanning.Panning + finalPan panning.Position } var ( @@ -59,9 +58,8 @@ var ( _ voice.FilterEnvelope = (*itVoice[period.Linear])(nil) ) -func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], ms *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) voice.RenderVoice[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { +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]{ - ms: ms, pitchAndFilterEnvShared: true, } @@ -263,7 +261,6 @@ func (v *itVoice[TPeriod]) Advance() { func (v *itVoice[TPeriod]) Clone() voice.Voice { vv := itVoice[TPeriod]{ - ms: v.ms, config: v.config, pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, filterEnvActive: v.filterEnvActive, @@ -300,10 +297,6 @@ func (v *itVoice[TPeriod]) Clone() voice.Voice { vv.config.VoiceFilter = v.config.VoiceFilter.Clone() } - if v.config.PluginFilter != nil { - vv.config.PluginFilter = v.config.PluginFilter.Clone() - } - return &vv } @@ -348,6 +341,6 @@ func (v *itVoice[TPeriod]) updateFinal() { v.finalPan = v.pan.GetFinalPan() } else { envPan := v.panEnv.GetCurrentValue() - v.finalPan = v.pitchPan.GetSeparatedPan(envPan) + v.finalPan = v.pitchPan.GetSeparatedPan(envPan).ToPosition() } } diff --git a/format/s3m/channel/effect_finevibrato.go b/format/s3m/channel/effect_finevibrato.go index fb31deb..278cb8a 100644 --- a/format/s3m/channel/effect_finevibrato.go +++ b/format/s3m/channel/effect_finevibrato.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -28,7 +27,7 @@ func (e FineVibrato) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV // 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)*s3mSystem.SlideFinesPerSemitone) + return m.SetChannelPeriodDelta(ch, period.Delta(value)) }) } return nil diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go index 62e76dc..8a1f558 100644 --- a/format/s3m/channel/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -30,7 +29,7 @@ func (e PortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVol return nil } - return m.DoChannelPortaDown(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) } func (e PortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 2424c63..5fd7368 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -34,7 +33,7 @@ func (e PortaToNote) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV return nil } - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) } func (e PortaToNote) TraceData() string { diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go index 6e89d21..9484352 100644 --- a/format/s3m/channel/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -30,7 +29,7 @@ func (e PortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum return nil } - return m.DoChannelPortaUp(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) } func (e PortaUp) TraceData() string { diff --git a/format/s3m/channel/effect_vibrato.go b/format/s3m/channel/effect_vibrato.go index 3a4527a..5d42914 100644 --- a/format/s3m/channel/effect_vibrato.go +++ b/format/s3m/channel/effect_vibrato.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -27,7 +26,7 @@ func (e Vibrato) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum // 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)*s3mSystem.SlideFinesPerSemitone) + return m.SetChannelPeriodDelta(ch, period.Delta(value)) }) } return nil diff --git a/format/s3m/settings/voicefactory.go b/format/s3m/settings/voicefactory.go index f1f6216..580075e 100644 --- a/format/s3m/settings/voicefactory.go +++ b/format/s3m/settings/voicefactory.go @@ -11,7 +11,7 @@ import ( 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, GetMachineSettings()) + return s3mVoice.New(config) } var ( diff --git a/format/s3m/voice/render.go b/format/s3m/voice/render.go deleted file mode 100644 index 1fc670e..0000000 --- a/format/s3m/voice/render.go +++ /dev/null @@ -1,52 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/volume" - s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" - "github.com/gotracker/playback/player/render" -) - -func (v *s3mVoice) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *render.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]) (*mixing.Data, error) { - if v.IsDone() { - return nil, nil - } - - if !v.IsActive() { - return nil, nil - } - - sampler, err := v.GetSampler(details.SamplerSpeed, renderChannel) - if err != nil || sampler == nil { - return nil, err - } - - // ... so grab the new value now. - pan := v.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.ToPosition(), - Volume: volume.Volume(1.0), - Pos: 0, - SamplesLen: details.Samples, - } - - if err := v.SetPos(sampler.GetPosition()); err != nil { - return nil, err - } - - return data, nil -} diff --git a/format/s3m/voice/sampler.go b/format/s3m/voice/sampler.go index 6847a13..2b46cfd 100644 --- a/format/s3m/voice/sampler.go +++ b/format/s3m/voice/sampler.go @@ -3,10 +3,7 @@ package voice import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/period" ) type voicerPos interface { @@ -33,37 +30,22 @@ func (v *s3mVoice) SetPos(pos sampling.Pos) error { } func (v *s3mVoice) GetSample(pos sampling.Pos) volume.Matrix { - var samp volume.Matrix + var dry volume.Matrix if sampler, ok := v.voicer.(voicerSampler); ok { - samp = sampler.GetSample(pos) - if samp.Channels == 0 { - samp.Channels = v.voicer.GetNumChannels() + dry = sampler.GetSample(pos) + if dry.Channels == 0 { + dry.Channels = v.voicer.GetNumChannels() } } vol := v.GetFinalVolume() - wet := samp.Apply(vol) + wet := dry.Apply(vol) if v.config.VoiceFilter != nil { wet = v.config.VoiceFilter.Filter(wet) } return wet } -func (v *s3mVoice) GetSampler(samplerRate float32, renderChannel *playerRender.Channel[s3mVolume.Volume, s3mVolume.FineVolume, s3mPanning.Panning]) (sampling.Sampler, error) { - o := component.OutputFilter{ - Input: v, - Output: renderChannel, - } - - pos, err := v.GetPos() - if err != nil { - return nil, err - } - - p := v.GetFinalPeriod() - - samplerAdd := float32(v.ms.PeriodConverter.GetSamplerAdd(p, float64(v.config.SampleRate)*float64(samplerRate))) - s := sampling.NewSampler(&o, pos, samplerAdd) - - return s, nil +func (v s3mVoice) GetSampleRate() period.Frequency { + return v.config.SampleRate } diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index de1d769..f837f4f 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -4,7 +4,6 @@ import ( s3mPanning "github.com/gotracker/playback/format/s3m/panning" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/loop" @@ -16,7 +15,6 @@ type Period interface { } type s3mVoice struct { - ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] config voice.InstrumentConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] component.KeyModulator @@ -35,10 +33,8 @@ var ( _ voice.PanModulator[s3mPanning.Panning] = (*s3mVoice)(nil) ) -func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], ms *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) voice.RenderVoice[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { - v := &s3mVoice{ - ms: ms, - } +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{} v.KeyModulator.Setup(component.KeyModulatorSettings{ Attack: v.doAttack, @@ -54,6 +50,8 @@ func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.Fine DefaultVolume: config.InitialVolume, }) + v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{}) + v.PanModulator.Setup(component.PanModulatorSettings[s3mPanning.Panning]{ Enabled: config.PanEnabled, InitialPan: config.InitialPan, @@ -95,12 +93,14 @@ func (v *s3mVoice) doDeferredRelease() { func (v *s3mVoice) Setup(config voice.InstrumentConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) { v.config = config - v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{}) v.KeyModulator.Release() v.Reset() } func (v *s3mVoice) Reset() { + v.AmpModulator.Reset() + v.FreqModulator.Reset() + v.PanModulator.Reset() v.vol0Opt.Reset() } @@ -126,7 +126,6 @@ func (v *s3mVoice) Advance() { func (v *s3mVoice) Clone() voice.Voice { vv := s3mVoice{ - ms: v.ms, config: v.config, AmpModulator: v.AmpModulator.Clone(), FreqModulator: v.FreqModulator.Clone(), @@ -150,10 +149,6 @@ func (v *s3mVoice) Clone() voice.Voice { vv.config.VoiceFilter = v.config.VoiceFilter.Clone() } - if v.config.PluginFilter != nil { - vv.config.PluginFilter = v.config.PluginFilter.Clone() - } - return &vv } diff --git a/format/xm/settings/voicefactory.go b/format/xm/settings/voicefactory.go index a33a918..7401f69 100644 --- a/format/xm/settings/voicefactory.go +++ b/format/xm/settings/voicefactory.go @@ -11,7 +11,7 @@ import ( 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, GetMachineSettings[TPeriod]()) + return xmVoice.New(config) } var ( diff --git a/format/xm/voice/modulator_pan.go b/format/xm/voice/modulator_pan.go index 6beaa82..171fd36 100644 --- a/format/xm/voice/modulator_pan.go +++ b/format/xm/voice/modulator_pan.go @@ -1,6 +1,7 @@ package voice import ( + "github.com/gotracker/gomixing/panning" xmPanning "github.com/gotracker/playback/format/xm/panning" "github.com/gotracker/playback/voice/types" ) @@ -23,11 +24,11 @@ func (v xmVoice[TPeriod]) GetPanDelta() types.PanDelta { return v.pan.GetPanDelta() } -func (v xmVoice[TPeriod]) GetFinalPan() xmPanning.Panning { +func (v xmVoice[TPeriod]) GetFinalPan() panning.Position { if !v.IsPanEnvelopeEnabled() { return v.pan.GetFinalPan() } envPan := v.panEnv.GetCurrentValue() - return envPan + return envPan.ToPosition() } diff --git a/format/xm/voice/render.go b/format/xm/voice/render.go deleted file mode 100644 index 45b3f6d..0000000 --- a/format/xm/voice/render.go +++ /dev/null @@ -1,52 +0,0 @@ -package voice - -import ( - "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/gomixing/volume" - xmPanning "github.com/gotracker/playback/format/xm/panning" - xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/player/render" -) - -func (v *xmVoice[TPeriod]) Render(centerAheadPan volume.Matrix, details render.Details, renderChannel *render.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) (*mixing.Data, error) { - if v.IsDone() { - return nil, nil - } - - if !v.IsActive() { - return nil, nil - } - - sampler, err := v.GetSampler(details.SamplerSpeed, renderChannel) - if err != nil || sampler == nil { - return nil, err - } - - // ... so grab the new value now. - pan := v.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.ToPosition(), - Volume: volume.Volume(1.0), - Pos: 0, - SamplesLen: details.Samples, - } - - if err := v.SetPos(sampler.GetPosition()); err != nil { - return nil, err - } - - return data, nil -} diff --git a/format/xm/voice/sampler.go b/format/xm/voice/sampler.go index 0c5d730..3e960a7 100644 --- a/format/xm/voice/sampler.go +++ b/format/xm/voice/sampler.go @@ -3,10 +3,7 @@ package voice import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - xmPanning "github.com/gotracker/playback/format/xm/panning" - xmVolume "github.com/gotracker/playback/format/xm/volume" - playerRender "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/voice/component" + "github.com/gotracker/playback/period" ) type voicerPos interface { @@ -33,37 +30,22 @@ func (v *xmVoice[TPeriod]) SetPos(pos sampling.Pos) error { } func (v *xmVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { - var samp volume.Matrix + var dry volume.Matrix if sampler, ok := v.voicer.(voicerSampler); ok { - samp = sampler.GetSample(pos) - if samp.Channels == 0 { - samp.Channels = v.voicer.GetNumChannels() + dry = sampler.GetSample(pos) + if dry.Channels == 0 { + dry.Channels = v.voicer.GetNumChannels() } } vol := v.GetFinalVolume() - wet := samp.Apply(vol) + wet := dry.Apply(vol) if v.config.VoiceFilter != nil { wet = v.config.VoiceFilter.Filter(wet) } return wet } -func (v *xmVoice[TPeriod]) GetSampler(samplerRate float32, renderChannel *playerRender.Channel[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) (sampling.Sampler, error) { - o := component.OutputFilter{ - Input: v, - Output: renderChannel, - } - - pos, err := v.GetPos() - if err != nil { - return nil, err - } - - p := v.GetFinalPeriod() - - samplerAdd := float32(v.ms.PeriodConverter.GetSamplerAdd(p, float64(v.config.SampleRate)*float64(samplerRate))) - s := sampling.NewSampler(&o, pos, samplerAdd) - - return s, nil +func (v xmVoice[TPeriod]) GetSampleRate() period.Frequency { + return v.config.SampleRate } diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 1685f44..65d0ec5 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -4,7 +4,6 @@ import ( 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/player/machine/settings" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" @@ -17,7 +16,6 @@ type Period interface { } type xmVoice[TPeriod Period] struct { - ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] config voice.InstrumentConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] fadeoutMode fadeout.Mode @@ -45,10 +43,8 @@ var ( _ voice.PanEnvelope[xmPanning.Panning] = (*xmVoice[period.Linear])(nil) ) -func New[TPeriod Period](config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], ms *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) voice.RenderVoice[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { - v := &xmVoice[TPeriod]{ - ms: ms, - } +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, @@ -200,7 +196,6 @@ func (v *xmVoice[TPeriod]) Advance() { func (v *xmVoice[TPeriod]) Clone() voice.Voice { vv := xmVoice[TPeriod]{ - ms: v.ms, config: v.config, fadeoutMode: v.fadeoutMode, amp: v.amp.Clone(), @@ -232,10 +227,6 @@ func (v *xmVoice[TPeriod]) Clone() voice.Voice { vv.config.VoiceFilter = v.config.VoiceFilter.Clone() } - if v.config.PluginFilter != nil { - vv.config.PluginFilter = v.config.PluginFilter.Clone() - } - return &vv } diff --git a/player/machine/channel.go b/player/machine/channel.go index 06506b7..fc62567 100644 --- a/player/machine/channel.go +++ b/player/machine/channel.go @@ -8,6 +8,7 @@ import ( "github.com/gotracker/playback/memory" "github.com/gotracker/playback/note" "github.com/gotracker/playback/player/machine/instruction" + "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/oscillator" @@ -44,8 +45,8 @@ type channel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann filterEnabled bool nna note.Action - cv voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - pn pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + cv voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] + pastNotes []*render.Channel[TPeriod] instructions []instruction.Instruction } diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index f4f9ce2..7259567 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -27,17 +27,20 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo c.target.ActionTick.Reset() // perform new note action - if m.canPastNote() { - pn := c.cv.Clone() + if na.Action != note.ActionContinue && m.canPastNote() { + var pn voice.Voice switch c.nna { case note.ActionCut: - pn.Stop() + c.cv.Stop() case note.ActionRelease: + pn = c.cv.Clone() pn.Release() case note.ActionFadeout: + pn = c.cv.Clone() pn.Release() pn.Fadeout() case note.ActionRetrigger: + pn = c.cv.Clone() pn.Release() pn.Attack() @@ -47,16 +50,15 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo // nothing } - m.addPastNote(ch, pn.(voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning])) - } - - if pitchPanMod, ok := c.cv.(voice.PitchPanModulator[TPanning]); ok { - pitchPanMod.SetPitchPanNote(c.prev.Semitone.Coalesce(0)) + 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() @@ -77,13 +79,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo o.Reset() } - if ampMod, ok := c.cv.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok { - ampMod.SetVolumeDelta(0) - } - - if freqMod, ok := c.cv.(voice.FreqModulator[TPeriod]); ok { - freqMod.SetPeriodDelta(0) - } + c.cv.Reset() c.cv.Attack() @@ -92,6 +88,18 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo 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 { + samp.SetPos(pos) + } + c.target.Pos.Reset() + } + return nil } @@ -116,8 +124,6 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe default: panic("unhandled instrument kind") } - } else { - c.cv.Reset() } } else { c.cv.Stop() @@ -126,17 +132,18 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe } func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupPCM(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.PCM[TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { - var ( - voiceFilter filter.Filter - pluginFilter filter.Filter - ) + var voiceFilter filter.Filter if factory := inst.GetFilterFactory(); factory != nil { voiceFilter = factory(inst.SampleRate) voiceFilter.SetPlaybackRate(outputRate) } + + rc := &m.actualOutputs[ch] if factory := inst.GetPluginFilterFactory(); factory != nil { - pluginFilter = factory(inst.SampleRate) - pluginFilter.SetPlaybackRate(outputRate) + rc.PluginFilter = factory(inst.SampleRate) + rc.PluginFilter.SetPlaybackRate(outputRate) + } else { + rc.PluginFilter = nil } c.cv.Setup(voice.InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ @@ -144,7 +151,6 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe AutoVibrato: inst.GetAutoVibrato(), Data: d, VoiceFilter: voiceFilter, - PluginFilter: pluginFilter, FadeOut: d.FadeOut, PitchPan: d.PitchPan, VolEnv: d.VolEnv, diff --git a/player/machine/channel_pastnote.go b/player/machine/channel_pastnote.go new file mode 100644 index 0000000..fcbd817 --- /dev/null +++ b/player/machine/channel_pastnote.go @@ -0,0 +1,76 @@ +package machine + +import ( + "github.com/gotracker/playback/note" + "github.com/gotracker/playback/player/render" + "github.com/gotracker/playback/voice" +) + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], pn voice.Voice) { + mpnpc := m.songData.GetSystem().GetMaxPastNotesPerChannel() + + // make room + if over := len(c.pastNotes) + 1 - mpnpc; over > 0 { + for _, rc := range c.pastNotes[0:over] { + rc.StopVoice() + } + + c.pastNotes = c.pastNotes[over:] + } + + for i := range m.virtualOutputs { + rc := &m.virtualOutputs[i] + + if rc.Voice == nil { + rc.Voice = pn + c.pastNotes = append(c.pastNotes, rc) + return + } + } + + // we failed to find a spot? + pn.Stop() +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPastNoteAction(na note.Action) { + for _, pn := range c.pastNotes { + if pn.Voice == nil { + continue + } + + switch na { + case note.ActionCut: + pn.StopVoice() + case note.ActionRelease: + pn.Voice.Release() + case note.ActionFadeout: + pn.Voice.Release() + pn.Voice.Fadeout() + case note.ActionRetrigger: + pn.Voice.Release() + pn.Voice.Attack() + + case note.ActionContinue: + fallthrough + default: + // nothing + } + } +} + +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) updatePastNotes() { + var updated []*render.Channel[TPeriod] + for _, pn := range c.pastNotes { + if pn.Voice == nil { + continue + } + + if pn.Voice.IsDone() { + pn.StopVoice() + continue + } + + updated = append(updated, pn) + } + c.pastNotes = updated +} diff --git a/player/machine/channel_tick.go b/player/machine/channel_tick.go index 5d235ee..8bbea7c 100644 --- a/player/machine/channel_tick.go +++ b/player/machine/channel_tick.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -105,6 +106,13 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RowS 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 } diff --git a/player/machine/machine.go b/player/machine/machine.go index 5ea8779..a830052 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -126,7 +126,7 @@ type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann globals[TGlobalVolume] singleRow channels []channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - pastNotes []index.Channel + pastNotes []pastNote[TPeriod] ticker ticker age int @@ -136,8 +136,11 @@ type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann us settings.UserSettings opl2 render.OPL2Chip - rowStringer render.RowStringer - outputChannels []render.Channel[TGlobalVolume, TMixingVolume, TPanning] + 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) { diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 17380e4..95ca7b6 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -17,6 +17,10 @@ import ( "github.com/gotracker/playback/voice/types" ) +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canPastNote() bool { + return 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 { @@ -509,8 +513,10 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetC } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPastNoteEffect(ch index.Channel, na note.Action) error { - m.doPastNoteAction(ch, na) - return nil + return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { + c.doPastNoteAction(na) + return nil + }) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelNewNoteAction(ch index.Channel, na note.Action) error { diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 1dccf72..c5fef92 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -96,15 +96,19 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum channels := songData.GetNumChannels() m.channels = make([]channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], channels) - // make at least 64 output channels - m.outputChannels = make([]render.Channel[TGlobalVolume, TMixingVolume, TPanning], channels) + m.actualOutputs = make([]render.Channel[TPeriod], channels) + + 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.outputChannels[ch] - rc.ChannelNum = i - rc.Filter = nil + rc := &m.actualOutputs[ch] + rc.OutputFilter = nil if cs.IsDefaultFilterEnabled() { name := cs.GetDefaultFilterName() @@ -118,7 +122,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum return nil, err } - rc.Filter = filt + rc.OutputFilter = filt } rc.GetOPL2Chip = func() render.OPL2Chip { return m.opl2 @@ -151,6 +155,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum Vol0Optimization: cs.GetVol0OptimizationSettings(), }) c.memory = cs.GetMemory() + rc.Voice = c.cv c.target.ActionTick.Reset() c.nna = note.ActionCut diff --git a/player/machine/machine_pastnote.go b/player/machine/machine_pastnote.go deleted file mode 100644 index 078830f..0000000 --- a/player/machine/machine_pastnote.go +++ /dev/null @@ -1,78 +0,0 @@ -package machine - -import ( - "sort" - - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/voice" -) - -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canPastNote() bool { - return m.songData.GetSystem().GetMaxPastNotesPerChannel() > 0 -} - -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(ch index.Channel, pn voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) { - type pastNoteAges struct { - holder int - entry pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - } - - // first pass, try to add it to the channel listed - maxNotes := len(m.channels) * m.songData.GetSystem().GetMaxPastNotesPerChannel() - if len(m.pastNotes) < maxNotes { - c := &m.channels[ch] - - m.pastNotes = append(m.pastNotes, ch) - c.pn.AddPastNote(ch, pn, m.age) - return - } - - ages := make([]pastNoteAges, 0, len(m.outputChannels)) - // second pass, try to bump the oldest - for i := range m.channels { - c := &m.channels[i] - - if oldest := c.pn.GetOldest(); oldest != nil { - ages = append(ages, pastNoteAges{ - holder: i, - entry: *oldest, - }) - } - } - - if len(ages) == 0 { - // impossible, but just in case - pn.Stop() - return - } - - // find the oldest - sort.Slice(ages, func(i, j int) bool { - return ages[i].entry.age < ages[j].entry.age - }) - - // jam it in - oldestHolder := ages[0].holder - m.channels[oldestHolder].pn.AddPastNote(ch, pn, m.age) - m.pastNotes = append(m.pastNotes, ch) - - if over := len(m.pastNotes) - maxNotes; over > 0 { - for _, n := range m.pastNotes[0:over] { - if oldest := m.channels[n].pn.GetOldest(); oldest != nil { - oldest.v.Stop() - } - } - m.pastNotes = m.pastNotes[over:] - } -} - -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPastNoteAction(ch index.Channel, na note.Action) { - if int(ch) >= len(m.outputChannels) { - return - } - - for i := range m.channels { - m.channels[i].pn.DoPastNoteAction(ch, na) - } -} diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 1b06718..73440a9 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -12,7 +12,7 @@ import ( "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/system" - "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/mixer" ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) render(s *sampler.Sampler) (*output.PremixData, error) { @@ -40,9 +40,10 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend sys := m.songData.GetSystem() - details := render.Details{ + details := mixer.Details{ Mix: s.Mixer(), Panmixer: s.GetPanMixer(), + SampleRate: system.Frequency(s.SampleRate), SamplerSpeed: sys.GetSamplerSpeed(period.Frequency(s.SampleRate)), Samples: premix.SamplesLen, Duration: tickDuration, @@ -50,48 +51,47 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) - for i := range m.channels { - ch := index.Channel(i) - c := &m.channels[ch] - var mixData []mixing.Data - - if pos, set := c.target.Pos.Get(); set { - if samp, ok := c.cv.(voice.Sampler); ok { - samp.SetPos(pos) - } - c.target.Pos.Reset() - } - - rc := &m.outputChannels[ch] - if filt := rc.Filter; filt != nil { - filt.SetPlaybackRate(system.Frequency(s.SampleRate)) - } + var mixData []mixing.Data + for i := range m.actualOutputs { + rc := &m.actualOutputs[i] rc.GlobalVolume = m.gv.ToVolume() - c.cv.DumpState(ch, m.us.Tracer) - data, err := c.cv.Render(centerAheadPan, details, rc) + rc.Voice.DumpState(index.Channel(i), m.us.Tracer) + data, err := rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) if err != nil { return nil, err } - c.cv.Advance() + if data != nil { - mixData = append(mixData, *data) + mixData = append(mixData, data...) } + } - pnData, err := c.pn.RenderAndAdvance(centerAheadPan, details, rc) + for i := range m.virtualOutputs { + rc := &m.virtualOutputs[i] + + if rc.Voice == nil { + continue + } + + rc.GlobalVolume = m.gv.ToVolume() + + //rc.Voice.DumpState(index.Channel(i), m.us.Tracer) + data, err := rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) if err != nil { return nil, err } - if len(pnData) > 0 { - mixData = append(mixData, pnData...) - } - if len(mixData) > 0 { - premix.Data = append(premix.Data, mixData) + if data != nil { + mixData = append(mixData, data...) } } + 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 { diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index c42c239..9e5bdcf 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -40,7 +40,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTi return err } - c.pn.UpdatePastNotes() + c.updatePastNotes() } return nil diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go index 6fd3fd8..fbdc82b 100644 --- a/player/machine/pastnote.go +++ b/player/machine/pastnote.go @@ -3,107 +3,15 @@ package machine import ( "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/render" - "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/mixer" ) -type pastNote[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { - v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - ch index.Channel - age int +type pastNote[TPeriod Period] struct { + rc *render.Channel[TPeriod] } -type pastNotes[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { - pn []pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] -} - -func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) AddPastNote(ch index.Channel, v voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], age int) { - o.pn = append(o.pn, pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ - v: v, - ch: ch, - age: age, - }) -} - -func (o pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetOldest() *pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] { - if len(o.pn) == 0 { - return nil - } - - oldest := o.pn[0] - return &oldest -} - -func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) UpdatePastNotes() { - var updated []pastNote[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - for _, pn := range o.pn { - if pn.v.IsDone() { - pn.v.Stop() - continue - } - updated = append(updated, pn) - } - o.pn = updated -} - -func (o pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) HasPastNoteForChannel(ch index.Channel) bool { - for _, pn := range o.pn { - if pn.ch == ch { - return true - } - } - return false -} - -func (o *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoPastNoteAction(ch index.Channel, na note.Action) { - for _, pn := range o.pn { - if pn.ch != ch { - continue - } - - switch na { - case note.ActionCut: - pn.v.Stop() - case note.ActionRelease: - pn.v.Release() - case note.ActionFadeout: - pn.v.Release() - pn.v.Fadeout() - case note.ActionRetrigger: - pn.v.Release() - pn.v.Attack() - - case note.ActionContinue: - fallthrough - default: - // nothing - } - } -} - -func (p *pastNotes[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) RenderAndAdvance(centerAheadPan volume.Matrix, details render.Details, rc *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) ([]mixing.Data, error) { - var mixData []mixing.Data - - for _, pn := range p.pn { - if pn.v.IsDone() { - continue - } - - if ampMod, ok := pn.v.(voice.AmpModulator[TGlobalVolume, TMixingVolume, TVolume]); ok && !ampMod.IsActive() { - continue - } - - data, err := pn.v.Render(centerAheadPan, details, rc) - if err != nil { - return nil, err - } - pn.v.Advance() - - if data != nil { - mixData = append(mixData, *data) - } - } - return mixData, nil +func (p pastNote[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) ([]mixing.Data, error) { + return p.rc.RenderAndAdvance(pc, centerAheadPan, details) } diff --git a/player/machine/ticker.go b/player/machine/ticker.go index 1c4a986..d39b7bc 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -223,6 +223,9 @@ orderScan: } // 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{}{} diff --git a/player/render/channel.go b/player/render/channel.go index 8989962..b690841 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -1,11 +1,14 @@ package render import ( + "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/song" + "github.com/gotracker/playback/period" + "github.com/gotracker/playback/voice" channelfilter "github.com/gotracker/playback/voice/filter" + "github.com/gotracker/playback/voice/mixer" ) type ChannelIntf interface { @@ -14,34 +17,58 @@ type ChannelIntf interface { } // Channel is the important bits to make output to a particular downmixing channel work -type Channel[TGlobalVolume, TMixingVolume song.Volume, TPanning song.Panning] struct { - ChannelNum int - Filter filter.Filter +type Channel[TPeriod period.Period] struct { + PluginFilter filter.Filter + OutputFilter filter.Filter GetOPL2Chip func() OPL2Chip GlobalVolume volume.Volume // this is the channel's version of the GlobalVolume + Voice voice.Voice } -// ApplyFilter will apply the channel filter, if there is one. -func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) ApplyFilter(dry volume.Matrix) volume.Matrix { - if dry.Channels == 0 { - return volume.Matrix{} +func (c *Channel[TPeriod]) RenderAndAdvance(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.RenderAndAdvance(c.Voice, pc, centerAheadPan, details, c) + if err != nil { + return nil, err + } + + if data == nil { + return nil, nil + } + return []mixing.Data{*data}, nil } -// GetPremixVolume returns the premix volume of the output channel -func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) GetPremixVolume() volume.Volume { - return oc.GlobalVolume +func (c *Channel[TPeriod]) StopVoice() { + if c.Voice == nil { + return + } + + c.Voice.Stop() + c.Voice = nil } -// SetFilterEnvelopeValue updates the filter on the channel with the new envelope value -func (oc *Channel[TGlobalVolume, TMixingVolume, TPanning]) SetFilterEnvelopeValue(envVal uint8) { - if oc.Filter != nil { - oc.Filter.UpdateEnv(envVal) +// 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/tracker.go b/player/tracker.go index 6d5cc34..0c07850 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -116,6 +116,16 @@ func (t *Tracker) Configure(features []feature.Feature) (settings.UserSettings, for _, feat := range features { switch f := feat.(type) { + case feature.SongLoop: + us.SongLoop = f + case feature.StartOrderAndRow: + us.StartOrderAndRow = f + case feature.PlayUntilOrderAndRow: + us.PlayUntilOrderAndRow = f + case feature.SetDefaultTempo: + us.StartTempo = f.Tempo + case feature.SetDefaultBPM: + us.StartBPM = f.BPM case feature.IgnoreUnknownEffect: us.IgnoreUnknownEffect = f.Enabled case feature.EnableTracing: diff --git a/song/periodcalculator.go b/song/periodcalculator.go index 7a2941c..097f296 100644 --- a/song/periodcalculator.go +++ b/song/periodcalculator.go @@ -13,6 +13,8 @@ type PeriodCalculatorIntf interface { } type PeriodCalculator[TPeriod period.Period] interface { + PeriodCalculatorIntf + GetPeriod(note.Note) TPeriod PortaToNote(TPeriod, period.Delta, TPeriod) (TPeriod, error) PortaDown(TPeriod, period.Delta) (TPeriod, error) diff --git a/voice/component/envelope.go b/voice/component/envelope.go index d942127..bab3d52 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -206,7 +206,7 @@ func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { } e.keyed.pos++ - curTick, _ := loop.CalcLoopPos(e.settings.Envelope.Loop, e.settings.Envelope.Sustain, e.keyed.pos, e.settings.Envelope.Length, keyOn) + 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 { @@ -221,6 +221,11 @@ func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { return true } + if !keyOn && !looped && curTick >= e.settings.Length { + e.keyed.done = false + return true + } + e.updateValue() return false } diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index 7c1d8d0..da20915 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -42,6 +42,10 @@ func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { // SetPeriod sets the current period (before AutoVibrato and Delta calculation) func (f *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { + if period.IsInvalid() { + return + } + f.unkeyed.period = period f.updateFinal() } diff --git a/voice/component/modulator_pan.go b/voice/component/modulator_pan.go index 30fcef7..e7afbc0 100755 --- a/voice/component/modulator_pan.go +++ b/voice/component/modulator_pan.go @@ -3,6 +3,7 @@ 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" @@ -17,7 +18,7 @@ type PanModulator[TPanning types.Panning] struct { keyed struct { delta types.PanDelta } - final TPanning + final panning.Position } type PanModulatorSettings[TPanning types.Panning] struct { @@ -68,7 +69,7 @@ func (p PanModulator[TPanning]) GetPanDelta() types.PanDelta { } // GetFinalPan returns the current panning -func (p PanModulator[TPanning]) GetFinalPan() TPanning { +func (p PanModulator[TPanning]) GetFinalPan() panning.Position { return p.final } @@ -80,5 +81,5 @@ func (p PanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, co } func (p *PanModulator[TPanning]) updateFinal() { - p.final = types.AddPanningDelta(p.unkeyed.pan, p.keyed.delta) + p.final = types.AddPanningDelta(p.unkeyed.pan, p.keyed.delta).ToPosition() } diff --git a/voice/config.go b/voice/config.go index 7b74fe9..50b503f 100644 --- a/voice/config.go +++ b/voice/config.go @@ -22,7 +22,6 @@ type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volu AutoVibrato autovibrato.AutoVibratoSettings Data instrument.Data VoiceFilter filter.Filter - PluginFilter filter.Filter FadeOut fadeout.Settings PitchPan instrument.PitchPan VolEnv envelope.Envelope[TVolume] diff --git a/player/render/details.go b/voice/mixer/details.go similarity index 71% rename from player/render/details.go rename to voice/mixer/details.go index 7fea696..60ef752 100644 --- a/player/render/details.go +++ b/voice/mixer/details.go @@ -1,14 +1,16 @@ -package render +package mixer import ( "time" "github.com/gotracker/gomixing/mixing" + "github.com/gotracker/playback/period" ) type Details struct { Mix *mixing.Mixer Panmixer mixing.PanMixer + SampleRate period.Frequency SamplerSpeed float32 Samples int Duration time.Duration diff --git a/voice/component/output.go b/voice/mixer/output.go similarity index 63% rename from voice/component/output.go rename to voice/mixer/output.go index 1a69a90..15c7d5c 100644 --- a/voice/component/output.go +++ b/voice/mixer/output.go @@ -1,4 +1,4 @@ -package component +package mixer import ( "github.com/gotracker/gomixing/sampling" @@ -6,14 +6,15 @@ import ( "github.com/gotracker/playback/voice/filter" ) -// OutputFilter applies a filter to a sample stream -type OutputFilter struct { +// Output applies a filter to a sample stream +type Output struct { Input sampling.SampleStream Output filter.Applier } // GetSample operates the filter -func (o *OutputFilter) GetSample(pos sampling.Pos) volume.Matrix { +// must be pointer receiver +func (o *Output) GetSample(pos sampling.Pos) volume.Matrix { dry := o.Input.GetSample(pos) return o.Output.ApplyFilter(dry) } diff --git a/voice/render.go b/voice/render.go new file mode 100644 index 0000000..4392068 --- /dev/null +++ b/voice/render.go @@ -0,0 +1,72 @@ +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/filter" + "github.com/gotracker/playback/voice/mixer" +) + +func RenderAndAdvance[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details, out filter.Applier) (*mixing.Data, error) { + if in.IsDone() { + return nil, nil + } + + defer in.Advance() + + 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 := rs.GetFinalPeriod() + + samplerAdd := float32(pc.GetSamplerAdd(p, float64(rs.GetSampleRate())*float64(details.SamplerSpeed))) + + 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/voice.go b/voice/voice.go index 8ee16fd..09d8d74 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -1,13 +1,12 @@ package voice 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/index" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" - render "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/tracing" "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/pcm" @@ -32,6 +31,7 @@ type Voice interface { // General Parameters IsDone() bool + GetSampleRate() period.Frequency } type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { @@ -40,9 +40,6 @@ type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, T // Configuration Setup(config InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetPCM(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop, mixVolume TMixingVolume, defaultVolume TVolume) - - // Render - Render(panningMatrix volume.Matrix, details render.Details, renderChannel *render.Channel[TGlobalVolume, TMixingVolume, TPanning]) (*mixing.Data, error) } type AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { @@ -78,13 +75,27 @@ type Sampler interface { GetPos() (sampling.Pos, error) } +type RenderSampler[TPeriod Period] interface { + Voice + sampling.SampleStream + + IsActive() bool + + SetPos(pos sampling.Pos) error + GetPos() (sampling.Pos, error) + + GetFinalPeriod() TPeriod + GetFinalVolume() volume.Volume + GetFinalPan() panning.Position +} + type PanModulator[TPanning Panning] interface { // Pan Parameters GetPan() TPanning SetPan(pan TPanning) GetPanDelta() types.PanDelta SetPanDelta(d types.PanDelta) - GetFinalPan() TPanning + GetFinalPan() panning.Position } type PitchPanModulator[TPanning Panning] interface { From 9dd2d0afa177eea4e07c2202742431fbf8fe2e9d Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 14 Jan 2024 12:56:51 -0800 Subject: [PATCH 37/63] hook up new note actions flag --- format/s3m/playback/playback.go | 12 ------------ format/xm/playback/playback.go | 12 ------------ player/machine/machine_channel.go | 2 +- 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go index 2afeff5..dca0b9d 100644 --- a/format/s3m/playback/playback.go +++ b/format/s3m/playback/playback.go @@ -33,18 +33,6 @@ func (m *manager) Configure(features []feature.Feature) error { if err != nil { return err } - for _, feat := range features { - switch f := feat.(type) { - case feature.SongLoop: - us.SongLoop = f - case feature.PlayUntilOrderAndRow: - us.PlayUntilOrderAndRow = f - case feature.SetDefaultTempo: - us.StartTempo = f.Tempo - case feature.SetDefaultBPM: - us.StartBPM = f.BPM - } - } return m.SetupMachine(m.song, us) } diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go index 56a06bf..c9ab975 100644 --- a/format/xm/playback/playback.go +++ b/format/xm/playback/playback.go @@ -50,17 +50,5 @@ func (m *manager[TPeriod]) Configure(features []feature.Feature) error { if err != nil { return err } - for _, feat := range features { - switch f := feat.(type) { - case feature.SongLoop: - us.SongLoop = f - case feature.PlayUntilOrderAndRow: - us.PlayUntilOrderAndRow = f - case feature.SetDefaultTempo: - us.StartTempo = f.Tempo - case feature.SetDefaultBPM: - us.StartBPM = f.BPM - } - } return m.SetupMachine(m.song, us) } diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 95ca7b6..1c51183 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -18,7 +18,7 @@ import ( ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) canPastNote() bool { - return len(m.virtualOutputs) > 0 + 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 { From 6a1e5e8a8d88643bb515a144c98e34497c9e6bcf Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 14 Jan 2024 17:22:38 -0800 Subject: [PATCH 38/63] move frequency type to its own package - reduces issue with cycle detection misidentifying period package --- filter/amigafilter.go | 8 +++---- filter/echofilter.go | 9 ++++---- filter/filter.go | 6 +++--- format/it/filter/resonantfilter.go | 6 +++--- format/it/load/instrument.go | 6 +++--- format/it/period/util.go | 11 +++++----- format/it/settings/machine.go | 3 ++- format/it/system/system.go | 3 ++- format/it/voice/sampler.go | 4 ++-- format/s3m/load/s3mformat.go | 10 ++++----- format/s3m/period/util.go | 11 +++++----- format/s3m/settings/machine.go | 3 ++- format/s3m/system/system.go | 6 +++--- format/s3m/voice/sampler.go | 4 ++-- format/xm/load/xmformat.go | 3 ++- format/xm/period/util.go | 12 +++++------ format/xm/settings/machine.go | 3 ++- format/xm/system/system.go | 3 ++- format/xm/voice/sampler.go | 4 ++-- frequency/frequency.go | 25 ++++++++++++++++++++++ instrument/instrument.go | 12 +++++------ period/amiga.go | 3 ++- period/amigaconverter.go | 5 +++-- period/frequency.go | 8 ------- period/linearconverter.go | 5 +++-- period/periodconverter.go | 3 ++- player/machine/channel_noteaction.go | 8 +++---- player/machine/machine_render.go | 7 +++--- player/machine/machine_tick.go | 4 ++-- player/machine/settings/machinesettings.go | 4 ++-- player/render/channel.go | 3 +-- player/sampler/sampler.go | 4 ++-- player/tracker.go | 6 +++--- song/periodcalculator.go | 3 ++- system/clockedsystem.go | 15 +++++++------ system/frequency.go | 4 ---- system/system.go | 6 ++++-- voice/component/opl2.go | 7 +++--- voice/config.go | 4 ++-- voice/mixer/details.go | 4 ++-- voice/mixer/output.go | 7 ++++-- voice/render.go | 3 +-- voice/voice.go | 3 ++- 43 files changed, 149 insertions(+), 119 deletions(-) create mode 100644 frequency/frequency.go delete mode 100755 period/frequency.go delete mode 100644 system/frequency.go diff --git a/filter/amigafilter.go b/filter/amigafilter.go index 9480246..dfad1b3 100644 --- a/filter/amigafilter.go +++ b/filter/amigafilter.go @@ -4,7 +4,7 @@ import ( "math" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) type channelData struct { @@ -19,11 +19,11 @@ type AmigaLPF struct { b0 volume.Volume b1 volume.Volume - playbackRate period.Frequency + playbackRate frequency.Frequency } // NewAmigaLPF creates a new AmigaLPF -func NewAmigaLPF(instrument period.Frequency) *AmigaLPF { +func NewAmigaLPF(instrument frequency.Frequency) *AmigaLPF { var f AmigaLPF f.SetPlaybackRate(instrument) return &f @@ -38,7 +38,7 @@ func (f *AmigaLPF) Clone() Filter { return &c } -func (f *AmigaLPF) SetPlaybackRate(playback period.Frequency) { +func (f *AmigaLPF) SetPlaybackRate(playback frequency.Frequency) { if f.playbackRate == playback { return } diff --git a/filter/echofilter.go b/filter/echofilter.go index 9ecb561..2382456 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,7 +21,7 @@ type EchoFilterFactory struct { } func (e *EchoFilterFactory) Factory() Factory { - return func(instrument period.Frequency) Filter { + return func(instrument frequency.Frequency) Filter { echo := EchoFilter{ EchoFilterSettings: e.EchoFilterSettings, } @@ -42,10 +41,10 @@ type EchoFilter struct { initialFeedback volume.Volume writePos int delay [2]delayInfo // L,R - playbackRate period.Frequency + playbackRate frequency.Frequency } -func (e *EchoFilter) SetPlaybackRate(playback period.Frequency) { +func (e *EchoFilter) SetPlaybackRate(playback frequency.Frequency) { if e.playbackRate == playback { return } diff --git a/filter/filter.go b/filter/filter.go index 4114912..f0cee65 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -2,16 +2,16 @@ package filter import ( "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) // Filter is an interface to a filter type Filter interface { Filter(volume.Matrix) volume.Matrix - SetPlaybackRate(playbackRate period.Frequency) + 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 period.Frequency) Filter +type Factory func(instrument frequency.Frequency) Filter diff --git a/format/it/filter/resonantfilter.go b/format/it/filter/resonantfilter.go index 73899ca..b04579b 100644 --- a/format/it/filter/resonantfilter.go +++ b/format/it/filter/resonantfilter.go @@ -4,9 +4,9 @@ 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" ) @@ -31,7 +31,7 @@ type ResonantFilter struct { f2 float64 fr float64 efr float64 - playbackRate period.Frequency + playbackRate frequency.Frequency } // NewResonantFilter creates a new resonant filter with the provided cutoff and resonance values @@ -53,7 +53,7 @@ func NewResonantFilter(cutoff uint8, resonance uint8, extendedFilterRange bool, return &rf } -func (f *ResonantFilter) SetPlaybackRate(playback period.Frequency) { +func (f *ResonantFilter) SetPlaybackRate(playback frequency.Frequency) { if f.playbackRate == playback { return } diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 8736612..37d6256 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -9,7 +9,7 @@ import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice/autovibrato" @@ -148,7 +148,7 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf pluginFilterFactory filter.Factory ) if inst.InitialFilterResonance != 0 { - channelFilterFactory = func(instrument period.Frequency) filter.Filter { + channelFilterFactory = func(instrument frequency.Frequency) filter.Filter { return itfilter.NewResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, convSettings.extendedFilterRange, convSettings.useHighPassFilter) } } @@ -459,7 +459,7 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument[itVolume.FineV ii.Static.Filename = si.Header.GetFilename() ii.Static.Name = si.Header.GetName() - ii.SampleRate = period.Frequency(si.Header.C5Speed) + ii.SampleRate = frequency.Frequency(si.Header.C5Speed) ii.Static.AutoVibrato = autovibrato.AutoVibratoSettings{ Enabled: (si.Header.VibratoDepth != 0 && si.Header.VibratoSpeed != 0 && si.Header.VibratoSweep != 0), Sweep: 255, diff --git a/format/it/period/util.go b/format/it/period/util.go index 2904a3e..bc5d2c3 100644 --- a/format/it/period/util.go +++ b/format/it/period/util.go @@ -4,6 +4,7 @@ import ( "math" "github.com/gotracker/playback/format/it/system" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) @@ -12,7 +13,7 @@ var DefaultC5SampleRate = system.DefaultC5SampleRate 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[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c5SampleRate period.Frequency) TPeriod { +func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c5SampleRate frequency.Frequency) TPeriod { if semi == note.UnchangedSemitone { panic("how?") } @@ -30,7 +31,7 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet } if c5SampleRate == 0 { - c5SampleRate = period.Frequency(system.DefaultC5SampleRate) + c5SampleRate = frequency.Frequency(system.DefaultC5SampleRate) } if ft != 0 { @@ -45,7 +46,7 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet } // CalcFinetuneC5SampleRate calculates a new frequency after a finetune adjustment -func CalcFinetuneC5SampleRate[TPeriod period.Period](converter period.PeriodConverter[TPeriod], c5SampleRate period.Frequency, finetune note.Finetune) period.Frequency { +func CalcFinetuneC5SampleRate[TPeriod period.Period](converter period.PeriodConverter[TPeriod], c5SampleRate frequency.Frequency, finetune note.Finetune) frequency.Frequency { if finetune == 0 { return c5SampleRate } @@ -56,13 +57,13 @@ func CalcFinetuneC5SampleRate[TPeriod period.Period](converter period.PeriodConv } // FrequencyFromSemitone returns the frequency from the semitone (and c5 sample rate) -func FrequencyFromSemitone[TPeriod period.Period](converter period.PeriodConverter[TPeriod], semitone note.Semitone, c5SampleRate period.Frequency) float32 { +func FrequencyFromSemitone[TPeriod period.Period](converter period.PeriodConverter[TPeriod], semitone note.Semitone, c5SampleRate frequency.Frequency) float32 { p := CalcSemitonePeriod[TPeriod](semitone, 0, c5SampleRate) return float32(converter.GetFrequency(p)) } // ToAmigaPeriod calculates an amiga period for a linear finetune period -func ToAmigaPeriod(finetunes note.Finetune, c5SampleRate period.Frequency) period.Amiga { +func ToAmigaPeriod(finetunes note.Finetune, c5SampleRate frequency.Frequency) period.Amiga { if finetunes < 0 { finetunes = 0 } diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go index d0df42b..916eee7 100644 --- a/format/it/settings/machine.go +++ b/format/it/settings/machine.go @@ -7,6 +7,7 @@ import ( 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/frequency" "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" @@ -51,7 +52,7 @@ var ( func filterFactory(name string) (settings.FilterFactoryFunc, error) { switch name { case "amigalpf": - return func(instrument period.Frequency) (filter.Filter, error) { + return func(instrument frequency.Frequency) (filter.Filter, error) { lpf := filter.NewAmigaLPF(instrument) return lpf, nil }, nil diff --git a/format/it/system/system.go b/format/it/system/system.go index f44ad27..58880dc 100644 --- a/format/it/system/system.go +++ b/format/it/system/system.go @@ -1,6 +1,7 @@ package system import ( + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/system" ) @@ -11,7 +12,7 @@ const ( C5Period = 428 // ITBaseClock is the base clock speed of IT files - ITBaseClock system.Frequency = DefaultC5SampleRate * C5Period + ITBaseClock frequency.Frequency = DefaultC5SampleRate * C5Period NotesPerOctave = 12 SlideFinesPerSemitone = 4 diff --git a/format/it/voice/sampler.go b/format/it/voice/sampler.go index 81a5bff..f3a91ce 100644 --- a/format/it/voice/sampler.go +++ b/format/it/voice/sampler.go @@ -3,7 +3,7 @@ package voice import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) type voicerPos interface { @@ -46,6 +46,6 @@ func (v *itVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { return wet } -func (v itVoice[TPeriod]) GetSampleRate() period.Frequency { +func (v itVoice[TPeriod]) GetSampleRate() frequency.Frequency { return v.config.SampleRate } diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index 6550d89..1dc29d7 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -15,9 +15,9 @@ import ( "github.com/gotracker/playback/format/s3m/pattern" 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/player/feature" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice/fadeout" @@ -54,7 +54,7 @@ func scrsNoneToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSNoneHeader) (* Name: si.GetSampleName(), Volume: s3mVolume.Volume(si.Volume), }, - SampleRate: period.Frequency(si.C2Spd.Lo), + SampleRate: frequency.Frequency(si.C2Spd.Lo), } return &sample, nil } @@ -66,10 +66,10 @@ func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHead Name: si.GetSampleName(), Volume: s3mVolume.Volume(si.Volume), }, - SampleRate: period.Frequency(si.C2Spd.Lo), + SampleRate: frequency.Frequency(si.C2Spd.Lo), } if sample.SampleRate == 0 { - sample.SampleRate = period.Frequency(s3mfile.DefaultC2Spd) + sample.SampleRate = frequency.Frequency(s3mfile.DefaultC2Spd) } instLen := int(si.Length.Lo) @@ -125,7 +125,7 @@ func scrsOpl2ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSAdlibHeader) ( Name: si.GetSampleName(), Volume: s3mVolume.Volume(si.Volume), }, - SampleRate: period.Frequency(si.C2Spd.Lo), + SampleRate: frequency.Frequency(si.C2Spd.Lo), } idata := instrument.OPL2{ diff --git a/format/s3m/period/util.go b/format/s3m/period/util.go index e8ef399..3312bc6 100644 --- a/format/s3m/period/util.go +++ b/format/s3m/period/util.go @@ -4,6 +4,7 @@ import ( "math" "github.com/gotracker/playback/format/s3m/system" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) @@ -12,7 +13,7 @@ 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, c4SampleRate period.Frequency) period.Amiga { +func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c4SampleRate frequency.Frequency) period.Amiga { if semi == note.UnchangedSemitone { panic("how?") } @@ -26,7 +27,7 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c4SampleRate perio } if c4SampleRate == 0 { - c4SampleRate = period.Frequency(system.DefaultC4SampleRate) + c4SampleRate = frequency.Frequency(system.DefaultC4SampleRate) } if ft != 0 { @@ -37,7 +38,7 @@ func CalcSemitonePeriod(semi note.Semitone, ft note.Finetune, c4SampleRate perio } // CalcFinetuneC4SampleRate calculates a new frequency after a finetune adjustment -func CalcFinetuneC4SampleRate(c4SampleRate period.Frequency, finetune note.Finetune) period.Frequency { +func CalcFinetuneC4SampleRate(c4SampleRate frequency.Frequency, finetune note.Finetune) frequency.Frequency { if finetune == 0 { return c4SampleRate } @@ -48,13 +49,13 @@ func CalcFinetuneC4SampleRate(c4SampleRate period.Frequency, finetune note.Finet } // FrequencyFromSemitone returns the frequency from the semitone (and c4 sample rate) -func FrequencyFromSemitone(semitone note.Semitone, c4SampleRate period.Frequency) float32 { +func FrequencyFromSemitone(semitone note.Semitone, c4SampleRate frequency.Frequency) float32 { p := CalcSemitonePeriod(semitone, 0, c4SampleRate) return float32(p.GetFrequency()) } // ToAmigaPeriod calculates an amiga period for a linear finetune period -func ToAmigaPeriod(finetunes note.Finetune, c4SampleRate period.Frequency) period.Amiga { +func ToAmigaPeriod(finetunes note.Finetune, c4SampleRate frequency.Frequency) period.Amiga { if finetunes < 0 { finetunes = 0 } diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go index 2d9f38e..1e47ef9 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -7,6 +7,7 @@ import ( 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/frequency" "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" @@ -32,7 +33,7 @@ var ( func filterFactory(name string) (settings.FilterFactoryFunc, error) { switch name { case "amigalpf": - return func(instrument period.Frequency) (filter.Filter, error) { + return func(instrument frequency.Frequency) (filter.Filter, error) { lpf := filter.NewAmigaLPF(instrument) return lpf, nil }, nil diff --git a/format/s3m/system/system.go b/format/s3m/system/system.go index 4e03878..cfe8bef 100644 --- a/format/s3m/system/system.go +++ b/format/s3m/system/system.go @@ -3,7 +3,7 @@ package system import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/system" ) @@ -12,10 +12,10 @@ const ( c2Period = 1712 // DefaultC4SampleRate is the default c4 sample rate for S3M samples - DefaultC4SampleRate = system.Frequency(s3mfile.DefaultC2Spd) + DefaultC4SampleRate = frequency.Frequency(s3mfile.DefaultC2Spd) // S3MBaseClock is the base clock speed of S3M files - S3MBaseClock period.Frequency = DefaultC4SampleRate * c2Period + S3MBaseClock frequency.Frequency = DefaultC4SampleRate * c2Period NotesPerOctave = 12 SlideFinesPerSemitone = 4 diff --git a/format/s3m/voice/sampler.go b/format/s3m/voice/sampler.go index 2b46cfd..96fa508 100644 --- a/format/s3m/voice/sampler.go +++ b/format/s3m/voice/sampler.go @@ -3,7 +3,7 @@ package voice import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) type voicerPos interface { @@ -46,6 +46,6 @@ func (v *s3mVoice) GetSample(pos sampling.Pos) volume.Matrix { return wet } -func (v s3mVoice) GetSampleRate() period.Frequency { +func (v s3mVoice) GetSampleRate() frequency.Frequency { return v.config.SampleRate } diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index d8d6179..2a40caf 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -16,6 +16,7 @@ import ( xmPeriod "github.com/gotracker/playback/format/xm/period" 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" @@ -85,7 +86,7 @@ func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlid Factory: oscillator.NewProtrackerOscillator, }, }, - SampleRate: period.Frequency(0), // uses si.Finetune, below + SampleRate: frequency.Frequency(0), // uses si.Finetune, below } if !linearFrequencySlides { diff --git a/format/xm/period/util.go b/format/xm/period/util.go index 1e17661..e09214f 100644 --- a/format/xm/period/util.go +++ b/format/xm/period/util.go @@ -4,13 +4,13 @@ import ( "math" xmSystem "github.com/gotracker/playback/format/xm/system" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/system" ) // CalcSemitonePeriod calculates the semitone period for it notes -func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c4SampleRate period.Frequency) TPeriod { +func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c4SampleRate frequency.Frequency) TPeriod { if semi == note.UnchangedSemitone { panic("how?") } @@ -27,7 +27,7 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet } if c4SampleRate == 0 { - c4SampleRate = period.Frequency(xmSystem.DefaultC4SampleRate) + c4SampleRate = frequency.Frequency(xmSystem.DefaultC4SampleRate) } per := max(float64(xmSystem.C4Note)*xmSystem.SlideFinesPerNote-float64(ft)/2, 0) @@ -53,7 +53,7 @@ func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finet } // CalcFinetuneC4SampleRate calculates a new c4 sample rate after a finetune adjustment -func CalcFinetuneC4SampleRate(c4SampleRate period.Frequency, st note.Semitone, finetune note.Finetune) period.Frequency { +func CalcFinetuneC4SampleRate(c4SampleRate frequency.Frequency, st note.Semitone, finetune note.Finetune) frequency.Frequency { if finetune == 0 && st == xmSystem.C4Note { return c4SampleRate } @@ -64,11 +64,11 @@ func CalcFinetuneC4SampleRate(c4SampleRate period.Frequency, st note.Semitone, f freq := math.Floor(float64(c4SampleRate) * pow) - return system.Frequency(freq) + return frequency.Frequency(freq) } // FrequencyFromSemitone returns the frequency from the semitone (and c4 sample rate) -func FrequencyFromSemitone[TPeriod period.Period](converter period.PeriodConverter[TPeriod], semitone note.Semitone, c4SampleRate period.Frequency) float32 { +func FrequencyFromSemitone[TPeriod period.Period](converter period.PeriodConverter[TPeriod], semitone note.Semitone, c4SampleRate frequency.Frequency) float32 { p := CalcSemitonePeriod[TPeriod](semitone, 0, c4SampleRate) return float32(converter.GetFrequency(p)) } diff --git a/format/xm/settings/machine.go b/format/xm/settings/machine.go index 07a45fd..467bec7 100644 --- a/format/xm/settings/machine.go +++ b/format/xm/settings/machine.go @@ -7,6 +7,7 @@ import ( 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/frequency" "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" @@ -50,7 +51,7 @@ var ( func filterFactory(name string) (settings.FilterFactoryFunc, error) { switch name { case "amigalpf": - return func(instrument period.Frequency) (filter.Filter, error) { + return func(instrument frequency.Frequency) (filter.Filter, error) { lpf := filter.NewAmigaLPF(instrument) return lpf, nil }, nil diff --git a/format/xm/system/system.go b/format/xm/system/system.go index eff5659..924ba9e 100644 --- a/format/xm/system/system.go +++ b/format/xm/system/system.go @@ -1,6 +1,7 @@ package system import ( + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/system" ) @@ -16,7 +17,7 @@ const ( floatDefaultC4SampleRate = float32(DefaultC4SampleRate) // XMBaseClock is the base clock speed of XM files - XMBaseClock system.Frequency = DefaultC4SampleRate * C4Period + XMBaseClock frequency.Frequency = DefaultC4SampleRate * C4Period NotesPerOctave = 12 SlideFinesPerSemitone = 4 diff --git a/format/xm/voice/sampler.go b/format/xm/voice/sampler.go index 3e960a7..7eea553 100644 --- a/format/xm/voice/sampler.go +++ b/format/xm/voice/sampler.go @@ -3,7 +3,7 @@ package voice import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) type voicerPos interface { @@ -46,6 +46,6 @@ func (v *xmVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { return wet } -func (v xmVoice[TPeriod]) GetSampleRate() period.Frequency { +func (v xmVoice[TPeriod]) GetSampleRate() frequency.Frequency { return v.config.SampleRate } 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/instrument/instrument.go b/instrument/instrument.go index 56e917c..d4f8c0a 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -6,16 +6,16 @@ import ( "github.com/heucuva/optional" "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/types" ) type InstrumentIntf interface { IsInvalid() bool - GetSampleRate() period.Frequency - SetSampleRate(sampleRate period.Frequency) + GetSampleRate() frequency.Frequency + SetSampleRate(sampleRate frequency.Frequency) GetLength() sampling.Pos SetFinetune(ft note.Finetune) GetFinetune() note.Finetune @@ -52,7 +52,7 @@ type StaticValues[TMixingVolume, TVolume types.Volume, TPanning types.Panning] s type Instrument[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { Static StaticValues[TMixingVolume, TVolume, TPanning] Inst Data - SampleRate period.Frequency + SampleRate frequency.Frequency Finetune optional.Value[note.Finetune] } @@ -63,12 +63,12 @@ func (inst Instrument[TMixingVolume, TVolume, TPanning]) IsInvalid() bool { // GetSampleRate returns the central-note sample rate value for the instrument // This may get mutated if a finetune effect is processed -func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetSampleRate() period.Frequency { +func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetSampleRate() frequency.Frequency { return inst.SampleRate } // SetSampleRate sets the central-note sample rate value for the instrument -func (inst *Instrument[TMixingVolume, TVolume, TPanning]) SetSampleRate(sampleRate period.Frequency) { +func (inst *Instrument[TMixingVolume, TVolume, TPanning]) SetSampleRate(sampleRate frequency.Frequency) { inst.SampleRate = sampleRate } diff --git a/period/amiga.go b/period/amiga.go index d7d2d19..35c6fb6 100644 --- a/period/amiga.go +++ b/period/amiga.go @@ -3,6 +3,7 @@ package period import ( "fmt" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/util" "github.com/heucuva/comparison" ) @@ -78,7 +79,7 @@ func (p Amiga) Lerp(t float64, rhs Amiga) Amiga { } // GetFrequency returns the frequency defined by the period -func (p Amiga) GetFrequency() Frequency { +func (p Amiga) GetFrequency() frequency.Frequency { panic("unimplemented") // must be implemented by format } diff --git a/period/amigaconverter.go b/period/amigaconverter.go index 298ea72..04a952b 100644 --- a/period/amigaconverter.go +++ b/period/amigaconverter.go @@ -3,6 +3,7 @@ package period import ( "errors" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/system" ) @@ -16,11 +17,11 @@ type AmigaConverter struct { var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) // GetFrequency returns the frequency defined by the period -func (c AmigaConverter) GetFrequency(p Amiga) Frequency { +func (c AmigaConverter) GetFrequency(p Amiga) frequency.Frequency { if p.IsInvalid() { return 0 } - return c.System.GetBaseClock() / Frequency(p) + return c.System.GetBaseClock() / frequency.Frequency(p) } // GetSamplerAdd returns the number of samples to advance an instrument by given the period diff --git a/period/frequency.go b/period/frequency.go deleted file mode 100755 index 8b1c5b9..0000000 --- a/period/frequency.go +++ /dev/null @@ -1,8 +0,0 @@ -package period - -import ( - "github.com/gotracker/playback/system" -) - -// Frequency is a frequency value, in Hertz (Hz) -type Frequency = system.Frequency diff --git a/period/linearconverter.go b/period/linearconverter.go index fb2426f..8d6e281 100644 --- a/period/linearconverter.go +++ b/period/linearconverter.go @@ -4,6 +4,7 @@ import ( "errors" "math" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/system" ) @@ -22,12 +23,12 @@ func (c LinearConverter) GetSamplerAdd(p Linear, samplerSpeed float64) float64 { } // GetFrequency returns the frequency defined by the period -func (c LinearConverter) GetFrequency(p Linear) Frequency { +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(math.Pow(2.0, pft)) + f := frequency.Frequency(math.Pow(2.0, pft)) return f } diff --git a/period/periodconverter.go b/period/periodconverter.go index 559a2b4..8ed2baf 100644 --- a/period/periodconverter.go +++ b/period/periodconverter.go @@ -1,6 +1,7 @@ package period import ( + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" ) @@ -15,5 +16,5 @@ type PeriodConverter[TPeriod Period] interface { PortaDown(TPeriod, Delta) (TPeriod, error) PortaUp(TPeriod, Delta) (TPeriod, error) GetSamplerAdd(TPeriod, float64) float64 - GetFrequency(TPeriod) Frequency + GetFrequency(TPeriod) frequency.Frequency } diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index 7259567..f60e3bc 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -2,15 +2,15 @@ package machine import ( "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" "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" "github.com/gotracker/playback/voice/types" ) -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error { na, set := c.target.ActionTick.Get() if !set { // assume continue @@ -103,7 +103,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupInstrument(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupInstrument(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error { inst := c.target.Inst prevInst := c.prev.Inst if inst != nil { @@ -131,7 +131,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupPCM(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.PCM[TMixingVolume, TVolume, TPanning], outputRate system.Frequency) error { +func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupPCM(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.PCM[TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error { var voiceFilter filter.Filter if factory := inst.GetFilterFactory(); factory != nil { voiceFilter = factory(inst.SampleRate) diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 73440a9..4561d5a 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -6,12 +6,11 @@ import ( "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/period" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" - "github.com/gotracker/playback/system" "github.com/gotracker/playback/voice/mixer" ) @@ -43,8 +42,8 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend details := mixer.Details{ Mix: s.Mixer(), Panmixer: s.GetPanMixer(), - SampleRate: system.Frequency(s.SampleRate), - SamplerSpeed: sys.GetSamplerSpeed(period.Frequency(s.SampleRate)), + SampleRate: frequency.Frequency(s.SampleRate), + SamplerSpeed: sys.GetSamplerSpeed(frequency.Frequency(s.SampleRate)), Samples: premix.SamplesLen, Duration: tickDuration, } diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index 9e5bdcf..c9f7988 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -3,16 +3,16 @@ package machine import ( "errors" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/system" ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(s *sampler.Sampler) (*output.PremixData, error) { for i := range m.channels { - if err := m.channels[i].DoNoteAction(index.Channel(i), m, system.Frequency(s.SampleRate)); err != nil { + if err := m.channels[i].DoNoteAction(index.Channel(i), m, frequency.Frequency(s.SampleRate)); err != nil { return nil, err } } diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go index 395d0aa..b428cf7 100644 --- a/player/machine/settings/machinesettings.go +++ b/player/machine/settings/machinesettings.go @@ -2,7 +2,7 @@ package settings import ( "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/oscillator" @@ -14,7 +14,7 @@ type ( Panning = voice.Panning ) -type FilterFactoryFunc func(instrument period.Frequency) (filter.Filter, error) +type FilterFactoryFunc func(instrument frequency.Frequency) (filter.Filter, error) type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { PeriodConverter song.PeriodCalculator[TPeriod] diff --git a/player/render/channel.go b/player/render/channel.go index b690841..fc1b673 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -7,12 +7,11 @@ import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice" - channelfilter "github.com/gotracker/playback/voice/filter" "github.com/gotracker/playback/voice/mixer" ) type ChannelIntf interface { - channelfilter.Applier + ApplyFilter(dry volume.Matrix) volume.Matrix GetPremixVolume() volume.Volume } diff --git a/player/sampler/sampler.go b/player/sampler/sampler.go index fe07fdf..d31d555 100644 --- a/player/sampler/sampler.go +++ b/player/sampler/sampler.go @@ -2,13 +2,13 @@ package sampler import ( "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) // Sampler is a container of sampler/mixer settings type Sampler struct { SampleRate int - BaseClockRate period.Frequency + BaseClockRate frequency.Frequency mixer mixing.Mixer } diff --git a/player/tracker.go b/player/tracker.go index 0c07850..93df1d4 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -4,8 +4,8 @@ import ( "errors" "time" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/output" - "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/player/machine/settings" @@ -64,8 +64,8 @@ func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) } // GetSampleRate returns the sample rate of the sampler -func (t *Tracker) GetSampleRate() period.Frequency { - return period.Frequency(t.s.SampleRate) +func (t *Tracker) GetSampleRate() frequency.Frequency { + return frequency.Frequency(t.s.SampleRate) } // SetupSampler configures the internal sampler diff --git a/song/periodcalculator.go b/song/periodcalculator.go index 097f296..e8a9196 100644 --- a/song/periodcalculator.go +++ b/song/periodcalculator.go @@ -1,6 +1,7 @@ package song import ( + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" ) @@ -20,5 +21,5 @@ type PeriodCalculator[TPeriod period.Period] interface { PortaDown(TPeriod, period.Delta) (TPeriod, error) PortaUp(TPeriod, period.Delta) (TPeriod, error) GetSamplerAdd(TPeriod, float64) float64 - GetFrequency(TPeriod) period.Frequency + GetFrequency(TPeriod) frequency.Frequency } diff --git a/system/clockedsystem.go b/system/clockedsystem.go index 05dbdba..aaa7b21 100644 --- a/system/clockedsystem.go +++ b/system/clockedsystem.go @@ -1,27 +1,28 @@ package system import ( + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" ) type ClockableSystem interface { System - GetBaseClock() Frequency + GetBaseClock() frequency.Frequency GetBaseFinetunes() note.Finetune GetFinetunesPerOctave() note.Finetune GetFinetunesPerSemitone() note.Finetune GetSemitonePeriod(note.Key) (float32, bool) - GetSamplerSpeed(sampleRate Frequency) float32 + GetSamplerSpeed(sampleRate frequency.Frequency) float32 } type ClockedSystem struct { MaxPastNotesPerChannel int - BaseClock Frequency + BaseClock frequency.Frequency BaseFinetunes note.Finetune FinetunesPerOctave note.Finetune FinetunesPerNote note.Finetune - CommonRate Frequency + CommonRate frequency.Frequency SemitonePeriods [note.NumKeys]float32 } @@ -31,7 +32,7 @@ func (s ClockedSystem) GetMaxPastNotesPerChannel() int { return s.MaxPastNotesPerChannel } -func (s ClockedSystem) GetBaseClock() Frequency { +func (s ClockedSystem) GetBaseClock() frequency.Frequency { return s.BaseClock } @@ -47,11 +48,11 @@ func (s ClockedSystem) GetFinetunesPerSemitone() note.Finetune { return s.FinetunesPerNote } -func (s ClockedSystem) GetCommonRate() Frequency { +func (s ClockedSystem) GetCommonRate() frequency.Frequency { return s.CommonRate } -func (s ClockedSystem) GetSamplerSpeed(sampleRate Frequency) float32 { +func (s ClockedSystem) GetSamplerSpeed(sampleRate frequency.Frequency) float32 { if sampleRate == 0 { panic("sampleRate is 0") } diff --git a/system/frequency.go b/system/frequency.go deleted file mode 100644 index 31774f2..0000000 --- a/system/frequency.go +++ /dev/null @@ -1,4 +0,0 @@ -package system - -// Frequency is a frequency value, in Hertz (Hz) -type Frequency float64 diff --git a/system/system.go b/system/system.go index 296116f..2760304 100644 --- a/system/system.go +++ b/system/system.go @@ -1,7 +1,9 @@ package system +import "github.com/gotracker/playback/frequency" + type System interface { GetMaxPastNotesPerChannel() int - GetCommonRate() Frequency - GetSamplerSpeed(sampleRate Frequency) float32 + GetCommonRate() frequency.Frequency + GetSamplerSpeed(sampleRate frequency.Frequency) float32 } diff --git a/voice/component/opl2.go b/voice/component/opl2.go index 15145c2..2eec771 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -4,6 +4,7 @@ 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/player/render" @@ -32,14 +33,14 @@ type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { chip render.OPL2Chip channel int reg OPL2Registers - baseFreq period.Frequency + baseFreq frequency.Frequency periodConverter period.PeriodConverter[TPeriod] defaultVolume TVolume keyOn bool } // Setup sets up the OPL2 component -func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq period.Frequency, defaultVolume TVolume) { +func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq frequency.Frequency, defaultVolume TVolume) { o.chip = chip o.channel = channel o.reg = reg @@ -166,7 +167,7 @@ func (o *OPL2[TPeriod, TMixingVolume, TVolume]) calc40(reg40 uint8, vol volume.V return result } -func (o *OPL2[TPeriod, TMixingVolume, TVolume]) periodToFreqBlock(p TPeriod, baseFreq period.Frequency) (uint16, uint8) { +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 diff --git a/voice/config.go b/voice/config.go index 50b503f..88346ba 100644 --- a/voice/config.go +++ b/voice/config.go @@ -2,8 +2,8 @@ package voice import ( "github.com/gotracker/playback/filter" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/envelope" "github.com/gotracker/playback/voice/fadeout" @@ -18,7 +18,7 @@ type ( ) type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { - SampleRate period.Frequency + SampleRate frequency.Frequency AutoVibrato autovibrato.AutoVibratoSettings Data instrument.Data VoiceFilter filter.Filter diff --git a/voice/mixer/details.go b/voice/mixer/details.go index 60ef752..0192330 100644 --- a/voice/mixer/details.go +++ b/voice/mixer/details.go @@ -4,13 +4,13 @@ import ( "time" "github.com/gotracker/gomixing/mixing" - "github.com/gotracker/playback/period" + "github.com/gotracker/playback/frequency" ) type Details struct { Mix *mixing.Mixer Panmixer mixing.PanMixer - SampleRate period.Frequency + SampleRate frequency.Frequency SamplerSpeed float32 Samples int Duration time.Duration diff --git a/voice/mixer/output.go b/voice/mixer/output.go index 15c7d5c..d47ae9b 100644 --- a/voice/mixer/output.go +++ b/voice/mixer/output.go @@ -3,13 +3,12 @@ package mixer import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/voice/filter" ) // Output applies a filter to a sample stream type Output struct { Input sampling.SampleStream - Output filter.Applier + Output ApplyFilter } // GetSample operates the filter @@ -18,3 +17,7 @@ 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/render.go b/voice/render.go index 4392068..5a3ba9d 100644 --- a/voice/render.go +++ b/voice/render.go @@ -5,11 +5,10 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/filter" "github.com/gotracker/playback/voice/mixer" ) -func RenderAndAdvance[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details, out filter.Applier) (*mixing.Data, error) { +func RenderAndAdvance[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 } diff --git a/voice/voice.go b/voice/voice.go index 09d8d74..17934d2 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -4,6 +4,7 @@ import ( "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" @@ -31,7 +32,7 @@ type Voice interface { // General Parameters IsDone() bool - GetSampleRate() period.Frequency + GetSampleRate() frequency.Frequency } type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { From f5de2c9101fd6e68c9ffa264d92c517557c71a43 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 14 Jan 2024 17:26:34 -0800 Subject: [PATCH 39/63] consistent package imports --- format/it/settings/machine.go | 16 ++++++++-------- format/s3m/settings/machine.go | 16 ++++++++-------- format/xm/settings/machine.go | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go index 916eee7..4074df7 100644 --- a/format/it/settings/machine.go +++ b/format/it/settings/machine.go @@ -8,11 +8,11 @@ import ( itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/frequency" - "github.com/gotracker/playback/oscillator" + oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/player/machine/settings" - voiceOscillator "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/oscillator" ) func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { @@ -62,16 +62,16 @@ func filterFactory(name string) (settings.FilterFactoryFunc, error) { } } -func vibratoFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewImpulseTrackerOscillator(4), nil +func vibratoFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(4), nil } -func tremoloFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewImpulseTrackerOscillator(4), nil +func tremoloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(4), nil } -func panbrelloFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewImpulseTrackerOscillator(1), nil +func panbrelloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(1), nil } func init() { diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go index 1e47ef9..7d9433c 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -8,10 +8,10 @@ import ( s3mPeriod "github.com/gotracker/playback/format/s3m/period" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/frequency" - "github.com/gotracker/playback/oscillator" + oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" - voiceOscillator "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/oscillator" ) func GetMachineSettings() *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { @@ -43,14 +43,14 @@ func filterFactory(name string) (settings.FilterFactoryFunc, error) { } } -func vibratoFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewProtrackerOscillator(), nil +func vibratoFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil } -func tremoloFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewProtrackerOscillator(), nil +func tremoloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil } -func panbrelloFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewProtrackerOscillator(), nil +func panbrelloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewProtrackerOscillator(), nil } diff --git a/format/xm/settings/machine.go b/format/xm/settings/machine.go index 467bec7..6618ffd 100644 --- a/format/xm/settings/machine.go +++ b/format/xm/settings/machine.go @@ -8,10 +8,10 @@ import ( xmPeriod "github.com/gotracker/playback/format/xm/period" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/frequency" - "github.com/gotracker/playback/oscillator" + oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" - voiceOscillator "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/oscillator" ) func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { @@ -61,14 +61,14 @@ func filterFactory(name string) (settings.FilterFactoryFunc, error) { } } -func vibratoFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewImpulseTrackerOscillator(4), nil +func vibratoFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(4), nil } -func tremoloFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewImpulseTrackerOscillator(4), nil +func tremoloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(4), nil } -func panbrelloFactory() (voiceOscillator.Oscillator, error) { - return oscillator.NewImpulseTrackerOscillator(1), nil +func panbrelloFactory() (oscillator.Oscillator, error) { + return oscillatorImpl.NewImpulseTrackerOscillator(1), nil } From cdc86faa1aad43279fd1eee1c0eff49fc9584200 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 14 Jan 2024 18:56:25 -0800 Subject: [PATCH 40/63] make format voices understand instruments instead of generic configs - also fix mixing overload on virtual outputs --- filter/amigafilter.go | 8 +- filter/echofilter.go | 4 +- .../it_resonantfilter.go | 15 +-- format/it/load/instrument.go | 8 +- format/it/voice/modulator_amp.go | 9 ++ format/it/voice/sampler.go | 6 +- format/it/voice/voice.go | 120 ++++++++++++------ format/s3m/voice/sampler.go | 6 +- format/s3m/voice/voice.go | 52 ++++++-- format/xm/voice/sampler.go | 6 +- format/xm/voice/voice.go | 96 +++++++++----- instrument/pcm.go | 5 +- player/machine/channel_noteaction.go | 74 ++--------- player/machine/machine_render.go | 38 ++++-- player/machine/pastnote.go | 2 +- player/render/channel.go | 4 +- voice/component/envelope_filter.go | 8 +- voice/component/envelope_pitch.go | 8 +- voice/component/modulator_amp.go | 24 +++- voice/config.go | 4 +- {filter => voice/types}/pitchfiltervalue.go | 2 +- voice/voice.go | 8 +- 22 files changed, 294 insertions(+), 213 deletions(-) rename format/it/filter/resonantfilter.go => filter/it_resonantfilter.go (89%) rename {filter => voice/types}/pitchfiltervalue.go (65%) diff --git a/filter/amigafilter.go b/filter/amigafilter.go index dfad1b3..c8e6895 100644 --- a/filter/amigafilter.go +++ b/filter/amigafilter.go @@ -7,14 +7,14 @@ import ( "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 @@ -31,7 +31,7 @@ func NewAmigaLPF(instrument frequency.Frequency) *AmigaLPF { 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] } @@ -76,7 +76,7 @@ func (f *AmigaLPF) 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, amigaLPFChannelData{}) } c := &f.channels[i] diff --git a/filter/echofilter.go b/filter/echofilter.go index 2382456..27b7eb7 100644 --- a/filter/echofilter.go +++ b/filter/echofilter.go @@ -29,7 +29,7 @@ func (e *EchoFilterFactory) Factory() Factory { } } -type delayInfo struct { +type echoFilterDelayInfo struct { buf []volume.Volume delay int } @@ -40,7 +40,7 @@ type EchoFilter struct { EchoFilterSettings initialFeedback volume.Volume writePos int - delay [2]delayInfo // L,R + delay [2]echoFilterDelayInfo // L,R playbackRate frequency.Frequency } diff --git a/format/it/filter/resonantfilter.go b/filter/it_resonantfilter.go similarity index 89% rename from format/it/filter/resonantfilter.go rename to filter/it_resonantfilter.go index b04579b..e18cef8 100644 --- a/format/it/filter/resonantfilter.go +++ b/filter/it_resonantfilter.go @@ -5,19 +5,18 @@ import ( "github.com/gotracker/gomixing/volume" - "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 @@ -34,8 +33,8 @@ type ResonantFilter struct { playbackRate frequency.Frequency } -// NewResonantFilter creates a new resonant filter with the provided cutoff and resonance values -func NewResonantFilter(cutoff uint8, resonance uint8, extendedFilterRange bool, highpass bool) filter.Filter { +// 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, @@ -73,9 +72,9 @@ func (f *ResonantFilter) SetPlaybackRate(playback frequency.Frequency) { f.recalculate(c) } -func (f *ResonantFilter) Clone() filter.Filter { +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] } @@ -91,7 +90,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] diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 37d6256..cb9738c 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -18,10 +18,10 @@ import ( "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback/voice/pcm" + "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" @@ -149,7 +149,7 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf ) if inst.InitialFilterResonance != 0 { channelFilterFactory = func(instrument frequency.Frequency) filter.Filter { - return itfilter.NewResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, convSettings.extendedFilterRange, convSettings.useHighPassFilter) + return filter.NewITResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, convSettings.extendedFilterRange, convSettings.useHighPassFilter) } } @@ -236,8 +236,8 @@ func convertPanEnvValue(v int8) itPanning.Panning { return itPanning.Panning(int(v) + 128) } -func convertPitchEnvValue(v int8) filter.PitchFiltValue { - return filter.PitchFiltValue(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 { diff --git a/format/it/voice/modulator_amp.go b/format/it/voice/modulator_amp.go index e793b35..a778d8d 100644 --- a/format/it/voice/modulator_amp.go +++ b/format/it/voice/modulator_amp.go @@ -4,6 +4,7 @@ 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 == @@ -26,6 +27,14 @@ func (v itVoice[TPeriod]) GetMixingVolume() itVolume.FineVolume { return v.amp.GetMixingVolume() } +func (v *itVoice[TPeriod]) SetMixingVolumeOverride(mvo optional.Value[itVolume.FineVolume]) { + 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) { if vol.IsUseInstrumentVol() { vol = v.voicer.GetDefaultVolume() diff --git a/format/it/voice/sampler.go b/format/it/voice/sampler.go index f3a91ce..32b0281 100644 --- a/format/it/voice/sampler.go +++ b/format/it/voice/sampler.go @@ -40,12 +40,12 @@ func (v *itVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { vol := v.GetFinalVolume() wet := samp.Apply(vol) - if v.config.VoiceFilter != nil { - wet = v.config.VoiceFilter.Filter(wet) + if v.voiceFilter != nil { + wet = v.voiceFilter.Filter(wet) } return wet } func (v itVoice[TPeriod]) GetSampleRate() frequency.Frequency { - return v.config.SampleRate + return v.inst.SampleRate } diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 7b1f39d..b9765ac 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -1,10 +1,16 @@ package voice import ( + "errors" + "fmt" + "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/filter" 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/component" @@ -18,7 +24,8 @@ type Period interface { } type itVoice[TPeriod Period] struct { - config voice.InstrumentConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + inst *instrument.Instrument[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 @@ -38,6 +45,7 @@ type itVoice[TPeriod Period] struct { panEnv component.PanEnvelope[itPanning.Panning] filterEnv component.FilterEnvelope vol0Opt component.Vol0Optimization + voiceFilter filter.Filter // finals finalVol volume.Volume @@ -117,7 +125,11 @@ func (v *itVoice[TPeriod]) doRelease() { if v.voicer != nil { v.voicer.Release() } - v.updateFinal() + if v.background && !v.volEnv.CanLoop() { + v.KeyModulator.Fadeout() // triggers updateFinal + } else { + v.updateFinal() + } } func (v *itVoice[TPeriod]) doFadeout() { @@ -143,52 +155,75 @@ func (v itVoice[TPeriod]) getFadeoutEnabled() bool { return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) } -func (v *itVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]) { - v.config = config - v.filterEnvActive = v.config.PitchFiltMode - v.fadeoutMode = v.config.FadeOut.Mode +func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning], outputRate frequency.Frequency) error { + v.inst = inst - v.fadeout.Setup(component.FadeoutModulatorSettings{ - Enabled: v.config.FadeOut.Mode != fadeout.ModeDisabled, - GetActive: v.getFadeoutEnabled, - Amount: config.FadeOut.Amount, - }) + 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.autoVibrato.Setup(config.AutoVibrato) + v.pitchEnv.Setup(component.EnvelopeSettings[int8, period.Delta]{ + Envelope: d.PitchFiltEnv, + }) - v.pitchPan.Setup(component.PitchPanModulatorSettings[itPanning.Panning]{ - PitchPanEnable: config.PitchPan.Enabled, - PitchPanCenter: config.PitchPan.Center, - PitchPanSeparation: config.PitchPan.Separation, - }) + v.panEnv.Setup(component.EnvelopeSettings[itPanning.Panning, itPanning.Panning]{ + Envelope: d.PanEnv, + }) + + v.filterEnv.Setup(component.EnvelopeSettings[int8, uint8]{ + Envelope: d.PitchFiltEnv, + }) + + v.amp.SetMixingVolumeOverride(d.MixingVolume) - volEnvSettings := component.EnvelopeSettings[itVolume.Volume, itVolume.Volume]{ - Envelope: config.VolEnv, + v.setupPCM(d.Sample, d.Loop, d.SustainLoop, itVolume.MaxItFineVolume, inst.GetDefaultVolume()) + + default: + return fmt.Errorf("unhandled instrument type: %T", inst) } - if config.VolEnvFinishFadesOut { - volEnvSettings.OnFinished = func(v voice.Voice) { - v.Fadeout() - } + if inst == nil { + return errors.New("instrument is nil") } - v.volEnv.Setup(volEnvSettings) - v.pitchEnv.Setup(component.EnvelopeSettings[int8, period.Delta]{ - Envelope: config.PitchFiltEnv, - }) - - v.panEnv.Setup(component.EnvelopeSettings[itPanning.Panning, itPanning.Panning]{ - Envelope: config.PanEnv, - }) + v.autoVibrato.Setup(inst.GetAutoVibrato()) - v.filterEnv.Setup(component.EnvelopeSettings[int8, uint8]{ - Envelope: config.PitchFiltEnv, - }) + if factory := inst.GetFilterFactory(); factory != nil { + v.voiceFilter = factory(inst.SampleRate) + v.voiceFilter.SetPlaybackRate(outputRate) + } else { + v.voiceFilter = nil + } - v.KeyModulator.Release() v.Reset() + return nil } func (v *itVoice[TPeriod]) Reset() { + v.KeyModulator.Release() v.amp.Reset() v.fadeout.Reset() v.freq.Reset() @@ -245,9 +280,9 @@ func (v *itVoice[TPeriod]) Advance() { } } - if v.config.VoiceFilter != nil && v.IsFilterEnvelopeEnabled() { + if v.voiceFilter != nil && v.IsFilterEnvelopeEnabled() { fval := v.GetCurrentFilterEnvelope() - v.config.VoiceFilter.UpdateEnv(fval) + v.voiceFilter.UpdateEnv(fval) } // has to be after the mod/env updates @@ -259,9 +294,10 @@ func (v *itVoice[TPeriod]) Advance() { v.updateFinal() } -func (v *itVoice[TPeriod]) Clone() voice.Voice { +func (v *itVoice[TPeriod]) Clone(background bool) voice.Voice { vv := itVoice[TPeriod]{ - config: v.config, + inst: v.inst, + background: background, pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, filterEnvActive: v.filterEnvActive, fadeoutMode: v.fadeoutMode, @@ -293,14 +329,14 @@ func (v *itVoice[TPeriod]) Clone() voice.Voice { vv.voicer = v.voicer.Clone() } - if v.config.VoiceFilter != nil { - vv.config.VoiceFilter = v.config.VoiceFilter.Clone() + if v.voiceFilter != nil { + vv.voiceFilter = v.voiceFilter.Clone() } return &vv } -func (v *itVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol itVolume.FineVolume, defVol itVolume.Volume) { +func (v *itVoice[TPeriod]) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol itVolume.FineVolume, defVol itVolume.Volume) { var s component.Sampler[TPeriod, itVolume.FineVolume, itVolume.Volume] s.Setup(component.SamplerSettings[TPeriod, itVolume.FineVolume, itVolume.Volume]{ Sample: samp, diff --git a/format/s3m/voice/sampler.go b/format/s3m/voice/sampler.go index 96fa508..d72bd4b 100644 --- a/format/s3m/voice/sampler.go +++ b/format/s3m/voice/sampler.go @@ -40,12 +40,12 @@ func (v *s3mVoice) GetSample(pos sampling.Pos) volume.Matrix { vol := v.GetFinalVolume() wet := dry.Apply(vol) - if v.config.VoiceFilter != nil { - wet = v.config.VoiceFilter.Filter(wet) + if v.voiceFilter != nil { + wet = v.voiceFilter.Filter(wet) } return wet } func (v s3mVoice) GetSampleRate() frequency.Frequency { - return v.config.SampleRate + return v.inst.SampleRate } diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index f837f4f..a9e5ce5 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -1,8 +1,14 @@ package voice import ( + "errors" + "fmt" + + "github.com/gotracker/playback/filter" s3mPanning "github.com/gotracker/playback/format/s3m/panning" s3mVolume "github.com/gotracker/playback/format/s3m/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/component" @@ -10,12 +16,8 @@ import ( "github.com/gotracker/playback/voice/pcm" ) -type Period interface { - period.Period -} - type s3mVoice struct { - config voice.InstrumentConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + inst *instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] component.KeyModulator @@ -23,7 +25,8 @@ type s3mVoice struct { component.AmpModulator[s3mVolume.FineVolume, s3mVolume.Volume] component.FreqModulator[period.Amiga] component.PanModulator[s3mPanning.Panning] - vol0Opt component.Vol0Optimization + vol0Opt component.Vol0Optimization + voiceFilter filter.Filter } var ( @@ -58,6 +61,7 @@ func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.Fine }) v.vol0Opt.Setup(config.Vol0Optimization) + return v } @@ -90,11 +94,31 @@ func (v *s3mVoice) doDeferredRelease() { } } -func (v *s3mVoice) Setup(config voice.InstrumentConfig[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) { - v.config = config +func (v *s3mVoice) Setup(inst *instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], outputRate frequency.Frequency) error { + v.inst = inst + + switch d := inst.GetData().(type) { + case *instrument.PCM[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]: + v.AmpModulator.SetMixingVolumeOverride(d.MixingVolume) + + v.setupPCM(d.Sample, d.Loop, d.SustainLoop, s3mVolume.MaxFineVolume, inst.GetDefaultVolume()) + + default: + return fmt.Errorf("unhandled instrument type: %T", inst) + } + if inst == nil { + return errors.New("instrument is nil") + } + + if factory := inst.GetFilterFactory(); factory != nil { + v.voiceFilter = factory(inst.SampleRate) + v.voiceFilter.SetPlaybackRate(outputRate) + } else { + v.voiceFilter = nil + } - v.KeyModulator.Release() v.Reset() + return nil } func (v *s3mVoice) Reset() { @@ -124,9 +148,9 @@ func (v *s3mVoice) Advance() { v.KeyModulator.Advance() } -func (v *s3mVoice) Clone() voice.Voice { +func (v *s3mVoice) Clone(bool) voice.Voice { vv := s3mVoice{ - config: v.config, + inst: v.inst, AmpModulator: v.AmpModulator.Clone(), FreqModulator: v.FreqModulator.Clone(), PanModulator: v.PanModulator.Clone(), @@ -145,14 +169,14 @@ func (v *s3mVoice) Clone() voice.Voice { vv.voicer = v.voicer.Clone() } - if v.config.VoiceFilter != nil { - vv.config.VoiceFilter = v.config.VoiceFilter.Clone() + if v.voiceFilter != nil { + vv.voiceFilter = v.voiceFilter.Clone() } return &vv } -func (v *s3mVoice) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol s3mVolume.FineVolume, defVol s3mVolume.Volume) { +func (v *s3mVoice) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol s3mVolume.FineVolume, defVol s3mVolume.Volume) { var s component.Sampler[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] s.Setup(component.SamplerSettings[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume]{ Sample: samp, diff --git a/format/xm/voice/sampler.go b/format/xm/voice/sampler.go index 7eea553..d2475af 100644 --- a/format/xm/voice/sampler.go +++ b/format/xm/voice/sampler.go @@ -40,12 +40,12 @@ func (v *xmVoice[TPeriod]) GetSample(pos sampling.Pos) volume.Matrix { vol := v.GetFinalVolume() wet := dry.Apply(vol) - if v.config.VoiceFilter != nil { - wet = v.config.VoiceFilter.Filter(wet) + if v.voiceFilter != nil { + wet = v.voiceFilter.Filter(wet) } return wet } func (v xmVoice[TPeriod]) GetSampleRate() frequency.Frequency { - return v.config.SampleRate + return v.inst.SampleRate } diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 65d0ec5..fff0bc6 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -1,8 +1,14 @@ package voice import ( + "errors" + "fmt" + + "github.com/gotracker/playback/filter" 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/component" @@ -16,7 +22,7 @@ type Period interface { } type xmVoice[TPeriod Period] struct { - config voice.InstrumentConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + inst *instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] fadeoutMode fadeout.Mode @@ -31,6 +37,7 @@ type xmVoice[TPeriod Period] struct { volEnv component.VolumeEnvelope[xmVolume.XmVolume] panEnv component.PanEnvelope[xmPanning.Panning] vol0Opt component.Vol0Optimization + voiceFilter filter.Filter } var ( @@ -117,41 +124,64 @@ func (v xmVoice[TPeriod]) getFadeoutEnabled() bool { return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) } -func (v *xmVoice[TPeriod]) Setup(config voice.InstrumentConfig[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) { - v.config = config - v.fadeout.Setup(component.FadeoutModulatorSettings{ - Enabled: v.config.FadeOut.Mode != fadeout.ModeDisabled, - GetActive: v.getFadeoutEnabled, - Amount: config.FadeOut.Amount, - }) - v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) - v.autoVibrato.Setup(config.AutoVibrato) - volEnvSettings := component.EnvelopeSettings[xmVolume.XmVolume, xmVolume.XmVolume]{ - Envelope: config.VolEnv, - } - if config.VolEnvFinishFadesOut { - volEnvSettings.OnFinished = func(v voice.Voice) { - v.Fadeout() +func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], outputRate frequency.Frequency) 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, + }) + + v.amp.SetMixingVolumeOverride(d.MixingVolume) + + v.setupPCM(d.Sample, d.Loop, d.SustainLoop, xmVolume.DefaultXmMixingVolume, inst.GetDefaultVolume()) + + default: + return fmt.Errorf("unhandled instrument type: %T", inst) } - v.volEnv.Setup(volEnvSettings) - v.panEnv.Setup(component.EnvelopeSettings[xmPanning.Panning, xmPanning.Panning]{ - Envelope: config.PanEnv, - }) - v.KeyModulator.Release() + if inst == nil { + return errors.New("instrument is nil") + } + + v.autoVibrato.Setup(inst.GetAutoVibrato()) + + if factory := inst.GetFilterFactory(); factory != nil { + v.voiceFilter = factory(inst.SampleRate) + v.voiceFilter.SetPlaybackRate(outputRate) + } else { + v.voiceFilter = nil + } + v.Reset() + return nil } func (v *xmVoice[TPeriod]) Reset() { - v.fadeoutMode = v.config.FadeOut.Mode - + v.KeyModulator.Release() + v.amp.Reset() v.fadeout.Reset() - - v.volEnv.Reset() - v.panEnv.Reset() - + v.freq.Reset() v.autoVibrato.Reset() - + v.pan.Reset() v.volEnv.Reset() v.panEnv.Reset() v.vol0Opt.Reset() @@ -194,9 +224,9 @@ func (v *xmVoice[TPeriod]) Advance() { v.KeyModulator.Advance() } -func (v *xmVoice[TPeriod]) Clone() voice.Voice { +func (v *xmVoice[TPeriod]) Clone(bool) voice.Voice { vv := xmVoice[TPeriod]{ - config: v.config, + inst: v.inst, fadeoutMode: v.fadeoutMode, amp: v.amp.Clone(), fadeout: v.fadeout.Clone(), @@ -223,14 +253,14 @@ func (v *xmVoice[TPeriod]) Clone() voice.Voice { vv.voicer = v.voicer.Clone() } - if v.config.VoiceFilter != nil { - vv.config.VoiceFilter = v.config.VoiceFilter.Clone() + if v.voiceFilter != nil { + vv.voiceFilter = v.voiceFilter.Clone() } return &vv } -func (v *xmVoice[TPeriod]) SetPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol, defVol xmVolume.XmVolume) { +func (v *xmVoice[TPeriod]) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol, defVol xmVolume.XmVolume) { var s component.Sampler[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] s.Setup(component.SamplerSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume]{ Sample: samp, diff --git a/instrument/pcm.go b/instrument/pcm.go index 1781451..0709686 100644 --- a/instrument/pcm.go +++ b/instrument/pcm.go @@ -2,7 +2,6 @@ package instrument import ( "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/playback/filter" "github.com/gotracker/playback/note" "github.com/gotracker/playback/voice/envelope" "github.com/gotracker/playback/voice/fadeout" @@ -24,8 +23,8 @@ type PCM[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { VolEnv envelope.Envelope[TVolume] VolEnvFinishFadesOut bool PanEnv envelope.Envelope[TPanning] - PitchFiltMode bool // true = filter, false = pitch - PitchFiltEnv envelope.Envelope[filter.PitchFiltValue] // this is either pitch or filter + PitchFiltMode bool // true = filter, false = pitch + PitchFiltEnv envelope.Envelope[types.PitchFiltValue] // this is either pitch or filter } type PitchPan struct { diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index f60e3bc..c1d13f2 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -1,13 +1,11 @@ package machine import ( - "github.com/gotracker/playback/filter" "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/voice" - "github.com/gotracker/playback/voice/types" ) func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNoteAction(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error { @@ -33,14 +31,14 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo case note.ActionCut: c.cv.Stop() case note.ActionRelease: - pn = c.cv.Clone() + pn = c.cv.Clone(true) pn.Release() case note.ActionFadeout: - pn = c.cv.Clone() + pn = c.cv.Clone(true) pn.Release() pn.Fadeout() case note.ActionRetrigger: - pn = c.cv.Clone() + pn = c.cv.Clone(true) pn.Release() pn.Attack() @@ -108,21 +106,16 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe prevInst := c.prev.Inst if inst != nil { if prevInst != inst { - switch inst.GetKind() { - case instrument.KindPCM: - d := inst.GetData().(*instrument.PCM[TMixingVolume, TVolume, TPanning]) - if err := c.doSetupPCM(ch, m, inst, d, outputRate); err != nil { - return err - } - - case instrument.KindOPL2: - d := inst.GetData().(*instrument.OPL2) - if err := c.doSetupOPL2(ch, m, inst, d); err != nil { - return err - } - - default: - panic("unhandled instrument kind") + rc := &m.actualOutputs[ch] + if factory := inst.GetPluginFilterFactory(); factory != nil { + rc.PluginFilter = factory(inst.SampleRate) + rc.PluginFilter.SetPlaybackRate(outputRate) + } else { + rc.PluginFilter = nil + } + + if err := c.cv.Setup(inst, outputRate); err != nil { + return err } } } else { @@ -131,47 +124,6 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupPCM(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.PCM[TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error { - var voiceFilter filter.Filter - if factory := inst.GetFilterFactory(); factory != nil { - voiceFilter = factory(inst.SampleRate) - voiceFilter.SetPlaybackRate(outputRate) - } - - rc := &m.actualOutputs[ch] - if factory := inst.GetPluginFilterFactory(); factory != nil { - rc.PluginFilter = factory(inst.SampleRate) - rc.PluginFilter.SetPlaybackRate(outputRate) - } else { - rc.PluginFilter = nil - } - - c.cv.Setup(voice.InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ - SampleRate: inst.GetSampleRate(), - AutoVibrato: inst.GetAutoVibrato(), - Data: d, - VoiceFilter: voiceFilter, - FadeOut: d.FadeOut, - PitchPan: d.PitchPan, - VolEnv: d.VolEnv, - VolEnvFinishFadesOut: d.VolEnvFinishFadesOut, - PanEnv: d.PanEnv, - PitchFiltMode: d.PitchFiltMode, - PitchFiltEnv: d.PitchFiltEnv, - }) - - var mixVol TMixingVolume - if mv, set := d.MixingVolume.Get(); set { - mixVol = mv - } else { - mixVol = types.GetMaxVolume[TMixingVolume]() - } - - c.cv.SetPCM(d.Sample, d.Loop, d.SustainLoop, mixVol, inst.GetDefaultVolume()) - - return nil -} - func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupOPL2(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.OPL2) error { panic("unimplemented") //var o component.OPL2[TPeriod, TVolume] diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 4561d5a..10cdf78 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -63,27 +63,43 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend } if data != nil { - mixData = append(mixData, data...) + 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] - if rc.Voice == nil { - continue - } - - rc.GlobalVolume = m.gv.ToVolume() + var data *mixing.Data + if rc.Voice != nil { + rc.GlobalVolume = m.gv.ToVolume() - //rc.Voice.DumpState(index.Channel(i), m.us.Tracer) - data, err := rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) - if err != nil { - return nil, err + //rc.Voice.DumpState(index.Channel(i), m.us.Tracer) + var err error + data, err = rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) + if err != nil { + return nil, err + } } if data != nil { - mixData = append(mixData, data...) + 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, + }) } } diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go index fbdc82b..b7d5b0f 100644 --- a/player/machine/pastnote.go +++ b/player/machine/pastnote.go @@ -12,6 +12,6 @@ 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) { +func (p pastNote[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { return p.rc.RenderAndAdvance(pc, centerAheadPan, details) } diff --git a/player/render/channel.go b/player/render/channel.go index fc1b673..2991e5e 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -24,7 +24,7 @@ type Channel[TPeriod period.Period] struct { Voice voice.Voice } -func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) ([]mixing.Data, error) { +func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { if filt := c.PluginFilter; filt != nil { filt.SetPlaybackRate(details.SampleRate) } @@ -41,7 +41,7 @@ func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], if data == nil { return nil, nil } - return []mixing.Data{*data}, nil + return data, nil } func (c *Channel[TPeriod]) StopVoice() { diff --git a/voice/component/envelope_filter.go b/voice/component/envelope_filter.go index 3d8bd92..dc65142 100755 --- a/voice/component/envelope_filter.go +++ b/voice/component/envelope_filter.go @@ -1,17 +1,17 @@ package component import ( - "github.com/gotracker/playback/filter" "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/types" ) // FilterEnvelope is a filter frequency cutoff modulation envelope type FilterEnvelope struct { - baseEnvelope[filter.PitchFiltValue, uint8] + baseEnvelope[types.PitchFiltValue, uint8] } -func (e *FilterEnvelope) Setup(settings EnvelopeSettings[filter.PitchFiltValue, uint8]) { +func (e *FilterEnvelope) Setup(settings EnvelopeSettings[types.PitchFiltValue, uint8]) { e.baseEnvelope.Setup(settings, e.calc) } @@ -21,7 +21,7 @@ func (e FilterEnvelope) Clone(onFinished voice.Callback) FilterEnvelope { return m } -func (e *FilterEnvelope) calc(y0, y1 filter.PitchFiltValue, t float64) uint8 { +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_pitch.go b/voice/component/envelope_pitch.go index 724a1ff..c29c02e 100755 --- a/voice/component/envelope_pitch.go +++ b/voice/component/envelope_pitch.go @@ -1,18 +1,18 @@ package component import ( - "github.com/gotracker/playback/filter" "github.com/gotracker/playback/period" "github.com/gotracker/playback/util" "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/types" ) // PitchEnvelope is an frequency modulation envelope type PitchEnvelope struct { - baseEnvelope[filter.PitchFiltValue, period.Delta] + baseEnvelope[types.PitchFiltValue, period.Delta] } -func (e *PitchEnvelope) Setup(settings EnvelopeSettings[filter.PitchFiltValue, period.Delta]) { +func (e *PitchEnvelope) Setup(settings EnvelopeSettings[types.PitchFiltValue, period.Delta]) { e.baseEnvelope.Setup(settings, e.calc) } @@ -22,6 +22,6 @@ func (e PitchEnvelope) Clone(onFinished voice.Callback) PitchEnvelope { return m } -func (e *PitchEnvelope) calc(y0, y1 filter.PitchFiltValue, t float64) period.Delta { +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/modulator_amp.go b/voice/component/modulator_amp.go index 67225ea..83c84b9 100755 --- a/voice/component/modulator_amp.go +++ b/voice/component/modulator_amp.go @@ -7,6 +7,7 @@ import ( "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 @@ -19,7 +20,8 @@ type AmpModulator[TMixingVolume, TVolume types.Volume] struct { mixing TMixingVolume } keyed struct { - delta types.VolumeDelta + delta types.VolumeDelta + mixingOverride optional.Value[TMixingVolume] } final volume.Volume // = active? * mixing * vol } @@ -45,6 +47,7 @@ func (a AmpModulator[TMixingVolume, TVolume]) Clone() AmpModulator[TMixingVolume func (a *AmpModulator[TMixingVolume, TVolume]) Reset() { a.keyed.delta = 0 + a.keyed.mixingOverride.Reset() a.updateFinal() } @@ -70,6 +73,14 @@ func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolume() TMixingVolume { return a.unkeyed.mixing } +func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolumeOverride(mvo optional.Value[TMixingVolume]) { + a.keyed.mixingOverride = mvo +} + +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) { if vol.IsUseInstrumentVol() { @@ -105,14 +116,21 @@ func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() { } v := types.AddVolumeDelta(a.unkeyed.vol, a.keyed.delta) - a.final = a.unkeyed.mixing.ToVolume() * v.ToVolume() + + mv := a.unkeyed.mixing + if mvo, set := a.keyed.mixingOverride.Get(); set { + mv = mvo + } + + a.final = mv.ToVolume() * v.ToVolume() } func (a AmpModulator[TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} vol{%v} mixing{%v} delta{%v} final{%v}", + t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} vol{%v} mixing{%v} mixingOverride{%v} delta{%v} final{%v}", a.unkeyed.active, a.unkeyed.vol, a.unkeyed.mixing, + a.keyed.mixingOverride, a.keyed.delta, a.final, ), comment) diff --git a/voice/config.go b/voice/config.go index 88346ba..2b9c2ea 100644 --- a/voice/config.go +++ b/voice/config.go @@ -27,8 +27,8 @@ type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volu VolEnv envelope.Envelope[TVolume] VolEnvFinishFadesOut bool PanEnv envelope.Envelope[TPanning] - PitchFiltMode bool // true = filter, false = pitch - PitchFiltEnv envelope.Envelope[filter.PitchFiltValue] // this is either pitch or filter + PitchFiltMode bool // true = filter, false = pitch + PitchFiltEnv envelope.Envelope[types.PitchFiltValue] // this is either pitch or filter } type VoiceConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { diff --git a/filter/pitchfiltervalue.go b/voice/types/pitchfiltervalue.go similarity index 65% rename from filter/pitchfiltervalue.go rename to voice/types/pitchfiltervalue.go index b2e1f88..e14734c 100644 --- a/filter/pitchfiltervalue.go +++ b/voice/types/pitchfiltervalue.go @@ -1,3 +1,3 @@ -package filter +package types type PitchFiltValue = int8 diff --git a/voice/voice.go b/voice/voice.go index 17934d2..5cae57d 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -6,16 +6,15 @@ import ( "github.com/gotracker/gomixing/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/period" "github.com/gotracker/playback/tracing" - "github.com/gotracker/playback/voice/loop" - "github.com/gotracker/playback/voice/pcm" "github.com/gotracker/playback/voice/types" ) type Voice interface { - Clone() Voice + Clone(background bool) Voice DumpState(ch index.Channel, t tracing.Tracer) // Configuration @@ -39,8 +38,7 @@ type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, T Voice // Configuration - Setup(config InstrumentConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) - SetPCM(sample pcm.Sample, wholeLoop loop.Loop, sustainLoop loop.Loop, mixVolume TMixingVolume, defaultVolume TVolume) + Setup(inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error } type AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { From d945fea5c8699380893f6fa21b0c24dbce0dea6c Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 11:55:56 -0800 Subject: [PATCH 41/63] Drastically simpler patterns and rows --- format/it/layout/row.go | 28 +++++++++++++++ format/it/layout/song.go | 22 +++++------- format/it/load/itformat.go | 13 ++++--- format/it/pattern/pattern.go | 12 ------- format/it/pattern/row.go | 12 ------- format/s3m/layout/row.go | 27 +++++++++++++++ format/s3m/layout/song.go | 22 +++++------- format/s3m/load/modconv/modconverter.go | 3 +- format/s3m/load/s3mformat.go | 25 +++++++------- format/s3m/pattern/pattern.go | 9 ----- format/s3m/pattern/row.go | 9 ----- format/xm/layout/row.go | 28 +++++++++++++++ format/xm/layout/song.go | 24 +++++-------- format/xm/load/xmformat.go | 17 +++++---- format/xm/pattern/pattern.go | 12 ------- format/xm/pattern/row.go | 12 ------- player/machine/machine.go | 46 ++++++++++++------------- player/machine/ticker.go | 2 +- song/pattern.go | 14 +++----- song/row.go | 26 ++++++++------ song/song.go | 6 ++-- 21 files changed, 182 insertions(+), 187 deletions(-) create mode 100644 format/it/layout/row.go delete mode 100644 format/it/pattern/pattern.go delete mode 100644 format/it/pattern/row.go create mode 100644 format/s3m/layout/row.go delete mode 100644 format/s3m/pattern/pattern.go delete mode 100644 format/s3m/pattern/row.go create mode 100644 format/xm/layout/row.go delete mode 100644 format/xm/pattern/pattern.go delete mode 100644 format/xm/pattern/row.go 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 5eb1e72..eeb7f27 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -9,7 +9,6 @@ import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/format/it/channel" itPanning "github.com/gotracker/playback/format/it/panning" - "github.com/gotracker/playback/format/it/pattern" itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/index" @@ -27,7 +26,7 @@ type Song[TPeriod period.Period] struct { Head Header Instruments map[uint8]*instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument - Patterns []pattern.Pattern[TPeriod] + Patterns []song.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern FilterPlugins map[int]filter.Factory @@ -110,20 +109,15 @@ func (s Song[TPeriod]) GetTickDuration(bpm int) time.Duration { } // GetPattern returns a specific pattern indexed by `patNum` -func (s Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern[channel.Data[TPeriod], itVolume.Volume], error) { +func (s Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern, error) { if int(patNum) >= len(s.Patterns) { return nil, song.ErrStopSong } - return s.Patterns[patNum].Pattern, nil -} - -// GetPatternIntf returns an interface to a specific pattern indexed by `patNum` -func (s Song[TPeriod]) GetPatternIntf(patNum index.Pattern) (song.PatternIntf, error) { - return s.GetPattern(patNum) + return s.Patterns[patNum], nil } // GetPatternIntfByOrder returns the pattern specified by the order index provided -func (s Song[TPeriod]) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, error) { +func (s Song[TPeriod]) GetPatternByOrder(o index.Order) (song.Pattern, error) { if int(o) >= len(s.OrderList) { return nil, song.ErrStopSong } @@ -136,7 +130,7 @@ func (s Song[TPeriod]) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, e return nil, index.ErrNextPattern } - return s.GetPatternIntf(pat) + return s.GetPattern(pat) } // GetNumChannels returns the number of channels the song has @@ -199,10 +193,10 @@ func (s Song[TPeriod]) GetInitialOrder() index.Order { return s.Head.InitialOrder } -func (s Song[TPeriod]) GetRowRenderStringer(row song.RowIntf, channels int, longFormat bool) render.RowStringer { +func (s Song[TPeriod]) GetRowRenderStringer(row song.Row, channels int, longFormat bool) render.RowStringer { rt := render.NewRowText[channel.Data[TPeriod]](channels, longFormat) - rowData := make(song.Row[channel.Data[TPeriod], itVolume.Volume], channels) - copy(rowData, row.(song.Row[channel.Data[TPeriod], itVolume.Volume])) + rowData := make([]channel.Data[TPeriod], channels) + copy(rowData, row.(Row[TPeriod])) rt.Channels = rowData return rt } diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index 4babac2..1fa1959 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -15,7 +15,6 @@ import ( "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/pattern" itSystem "github.com/gotracker/playback/format/it/system" itVolume "github.com/gotracker/playback/format/it/volume" "github.com/gotracker/playback/index" @@ -48,14 +47,14 @@ func moduleHeaderToHeader(fh *itfile.ModuleHeader) (*layout.Header, error) { return &head, nil } -func convertItPattern[TPeriod period.Period](pkt itfile.PackedPattern, channels int) (*pattern.Pattern[TPeriod], int, error) { - pat := make(song.Pattern[channel.Data[TPeriod], itVolume.Volume], pkt.Rows) +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++ { - row := make([]channel.Data[TPeriod], channels) + row := make(layout.Row[TPeriod], channels) pat[rowNum] = row channelLoop: for { @@ -86,7 +85,7 @@ func convertItPattern[TPeriod period.Period](pkt itfile.PackedPattern, channels } } - return &pattern.Pattern[TPeriod]{Pattern: pat}, int(maxCh), nil + return pat, int(maxCh), nil } func convertItFileToSong(f *itfile.File, features []feature.Feature) (song.Data, error) { @@ -114,7 +113,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] Head: *h, Instruments: make(map[uint8]*instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]), InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument), - Patterns: make([]pattern.Pattern[TPeriod], len(f.Patterns)), + Patterns: make([]song.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.OrderCount)), FilterPlugins: make(map[int]filter.Factory), } @@ -177,7 +176,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - songData.Patterns[patNum] = *p + songData.Patterns[patNum] = p } sharedMem := channel.SharedMemory{ diff --git a/format/it/pattern/pattern.go b/format/it/pattern/pattern.go deleted file mode 100644 index 4027b7b..0000000 --- a/format/it/pattern/pattern.go +++ /dev/null @@ -1,12 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/format/it/channel" - itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/song" -) - -type Pattern[TPeriod period.Period] struct { - song.Pattern[channel.Data[TPeriod], itVolume.Volume] -} diff --git a/format/it/pattern/row.go b/format/it/pattern/row.go deleted file mode 100644 index 740df7c..0000000 --- a/format/it/pattern/row.go +++ /dev/null @@ -1,12 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/format/it/channel" - itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/song" -) - -type Row[TPeriod period.Period] struct { - song.Row[channel.Data[TPeriod], itVolume.Volume] -} 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 aef1af1..d630508 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -7,7 +7,6 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/format/s3m/channel" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - "github.com/gotracker/playback/format/s3m/pattern" s3mPeriod "github.com/gotracker/playback/format/s3m/period" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" @@ -24,7 +23,7 @@ type Song struct { System system.System Head Header Instruments []*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] - Patterns []pattern.Pattern + Patterns []song.Pattern ChannelSettings []ChannelSetting NumChannels int OrderList []index.Pattern @@ -100,20 +99,15 @@ func (s Song) GetTickDuration(bpm int) time.Duration { } // GetPattern returns a specific pattern indexed by `patNum` -func (s Song) GetPattern(patNum index.Pattern) (song.Pattern[channel.Data, s3mVolume.Volume], error) { +func (s Song) GetPattern(patNum index.Pattern) (song.Pattern, error) { if int(patNum) >= len(s.Patterns) { return nil, song.ErrStopSong } return s.Patterns[patNum], nil } -// GetPattern returns an interface to a specific pattern indexed by `patNum` -func (s Song) GetPatternIntf(patNum index.Pattern) (song.PatternIntf, error) { - return s.GetPattern(patNum) -} - // GetPatternByOrder returns the pattern specified by the order index provided -func (s Song) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, error) { +func (s Song) GetPatternByOrder(o index.Order) (song.Pattern, error) { if int(o) >= len(s.OrderList) { return nil, song.ErrStopSong } @@ -126,7 +120,7 @@ func (s Song) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, error) { return nil, index.ErrNextPattern } - return s.GetPatternIntf(pat) + return s.GetPattern(pat) } // GetNumChannels returns the number of channels the song has @@ -183,12 +177,12 @@ func (s Song) GetInitialOrder() index.Order { return s.Head.InitialOrder } -func (s Song) GetRowRenderStringer(row song.RowIntf, channels int, longFormat bool) render.RowStringer { +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(pattern.Row, 0, nch) - pr := row.(pattern.Row) - nprch := min(pr.GetNumChannels(), nch) + rowData := make([]channel.Data, 0, nch) + pr := row.(Row) + nprch := min(len(pr), nch) for i := 0; i < nprch; i++ { if !s.ChannelSettings[i].Enabled { continue diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index 10dc533..d774223 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -10,6 +10,7 @@ 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" ) @@ -18,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() diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index 1dc29d7..472c671 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -12,7 +12,6 @@ import ( "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/pattern" s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/frequency" @@ -185,14 +184,14 @@ 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, int) { - pat := make(pattern.Pattern, numRows) +func convertS3MPackedPattern(pkt s3mfile.PackedPattern, numRows uint8) (song.Pattern, int) { + pat := make(song.Pattern, numRows) buffer := bytes.NewBuffer(pkt.Data) maxCh := uint8(0) for rowNum := uint8(0); rowNum < numRows; rowNum++ { - row := make(pattern.Row, 0) + row := make(layout.Row, 0) channelLoop: for { var what s3mfile.PatternFlags @@ -256,7 +255,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, return nil, err } - song := layout.Song{ + s := layout.Song{ System: s3mSystem.S3MSystem, Head: *h, Instruments: make([]*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.InstrumentPointers)), @@ -282,10 +281,10 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, //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[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.Instruments)) + s.Instruments = make([]*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.Instruments)) for instNum, scrs := range f.Instruments { sample, err := convertSCRSFullToInstrument(&scrs, signedSamples, features) if err != nil { @@ -295,11 +294,11 @@ 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 } maxPatternChannel := 0 - song.Patterns = make([]pattern.Pattern, len(f.Patterns)) + s.Patterns = make([]song.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { pattern, maxCh := convertS3MPackedPattern(pkt, getPatternLen(patNum)) if pattern == nil { @@ -308,7 +307,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, if maxPatternChannel < maxCh { maxPatternChannel = maxCh } - song.Patterns[patNum] = pattern + s.Patterns[patNum] = pattern } sharedMem := channel.SharedMemory{ @@ -364,10 +363,10 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } } - song.NumChannels = lastEnabledChannel + 1 - song.ChannelSettings = channels[:maxPatternChannel+1] + s.NumChannels = lastEnabledChannel + 1 + s.ChannelSettings = channels[:maxPatternChannel+1] - return &song, nil + return &s, nil } func readS3M(r io.Reader, features []feature.Feature) (song.Data, error) { diff --git a/format/s3m/pattern/pattern.go b/format/s3m/pattern/pattern.go deleted file mode 100644 index 6d5ab79..0000000 --- a/format/s3m/pattern/pattern.go +++ /dev/null @@ -1,9 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/format/s3m/channel" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" - "github.com/gotracker/playback/song" -) - -type Pattern = song.Pattern[channel.Data, s3mVolume.Volume] diff --git a/format/s3m/pattern/row.go b/format/s3m/pattern/row.go deleted file mode 100644 index 0396bf1..0000000 --- a/format/s3m/pattern/row.go +++ /dev/null @@ -1,9 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/format/s3m/channel" - s3mVolume "github.com/gotracker/playback/format/s3m/volume" - "github.com/gotracker/playback/song" -) - -type Row = song.Row[channel.Data, s3mVolume.Volume] 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 673a9dd..43edab2 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -7,7 +7,6 @@ import ( "github.com/gotracker/gomixing/volume" "github.com/gotracker/playback/format/xm/channel" xmPanning "github.com/gotracker/playback/format/xm/panning" - "github.com/gotracker/playback/format/xm/pattern" xmPeriod "github.com/gotracker/playback/format/xm/period" xmVolume "github.com/gotracker/playback/format/xm/volume" "github.com/gotracker/playback/index" @@ -25,7 +24,7 @@ type Song[TPeriod period.Period] struct { Head Header Instruments map[uint8]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] - Patterns []pattern.Pattern[TPeriod] + Patterns []song.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern } @@ -107,20 +106,15 @@ func (s Song[TPeriod]) GetTickDuration(bpm int) time.Duration { } // GetPattern returns a specific pattern indexed by `patNum` -func (s Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume], error) { +func (s Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern, error) { if int(patNum) >= len(s.Patterns) { return nil, song.ErrStopSong } - return s.Patterns[patNum].Pattern, nil + return s.Patterns[patNum], nil } -// GetPatternIntf returns an interface to a specific pattern indexed by `patNum` -func (s Song[TPeriod]) GetPatternIntf(patNum index.Pattern) (song.PatternIntf, error) { - return s.GetPattern(patNum) -} - -// GetPatternIntfByOrder returns the pattern specified by the order index provided -func (s Song[TPeriod]) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, error) { +// GetPatternByOrder returns the pattern specified by the order index provided +func (s Song[TPeriod]) GetPatternByOrder(o index.Order) (song.Pattern, error) { if int(o) >= len(s.OrderList) { return nil, song.ErrStopSong } @@ -133,7 +127,7 @@ func (s Song[TPeriod]) GetPatternIntfByOrder(o index.Order) (song.PatternIntf, e return nil, index.ErrNextPattern } - return s.GetPatternIntf(pat) + return s.GetPattern(pat) } // GetNumChannels returns the number of channels the song has @@ -196,10 +190,10 @@ func (s Song[TPeriod]) GetInitialOrder() index.Order { return s.Head.InitialOrder } -func (s Song[TPeriod]) GetRowRenderStringer(row song.RowIntf, channels int, longFormat bool) render.RowStringer { +func (s Song[TPeriod]) GetRowRenderStringer(row song.Row, channels int, longFormat bool) render.RowStringer { rt := render.NewRowText[channel.Data[TPeriod]](channels, longFormat) - rowData := make(song.Row[channel.Data[TPeriod], xmVolume.XmVolume], channels) - copy(rowData, row.(song.Row[channel.Data[TPeriod], xmVolume.XmVolume])) + rowData := make([]channel.Data[TPeriod], channels) + copy(rowData, row.(Row[TPeriod])) rt.Channels = rowData return rt } diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index 2a40caf..49b87b2 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -12,7 +12,6 @@ import ( "github.com/gotracker/playback/format/xm/channel" "github.com/gotracker/playback/format/xm/layout" xmPanning "github.com/gotracker/playback/format/xm/panning" - "github.com/gotracker/playback/format/xm/pattern" xmPeriod "github.com/gotracker/playback/format/xm/period" xmSystem "github.com/gotracker/playback/format/xm/system" xmVolume "github.com/gotracker/playback/format/xm/volume" @@ -256,12 +255,12 @@ func convertXMInstrumentToInstrument(ih *xmfile.InstrumentHeader, linearFrequenc return xmInstrumentToInstrument(ih, linearFrequencySlides, features) } -func convertXmPattern[TPeriod period.Period](pkt xmfile.Pattern) (*pattern.Pattern[TPeriod], int) { - pat := make(song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume], len(pkt.Data)) +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 { - row := make(song.Row[channel.Data[TPeriod], xmVolume.XmVolume], len(drow)) + row := make(layout.Row[TPeriod], len(drow)) pat[rowNum] = row for channelNum, chn := range drow { @@ -280,7 +279,7 @@ func convertXmPattern[TPeriod period.Period](pkt xmfile.Pattern) (*pattern.Patte } } - return &pattern.Pattern[TPeriod]{Pattern: pat}, int(maxCh) + return pat, int(maxCh) } func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (song.Data, error) { @@ -304,7 +303,7 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] Head: *h, Instruments: make(map[uint8]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), - Patterns: make([]pattern.Pattern[TPeriod], len(f.Patterns)), + Patterns: make([]song.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.SongLength)), } @@ -346,14 +345,14 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] lastEnabledChannel := 0 for patNum, pkt := range f.Patterns { - pattern, maxCh := convertXmPattern[TPeriod](pkt) - if pattern == nil { + pat, maxCh := convertXmPattern[TPeriod](pkt) + if pat == nil { continue } if lastEnabledChannel < maxCh { lastEnabledChannel = maxCh } - s.Patterns[patNum] = *pattern + s.Patterns[patNum] = pat } sharedMem := channel.SharedMemory{ diff --git a/format/xm/pattern/pattern.go b/format/xm/pattern/pattern.go deleted file mode 100644 index 1fc26b1..0000000 --- a/format/xm/pattern/pattern.go +++ /dev/null @@ -1,12 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/format/xm/channel" - xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/song" -) - -type Pattern[TPeriod period.Period] struct { - song.Pattern[channel.Data[TPeriod], xmVolume.XmVolume] -} diff --git a/format/xm/pattern/row.go b/format/xm/pattern/row.go deleted file mode 100644 index 8521c07..0000000 --- a/format/xm/pattern/row.go +++ /dev/null @@ -1,12 +0,0 @@ -package pattern - -import ( - "github.com/gotracker/playback/format/xm/channel" - xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/song" -) - -type Row[TPeriod period.Period] struct { - song.Row[channel.Data[TPeriod], xmVolume.XmVolume] -} diff --git a/player/machine/machine.go b/player/machine/machine.go index a830052..5c028df 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -206,8 +206,8 @@ type dataInstructionGenerator[TPeriod Period, TGlobalVolume, TMixingVolume, TVol 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.RowIntf, error) { - pat, err := m.songData.GetPatternIntfByOrder(m.ticker.current.order) +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 } @@ -215,7 +215,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) getR return nil, song.ErrStopSong } - row := pat.GetRowIntf(m.ticker.current.row) + row := pat.GetRow(m.ticker.current.row) if row == nil { return nil, song.ErrStopSong } @@ -223,37 +223,35 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) getR return row, nil } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) updateInstructions(rowData song.RowIntf) error { - rowChannels := min(m.songData.GetNumChannels(), rowData.GetNumChannels()) - for i := range m.channels { - ch := index.Channel(i) +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) updateInstructions(rowData song.Row) error { + 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 false, nil + } c := &m.channels[ch] c.instructions = nil - if !c.enabled || i >= rowChannels { - continue + if !c.enabled || d == nil { + return true, nil } - d := rowData.GetChannelIntf(ch) + if err := c.decodeNote(m, d); err != nil { + return false, fmt.Errorf("channel[%d] decode error: %w", ch, err) + } - if d != nil { - if err := c.decodeNote(m, d); err != nil { - return 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) } - if gen, ok := d.(dataInstructionGenerator[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]); ok { - insts, err := gen.ToInstructions(m, ch, m.songData) - if err != nil { - return fmt.Errorf("channel[%d] instruction error: %w", ch, err) - } - - c.instructions = insts - } + c.instructions = insts } - } - - return nil + return true, nil + }) } func GetPeriodCalculator[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m Machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) song.PeriodCalculator[TPeriod] { diff --git a/player/machine/ticker.go b/player/machine/ticker.go index d39b7bc..8e98ba4 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -186,7 +186,7 @@ orderScan: orderUpdated = true } - pat, err := m.songData.GetPatternIntfByOrder(index.Order(order)) + pat, err := m.songData.GetPatternByOrder(index.Order(order)) if err != nil { if errors.Is(err, index.ErrNextPattern) { order++ diff --git a/song/pattern.go b/song/pattern.go index f6dcbb3..6fa5d41 100644 --- a/song/pattern.go +++ b/song/pattern.go @@ -12,26 +12,22 @@ var ( ) type PatternIntf interface { - GetRowIntf(row index.Row) RowIntf + GetRow(row index.Row) Row NumRows() int } // Pattern is structure containing the pattern data -type Pattern[TChannelData ChannelData[TVolume], TVolume Volume] []Row[TChannelData, TVolume] +type Pattern []Row // GetRow returns a single row of channel data -func (p Pattern[TChannelData, TVolume]) GetRow(row index.Row) Row[TChannelData, TVolume] { - return p[row] -} - -func (p Pattern[TChannelData, TVolume]) GetRowIntf(row index.Row) RowIntf { +func (p Pattern) GetRow(row index.Row) Row { return p[row] } // NumRows returns the number of rows contained within the pattern -func (p Pattern[TChannelData, TVolume]) NumRows() int { +func (p Pattern) NumRows() int { return len(p) } // Patterns is an array of pattern interfaces -type Patterns[TChannelData ChannelData[TVolume], TVolume Volume] []Pattern[TChannelData, TVolume] +type Patterns[TChannelData ChannelData[TVolume], TVolume Volume] []Pattern diff --git a/song/row.go b/song/row.go index 1181488..ab39fac 100644 --- a/song/row.go +++ b/song/row.go @@ -2,24 +2,28 @@ package song import "github.com/gotracker/playback/index" -type RowIntf interface { - GetNumChannels() int - GetChannelIntf(index.Channel) ChannelDataIntf +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[TChannelData ChannelData[TVolume], TVolume Volume] []TChannelData +type Row any -func (r Row[TChannelData, TVolume]) GetNumChannels() int { - return len(r) +func GetRowNumChannels[TVolume Volume](r Row) int { + if row, ok := r.(rowIntf[TVolume]); ok { + return row.Len() + } + return 0 } -func (r Row[TChannelData, TVolume]) GetChannel(ch index.Channel) TChannelData { - return r[ch] -} +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 + } -func (r Row[TChannelData, TVolume]) GetChannelIntf(ch index.Channel) ChannelDataIntf { - return r[ch] + return row.ForEach(fn) } type RowStringer interface { diff --git a/song/song.go b/song/song.go index fac57e8..12423e6 100644 --- a/song/song.go +++ b/song/song.go @@ -32,11 +32,11 @@ type Data interface { IsValidInstrumentID(instrument.ID) bool GetInstrument(instrument.ID) (instrument.InstrumentIntf, note.Semitone) GetName() string - GetPatternIntfByOrder(index.Order) (PatternIntf, error) - GetPatternIntf(index.Pattern) (PatternIntf, error) + GetPatternByOrder(index.Order) (Pattern, error) + GetPattern(index.Pattern) (Pattern, error) GetPeriodCalculator() PeriodCalculatorIntf GetInitialOrder() index.Order - GetRowRenderStringer(RowIntf, int, bool) RowStringer + GetRowRenderStringer(Row, int, bool) RowStringer GetSystem() system.System } From 5388f66abc5a3631c4324f531a15c577044a2827 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 12:21:13 -0800 Subject: [PATCH 42/63] Clean up settings object --- player/machine/machine.go | 2 +- player/machine/machine_factory.go | 14 +++++++------- player/machine/settings/usersettings.go | 24 +++++++++++++++--------- player/tracker.go | 17 ++++++++++++----- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/player/machine/machine.go b/player/machine/machine.go index 5c028df..015c041 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -179,7 +179,7 @@ func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetNu } func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) CanOrderLoop() bool { - return m.us.SongLoop.Count != 0 + return m.us.SongLoopCount != 0 } func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetName() string { diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index c5fef92..9d1bb2f 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -51,25 +51,25 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum m.us = us order := songData.GetInitialOrder() - if o, set := us.StartOrderAndRow.Order.Get(); set { + if o, set := us.Start.Order.Get(); set { order = index.Order(o) } var row index.Row - if r, set := us.StartOrderAndRow.Row.Get(); set { + if r, set := us.Start.Row.Get(); set { row = index.Row(r) } sys := songData.GetSystem() bpm := songData.GetInitialBPM() - if us.StartBPM != 0 { - bpm = us.StartBPM + if us.Start.BPM != 0 { + bpm = us.Start.BPM } tempo := songData.GetInitialTempo() - if us.StartTempo != 0 { - tempo = us.StartTempo + if us.Start.Tempo != 0 { + tempo = us.Start.Tempo } if err := m.SetBPM(bpm); err != nil { @@ -189,7 +189,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum InitialOrder: order, InitialRow: row, SongLoopStartingOrder: 0, - SongLoopCount: us.SongLoop.Count, + SongLoopCount: us.SongLoopCount, }); err != nil { return nil, err } diff --git a/player/machine/settings/usersettings.go b/player/machine/settings/usersettings.go index c6576d7..659f44d 100644 --- a/player/machine/settings/usersettings.go +++ b/player/machine/settings/usersettings.go @@ -2,20 +2,26 @@ package settings import ( "github.com/gotracker/playback/index" - "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/tracing" + "github.com/heucuva/optional" ) type UserSettings struct { - Tracer tracing.Tracer - SongLoop feature.SongLoop - StartOrderAndRow feature.StartOrderAndRow - PlayUntilOrderAndRow feature.PlayUntilOrderAndRow + tracing.Tracer + 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 - StartTempo int - StartBPM int } func (s UserSettings) SetTracingTick(order index.Order, row index.Row, tick int) { @@ -71,7 +77,7 @@ func (s UserSettings) TraceChannelWithComment(ch index.Channel, op, comment stri return } - s.TraceChannelWithComment(ch, op, comment) + s.Tracer.TraceChannelWithComment(ch, op, comment) } func (s UserSettings) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { @@ -79,7 +85,7 @@ func (s UserSettings) TraceChannelValueChange(ch index.Channel, op string, prev, return } - s.TraceChannelValueChange(ch, op, prev, new) + s.Tracer.TraceChannelValueChange(ch, op, prev, new) } func (s UserSettings) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, comment string) { diff --git a/player/tracker.go b/player/tracker.go index 93df1d4..6af8602 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/player/machine" @@ -117,15 +118,21 @@ func (t *Tracker) Configure(features []feature.Feature) (settings.UserSettings, for _, feat := range features { switch f := feat.(type) { case feature.SongLoop: - us.SongLoop = f + us.SongLoopCount = f.Count case feature.StartOrderAndRow: - us.StartOrderAndRow = f + 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.PlayUntilOrderAndRow = f + us.PlayUntil.Order.Set(index.Order(f.Order)) + us.PlayUntil.Row.Set(index.Row(f.Row)) case feature.SetDefaultTempo: - us.StartTempo = f.Tempo + us.Start.Tempo = f.Tempo case feature.SetDefaultBPM: - us.StartBPM = f.BPM + us.Start.BPM = f.BPM case feature.IgnoreUnknownEffect: us.IgnoreUnknownEffect = f.Enabled case feature.EnableTracing: From f5f035c398bcac7881db690ab1c151e46c4dc014 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 15:54:16 -0800 Subject: [PATCH 43/63] pipe opl2 code back in --- format/it/layout/channelsetting.go | 5 ++ format/it/layout/song.go | 20 ++++++ format/it/voice/voice.go | 6 +- format/s3m/layout/channelsetting.go | 10 +++ format/s3m/layout/song.go | 31 ++++++++++ format/s3m/load/modconv/modconverter.go | 9 +-- format/s3m/load/s3mformat.go | 17 +++++ format/s3m/voice/voice.go | 56 +++++++++++------ format/xm/layout/channelsetting.go | 5 ++ format/xm/layout/song.go | 20 ++++++ format/xm/voice/voice.go | 6 +- index/channel.go | 10 +++ player/machine/channel_noteaction.go | 8 --- player/machine/machine.go | 10 +-- player/machine/machine_factory.go | 6 +- player/machine/machine_opl2.go | 36 ++++++----- player/machine/machine_tick.go | 75 +++++++++++++---------- player/render/channel.go | 3 +- song/channelsettings.go | 2 + song/song.go | 2 + voice/component/opl2.go | 22 ++++--- voice/config.go | 4 ++ {player/render => voice/opl2}/opl2intf.go | 10 +-- voice/voice.go | 2 + 24 files changed, 274 insertions(+), 101 deletions(-) rename {player/render => voice/opl2}/opl2intf.go (57%) diff --git a/format/it/layout/channelsetting.go b/format/it/layout/channelsetting.go index 9a62201..9b94247 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -4,6 +4,7 @@ import ( "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" ) @@ -67,3 +68,7 @@ func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0Optim MaxTicksAt0: 3, } } + +func (ChannelSetting) GetOPLChannel() index.OPLChannel { + return index.InvalidOPLChannel +} diff --git a/format/it/layout/song.go b/format/it/layout/song.go index eeb7f27..c241f3c 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -204,3 +204,23 @@ func (s Song[TPeriod]) GetRowRenderStringer(row song.Row, channels int, longForm func (s Song[TPeriod]) GetSystem() system.System { return s.System } + +func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { + for i, cs := range s.ChannelSettings { + if enabledOnly && !cs.Enabled { + continue + } + cont, err := fn(index.Channel(i)) + if err != nil { + return err + } + if !cont { + break + } + } + return nil +} + +func (Song[TPeriod]) IsOPL2Enabled() bool { + return false +} diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index b9765ac..1e60964 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -16,6 +16,7 @@ import ( "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/pcm" ) @@ -97,6 +98,9 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, return v } +func (v *itVoice[TPeriod]) SetOPL2Chip(chip opl2.Chip) { +} + func (v *itVoice[TPeriod]) doAttack() { v.vol0Opt.Reset() v.autoVibrato.Reset() @@ -203,7 +207,7 @@ func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[itVolume.FineVolume v.setupPCM(d.Sample, d.Loop, d.SustainLoop, itVolume.MaxItFineVolume, inst.GetDefaultVolume()) default: - return fmt.Errorf("unhandled instrument type: %T", inst) + return fmt.Errorf("unhandled instrument type: %T", d) } if inst == nil { return errors.New("instrument is nil") diff --git a/format/s3m/layout/channelsetting.go b/format/s3m/layout/channelsetting.go index 8bcf0a6..36bd3d2 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -68,3 +69,12 @@ func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0Optim MaxTicksAt0: 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/song.go b/format/s3m/layout/song.go index d630508..91be12e 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -25,6 +25,7 @@ type Song struct { Instruments []*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] Patterns []song.Pattern ChannelSettings []ChannelSetting + ChannelOrders []index.Channel NumChannels int OrderList []index.Pattern } @@ -199,3 +200,33 @@ func (s Song) GetRowRenderStringer(row song.Row, channels int, longFormat bool) func (s Song) GetSystem() system.System { return s.System } + +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 && !cs.Enabled { + continue + } + cont, err := fn(ch) + if err != nil { + return err + } + if !cont { + break + } + } + return nil +} + +func (s Song) IsOPL2Enabled() bool { + for _, cs := range s.ChannelSettings { + if !cs.Enabled { + continue + } + + if cs.GetOPLChannel().IsValid() { + return true + } + } + return false +} diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index d774223..c5f0fcd 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -313,6 +313,7 @@ func Read(r io.Reader) (*s3mfile.File, error) { InstrumentCount: 31, PatternCount: uint16(len(mf.Patterns)), Flags: 0x0004 | 0x0010 | 0x0020, // amigaSlides | amigaLimits | sbFilterEnable + SCRM: [4]byte{'S', 'C', 'R', 'M'}, GlobalVolume: s3mfile.DefaultVolume, InitialSpeed: 6, InitialTempo: 125, @@ -330,12 +331,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 } } diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index 472c671..05ae8b5 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -366,6 +366,23 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, 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 &s, nil } diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index a9e5ce5..be50ace 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -6,18 +6,22 @@ import ( "github.com/gotracker/playback/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" - "github.com/gotracker/playback/voice/loop" - "github.com/gotracker/playback/voice/pcm" + "github.com/gotracker/playback/voice/opl2" ) type s3mVoice struct { - inst *instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + inst *instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + opl2Chip opl2.Chip + opl2Channel index.OPLChannel component.KeyModulator @@ -25,6 +29,7 @@ type s3mVoice struct { component.AmpModulator[s3mVolume.FineVolume, s3mVolume.Volume] component.FreqModulator[period.Amiga] component.PanModulator[s3mPanning.Panning] + opl2 component.OPL2Registers vol0Opt component.Vol0Optimization voiceFilter filter.Filter } @@ -37,7 +42,10 @@ var ( ) 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{} + v := &s3mVoice{ + opl2Chip: config.OPLChip, + opl2Channel: config.OPLChannel, + } v.KeyModulator.Setup(component.KeyModulatorSettings{ Attack: v.doAttack, @@ -65,6 +73,10 @@ func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.Fine return v } +func (v *s3mVoice) SetOPL2Chip(chip opl2.Chip) { + v.opl2Chip = chip +} + func (v *s3mVoice) doAttack() { v.vol0Opt.Reset() @@ -101,10 +113,23 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[s3mVolume.FineVolume, s3mVo case *instrument.PCM[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]: v.AmpModulator.SetMixingVolumeOverride(d.MixingVolume) - v.setupPCM(d.Sample, d.Loop, d.SustainLoop, s3mVolume.MaxFineVolume, inst.GetDefaultVolume()) + 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: + var o component.OPL2[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] + o.Setup(v.opl2Chip, int(v.opl2Channel), v.opl2, s3mPeriod.AmigaConverter, s3mSystem.S3MBaseClock, inst.GetDefaultVolume()) + v.voicer = &o default: - return fmt.Errorf("unhandled instrument type: %T", inst) + return fmt.Errorf("unhandled instrument type: %T", d) } if inst == nil { return errors.New("instrument is nil") @@ -144,6 +169,10 @@ func (v *s3mVoice) Advance() { // 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 { + o.Advance(v.GetFinalVolume(), v.GetFinalPeriod()) + } + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) v.KeyModulator.Advance() } @@ -151,9 +180,12 @@ func (v *s3mVoice) Advance() { func (v *s3mVoice) Clone(bool) voice.Voice { vv := s3mVoice{ inst: v.inst, + opl2Chip: v.opl2Chip, + opl2Channel: v.opl2Channel, AmpModulator: v.AmpModulator.Clone(), FreqModulator: v.FreqModulator.Clone(), PanModulator: v.PanModulator.Clone(), + opl2: v.opl2.Clone(), vol0Opt: v.vol0Opt.Clone(), } @@ -175,15 +207,3 @@ func (v *s3mVoice) Clone(bool) voice.Voice { return &vv } - -func (v *s3mVoice) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol s3mVolume.FineVolume, defVol s3mVolume.Volume) { - var s component.Sampler[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] - s.Setup(component.SamplerSettings[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume]{ - Sample: samp, - DefaultVolume: defVol, - MixVolume: mixVol, - WholeLoop: wholeLoop, - SustainLoop: sustainLoop, - }) - v.voicer = &s -} diff --git a/format/xm/layout/channelsetting.go b/format/xm/layout/channelsetting.go index e06a7da..bf1af55 100644 --- a/format/xm/layout/channelsetting.go +++ b/format/xm/layout/channelsetting.go @@ -4,6 +4,7 @@ import ( "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" ) @@ -61,3 +62,7 @@ func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0Optim MaxTicksAt0: 3, } } + +func (ChannelSetting) GetOPLChannel() index.OPLChannel { + return index.InvalidOPLChannel +} diff --git a/format/xm/layout/song.go b/format/xm/layout/song.go index 43edab2..c9e7f42 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -201,3 +201,23 @@ func (s Song[TPeriod]) GetRowRenderStringer(row song.Row, channels int, longForm func (s Song[TPeriod]) GetSystem() system.System { return s.System } + +func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { + for i, cs := range s.ChannelSettings { + if enabledOnly && !cs.Enabled { + continue + } + cont, err := fn(index.Channel(i)) + if err != nil { + return err + } + if !cont { + break + } + } + return nil +} + +func (Song[TPeriod]) IsOPL2Enabled() bool { + return false +} diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index fff0bc6..6e7caa2 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -14,6 +14,7 @@ import ( "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" "github.com/gotracker/playback/voice/loop" + "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/pcm" ) @@ -77,6 +78,9 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xm return v } +func (v *xmVoice[TPeriod]) SetOPL2Chip(chip opl2.Chip) { +} + func (v *xmVoice[TPeriod]) doAttack() { v.vol0Opt.Reset() v.autoVibrato.ResetAutoVibrato() @@ -156,7 +160,7 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[xmVolume.XmVolume, v.setupPCM(d.Sample, d.Loop, d.SustainLoop, xmVolume.DefaultXmMixingVolume, inst.GetDefaultVolume()) default: - return fmt.Errorf("unhandled instrument type: %T", inst) + return fmt.Errorf("unhandled instrument type: %T", d) } if inst == nil { return errors.New("instrument is nil") diff --git a/index/channel.go b/index/channel.go index dc8de36..307fc9c 100644 --- a/index/channel.go +++ b/index/channel.go @@ -1,3 +1,13 @@ package index type Channel uint8 + +const ( + InvalidOPLChannel = OPLChannel(0xFF) +) + +type OPLChannel uint8 + +func (c OPLChannel) IsValid() bool { + return c != InvalidOPLChannel +} diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index c1d13f2..de3c155 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -3,7 +3,6 @@ package machine import ( "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" - "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/voice" ) @@ -123,10 +122,3 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe } return nil } - -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupOPL2(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], d *instrument.OPL2) error { - panic("unimplemented") - //var o component.OPL2[TPeriod, TVolume] - //o.Setup(chip, channel, reg, baseFreq, keyModulator, defaultVolume) - //c.cv.voicer = &o -} diff --git a/player/machine/machine.go b/player/machine/machine.go index 015c041..bdcd59b 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -18,6 +18,7 @@ import ( "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/song" + "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback/voice/types" ) @@ -131,10 +132,11 @@ type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann ticker ticker age int - songData song.Data - ms *settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - us settings.UserSettings - opl2 render.OPL2Chip + 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 diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 9d1bb2f..372ecc1 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -11,6 +11,7 @@ import ( "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" + "github.com/gotracker/playback/voice/opl2" ) type typeLookup struct { @@ -98,6 +99,8 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum 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) @@ -124,7 +127,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc.OutputFilter = filt } - rc.GetOPL2Chip = func() render.OPL2Chip { + rc.GetOPL2Chip = func() opl2.Chip { return m.opl2 } @@ -148,6 +151,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum c := &m.channels[ch] c.enabled = cs.GetEnabled() c.cv = m.ms.VoiceFactory.NewVoice(voice.VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ + OPLChannel: cs.GetOPLChannel(), InitialVolume: initialVolume, InitialMixing: initialMixing, PanEnabled: cs.IsPanEnabled(), diff --git a/player/machine/machine_opl2.go b/player/machine/machine_opl2.go index 401815e..34eb283 100644 --- a/player/machine/machine_opl2.go +++ b/player/machine/machine_opl2.go @@ -6,30 +6,34 @@ import ( "github.com/gotracker/gomixing/mixing" "github.com/gotracker/gomixing/panning" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" + "github.com/gotracker/playback/voice/opl2" ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setupOPL2(s *sampler.Sampler) error { - if m.opl2 != nil || !m.ms.OPL2Enabled { - return nil + if s == nil { + return errors.New("sampler is nil") } - if s != nil { - return errors.New("sampler is nil") + o := opl2.NewOPL2Chip(uint32(s.SampleRate)) + 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 rc.Voice != nil { + rc.Voice.SetOPL2Chip(m.opl2) + } } - var oplLen int - //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Melody)-1]) - //oplLen += len(m.chOrder[int(s3mfile.ChannelCategoryOPL2Drums)-1]) - - if oplLen > 0 { - o := render.NewOPL2Chip(uint32(s.SampleRate)) - 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.virtualOutputs { + rc := &m.virtualOutputs[i] + if rc.Voice != nil { + rc.Voice.SetOPL2Chip(m.opl2) + } } return nil diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index c9f7988..c80fd9a 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -11,12 +11,22 @@ import ( ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(s *sampler.Sampler) (*output.PremixData, error) { - for i := range m.channels { - if err := m.channels[i].DoNoteAction(index.Channel(i), m, frequency.Frequency(s.SampleRate)); err != nil { + if m.opl2Enabled && m.opl2 == nil && m.ms.OPL2Enabled { + if err := m.setupOPL2(s); err != nil { return nil, err } } + if err := m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] + if err := c.DoNoteAction(ch, m, frequency.Frequency(s.SampleRate)); err != nil { + return false, err + } + return true, nil + }); err != nil { + return nil, err + } + premix, err := m.render(s) if err != nil { return premix, err @@ -34,26 +44,25 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTick() error { - for i := range m.channels { - c := &m.channels[i] - if err := c.Tick(index.Channel(i), m); err != nil { - return err + 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() - } - - return nil + return true, nil + }) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onOrderStart() error { - for ch := range m.channels { - if err := m.channels[ch].OrderStart(index.Channel(ch), m); err != nil { - return err + 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 nil + return true, nil + }) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowStart() error { @@ -74,31 +83,31 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRo return err } - for ch := range m.channels { - if err := m.channels[ch].RowStart(index.Channel(ch), m); 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 nil + return true, nil + }) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowEnd() error { - for ch := range m.channels { - if err := m.channels[ch].RowEnd(index.Channel(ch), m); err != nil { - return err + return 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 nil + return true, nil + }) } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onOrderEnd() error { - for ch := range m.channels { - if err := m.channels[ch].OrderEnd(index.Channel(ch), m); err != nil { - return err + 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 nil + return true, nil + }) } diff --git a/player/render/channel.go b/player/render/channel.go index 2991e5e..743a917 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -8,6 +8,7 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/mixer" + "github.com/gotracker/playback/voice/opl2" ) type ChannelIntf interface { @@ -19,7 +20,7 @@ type ChannelIntf interface { type Channel[TPeriod period.Period] struct { PluginFilter filter.Filter OutputFilter filter.Filter - GetOPL2Chip func() OPL2Chip + GetOPL2Chip func() opl2.Chip GlobalVolume volume.Volume // this is the channel's version of the GlobalVolume Voice voice.Voice } diff --git a/song/channelsettings.go b/song/channelsettings.go index f779dbf..7458202 100644 --- a/song/channelsettings.go +++ b/song/channelsettings.go @@ -3,6 +3,7 @@ package song import ( "errors" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/voice/vol0optimization" ) @@ -14,6 +15,7 @@ type ChannelSettings interface { GetDefaultFilterName() string IsDefaultFilterEnabled() bool GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings + GetOPLChannel() index.OPLChannel } type channelInitialVolumeGetter[TVolume Volume] interface { diff --git a/song/song.go b/song/song.go index 12423e6..6cbf2a4 100644 --- a/song/song.go +++ b/song/song.go @@ -38,6 +38,8 @@ type Data interface { GetInitialOrder() index.Order GetRowRenderStringer(Row, int, bool) RowStringer GetSystem() system.System + ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error + IsOPL2Enabled() bool } type ( diff --git a/voice/component/opl2.go b/voice/component/opl2.go index 2eec771..1533311 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -2,13 +2,13 @@ package component import ( "github.com/gotracker/gomixing/volume" - "github.com/gotracker/opl2" + opl2Impl "github.com/gotracker/opl2" "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/types" ) @@ -28,9 +28,14 @@ type OPL2Registers struct { RegC0 uint8 } +func (o OPL2Registers) Clone() OPL2Registers { + m := o + return m +} + // OPL2 is an OPL2 component type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { - chip render.OPL2Chip + chip opl2.Chip channel int reg OPL2Registers baseFreq frequency.Frequency @@ -40,11 +45,12 @@ type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { } // Setup sets up the OPL2 component -func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Setup(chip render.OPL2Chip, channel int, reg OPL2Registers, baseFreq frequency.Frequency, defaultVolume TVolume) { +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 } @@ -130,10 +136,8 @@ func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Advance(carVol volume.Volume, pe } func (o OPL2[TPeriod, TMixingVolume, TVolume]) Clone() Voicer[TPeriod, TMixingVolume, TVolume] { - var out OPL2[TPeriod, TMixingVolume, TVolume] - out.Setup(o.chip, o.channel, o.reg, o.baseFreq, o.defaultVolume) - out.periodConverter = o.periodConverter - return &out + m := o + return &m } func (o OPL2[TPeriod, TMixingVolume, TVolume]) GetDefaultVolume() TVolume { @@ -204,7 +208,7 @@ func (o *OPL2[TPeriod, TMixingVolume, TVolume]) freqToFnumBlock(freq float64) (u } else { block = 0 } - fnum := uint16(freq * float64(int(1)<<(20-block)) / opl2.OPLRATE) + fnum := uint16(freq * float64(int(1)<<(20-block)) / opl2Impl.OPLRATE) return fnum, block } diff --git a/voice/config.go b/voice/config.go index 2b9c2ea..a1c5517 100644 --- a/voice/config.go +++ b/voice/config.go @@ -3,10 +3,12 @@ package voice import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/envelope" "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/types" "github.com/gotracker/playback/voice/vol0optimization" ) @@ -32,6 +34,8 @@ type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volu } type VoiceConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + OPLChip opl2.Chip + OPLChannel index.OPLChannel InitialVolume TVolume InitialMixing TMixingVolume PanEnabled bool diff --git a/player/render/opl2intf.go b/voice/opl2/opl2intf.go similarity index 57% rename from player/render/opl2intf.go rename to voice/opl2/opl2intf.go index cd0235f..75af19b 100644 --- a/player/render/opl2intf.go +++ b/voice/opl2/opl2intf.go @@ -1,21 +1,21 @@ -package render +package opl2 import ( "github.com/gotracker/opl2" ) -// OPL2Chip sets up a contract that the chip definition will contain these interfaces -type OPL2Chip interface { +// Chip sets up a contract that the chip definition will contain these interfaces +type Chip interface { WriteReg(uint32, uint8) GenerateBlock2(uint, []int32) } // OPL2Intf is an interface to get the active OPL2 chip type OPL2Intf interface { - GetOPL2Chip() OPL2Chip + GetOPL2Chip() Chip } // NewOPL2Chip generates a new OPL2 chip -func NewOPL2Chip(rate uint32) OPL2Chip { +func NewOPL2Chip(rate uint32) Chip { return opl2.NewChip(rate, false) } diff --git a/voice/voice.go b/voice/voice.go index 5cae57d..037797b 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -10,6 +10,7 @@ import ( "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" "github.com/gotracker/playback/tracing" + "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/types" ) @@ -19,6 +20,7 @@ type Voice interface { // Configuration Reset() + SetOPL2Chip(chip opl2.Chip) // Actions Attack() From 2907fec4bb1baf1c9702271c59f7bb63e34ace2b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 15:56:34 -0800 Subject: [PATCH 44/63] fix ordering --- player/machine/machine_globals.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/player/machine/machine_globals.go b/player/machine/machine_globals.go index 9ae3d9a..cc22c18 100644 --- a/player/machine/machine_globals.go +++ b/player/machine/machine_globals.go @@ -149,18 +149,17 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetF cr := m.songData.GetSystem().GetCommonRate() - for i := range m.channels { - c := &m.channels[i] + return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { + c := &m.channels[ch] if enabled { filt, err := filtFactory(cr) if err != nil { - return err + return false, err } c.filter = filt } else { c.filter = nil } - } - - return nil + return true, nil + }) } From 4fc3866387de17b008a81c51d460bad94d4ba71f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 17:05:22 -0800 Subject: [PATCH 45/63] fix for runaway past notes --- index/channel.go | 9 ++- player/machine/channel.go | 3 +- player/machine/channel_pastnote.go | 104 ++++++++++++++++++++--------- player/machine/machine_channel.go | 2 +- player/machine/machine_factory.go | 2 +- player/machine/machine_opl2.go | 8 +-- player/machine/machine_render.go | 6 +- player/machine/machine_tick.go | 2 +- player/render/channel.go | 27 ++++++-- 9 files changed, 112 insertions(+), 51 deletions(-) diff --git a/index/channel.go b/index/channel.go index 307fc9c..b3826cc 100644 --- a/index/channel.go +++ b/index/channel.go @@ -1,11 +1,16 @@ package index -type Channel uint8 - 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 { diff --git a/player/machine/channel.go b/player/machine/channel.go index fc62567..2c1bf06 100644 --- a/player/machine/channel.go +++ b/player/machine/channel.go @@ -8,7 +8,6 @@ import ( "github.com/gotracker/playback/memory" "github.com/gotracker/playback/note" "github.com/gotracker/playback/player/machine/instruction" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/oscillator" @@ -46,7 +45,7 @@ type channel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann nna note.Action cv voice.RenderVoice[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] - pastNotes []*render.Channel[TPeriod] + pastNotes []index.Channel instructions []instruction.Instruction } diff --git a/player/machine/channel_pastnote.go b/player/machine/channel_pastnote.go index fcbd817..9caeb4b 100644 --- a/player/machine/channel_pastnote.go +++ b/player/machine/channel_pastnote.go @@ -1,54 +1,92 @@ package machine import ( + "slices" + "sort" + + "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/voice" ) func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) addPastNote(m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], pn voice.Voice) { - mpnpc := m.songData.GetSystem().GetMaxPastNotesPerChannel() - - // make room - if over := len(c.pastNotes) + 1 - mpnpc; over > 0 { - for _, rc := range c.pastNotes[0:over] { - rc.StopVoice() - } - - c.pastNotes = c.pastNotes[over:] + // 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 { - rc := &m.virtualOutputs[i] + ch := index.Channel(i) + rc := &m.virtualOutputs[ch] - if rc.Voice == nil { - rc.Voice = pn - c.pastNotes = append(c.pastNotes, rc) + 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? - pn.Stop() + 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]) doPastNoteAction(na note.Action) { - for _, pn := range c.pastNotes { - if pn.Voice == nil { +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: - pn.StopVoice() + rc.StopVoice() case note.ActionRelease: - pn.Voice.Release() + v.Release() case note.ActionFadeout: - pn.Voice.Release() - pn.Voice.Fadeout() + v.Release() + v.Fadeout() case note.ActionRetrigger: - pn.Voice.Release() - pn.Voice.Attack() + v.Release() + v.Attack() case note.ActionContinue: fallthrough @@ -58,19 +96,21 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPa } } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) updatePastNotes() { - var updated []*render.Channel[TPeriod] - for _, pn := range c.pastNotes { - if pn.Voice == nil { +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 pn.Voice.IsDone() { - pn.StopVoice() + if v.IsDone() { + rc.StopVoice() continue } - updated = append(updated, pn) + updated = append(updated, ch) } c.pastNotes = updated } diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 1c51183..d7cdc5c 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -514,7 +514,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetC 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(na) + c.doPastNoteAction(m, na) return nil }) } diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 372ecc1..74c5078 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -159,7 +159,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum Vol0Optimization: cs.GetVol0OptimizationSettings(), }) c.memory = cs.GetMemory() - rc.Voice = c.cv + 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 diff --git a/player/machine/machine_opl2.go b/player/machine/machine_opl2.go index 34eb283..a6c83a4 100644 --- a/player/machine/machine_opl2.go +++ b/player/machine/machine_opl2.go @@ -24,15 +24,15 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setu for i := range m.actualOutputs { rc := &m.actualOutputs[i] - if rc.Voice != nil { - rc.Voice.SetOPL2Chip(m.opl2) + if v := rc.GetVoice(); v != nil { + v.SetOPL2Chip(m.opl2) } } for i := range m.virtualOutputs { rc := &m.virtualOutputs[i] - if rc.Voice != nil { - rc.Voice.SetOPL2Chip(m.opl2) + if v := rc.GetVoice(); v != nil { + v.SetOPL2Chip(m.opl2) } } diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 10cdf78..2454d99 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -56,7 +56,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend rc.GlobalVolume = m.gv.ToVolume() - rc.Voice.DumpState(index.Channel(i), m.us.Tracer) + rc.GetVoice().DumpState(index.Channel(i), m.us.Tracer) data, err := rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) if err != nil { return nil, err @@ -79,10 +79,10 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend rc := &m.virtualOutputs[i] var data *mixing.Data - if rc.Voice != nil { + if rc.GetVoice() != nil { rc.GlobalVolume = m.gv.ToVolume() - //rc.Voice.DumpState(index.Channel(i), m.us.Tracer) + //rc.GetVoice().DumpState(index.Channel(i), m.us.Tracer) var err error data, err = rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) if err != nil { diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index c80fd9a..1a36843 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -50,7 +50,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTi return false, err } - c.updatePastNotes() + c.updatePastNotes(m) return true, nil }) } diff --git a/player/render/channel.go b/player/render/channel.go index 743a917..9e0ab6b 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -22,7 +22,9 @@ type Channel[TPeriod period.Period] struct { OutputFilter filter.Filter GetOPL2Chip func() opl2.Chip GlobalVolume volume.Volume // this is the channel's version of the GlobalVolume - Voice voice.Voice + + v voice.Voice + vrem func() // function to call when voice is stopped/removed } func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { @@ -34,7 +36,7 @@ func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], filt.SetPlaybackRate(details.SampleRate) } - data, err := voice.RenderAndAdvance(c.Voice, pc, centerAheadPan, details, c) + data, err := voice.RenderAndAdvance(c.v, pc, centerAheadPan, details, c) if err != nil { return nil, err } @@ -45,13 +47,28 @@ func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], return data, nil } +func (c Channel[TPeriod]) GetVoice() voice.Voice { + return c.v +} + +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.Voice == nil { + if c.v == nil { return } - c.Voice.Stop() - c.Voice = nil + 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. From 91c7806045a1a54e6a1b515865c162ca21ebecbf Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 17:51:29 -0800 Subject: [PATCH 46/63] fix vol0 opt code - fix s3m/mod portamentos - fix channel instructions sticking between rows --- format/it/layout/channelsetting.go | 4 +-- format/it/voice/voice.go | 7 +++-- .../s3m/channel/effect_extrafineportadown.go | 3 +- format/s3m/channel/effect_extrafineportaup.go | 3 +- format/s3m/channel/effect_fineportadown.go | 3 +- format/s3m/channel/effect_fineportaup.go | 3 +- format/s3m/channel/effect_portadown.go | 5 ++-- format/s3m/channel/effect_portatonote.go | 5 ++-- format/s3m/channel/effect_portaup.go | 5 ++-- format/s3m/channel/machine.go | 28 ------------------- format/s3m/layout/channelsetting.go | 4 +-- format/s3m/voice/voice.go | 7 +++-- format/xm/layout/channelsetting.go | 4 +-- format/xm/voice/voice.go | 7 +++-- player/machine/channel_noteaction.go | 3 ++ player/machine/machine.go | 8 ++++-- player/machine/machine_render.go | 4 +-- player/machine/machine_tick.go | 22 +++++++++++++-- player/machine/machine_tracing.go | 2 +- player/machine/pastnote.go | 2 +- player/render/channel.go | 4 +-- voice/component/vol0optimization.go | 14 +++++----- voice/render.go | 4 +-- voice/voice.go | 3 +- voice/vol0optimization/settings.go | 4 +-- 25 files changed, 85 insertions(+), 73 deletions(-) diff --git a/format/it/layout/channelsetting.go b/format/it/layout/channelsetting.go index 9b94247..2263def 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -64,8 +64,8 @@ func (c ChannelSetting) IsDefaultFilterEnabled() bool { func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { return vol0optimization.Vol0OptimizationSettings{ - Enabled: c.Vol0OptEnabled, - MaxTicksAt0: 3, + Enabled: c.Vol0OptEnabled, + MaxRowsAt0: 3, } } diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 1e60964..c5d49fb 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -259,7 +259,7 @@ func (v *itVoice[TPeriod]) IsDone() bool { return v.vol0Opt.IsDone() } -func (v *itVoice[TPeriod]) Advance() { +func (v *itVoice[TPeriod]) Tick() { v.fadeout.Advance() v.autoVibrato.Advance() v.pitchPan.Advance() @@ -292,12 +292,15 @@ func (v *itVoice[TPeriod]) Advance() { // has to be after the mod/env updates v.KeyModulator.DeferredUpdate() - v.vol0Opt.ObserveVolume(v.GetFinalVolume()) v.KeyModulator.Advance() v.updateFinal() } +func (v *itVoice[TPeriod]) RowEnd() { + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) +} + func (v *itVoice[TPeriod]) Clone(background bool) voice.Voice { vv := itVoice[TPeriod]{ inst: v.inst, diff --git a/format/s3m/channel/effect_extrafineportadown.go b/format/s3m/channel/effect_extrafineportadown.go index 112e020..d90b97c 100644 --- a/format/s3m/channel/effect_extrafineportadown.go +++ b/format/s3m/channel/effect_extrafineportadown.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -23,7 +24,7 @@ func (e ExtraFinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amig } y := DataEffect(e) & 0x0F - return doPortaDown(ch, m, float32(y), 1) + return m.DoChannelPortaDown(ch, period.Delta(y)*4*s3mSystem.SlideFinesPerSemitone) } func (e ExtraFinePortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_extrafineportaup.go b/format/s3m/channel/effect_extrafineportaup.go index c6fcfe1..ab027ed 100644 --- a/format/s3m/channel/effect_extrafineportaup.go +++ b/format/s3m/channel/effect_extrafineportaup.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -23,7 +24,7 @@ func (e ExtraFinePortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, } y := DataEffect(e) & 0x0F - return doPortaUp(ch, m, float32(y), 1) + return m.DoChannelPortaUp(ch, period.Delta(y)*1*s3mSystem.SlideFinesPerSemitone) } func (e ExtraFinePortaUp) TraceData() string { diff --git a/format/s3m/channel/effect_fineportadown.go b/format/s3m/channel/effect_fineportadown.go index ee9b3fb..33e2a66 100644 --- a/format/s3m/channel/effect_fineportadown.go +++ b/format/s3m/channel/effect_fineportadown.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -23,7 +24,7 @@ func (e FinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3 } y := DataEffect(e) & 0x0F - return doPortaDown(ch, m, float32(y), 4) + return m.DoChannelPortaDown(ch, period.Delta(y)*4*s3mSystem.SlideFinesPerSemitone) } func (e FinePortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_fineportaup.go b/format/s3m/channel/effect_fineportaup.go index 79a3c24..36b6fe0 100644 --- a/format/s3m/channel/effect_fineportaup.go +++ b/format/s3m/channel/effect_fineportaup.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -23,7 +24,7 @@ func (e FinePortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV } y := DataEffect(e) & 0x0F - return doPortaUp(ch, m, float32(y), 4) + return m.DoChannelPortaUp(ch, period.Delta(y)*4*s3mSystem.SlideFinesPerSemitone) } func (e FinePortaUp) TraceData() string { diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go index 8a1f558..5cd41ed 100644 --- a/format/s3m/channel/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -25,11 +26,11 @@ func (e PortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVol xx := mem.LastNonZero(DataEffect(e)) - if tick == 0 { + if tick == 0 && !mem.Shared.AmigaSlides { return nil } - return m.DoChannelPortaDown(ch, period.Delta(xx)*4) + return m.DoChannelPortaDown(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) } func (e PortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 5fd7368..133d252 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -29,11 +30,11 @@ func (e PortaToNote) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV xx := mem.PortaToNote(DataEffect(e)) - if tick == 0 { + if tick == 0 && !mem.Shared.AmigaSlides { return nil } - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) } func (e PortaToNote) TraceData() string { diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go index 9484352..91892ab 100644 --- a/format/s3m/channel/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -4,6 +4,7 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" + s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -25,11 +26,11 @@ func (e PortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum xx := mem.LastNonZero(DataEffect(e)) - if tick == 0 { + if tick == 0 && !mem.Shared.AmigaSlides { return nil } - return m.DoChannelPortaUp(ch, period.Delta(xx)*4) + return m.DoChannelPortaUp(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) } func (e PortaUp) TraceData() string { diff --git a/format/s3m/channel/machine.go b/format/s3m/channel/machine.go index f81c032..d9145fa 100644 --- a/format/s3m/channel/machine.go +++ b/format/s3m/channel/machine.go @@ -17,34 +17,6 @@ func withOscillatorDo(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum return fn(value) } -func doPortaUp(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], amount float32, multiplier float32) error { - cur, err := m.GetChannelPeriod(ch) - if err != nil { - return err - } - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = cur.PortaUp(delta) - return m.SetChannelPeriod(ch, cur) -} - -func doPortaDown(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], amount float32, multiplier float32) error { - cur, err := m.GetChannelPeriod(ch) - if err != nil { - return err - } - if cur.IsInvalid() { - return nil - } - - delta := int(amount * multiplier) - cur = cur.PortaDown(delta) - return m.SetChannelPeriod(ch, cur) -} - 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: diff --git a/format/s3m/layout/channelsetting.go b/format/s3m/layout/channelsetting.go index 36bd3d2..421ea60 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -65,8 +65,8 @@ func (c ChannelSetting) IsDefaultFilterEnabled() bool { func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { return vol0optimization.Vol0OptimizationSettings{ - Enabled: c.Memory.Shared.ZeroVolOptimization, - MaxTicksAt0: 3, + Enabled: c.Memory.Shared.ZeroVolOptimization, + MaxRowsAt0: 3, } } diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index be50ace..011d355 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -165,7 +165,7 @@ func (v *s3mVoice) IsDone() bool { return v.vol0Opt.IsDone() } -func (v *s3mVoice) Advance() { +func (v *s3mVoice) Tick() { // has to be after the mod/env updates v.KeyModulator.DeferredUpdate() @@ -173,10 +173,13 @@ func (v *s3mVoice) Advance() { o.Advance(v.GetFinalVolume(), v.GetFinalPeriod()) } - v.vol0Opt.ObserveVolume(v.GetFinalVolume()) v.KeyModulator.Advance() } +func (v *s3mVoice) RowEnd() { + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) +} + func (v *s3mVoice) Clone(bool) voice.Voice { vv := s3mVoice{ inst: v.inst, diff --git a/format/xm/layout/channelsetting.go b/format/xm/layout/channelsetting.go index bf1af55..2d2d48a 100644 --- a/format/xm/layout/channelsetting.go +++ b/format/xm/layout/channelsetting.go @@ -58,8 +58,8 @@ func (c ChannelSetting) IsDefaultFilterEnabled() bool { func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { return vol0optimization.Vol0OptimizationSettings{ - Enabled: true, - MaxTicksAt0: 3, + Enabled: true, + MaxRowsAt0: 3, } } diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 6e7caa2..f126a23 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -207,7 +207,7 @@ func (v *xmVoice[TPeriod]) IsDone() bool { return v.vol0Opt.IsDone() } -func (v *xmVoice[TPeriod]) Advance() { +func (v *xmVoice[TPeriod]) Tick() { v.fadeout.Advance() v.autoVibrato.Advance() if v.IsVolumeEnvelopeEnabled() { @@ -224,10 +224,13 @@ func (v *xmVoice[TPeriod]) Advance() { // has to be after the mod/env updates v.KeyModulator.DeferredUpdate() - v.vol0Opt.ObserveVolume(v.GetFinalVolume()) v.KeyModulator.Advance() } +func (v *xmVoice[TPeriod]) RowEnd() { + v.vol0Opt.ObserveVolume(v.GetFinalVolume()) +} + func (v *xmVoice[TPeriod]) Clone(bool) voice.Voice { vv := xmVoice[TPeriod]{ inst: v.inst, diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index de3c155..6956b70 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -92,8 +92,11 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo 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() } diff --git a/player/machine/machine.go b/player/machine/machine.go index bdcd59b..98e1a6d 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -226,16 +226,18 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) getR } 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 false, nil + return true, nil } c := &m.channels[ch] - c.instructions = nil - if !c.enabled || d == nil { return true, nil } diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 2454d99..492e969 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -57,7 +57,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend rc.GlobalVolume = m.gv.ToVolume() rc.GetVoice().DumpState(index.Channel(i), m.us.Tracer) - data, err := rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) + data, err := rc.RenderAndTick(m.ms.PeriodConverter, centerAheadPan, details) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend //rc.GetVoice().DumpState(index.Channel(i), m.us.Tracer) var err error - data, err = rc.RenderAndAdvance(m.ms.PeriodConverter, centerAheadPan, details) + data, err = rc.RenderAndTick(m.ms.PeriodConverter, centerAheadPan, details) if err != nil { return nil, err } diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index 1a36843..8e0367d 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -93,13 +93,31 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRo } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onRowEnd() error { - return m.songData.ForEachChannel(true, func(ch index.Channel) (bool, 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 { diff --git a/player/machine/machine_tracing.go b/player/machine/machine_tracing.go index 499e2e4..c3dfdeb 100644 --- a/player/machine/machine_tracing.go +++ b/player/machine/machine_tracing.go @@ -68,7 +68,7 @@ func traceChannelWithComment[TPeriod Period, TGlobalVolume, TMixingVolume, TVolu m.us.TraceChannelWithComment(ch, name, comment) } -func traceChannelOptionalValueClear[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]) { +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) } diff --git a/player/machine/pastnote.go b/player/machine/pastnote.go index b7d5b0f..94552d7 100644 --- a/player/machine/pastnote.go +++ b/player/machine/pastnote.go @@ -13,5 +13,5 @@ type pastNote[TPeriod Period] struct { } func (p pastNote[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { - return p.rc.RenderAndAdvance(pc, centerAheadPan, details) + return p.rc.RenderAndTick(pc, centerAheadPan, details) } diff --git a/player/render/channel.go b/player/render/channel.go index 9e0ab6b..15d816e 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -27,7 +27,7 @@ type Channel[TPeriod period.Period] struct { vrem func() // function to call when voice is stopped/removed } -func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details) (*mixing.Data, error) { +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) } @@ -36,7 +36,7 @@ func (c *Channel[TPeriod]) RenderAndAdvance(pc period.PeriodConverter[TPeriod], filt.SetPlaybackRate(details.SampleRate) } - data, err := voice.RenderAndAdvance(c.v, pc, centerAheadPan, details, c) + data, err := voice.RenderAndTick(c.v, pc, centerAheadPan, details, c) if err != nil { return nil, err } diff --git a/voice/component/vol0optimization.go b/voice/component/vol0optimization.go index 393f6f4..b05571e 100644 --- a/voice/component/vol0optimization.go +++ b/voice/component/vol0optimization.go @@ -16,7 +16,7 @@ type Vol0Optimization struct { enabled bool } keyed struct { - ticksAt0 int + rowsAt0 int } } @@ -36,26 +36,26 @@ func (c *Vol0Optimization) SetEnabled(enabled bool) { } func (c *Vol0Optimization) Reset() { - c.keyed.ticksAt0 = 0 + c.keyed.rowsAt0 = 0 } func (c *Vol0Optimization) ObserveVolume(v volume.Volume) { if c.unkeyed.enabled { if v == 0 { - c.keyed.ticksAt0++ + c.keyed.rowsAt0++ } else { - c.keyed.ticksAt0 = 0 + c.keyed.rowsAt0 = 0 } } } func (c Vol0Optimization) IsDone() bool { - return c.unkeyed.enabled && c.keyed.ticksAt0 >= c.settings.MaxTicksAt0 + return c.unkeyed.enabled && c.keyed.rowsAt0 >= c.settings.MaxRowsAt0 } func (c Vol0Optimization) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} ticksAt0{%v}", + t.TraceChannelWithComment(ch, fmt.Sprintf("enabled{%v} rowsAt0{%v}", c.unkeyed.enabled, - c.keyed.ticksAt0, + c.keyed.rowsAt0, ), comment) } diff --git a/voice/render.go b/voice/render.go index 5a3ba9d..e2da9ec 100644 --- a/voice/render.go +++ b/voice/render.go @@ -8,12 +8,12 @@ import ( "github.com/gotracker/playback/voice/mixer" ) -func RenderAndAdvance[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], centerAheadPan volume.Matrix, details mixer.Details, out mixer.ApplyFilter) (*mixing.Data, error) { +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.Advance() + defer in.Tick() rs, ok := in.(RenderSampler[TPeriod]) if !ok { diff --git a/voice/voice.go b/voice/voice.go index 037797b..c919099 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -29,7 +29,8 @@ type Voice interface { Stop() // State Machine Update - Advance() + Tick() + RowEnd() // General Parameters IsDone() bool diff --git a/voice/vol0optimization/settings.go b/voice/vol0optimization/settings.go index 913ceea..5c06271 100644 --- a/voice/vol0optimization/settings.go +++ b/voice/vol0optimization/settings.go @@ -1,6 +1,6 @@ package vol0optimization type Vol0OptimizationSettings struct { - Enabled bool - MaxTicksAt0 int + Enabled bool + MaxRowsAt0 int } From cac239bab8ff8e4842dbd202414fdedee06161fb Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 19:56:45 -0800 Subject: [PATCH 47/63] safer amiga limits --- format/it/layout/noteinstrument.go | 5 +- format/it/layout/song.go | 9 +++- format/it/load/instrument.go | 31 +++++++----- format/it/load/itformat.go | 18 ++++--- format/it/period/amigaconverter.go | 6 ++- format/it/voice/enveloper_filter.go | 18 ++++--- format/it/voice/enveloper_pan.go | 13 +++-- format/it/voice/enveloper_pitch.go | 13 +++-- format/it/voice/enveloper_volume.go | 13 +++-- format/it/voice/modulator_amp.go | 22 ++++----- format/it/voice/modulator_freq.go | 15 +++--- format/it/voice/modulator_pan.go | 16 +++---- format/it/voice/voice.go | 60 ++++++++++++++---------- format/s3m/layout/song.go | 9 +++- format/s3m/load/s3mformat.go | 31 +++++++----- format/s3m/period/amigaconverter.go | 15 +++++- format/s3m/s3m.go | 2 +- format/s3m/settings/machine.go | 21 +++++++-- format/s3m/voice/voice.go | 34 +++++++++----- format/xm/layout/song.go | 9 +++- format/xm/load/xmformat.go | 27 ++++++----- format/xm/period/amigaconverter.go | 4 +- format/xm/voice/enveloper_pan.go | 13 +++-- format/xm/voice/enveloper_volume.go | 13 +++-- format/xm/voice/modulator_amp.go | 18 ++++--- format/xm/voice/modulator_freq.go | 19 ++++---- format/xm/voice/modulator_pan.go | 8 ++-- format/xm/voice/voice.go | 36 ++++++++------ instrument/instrument.go | 50 +++++++++----------- period/amigaconverter.go | 47 +++++++++++++++---- period/linearconverter.go | 33 +++++++++++-- period/period.go | 37 --------------- period/periodconverter.go | 3 ++ player/machine/channel.go | 6 +-- player/machine/channel_notedecode.go | 6 +-- player/machine/machine.go | 2 +- player/machine/machine_channel.go | 21 ++++++--- player/machine/machine_factory.go | 10 +++- player/machine/newnoteinfo.go | 2 +- song/periodcalculator.go | 3 ++ song/song.go | 1 + voice/autovibrato/settings.go | 10 ++-- voice/component/envelope.go | 28 ++++++----- voice/component/modulator_amp.go | 34 ++++++++------ voice/component/modulator_autovibrato.go | 20 ++++---- voice/component/modulator_fadeout.go | 6 ++- voice/component/modulator_freq.go | 30 +++++++----- voice/component/modulator_pan.go | 27 ++++++----- voice/component/modulator_pitchpan.go | 17 +++---- voice/component/vol0optimization.go | 3 +- voice/config.go | 22 +-------- voice/render.go | 5 +- voice/voice.go | 48 +++++++++---------- 53 files changed, 571 insertions(+), 398 deletions(-) diff --git a/format/it/layout/noteinstrument.go b/format/it/layout/noteinstrument.go index 2e716c0..9850c86 100644 --- a/format/it/layout/noteinstrument.go +++ b/format/it/layout/noteinstrument.go @@ -5,10 +5,11 @@ import ( 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[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + Inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] } diff --git a/format/it/layout/song.go b/format/it/layout/song.go index c241f3c..bfcd4ae 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -23,9 +23,10 @@ import ( // Song is the full definition of the song data of an IT file type Song[TPeriod period.Period] struct { System system.System + MS any Head Header - Instruments map[uint8]*instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] - InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument + Instruments map[uint8]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument[TPeriod] Patterns []song.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern @@ -224,3 +225,7 @@ func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel func (Song[TPeriod]) IsOPL2Enabled() bool { return false } + +func (s Song[TPeriod]) GetMachineSettings() any { + return s.MS +} diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index cb9738c..151c217 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -10,6 +10,7 @@ import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" "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/util" "github.com/gotracker/playback/voice/autovibrato" @@ -30,8 +31,8 @@ import ( oscillatorImpl "github.com/gotracker/playback/oscillator" ) -type convInst struct { - Inst *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] +type convInst[TPeriod period.Period] struct { + Inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] NR []noteRemap } @@ -41,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 @@ -71,8 +72,11 @@ func convertITInstrumentOldToInstrument(inst *itfile.IMPIInstrumentOld, sampData }, } - ii := instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + 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 { @@ -136,8 +140,8 @@ 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.Factory, 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 @@ -172,8 +176,9 @@ func convertITInstrumentToInstrument(inst *itfile.IMPIInstrument, sampData []itf }, } - ii := instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ - Static: instrument.StaticValues[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + ii := instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + Static: instrument.StaticValues[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ + PC: pc, FilterFactory: channelFilterFactory, PluginFilter: pluginFilterFactory, }, @@ -293,7 +298,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 { @@ -308,7 +313,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{ @@ -355,7 +360,7 @@ func itAutoVibratoWSToProtrackerWS(vibtype uint8) uint8 { } } -func addSampleInfoToConvertedInstrument(ii *instrument.Instrument[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 { +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 @@ -460,7 +465,7 @@ func addSampleInfoToConvertedInstrument(ii *instrument.Instrument[itVolume.FineV ii.Static.Filename = si.Header.GetFilename() ii.Static.Name = si.Header.GetName() ii.SampleRate = frequency.Frequency(si.Header.C5Speed) - ii.Static.AutoVibrato = autovibrato.AutoVibratoSettings{ + ii.Static.AutoVibrato = autovibrato.AutoVibratoSettings[TPeriod]{ Enabled: (si.Header.VibratoDepth != 0 && si.Header.VibratoSpeed != 0 && si.Header.VibratoSweep != 0), Sweep: 255, WaveformSelection: itAutoVibratoWSToProtrackerWS(si.Header.VibratoType), diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index 1fa1959..c31bfed 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -15,6 +15,7 @@ import ( "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" @@ -108,11 +109,14 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] stereoMode := f.Head.Flags.IsStereo() vol0Enabled := f.Head.Flags.IsVol0Optimizations() + ms := settings.GetMachineSettings[TPeriod]() + songData := &layout.Song[TPeriod]{ System: itSystem.ITSystem, + MS: ms, Head: *h, - Instruments: make(map[uint8]*instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning]), - InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument), + Instruments: make(map[uint8]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]), + InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument[TPeriod]), Patterns: make([]song.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.OrderCount)), FilterPlugins: make(map[int]filter.Factory), @@ -142,7 +146,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] } 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 } @@ -152,7 +156,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] } case *itfile.IMPIInstrument: - instMap, err := convertITInstrumentToInstrument(ii, f.Samples, convSettings, songData.FilterPlugins, features) + instMap, err := convertITInstrumentToInstrument(ii, ms.PeriodConverter, f.Samples, convSettings, songData.FilterPlugins, features) if err != nil { return nil, err } @@ -230,7 +234,7 @@ type noteRemap struct { Remap note.Semitone } -func addSampleWithNoteMapToSong[TPeriod period.Period](song *layout.Song[TPeriod], sample *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning], sts []noteRemap, instNum int) { +func addSampleWithNoteMapToSong[TPeriod period.Period](song *layout.Song[TPeriod], sample *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], sts []noteRemap, instNum int) { if sample == nil { return } @@ -246,11 +250,11 @@ func addSampleWithNoteMapToSong[TPeriod period.Period](song *layout.Song[TPeriod } inm, ok := song.InstrumentNoteMap[id.InstID] if !ok { - inm = make(map[note.Semitone]layout.NoteInstrument) + inm = make(map[note.Semitone]layout.NoteInstrument[TPeriod]) song.InstrumentNoteMap[id.InstID] = inm } for _, st := range sts { - inm[st.Orig] = layout.NoteInstrument{ + inm[st.Orig] = layout.NoteInstrument[TPeriod]{ NoteRemap: st.Remap, Inst: sample, } diff --git a/format/it/period/amigaconverter.go b/format/it/period/amigaconverter.go index a0cda98..d5572b9 100644 --- a/format/it/period/amigaconverter.go +++ b/format/it/period/amigaconverter.go @@ -1,10 +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, + System: system.ITSystem, + MinPeriod: 1, + MaxPeriod: math.MaxUint16, } diff --git a/format/it/voice/enveloper_filter.go b/format/it/voice/enveloper_filter.go index 9b00352..2f9402e 100644 --- a/format/it/voice/enveloper_filter.go +++ b/format/it/voice/enveloper_filter.go @@ -2,18 +2,17 @@ package voice // == FilterEnveloper == -func (v *itVoice[TPeriod]) EnableFilterEnvelope(enabled bool) { +func (v *itVoice[TPeriod]) EnableFilterEnvelope(enabled bool) error { if !v.pitchAndFilterEnvShared { - v.filterEnv.SetEnabled(enabled) - return + return v.filterEnv.SetEnabled(enabled) } // shared filter/pitch envelope if !v.filterEnvActive { - return + return nil } - v.filterEnv.SetEnabled(enabled) + return v.filterEnv.SetEnabled(enabled) } func (v itVoice[TPeriod]) IsFilterEnvelopeEnabled() bool { @@ -27,12 +26,17 @@ func (v itVoice[TPeriod]) GetCurrentFilterEnvelope() uint8 { return v.filterEnv.GetCurrentValue() } -func (v *itVoice[TPeriod]) SetFilterEnvelopePosition(pos int) { +func (v *itVoice[TPeriod]) SetFilterEnvelopePosition(pos int) error { if !v.pitchAndFilterEnvShared || v.filterEnvActive { - if doneCB := v.filterEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB, err := v.filterEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { doneCB(v) } } + return nil } func (v itVoice[TPeriod]) GetFilterEnvelopePosition() int { diff --git a/format/it/voice/enveloper_pan.go b/format/it/voice/enveloper_pan.go index 8594551..8e6bf5d 100644 --- a/format/it/voice/enveloper_pan.go +++ b/format/it/voice/enveloper_pan.go @@ -6,8 +6,8 @@ import ( // == PanEnveloper == -func (v *itVoice[TPeriod]) EnablePanEnvelope(enabled bool) { - v.panEnv.SetEnabled(enabled) +func (v *itVoice[TPeriod]) EnablePanEnvelope(enabled bool) error { + return v.panEnv.SetEnabled(enabled) } func (v itVoice[TPeriod]) IsPanEnvelopeEnabled() bool { @@ -21,10 +21,15 @@ func (v itVoice[TPeriod]) GetCurrentPanEnvelope() itPanning.Panning { return itPanning.DefaultPanning } -func (v *itVoice[TPeriod]) SetPanEnvelopePosition(pos int) { - if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { +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 { diff --git a/format/it/voice/enveloper_pitch.go b/format/it/voice/enveloper_pitch.go index 7567833..b9c1d96 100644 --- a/format/it/voice/enveloper_pitch.go +++ b/format/it/voice/enveloper_pitch.go @@ -6,8 +6,8 @@ import ( // == PitchEnveloper == -func (v *itVoice[TPeriod]) EnablePitchEnvelope(enabled bool) { - v.pitchEnv.SetEnabled(enabled) +func (v *itVoice[TPeriod]) EnablePitchEnvelope(enabled bool) error { + return v.pitchEnv.SetEnabled(enabled) } func (v itVoice[TPeriod]) IsPitchEnvelopeEnabled() bool { @@ -24,12 +24,17 @@ func (v itVoice[TPeriod]) GetCurrentPitchEnvelope() period.Delta { return 0 } -func (v *itVoice[TPeriod]) SetPitchEnvelopePosition(pos int) { +func (v *itVoice[TPeriod]) SetPitchEnvelopePosition(pos int) error { if !v.pitchAndFilterEnvShared || !v.filterEnvActive { - if doneCB := v.pitchEnv.SetEnvelopePosition(pos); doneCB != nil { + doneCB, err := v.pitchEnv.SetEnvelopePosition(pos) + if err != nil { + return err + } + if doneCB != nil { doneCB(v) } } + return nil } func (v itVoice[TPeriod]) GetPitchEnvelopePosition() int { diff --git a/format/it/voice/enveloper_volume.go b/format/it/voice/enveloper_volume.go index 931a45f..2730568 100644 --- a/format/it/voice/enveloper_volume.go +++ b/format/it/voice/enveloper_volume.go @@ -6,8 +6,8 @@ import ( // == VolumeEnveloper == -func (v *itVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) { - v.volEnv.SetEnabled(enabled) +func (v *itVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) error { + return v.volEnv.SetEnabled(enabled) } func (v itVoice[TPeriod]) IsVolumeEnvelopeEnabled() bool { @@ -21,10 +21,15 @@ func (v itVoice[TPeriod]) GetCurrentVolumeEnvelope() itVolume.Volume { return itVolume.Volume(itVolume.MaxItVolume) } -func (v *itVoice[TPeriod]) SetVolumeEnvelopePosition(pos int) { - if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { +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 { diff --git a/format/it/voice/modulator_amp.go b/format/it/voice/modulator_amp.go index a778d8d..99dfb54 100644 --- a/format/it/voice/modulator_amp.go +++ b/format/it/voice/modulator_amp.go @@ -9,45 +9,43 @@ import ( // == AmpModulator == -func (v *itVoice[TPeriod]) SetActive(on bool) { - v.amp.SetActive(on) +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) { - if !vol.IsUseInstrumentVol() { - v.amp.SetMixingVolume(vol) - } +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]) { - v.amp.SetMixingVolumeOverride(mvo) +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) { +func (v *itVoice[TPeriod]) SetVolume(vol itVolume.Volume) error { if vol.IsUseInstrumentVol() { vol = v.voicer.GetDefaultVolume() } - v.amp.SetVolume(vol) + return v.amp.SetVolume(vol) } func (v itVoice[TPeriod]) GetVolume() itVolume.Volume { return v.amp.GetVolume() } -func (v *itVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) { - v.amp.SetVolumeDelta(d) +func (v *itVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) error { + return v.amp.SetVolumeDelta(d) } func (v itVoice[TPeriod]) GetVolumeDelta() types.VolumeDelta { diff --git a/format/it/voice/modulator_freq.go b/format/it/voice/modulator_freq.go index 6c2313b..ce9f946 100644 --- a/format/it/voice/modulator_freq.go +++ b/format/it/voice/modulator_freq.go @@ -6,24 +6,25 @@ import ( // == FreqModulator == -func (v *itVoice[TPeriod]) SetPeriod(period TPeriod) { - if !period.IsInvalid() { - v.freq.SetPeriod(period) +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) { - v.freq.SetPeriodDelta(delta) +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 { - return v.finalPeriod +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 index 4e9451d..c4cdf78 100644 --- a/format/it/voice/modulator_pan.go +++ b/format/it/voice/modulator_pan.go @@ -9,16 +9,16 @@ import ( // == PanModulator == -func (v *itVoice[TPeriod]) SetPan(pan itPanning.Panning) { - v.pan.SetPan(pan) +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) { - v.pan.SetPanDelta(d) +func (v *itVoice[TPeriod]) SetPanDelta(d types.PanDelta) error { + return v.pan.SetPanDelta(d) } func (v itVoice[TPeriod]) GetPanDelta() types.PanDelta { @@ -29,12 +29,12 @@ func (v itVoice[TPeriod]) GetPanSeparation() float32 { return v.pitchPan.GetPanSeparation() } -func (v *itVoice[TPeriod]) SetPitchPanNote(st note.Semitone) { - v.pitchPan.SetPitch(st) +func (v *itVoice[TPeriod]) SetPitchPanNote(st note.Semitone) error { + return v.pitchPan.SetPitch(st) } -func (v *itVoice[TPeriod]) EnablePitchPan(enabled bool) { - v.pitchPan.EnablePitchPan(enabled) +func (v *itVoice[TPeriod]) EnablePitchPan(enabled bool) error { + return v.pitchPan.EnablePitchPan(enabled) } func (v itVoice[TPeriod]) IsPitchPanEnabled() bool { diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index c5d49fb..e353981 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -25,7 +25,7 @@ type Period interface { } type itVoice[TPeriod Period] struct { - inst *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] background bool pitchAndFilterEnvShared bool @@ -86,7 +86,9 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, DefaultVolume: config.InitialVolume, }) - v.freq.Setup(component.FreqModulatorSettings[TPeriod]{}) + v.freq.Setup(component.FreqModulatorSettings[TPeriod]{ + PC: config.PC, + }) v.pan.Setup(component.PanModulatorSettings[itPanning.Panning]{ Enabled: config.PanEnabled, @@ -159,7 +161,7 @@ func (v itVoice[TPeriod]) getFadeoutEnabled() bool { return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) } -func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[itVolume.FineVolume, itVolume.Volume, itPanning.Panning], outputRate frequency.Frequency) error { +func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], outputRate frequency.Frequency) error { v.inst = inst v.voicer = nil @@ -213,7 +215,7 @@ func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[itVolume.FineVolume return errors.New("instrument is nil") } - v.autoVibrato.Setup(inst.GetAutoVibrato()) + v.autoVibrato.Setup(inst.Static.AutoVibrato) if factory := inst.GetFilterFactory(); factory != nil { v.voiceFilter = factory(inst.SampleRate) @@ -226,20 +228,22 @@ func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[itVolume.FineVolume return nil } -func (v *itVoice[TPeriod]) Reset() { +func (v *itVoice[TPeriod]) Reset() error { v.KeyModulator.Release() - 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() + 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() { @@ -259,7 +263,7 @@ func (v *itVoice[TPeriod]) IsDone() bool { return v.vol0Opt.IsDone() } -func (v *itVoice[TPeriod]) Tick() { +func (v *itVoice[TPeriod]) Tick() error { v.fadeout.Advance() v.autoVibrato.Advance() v.pitchPan.Advance() @@ -294,11 +298,12 @@ func (v *itVoice[TPeriod]) Tick() { v.KeyModulator.Advance() - v.updateFinal() + return v.updateFinal() } -func (v *itVoice[TPeriod]) RowEnd() { +func (v *itVoice[TPeriod]) RowEnd() error { v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + return nil } func (v *itVoice[TPeriod]) Clone(background bool) voice.Voice { @@ -355,10 +360,10 @@ func (v *itVoice[TPeriod]) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop v.voicer = &s } -func (v *itVoice[TPeriod]) updateFinal() { +func (v *itVoice[TPeriod]) updateFinal() error { if v.IsDone() { v.finalVol = 0 - return + return nil } // volume @@ -372,10 +377,16 @@ func (v *itVoice[TPeriod]) updateFinal() { v.finalVol = vol * volEnv * fadeVol // period - p := v.freq.GetFinalPeriod() + p, err := v.freq.GetFinalPeriod() + if err != nil { + return err + } if v.IsPitchEnvelopeEnabled() { delta := v.GetCurrentPitchEnvelope() - p = period.AddDelta(p, delta) + p, err = v.inst.Static.PC.AddDelta(p, delta) + if err != nil { + return err + } } v.finalPeriod = p @@ -386,4 +397,5 @@ func (v *itVoice[TPeriod]) updateFinal() { envPan := v.panEnv.GetCurrentValue() v.finalPan = v.pitchPan.GetSeparatedPan(envPan).ToPosition() } + return err } diff --git a/format/s3m/layout/song.go b/format/s3m/layout/song.go index 91be12e..42f489b 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -21,8 +21,9 @@ import ( // Song is the full definition of the song data of an Song file type Song struct { System system.System + MS any Head Header - Instruments []*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + Instruments []*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] Patterns []song.Pattern ChannelSettings []ChannelSetting ChannelOrders []index.Channel @@ -171,7 +172,7 @@ func (s Song) GetName() string { } func (s Song) GetPeriodCalculator() song.PeriodCalculatorIntf { - return s3mPeriod.AmigaConverter + return s3mPeriod.S3MAmigaConverter } func (s Song) GetInitialOrder() index.Order { @@ -230,3 +231,7 @@ func (s Song) IsOPL2Enabled() bool { } return false } + +func (s Song) GetMachineSettings() any { + return s.MS +} diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index 05ae8b5..c8dd571 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -12,11 +12,13 @@ import ( "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/period" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/song" "github.com/gotracker/playback/voice/fadeout" @@ -46,9 +48,9 @@ func moduleHeaderToHeader(fh *s3mfile.ModuleHeader) (*layout.Header, error) { return &head, nil } -func scrsNoneToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSNoneHeader) (*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { - sample := instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ - Static: instrument.StaticValues[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ +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.Volume(si.Volume), @@ -58,9 +60,9 @@ func scrsNoneToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSNoneHeader) (* return &sample, nil } -func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHeader, signedSamples bool, features []feature.Feature) (*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { - sample := instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ - Static: instrument.StaticValues[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ +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.Volume(si.Volume), @@ -117,9 +119,9 @@ func scrsDp30ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSDigiplayerHead return &sample, nil } -func scrsOpl2ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSAdlibHeader) (*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], error) { - inst := instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ - Static: instrument.StaticValues[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ +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.Volume(si.Volume), @@ -164,7 +166,7 @@ func scrsOpl2ToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSAdlibHeader) ( return &inst, nil } -func convertSCRSFullToInstrument(scrs *s3mfile.SCRSFull, signedSamples bool, features []feature.Feature) (*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], 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") } @@ -255,10 +257,13 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, return nil, err } + amigaLimits := (f.Head.Flags & 0x0010) != 0 + s := layout.Song{ System: s3mSystem.S3MSystem, + MS: settings.GetMachineSettings(amigaLimits), Head: *h, - Instruments: make([]*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.InstrumentPointers)), + Instruments: make([]*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.InstrumentPointers)), OrderList: make([]index.Pattern, len(f.OrderList)), } @@ -272,7 +277,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, st2Tempo := (f.Head.Flags & 0x0002) != 0 amigaSlides := (f.Head.Flags & 0x0004) != 0 zeroVolOpt := (f.Head.Flags & 0x0008) != 0 - amigaLimits := (f.Head.Flags & 0x0010) != 0 + //amigaLimits := (f.Head.Flags & 0x0010) != 0 sbFilterEnable := (f.Head.Flags & 0x0020) != 0 st300volSlides := (f.Head.Flags & 0x0040) != 0 if f.Head.TrackerVersion == 0x1300 { @@ -284,7 +289,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, s.OrderList[i] = index.Pattern(o) } - s.Instruments = make([]*instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], 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 { diff --git a/format/s3m/period/amigaconverter.go b/format/s3m/period/amigaconverter.go index 432e3f2..ead8811 100644 --- a/format/s3m/period/amigaconverter.go +++ b/format/s3m/period/amigaconverter.go @@ -5,6 +5,17 @@ import ( "github.com/gotracker/playback/period" ) -var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ - System: system.S3MSystem, +var S3MAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + System: system.S3MSystem, + MinPeriod: 64, + MaxPeriod: 32767, +} + +//MinMOD15Period = 113 << 2 +//MaxMOD15Period = 856 << 2 + +var MODAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ + System: system.S3MSystem, + MinPeriod: 14 << 2, + MaxPeriod: 3424 << 2, } diff --git a/format/s3m/s3m.go b/format/s3m/s3m.go index b12c217..5deba3a 100644 --- a/format/s3m/s3m.go +++ b/format/s3m/s3m.go @@ -35,5 +35,5 @@ func (f format) LoadFromReader(r io.Reader, features []feature.Feature) (playbac } func init() { - machine.RegisterMachine(settings.GetMachineSettings()) + machine.RegisterMachine(settings.GetMachineSettings(true)) } diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go index 7d9433c..9b99345 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -14,13 +14,26 @@ import ( "github.com/gotracker/playback/voice/oscillator" ) -func GetMachineSettings() *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { - return &amigaSettings +func GetMachineSettings(modLimits bool) *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { + if modLimits { + return &amigaMOD31Settings + } + return &amigaS3MSettings } var ( - amigaSettings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ - PeriodConverter: s3mPeriod.AmigaConverter, + amigaMOD31Settings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + PeriodConverter: s3mPeriod.S3MAmigaConverter, + GetFilterFactory: filterFactory, + GetVibratoFactory: vibratoFactory, + GetTremoloFactory: tremoloFactory, + GetPanbrelloFactory: panbrelloFactory, + VoiceFactory: amigaVoiceFactory, + OPL2Enabled: true, + } + + amigaS3MSettings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ + PeriodConverter: s3mPeriod.S3MAmigaConverter, GetFilterFactory: filterFactory, GetVibratoFactory: vibratoFactory, GetTremoloFactory: tremoloFactory, diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index 011d355..e9d5ee5 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -19,7 +19,7 @@ import ( ) type s3mVoice struct { - inst *instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + inst *instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] opl2Chip opl2.Chip opl2Channel index.OPLChannel @@ -61,7 +61,9 @@ func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.Fine DefaultVolume: config.InitialVolume, }) - v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{}) + v.FreqModulator.Setup(component.FreqModulatorSettings[period.Amiga]{ + PC: config.PC, + }) v.PanModulator.Setup(component.PanModulatorSettings[s3mPanning.Panning]{ Enabled: config.PanEnabled, @@ -106,7 +108,7 @@ func (v *s3mVoice) doDeferredRelease() { } } -func (v *s3mVoice) Setup(inst *instrument.Instrument[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], outputRate frequency.Frequency) error { +func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], outputRate frequency.Frequency) error { v.inst = inst switch d := inst.GetData().(type) { @@ -125,7 +127,7 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[s3mVolume.FineVolume, s3mVo case *instrument.OPL2: var o component.OPL2[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] - o.Setup(v.opl2Chip, int(v.opl2Channel), v.opl2, s3mPeriod.AmigaConverter, s3mSystem.S3MBaseClock, inst.GetDefaultVolume()) + o.Setup(v.opl2Chip, int(v.opl2Channel), v.opl2, s3mPeriod.S3MAmigaConverter, s3mSystem.S3MBaseClock, inst.GetDefaultVolume()) v.voicer = &o default: @@ -146,11 +148,13 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[s3mVolume.FineVolume, s3mVo return nil } -func (v *s3mVoice) Reset() { - v.AmpModulator.Reset() - v.FreqModulator.Reset() - v.PanModulator.Reset() - v.vol0Opt.Reset() +func (v *s3mVoice) Reset() error { + return errors.Join( + v.AmpModulator.Reset(), + v.FreqModulator.Reset(), + v.PanModulator.Reset(), + v.vol0Opt.Reset(), + ) } func (v *s3mVoice) Stop() { @@ -165,19 +169,25 @@ func (v *s3mVoice) IsDone() bool { return v.vol0Opt.IsDone() } -func (v *s3mVoice) Tick() { +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 { - o.Advance(v.GetFinalVolume(), v.GetFinalPeriod()) + fp, err := v.GetFinalPeriod() + if err != nil { + return err + } + o.Advance(v.GetFinalVolume(), fp) } v.KeyModulator.Advance() + return nil } -func (v *s3mVoice) RowEnd() { +func (v *s3mVoice) RowEnd() error { v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + return nil } func (v *s3mVoice) Clone(bool) voice.Voice { diff --git a/format/xm/layout/song.go b/format/xm/layout/song.go index c9e7f42..3311371 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -21,9 +21,10 @@ import ( // Song is the full definition of the song data of an XM file type Song[TPeriod period.Period] struct { System system.System + MS any Head Header - Instruments map[uint8]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] - InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + Instruments map[uint8]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] Patterns []song.Pattern ChannelSettings []ChannelSetting OrderList []index.Pattern @@ -221,3 +222,7 @@ func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel func (Song[TPeriod]) IsOPL2Enabled() bool { return false } + +func (s Song[TPeriod]) GetMachineSettings() any { + return s.MS +} diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index 49b87b2..d9c26c1 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -13,6 +13,7 @@ import ( "github.com/gotracker/playback/format/xm/layout" xmPanning "github.com/gotracker/playback/format/xm/panning" xmPeriod "github.com/gotracker/playback/format/xm/period" + "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" @@ -63,20 +64,21 @@ func xmAutoVibratoWSToProtrackerWS(vibtype uint8) uint8 { } } -func xmInstrumentToInstrument(inst *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], 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[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + var instruments []*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] for _, si := range inst.Samples { v := min(xmVolume.XmVolume(si.Volume), 0x40) - sample := instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ - Static: instrument.StaticValues[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ + 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, RelativeNoteNumber: si.RelativeNoteNumber, - AutoVibrato: autovibrato.AutoVibratoSettings{ + AutoVibrato: autovibrato.AutoVibratoSettings[TPeriod]{ Enabled: (inst.VibratoDepth != 0 && inst.VibratoRate != 0), Sweep: int(inst.VibratoSweep), WaveformSelection: xmAutoVibratoWSToProtrackerWS(inst.VibratoType), @@ -247,12 +249,12 @@ func xmLoopModeToLoopMode(mode xmfile.SampleLoopMode) loop.Mode { } } -func convertXMInstrumentToInstrument(ih *xmfile.InstrumentHeader, linearFrequencySlides bool, features []feature.Feature) ([]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], 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[TPeriod period.Period](pkt xmfile.Pattern) (song.Pattern, int) { @@ -298,11 +300,14 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] linearFrequencySlides := f.Head.Flags.IsLinearSlides() + ms := settings.GetMachineSettings[TPeriod]() + s := layout.Song[TPeriod]{ System: xmSystem.XMSystem, + MS: ms, Head: *h, - Instruments: make(map[uint8]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), - InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), + Instruments: make(map[uint8]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), + InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), Patterns: make([]song.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, int(f.Head.SongLength)), } @@ -312,7 +317,7 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] } 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 } @@ -334,7 +339,7 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] } inm, ok := s.InstrumentNoteMap[id.InstID] if !ok { - inm = make(map[note.Semitone]*instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) + inm = make(map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) s.InstrumentNoteMap[id.InstID] = inm } for _, st := range sts { diff --git a/format/xm/period/amigaconverter.go b/format/xm/period/amigaconverter.go index ff818b4..abae9a5 100644 --- a/format/xm/period/amigaconverter.go +++ b/format/xm/period/amigaconverter.go @@ -6,5 +6,7 @@ import ( ) var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ - System: system.XMSystem, + System: system.XMSystem, + MinPeriod: 1, + MaxPeriod: 31999, } diff --git a/format/xm/voice/enveloper_pan.go b/format/xm/voice/enveloper_pan.go index 6620c06..10851e8 100644 --- a/format/xm/voice/enveloper_pan.go +++ b/format/xm/voice/enveloper_pan.go @@ -6,8 +6,8 @@ import ( // == PanEnveloper == -func (v *xmVoice[TPeriod]) EnablePanEnvelope(enabled bool) { - v.panEnv.SetEnabled(enabled) +func (v *xmVoice[TPeriod]) EnablePanEnvelope(enabled bool) error { + return v.panEnv.SetEnabled(enabled) } func (v xmVoice[TPeriod]) IsPanEnvelopeEnabled() bool { @@ -21,10 +21,15 @@ func (v xmVoice[TPeriod]) GetCurrentPanEnvelope() xmPanning.Panning { return xmPanning.DefaultPanning } -func (v *xmVoice[TPeriod]) SetPanEnvelopePosition(pos int) { - if doneCB := v.panEnv.SetEnvelopePosition(pos); doneCB != nil { +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 { diff --git a/format/xm/voice/enveloper_volume.go b/format/xm/voice/enveloper_volume.go index 84845fe..c45e9d8 100644 --- a/format/xm/voice/enveloper_volume.go +++ b/format/xm/voice/enveloper_volume.go @@ -6,8 +6,8 @@ import ( // == VolumeEnveloper == -func (v *xmVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) { - v.volEnv.SetEnabled(enabled) +func (v *xmVoice[TPeriod]) EnableVolumeEnvelope(enabled bool) error { + return v.volEnv.SetEnabled(enabled) } func (v xmVoice[TPeriod]) IsVolumeEnvelopeEnabled() bool { @@ -21,10 +21,15 @@ func (v xmVoice[TPeriod]) GetCurrentVolumeEnvelope() xmVolume.XmVolume { return xmVolume.DefaultXmVolume } -func (v *xmVoice[TPeriod]) SetVolumeEnvelopePosition(pos int) { - if doneCB := v.volEnv.SetEnvelopePosition(pos); doneCB != nil { +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 { diff --git a/format/xm/voice/modulator_amp.go b/format/xm/voice/modulator_amp.go index b9b91f0..bb7be8c 100644 --- a/format/xm/voice/modulator_amp.go +++ b/format/xm/voice/modulator_amp.go @@ -8,37 +8,35 @@ import ( // == AmpModulator == -func (v *xmVoice[TPeriod]) SetActive(on bool) { - v.amp.SetActive(on) +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) { - if !vol.IsUseInstrumentVol() { - v.amp.SetMixingVolume(vol) - } +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) { +func (v *xmVoice[TPeriod]) SetVolume(vol xmVolume.XmVolume) error { if vol.IsUseInstrumentVol() { vol = v.voicer.GetDefaultVolume() } - v.amp.SetVolume(vol) + return v.amp.SetVolume(vol) } func (v xmVoice[TPeriod]) GetVolume() xmVolume.XmVolume { return v.amp.GetVolume() } -func (v *xmVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) { - v.amp.SetVolumeDelta(d) +func (v *xmVoice[TPeriod]) SetVolumeDelta(d types.VolumeDelta) error { + return v.amp.SetVolumeDelta(d) } func (v xmVoice[TPeriod]) GetVolumeDelta() types.VolumeDelta { diff --git a/format/xm/voice/modulator_freq.go b/format/xm/voice/modulator_freq.go index f1d3760..a840c4e 100644 --- a/format/xm/voice/modulator_freq.go +++ b/format/xm/voice/modulator_freq.go @@ -6,27 +6,30 @@ import ( // == FreqModulator == -func (v *xmVoice[TPeriod]) SetPeriod(period TPeriod) { - v.freq.SetPeriod(period) +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) { - v.freq.SetPeriodDelta(delta) +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 { - p := v.freq.GetFinalPeriod() +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 = period.AddDelta(p, delta) + p, err = v.inst.Static.PC.AddDelta(p, delta) } - return p + return p, err } diff --git a/format/xm/voice/modulator_pan.go b/format/xm/voice/modulator_pan.go index 171fd36..b33ce3a 100644 --- a/format/xm/voice/modulator_pan.go +++ b/format/xm/voice/modulator_pan.go @@ -8,16 +8,16 @@ import ( // == PanModulator == -func (v *xmVoice[TPeriod]) SetPan(pan xmPanning.Panning) { - v.pan.SetPan(pan) +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) { - v.pan.SetPanDelta(d) +func (v *xmVoice[TPeriod]) SetPanDelta(d types.PanDelta) error { + return v.pan.SetPanDelta(d) } func (v xmVoice[TPeriod]) GetPanDelta() types.PanDelta { diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index f126a23..d9569e9 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -23,7 +23,7 @@ type Period interface { } type xmVoice[TPeriod Period] struct { - inst *instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + inst *instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] fadeoutMode fadeout.Mode @@ -68,6 +68,10 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xm 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, @@ -128,7 +132,7 @@ func (v xmVoice[TPeriod]) getFadeoutEnabled() bool { return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) } -func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], outputRate frequency.Frequency) error { +func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], outputRate frequency.Frequency) error { v.inst = inst switch d := inst.GetData().(type) { @@ -166,7 +170,7 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[xmVolume.XmVolume, return errors.New("instrument is nil") } - v.autoVibrato.Setup(inst.GetAutoVibrato()) + v.autoVibrato.Setup(inst.Static.AutoVibrato) if factory := inst.GetFilterFactory(); factory != nil { v.voiceFilter = factory(inst.SampleRate) @@ -179,16 +183,18 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[xmVolume.XmVolume, return nil } -func (v *xmVoice[TPeriod]) Reset() { +func (v *xmVoice[TPeriod]) Reset() error { v.KeyModulator.Release() - v.amp.Reset() - v.fadeout.Reset() - v.freq.Reset() - v.autoVibrato.Reset() - v.pan.Reset() - v.volEnv.Reset() - v.panEnv.Reset() - v.vol0Opt.Reset() + 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() { @@ -207,7 +213,7 @@ func (v *xmVoice[TPeriod]) IsDone() bool { return v.vol0Opt.IsDone() } -func (v *xmVoice[TPeriod]) Tick() { +func (v *xmVoice[TPeriod]) Tick() error { v.fadeout.Advance() v.autoVibrato.Advance() if v.IsVolumeEnvelopeEnabled() { @@ -225,10 +231,12 @@ func (v *xmVoice[TPeriod]) Tick() { v.KeyModulator.DeferredUpdate() v.KeyModulator.Advance() + return nil } -func (v *xmVoice[TPeriod]) RowEnd() { +func (v *xmVoice[TPeriod]) RowEnd() error { v.vol0Opt.ObserveVolume(v.GetFinalVolume()) + return nil } func (v *xmVoice[TPeriod]) Clone(bool) voice.Voice { diff --git a/instrument/instrument.go b/instrument/instrument.go index d4f8c0a..dc8956b 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -8,6 +8,7 @@ import ( "github.com/gotracker/playback/filter" "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/types" ) @@ -26,14 +27,14 @@ type InstrumentIntf interface { GetData() Data GetFilterFactory() filter.Factory GetPluginFilterFactory() filter.Factory - GetAutoVibrato() autovibrato.AutoVibratoSettings IsReleaseNote(n note.Note) bool IsStopNote(n note.Note) bool GetDefaultVolumeGeneric() volume.Volume } // StaticValues are the static values associated with an instrument -type StaticValues[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { +type StaticValues[TPeriod types.Period, TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { + PC period.PeriodConverter[TPeriod] Filename string Name string ID ID @@ -41,7 +42,7 @@ type StaticValues[TMixingVolume, TVolume types.Volume, TPanning types.Panning] s MixingVolume optional.Value[TMixingVolume] Panning optional.Value[TPanning] RelativeNoteNumber int8 - AutoVibrato autovibrato.AutoVibratoSettings + AutoVibrato autovibrato.AutoVibratoSettings[TPeriod] NewNoteAction note.Action Finetune note.Finetune FilterFactory filter.Factory @@ -49,51 +50,51 @@ type StaticValues[TMixingVolume, TVolume types.Volume, TPanning types.Panning] s } // Instrument is the mildly-decoded instrument/sample header -type Instrument[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { - Static StaticValues[TMixingVolume, TVolume, TPanning] +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[TMixingVolume, TVolume, TPanning]) IsInvalid() bool { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) IsInvalid() bool { return false } // GetSampleRate returns the central-note sample rate value for the instrument // This may get mutated if a finetune effect is processed -func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetSampleRate() frequency.Frequency { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetSampleRate() frequency.Frequency { return inst.SampleRate } // SetSampleRate sets the central-note sample rate value for the instrument -func (inst *Instrument[TMixingVolume, TVolume, TPanning]) SetSampleRate(sampleRate frequency.Frequency) { +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[TMixingVolume, TVolume, TPanning]) GetDefaultVolume() TVolume { +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[TMixingVolume, TVolume, TPanning]) GetDefaultVolumeGeneric() volume.Volume { +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[TMixingVolume, TVolume, TPanning]) 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[TMixingVolume, TVolume, TPanning]) 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[TMixingVolume, TVolume, TPanning]) GetFinetune() note.Finetune { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetFinetune() note.Finetune { if ft, ok := inst.Finetune.Get(); ok { return ft } @@ -101,47 +102,42 @@ func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetFinetune() note.Fine } // GetID returns the instrument number (1-based) -func (inst Instrument[TMixingVolume, TVolume, TPanning]) 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[TMixingVolume, TVolume, TPanning]) 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[TMixingVolume, TVolume, TPanning]) GetKind() Kind { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetKind() Kind { return inst.Inst.GetKind() } // GetNewNoteAction returns the NewNoteAction associated to the instrument -func (inst Instrument[TMixingVolume, TVolume, TPanning]) 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[TMixingVolume, TVolume, TPanning]) 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[TMixingVolume, TVolume, TPanning]) GetFilterFactory() filter.Factory { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetFilterFactory() filter.Factory { return inst.Static.FilterFactory } // GetPluginFilterFactory returns the factory for the channel plugin filter -func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetPluginFilterFactory() filter.Factory { +func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetPluginFilterFactory() filter.Factory { return inst.Static.PluginFilter } -// GetAutoVibrato returns the settings for the autovibrato system -func (inst Instrument[TMixingVolume, TVolume, TPanning]) GetAutoVibrato() autovibrato.AutoVibratoSettings { - return inst.Static.AutoVibrato -} - // IsReleaseNote returns true if the note is a release (Note-Off) -func (inst Instrument[TMixingVolume, TVolume, TPanning]) 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 @@ -150,7 +146,7 @@ func (inst Instrument[TMixingVolume, TVolume, TPanning]) IsReleaseNote(n note.No } // IsStopNote returns true if the note is a stop (Note-Cut) -func (inst Instrument[TMixingVolume, TVolume, TPanning]) 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 diff --git a/period/amigaconverter.go b/period/amigaconverter.go index 04a952b..1a5d721 100644 --- a/period/amigaconverter.go +++ b/period/amigaconverter.go @@ -6,12 +6,15 @@ import ( "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/system" + "github.com/heucuva/comparison" ) // AmigaConverter defines a sampler period that follows the AmigaConverter-style approach of note // definition. Useful in calculating resampling. type AmigaConverter struct { - System system.ClockableSystem + System system.ClockableSystem + MinPeriod Amiga + MaxPeriod Amiga } var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) @@ -45,7 +48,7 @@ func (c AmigaConverter) GetPeriod(n note.Note) Amiga { return Amiga(0) } - return Amiga(float64(keyPeriod) / float64(uint32(1)<= nPoints { e.value = e.updater(cur.Y, cur.Y, 0) - return + return nil } next := e.settings.Values[nextPoint] @@ -181,6 +182,7 @@ func (e *baseEnvelope[TIn, TOut]) updateValue() { } e.value = e.updater(cur.Y, next.Y, t) + return nil } func (e *baseEnvelope[TIn, TOut]) stateAdvance(keyOn bool) bool { diff --git a/voice/component/modulator_amp.go b/voice/component/modulator_amp.go index 83c84b9..c2cf178 100755 --- a/voice/component/modulator_amp.go +++ b/voice/component/modulator_amp.go @@ -45,15 +45,15 @@ func (a AmpModulator[TMixingVolume, TVolume]) Clone() AmpModulator[TMixingVolume return m } -func (a *AmpModulator[TMixingVolume, TVolume]) Reset() { +func (a *AmpModulator[TMixingVolume, TVolume]) Reset() error { a.keyed.delta = 0 a.keyed.mixingOverride.Reset() - a.updateFinal() + return a.updateFinal() } -func (a *AmpModulator[TMixingVolume, TVolume]) SetActive(active bool) { +func (a *AmpModulator[TMixingVolume, TVolume]) SetActive(active bool) error { a.unkeyed.active = active - a.updateFinal() + return a.updateFinal() } func (a AmpModulator[TMixingVolume, TVolume]) IsActive() bool { @@ -61,11 +61,13 @@ func (a AmpModulator[TMixingVolume, TVolume]) IsActive() bool { } // SetMixingVolume configures the mixing volume of the modulator -func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolume(mixing TMixingVolume) { - if !mixing.IsUseInstrumentVol() { - a.unkeyed.mixing = mixing - a.updateFinal() +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 @@ -73,8 +75,9 @@ func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolume() TMixingVolume { return a.unkeyed.mixing } -func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolumeOverride(mvo optional.Value[TMixingVolume]) { +func (a *AmpModulator[TMixingVolume, TVolume]) SetMixingVolumeOverride(mvo optional.Value[TMixingVolume]) error { a.keyed.mixingOverride = mvo + return nil } func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolumeOverride() optional.Value[TMixingVolume] { @@ -82,12 +85,12 @@ func (a AmpModulator[TMixingVolume, TVolume]) GetMixingVolumeOverride() optional } // SetVolume sets the current volume (before fadeout calculation) -func (a *AmpModulator[TMixingVolume, TVolume]) SetVolume(vol TVolume) { +func (a *AmpModulator[TMixingVolume, TVolume]) SetVolume(vol TVolume) error { if vol.IsUseInstrumentVol() { vol = a.settings.DefaultVolume } a.unkeyed.vol = vol - a.updateFinal() + return a.updateFinal() } // GetVolume returns the current volume (before fadeout calculation) @@ -95,9 +98,9 @@ func (a AmpModulator[TMixingVolume, TVolume]) GetVolume() TVolume { return a.unkeyed.vol } -func (a *AmpModulator[TMixingVolume, TVolume]) SetVolumeDelta(d types.VolumeDelta) { +func (a *AmpModulator[TMixingVolume, TVolume]) SetVolumeDelta(d types.VolumeDelta) error { a.keyed.delta = d - a.updateFinal() + return a.updateFinal() } func (a AmpModulator[TMixingVolume, TVolume]) GetVolumeDelta() types.VolumeDelta { @@ -109,10 +112,10 @@ func (a AmpModulator[TMixingVolume, TVolume]) GetFinalVolume() volume.Volume { return a.final } -func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() { +func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() error { if !a.unkeyed.active { a.final = 0 - return + return nil } v := types.AddVolumeDelta(a.unkeyed.vol, a.keyed.delta) @@ -123,6 +126,7 @@ func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() { } a.final = mv.ToVolume() * v.ToVolume() + return nil } func (a AmpModulator[TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { diff --git a/voice/component/modulator_autovibrato.go b/voice/component/modulator_autovibrato.go index 71d57de..9847c98 100644 --- a/voice/component/modulator_autovibrato.go +++ b/voice/component/modulator_autovibrato.go @@ -8,11 +8,12 @@ import ( "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 period.Period] struct { - settings autovibrato.AutoVibratoSettings +type AutoVibratoModulator[TPeriod types.Period] struct { + settings autovibrato.AutoVibratoSettings[TPeriod] unkeyed struct { enabled bool } @@ -22,7 +23,7 @@ type AutoVibratoModulator[TPeriod period.Period] struct { autoVibrato oscillator.Oscillator } -func (f *AutoVibratoModulator[TPeriod]) Setup(settings autovibrato.AutoVibratoSettings) { +func (f *AutoVibratoModulator[TPeriod]) Setup(settings autovibrato.AutoVibratoSettings[TPeriod]) { f.settings = settings f.unkeyed.enabled = f.settings.Enabled f.Reset() @@ -36,10 +37,10 @@ func (f AutoVibratoModulator[TPeriod]) Clone() AutoVibratoModulator[TPeriod] { return m } -func (f *AutoVibratoModulator[TPeriod]) Reset() { +func (f *AutoVibratoModulator[TPeriod]) Reset() error { f.keyed.age = 0 f.autoVibrato = f.settings.Generate() - f.ResetAutoVibrato() + return f.ResetAutoVibrato() } // SetEnabled sets the status of the AutoVibrato enablement flag @@ -53,12 +54,13 @@ func (f *AutoVibratoModulator[TPeriod]) ConfigureAutoVibrato() { } // ResetAutoVibrato resets the current AutoVibrato -func (f *AutoVibratoModulator[TPeriod]) ResetAutoVibrato() { +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 @@ -67,9 +69,9 @@ func (f *AutoVibratoModulator[TPeriod]) IsAutoVibratoEnabled() bool { } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) -func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) TPeriod { +func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) (TPeriod, error) { if !f.unkeyed.enabled { - return in + return in, nil } depth := f.settings.Depth @@ -78,7 +80,7 @@ func (f *AutoVibratoModulator[TPeriod]) GetAdjustedPeriod(in TPeriod) TPeriod { } avDelta := f.autoVibrato.GetWave(depth) d := period.Delta(avDelta) - return period.AddDelta(in, d) + return f.settings.PC.AddDelta(in, d) } // Advance advances the autoVibrato value by 1 tick diff --git a/voice/component/modulator_fadeout.go b/voice/component/modulator_fadeout.go index a188732..aebc7b3 100644 --- a/voice/component/modulator_fadeout.go +++ b/voice/component/modulator_fadeout.go @@ -37,13 +37,15 @@ func (a FadeoutModulator) Clone() FadeoutModulator { } // Reset disables the fadeout and resets its volume -func (a *FadeoutModulator) Reset() { +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) { +func (a *FadeoutModulator) SetEnabled(enabled bool) error { a.unkeyed.enabled = enabled + return nil } // IsEnabled returns the status of the fadeout enablement flag diff --git a/voice/component/modulator_freq.go b/voice/component/modulator_freq.go index da20915..693b81a 100755 --- a/voice/component/modulator_freq.go +++ b/voice/component/modulator_freq.go @@ -21,18 +21,19 @@ type FreqModulator[TPeriod period.Period] struct { } type FreqModulatorSettings[TPeriod period.Period] struct { + PC period.PeriodConverter[TPeriod] } -func (f *FreqModulator[TPeriod]) Setup(settings FreqModulatorSettings[TPeriod]) { +func (f *FreqModulator[TPeriod]) Setup(settings FreqModulatorSettings[TPeriod]) error { f.settings = settings var empty TPeriod f.unkeyed.period = empty - f.Reset() + return f.Reset() } -func (f *FreqModulator[TPeriod]) Reset() { +func (f *FreqModulator[TPeriod]) Reset() error { f.keyed.delta = 0 - f.updateFinal() + return f.updateFinal() } func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { @@ -41,13 +42,14 @@ func (f FreqModulator[TPeriod]) Clone() FreqModulator[TPeriod] { } // SetPeriod sets the current period (before AutoVibrato and Delta calculation) -func (f *FreqModulator[TPeriod]) SetPeriod(period TPeriod) { +func (f *FreqModulator[TPeriod]) SetPeriod(period TPeriod) error { if period.IsInvalid() { - return + // ignore it for now + return nil } f.unkeyed.period = period - f.updateFinal() + return f.updateFinal() } // GetPeriod returns the current period (before AutoVibrato and Delta calculation) @@ -56,9 +58,9 @@ func (f *FreqModulator[TPeriod]) GetPeriod() TPeriod { } // SetPeriodDelta sets the current period delta (before AutoVibrato calculation) -func (f *FreqModulator[TPeriod]) SetPeriodDelta(delta period.Delta) { +func (f *FreqModulator[TPeriod]) SetPeriodDelta(delta period.Delta) error { f.keyed.delta = delta - f.updateFinal() + return f.updateFinal() } // GetDelta returns the current period delta (before AutoVibrato calculation) @@ -67,8 +69,8 @@ func (f *FreqModulator[TPeriod]) GetPeriodDelta() period.Delta { } // GetFinalPeriod returns the current period (after AutoVibrato and Delta calculation) -func (f *FreqModulator[TPeriod]) GetFinalPeriod() TPeriod { - return f.final +func (f *FreqModulator[TPeriod]) GetFinalPeriod() (TPeriod, error) { + return f.final, nil } func (f FreqModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { @@ -79,6 +81,8 @@ func (f FreqModulator[TPeriod]) DumpState(ch index.Channel, t tracing.Tracer, co ), comment) } -func (f *FreqModulator[TPeriod]) updateFinal() { - f.final = period.AddDelta(f.unkeyed.period, f.keyed.delta) +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_pan.go b/voice/component/modulator_pan.go index e7afbc0..04ed488 100755 --- a/voice/component/modulator_pan.go +++ b/voice/component/modulator_pan.go @@ -32,9 +32,9 @@ func (p *PanModulator[TPanning]) Setup(settings PanModulatorSettings[TPanning]) p.Reset() } -func (p *PanModulator[TPanning]) Reset() { +func (p *PanModulator[TPanning]) Reset() error { p.keyed.delta = 0 - p.updateFinal() + return p.updateFinal() } func (p PanModulator[TPanning]) Clone() PanModulator[TPanning] { @@ -43,11 +43,13 @@ func (p PanModulator[TPanning]) Clone() PanModulator[TPanning] { } // SetPan sets the current panning -func (p *PanModulator[TPanning]) SetPan(pan TPanning) { - if p.settings.Enabled { - p.unkeyed.pan = pan - p.updateFinal() +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 @@ -56,11 +58,13 @@ func (p PanModulator[TPanning]) GetPan() TPanning { } // SetPanDelta sets the current panning delta -func (p *PanModulator[TPanning]) SetPanDelta(d types.PanDelta) { - if p.settings.Enabled { - p.keyed.delta = d - p.updateFinal() +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 @@ -80,6 +84,7 @@ func (p PanModulator[TPanning]) DumpState(ch index.Channel, t tracing.Tracer, co ), comment) } -func (p *PanModulator[TPanning]) updateFinal() { +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 index 021dd82..b983f17 100644 --- a/voice/component/modulator_pitchpan.go +++ b/voice/component/modulator_pitchpan.go @@ -38,14 +38,14 @@ func (p PitchPanModulator[TPanning]) Clone() PitchPanModulator[TPanning] { return m } -func (p *PitchPanModulator[TPanning]) Reset() { - p.updatePitchPan() +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) { +func (p *PitchPanModulator[TPanning]) SetPitch(st note.Semitone) error { p.unkeyed.pitch = st - p.updatePitchPan() + return p.updatePitchPan() } // IsPitchPanEnabled returns the enablement of the pitch-pan separation function @@ -54,9 +54,9 @@ func (p PitchPanModulator[TPanning]) IsPitchPanEnabled() bool { } // EnablePitchPan enables the pitch-pan separation function -func (p *PitchPanModulator[TPanning]) EnablePitchPan(enabled bool) { +func (p *PitchPanModulator[TPanning]) EnablePitchPan(enabled bool) error { p.unkeyed.enabled = enabled - p.updatePitchPan() + return p.updatePitchPan() } // SetPanSeparation gets the current pan separation @@ -78,12 +78,13 @@ func (p PitchPanModulator[TPanning]) GetSeparatedPan(pan TPanning) TPanning { func (p *PitchPanModulator[TPanning]) Advance() { } -func (p *PitchPanModulator[TPanning]) updatePitchPan() { +func (p *PitchPanModulator[TPanning]) updatePitchPan() error { if !p.unkeyed.enabled { - return + 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) { diff --git a/voice/component/vol0optimization.go b/voice/component/vol0optimization.go index b05571e..ab90d34 100644 --- a/voice/component/vol0optimization.go +++ b/voice/component/vol0optimization.go @@ -35,8 +35,9 @@ func (c *Vol0Optimization) SetEnabled(enabled bool) { c.unkeyed.enabled = enabled } -func (c *Vol0Optimization) Reset() { +func (c *Vol0Optimization) Reset() error { c.keyed.rowsAt0 = 0 + return nil } func (c *Vol0Optimization) ObserveVolume(v volume.Volume) { diff --git a/voice/config.go b/voice/config.go index a1c5517..2f3cf9b 100644 --- a/voice/config.go +++ b/voice/config.go @@ -1,13 +1,8 @@ package voice import ( - "github.com/gotracker/playback/filter" - "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" - "github.com/gotracker/playback/instrument" - "github.com/gotracker/playback/voice/autovibrato" - "github.com/gotracker/playback/voice/envelope" - "github.com/gotracker/playback/voice/fadeout" + "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/types" "github.com/gotracker/playback/voice/vol0optimization" @@ -19,21 +14,8 @@ type ( Panning = types.Panning ) -type InstrumentConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { - SampleRate frequency.Frequency - AutoVibrato autovibrato.AutoVibratoSettings - Data instrument.Data - VoiceFilter filter.Filter - FadeOut fadeout.Settings - PitchPan instrument.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 -} - type VoiceConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { + PC period.PeriodConverter[TPeriod] OPLChip opl2.Chip OPLChannel index.OPLChannel InitialVolume TVolume diff --git a/voice/render.go b/voice/render.go index e2da9ec..786e377 100644 --- a/voice/render.go +++ b/voice/render.go @@ -29,7 +29,10 @@ func RenderAndTick[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], return nil, err } - p := rs.GetFinalPeriod() + p, err := rs.GetFinalPeriod() + if err != nil { + return nil, err + } samplerAdd := float32(pc.GetSamplerAdd(p, float64(rs.GetSampleRate())*float64(details.SamplerSpeed))) diff --git a/voice/voice.go b/voice/voice.go index c919099..da7ede3 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -19,7 +19,7 @@ type Voice interface { DumpState(ch index.Channel, t tracing.Tracer) // Configuration - Reset() + Reset() error SetOPL2Chip(chip opl2.Chip) // Actions @@ -29,8 +29,8 @@ type Voice interface { Stop() // State Machine Update - Tick() - RowEnd() + Tick() error + RowEnd() error // General Parameters IsDone() bool @@ -41,19 +41,19 @@ type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, T Voice // Configuration - Setup(inst *instrument.Instrument[TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error + Setup(inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error } type AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { // Amp/Volume Parameters IsActive() bool - SetActive(active bool) + SetActive(active bool) error GetMixingVolume() TMixingVolume - SetMixingVolume(v TMixingVolume) + SetMixingVolume(v TMixingVolume) error GetVolume() TVolume - SetVolume(v TVolume) + SetVolume(v TVolume) error GetVolumeDelta() types.VolumeDelta - SetVolumeDelta(d types.VolumeDelta) + SetVolumeDelta(d types.VolumeDelta) error GetFinalVolume() volume.Volume } @@ -65,10 +65,10 @@ type FadeoutModulator interface { type FreqModulator[TPeriod Period] interface { // Frequency/Pitch Parameters GetPeriod() TPeriod - SetPeriod(p TPeriod) + SetPeriod(p TPeriod) error GetPeriodDelta() period.Delta - SetPeriodDelta(delta period.Delta) - GetFinalPeriod() TPeriod + SetPeriodDelta(delta period.Delta) error + GetFinalPeriod() (TPeriod, error) } type Sampler interface { @@ -86,7 +86,7 @@ type RenderSampler[TPeriod Period] interface { SetPos(pos sampling.Pos) error GetPos() (sampling.Pos, error) - GetFinalPeriod() TPeriod + GetFinalPeriod() (TPeriod, error) GetFinalVolume() volume.Volume GetFinalPan() panning.Position } @@ -94,51 +94,51 @@ type RenderSampler[TPeriod Period] interface { type PanModulator[TPanning Panning] interface { // Pan Parameters GetPan() TPanning - SetPan(pan TPanning) + SetPan(pan TPanning) error GetPanDelta() types.PanDelta - SetPanDelta(d types.PanDelta) + SetPanDelta(d types.PanDelta) error GetFinalPan() panning.Position } type PitchPanModulator[TPanning Panning] interface { - SetPitchPanNote(st note.Semitone) + SetPitchPanNote(st note.Semitone) error IsPitchPanEnabled() bool - EnablePitchPan(enabled bool) + EnablePitchPan(enabled bool) error GetPanSeparation() float32 } type VolumeEnvelope[TGlobalVolume, TMixingVolume, TVolume Volume] interface { // Amp/Volume Envelope Parameters IsVolumeEnvelopeEnabled() bool - EnableVolumeEnvelope(enabled bool) + EnableVolumeEnvelope(enabled bool) error GetVolumeEnvelopePosition() int - SetVolumeEnvelopePosition(pos int) + SetVolumeEnvelopePosition(pos int) error GetCurrentVolumeEnvelope() TVolume } type PitchEnvelope[TPeriod Period] interface { // Frequency/Pitch Envelope Parameters IsPitchEnvelopeEnabled() bool - EnablePitchEnvelope(enabled bool) + EnablePitchEnvelope(enabled bool) error GetPitchEnvelopePosition() int - SetPitchEnvelopePosition(pos int) + SetPitchEnvelopePosition(pos int) error GetCurrentPitchEnvelope() period.Delta } type PanEnvelope[TPanning Panning] interface { // Pan Envelope Parameters IsPanEnvelopeEnabled() bool - EnablePanEnvelope(enabled bool) + EnablePanEnvelope(enabled bool) error GetPanEnvelopePosition() int - SetPanEnvelopePosition(pos int) + SetPanEnvelopePosition(pos int) error GetCurrentPanEnvelope() TPanning } type FilterEnvelope interface { // Filter Envelope Parameters IsFilterEnvelopeEnabled() bool - EnableFilterEnvelope(enabled bool) + EnableFilterEnvelope(enabled bool) error GetFilterEnvelopePosition() int - SetFilterEnvelopePosition(pos int) + SetFilterEnvelopePosition(pos int) error GetCurrentFilterEnvelope() uint8 } From 7eb270ddbc0c990d60f4970252c355b1d7c8aa4f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Mon, 15 Jan 2024 20:15:45 -0800 Subject: [PATCH 48/63] fix extra spammative new note actions --- player/machine/channel.go | 1 + player/machine/channel_noteaction.go | 3 ++- player/machine/channel_notedecode.go | 4 ++++ player/machine/machine_channel.go | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/player/machine/channel.go b/player/machine/channel.go index 1d77fed..a112672 100644 --- a/player/machine/channel.go +++ b/player/machine/channel.go @@ -34,6 +34,7 @@ type channel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann Inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] Pos optional.Value[sampling.Pos] ActionTick optional.Value[ActionTick] + TriggerNNA bool } portaPeriod TPeriod newNote NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning] diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index 6956b70..1405e99 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -24,7 +24,8 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo c.target.ActionTick.Reset() // perform new note action - if na.Action != note.ActionContinue && m.canPastNote() { + if c.target.TriggerNNA && m.canPastNote() { + c.target.TriggerNNA = false var pn voice.Voice switch c.nna { case note.ActionCut: diff --git a/player/machine/channel_notedecode.go b/player/machine/channel_notedecode.go index f520dac..750ec0d 100644 --- a/player/machine/channel_notedecode.go +++ b/player/machine/channel_notedecode.go @@ -22,6 +22,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco na note.Action = note.ActionContinue needNoteInstIdent bool wantInstrumentDefaults bool + wantTriggerNNA bool ) if n != nil { switch n.Type() { @@ -51,10 +52,12 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco inst, _ = ii.(*instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) wantInstrumentDefaults = inst != nil changeNote.Inst.Set(inst) + wantTriggerNNA = true } else if st != 0 { // retrigger same instrument inst = c.target.Inst wantInstrumentDefaults = true + wantTriggerNNA = true } if inst != nil && n != nil { @@ -97,6 +100,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco } c.newNote = changeNote + c.target.TriggerNNA = wantTriggerNNA return nil } diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 757b625..6a7e6ea 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -327,6 +327,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Star traceChannelOptionalValueResetWithComment(m, ch, "newNote.Pos", c.newNote.Pos, "StartChannelPortaToNote") c.newNote.Pos.Reset() + c.target.TriggerNNA = false return nil }) From 500042d8fd3e39eebd2b00f8805f159d43a840c9 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 16 Jan 2024 08:41:05 -0800 Subject: [PATCH 49/63] produce multiple ticks worth of output in allotted time --- player/tracker.go | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/player/tracker.go b/player/tracker.go index 6af8602..5e70f43 100644 --- a/player/tracker.go +++ b/player/tracker.go @@ -41,12 +41,36 @@ func (t *Tracker) Close() { // 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 premix != nil && len(premix.Data) > 0 { - out <- premix + remaining := deltaTime + + var first time.Duration + firstSet := false + + for !firstSet || remaining < first { + premix, err := t.Generate(deltaTime) + if premix != nil && len(premix.Data) > 0 { + out <- premix + } + + if err != nil { + return err + } + + if premix == nil { + continue + } + + dur := time.Duration(premix.SamplesLen) * time.Second / time.Duration(t.s.SampleRate) + + if !firstSet { + firstSet = true + first = dur + } + + remaining -= dur } - return err + return nil } func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) { @@ -56,12 +80,7 @@ func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) defer t.OutputTraces() - premix, err := t.M.Tick(t.s) - if err != nil && !errors.Is(err, song.ErrStopSong) { - return nil, err - } - - return premix, err + return t.M.Tick(t.s) } // GetSampleRate returns the sample rate of the sampler From fd24f93e10049db8bc26ca1df4c1d4381fdde53c Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 16 Jan 2024 09:45:14 -0800 Subject: [PATCH 50/63] support s3m quirk of porta from final period - implement octave shift for amiga periods (xm/s3m) --- format/it/channel/effect_portatonote.go | 2 +- .../s3m/channel/effect_extrafineportadown.go | 3 +- format/s3m/channel/effect_extrafineportaup.go | 3 +- format/s3m/channel/effect_fineportadown.go | 3 +- format/s3m/channel/effect_fineportaup.go | 3 +- format/s3m/channel/effect_portadown.go | 3 +- format/s3m/channel/effect_portatonote.go | 3 +- format/s3m/channel/effect_portaup.go | 3 +- format/s3m/period/amigaconverter.go | 2 + format/s3m/system/system.go | 11 ++++-- format/xm/channel/effect_portatonote.go | 2 +- format/xm/period/amigaconverter.go | 1 + period/amigaconverter.go | 7 ++-- player/machine/channel.go | 12 +++--- player/machine/machine.go | 2 +- player/machine/machine_channel.go | 25 ++++++++---- player/machine/machine_tracing.go | 38 +++++++++---------- player/machine/settings/usersettings.go | 16 ++++---- tracing/tracer.go | 8 ++-- tracing/tracing.go | 16 ++++---- 20 files changed, 86 insertions(+), 77 deletions(-) diff --git a/format/it/channel/effect_portatonote.go b/format/it/channel/effect_portatonote.go index 7948ce5..85f7525 100644 --- a/format/it/channel/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -30,7 +30,7 @@ func (e PortaToNote[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xx := mem.PortaToNote(DataEffect(e)) if !mem.Shared.OldEffectMode || tick != 0 { - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4, false) } return nil } diff --git a/format/s3m/channel/effect_extrafineportadown.go b/format/s3m/channel/effect_extrafineportadown.go index d90b97c..be71f04 100644 --- a/format/s3m/channel/effect_extrafineportadown.go +++ b/format/s3m/channel/effect_extrafineportadown.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -24,7 +23,7 @@ func (e ExtraFinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amig } y := DataEffect(e) & 0x0F - return m.DoChannelPortaDown(ch, period.Delta(y)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaDown(ch, period.Delta(y)*4) } func (e ExtraFinePortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_extrafineportaup.go b/format/s3m/channel/effect_extrafineportaup.go index ab027ed..367f700 100644 --- a/format/s3m/channel/effect_extrafineportaup.go +++ b/format/s3m/channel/effect_extrafineportaup.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -24,7 +23,7 @@ func (e ExtraFinePortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, } y := DataEffect(e) & 0x0F - return m.DoChannelPortaUp(ch, period.Delta(y)*1*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaUp(ch, period.Delta(y)*1) } func (e ExtraFinePortaUp) TraceData() string { diff --git a/format/s3m/channel/effect_fineportadown.go b/format/s3m/channel/effect_fineportadown.go index 33e2a66..10cbb7b 100644 --- a/format/s3m/channel/effect_fineportadown.go +++ b/format/s3m/channel/effect_fineportadown.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -24,7 +23,7 @@ func (e FinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3 } y := DataEffect(e) & 0x0F - return m.DoChannelPortaDown(ch, period.Delta(y)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaDown(ch, period.Delta(y)*4) } func (e FinePortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_fineportaup.go b/format/s3m/channel/effect_fineportaup.go index 36b6fe0..72e262e 100644 --- a/format/s3m/channel/effect_fineportaup.go +++ b/format/s3m/channel/effect_fineportaup.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -24,7 +23,7 @@ func (e FinePortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV } y := DataEffect(e) & 0x0F - return m.DoChannelPortaUp(ch, period.Delta(y)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaUp(ch, period.Delta(y)*4) } func (e FinePortaUp) TraceData() string { diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go index 5cd41ed..ed342fa 100644 --- a/format/s3m/channel/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -30,7 +29,7 @@ func (e PortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVol return nil } - return m.DoChannelPortaDown(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaDown(ch, period.Delta(xx)*4) } func (e PortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 133d252..8ccdf85 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -34,7 +33,7 @@ func (e PortaToNote) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV return nil } - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4, true) } func (e PortaToNote) TraceData() string { diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go index 91892ab..0da1f50 100644 --- a/format/s3m/channel/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -4,7 +4,6 @@ import ( "fmt" s3mPanning "github.com/gotracker/playback/format/s3m/panning" - s3mSystem "github.com/gotracker/playback/format/s3m/system" s3mVolume "github.com/gotracker/playback/format/s3m/volume" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" @@ -30,7 +29,7 @@ func (e PortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum return nil } - return m.DoChannelPortaUp(ch, period.Delta(xx)*4*s3mSystem.SlideFinesPerSemitone) + return m.DoChannelPortaUp(ch, period.Delta(xx)*4) } func (e PortaUp) TraceData() string { diff --git a/format/s3m/period/amigaconverter.go b/format/s3m/period/amigaconverter.go index ead8811..81c79d3 100644 --- a/format/s3m/period/amigaconverter.go +++ b/format/s3m/period/amigaconverter.go @@ -9,6 +9,7 @@ var S3MAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConvert System: system.S3MSystem, MinPeriod: 64, MaxPeriod: 32767, + DeltaMult: 2, } //MinMOD15Period = 113 << 2 @@ -18,4 +19,5 @@ var MODAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConvert System: system.S3MSystem, MinPeriod: 14 << 2, MaxPeriod: 3424 << 2, + DeltaMult: 2, } diff --git a/format/s3m/system/system.go b/format/s3m/system/system.go index cfe8bef..81175ea 100644 --- a/format/s3m/system/system.go +++ b/format/s3m/system/system.go @@ -9,21 +9,24 @@ import ( const ( floatDefaultC4SampleRate = float32(DefaultC4SampleRate) - c2Period = 1712 // 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 * c2Period + S3MBaseClock frequency.Frequency = DefaultC4SampleRate * C4Period NotesPerOctave = 12 SlideFinesPerSemitone = 4 SemitonesPerNote = 16 SlideFinesPerNote = SlideFinesPerSemitone * SemitonesPerNote SlideFinesPerOctave = SlideFinesPerNote * NotesPerOctave - - C4SlideFines = 4 * SlideFinesPerOctave + C4SlideFines = C4Note * SlideFinesPerNote ) var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} diff --git a/format/xm/channel/effect_portatonote.go b/format/xm/channel/effect_portatonote.go index c840190..1656ee5 100644 --- a/format/xm/channel/effect_portatonote.go +++ b/format/xm/channel/effect_portatonote.go @@ -32,7 +32,7 @@ func (e PortaToNote[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, } xx := mem.PortaToNote(DataEffect(e)) - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4, false) } func (e PortaToNote[TPeriod]) TraceData() string { diff --git a/format/xm/period/amigaconverter.go b/format/xm/period/amigaconverter.go index abae9a5..6670fb1 100644 --- a/format/xm/period/amigaconverter.go +++ b/format/xm/period/amigaconverter.go @@ -9,4 +9,5 @@ var AmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ System: system.XMSystem, MinPeriod: 1, MaxPeriod: 31999, + DeltaMult: 2, } diff --git a/period/amigaconverter.go b/period/amigaconverter.go index 1a5d721..b4c3942 100644 --- a/period/amigaconverter.go +++ b/period/amigaconverter.go @@ -15,6 +15,7 @@ type AmigaConverter struct { System system.ClockableSystem MinPeriod Amiga MaxPeriod Amiga + DeltaMult Delta } var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) @@ -92,7 +93,7 @@ func (c AmigaConverter) PortaToNoteGeneric(p Period, delta Delta, target Period) } func (c AmigaConverter) PortaDown(p Amiga, delta Delta) (Amiga, error) { - return min(max(p.PortaDown(int(delta)), c.MinPeriod), c.MaxPeriod), nil + return min(max(p.PortaDown(int(delta*c.DeltaMult)), c.MinPeriod), c.MaxPeriod), nil } func (c AmigaConverter) PortaDownGeneric(p Period, delta Delta) (Period, error) { @@ -105,7 +106,7 @@ func (c AmigaConverter) PortaDownGeneric(p Period, delta Delta) (Period, error) } func (c AmigaConverter) PortaUp(p Amiga, delta Delta) (Amiga, error) { - return min(max(p.PortaUp(int(delta)), c.MinPeriod), c.MaxPeriod), nil + return min(max(p.PortaUp(int(delta*c.DeltaMult)), c.MinPeriod), c.MaxPeriod), nil } func (c AmigaConverter) PortaUpGeneric(p Period, delta Delta) (Period, error) { @@ -118,7 +119,7 @@ func (c AmigaConverter) PortaUpGeneric(p Period, delta Delta) (Period, error) { } func (c AmigaConverter) AddDelta(p Amiga, delta Delta) (Amiga, error) { - return min(max(p.Add(delta), c.MinPeriod), c.MaxPeriod), nil + return min(max(p.Add(delta*c.DeltaMult), c.MinPeriod), c.MaxPeriod), nil } func (c AmigaConverter) AddDeltaGeneric(p Period, delta Delta) (Period, error) { diff --git a/player/machine/channel.go b/player/machine/channel.go index a112672..50ba23a 100644 --- a/player/machine/channel.go +++ b/player/machine/channel.go @@ -31,13 +31,13 @@ type channel[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann Semitone memory.Value[note.Semitone] } target struct { - Inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] - Pos optional.Value[sampling.Pos] - ActionTick optional.Value[ActionTick] - TriggerNNA bool + PortaPeriod TPeriod + Inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning] + Pos optional.Value[sampling.Pos] + ActionTick optional.Value[ActionTick] + TriggerNNA bool } - portaPeriod TPeriod - newNote NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning] + newNote NewNoteInfo[TPeriod, TMixingVolume, TVolume, TPanning] mute bool surround bool diff --git a/player/machine/machine.go b/player/machine/machine.go index 18749d8..1e198ae 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -98,7 +98,7 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann 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 + DoChannelPortaToNote(ch index.Channel, delta period.Delta, useFinalPeriod bool) error DoChannelPortaDown(ch index.Channel, delta period.Delta) error DoChannelPortaUp(ch index.Channel, delta period.Delta) error DoChannelArpeggio(ch index.Channel, delta int8) error diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 6a7e6ea..7f8b794 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -321,8 +321,8 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Star if p, set := c.newNote.Period.Get(); set { traceChannelOptionalValueResetWithComment(m, ch, "newNote.Period", c.newNote.Period, "StartChannelPortaToNote") c.newNote.Period.Reset() - traceChannelValueChangeWithComment(m, ch, "portaPeriod", c.portaPeriod, p, "StartChannelPortaToNote") - c.portaPeriod = p + traceChannelValueChangeWithComment(m, ch, "target.PortaPeriod", c.target.PortaPeriod, p, "StartChannelPortaToNote") + c.target.PortaPeriod = p } traceChannelOptionalValueResetWithComment(m, ch, "newNote.Pos", c.newNote.Pos, "StartChannelPortaToNote") @@ -333,16 +333,25 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Star }) } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPortaToNote(ch index.Channel, delta period.Delta) error { +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPortaToNote(ch index.Channel, delta period.Delta, useFinalPeriod bool) 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.PortaToNote(p, delta, c.portaPeriod) + var p TPeriod + if useFinalPeriod { + 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") + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaToNote (%d)", delta) freqMod.SetPeriod(tp) } return nil @@ -358,7 +367,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoCh return err } - traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaDown") + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaDown (%d)", delta) freqMod.SetPeriod(tp) } return nil @@ -374,7 +383,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoCh return err } - traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaUp") + traceChannelValueChangeWithComment(m, ch, "target.Period", p, tp, "DoChannelPortaUp (%d)", delta) freqMod.SetPeriod(tp) } return nil diff --git a/player/machine/machine_tracing.go b/player/machine/machine_tracing.go index c3dfdeb..4bc4148 100644 --- a/player/machine/machine_tracing.go +++ b/player/machine/machine_tracing.go @@ -11,7 +11,7 @@ func trace[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPannin 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], comment string) { +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) } @@ -26,8 +26,8 @@ func traceOptionalValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolum m.us.TraceValueChange(name, nil, after) } -func traceWithComment[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning](m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], name, comment string) { - m.us.TraceWithComment(name, comment) +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) { @@ -37,35 +37,35 @@ func traceValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolume, TVolu 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], comment string) { +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, comment) + 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, comment string) { +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, comment) + traceValueChangeWithComment(m, name, v, after, commentFmt, commentParams...) return } - m.us.TraceValueChangeWithComment(name, nil, after, comment) + 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, comment string) { +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, comment) + 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, comment string) { - m.us.TraceChannelWithComment(ch, name, comment) +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]) { @@ -91,25 +91,25 @@ func traceChannelValueChange[T any, TPeriod Period, TGlobalVolume, TMixingVolume 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], comment string) { +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, comment) + 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, comment string) { +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, comment) + traceChannelValueChangeWithComment(m, ch, name, v, after, commentFmt, commentParams...) return } - m.us.TraceChannelValueChangeWithComment(ch, name, nil, after, comment) + 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, comment string) { +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, comment) + m.us.TraceChannelValueChangeWithComment(ch, name, before, after, commentFmt, commentParams...) } diff --git a/player/machine/settings/usersettings.go b/player/machine/settings/usersettings.go index 659f44d..e2e19ca 100644 --- a/player/machine/settings/usersettings.go +++ b/player/machine/settings/usersettings.go @@ -40,12 +40,12 @@ func (s UserSettings) Trace(op string) { s.Tracer.Trace(op) } -func (s UserSettings) TraceWithComment(op, comment string) { +func (s UserSettings) TraceWithComment(op, commentFmt string, commentParams ...any) { if s.Tracer == nil { return } - s.Tracer.TraceWithComment(op, comment) + s.Tracer.TraceWithComment(op, commentFmt, commentParams...) } func (s UserSettings) TraceValueChange(op string, prev, new any) { @@ -56,12 +56,12 @@ func (s UserSettings) TraceValueChange(op string, prev, new any) { s.Tracer.TraceValueChange(op, prev, new) } -func (s UserSettings) TraceValueChangeWithComment(op string, prev, new any, comment string) { +func (s UserSettings) TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) { if s.Tracer == nil { return } - s.Tracer.TraceValueChangeWithComment(op, prev, new, comment) + s.Tracer.TraceValueChangeWithComment(op, prev, new, commentFmt, commentParams...) } func (s UserSettings) TraceChannel(ch index.Channel, op string) { @@ -72,12 +72,12 @@ func (s UserSettings) TraceChannel(ch index.Channel, op string) { s.Tracer.TraceChannel(ch, op) } -func (s UserSettings) TraceChannelWithComment(ch index.Channel, op, comment string) { +func (s UserSettings) TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) { if s.Tracer == nil { return } - s.Tracer.TraceChannelWithComment(ch, op, comment) + s.Tracer.TraceChannelWithComment(ch, op, commentFmt, commentParams...) } func (s UserSettings) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { @@ -88,10 +88,10 @@ func (s UserSettings) TraceChannelValueChange(ch index.Channel, op string, prev, s.Tracer.TraceChannelValueChange(ch, op, prev, new) } -func (s UserSettings) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, comment string) { +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, comment) + s.Tracer.TraceChannelValueChangeWithComment(ch, op, prev, new, commentFmt, commentParams...) } diff --git a/tracing/tracer.go b/tracing/tracer.go index 30092c0..3ade08a 100644 --- a/tracing/tracer.go +++ b/tracing/tracer.go @@ -5,11 +5,11 @@ import "github.com/gotracker/playback/index" type Tracer interface { SetTracingTick(order index.Order, row index.Row, tick int) Trace(op string) - TraceWithComment(op, comment string) + TraceWithComment(op, commentFmt string, commentParams ...any) TraceValueChange(op string, prev, new any) - TraceValueChangeWithComment(op string, prev, new any, comment string) + TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) TraceChannel(ch index.Channel, op string) - TraceChannelWithComment(ch index.Channel, op, comment 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, comment string) + TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) } diff --git a/tracing/tracing.go b/tracing/tracing.go index f30093d..f34887a 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -100,16 +100,16 @@ func (t *Tracing) Trace(op string) { t.TraceWithComment(op, "") } -func (t *Tracing) TraceWithComment(op, comment string) { - traceWithPayload(t, t.GetTracingTick(), op, comment, empty) +func (t *Tracing) TraceWithComment(op, commentFmt string, commentParams ...any) { + traceWithPayload(t, t.GetTracingTick(), op, fmt.Sprintf(commentFmt, commentParams...), empty) } func (t *Tracing) TraceValueChange(op string, prev, new any) { t.TraceValueChangeWithComment(op, prev, new, "") } -func (t *Tracing) TraceValueChangeWithComment(op string, prev, new any, comment string) { - traceWithPayload(t, t.GetTracingTick(), op, comment, valueUpdate{ +func (t *Tracing) TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) { + traceWithPayload(t, t.GetTracingTick(), op, fmt.Sprintf(commentFmt, commentParams...), valueUpdate{ old: prev, new: new, }) @@ -119,24 +119,24 @@ func (t *Tracing) TraceChannel(ch index.Channel, op string) { t.TraceChannelWithComment(ch, op, "") } -func (t *Tracing) TraceChannelWithComment(ch index.Channel, op, comment string) { +func (t *Tracing) TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) { tc := tickChannel{ tick: t.GetTracingTick(), ch: ch, } - traceWithPayload(t, tc, op, comment, empty) + traceWithPayload(t, tc, op, fmt.Sprintf(commentFmt, commentParams...), empty) } func (t *Tracing) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { t.TraceChannelValueChangeWithComment(ch, op, prev, new, "") } -func (t *Tracing) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, comment string) { +func (t *Tracing) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) { tc := tickChannel{ tick: t.GetTracingTick(), ch: ch, } - traceWithPayload(t, tc, op, comment, valueUpdate{ + traceWithPayload(t, tc, op, fmt.Sprintf(commentFmt, commentParams...), valueUpdate{ old: prev, new: new, }) From 69957ccc61fda8e66219ccc0e8c27fcc68c79b16 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 16 Jan 2024 12:42:45 -0800 Subject: [PATCH 51/63] Fix OPL2 deferred keyon and base freq --- format/it/voice/voice.go | 28 ++++++++----------- format/s3m/voice/voice.go | 10 +++++-- format/xm/voice/voice.go | 28 ++++++++----------- voice/component/modulator_key.go | 47 ++++++++++++++++++++++++-------- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index e353981..135f076 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -15,9 +15,7 @@ import ( "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/opl2" - "github.com/gotracker/playback/voice/pcm" ) type Period interface { @@ -204,9 +202,19 @@ func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, itVolume.F Envelope: d.PitchFiltEnv, }) - v.amp.SetMixingVolumeOverride(d.MixingVolume) + if err := v.amp.SetMixingVolumeOverride(d.MixingVolume); err != nil { + return err + } - v.setupPCM(d.Sample, d.Loop, d.SustainLoop, itVolume.MaxItFineVolume, inst.GetDefaultVolume()) + 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) @@ -348,18 +356,6 @@ func (v *itVoice[TPeriod]) Clone(background bool) voice.Voice { return &vv } -func (v *itVoice[TPeriod]) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol itVolume.FineVolume, defVol itVolume.Volume) { - var s component.Sampler[TPeriod, itVolume.FineVolume, itVolume.Volume] - s.Setup(component.SamplerSettings[TPeriod, itVolume.FineVolume, itVolume.Volume]{ - Sample: samp, - DefaultVolume: defVol, - MixVolume: mixVol, - WholeLoop: wholeLoop, - SustainLoop: sustainLoop, - }) - v.voicer = &s -} - func (v *itVoice[TPeriod]) updateFinal() error { if v.IsDone() { v.finalVol = 0 diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index e9d5ee5..e85d722 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -113,7 +113,9 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.Fin switch d := inst.GetData().(type) { case *instrument.PCM[s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]: - v.AmpModulator.SetMixingVolumeOverride(d.MixingVolume) + 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]{ @@ -126,8 +128,12 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.Fin 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.S3MBaseClock, inst.GetDefaultVolume()) + o.Setup(v.opl2Chip, int(v.opl2Channel), v.opl2, s3mPeriod.S3MAmigaConverter, s3mSystem.DefaultC4SampleRate, inst.GetDefaultVolume()) v.voicer = &o default: diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index d9569e9..bc20aa7 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -13,9 +13,7 @@ import ( "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/loop" "github.com/gotracker/playback/voice/opl2" - "github.com/gotracker/playback/voice/pcm" ) type Period interface { @@ -159,9 +157,19 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.X Envelope: d.PanEnv, }) - v.amp.SetMixingVolumeOverride(d.MixingVolume) + if err := v.amp.SetMixingVolumeOverride(d.MixingVolume); err != nil { + return err + } - v.setupPCM(d.Sample, d.Loop, d.SustainLoop, xmVolume.DefaultXmMixingVolume, inst.GetDefaultVolume()) + 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) @@ -274,15 +282,3 @@ func (v *xmVoice[TPeriod]) Clone(bool) voice.Voice { return &vv } - -func (v *xmVoice[TPeriod]) setupPCM(samp pcm.Sample, wholeLoop, sustainLoop loop.Loop, mixVol, defVol xmVolume.XmVolume) { - var s component.Sampler[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] - s.Setup(component.SamplerSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume]{ - Sample: samp, - DefaultVolume: defVol, - MixVolume: mixVol, - WholeLoop: wholeLoop, - SustainLoop: sustainLoop, - }) - v.voicer = &s -} diff --git a/voice/component/modulator_key.go b/voice/component/modulator_key.go index f8b8345..468273e 100644 --- a/voice/component/modulator_key.go +++ b/voice/component/modulator_key.go @@ -10,7 +10,9 @@ import ( type KeyModulator struct { settings KeyModulatorSettings slimKeyModulator - fadeout bool + attackTriggersRelease bool + fadeout bool + deferredUpdates []bool } type KeyModulatorSettings struct { @@ -25,26 +27,43 @@ func (k *KeyModulator) Setup(settings KeyModulatorSettings) { k.settings = settings } -func (k *KeyModulator) DeferredUpdate() { - if k.keyOn == k.prevKeyOn { - return - } +func (k KeyModulator) GetAttackTriggersRelease() bool { + return k.attackTriggersRelease +} - if k.keyOn { - if k.settings.DeferredAttack != nil { - k.settings.DeferredAttack() - } - } else { - if k.settings.DeferredRelease != nil { - k.settings.DeferredRelease() +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() } @@ -53,6 +72,10 @@ func (k *KeyModulator) 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() } From 70f7c10799fd853edfe0035e0559e33ce5ff75b4 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Tue, 16 Jan 2024 20:13:03 -0800 Subject: [PATCH 52/63] reintegrate play until order/row support - fix up some quirks - clean up period converters --- format/it/channel/effect_portatonote.go | 2 +- format/it/channel/memory.go | 2 +- format/it/layout/channelsetting.go | 7 +- format/it/layout/song.go | 9 +- format/it/load/itformat.go | 1 + format/it/system/system.go | 1 + format/it/voice/voice.go | 10 ++- format/s3m/channel/effect_arpeggio.go | 7 +- .../s3m/channel/effect_extrafineportadown.go | 2 +- format/s3m/channel/effect_portadown.go | 2 +- format/s3m/channel/effect_portatonote.go | 4 +- format/s3m/channel/effect_portaup.go | 2 +- format/s3m/channel/effect_setfinetune.go | 5 +- format/s3m/channel/effectfactory.go | 8 +- format/s3m/channel/memory.go | 12 +-- format/s3m/layout/channelsetting.go | 7 +- format/s3m/layout/song.go | 13 +-- format/s3m/load/modconv/modconverter.go | 33 ++++--- format/s3m/load/s3mformat.go | 37 ++++---- format/s3m/period/amigaconverter.go | 10 +-- format/s3m/period/util.go | 89 ++++++++----------- format/s3m/settings/machine.go | 7 ++ format/s3m/system/system.go | 1 + format/s3m/voice/voice.go | 6 +- format/xm/channel/effect_portatonote.go | 2 +- format/xm/channel/memory.go | 2 +- format/xm/layout/channelsetting.go | 7 +- format/xm/layout/song.go | 9 +- format/xm/load/xmformat.go | 1 + format/xm/period/amigaconverter.go | 1 - format/xm/system/system.go | 1 + format/xm/voice/voice.go | 10 ++- period/amiga.go | 22 ++--- period/amigaconverter.go | 80 +++-------------- period/linearconverter.go | 70 ++------------- period/period.go | 32 ------- period/periodconverter.go | 9 +- player/machine/channel.go | 1 - player/machine/channel_notedecode.go | 1 + player/machine/channel_tick.go | 15 +++- player/machine/machine.go | 13 +-- player/machine/machine_channel.go | 26 +++--- player/machine/machine_factory.go | 8 +- player/machine/machine_render.go | 13 ++- player/machine/settings/machinesettings.go | 7 ++ player/machine/ticker.go | 20 +++++ song/channelmemory.go | 2 +- song/channelsettings.go | 2 +- song/periodcalculator.go | 9 +- system/clockedsystem.go | 14 ++- system/system.go | 1 - voice/component/modulator_amp.go | 15 +++- voice/mixer/details.go | 11 ++- voice/render.go | 2 +- voice/voice.go | 2 + 55 files changed, 308 insertions(+), 377 deletions(-) diff --git a/format/it/channel/effect_portatonote.go b/format/it/channel/effect_portatonote.go index 85f7525..7948ce5 100644 --- a/format/it/channel/effect_portatonote.go +++ b/format/it/channel/effect_portatonote.go @@ -30,7 +30,7 @@ func (e PortaToNote[TPeriod]) Tick(ch index.Channel, m machine.Machine[TPeriod, xx := mem.PortaToNote(DataEffect(e)) if !mem.Shared.OldEffectMode || tick != 0 { - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4, false) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) } return nil } diff --git a/format/it/channel/memory.go b/format/it/channel/memory.go index d602a3f..80afede 100644 --- a/format/it/channel/memory.go +++ b/format/it/channel/memory.go @@ -163,7 +163,7 @@ func (m *Memory) Retrigger() { } // 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/layout/channelsetting.go b/format/it/layout/channelsetting.go index 2263def..a2aa64a 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -12,6 +12,7 @@ import ( // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled bool + Muted bool OutputChannelNum int InitialVolume itVolume.Volume ChannelVolume itVolume.FineVolume @@ -23,10 +24,14 @@ type ChannelSetting struct { var _ song.ChannelSettings = (*ChannelSetting)(nil) -func (c ChannelSetting) GetEnabled() bool { +func (c ChannelSetting) IsEnabled() bool { return c.Enabled } +func (c ChannelSetting) IsMuted() bool { + return c.Muted +} + func (c ChannelSetting) GetOutputChannelNum() int { return c.OutputChannelNum } diff --git a/format/it/layout/song.go b/format/it/layout/song.go index bfcd4ae..5ac62b5 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -15,6 +15,7 @@ import ( "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/system" @@ -23,7 +24,7 @@ import ( // Song is the full definition of the song data of an IT file type Song[TPeriod period.Period] struct { System system.System - MS any + MS *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] Head Header Instruments map[uint8]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument[TPeriod] @@ -208,8 +209,10 @@ func (s Song[TPeriod]) GetSystem() system.System { func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { for i, cs := range s.ChannelSettings { - if enabledOnly && !cs.Enabled { - continue + if enabledOnly { + if !cs.Enabled || (cs.Muted && s.MS.Quirks.DoNotProcessEffectsOnMutedChannels) { + continue + } } cont, err := fn(index.Channel(i)) if err != nil { diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index c31bfed..7bb1a12 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -194,6 +194,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] cs := layout.ChannelSetting{ OutputChannelNum: chNum, Enabled: true, + Muted: false, InitialVolume: itVolume.Volume(itVolume.DefaultItVolume), ChannelVolume: min(itVolume.FineVolume(f.Head.ChannelVol[chNum]*2), itVolume.MaxItFineVolume), PanEnabled: stereoMode, diff --git a/format/it/system/system.go b/format/it/system/system.go index 58880dc..cc89289 100644 --- a/format/it/system/system.go +++ b/format/it/system/system.go @@ -30,6 +30,7 @@ var ITSystem system.ClockableSystem = system.ClockedSystem{ BaseFinetunes: C5SlideFines, FinetunesPerOctave: SlideFinesPerOctave, FinetunesPerNote: SlideFinesPerNote, + CommonPeriod: C5Period, CommonRate: DefaultC5SampleRate, SemitonePeriods: semitonePeriodTable, } diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 135f076..b94e095 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -259,7 +259,7 @@ func (v *itVoice[TPeriod]) Stop() { v.updateFinal() } -func (v *itVoice[TPeriod]) IsDone() bool { +func (v itVoice[TPeriod]) IsDone() bool { if v.voicer == nil { return true } @@ -271,6 +271,14 @@ func (v *itVoice[TPeriod]) IsDone() bool { 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() diff --git a/format/s3m/channel/effect_arpeggio.go b/format/s3m/channel/effect_arpeggio.go index 3ce0fad..ee3cf59 100644 --- a/format/s3m/channel/effect_arpeggio.go +++ b/format/s3m/channel/effect_arpeggio.go @@ -18,12 +18,7 @@ func (e Arpeggio) String() string { } func (e Arpeggio) 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)) + x, y := DataEffect(e)>>4, DataEffect(e)&0xF return doArpeggio(ch, m, tick, int8(x), int8(y)) } diff --git a/format/s3m/channel/effect_extrafineportadown.go b/format/s3m/channel/effect_extrafineportadown.go index be71f04..af3325b 100644 --- a/format/s3m/channel/effect_extrafineportadown.go +++ b/format/s3m/channel/effect_extrafineportadown.go @@ -23,7 +23,7 @@ func (e ExtraFinePortaDown) Tick(ch index.Channel, m machine.Machine[period.Amig } y := DataEffect(e) & 0x0F - return m.DoChannelPortaDown(ch, period.Delta(y)*4) + return m.DoChannelPortaDown(ch, period.Delta(y)*1) } func (e ExtraFinePortaDown) TraceData() string { diff --git a/format/s3m/channel/effect_portadown.go b/format/s3m/channel/effect_portadown.go index ed342fa..50dec35 100644 --- a/format/s3m/channel/effect_portadown.go +++ b/format/s3m/channel/effect_portadown.go @@ -23,7 +23,7 @@ func (e PortaDown) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVol return err } - xx := mem.LastNonZero(DataEffect(e)) + xx := mem.Porta(DataEffect(e)) if tick == 0 && !mem.Shared.AmigaSlides { return nil diff --git a/format/s3m/channel/effect_portatonote.go b/format/s3m/channel/effect_portatonote.go index 8ccdf85..f50307f 100644 --- a/format/s3m/channel/effect_portatonote.go +++ b/format/s3m/channel/effect_portatonote.go @@ -27,13 +27,13 @@ func (e PortaToNote) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mV return err } - xx := mem.PortaToNote(DataEffect(e)) + xx := mem.Porta(DataEffect(e)) if tick == 0 && !mem.Shared.AmigaSlides { return nil } - return m.DoChannelPortaToNote(ch, period.Delta(xx)*4, true) + return m.DoChannelPortaToNote(ch, period.Delta(xx)*4) } func (e PortaToNote) TraceData() string { diff --git a/format/s3m/channel/effect_portaup.go b/format/s3m/channel/effect_portaup.go index 0da1f50..cbfe173 100644 --- a/format/s3m/channel/effect_portaup.go +++ b/format/s3m/channel/effect_portaup.go @@ -23,7 +23,7 @@ func (e PortaUp) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3mVolum return err } - xx := mem.LastNonZero(DataEffect(e)) + xx := mem.Porta(DataEffect(e)) if tick == 0 && !mem.Shared.AmigaSlides { return nil diff --git a/format/s3m/channel/effect_setfinetune.go b/format/s3m/channel/effect_setfinetune.go index fce3862..d2b3111 100644 --- a/format/s3m/channel/effect_setfinetune.go +++ b/format/s3m/channel/effect_setfinetune.go @@ -4,9 +4,9 @@ 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/note" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" ) @@ -27,8 +27,7 @@ func (e SetFinetune) RowStart(ch index.Channel, m machine.Machine[period.Amiga, } if inst != nil { - ft := (note.Finetune(x) - 8) * 4 - inst.SetFinetune(ft) + inst.SetSampleRate(s3mPeriod.CalcFinetuneC4SampleRate(uint8(x))) } return nil } diff --git a/format/s3m/channel/effectfactory.go b/format/s3m/channel/effectfactory.go index dd15eca..66fbd5e 100644 --- a/format/s3m/channel/effectfactory.go +++ b/format/s3m/channel/effectfactory.go @@ -19,7 +19,9 @@ func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) playbac return nil } - mem.LastNonZero(d.Info) + // Store the last non-zero value + _ = mem.LastNonZero(d.Info) + switch d.Command + '@' { case '@': // unused return nil @@ -32,7 +34,7 @@ func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) playbac case 'D': // Volume Slide / Fine Volume Slide return volumeSlideFactory(mem, d.Command, d.Info) case 'E': // Porta Down/Fine Porta Down/Xtra Fine Porta - xx := mem.LastNonZero(d.Info) + xx := mem.Porta(d.Info) x := xx >> 4 if x == 0x0F { return FinePortaDown(xx) @@ -41,7 +43,7 @@ func EffectFactory(mem *Memory, data song.ChannelData[s3mVolume.Volume]) playbac } return PortaDown(d.Info) case 'F': // Porta Up/Fine Porta Up/Extra Fine Porta Down - xx := mem.LastNonZero(d.Info) + xx := mem.Porta(d.Info) x := xx >> 4 if x == 0x0F { return FinePortaUp(xx) diff --git a/format/s3m/channel/memory.go b/format/s3m/channel/memory.go index d87c247..f80aa06 100644 --- a/format/s3m/channel/memory.go +++ b/format/s3m/channel/memory.go @@ -7,7 +7,7 @@ import ( // 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] @@ -22,9 +22,9 @@ type Memory struct { Shared *SharedMemory } -// 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 @@ -79,9 +79,9 @@ func (m *Memory) Retrigger() { } // 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/layout/channelsetting.go b/format/s3m/layout/channelsetting.go index 421ea60..9db9690 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -13,6 +13,7 @@ import ( // ChannelSetting is settings specific to a single channel type ChannelSetting struct { Enabled bool + Muted bool OutputChannelNum int Category s3mfile.ChannelCategory InitialVolume s3mVolume.Volume @@ -24,10 +25,14 @@ type ChannelSetting struct { var _ song.ChannelSettings = (*ChannelSetting)(nil) -func (c ChannelSetting) GetEnabled() bool { +func (c ChannelSetting) IsEnabled() bool { return c.Enabled } +func (c ChannelSetting) IsMuted() bool { + return c.Muted +} + func (c ChannelSetting) GetOutputChannelNum() int { return c.OutputChannelNum } diff --git a/format/s3m/layout/song.go b/format/s3m/layout/song.go index 42f489b..ab9b4cf 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -13,6 +13,7 @@ import ( "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" "github.com/gotracker/playback/system" @@ -21,7 +22,7 @@ import ( // Song is the full definition of the song data of an Song file type Song struct { System system.System - MS any + MS *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] Head Header Instruments []*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] Patterns []song.Pattern @@ -186,7 +187,7 @@ func (s Song) GetRowRenderStringer(row song.Row, channels int, longFormat bool) pr := row.(Row) nprch := min(len(pr), nch) for i := 0; i < nprch; i++ { - if !s.ChannelSettings[i].Enabled { + if !s.ChannelSettings[i].Enabled || s.ChannelSettings[i].Muted { continue } rowData = append(rowData, pr[i]) @@ -205,8 +206,10 @@ func (s Song) GetSystem() system.System { 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 && !cs.Enabled { - continue + if enabledOnly { + if !cs.Enabled || (cs.Muted && s.MS.Quirks.DoNotProcessEffectsOnMutedChannels) { + continue + } } cont, err := fn(ch) if err != nil { @@ -221,7 +224,7 @@ func (s Song) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, func (s Song) IsOPL2Enabled() bool { for _, cs := range s.ChannelSettings { - if !cs.Enabled { + if !cs.Enabled || cs.Muted { continue } diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index c5f0fcd..1e3bf23 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -290,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 } @@ -308,16 +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)), - Flags: 0x0004 | 0x0010 | 0x0020, // amigaSlides | amigaLimits | sbFilterEnable - 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) + 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 }, } @@ -367,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 c8dd571..1c8412a 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -257,7 +257,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, return nil, err } - amigaLimits := (f.Head.Flags & 0x0010) != 0 + amigaLimits := (f.Head.Flags&0x0010) != 0 || wasModFile s := layout.Song{ System: s3mSystem.S3MSystem, @@ -267,22 +267,28 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, OrderList: make([]index.Pattern, len(f.OrderList)), } - signedSamples := false - if f.Head.FileFormatInformation == 1 { - signedSamples = true + f.Head.GlobalVolume = min(f.Head.GlobalVolume, s3mfile.Volume(s3mVolume.MaxVolume)) + if f.Head.GlobalVolume == 0 && f.Head.TrackerVersion < 0x1320 { + f.Head.GlobalVolume = s3mfile.Volume(s3mVolume.MaxVolume) } - stereoMode := (f.Head.MixingVolume & 0x80) != 0 + if f.Head.InitialSpeed == 0 || f.Head.InitialSpeed == 255 { + s.Head.InitialSpeed = 6 + } + + if f.Head.InitialTempo < 33 { + s.Head.InitialTempo = 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 //ptrSpecialIsValid := (f.Head.Flags & 0x0080) != 0 for i, o := range f.OrderList { @@ -302,7 +308,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, s.Instruments[instNum] = sample } - maxPatternChannel := 0 + maxPatternChannel := 3 s.Patterns = make([]song.Pattern, len(f.Patterns)) for patNum, pkt := range f.Patterns { pattern, maxCh := convertS3MPackedPattern(pkt, getPatternLen(patNum)) @@ -328,11 +334,12 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } channels := make([]layout.ChannelSetting, 0, maxPatternChannel+1) - lastEnabledChannel := 0 + 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.Volume(s3mfile.DefaultVolume), diff --git a/format/s3m/period/amigaconverter.go b/format/s3m/period/amigaconverter.go index 81c79d3..912e7fb 100644 --- a/format/s3m/period/amigaconverter.go +++ b/format/s3m/period/amigaconverter.go @@ -9,15 +9,13 @@ var S3MAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConvert System: system.S3MSystem, MinPeriod: 64, MaxPeriod: 32767, - DeltaMult: 2, } -//MinMOD15Period = 113 << 2 -//MaxMOD15Period = 856 << 2 +//MinMOD15Period = 452 +//MaxMOD15Period = 3424 var MODAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ System: system.S3MSystem, - MinPeriod: 14 << 2, - MaxPeriod: 3424 << 2, - DeltaMult: 2, + MinPeriod: 56, + MaxPeriod: 13696, } diff --git a/format/s3m/period/util.go b/format/s3m/period/util.go index 3312bc6..9ded5d8 100644 --- a/format/s3m/period/util.go +++ b/format/s3m/period/util.go @@ -1,66 +1,49 @@ package period import ( - "math" - "github.com/gotracker/playback/format/s3m/system" "github.com/gotracker/playback/frequency" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" ) 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, c4SampleRate frequency.Frequency) period.Amiga { - if semi == note.UnchangedSemitone { - panic("how?") - } - - key := int(semi.Key()) - octave := uint32(semi.Octave()) - - if key >= len(semitonePeriodTable) { - var empty period.Amiga - return empty - } - - if c4SampleRate == 0 { - c4SampleRate = frequency.Frequency(system.DefaultC4SampleRate) - } - - if ft != 0 { - c4SampleRate = CalcFinetuneC4SampleRate(c4SampleRate, ft) - } - - return period.Amiga(float64(semitonePeriodTable[key]*float32(system.DefaultC4SampleRate)) / float64(uint32(c4SampleRate)<= len(m.channels) { - return false - } - - return m.songData.GetChannelSettings(ch).GetEnabled() -} - 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) } diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 7f8b794..d82a4a6 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -62,6 +62,20 @@ func GetChannelMemory[TMemory song.ChannelMemory, TPeriod Period, TGlobalVolume, 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) @@ -202,14 +216,6 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetC }) } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelMute(ch index.Channel, enabled bool) error { - return withChannel(m, ch, func(c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) error { - traceChannelValueChangeWithComment(m, ch, "mute", c.mute, enabled, "SetChannelMute") - c.mute = enabled - 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") @@ -333,11 +339,11 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Star }) } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoChannelPortaToNote(ch index.Channel, delta period.Delta, useFinalPeriod bool) error { +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 useFinalPeriod { + if m.ms.Quirks.PortaToNoteUsesModifiedPeriod { var err error p, err = freqMod.GetFinalPeriod() if err != nil { diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 231d65e..81429c9 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -156,7 +156,10 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc.GlobalVolume = volume.Volume(1) c := &m.channels[ch] - c.enabled = cs.GetEnabled() + c.enabled = true + if m.ms.Quirks.DoNotProcessEffectsOnMutedChannels && cs.IsMuted() { + c.enabled = false + } c.cv = m.ms.VoiceFactory.NewVoice(voice.VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ PC: ms.PeriodConverter, OPLChannel: cs.GetOPLChannel(), @@ -184,6 +187,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum 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 { @@ -202,6 +206,8 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum InitialRow: row, SongLoopStartingOrder: 0, SongLoopCount: us.SongLoopCount, + PlayUntilOrder: us.PlayUntil.Order, + PlayUntilRow: us.PlayUntil.Row, }); err != nil { return nil, err } diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index 492e969..e9f31ec 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -37,15 +37,12 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend renderRow.RowText = m.rowStringer } - sys := m.songData.GetSystem() - details := mixer.Details{ - Mix: s.Mixer(), - Panmixer: s.GetPanMixer(), - SampleRate: frequency.Frequency(s.SampleRate), - SamplerSpeed: sys.GetSamplerSpeed(frequency.Frequency(s.SampleRate)), - Samples: premix.SamplesLen, - Duration: tickDuration, + Mix: s.Mixer(), + Panmixer: s.GetPanMixer(), + SampleRate: frequency.Frequency(s.SampleRate), + Samples: premix.SamplesLen, + Duration: tickDuration, } centerAheadPan := details.Panmixer.GetMixingMatrix(panning.CenterAhead) diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go index b428cf7..90f1fa0 100644 --- a/player/machine/settings/machinesettings.go +++ b/player/machine/settings/machinesettings.go @@ -24,4 +24,11 @@ type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum 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/ticker.go b/player/machine/ticker.go index 8e98ba4..81cea89 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -35,6 +35,8 @@ type tickerSettings struct { 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 { @@ -127,6 +129,24 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann 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 } diff --git a/song/channelmemory.go b/song/channelmemory.go index 1f9d26e..02bcc01 100644 --- a/song/channelmemory.go +++ b/song/channelmemory.go @@ -2,5 +2,5 @@ package song type ChannelMemory interface { Retrigger() - StartOrder() + StartOrder0() } diff --git a/song/channelsettings.go b/song/channelsettings.go index 7458202..901a064 100644 --- a/song/channelsettings.go +++ b/song/channelsettings.go @@ -8,7 +8,7 @@ import ( ) type ChannelSettings interface { - GetEnabled() bool + IsMuted() bool GetOutputChannelNum() int GetMemory() ChannelMemory IsPanEnabled() bool diff --git a/song/periodcalculator.go b/song/periodcalculator.go index 4fcfb47..cb75dd4 100644 --- a/song/periodcalculator.go +++ b/song/periodcalculator.go @@ -4,14 +4,11 @@ import ( "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" + "github.com/gotracker/playback/system" ) type PeriodCalculatorIntf interface { - GetPeriodGeneric(note.Note) period.Period - PortaToNoteGeneric(period.Period, period.Delta, period.Period) (period.Period, error) - PortaDownGeneric(period.Period, period.Delta) (period.Period, error) - PortaUpGeneric(period.Period, period.Delta) (period.Period, error) - AddDeltaGeneric(period.Period, period.Delta) (period.Period, error) + GetSystem() system.System } type PeriodCalculator[TPeriod period.Period] interface { @@ -23,6 +20,6 @@ type PeriodCalculator[TPeriod period.Period] interface { PortaUp(TPeriod, period.Delta) (TPeriod, error) AddDelta(TPeriod, period.Delta) (TPeriod, error) - GetSamplerAdd(TPeriod, float64) float64 + GetSamplerAdd(TPeriod, frequency.Frequency, frequency.Frequency) float64 GetFrequency(TPeriod) frequency.Frequency } diff --git a/system/clockedsystem.go b/system/clockedsystem.go index aaa7b21..aeba5d4 100644 --- a/system/clockedsystem.go +++ b/system/clockedsystem.go @@ -8,11 +8,11 @@ import ( type ClockableSystem interface { System GetBaseClock() frequency.Frequency + GetCommonPeriod() float64 GetBaseFinetunes() note.Finetune GetFinetunesPerOctave() note.Finetune GetFinetunesPerSemitone() note.Finetune GetSemitonePeriod(note.Key) (float32, bool) - GetSamplerSpeed(sampleRate frequency.Frequency) float32 } type ClockedSystem struct { @@ -22,6 +22,7 @@ type ClockedSystem struct { BaseFinetunes note.Finetune FinetunesPerOctave note.Finetune FinetunesPerNote note.Finetune + CommonPeriod float64 CommonRate frequency.Frequency SemitonePeriods [note.NumKeys]float32 } @@ -48,15 +49,12 @@ func (s ClockedSystem) GetFinetunesPerSemitone() note.Finetune { return s.FinetunesPerNote } -func (s ClockedSystem) GetCommonRate() frequency.Frequency { - return s.CommonRate +func (s ClockedSystem) GetCommonPeriod() float64 { + return s.CommonPeriod } -func (s ClockedSystem) GetSamplerSpeed(sampleRate frequency.Frequency) float32 { - if sampleRate == 0 { - panic("sampleRate is 0") - } - return float32(s.BaseClock) / float32(sampleRate) +func (s ClockedSystem) GetCommonRate() frequency.Frequency { + return s.CommonRate } func (s ClockedSystem) GetSemitonePeriod(k note.Key) (float32, bool) { diff --git a/system/system.go b/system/system.go index 2760304..6d5a2c4 100644 --- a/system/system.go +++ b/system/system.go @@ -5,5 +5,4 @@ import "github.com/gotracker/playback/frequency" type System interface { GetMaxPastNotesPerChannel() int GetCommonRate() frequency.Frequency - GetSamplerSpeed(sampleRate frequency.Frequency) float32 } diff --git a/voice/component/modulator_amp.go b/voice/component/modulator_amp.go index c2cf178..687ab14 100755 --- a/voice/component/modulator_amp.go +++ b/voice/component/modulator_amp.go @@ -16,6 +16,7 @@ type AmpModulator[TMixingVolume, TVolume types.Volume] struct { unkeyed struct { active bool + muted bool vol TVolume mixing TMixingVolume } @@ -28,6 +29,7 @@ type AmpModulator[TMixingVolume, TVolume types.Volume] struct { type AmpModulatorSettings[TMixingVolume, TVolume types.Volume] struct { Active bool + Muted bool DefaultMixingVolume TMixingVolume DefaultVolume TVolume } @@ -35,6 +37,7 @@ type AmpModulatorSettings[TMixingVolume, TVolume types.Volume] struct { 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() @@ -60,6 +63,15 @@ 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() { @@ -130,8 +142,9 @@ func (a *AmpModulator[TMixingVolume, TVolume]) updateFinal() error { } func (a AmpModulator[TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - t.TraceChannelWithComment(ch, fmt.Sprintf("active{%v} vol{%v} mixing{%v} mixingOverride{%v} delta{%v} final{%v}", + 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, diff --git a/voice/mixer/details.go b/voice/mixer/details.go index 0192330..3b3ce39 100644 --- a/voice/mixer/details.go +++ b/voice/mixer/details.go @@ -8,10 +8,9 @@ import ( ) type Details struct { - Mix *mixing.Mixer - Panmixer mixing.PanMixer - SampleRate frequency.Frequency - SamplerSpeed float32 - Samples int - Duration time.Duration + Mix *mixing.Mixer + Panmixer mixing.PanMixer + SampleRate frequency.Frequency + Samples int + Duration time.Duration } diff --git a/voice/render.go b/voice/render.go index 786e377..93193f1 100644 --- a/voice/render.go +++ b/voice/render.go @@ -34,7 +34,7 @@ func RenderAndTick[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], return nil, err } - samplerAdd := float32(pc.GetSamplerAdd(p, float64(rs.GetSampleRate())*float64(details.SamplerSpeed))) + samplerAdd := float32(pc.GetSamplerAdd(p, rs.GetSampleRate(), details.SampleRate)) o := mixer.Output{ Input: rs, diff --git a/voice/voice.go b/voice/voice.go index da7ede3..c303b35 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -34,6 +34,8 @@ type Voice interface { // General Parameters IsDone() bool + SetMuted(muted bool) error + IsMuted() bool GetSampleRate() frequency.Frequency } From 6567270558c67ccaa63bf4e8ee14031bd427a625 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Wed, 17 Jan 2024 20:32:59 -0800 Subject: [PATCH 53/63] major cleanup --- format.go | 13 -- format/common/format.go | 35 +++++ format/common/loadformat.go | 14 +- format/format.go | 28 ++-- format/it/it.go | 32 +++- format/it/load/load.go | 7 +- format/it/period/util.go | 74 --------- format/it/playback/playback.go | 60 -------- format/it/system/system.go | 2 +- format/it/voice/voice.go | 10 +- format/mod/mod.go | 11 +- format/s3m/channel/effect_fineportadown.go | 12 +- format/s3m/channel/effect_fineportaup.go | 12 +- format/s3m/channel/effect_portadown.go | 6 +- format/s3m/channel/effect_portatonote.go | 6 +- format/s3m/channel/effect_portaup.go | 6 +- format/s3m/channel/effect_volslidedown.go | 2 +- format/s3m/channel/effect_volslideup.go | 2 +- format/s3m/channel/sharedmem.go | 3 +- format/s3m/load/load.go | 10 +- format/s3m/load/s3mformat.go | 4 +- format/s3m/period/amigaconverter.go | 7 +- format/s3m/playback/playback.go | 46 ------ format/s3m/s3m.go | 15 +- format/s3m/settings/machine.go | 5 +- format/s3m/system/system.go | 5 +- format/s3m/voice/voice.go | 18 ++- format/xm/load/load.go | 7 +- format/xm/period/util.go | 50 ------- format/xm/playback/playback.go | 54 ------- format/xm/system/system.go | 2 +- format/xm/voice/voice.go | 10 +- format/xm/xm.go | 17 ++- internal/examples/bufferload/example.go | 54 ++++--- internal/examples/fileload/example.go | 37 +++-- note/note_test.go | 2 +- pattern/transaction.go | 112 -------------- period/amiga.go | 44 ++++-- period/amigaconverter.go | 30 ++-- playback.go | 4 + player/feature/enabletracing.go | 5 - player/machine/channel_noteaction.go | 10 +- player/machine/machine.go | 14 +- player/machine/machine_factory.go | 5 +- player/machine/machine_tick.go | 28 ++-- player/machine/settings/usersettings.go | 38 ++++- player/machine/ticker.go | 4 +- player/tracker.go | 165 --------------------- song/channelsettings.go | 1 + system/clockedsystem.go | 20 ++- tracing/entry.go | 8 +- tracing/tickchannel.go | 6 +- tracing/tracer.go | 25 +++- tracing/{tracing.go => tracerfile.go} | 59 ++++---- tracing/valueupdate.go | 12 +- voice/render.go | 4 + voice/voice.go | 4 +- 57 files changed, 465 insertions(+), 811 deletions(-) delete mode 100644 format.go create mode 100644 format/common/format.go delete mode 100644 format/it/period/util.go delete mode 100644 format/it/playback/playback.go delete mode 100644 format/s3m/playback/playback.go delete mode 100644 format/xm/playback/playback.go delete mode 100644 pattern/transaction.go delete mode 100644 player/feature/enabletracing.go delete mode 100644 player/tracker.go rename tracing/{tracing.go => tracerfile.go} (54%) diff --git a/format.go b/format.go deleted file mode 100644 index 2708d56..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 interface { - Load(filename string, features []feature.Feature) (Playback, error) - LoadFromReader(r io.Reader, features []feature.Feature) (Playback, error) -} 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 94726a7..bd98d53 100644 --- a/format/common/loadformat.go +++ b/format/common/loadformat.go @@ -3,22 +3,12 @@ package common import ( "io" - "github.com/gotracker/playback" "github.com/gotracker/playback/player/feature" "github.com/gotracker/playback/song" ) type ReaderFunc func(r io.Reader, features []feature.Feature) (song.Data, error) -type ManagerFactory func(song.Data) (playback.Playback, error) - -func Load(r io.Reader, reader ReaderFunc, factory ManagerFactory, features []feature.Feature) (playback.Playback, 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 c1d08d9..1ecb71c 100644 --- a/format/format.go +++ b/format/format.go @@ -5,23 +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) + 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, 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 } @@ -30,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, 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] @@ -39,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 } @@ -48,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/it.go b/format/it/it.go index 9cdbbe9..3d1e7b5 100644 --- a/format/it/it.go +++ b/format/it/it.go @@ -4,16 +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" - "github.com/gotracker/playback/format/it/settings" + 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 @@ -21,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 @@ -31,11 +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(settings.GetMachineSettings[period.Amiga]()) - machine.RegisterMachine(settings.GetMachineSettings[period.Linear]()) + machine.RegisterMachine(itSettings.GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(itSettings.GetMachineSettings[period.Linear]()) } 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/period/util.go b/format/it/period/util.go deleted file mode 100644 index bc5d2c3..0000000 --- a/format/it/period/util.go +++ /dev/null @@ -1,74 +0,0 @@ -package period - -import ( - "math" - - "github.com/gotracker/playback/format/it/system" - "github.com/gotracker/playback/frequency" - "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" -) - -var DefaultC5SampleRate = system.DefaultC5SampleRate -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[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c5SampleRate frequency.Frequency) TPeriod { - if semi == note.UnchangedSemitone { - panic("how?") - } - var result TPeriod - switch p := any(&result).(type) { - case *period.Linear: - nft := int(semi)*system.SlideFinesPerNote + int(ft) - p.Finetune = note.Finetune(nft) - case *period.Amiga: - key := int(semi.Key()) - octave := uint32(semi.Octave()) - - if key >= len(semitonePeriodTable) { - return result - } - - if c5SampleRate == 0 { - c5SampleRate = frequency.Frequency(system.DefaultC5SampleRate) - } - - if ft != 0 { - c5SampleRate = CalcFinetuneC5SampleRate[period.Amiga](AmigaConverter, c5SampleRate, ft) - } - - *p = period.Amiga(float64(semitonePeriodTable[key]*system.DefaultC5SampleRate) / float64(uint32(c5SampleRate)<> 4 - if mem.Shared.VolSlideEveryFrame || tick != 0 { + if mem.Shared.VolSlideEveryTick || tick != 0 { return m.SlideChannelVolume(ch, 1, float32(x)) } return nil 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/load/load.go b/format/s3m/load/load.go index 1cdc298..9be1672 100644 --- a/format/s3m/load/load.go +++ b/format/s3m/load/load.go @@ -3,10 +3,8 @@ package load import ( "io" - "github.com/gotracker/playback" "github.com/gotracker/playback/format/common" "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" ) @@ -23,11 +21,11 @@ func readMOD(r io.Reader, features []feature.Feature) (song.Data, 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/s3mformat.go b/format/s3m/load/s3mformat.go index 1c8412a..c9634c9 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -289,6 +289,7 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, 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 { @@ -322,7 +323,8 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, } sharedMem := channel.SharedMemory{ - VolSlideEveryFrame: st300volSlides, + VolSlideEveryTick: st300volSlides, + ST300Portas: st300portas, LowPassFilterEnable: sbFilterEnable, ResetMemoryAtStartOfOrder0: true, ST2Vibrato: st2Vibrato, diff --git a/format/s3m/period/amigaconverter.go b/format/s3m/period/amigaconverter.go index 912e7fb..8a0e787 100644 --- a/format/s3m/period/amigaconverter.go +++ b/format/s3m/period/amigaconverter.go @@ -6,9 +6,10 @@ import ( ) var S3MAmigaConverter period.PeriodConverter[period.Amiga] = period.AmigaConverter{ - System: system.S3MSystem, - MinPeriod: 64, - MaxPeriod: 32767, + System: system.S3MSystem, + MinPeriod: 64, + MaxPeriod: 32767, + SlideTo0Allowed: true, // this will allow sliding unclamped to 0, which in S3M stops the playback } //MinMOD15Period = 452 diff --git a/format/s3m/playback/playback.go b/format/s3m/playback/playback.go deleted file mode 100644 index dca0b9d..0000000 --- a/format/s3m/playback/playback.go +++ /dev/null @@ -1,46 +0,0 @@ -package playback - -import ( - "github.com/gotracker/playback" - "github.com/gotracker/playback/format/s3m/layout" - "github.com/gotracker/playback/player" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" -) - -// manager is a playback manager for S3M music -type manager struct { - player.Tracker - song *layout.Song -} - -var _ playback.Playback = (*manager)(nil) - -// NewManager creates a new manager for an S3M song -func NewManager(songData song.Data) (playback.Playback, error) { - s := songData.(*layout.Song) - - m := manager{ - song: s, - } - - return &m, nil -} - -// Configure sets specified features -func (m *manager) Configure(features []feature.Feature) error { - us, err := m.Tracker.Configure(features) - if err != nil { - return err - } - return m.SetupMachine(m.song, us) -} - -// SetupSampler configures the internal sampler -func (m *manager) SetupSampler(samplesPerSecond int, channels int) error { - if err := m.Tracker.SetupSampler(samplesPerSecond, channels); err != nil { - return err - } - - return nil -} diff --git a/format/s3m/s3m.go b/format/s3m/s3m.go index 5deba3a..b7fad94 100644 --- a/format/s3m/s3m.go +++ b/format/s3m/s3m.go @@ -4,15 +4,18 @@ package s3m import ( "io" - "github.com/gotracker/playback" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/load" - "github.com/gotracker/playback/format/s3m/settings" + 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 @@ -20,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 @@ -30,10 +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(settings.GetMachineSettings(true)) + machine.RegisterMachine(s3mSettings.GetMachineSettings(true)) } diff --git a/format/s3m/settings/machine.go b/format/s3m/settings/machine.go index 06ee2e0..63699a9 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -23,8 +23,9 @@ func GetMachineSettings(modLimits bool) *settings.MachineSettings[period.Amiga, var ( s3mQuirks = settings.MachineQuirks{ - PreviousPeriodUsesModifiedPeriod: true, - PortaToNoteUsesModifiedPeriod: true, + PreviousPeriodUsesModifiedPeriod: true, + PortaToNoteUsesModifiedPeriod: true, + DoNotProcessEffectsOnMutedChannels: true, } amigaMOD31Settings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ diff --git a/format/s3m/system/system.go b/format/s3m/system/system.go index b4288e5..4fea21e 100644 --- a/format/s3m/system/system.go +++ b/format/s3m/system/system.go @@ -8,8 +8,6 @@ import ( ) const ( - floatDefaultC4SampleRate = float32(DefaultC4SampleRate) - // 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 @@ -29,7 +27,7 @@ const ( C4SlideFines = C4Note * SlideFinesPerNote ) -var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} +var semitonePeriodTable = [...]uint16{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} var S3MSystem system.ClockableSystem = system.ClockedSystem{ MaxPastNotesPerChannel: 0, @@ -40,4 +38,5 @@ var S3MSystem system.ClockableSystem = system.ClockedSystem{ CommonPeriod: C4Period, CommonRate: DefaultC4SampleRate, SemitonePeriods: semitonePeriodTable, + OctaveShift: 1, } diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index 02adad9..ced0d29 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -108,7 +108,22 @@ func (v *s3mVoice) doDeferredRelease() { } } -func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], outputRate frequency.Frequency) error { +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) { @@ -145,7 +160,6 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.Fin if factory := inst.GetFilterFactory(); factory != nil { v.voiceFilter = factory(inst.SampleRate) - v.voiceFilter.SetPlaybackRate(outputRate) } else { v.voiceFilter = 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/period/util.go b/format/xm/period/util.go index e09214f..d17bbb6 100644 --- a/format/xm/period/util.go +++ b/format/xm/period/util.go @@ -6,52 +6,8 @@ import ( xmSystem "github.com/gotracker/playback/format/xm/system" "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/period" ) -// CalcSemitonePeriod calculates the semitone period for it notes -func CalcSemitonePeriod[TPeriod period.Period](semi note.Semitone, ft note.Finetune, c4SampleRate frequency.Frequency) TPeriod { - if semi == note.UnchangedSemitone { - panic("how?") - } - - var result TPeriod - switch p := any(&result).(type) { - case *period.Linear: - nft := int(semi)*xmSystem.SlideFinesPerNote + int(ft)/2 - p.Finetune = note.Finetune(nft) - case *period.Amiga: - stp, ok := xmSystem.XMSystem.GetSemitonePeriod(semi.Key()) - if !ok { - return result - } - - if c4SampleRate == 0 { - c4SampleRate = frequency.Frequency(xmSystem.DefaultC4SampleRate) - } - - per := max(float64(xmSystem.C4Note)*xmSystem.SlideFinesPerNote-float64(ft)/2, 0) - - exp := (per / xmSystem.SlideFinesPerOctave) - xmSystem.C4Octave + float64(semi.Octave()) - - pow := math.Pow(2.0, exp) - sampleRate := math.Floor(float64(c4SampleRate) * pow) - - if sampleRate == 0 { - return result - } - - const defaultC4SampleRate = float64(xmSystem.DefaultC4SampleRate) - - ratio := defaultC4SampleRate / float64(sampleRate) - - *p = period.Amiga(ratio * float64(stp)) - default: - } - - return result -} - // 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 { @@ -66,9 +22,3 @@ func CalcFinetuneC4SampleRate(c4SampleRate frequency.Frequency, st note.Semitone return frequency.Frequency(freq) } - -// FrequencyFromSemitone returns the frequency from the semitone (and c4 sample rate) -func FrequencyFromSemitone[TPeriod period.Period](converter period.PeriodConverter[TPeriod], semitone note.Semitone, c4SampleRate frequency.Frequency) float32 { - p := CalcSemitonePeriod[TPeriod](semitone, 0, c4SampleRate) - return float32(converter.GetFrequency(p)) -} diff --git a/format/xm/playback/playback.go b/format/xm/playback/playback.go deleted file mode 100644 index c9ab975..0000000 --- a/format/xm/playback/playback.go +++ /dev/null @@ -1,54 +0,0 @@ -package playback - -import ( - "errors" - - "github.com/gotracker/playback" - - "github.com/gotracker/playback/format/xm/layout" - "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player" - "github.com/gotracker/playback/player/feature" - "github.com/gotracker/playback/song" -) - -// manager is a playback manager for XM music -type manager[TPeriod period.Period] struct { - player.Tracker - song *layout.Song[TPeriod] -} - -var _ playback.Playback = (*manager[period.Linear])(nil) - -// NewManager creates a new manager for an XM song -func NewManager(songData song.Data) (playback.Playback, error) { - if songData == nil { - return nil, errors.New("song cannot be nil") - } - - switch s := songData.(type) { - case *layout.Song[period.Linear]: - m := manager[period.Linear]{ - song: s, - } - return &m, nil - - case *layout.Song[period.Amiga]: - m := manager[period.Amiga]{ - song: s, - } - return &m, nil - - default: - return nil, errors.New("unsupported xm song data") - } -} - -// Configure sets specified features -func (m *manager[TPeriod]) Configure(features []feature.Feature) error { - us, err := m.Tracker.Configure(features) - if err != nil { - return err - } - return m.SetupMachine(m.song, us) -} diff --git a/format/xm/system/system.go b/format/xm/system/system.go index 7f9606b..28a33f8 100644 --- a/format/xm/system/system.go +++ b/format/xm/system/system.go @@ -27,7 +27,7 @@ const ( C4SlideFines = C4Note * SlideFinesPerNote ) -var semitonePeriodTable = [...]float32{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} +var semitonePeriodTable = [...]uint16{27392, 25856, 24384, 23040, 21696, 20480, 19328, 18240, 17216, 16256, 15360, 14496} var XMSystem system.ClockableSystem = system.ClockedSystem{ MaxPastNotesPerChannel: 0, diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 623b4d7..43fce9e 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -130,7 +130,14 @@ func (v xmVoice[TPeriod]) getFadeoutEnabled() bool { return v.fadeoutMode.IsFadeoutActive(v.IsKeyFadeout(), v.volEnv.IsEnabled(), v.volEnv.IsDone()) } -func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning], outputRate frequency.Frequency) error { +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) { @@ -182,7 +189,6 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.X if factory := inst.GetFilterFactory(); factory != nil { v.voiceFilter = factory(inst.SampleRate) - v.voiceFilter.SetPlaybackRate(outputRate) } else { v.voiceFilter = nil } diff --git a/format/xm/xm.go b/format/xm/xm.go index ba5ba4b..19f16ce 100644 --- a/format/xm/xm.go +++ b/format/xm/xm.go @@ -4,16 +4,19 @@ package xm import ( "io" - "github.com/gotracker/playback" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/load" - "github.com/gotracker/playback/format/xm/settings" + 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 @@ -21,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 @@ -31,11 +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(settings.GetMachineSettings[period.Amiga]()) - machine.RegisterMachine(settings.GetMachineSettings[period.Linear]()) + machine.RegisterMachine(xmSettings.GetMachineSettings[period.Amiga]()) + machine.RegisterMachine(xmSettings.GetMachineSettings[period.Linear]()) } diff --git a/internal/examples/bufferload/example.go b/internal/examples/bufferload/example.go index 1372da9..240a931 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,35 +53,43 @@ 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) } - // Now that the player is configured, we can allocate a channel is filled with bundles of - // pre-mix data (i.e.: data which is ready to be converted into the final, mixed version). - // We want to use a buffered channel with a bit of room in it. It doesn't have to be huge, - // but the more space you supply, the more likely the player will not underflow the channel. + // 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. + out := sampler.NewSampler(sampleRate, channels) + if out == nil { + panic(errors.New("could not create sampler")) + } + + // The player's nearly ready to start playing! Now that the player is configured, we can allocate + // a channel is filled with bundles of pre-mix data (i.e.: data which is ready to be converted into + // the final, mixed version). We want to use a buffered channel with a bit of room in it. It doesn't + // have to be huge, but the more space you supply, the more likely the player will not underflow the + // channel. const premixChannelSize = 8 premixDataChannel := make(chan *output.PremixData, premixChannelSize) @@ -119,7 +130,8 @@ playerUpdateLoop: // 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 { + premix, err := player.Generate(out) + if 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) { @@ -128,6 +140,8 @@ playerUpdateLoop: // If we get here, then we don't know what the error is... panic(err) } + + premixDataChannel <- premix } // We're done! diff --git a/internal/examples/fileload/example.go b/internal/examples/fileload/example.go index 1bcf569..7464e09 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,27 +56,34 @@ 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) } + // 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. + out := sampler.NewSampler(sampleRate, channels) + if out == nil { + panic(errors.New("could not create sampler")) + } + // Now that the player is configured, we can allocate a channel is filled with bundles of // pre-mix data (i.e.: data which is ready to be converted into the final, mixed version). // We want to use a buffered channel with a bit of room in it. It doesn't have to be huge, @@ -118,7 +128,8 @@ playerUpdateLoop: // 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 { + premix, err := player.Generate(out) + if 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) { @@ -127,6 +138,8 @@ playerUpdateLoop: // If we get here, then we don't know what the error is... panic(err) } + + premixDataChannel <- premix } // We're done! diff --git a/note/note_test.go b/note/note_test.go index 8183ab8..6a21fcd 100644 --- a/note/note_test.go +++ b/note/note_test.go @@ -15,7 +15,7 @@ type testPeriod = period.Amiga 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) } } 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 index f91db34..b3e6714 100644 --- a/period/amiga.go +++ b/period/amiga.go @@ -10,39 +10,51 @@ import ( type Amiga uint16 // Add adds the current period to a delta value then returns the resulting period -func (p Amiga) Add(d Delta) Amiga { +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 } - p = Amiga(max(a-int(d), 1)) - 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) Amiga { - return p.Add(-amount) +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) Amiga { - return p.Add(amount) +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 Amiga) Amiga { +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) - if p.Compare(target) == comparison.SpaceshipRightGreater { - return target - } + p = p.PortaDown(amount, minPeriod, target, false) case comparison.SpaceshipRightGreater: // porta up to target - p = p.PortaUp(amount) - if p.Compare(target) == comparison.SpaceshipLeftGreater { - return target - } + p = p.PortaUp(amount, target, maxPeriod, false) } return p } diff --git a/period/amigaconverter.go b/period/amigaconverter.go index 0ae7929..2d5c312 100644 --- a/period/amigaconverter.go +++ b/period/amigaconverter.go @@ -9,9 +9,10 @@ import ( // 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 + System system.ClockableSystem + MinPeriod Amiga + MaxPeriod Amiga + SlideTo0Allowed bool } var _ PeriodConverter[Amiga] = (*AmigaConverter)(nil) @@ -25,7 +26,7 @@ func (c AmigaConverter) GetFrequency(p Amiga) frequency.Frequency { if p.IsInvalid() { return 0 } - return c.System.GetBaseClock() / frequency.Frequency(p) + return c.System.GetBaseClock() / frequency.Frequency(p<> octave + + return p.Clamp(c.MinPeriod, c.MaxPeriod) case note.SpecialTypeInvalid: fallthrough default: @@ -58,19 +64,19 @@ func (c AmigaConverter) GetPeriod(n note.Note) Amiga { } func (c AmigaConverter) PortaToNote(p Amiga, delta Delta, target Amiga) (Amiga, error) { - return c.clamp(p.PortaTo(delta, target)), nil + return p.PortaTo(delta, target, c.MinPeriod, c.MaxPeriod), nil } func (c AmigaConverter) PortaDown(p Amiga, delta Delta) (Amiga, error) { - return c.clamp(p.PortaDown(delta)), nil + return p.PortaDown(delta, c.MinPeriod, c.MaxPeriod, c.SlideTo0Allowed), nil } func (c AmigaConverter) PortaUp(p Amiga, delta Delta) (Amiga, error) { - return c.clamp(p.PortaUp(delta)), nil + return p.PortaUp(delta, c.MinPeriod, c.MaxPeriod, c.SlideTo0Allowed), nil } func (c AmigaConverter) AddDelta(p Amiga, delta Delta) (Amiga, error) { - return c.clamp(p.Add(delta)), nil + return p.Add(delta, c.MinPeriod, c.MaxPeriod, c.SlideTo0Allowed), nil } func (c AmigaConverter) clamp(p Amiga) Amiga { diff --git a/playback.go b/playback.go index 96f101b..c81ea75 100644 --- a/playback.go +++ b/playback.go @@ -12,9 +12,13 @@ type Playback interface { SetupSampler(samplesPerSecond int, channels int) error Configure([]feature.Feature) error + // runs the internal ticker and returns sample output Update(time.Duration, chan<- *output.PremixData) error Generate(time.Duration) (*output.PremixData, error) + // runs the internal ticker without caring about the sample output + Tick() error + GetNumOrders() int CanOrderLoop() bool 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/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index 1405e99..38cb6b7 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -1,13 +1,12 @@ package machine import ( - "github.com/gotracker/playback/frequency" "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], outputRate frequency.Frequency) error { +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 @@ -67,7 +66,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo case note.ActionRetrigger: c.cv.Release() - if err := c.doSetupInstrument(ch, m, outputRate); err != nil { + if err := c.doSetupInstrument(ch, m); err != nil { return err } @@ -104,7 +103,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo return nil } -func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSetupInstrument(ch index.Channel, m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error { +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 { @@ -112,12 +111,11 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe rc := &m.actualOutputs[ch] if factory := inst.GetPluginFilterFactory(); factory != nil { rc.PluginFilter = factory(inst.SampleRate) - rc.PluginFilter.SetPlaybackRate(outputRate) } else { rc.PluginFilter = nil } - if err := c.cv.Setup(inst, outputRate); err != nil { + if err := c.cv.Setup(inst); err != nil { return err } } diff --git a/player/machine/machine.go b/player/machine/machine.go index 36a999d..8a305c2 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -46,7 +46,8 @@ type MachineInfo interface { type MachineTicker interface { MachineInfo - Tick(s *sampler.Sampler) (*output.PremixData, error) + Generate(s *sampler.Sampler) (*output.PremixData, error) + Tick() error } type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { @@ -54,6 +55,7 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann ConvertToPeriod(n note.Note) TPeriod IgnoreUnknownEffect() bool + GetQuirks() *settings.MachineQuirks // Globals SetTempo(tempo int) error @@ -169,11 +171,15 @@ func NewMachine(songData song.Data, us settings.UserSettings) (MachineTicker, er return factory(songData, us) } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) ConvertToPeriod(n note.Note) TPeriod { +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 { +func (m machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IgnoreUnknownEffect() bool { return m.us.IgnoreUnknownEffect } @@ -231,7 +237,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) upda } c := &m.channels[ch] - if !c.enabled || d == nil { + if !c.enabled || d == nil || (m.ms.Quirks.DoNotProcessEffectsOnMutedChannels && c.cv.IsMuted()) { return true, nil } diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 81429c9..fd9f211 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -156,10 +156,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc.GlobalVolume = volume.Volume(1) c := &m.channels[ch] - c.enabled = true - if m.ms.Quirks.DoNotProcessEffectsOnMutedChannels && cs.IsMuted() { - c.enabled = false - } + c.enabled = cs.IsEnabled() c.cv = m.ms.VoiceFactory.NewVoice(voice.VoiceConfig[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]{ PC: ms.PeriodConverter, OPLChannel: cs.GetOPLChannel(), diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index 8e0367d..70be690 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -3,44 +3,42 @@ package machine import ( "errors" - "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/sampler" - "github.com/gotracker/playback/song" ) -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick(s *sampler.Sampler) (*output.PremixData, error) { +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Generate(s *sampler.Sampler) (*output.PremixData, error) { if m.opl2Enabled && m.opl2 == nil && m.ms.OPL2Enabled { if err := m.setupOPL2(s); err != nil { return nil, err } } + tickErr := m.Tick() + + premix, err := m.render(s) + return premix, errors.Join(tickErr, err) +} + +func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick() error { if err := m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { c := &m.channels[ch] - if err := c.DoNoteAction(ch, m, frequency.Frequency(s.SampleRate)); err != nil { + if err := c.DoNoteAction(ch, m); err != nil { return false, err } return true, nil }); err != nil { - return nil, err + return err } - premix, err := m.render(s) + err := runTick(&m.ticker, m) if err != nil { - return premix, err - } - - tickErr := runTick(&m.ticker, m) - if tickErr != nil { - if !errors.Is(tickErr, song.ErrStopSong) { - return nil, tickErr - } + return err } m.age++ - return premix, errors.Join(tickErr, err) + return nil } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) onTick() error { diff --git a/player/machine/settings/usersettings.go b/player/machine/settings/usersettings.go index e2e19ca..86f7bdd 100644 --- a/player/machine/settings/usersettings.go +++ b/player/machine/settings/usersettings.go @@ -7,7 +7,7 @@ import ( ) type UserSettings struct { - tracing.Tracer + Tracer tracing.TracerWithClose SongLoopCount int Start struct { Order optional.Value[index.Order] // default: based on song @@ -24,6 +24,42 @@ type UserSettings struct { 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 diff --git a/player/machine/ticker.go b/player/machine/ticker.go index 81cea89..4f224ae 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -200,10 +200,12 @@ func advanceRowOrder[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum 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)) @@ -231,7 +233,7 @@ orderScan: goto orderScan } - if orderUpdated && order != int(t.current.order) && t.settings.SongLoopCount >= 0 { + 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 { diff --git a/player/tracker.go b/player/tracker.go deleted file mode 100644 index 5e70f43..0000000 --- a/player/tracker.go +++ /dev/null @@ -1,165 +0,0 @@ -package player - -import ( - "errors" - "time" - - "github.com/gotracker/playback/frequency" - "github.com/gotracker/playback/index" - "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" - "github.com/gotracker/playback/tracing" -) - -// 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 { - M machine.MachineTicker - s *sampler.Sampler - - tracing.Tracing -} - -func (t *Tracker) SetupMachine(s song.Data, us settings.UserSettings) error { - var err error - t.M, err = machine.NewMachine(s, us) - return err -} - -func (t *Tracker) Close() { - t.Trace("Close") - t.Tracing.Close() -} - -// Update runs processing on the tracker, producing premixed sound data -func (t *Tracker) Update(deltaTime time.Duration, out chan<- *output.PremixData) error { - remaining := deltaTime - - var first time.Duration - firstSet := false - - for !firstSet || remaining < first { - premix, err := t.Generate(deltaTime) - if premix != nil && len(premix.Data) > 0 { - out <- premix - } - - if err != nil { - return err - } - - if premix == nil { - continue - } - - dur := time.Duration(premix.SamplesLen) * time.Second / time.Duration(t.s.SampleRate) - - if !firstSet { - firstSet = true - first = dur - } - - remaining -= dur - } - - return nil -} - -func (t *Tracker) Generate(deltaTime time.Duration) (*output.PremixData, error) { - if t.M == nil { - return nil, nil - } - - defer t.OutputTraces() - - return t.M.Tick(t.s) -} - -// GetSampleRate returns the sample rate of the sampler -func (t *Tracker) GetSampleRate() frequency.Frequency { - return frequency.Frequency(t.s.SampleRate) -} - -// SetupSampler configures the internal sampler -func (t *Tracker) SetupSampler(samplesPerSecond int, channels int) error { - t.s = sampler.NewSampler(samplesPerSecond, channels) - 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 -} - -func (t *Tracker) GetNumOrders() int { - if t.M == nil { - return 0 - } - - return t.M.GetNumOrders() -} - -func (t *Tracker) CanOrderLoop() bool { - if t.M == nil { - return false - } - - return t.M.CanOrderLoop() -} - -func (t *Tracker) GetName() string { - if t.M == nil { - return "" - } - - return t.M.GetName() -} - -// Configure sets specified features -func (t *Tracker) Configure(features []feature.Feature) (settings.UserSettings, error) { - us := settings.UserSettings{ - LongChannelOutput: true, - EnableNewNoteActions: true, - } - - 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 - case feature.EnableTracing: - if err := t.EnableTracing(f.Filename); err != nil { - return us, err - } - us.Tracer = &t.Tracing - } - } - return us, nil -} diff --git a/song/channelsettings.go b/song/channelsettings.go index 901a064..f6ef09d 100644 --- a/song/channelsettings.go +++ b/song/channelsettings.go @@ -8,6 +8,7 @@ import ( ) type ChannelSettings interface { + IsEnabled() bool IsMuted() bool GetOutputChannelNum() int GetMemory() ChannelMemory diff --git a/system/clockedsystem.go b/system/clockedsystem.go index aeba5d4..68d1ad6 100644 --- a/system/clockedsystem.go +++ b/system/clockedsystem.go @@ -8,11 +8,12 @@ import ( type ClockableSystem interface { System GetBaseClock() frequency.Frequency - GetCommonPeriod() float64 + GetCommonPeriod() uint16 GetBaseFinetunes() note.Finetune GetFinetunesPerOctave() note.Finetune GetFinetunesPerSemitone() note.Finetune - GetSemitonePeriod(note.Key) (float32, bool) + GetSemitonePeriod(note.Key) (uint16, bool) + GetOctaveShift() uint16 } type ClockedSystem struct { @@ -22,9 +23,10 @@ type ClockedSystem struct { BaseFinetunes note.Finetune FinetunesPerOctave note.Finetune FinetunesPerNote note.Finetune - CommonPeriod float64 + CommonPeriod uint16 CommonRate frequency.Frequency - SemitonePeriods [note.NumKeys]float32 + SemitonePeriods [note.NumKeys]uint16 + OctaveShift uint16 } var _ ClockableSystem = (*ClockedSystem)(nil) @@ -49,7 +51,7 @@ func (s ClockedSystem) GetFinetunesPerSemitone() note.Finetune { return s.FinetunesPerNote } -func (s ClockedSystem) GetCommonPeriod() float64 { +func (s ClockedSystem) GetCommonPeriod() uint16 { return s.CommonPeriod } @@ -57,9 +59,13 @@ func (s ClockedSystem) GetCommonRate() frequency.Frequency { return s.CommonRate } -func (s ClockedSystem) GetSemitonePeriod(k note.Key) (float32, bool) { +func (s ClockedSystem) GetSemitonePeriod(k note.Key) (uint16, bool) { if int(k) < note.NumKeys { - return s.SemitonePeriods[int(k)], true + return s.SemitonePeriods[int(k)] >> s.OctaveShift, true } return 0, false } + +func (s ClockedSystem) GetOctaveShift() uint16 { + return s.OctaveShift +} diff --git a/tracing/entry.go b/tracing/entry.go index 9c2af1f..0f34eda 100644 --- a/tracing/entry.go +++ b/tracing/entry.go @@ -36,7 +36,7 @@ func (e entry[TPrefix, TPayload]) Prefix() string { /////////////////////////////////////////////////////////// -func (t *Tracing) trace(tick Tick, op string) { +func (t *tracerFile) trace(tick Tick, op string) { t.traceWithComment(tick, op, "") } @@ -48,14 +48,14 @@ func (emptyPayload) String() string { var empty emptyPayload -func (t *Tracing) traceWithComment(tick Tick, op, comment string) { - if t.tracingFile == nil { +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 *Tracing, prefix TPrefix, op, comment string, payload TPayload) { +func traceWithPayload[TPrefix Ticker, TPayload fmt.Stringer](t *tracerFile, prefix TPrefix, op, comment string, payload TPayload) { e := entry[TPrefix, TPayload]{ prefix: prefix, operation: op, diff --git a/tracing/tickchannel.go b/tracing/tickchannel.go index 695b16c..7453d02 100644 --- a/tracing/tickchannel.go +++ b/tracing/tickchannel.go @@ -21,12 +21,12 @@ func (e tickChannel) GetTick() Tick { /////////////////////////////////////////////////////////// -func (t *Tracing) traceChannel(tick Tick, ch index.Channel, op string) { +func (t *tracerFile) traceChannel(tick Tick, ch index.Channel, op string) { t.traceChannelWithComment(tick, ch, op, "") } -func (t *Tracing) traceChannelWithComment(tick Tick, ch index.Channel, op string, comment string) { - if t.tracingFile == nil { +func (t *tracerFile) traceChannelWithComment(tick Tick, ch index.Channel, op string, comment string) { + if t.file == nil { return } tc := tickChannel{ diff --git a/tracing/tracer.go b/tracing/tracer.go index 3ade08a..4133a67 100644 --- a/tracing/tracer.go +++ b/tracing/tracer.go @@ -1,8 +1,14 @@ package tracing -import "github.com/gotracker/playback/index" +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) @@ -13,3 +19,20 @@ type Tracer interface { 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/tracing.go b/tracing/tracerfile.go similarity index 54% rename from tracing/tracing.go rename to tracing/tracerfile.go index f34887a..c9c4d63 100644 --- a/tracing/tracing.go +++ b/tracing/tracerfile.go @@ -10,12 +10,12 @@ import ( "github.com/gotracker/playback/index" ) -type Tracing struct { - tracingFile *os.File - chMap map[int]*tracingChannelState - traces []tracingMsgFunc - c chan func(w io.Writer) - wg sync.WaitGroup +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 @@ -35,28 +35,21 @@ type tracingChannelState struct { traces []tracingMsgFunc } -func (t *Tracing) EnableTracing(filename string) error { - var err error - t.tracingFile, err = os.Create(filename) - if err != nil { - return err - } - - return nil -} - -func (t *Tracing) Close() { +func (t *tracerFile) Close() error { if t.c != nil { close(t.c) } - if t.tracingFile != nil { - t.tracingFile.Close() + if t.file != nil { + if err := t.file.Close(); err != nil { + return err + } } t.wg.Wait() + return nil } -func (t *Tracing) OutputTraces() { - if t.tracingFile == nil { +func (t *tracerFile) OutputTraces() { + if t.file == nil { return } @@ -67,10 +60,10 @@ func (t *Tracing) OutputTraces() { updates, t.updates = t.updates, nil go func() { - logger := log.New(t.tracingFile, "", 0) + logger := log.New(t.file, "", 0) for _, u := range updates { if tick := u.GetTick(); !tick.Equals(t.prevTick) { - fmt.Fprintln(t.tracingFile) + fmt.Fprintln(t.file) t.prevTick = tick } @@ -79,7 +72,7 @@ func (t *Tracing) OutputTraces() { }() } -func (t *Tracing) SetTracingTick(order index.Order, row index.Row, tick int) { +func (t *tracerFile) SetTracingTick(order index.Order, row index.Row, tick int) { t.mu.Lock() t.tick = Tick{ Order: order, @@ -89,37 +82,37 @@ func (t *Tracing) SetTracingTick(order index.Order, row index.Row, tick int) { t.mu.Unlock() } -func (t *Tracing) GetTracingTick() Tick { +func (t *tracerFile) GetTracingTick() Tick { t.mu.RLock() defer t.mu.RUnlock() return t.tick } -func (t *Tracing) Trace(op string) { +func (t *tracerFile) Trace(op string) { t.TraceWithComment(op, "") } -func (t *Tracing) TraceWithComment(op, commentFmt string, commentParams ...any) { +func (t *tracerFile) TraceWithComment(op, commentFmt string, commentParams ...any) { traceWithPayload(t, t.GetTracingTick(), op, fmt.Sprintf(commentFmt, commentParams...), empty) } -func (t *Tracing) TraceValueChange(op string, prev, new any) { +func (t *tracerFile) TraceValueChange(op string, prev, new any) { t.TraceValueChangeWithComment(op, prev, new, "") } -func (t *Tracing) TraceValueChangeWithComment(op string, prev, new any, commentFmt string, commentParams ...any) { +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 *Tracing) TraceChannel(ch index.Channel, op string) { +func (t *tracerFile) TraceChannel(ch index.Channel, op string) { t.TraceChannelWithComment(ch, op, "") } -func (t *Tracing) TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) { +func (t *tracerFile) TraceChannelWithComment(ch index.Channel, op, commentFmt string, commentParams ...any) { tc := tickChannel{ tick: t.GetTracingTick(), ch: ch, @@ -127,11 +120,11 @@ func (t *Tracing) TraceChannelWithComment(ch index.Channel, op, commentFmt strin traceWithPayload(t, tc, op, fmt.Sprintf(commentFmt, commentParams...), empty) } -func (t *Tracing) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { +func (t *tracerFile) TraceChannelValueChange(ch index.Channel, op string, prev, new any) { t.TraceChannelValueChangeWithComment(ch, op, prev, new, "") } -func (t *Tracing) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) { +func (t *tracerFile) TraceChannelValueChangeWithComment(ch index.Channel, op string, prev, new any, commentFmt string, commentParams ...any) { tc := tickChannel{ tick: t.GetTracingTick(), ch: ch, diff --git a/tracing/valueupdate.go b/tracing/valueupdate.go index 06fe6f8..a029e2c 100644 --- a/tracing/valueupdate.go +++ b/tracing/valueupdate.go @@ -18,12 +18,12 @@ func (e valueUpdate) String() string { /////////////////////////////////////////////////////////// -func (t *Tracing) traceValueChange(tick Tick, op string, prev, new any) { +func (t *tracerFile) traceValueChange(tick Tick, op string, prev, new any) { t.traceValueChangeWithComment(tick, op, prev, new, "") } -func (t *Tracing) traceValueChangeWithComment(tick Tick, op string, prev, new any, comment string) { - if t.tracingFile == nil { +func (t *tracerFile) traceValueChangeWithComment(tick Tick, op string, prev, new any, comment string) { + if t.file == nil { return } if reflect.DeepEqual(prev, new) { @@ -35,12 +35,12 @@ func (t *Tracing) traceValueChangeWithComment(tick Tick, op string, prev, new an }) } -func (t *Tracing) traceChannelValueChange(tick Tick, ch index.Channel, op string, prev, new any) { +func (t *tracerFile) traceChannelValueChange(tick Tick, ch index.Channel, op string, prev, new any) { t.traceChannelValueChangeWithComment(tick, ch, op, prev, new, "") } -func (t *Tracing) traceChannelValueChangeWithComment(tick Tick, ch index.Channel, op string, prev, new any, comment string) { - if t.tracingFile == nil { +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) { diff --git a/voice/render.go b/voice/render.go index 93193f1..c7f1b02 100644 --- a/voice/render.go +++ b/voice/render.go @@ -34,6 +34,10 @@ func RenderAndTick[TPeriod Period](in Voice, pc period.PeriodConverter[TPeriod], 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{ diff --git a/voice/voice.go b/voice/voice.go index c303b35..a06eb8d 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -20,6 +20,7 @@ type Voice interface { // Configuration Reset() error + SetPlaybackRate(outputRate frequency.Frequency) error SetOPL2Chip(chip opl2.Chip) // Actions @@ -43,7 +44,8 @@ type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, T Voice // Configuration - Setup(inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning], outputRate frequency.Frequency) error + SetPlaybackRate(outputRate frequency.Frequency) error + Setup(inst *instrument.Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) error } type AmpModulator[TGlobalVolume, TMixingVolume, TVolume Volume] interface { From a1099878e5966c881b74dc2cbdb32e30331839b7 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 19 Jan 2024 09:51:38 -0800 Subject: [PATCH 54/63] cleanup and more formalization --- format/common/basesong.go | 193 ++++++++++++++++++++++++++ format/it/channel/data.go | 3 +- format/it/channel/memory.go | 35 +---- format/it/channel/sampleid.go | 11 +- format/it/layout/song.go | 189 ++----------------------- format/it/load/itformat.go | 24 ++-- format/it/settings/machine.go | 6 - format/s3m/channel/instid.go | 10 +- format/s3m/layout/song.go | 178 ++---------------------- format/s3m/layout/stringrow.go | 138 ++++++++++++++++++ format/s3m/load/s3mformat.go | 28 ++-- format/xm/channel/data.go | 4 +- format/xm/channel/memory.go | 27 +--- format/xm/channel/sampid.go | 11 +- format/xm/layout/song.go | 187 ++----------------------- format/xm/layout/stringrow.go | 143 +++++++++++++++++++ format/xm/load/xmformat.go | 62 +++++---- instrument/instrument.go | 16 +-- instrument/opl2.go | 4 - instrument/pcm.go | 4 - instrument/util.go | 13 +- player/machine/channel_noteaction.go | 2 +- player/machine/channel_notedecode.go | 15 +- player/machine/channel_patternloop.go | 2 +- player/machine/channel_tick.go | 2 +- player/machine/machine.go | 11 +- player/machine/machine_channel.go | 8 +- player/machine/machine_factory.go | 12 +- player/machine/machine_instruction.go | 2 +- player/machine/machine_render.go | 8 +- player/machine/position.go | 9 ++ player/machine/ticker.go | 52 ++++--- voice/pcm/bit16.go | 24 +++- voice/pcm/bit32float.go | 10 +- voice/pcm/bit64float.go | 10 +- voice/pcm/bit8.go | 16 ++- voice/pcm/converter.go | 1 + voice/pcm/sample_native.go | 19 +++ voice/pcm/sampledata.go | 16 +++ voice/pcm/samplereader.go | 6 +- 40 files changed, 779 insertions(+), 732 deletions(-) create mode 100644 format/common/basesong.go create mode 100644 format/s3m/layout/stringrow.go create mode 100644 format/xm/layout/stringrow.go create mode 100644 player/machine/position.go diff --git a/format/common/basesong.go b/format/common/basesong.go new file mode 100644 index 0000000..7e8780f --- /dev/null +++ b/format/common/basesong.go @@ -0,0 +1,193 @@ +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) +} + +// IsValidInstrumentID returns true if the instrument exists +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IsValidInstrumentID(instNum instrument.ID) bool { + if instNum.IsEmpty() { + return false + } + idx, _ := instNum.GetIndexAndSemitone() + return idx > 0 && idx <= len(s.Instruments) +} + +// GetInstrument returns the instrument interface indexed by `instNum` (0-based) +func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetInstrument(instID instrument.ID) (instrument.InstrumentIntf, note.Semitone) { + if instID.IsEmpty() { + return nil, note.UnchangedSemitone + } + idx, st := instID.GetIndexAndSemitone() + 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/it/channel/data.go b/format/it/channel/data.go index 321e5ba..d25fcf7 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -6,6 +6,7 @@ import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" "github.com/gotracker/gomixing/volume" + "github.com/heucuva/optional" "github.com/gotracker/playback" itNote "github.com/gotracker/playback/format/it/note" @@ -72,7 +73,7 @@ func (d Data[TPeriod]) GetInstrument(stmem note.Semitone) instrument.ID { } return SampleID{ InstID: d.Instrument, - Semitone: st, + Semitone: optional.NewValue(st), } } diff --git a/format/it/channel/memory.go b/format/it/channel/memory.go index 80afede..ec058db 100644 --- a/format/it/channel/memory.go +++ b/format/it/channel/memory.go @@ -1,10 +1,7 @@ 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" ) @@ -28,22 +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 - 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) @@ -140,26 +127,8 @@ 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() - } } // StartOrder is called when the first order's row at tick 0 is started diff --git a/format/it/channel/sampleid.go b/format/it/channel/sampleid.go index 87c2423..71767e2 100644 --- a/format/it/channel/sampleid.go +++ b/format/it/channel/sampleid.go @@ -4,12 +4,13 @@ import ( "fmt" "github.com/gotracker/playback/note" + "github.com/heucuva/optional" ) // SampleID is an InstrumentID that is a combination of InstID and SampID type SampleID struct { InstID uint8 - Semitone note.Semitone + Semitone optional.Value[note.Semitone] } // IsEmpty returns true if the sample ID is empty @@ -17,6 +18,14 @@ func (s SampleID) IsEmpty() bool { return s.InstID == 0 } +func (s SampleID) GetIndexAndSemitone() (int, note.Semitone) { + st := note.UnchangedSemitone + if ost, set := s.Semitone.Get(); set { + st = ost + } + return int(s.InstID) - 1, st +} + func (s SampleID) String() string { return fmt.Sprintf("%d(%v)", s.InstID, s.Semitone) } diff --git a/format/it/layout/song.go b/format/it/layout/song.go index 5ac62b5..5d5dcf0 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -1,140 +1,27 @@ package layout import ( - "reflect" - "time" - - "github.com/gotracker/gomixing/volume" - "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" - itPeriod "github.com/gotracker/playback/format/it/period" 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/period" - "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/system" ) // Song is the full definition of the song data of an IT file type Song[TPeriod period.Period] struct { - System system.System - MS *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] - Head Header - Instruments map[uint8]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + common.BaseSong[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] + InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument[TPeriod] - Patterns []song.Pattern ChannelSettings []ChannelSetting - OrderList []index.Pattern FilterPlugins map[int]filter.Factory } -func (s Song[TPeriod]) GetPeriodType() reflect.Type { - if s.Head.LinearFreqSlides { - var p period.Linear - return reflect.TypeOf(p) - } else { - var p period.Amiga - return reflect.TypeOf(p) - } -} - -func (Song[TPeriod]) GetGlobalVolumeType() reflect.Type { - var gv itVolume.FineVolume - return reflect.TypeOf(gv) -} - -func (Song[TPeriod]) GetChannelMixingVolumeType() reflect.Type { - var cmv itVolume.FineVolume - return reflect.TypeOf(cmv) -} - -func (Song[TPeriod]) GetChannelVolumeType() reflect.Type { - var cv itVolume.Volume - return reflect.TypeOf(cv) -} - -func (Song[TPeriod]) GetChannelPanningType() reflect.Type { - var cp itPanning.Panning - return reflect.TypeOf(cp) -} - -// GetOrderList returns the list of all pattern orders for the song -func (s Song[TPeriod]) GetOrderList() []index.Pattern { - return s.OrderList -} - -// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song -func (s Song[TPeriod]) GetInitialBPM() int { - return s.Head.InitialTempo -} - -// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song -func (s Song[TPeriod]) GetInitialTempo() int { - return s.Head.InitialSpeed -} - -// GetGlobalVolumeGeneric returns the initial global volume for the song -func (s Song[TPeriod]) GetGlobalVolumeGeneric() volume.Volume { - return s.Head.GlobalVolume.ToVolume() -} - -// GetGlobalVolume returns the initial global volume for the song -func (s Song[TPeriod]) GetGlobalVolume() itVolume.FineVolume { - return s.Head.GlobalVolume -} - -// GetMixingVolumeGeneric returns the initial mixing volume for the song -func (s Song[TPeriod]) GetMixingVolumeGeneric() volume.Volume { - return s.Head.MixingVolume.ToVolume() -} - -// GetMixingVolume returns the initial mixing volume for the song -func (s Song[TPeriod]) GetMixingVolume() itVolume.FineVolume { - return s.Head.MixingVolume -} - -const durationPerBpm = time.Duration(2500) * time.Millisecond - -// GetTickDuration calculates the duration of a tick at a particular BPM -func (s Song[TPeriod]) 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 Song[TPeriod]) GetPattern(patNum index.Pattern) (song.Pattern, error) { - if int(patNum) >= len(s.Patterns) { - return nil, song.ErrStopSong - } - return s.Patterns[patNum], nil -} - -// GetPatternIntfByOrder returns the pattern specified by the order index provided -func (s Song[TPeriod]) 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 Song[TPeriod]) GetNumChannels() int { return len(s.ChannelSettings) @@ -145,68 +32,20 @@ func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.Channel return s.ChannelSettings[channelNum] } -// NumInstruments returns the number of instruments in the song -func (s Song[TPeriod]) NumInstruments() int { - return len(s.Instruments) -} - -// IsValidInstrumentID returns true if the instrument exists -func (s Song[TPeriod]) 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 -} - -// GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song[TPeriod]) GetInstrument(instNum instrument.ID) (instrument.InstrumentIntf, 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.Inst, sm.NoteRemap - } - } - } - return nil, note.UnchangedSemitone -} - -// GetName returns the name of the song -func (s Song[TPeriod]) GetName() string { - return s.Head.Name -} - -func (s Song[TPeriod]) GetPeriodCalculator() song.PeriodCalculatorIntf { - if s.Head.LinearFreqSlides { - return itPeriod.LinearConverter - } - return itPeriod.AmigaConverter -} - -func (s Song[TPeriod]) GetInitialOrder() index.Order { - return s.Head.InitialOrder -} - 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) - copy(rowData, row.(Row[TPeriod])) + 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 } -func (s Song[TPeriod]) GetSystem() system.System { - return s.System -} - func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { for i, cs := range s.ChannelSettings { if enabledOnly { @@ -224,11 +63,3 @@ func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel } return nil } - -func (Song[TPeriod]) IsOPL2Enabled() bool { - return false -} - -func (s Song[TPeriod]) GetMachineSettings() any { - return s.MS -} diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index 7bb1a12..3c78610 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -12,6 +12,7 @@ import ( itblock "github.com/gotracker/goaudiofile/music/tracked/it/block" "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" @@ -112,13 +113,20 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] ms := settings.GetMachineSettings[TPeriod]() songData := &layout.Song[TPeriod]{ - System: itSystem.ITSystem, - MS: ms, - Head: *h, - Instruments: make(map[uint8]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]), + 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], f.Head.InstrumentCount), + Patterns: make([]song.Pattern, len(f.Patterns)), + OrderList: make([]index.Pattern, int(f.Head.OrderCount)), + }, InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument[TPeriod]), - Patterns: make([]song.Pattern, len(f.Patterns)), - OrderList: make([]index.Pattern, int(f.Head.OrderCount)), FilterPlugins: make(map[int]filter.Factory), } @@ -205,8 +213,6 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] Vol0OptEnabled: vol0Enabled, } - cs.Memory.ResetOscillators() - channels[chNum] = cs } @@ -243,7 +249,7 @@ func addSampleWithNoteMapToSong[TPeriod period.Period](song *layout.Song[TPeriod InstID: uint8(instNum + 1), } sample.Static.ID = id - song.Instruments[id.InstID] = sample + song.Instruments[instNum] = sample id, ok := sample.Static.ID.(channel.SampleID) if !ok { diff --git a/format/it/settings/machine.go b/format/it/settings/machine.go index 4074df7..03ed792 100644 --- a/format/it/settings/machine.go +++ b/format/it/settings/machine.go @@ -10,7 +10,6 @@ import ( "github.com/gotracker/playback/frequency" oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/machine" "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/voice/oscillator" ) @@ -73,8 +72,3 @@ func tremoloFactory() (oscillator.Oscillator, error) { func panbrelloFactory() (oscillator.Oscillator, error) { return oscillatorImpl.NewImpulseTrackerOscillator(1), nil } - -func init() { - machine.RegisterMachine(GetMachineSettings[period.Amiga]()) - machine.RegisterMachine(GetMachineSettings[period.Linear]()) -} diff --git a/format/s3m/channel/instid.go b/format/s3m/channel/instid.go index 9a9b079..5b3f904 100644 --- a/format/s3m/channel/instid.go +++ b/format/s3m/channel/instid.go @@ -1,6 +1,10 @@ package channel -import "fmt" +import ( + "fmt" + + "github.com/gotracker/playback/note" +) // InstID is an instrument ID in S3M world type InstID uint8 @@ -10,6 +14,10 @@ func (s InstID) IsEmpty() bool { return s == 0 } +func (s InstID) GetIndexAndSemitone() (int, note.Semitone) { + return int(s) - 1, note.UnchangedSemitone +} + func (s InstID) String() string { return fmt.Sprint(uint8(s)) } diff --git a/format/s3m/layout/song.go b/format/s3m/layout/song.go index ab9b4cf..a09278c 100644 --- a/format/s3m/layout/song.go +++ b/format/s3m/layout/song.go @@ -1,129 +1,22 @@ package layout import ( - "reflect" - "time" - - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/s3m/channel" 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/instrument" - "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/system" ) -// Song is the full definition of the song data of an Song file type Song struct { - System system.System - MS *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] - Head Header - Instruments []*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] - Patterns []song.Pattern + common.BaseSong[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] + ChannelSettings []ChannelSetting ChannelOrders []index.Channel NumChannels int - OrderList []index.Pattern -} - -func (Song) GetPeriodType() reflect.Type { - var p period.Amiga - return reflect.TypeOf(p) -} - -func (s Song) GetGlobalVolumeType() reflect.Type { - return reflect.TypeOf(s.Head.GlobalVolume) -} - -func (s Song) GetChannelMixingVolumeType() reflect.Type { - return reflect.TypeOf(s.Head.MixingVolume) -} - -func (s Song) GetChannelVolumeType() reflect.Type { - var cs ChannelSetting - return reflect.TypeOf(cs.InitialVolume) -} - -func (s Song) GetChannelPanningType() reflect.Type { - var cs ChannelSetting - return reflect.TypeOf(cs.InitialPanning) -} - -// GetOrderList returns the list of all pattern orders for the song -func (s Song) GetOrderList() []index.Pattern { - return s.OrderList -} - -// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song -func (s Song) GetInitialBPM() int { - return s.Head.InitialTempo -} - -// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song -func (s Song) GetInitialTempo() int { - return s.Head.InitialSpeed -} - -// GetGlobalVolumeGeneric returns the initial global volume for the song -func (s Song) GetGlobalVolumeGeneric() volume.Volume { - return s.Head.GlobalVolume.ToVolume() -} - -// GetGlobalVolume returns the initial global volume for the song -func (s Song) GetGlobalVolume() s3mVolume.Volume { - return s.Head.GlobalVolume -} - -// GetMixingVolumeGeneric returns the initial mixing volume for the song -func (s Song) GetMixingVolumeGeneric() volume.Volume { - return s.Head.MixingVolume.ToVolume() -} - -// GetMixingVolume returns the initial mixing volume for the song -func (s Song) GetMixingVolume() s3mVolume.FineVolume { - return s.Head.MixingVolume -} - -const durationPerBpm = time.Duration(2500) * time.Millisecond - -// GetTickDuration calculates the duration of a tick at a particular BPM -func (s Song) 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 Song) 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 Song) 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 @@ -136,62 +29,17 @@ func (s Song) GetChannelSettings(channelNum index.Channel) song.ChannelSettings return s.ChannelSettings[channelNum] } -// NumInstruments returns the number of instruments in the song -func (s Song) NumInstruments() int { - return len(s.Instruments) -} - -// 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) - } - return false -} - -// GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song) GetInstrument(instID instrument.ID) (instrument.InstrumentIntf, 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 -} - -func (s Song) GetPeriodCalculator() song.PeriodCalculatorIntf { - return s3mPeriod.S3MAmigaConverter -} - -func (s Song) GetInitialOrder() index.Order { - return s.Head.InitialOrder -} - 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) - pr := row.(Row) - nprch := min(len(pr), nch) - for i := 0; i < nprch; i++ { - if !s.ChannelSettings[i].Enabled || s.ChannelSettings[i].Muted { - continue + _ = 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, pr[i]) - } + rowData = append(rowData, d.(channel.Data)) + return true, nil + }) for len(rowData) < nch { rowData = append(rowData, channel.Data{}) } @@ -199,10 +47,6 @@ func (s Song) GetRowRenderStringer(row song.Row, channels int, longFormat bool) return rt } -func (s Song) GetSystem() system.System { - return s.System -} - func (s Song) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { for _, ch := range s.ChannelOrders { cs := &s.ChannelSettings[ch] @@ -234,7 +78,3 @@ func (s Song) IsOPL2Enabled() bool { } return false } - -func (s Song) GetMachineSettings() any { - return s.MS -} diff --git a/format/s3m/layout/stringrow.go b/format/s3m/layout/stringrow.go new file mode 100644 index 0000000..348ca79 --- /dev/null +++ b/format/s3m/layout/stringrow.go @@ -0,0 +1,138 @@ +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 == "|" + }) + for ch, cstr := range cstrPieces { + d, err := r.decodeChannel(strings.TrimSuffix(cstr, "|")) + if err != nil { + return err + } + cont, err := fn(index.Channel(ch), d) + if err != nil { + return err + } + + if !cont { + break + } + } + return nil +} + +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 = channel.InstID(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/s3mformat.go b/format/s3m/load/s3mformat.go index c9634c9..c7892a0 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -9,6 +9,7 @@ import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" "github.com/gotracker/gomixing/volume" + "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" @@ -57,6 +58,9 @@ func scrsNoneToInstrument(scrs *s3mfile.SCRSFull, si *s3mfile.SCRSNoneHeader) (* }, SampleRate: frequency.Frequency(si.C2Spd.Lo), } + if sample.Static.Volume.IsInvalid() { + sample.Static.Volume = s3mVolume.MaxVolume + } return &sample, nil } @@ -260,24 +264,32 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, amigaLimits := (f.Head.Flags&0x0010) != 0 || wasModFile s := layout.Song{ - System: s3mSystem.S3MSystem, - MS: settings.GetMachineSettings(amigaLimits), - Head: *h, - Instruments: make([]*instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning], len(f.InstrumentPointers)), - OrderList: make([]index.Pattern, len(f.OrderList)), + 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 { - f.Head.GlobalVolume = s3mfile.Volume(s3mVolume.MaxVolume) + s.GlobalVolume = s3mVolume.MaxVolume } if f.Head.InitialSpeed == 0 || f.Head.InitialSpeed == 255 { - s.Head.InitialSpeed = 6 + s.InitialTempo = 6 } if f.Head.InitialTempo < 33 { - s.Head.InitialTempo = 125 + s.InitialBPM = 125 } signedSamples := f.Head.FileFormatInformation == 1 diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index 167b0c0..12cab92 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -6,6 +6,7 @@ import ( xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" "github.com/gotracker/gomixing/volume" + "github.com/heucuva/optional" "github.com/gotracker/playback" xmNote "github.com/gotracker/playback/format/xm/note" @@ -70,9 +71,10 @@ func (d Data[TPeriod]) GetInstrument(stmem note.Semitone) instrument.ID { st = note.Semitone(nn) } } + return SampleID{ InstID: d.Instrument, - Semitone: st, + Semitone: optional.NewValue[note.Semitone](st), } } diff --git a/format/xm/channel/memory.go b/format/xm/channel/memory.go index f163d3a..3b05b62 100644 --- a/format/xm/channel/memory.go +++ b/format/xm/channel/memory.go @@ -1,10 +1,7 @@ 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" ) @@ -29,19 +26,11 @@ type Memory struct { extraFinePortaUp memory.Value[DataEffect] extraFinePortaDown memory.Value[DataEffect] - tremorMem tremor.Tremor - vibratoOscillator oscillator.Oscillator - tremoloOscillator oscillator.Oscillator + 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) @@ -137,21 +126,7 @@ 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() - } } // StartOrder is called when the first order's row at tick 0 is started diff --git a/format/xm/channel/sampid.go b/format/xm/channel/sampid.go index b529a24..8a449d6 100644 --- a/format/xm/channel/sampid.go +++ b/format/xm/channel/sampid.go @@ -4,12 +4,13 @@ import ( "fmt" "github.com/gotracker/playback/note" + "github.com/heucuva/optional" ) // SampleID is an InstrumentID that is a combination of InstID and SampID type SampleID struct { InstID uint8 - Semitone note.Semitone + Semitone optional.Value[note.Semitone] } // IsEmpty returns true if the sample ID is empty @@ -17,6 +18,14 @@ func (s SampleID) IsEmpty() bool { return s.InstID == 0 } +func (s SampleID) GetIndexAndSemitone() (int, note.Semitone) { + st := note.UnchangedSemitone + if ost, set := s.Semitone.Get(); set { + st = ost + } + return int(s.InstID) - 1, st +} + func (s SampleID) String() string { return fmt.Sprint(s.InstID) } diff --git a/format/xm/layout/song.go b/format/xm/layout/song.go index 18ccde7..d34350b 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -1,135 +1,24 @@ package layout import ( - "reflect" - "time" - - "github.com/gotracker/gomixing/volume" + "github.com/gotracker/playback/format/common" "github.com/gotracker/playback/format/xm/channel" 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/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/player/machine/settings" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/system" ) // Song is the full definition of the song data of an XM file type Song[TPeriod period.Period] struct { - System system.System - MS *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] - Head Header - Instruments map[uint8]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + common.BaseSong[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] - Patterns []song.Pattern ChannelSettings []ChannelSetting - OrderList []index.Pattern -} - -func (s Song[TPeriod]) GetPeriodType() reflect.Type { - if s.Head.LinearFreqSlides { - var p period.Linear - return reflect.TypeOf(p) - } else { - var p period.Amiga - return reflect.TypeOf(p) - } -} - -func (Song[TPeriod]) GetGlobalVolumeType() reflect.Type { - var gv xmVolume.XmVolume - return reflect.TypeOf(gv) -} - -func (Song[TPeriod]) GetChannelMixingVolumeType() reflect.Type { - var cmv xmVolume.XmVolume - return reflect.TypeOf(cmv) -} - -func (Song[TPeriod]) GetChannelVolumeType() reflect.Type { - var cv xmVolume.XmVolume - return reflect.TypeOf(cv) -} - -func (Song[TPeriod]) GetChannelPanningType() reflect.Type { - var cp xmPanning.Panning - return reflect.TypeOf(cp) -} - -// GetOrderList returns the list of all pattern orders for the song -func (s Song[TPeriod]) GetOrderList() []index.Pattern { - return s.OrderList -} - -// GetInitialBPM returns the initial "tempo" (number of beats per minute) for the song -func (s Song[TPeriod]) GetInitialBPM() int { - return s.Head.InitialTempo -} - -// GetInitialTempo returns the initial "speed" (number of ticks per row) for the song -func (s Song[TPeriod]) GetInitialTempo() int { - return s.Head.InitialSpeed -} - -// GetGlobalVolumeGeneric returns the initial global volume for the song -func (s Song[TPeriod]) GetGlobalVolumeGeneric() volume.Volume { - return s.Head.GlobalVolume.ToVolume() -} - -// GetGlobalVolume returns the initial global volume for the song -func (s Song[TPeriod]) GetGlobalVolume() xmVolume.XmVolume { - return s.Head.GlobalVolume -} - -// GetMixingVolumeGeneric returns the initial mixing volume for the song -func (s Song[TPeriod]) GetMixingVolumeGeneric() volume.Volume { - return s.Head.MixingVolume.ToVolume() -} - -// GetMixingVolume returns the initial mixing volume for the song -func (s Song[TPeriod]) GetMixingVolume() xmVolume.XmVolume { - return s.Head.MixingVolume -} - -const durationPerBpm = time.Duration(2500) * time.Millisecond - -// GetTickDuration calculates the duration of a tick at a particular BPM -func (s Song[TPeriod]) 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 Song[TPeriod]) 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 Song[TPeriod]) 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 @@ -142,68 +31,20 @@ func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.Channel return s.ChannelSettings[channelNum] } -// NumInstruments returns the number of instruments in the song -func (s Song[TPeriod]) NumInstruments() int { - return len(s.Instruments) -} - -// IsValidInstrumentID returns true if the instrument exists -func (s Song[TPeriod]) 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 -} - -// GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song[TPeriod]) GetInstrument(instNum instrument.ID) (instrument.InstrumentIntf, 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 - } - } - } - return nil, note.UnchangedSemitone -} - -// GetName returns the name of the song -func (s Song[TPeriod]) GetName() string { - return s.Head.Name -} - -func (s Song[TPeriod]) GetPeriodCalculator() song.PeriodCalculatorIntf { - if s.Head.LinearFreqSlides { - return xmPeriod.LinearConverter - } - return xmPeriod.AmigaConverter -} - -func (s Song[TPeriod]) GetInitialOrder() index.Order { - return s.Head.InitialOrder -} - 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) - copy(rowData, row.(Row[TPeriod])) + 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 + } + rowData[ch] = d.(channel.Data[TPeriod]) + return true, nil + }) rt.Channels = rowData return rt } -func (s Song[TPeriod]) GetSystem() system.System { - return s.System -} - func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel) (bool, error)) error { for i, cs := range s.ChannelSettings { if enabledOnly { @@ -221,11 +62,3 @@ func (s Song[TPeriod]) ForEachChannel(enabledOnly bool, fn func(ch index.Channel } return nil } - -func (Song[TPeriod]) IsOPL2Enabled() bool { - return false -} - -func (s Song[TPeriod]) GetMachineSettings() any { - return s.MS -} diff --git a/format/xm/layout/stringrow.go b/format/xm/layout/stringrow.go new file mode 100644 index 0000000..2fcd625 --- /dev/null +++ b/format/xm/layout/stringrow.go @@ -0,0 +1,143 @@ +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 == "|" + }) + for ch, cstr := range cstrPieces { + d, err := r.decodeChannel(strings.TrimSuffix(cstr, "|")) + if err != nil { + return err + } + cont, err := fn(index.Channel(ch), d) + if err != nil { + return err + } + + if !cont { + break + } + } + return nil +} + +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 == "===" { + 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.Atoi(vol) + 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'; e < 36 { + d.Effect = channel.Command(e) + } + d.EffectParameter = channel.DataEffect(i) + } + + return d, nil +} diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index bb3d3e5..60cffc7 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -9,11 +9,12 @@ import ( "github.com/gotracker/gomixing/volume" "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" - "github.com/gotracker/playback/format/xm/settings" + 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" @@ -31,11 +32,11 @@ import ( "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{ + head := xmLayout.Header{ Name: fh.GetName(), InitialSpeed: int(fh.DefaultSpeed), InitialTempo: int(fh.DefaultTempo), @@ -262,17 +263,17 @@ func convertXmPattern[TPeriod period.Period](pkt xmfile.Pattern) (song.Pattern, maxCh := uint8(0) for rowNum, drow := range pkt.Data { - row := make(layout.Row[TPeriod], len(drow)) + row := make(xmLayout.Row[TPeriod], len(drow)) pat[rowNum] = row for channelNum, chn := range drow { - cd := channel.Data[TPeriod]{ + 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[channelNum] = cd if maxCh < uint8(channelNum) { @@ -292,7 +293,7 @@ func convertXmFileToSong(f *xmfile.File, features []feature.Feature) (song.Data, } } -func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features []feature.Feature) (*layout.Song[TPeriod], error) { +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 @@ -300,16 +301,23 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] linearFrequencySlides := f.Head.Flags.IsLinearSlides() - ms := settings.GetMachineSettings[TPeriod]() - - s := layout.Song[TPeriod]{ - System: xmSystem.XMSystem, - MS: ms, - Head: *h, - Instruments: make(map[uint8]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), + 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]map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), - Patterns: make([]song.Pattern, len(f.Patterns)), - OrderList: make([]index.Pattern, int(f.Head.SongLength)), } for i := 0; i < int(f.Head.SongLength); i++ { @@ -325,15 +333,15 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] if sample == nil { continue } - id := channel.SampleID{ + id := xmChannel.SampleID{ InstID: uint8(instNum + 1), } sample.Static.ID = id - s.Instruments[id.InstID] = sample + s.Instruments[instNum] = sample } for i, sts := range noteMap { sample := samples[i] - id, ok := sample.Static.ID.(channel.SampleID) + id, ok := sample.Static.ID.(xmChannel.SampleID) if !ok { continue } @@ -360,25 +368,23 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] 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, Muted: false, InitialVolume: xmVolume.DefaultXmVolume, InitialPanning: xmPanning.DefaultPanning, - Memory: channel.Memory{ + Memory: xmChannel.Memory{ Shared: &sharedMem, }, } - cs.Memory.ResetOscillators() - channels[chNum] = cs } diff --git a/instrument/instrument.go b/instrument/instrument.go index dc8956b..dd64617 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -22,7 +22,6 @@ type InstrumentIntf interface { GetFinetune() note.Finetune GetID() ID GetSemitoneShift() int8 - GetKind() Kind GetNewNoteAction() note.Action GetData() Data GetFilterFactory() filter.Factory @@ -111,11 +110,6 @@ func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetSemitoneShi return inst.Static.RelativeNoteNumber } -// GetKind returns the kind of the instrument -func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetKind() Kind { - return inst.Inst.GetKind() -} - // GetNewNoteAction returns the NewNoteAction associated to the instrument func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetNewNoteAction() note.Action { return inst.Static.NewNoteAction @@ -140,7 +134,10 @@ func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetPluginFilte 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) } @@ -149,7 +146,10 @@ func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) IsReleaseNote( 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 0709686..00b2f68 100644 --- a/instrument/pcm.go +++ b/instrument/pcm.go @@ -33,10 +33,6 @@ type PitchPan struct { Separation float32 } -func (PCM[TMixingVolume, TVolume, TPanning]) GetKind() Kind { - return KindPCM -} - 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..ee4088e 100644 --- a/instrument/util.go +++ b/instrument/util.go @@ -4,26 +4,17 @@ import ( "fmt" "github.com/gotracker/gomixing/sampling" + "github.com/gotracker/playback/note" ) // ID is an identifier for an instrument/sample that means something to the format type ID interface { IsEmpty() bool + GetIndexAndSemitone() (int, note.Semitone) 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/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index 38cb6b7..57c627f 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -13,7 +13,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoNo return nil } - if na.Tick != m.ticker.current.tick { + if na.Tick != m.ticker.current.Tick { // not time yet return nil } diff --git a/player/machine/channel_notedecode.go b/player/machine/channel_notedecode.go index b271a4d..d8706e9 100644 --- a/player/machine/channel_notedecode.go +++ b/player/machine/channel_notedecode.go @@ -113,15 +113,14 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco changeNote.Vol.Set(inst.GetDefaultVolume()) changeNote.NewNoteAction.Set(inst.GetNewNoteAction()) - switch inst.GetKind() { - case instrument.KindPCM: - if d, ok := inst.GetData().(*instrument.PCM[TMixingVolume, TVolume, TPanning]); ok { - if pan, set := d.Panning.Get(); set { - changeNote.Pan.Set(pan) - } + switch d := inst.GetData().(type) { + case *instrument.PCM[TMixingVolume, TVolume, TPanning]: + if pan, set := d.Panning.Get(); set { + changeNote.Pan.Set(pan) } - case instrument.KindOPL2: - // TODO + + case *instrument.OPL2: + // TODO - is there anything to do? } return nil } diff --git a/player/machine/channel_patternloop.go b/player/machine/channel_patternloop.go index 7723874..fd70b77 100644 --- a/player/machine/channel_patternloop.go +++ b/player/machine/channel_patternloop.go @@ -13,7 +13,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doPa return nil } - if m.ticker.current.row != c.patternLoop.End { + if m.ticker.current.Row != c.patternLoop.End { return nil } diff --git a/player/machine/channel_tick.go b/player/machine/channel_tick.go index 1c8bc74..0aaaa2b 100644 --- a/player/machine/channel_tick.go +++ b/player/machine/channel_tick.go @@ -11,7 +11,7 @@ import ( ) 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 { + if m.ticker.current.Order == 0 { c.memory.StartOrder0() } c.resetPatternLoop() diff --git a/player/machine/machine.go b/player/machine/machine.go index 8a305c2..ad4560c 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -69,6 +69,7 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann SetOrder(o index.Order) error SetRow(r index.Row, breakOrder bool) error SetFilterOnAllChannelsByFilterName(name string, enabled bool) error + GetPosition() Position // Single Row AddExtraTicks(ticks int) error @@ -171,6 +172,10 @@ func NewMachine(songData song.Data, us settings.UserSettings) (MachineTicker, er 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 } @@ -208,15 +213,15 @@ type dataInstructionGenerator[TPeriod Period, TGlobalVolume, TMixingVolume, TVol } func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) getRowData() (song.Row, error) { - pat, err := m.songData.GetPatternByOrder(m.ticker.current.order) + 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() { + if pat == nil || int(m.ticker.current.Row) >= pat.NumRows() { return nil, song.ErrStopSong } - row := pat.GetRow(m.ticker.current.row) + row := pat.GetRow(m.ticker.current.Row) if row == nil { return nil, song.ErrStopSong } diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index d82a4a6..3af3da7 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -294,8 +294,8 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetC 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 + traceChannelValueChangeWithComment(m, ch, "patternLoopStart", c.patternLoop.Start, m.ticker.current.Row, "SetPatternLoopStart") + c.patternLoop.Start = m.ticker.current.Row return nil }) } @@ -308,8 +308,8 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetP 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, "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 diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index fd9f211..416455d 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -1,7 +1,6 @@ package machine import ( - "errors" "fmt" "reflect" @@ -46,13 +45,20 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum 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 = songData.GetMachineSettings().(*settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) + m.ms, ok = sms.(*settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) if !ok { - return nil, errors.New("invalid machine settings from songdata") + return nil, fmt.Errorf("invalid machine settings from songdata: %T - expected %s", sms, tl.String()) } m.songData = songData diff --git a/player/machine/machine_instruction.go b/player/machine/machine_instruction.go index 828e562..0c63ca7 100644 --- a/player/machine/machine_instruction.go +++ b/player/machine/machine_instruction.go @@ -41,7 +41,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) DoIn if !ok { return nil } - return ii.Tick(ch, m, m.ticker.current.tick) + return ii.Tick(ch, m, m.ticker.current.Tick) } type instructionRowEnd[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { diff --git a/player/machine/machine_render.go b/player/machine/machine_render.go index e9f31ec..596e12b 100644 --- a/player/machine/machine_render.go +++ b/player/machine/machine_render.go @@ -21,9 +21,9 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend } renderRow := render.RowRender{ - Order: int(m.ticker.current.order), - Row: int(m.ticker.current.row), - Tick: m.ticker.current.tick, + Order: int(m.ticker.current.Order), + Row: int(m.ticker.current.Row), + Tick: m.ticker.current.Tick, } premix := output.PremixData{ @@ -32,7 +32,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) rend Userdata: &renderRow, } - if m.ticker.current.tick == 0 { + if m.ticker.current.Tick == 0 { // make a copy so it doesn't get stomped renderRow.RowText = m.rowStringer } 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/ticker.go b/player/machine/ticker.go index 4f224ae..39b35eb 100644 --- a/player/machine/ticker.go +++ b/player/machine/ticker.go @@ -10,12 +10,8 @@ import ( type ticker struct { settings tickerSettings - current struct { - tick int - row index.Row - order index.Order - } - next struct { + current Position + next struct { row optional.Value[tickerRowBreak] order optional.Value[index.Order] } @@ -41,9 +37,9 @@ type tickerSettings struct { 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.current.Tick = 0 + t.current.Row = 0 + t.current.Order = 0 t.next.row.Set(tickerRowBreak{ row: settings.InitialRow, breakOrder: false, @@ -56,19 +52,19 @@ func initTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPan } if row, set := nextRow.Get(); set { - t.current.row = row + t.current.Row = row } if order, set := nextOrder.Get(); set { - t.current.order = order + t.current.Order = order } if t.songLoop.detect == nil { t.songLoop.detect = make(map[index.Order]struct{}) } - t.songLoop.detect[t.current.order] = struct{}{} + t.songLoop.detect[t.current.Order] = struct{}{} - m.us.SetTracingTick(t.current.order, t.current.row, t.current.tick) + m.us.SetTracingTick(t.current.Order, t.current.Row, t.current.Tick) if err := m.onOrderStart(); err != nil { return err @@ -82,9 +78,9 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann return err } - tick := t.current.tick + 1 - row := t.current.row - order := t.current.order + tick := t.current.Tick + 1 + row := t.current.Row + order := t.current.Order rowAdvanced := false orderAdvanced := false done := false @@ -121,13 +117,13 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann } } - 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) + 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() @@ -135,12 +131,12 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann if oset || rset { orderMatch := true if oset { - orderMatch = (o == t.current.order) + orderMatch = (o == t.current.Order) } rowMatch := true if rset { - rowMatch = (r == t.current.row) + rowMatch = (r == t.current.Row) } done = orderMatch && rowMatch @@ -165,9 +161,9 @@ func runTick[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann } 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) + row := int(t.current.Row) rowUpdated := false - order := int(t.current.order) + order := int(t.current.Order) orderUpdated := false desiredRow, rowSet := t.next.row.Get() @@ -233,7 +229,7 @@ orderScan: goto orderScan } - if orderUpdated && (forceLoopDetect || order != int(t.current.order)) && t.settings.SongLoopCount >= 0 { + 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 { diff --git a/voice/pcm/bit16.go b/voice/pcm/bit16.go index fe2dde0..56214d7 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 SampleDataFormat16BitLESigned + } else { + return SampleDataFormat16BitBESigned + } +} + // 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 SampleDataFormat16BitLEUnsigned + } else { + return SampleDataFormat16BitBEUnsigned + } +} diff --git a/voice/pcm/bit32float.go b/voice/pcm/bit32float.go index 0cd88ac..301dafb 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 SampleDataFormat32BitLEFloat + } else { + return SampleDataFormat32BitBEFloat + } +} diff --git a/voice/pcm/bit64float.go b/voice/pcm/bit64float.go index 4e0c6fb..6065b48 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 SampleDataFormat64BitLEFloat + } else { + return SampleDataFormat64BitBEFloat + } +} 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/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..a40b2ea 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,6 +54,10 @@ 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{ @@ -213,3 +220,12 @@ 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) + } + + return NewSample(d, len(d), 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) +} From e95294079da4db6c1eeacc05ec7285aef2e23365 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 19 Jan 2024 17:48:03 -0800 Subject: [PATCH 55/63] reduce complexity of xm instrument mapping --- format/common/basesong.go | 3 +++ format/xm/layout/song.go | 24 +++++++++++++++++++++++- format/xm/layout/stringrow.go | 6 +++--- format/xm/load/xmformat.go | 16 +++++----------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/format/common/basesong.go b/format/common/basesong.go index 7e8780f..fc56f40 100644 --- a/format/common/basesong.go +++ b/format/common/basesong.go @@ -156,6 +156,9 @@ func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetI return nil, note.UnchangedSemitone } idx, st := instID.GetIndexAndSemitone() + if idx >= len(s.Instruments) { + return nil, st + } return s.Instruments[idx], st } diff --git a/format/xm/layout/song.go b/format/xm/layout/song.go index d34350b..891deeb 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -17,10 +17,12 @@ import ( type Song[TPeriod period.Period] struct { common.BaseSong[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] - InstrumentNoteMap map[uint8]map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] + InstrumentNoteMap map[uint8]SemitoneSamples ChannelSettings []ChannelSetting } +type SemitoneSamples [96]int // semitone -> instrument index + // GetNumChannels returns the number of channels the song has func (s Song[TPeriod]) GetNumChannels() int { return len(s.ChannelSettings) @@ -31,6 +33,26 @@ func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.Channel return s.ChannelSettings[channelNum] } +// GetInstrument returns the instrument interface indexed by `instNum` (0-based) +func (s Song[TPeriod]) GetInstrument(instID instrument.ID) (instrument.InstrumentIntf, note.Semitone) { + if instID.IsEmpty() { + return nil, note.UnchangedSemitone + } + + idx, st := instID.GetIndexAndSemitone() + + i := idx + if inm, ok := s.InstrumentNoteMap[uint8(idx)]; ok { + i = inm[st] + } + + if i < 0 || i >= len(s.Instruments) { + return nil, st + } + + return s.Instruments[i], 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) diff --git a/format/xm/layout/stringrow.go b/format/xm/layout/stringrow.go index 2fcd625..77fc7c4 100644 --- a/format/xm/layout/stringrow.go +++ b/format/xm/layout/stringrow.go @@ -57,7 +57,7 @@ func (StringRow[TPeriod]) decodeChannel(cstr string) (channel.Data[TPeriod], err d.Note = 0 // note - if note == "===" { + if note == "===" || note == "== " { d.What |= xmfile.ChannelFlagHasNote d.Note = 97 } else if note != "..." { @@ -113,7 +113,7 @@ func (StringRow[TPeriod]) decodeChannel(cstr string) (channel.Data[TPeriod], err // vol if vol != ".." { - v, err := strconv.Atoi(vol) + v, err := strconv.ParseUint(vol, 16, 8) if err != nil { return d, err } @@ -133,7 +133,7 @@ func (StringRow[TPeriod]) decodeChannel(cstr string) (channel.Data[TPeriod], err d.What |= xmfile.ChannelFlagHasEffect | xmfile.ChannelFlagHasEffectParameter if e := c - '0'; e <= 9 { d.Effect = channel.Command(e) - } else if e := c - 'A'; e < 36 { + } else if e := c - 'A' + 10; e < 36 { d.Effect = channel.Command(e) } d.EffectParameter = channel.DataEffect(i) diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index 60cffc7..c9c6424 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -317,7 +317,7 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] Patterns: make([]song.Pattern, len(f.Patterns)), OrderList: make([]index.Pattern, f.Head.SongLength), }, - InstrumentNoteMap: make(map[uint8]map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]), + InstrumentNoteMap: make(map[uint8]xmLayout.SemitoneSamples), } for i := 0; i < int(f.Head.SongLength); i++ { @@ -341,18 +341,12 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] } for i, sts := range noteMap { sample := samples[i] - id, ok := sample.Static.ID.(xmChannel.SampleID) - if !ok { - continue - } - inm, ok := s.InstrumentNoteMap[id.InstID] - if !ok { - inm = make(map[note.Semitone]*instrument.Instrument[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]) - s.InstrumentNoteMap[id.InstID] = inm - } + inm, _ := s.InstrumentNoteMap[uint8(instNum)] + idx, _ := sample.Static.ID.GetIndexAndSemitone() for _, st := range sts { - inm[st] = samples[i] + inm[st] = idx } + s.InstrumentNoteMap[uint8(instNum)] = inm } } From 422589eea3be81c7689a82eeefae6fc717ed3519 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 20 Jan 2024 14:03:35 -0800 Subject: [PATCH 56/63] simplify and clean up, fix pcm format detector --- filter/filter.go | 5 +++ filter/it_resonantfilter.go | 7 ++++ format/it/filter/factory.go | 39 +++++++++++++++++ format/it/layout/channelsetting.go | 5 ++- format/it/layout/song.go | 2 +- format/it/load/instrument.go | 34 ++++++++------- format/it/load/itformat.go | 15 ++++--- format/it/oscillator/factory.go | 37 ++++++++++++++++ format/it/settings/machine.go | 49 +++++----------------- format/it/voice/voice.go | 17 +++++--- format/s3m/channel/effect_enablefilter.go | 2 +- format/s3m/filter/factory.go | 24 +++++++++++ format/s3m/layout/channelsetting.go | 25 +++++------ format/s3m/layout/stringrow.go | 13 ++---- format/s3m/load/s3mformat.go | 3 +- format/s3m/oscillator/factory.go | 35 ++++++++++++++++ format/s3m/settings/machine.go | 49 +++++----------------- format/s3m/voice/voice.go | 10 +++-- format/s3m/volume/finevolume.go | 4 +- format/s3m/volume/volume.go | 14 ++++--- format/xm/filter/factory.go | 24 +++++++++++ format/xm/layout/channelsetting.go | 5 ++- format/xm/layout/stringrow.go | 14 +++---- format/xm/load/xmformat.go | 14 +++++-- format/xm/oscillator/factory.go | 35 ++++++++++++++++ format/xm/settings/machine.go | 49 +++++----------------- format/xm/voice/voice.go | 17 +++++--- instrument/instrument.go | 20 ++++----- instrument/pcm.go | 10 +---- player/machine/channel_noteaction.go | 9 ++-- player/machine/machine.go | 2 +- player/machine/machine_factory.go | 9 +--- player/machine/machine_globals.go | 14 +++---- player/machine/settings/machinesettings.go | 2 +- song/channelsettings.go | 3 +- voice/autovibrato/config.go | 34 +++++++++++++++ voice/autovibrato/settings.go | 24 ++--------- voice/component/modulator_autovibrato.go | 4 +- voice/pcm/bit16.go | 8 ++-- voice/pcm/bit32float.go | 4 +- voice/pcm/bit64float.go | 4 +- voice/pcm/format.go | 25 +++++++++++ voice/pcm/sampledata.go | 6 ++- voice/pitchpan/pitchpan.go | 9 ++++ 44 files changed, 462 insertions(+), 272 deletions(-) create mode 100644 format/it/filter/factory.go create mode 100644 format/it/oscillator/factory.go create mode 100644 format/s3m/filter/factory.go create mode 100644 format/s3m/oscillator/factory.go create mode 100644 format/xm/filter/factory.go create mode 100644 format/xm/oscillator/factory.go create mode 100644 voice/autovibrato/config.go create mode 100644 voice/pitchpan/pitchpan.go diff --git a/filter/filter.go b/filter/filter.go index f0cee65..5ef5a56 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -5,6 +5,11 @@ import ( "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 diff --git a/filter/it_resonantfilter.go b/filter/it_resonantfilter.go index e18cef8..a04e67e 100644 --- a/filter/it_resonantfilter.go +++ b/filter/it_resonantfilter.go @@ -33,6 +33,13 @@ type ResonantFilter struct { playbackRate frequency.Frequency } +type ITResonantFilterParams struct { + Cutoff uint8 + Resonance uint8 + ExtendedFilterRange bool + Highpass bool +} + // 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{ 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/layout/channelsetting.go b/format/it/layout/channelsetting.go index a2aa64a..162e09e 100644 --- a/format/it/layout/channelsetting.go +++ b/format/it/layout/channelsetting.go @@ -1,6 +1,7 @@ package layout import ( + "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" @@ -59,8 +60,8 @@ func (c ChannelSetting) IsPanEnabled() bool { return c.PanEnabled } -func (c ChannelSetting) GetDefaultFilterName() string { - return "" +func (c ChannelSetting) GetDefaultFilterInfo() filter.Info { + return filter.Info{} } func (c ChannelSetting) IsDefaultFilterEnabled() bool { diff --git a/format/it/layout/song.go b/format/it/layout/song.go index 5d5dcf0..fe4979e 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -19,7 +19,7 @@ type Song[TPeriod period.Period] struct { InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument[TPeriod] ChannelSettings []ChannelSetting - FilterPlugins map[int]filter.Factory + FilterPlugins map[int]filter.Info } // GetNumChannels returns the number of channels the song has diff --git a/format/it/load/instrument.go b/format/it/load/instrument.go index 151c217..0950a9a 100644 --- a/format/it/load/instrument.go +++ b/format/it/load/instrument.go @@ -17,8 +17,8 @@ import ( "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" @@ -140,7 +140,7 @@ func convertITInstrumentOldToInstrument[TPeriod period.Period](inst *itfile.IMPI return outInsts, nil } -func convertITInstrumentToInstrument[TPeriod period.Period](inst *itfile.IMPIInstrument, pc period.PeriodConverter[TPeriod], sampData []itfile.FullSample, convSettings convertITInstrumentSettings, pluginFilters map[int]filter.Factory, features []feature.Feature) (map[int]*convInst[TPeriod], error) { +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 { @@ -148,18 +148,22 @@ func convertITInstrumentToInstrument[TPeriod period.Period](inst *itfile.IMPIIns } var ( - channelFilterFactory filter.Factory - pluginFilterFactory filter.Factory + voiceFilter filter.Info + pluginFilter filter.Info ) if inst.InitialFilterResonance != 0 { - channelFilterFactory = func(instrument frequency.Frequency) filter.Filter { - return filter.NewITResonantFilter(inst.InitialFilterCutoff, inst.InitialFilterResonance, 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 } } @@ -169,7 +173,7 @@ func convertITInstrumentToInstrument[TPeriod period.Period](inst *itfile.IMPIIns Mode: fadeout.ModeAlwaysActive, Amount: volume.Volume(inst.Fadeout) / 1024, }, - PitchPan: instrument.PitchPan{ + PitchPan: pitchpan.PitchPan{ Enabled: inst.PitchPanSeparation != 0, Center: note.Semitone(inst.PitchPanCenter), Separation: float32(inst.PitchPanSeparation) / 8, @@ -178,9 +182,9 @@ func convertITInstrumentToInstrument[TPeriod period.Period](inst *itfile.IMPIIns ii := instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ Static: instrument.StaticValues[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ - PC: pc, - FilterFactory: channelFilterFactory, - PluginFilter: pluginFilterFactory, + PC: pc, + VoiceFilter: voiceFilter, + PluginFilter: pluginFilter, }, Inst: &id, } @@ -465,15 +469,13 @@ func addSampleInfoToConvertedInstrument[TPeriod period.Period](ii *instrument.In ii.Static.Filename = si.Header.GetFilename() ii.Static.Name = si.Header.GetName() ii.SampleRate = frequency.Frequency(si.Header.C5Speed) - ii.Static.AutoVibrato = autovibrato.AutoVibratoSettings[TPeriod]{ + 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) diff --git a/format/it/load/itformat.go b/format/it/load/itformat.go index 3c78610..b04c6cc 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -127,7 +127,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] OrderList: make([]index.Pattern, int(f.Head.OrderCount)), }, InstrumentNoteMap: make(map[uint8]map[note.Semitone]layout.NoteInstrument[TPeriod]), - FilterPlugins: make(map[int]filter.Factory), + FilterPlugins: make(map[int]filter.Info), } for _, block := range f.Blocks { @@ -220,7 +220,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] 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 { @@ -228,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 + } + echo := filter.Info{ + Name: "echo", + Params: e.EchoFilterSettings, } - return e.Factory(), nil + 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) } } 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/settings/machine.go b/format/it/settings/machine.go index 03ed792..f1b4d80 100644 --- a/format/it/settings/machine.go +++ b/format/it/settings/machine.go @@ -1,17 +1,13 @@ package settings import ( - "fmt" - - "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" itPeriod "github.com/gotracker/playback/format/it/period" itVolume "github.com/gotracker/playback/format/it/volume" - "github.com/gotracker/playback/frequency" - oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" - "github.com/gotracker/playback/voice/oscillator" ) func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeriod, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning] { @@ -29,46 +25,21 @@ func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeri var ( amigaMachine = settings.MachineSettings[period.Amiga, itVolume.FineVolume, itVolume.FineVolume, itVolume.Volume, itPanning.Panning]{ PeriodConverter: itPeriod.AmigaConverter, - GetFilterFactory: filterFactory, - GetVibratoFactory: vibratoFactory, - GetTremoloFactory: tremoloFactory, - GetPanbrelloFactory: panbrelloFactory, + 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: filterFactory, - GetVibratoFactory: vibratoFactory, - GetTremoloFactory: tremoloFactory, - GetPanbrelloFactory: panbrelloFactory, + GetFilterFactory: itFilter.Factory, + GetVibratoFactory: itOscillator.VibratoFactory, + GetTremoloFactory: itOscillator.TremoloFactory, + GetPanbrelloFactory: itOscillator.PanbrelloFactory, VoiceFactory: linearVoiceFactory, OPL2Enabled: false, } ) - -func filterFactory(name string) (settings.FilterFactoryFunc, error) { - switch name { - case "amigalpf": - return func(instrument frequency.Frequency) (filter.Filter, error) { - lpf := filter.NewAmigaLPF(instrument) - return lpf, nil - }, nil - - default: - return nil, fmt.Errorf("unsupported filter: %q", name) - } -} - -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 -} diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index c7e7932..067e057 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -7,12 +7,15 @@ import ( "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" "github.com/gotracker/playback/voice/opl2" @@ -230,13 +233,17 @@ func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, itVolume.F return errors.New("instrument is nil") } - v.autoVibrato.Setup(inst.Static.AutoVibrato) + v.autoVibrato.Setup(autovibrato.AutoVibratoSettings[TPeriod]{ + AutoVibratoConfig: inst.Static.AutoVibrato, + Factory: itOscillator.OscillatorFactory, + }) - if factory := inst.GetFilterFactory(); factory != nil { - v.voiceFilter = factory(inst.SampleRate) - } else { - v.voiceFilter = nil + 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 diff --git a/format/s3m/channel/effect_enablefilter.go b/format/s3m/channel/effect_enablefilter.go index c7bffc1..54a9e38 100644 --- a/format/s3m/channel/effect_enablefilter.go +++ b/format/s3m/channel/effect_enablefilter.go @@ -23,7 +23,7 @@ func (e EnableFilter) Tick(ch index.Channel, m machine.Machine[period.Amiga, s3m } x := DataEffect(e) & 0xf - return m.SetFilterOnAllChannelsByFilterName("amigalpf", x != 0) + return m.SetFilterOnAllChannelsByFilterName("amigalpf", x != 0, nil) } func (e EnableFilter) TraceData() string { 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 9db9690..5f1833c 100644 --- a/format/s3m/layout/channelsetting.go +++ b/format/s3m/layout/channelsetting.go @@ -2,6 +2,7 @@ package layout import ( s3mfile "github.com/gotracker/goaudiofile/music/tracked/s3m" + "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" @@ -12,15 +13,15 @@ import ( // ChannelSetting is settings specific to a single channel type ChannelSetting struct { - Enabled bool - Muted bool - OutputChannelNum int - Category s3mfile.ChannelCategory - InitialVolume s3mVolume.Volume - PanEnabled bool - InitialPanning s3mPanning.Panning - Memory channel.Memory - DefaultFilterName string + Enabled bool + Muted bool + OutputChannelNum int + Category s3mfile.ChannelCategory + InitialVolume s3mVolume.Volume + PanEnabled bool + InitialPanning s3mPanning.Panning + Memory channel.Memory + DefaultFilter filter.Info } var _ song.ChannelSettings = (*ChannelSetting)(nil) @@ -60,12 +61,12 @@ func (c ChannelSetting) IsPanEnabled() bool { return c.PanEnabled } -func (c ChannelSetting) GetDefaultFilterName() string { - return c.DefaultFilterName +func (c ChannelSetting) GetDefaultFilterInfo() filter.Info { + return c.DefaultFilter } func (c ChannelSetting) IsDefaultFilterEnabled() bool { - return len(c.DefaultFilterName) > 0 + return len(c.DefaultFilter.Name) > 0 } func (c ChannelSetting) GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings { diff --git a/format/s3m/layout/stringrow.go b/format/s3m/layout/stringrow.go index 348ca79..880a617 100644 --- a/format/s3m/layout/stringrow.go +++ b/format/s3m/layout/stringrow.go @@ -25,21 +25,16 @@ func (r StringRow) ForEach(fn func(ch index.Channel, d song.ChannelData[s3mVolum 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 } - cont, err := fn(index.Channel(ch), d) - if err != nil { - return err - } - - if !cont { - break - } + row[ch] = d } - return nil + + return row.ForEach(fn) } var channelRegex = regexp.MustCompile(`^(...) +(..) +(..) +(...)$`) diff --git a/format/s3m/load/s3mformat.go b/format/s3m/load/s3mformat.go index c7892a0..21b4a83 100644 --- a/format/s3m/load/s3mformat.go +++ b/format/s3m/load/s3mformat.go @@ -362,11 +362,10 @@ func convertS3MFileToSong(f *s3mfile.File, getPatternLen func(patNum int) uint8, Memory: channel.Memory{ Shared: &sharedMem, }, - DefaultFilterName: "", } if sbFilterEnable { - cs.DefaultFilterName = "amigalpf" + cs.DefaultFilter.Name = "amigalpf" } pf := f.Panning[chNum] 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/settings/machine.go b/format/s3m/settings/machine.go index 63699a9..b61cc9b 100644 --- a/format/s3m/settings/machine.go +++ b/format/s3m/settings/machine.go @@ -1,17 +1,13 @@ package settings import ( - "fmt" - - "github.com/gotracker/playback/filter" + 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/frequency" - oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" - "github.com/gotracker/playback/voice/oscillator" ) func GetMachineSettings(modLimits bool) *settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] { @@ -30,10 +26,10 @@ var ( amigaMOD31Settings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ PeriodConverter: s3mPeriod.S3MAmigaConverter, - GetFilterFactory: filterFactory, - GetVibratoFactory: vibratoFactory, - GetTremoloFactory: tremoloFactory, - GetPanbrelloFactory: panbrelloFactory, + GetFilterFactory: s3mFilter.Factory, + GetVibratoFactory: s3mOscillator.VibratoFactory, + GetTremoloFactory: s3mOscillator.TremoloFactory, + GetPanbrelloFactory: s3mOscillator.PanbrelloFactory, VoiceFactory: amigaVoiceFactory, OPL2Enabled: true, Quirks: s3mQuirks, @@ -41,37 +37,12 @@ var ( amigaS3MSettings = settings.MachineSettings[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]{ PeriodConverter: s3mPeriod.S3MAmigaConverter, - GetFilterFactory: filterFactory, - GetVibratoFactory: vibratoFactory, - GetTremoloFactory: tremoloFactory, - GetPanbrelloFactory: panbrelloFactory, + GetFilterFactory: s3mFilter.Factory, + GetVibratoFactory: s3mOscillator.VibratoFactory, + GetTremoloFactory: s3mOscillator.TremoloFactory, + GetPanbrelloFactory: s3mOscillator.PanbrelloFactory, VoiceFactory: amigaVoiceFactory, OPL2Enabled: true, Quirks: s3mQuirks, } ) - -func filterFactory(name string) (settings.FilterFactoryFunc, error) { - switch name { - case "amigalpf": - return func(instrument frequency.Frequency) (filter.Filter, error) { - lpf := filter.NewAmigaLPF(instrument) - return lpf, nil - }, nil - - default: - return nil, fmt.Errorf("unsupported filter: %q", name) - } -} - -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 -} diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index ced0d29..c06f543 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -5,6 +5,7 @@ import ( "fmt" "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" @@ -158,11 +159,12 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.Fin return errors.New("instrument is nil") } - if factory := inst.GetFilterFactory(); factory != nil { - v.voiceFilter = factory(inst.SampleRate) - } else { - v.voiceFilter = 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 diff --git a/format/s3m/volume/finevolume.go b/format/s3m/volume/finevolume.go index 6bed4eb..ba15d50 100644 --- a/format/s3m/volume/finevolume.go +++ b/format/s3m/volume/finevolume.go @@ -19,7 +19,7 @@ var ( _ types.VolumeDeltaer[FineVolume] = FineVolume(0) ) -const finevolCoeff = volume.Volume(1) / volume.Volume(0x80) +const finevolCoeff = volume.Volume(1) / volume.Volume(MaxFineVolume) func (v FineVolume) ToVolume() volume.Volume { if v != FineVolume(s3mfile.EmptyVolume) { @@ -29,7 +29,7 @@ func (v FineVolume) ToVolume() volume.Volume { } func (v FineVolume) IsInvalid() bool { - return v > 0x7f && v != FineVolume(s3mfile.EmptyVolume) + return v > MaxFineVolume && v != FineVolume(s3mfile.EmptyVolume) } func (v FineVolume) IsUseInstrumentVol() bool { diff --git a/format/s3m/volume/volume.go b/format/s3m/volume/volume.go index 5e77c79..f2d0ba5 100644 --- a/format/s3m/volume/volume.go +++ b/format/s3m/volume/volume.go @@ -13,8 +13,10 @@ const ( ) var ( + DefaultS3MVolume = Volume(s3mfile.DefaultVolume) + // DefaultVolume is the default volume value for most everything in S3M format - DefaultVolume = VolumeFromS3M(Volume(s3mfile.DefaultVolume)) + DefaultVolume = VolumeFromS3M(DefaultS3MVolume) ) type Volume s3mfile.Volume @@ -24,7 +26,7 @@ var ( _ types.VolumeDeltaer[Volume] = Volume(0) ) -const volCoeff = volume.Volume(1) / volume.Volume(64) +const volCoeff = volume.Volume(1) / volume.Volume(MaxVolume) func (v Volume) ToVolume() volume.Volume { if v != Volume(s3mfile.EmptyVolume) { @@ -34,7 +36,7 @@ func (v Volume) ToVolume() volume.Volume { } func (v Volume) IsInvalid() bool { - return v > 64 && v != Volume(s3mfile.EmptyVolume) + return v > MaxVolume && v != Volume(s3mfile.EmptyVolume) } func (v Volume) IsUseInstrumentVol() bool { @@ -64,9 +66,9 @@ func VolumeFromS3M(vol Volume) volume.Volume { 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 } @@ -79,7 +81,7 @@ func VolumeToS3M(v volume.Volume) Volume { case v == volume.VolumeUseInstVol: return Volume(s3mfile.EmptyVolume) default: - return Volume(v * 64.0) + return Volume(v * volume.Volume(MaxVolume)) } } 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 85c5928..c15f79a 100644 --- a/format/xm/layout/channelsetting.go +++ b/format/xm/layout/channelsetting.go @@ -1,6 +1,7 @@ package layout import ( + "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" @@ -53,8 +54,8 @@ func (c ChannelSetting) IsPanEnabled() bool { return true } -func (c ChannelSetting) GetDefaultFilterName() string { - return "" +func (c ChannelSetting) GetDefaultFilterInfo() filter.Info { + return filter.Info{} } func (c ChannelSetting) IsDefaultFilterEnabled() bool { diff --git a/format/xm/layout/stringrow.go b/format/xm/layout/stringrow.go index 77fc7c4..9210947 100644 --- a/format/xm/layout/stringrow.go +++ b/format/xm/layout/stringrow.go @@ -26,21 +26,17 @@ func (r StringRow[TPeriod]) ForEach(fn func(ch index.Channel, d song.ChannelData 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 } - cont, err := fn(index.Channel(ch), d) - if err != nil { - return err - } - - if !cont { - break - } + row[ch] = d } - return nil + + return row.ForEach(fn) } var channelRegex = regexp.MustCompile(`^(...) +(..) +(..) +(...)$`) diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index c9c6424..425f0d2 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -79,13 +79,13 @@ func xmInstrumentToInstrument[TPeriod period.Period](inst *xmfile.InstrumentHead Name: inst.GetName(), Volume: v, RelativeNoteNumber: si.RelativeNoteNumber, - AutoVibrato: autovibrato.AutoVibratoSettings[TPeriod]{ + 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", }, }, SampleRate: frequency.Frequency(0), // uses si.Finetune, below @@ -343,10 +343,18 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] sample := samples[i] inm, _ := s.InstrumentNoteMap[uint8(instNum)] idx, _ := sample.Static.ID.GetIndexAndSemitone() + + var remapped bool for _, st := range sts { 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 } - s.InstrumentNoteMap[uint8(instNum)] = inm } } 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/settings/machine.go b/format/xm/settings/machine.go index 6618ffd..e14f873 100644 --- a/format/xm/settings/machine.go +++ b/format/xm/settings/machine.go @@ -1,17 +1,13 @@ package settings import ( - "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" xmPeriod "github.com/gotracker/playback/format/xm/period" xmVolume "github.com/gotracker/playback/format/xm/volume" - "github.com/gotracker/playback/frequency" - oscillatorImpl "github.com/gotracker/playback/oscillator" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/settings" - "github.com/gotracker/playback/voice/oscillator" ) func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning] { @@ -29,46 +25,21 @@ func GetMachineSettings[TPeriod period.Period]() *settings.MachineSettings[TPeri var ( amigaMachine = settings.MachineSettings[period.Amiga, xmVolume.XmVolume, xmVolume.XmVolume, xmVolume.XmVolume, xmPanning.Panning]{ PeriodConverter: xmPeriod.AmigaConverter, - GetFilterFactory: filterFactory, - GetVibratoFactory: vibratoFactory, - GetTremoloFactory: tremoloFactory, - GetPanbrelloFactory: panbrelloFactory, + 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: filterFactory, - GetVibratoFactory: vibratoFactory, - GetTremoloFactory: tremoloFactory, - GetPanbrelloFactory: panbrelloFactory, + GetFilterFactory: xmFilter.Factory, + GetVibratoFactory: xmOscillator.VibratoFactory, + GetTremoloFactory: xmOscillator.TremoloFactory, + GetPanbrelloFactory: xmOscillator.PanbrelloFactory, VoiceFactory: linearVoiceFactory, OPL2Enabled: false, } ) - -func filterFactory(name string) (settings.FilterFactoryFunc, error) { - switch name { - case "amigalpf": - return func(instrument frequency.Frequency) (filter.Filter, error) { - lpf := filter.NewAmigaLPF(instrument) - return lpf, nil - }, nil - - default: - return nil, fmt.Errorf("unsupported filter: %q", name) - } -} - -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 -} diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 43fce9e..20272e5 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -5,12 +5,15 @@ import ( "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" "github.com/gotracker/playback/voice/opl2" @@ -185,13 +188,17 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.X return errors.New("instrument is nil") } - v.autoVibrato.Setup(inst.Static.AutoVibrato) + v.autoVibrato.Setup(autovibrato.AutoVibratoSettings[TPeriod]{ + AutoVibratoConfig: inst.Static.AutoVibrato, + Factory: xmOscillator.Factory, + }) - if factory := inst.GetFilterFactory(); factory != nil { - v.voiceFilter = factory(inst.SampleRate) - } else { - v.voiceFilter = nil + 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 diff --git a/instrument/instrument.go b/instrument/instrument.go index dd64617..0e2775f 100644 --- a/instrument/instrument.go +++ b/instrument/instrument.go @@ -24,8 +24,8 @@ type InstrumentIntf interface { GetSemitoneShift() int8 GetNewNoteAction() note.Action GetData() Data - GetFilterFactory() filter.Factory - GetPluginFilterFactory() filter.Factory + GetVoiceFilterInfo() filter.Info + GetPluginFilterInfo() filter.Info IsReleaseNote(n note.Note) bool IsStopNote(n note.Note) bool GetDefaultVolumeGeneric() volume.Volume @@ -41,11 +41,11 @@ type StaticValues[TPeriod types.Period, TMixingVolume, TVolume types.Volume, TPa MixingVolume optional.Value[TMixingVolume] Panning optional.Value[TPanning] RelativeNoteNumber int8 - AutoVibrato autovibrato.AutoVibratoSettings[TPeriod] + 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 @@ -120,13 +120,13 @@ func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) GetData() Data return inst.Inst } -// GetFilterFactory returns the factory for the channel filter -func (inst Instrument[TPeriod, TMixingVolume, TVolume, TPanning]) 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[TPeriod, TMixingVolume, TVolume, TPanning]) 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 } diff --git a/instrument/pcm.go b/instrument/pcm.go index 00b2f68..49bcf1f 100644 --- a/instrument/pcm.go +++ b/instrument/pcm.go @@ -2,11 +2,11 @@ package instrument import ( "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/playback/note" "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" ) @@ -19,7 +19,7 @@ type PCM[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { Panning optional.Value[TPanning] MixingVolume optional.Value[TMixingVolume] FadeOut fadeout.Settings - PitchPan PitchPan + PitchPan pitchpan.PitchPan VolEnv envelope.Envelope[TVolume] VolEnvFinishFadesOut bool PanEnv envelope.Envelope[TPanning] @@ -27,12 +27,6 @@ type PCM[TMixingVolume, TVolume types.Volume, TPanning types.Panning] struct { PitchFiltEnv envelope.Envelope[types.PitchFiltValue] // this is either pitch or filter } -type PitchPan struct { - Enabled bool - Center note.Semitone - Separation float32 -} - func (p PCM[TMixingVolume, TVolume, TPanning]) GetLength() sampling.Pos { return sampling.Pos{Pos: p.Sample.Length()} } diff --git a/player/machine/channel_noteaction.go b/player/machine/channel_noteaction.go index 57c627f..5cf2159 100644 --- a/player/machine/channel_noteaction.go +++ b/player/machine/channel_noteaction.go @@ -109,10 +109,11 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) doSe if inst != nil { if prevInst != inst { rc := &m.actualOutputs[ch] - if factory := inst.GetPluginFilterFactory(); factory != nil { - rc.PluginFilter = factory(inst.SampleRate) - } else { - rc.PluginFilter = nil + 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 { diff --git a/player/machine/machine.go b/player/machine/machine.go index ad4560c..991a5d5 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -68,7 +68,7 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann SetSampleVolume(v volume.Volume) error SetOrder(o index.Order) error SetRow(r index.Row, breakOrder bool) error - SetFilterOnAllChannelsByFilterName(name string, enabled bool) error + SetFilterOnAllChannelsByFilterName(name string, enabled bool, params any) error GetPosition() Position // Single Row diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 416455d..52557bb 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -127,13 +127,8 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc.OutputFilter = nil if cs.IsDefaultFilterEnabled() { - name := cs.GetDefaultFilterName() - filtFactory, err := ms.GetFilterFactory(name) - if err != nil { - return nil, err - } - - filt, err := filtFactory(sys.GetCommonRate()) + info := cs.GetDefaultFilterInfo() + filt, err := ms.GetFilterFactory(info.Name, sys.GetCommonRate(), info.Params) if err != nil { return nil, err } diff --git a/player/machine/machine_globals.go b/player/machine/machine_globals.go index cc22c18..33107d6 100644 --- a/player/machine/machine_globals.go +++ b/player/machine/machine_globals.go @@ -141,18 +141,16 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetR return nil } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetFilterOnAllChannelsByFilterName(name string, enabled bool) error { - filtFactory, err := m.ms.GetFilterFactory(name) - if err != nil { - return err - } - +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 := filtFactory(cr) + filt, err := m.ms.GetFilterFactory(name, cr, params) + if err != nil { + return false, err + } + if err != nil { return false, err } diff --git a/player/machine/settings/machinesettings.go b/player/machine/settings/machinesettings.go index 90f1fa0..ae55119 100644 --- a/player/machine/settings/machinesettings.go +++ b/player/machine/settings/machinesettings.go @@ -18,7 +18,7 @@ type FilterFactoryFunc func(instrument frequency.Frequency) (filter.Filter, erro type MachineSettings[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { PeriodConverter song.PeriodCalculator[TPeriod] - GetFilterFactory func(name string) (FilterFactoryFunc, error) + 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) diff --git a/song/channelsettings.go b/song/channelsettings.go index f6ef09d..064a523 100644 --- a/song/channelsettings.go +++ b/song/channelsettings.go @@ -3,6 +3,7 @@ package song import ( "errors" + "github.com/gotracker/playback/filter" "github.com/gotracker/playback/index" "github.com/gotracker/playback/voice/vol0optimization" ) @@ -13,7 +14,7 @@ type ChannelSettings interface { GetOutputChannelNum() int GetMemory() ChannelMemory IsPanEnabled() bool - GetDefaultFilterName() string + GetDefaultFilterInfo() filter.Info IsDefaultFilterEnabled() bool GetVol0OptimizationSettings() vol0optimization.Vol0OptimizationSettings GetOPLChannel() index.OPLChannel diff --git a/voice/autovibrato/config.go b/voice/autovibrato/config.go new file mode 100644 index 0000000..acc0f84 --- /dev/null +++ b/voice/autovibrato/config.go @@ -0,0 +1,34 @@ +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 { + if factory == nil { + return nil + } + + o, err := factory(a.FactoryName) + if err != nil { + // TODO: maybe return an error? maybe panic? + return nil + } + + o.SetWaveform(oscillator.WaveTableSelect(a.WaveformSelection)) + return o +} diff --git a/voice/autovibrato/settings.go b/voice/autovibrato/settings.go index e80e775..ca470d7 100644 --- a/voice/autovibrato/settings.go +++ b/voice/autovibrato/settings.go @@ -1,27 +1,11 @@ package autovibrato import ( - "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice/oscillator" + "github.com/gotracker/playback/voice/types" ) -// AutoVibratoSettings is the setting and memory for the auto-vibrato system -type AutoVibratoSettings[TPeriod period.Period] struct { - PC period.PeriodConverter[TPeriod] - 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 AutoVibratoSettings[TPeriod]) Generate() oscillator.Oscillator { - if a.Factory == nil { - return nil - } - o := a.Factory() - o.SetWaveform(oscillator.WaveTableSelect(a.WaveformSelection)) - return o +type AutoVibratoSettings[TPeriod types.Period] struct { + AutoVibratoConfig[TPeriod] + Factory func(string) (oscillator.Oscillator, error) } diff --git a/voice/component/modulator_autovibrato.go b/voice/component/modulator_autovibrato.go index 9847c98..d4ecca7 100644 --- a/voice/component/modulator_autovibrato.go +++ b/voice/component/modulator_autovibrato.go @@ -39,7 +39,7 @@ func (f AutoVibratoModulator[TPeriod]) Clone() AutoVibratoModulator[TPeriod] { func (f *AutoVibratoModulator[TPeriod]) Reset() error { f.keyed.age = 0 - f.autoVibrato = f.settings.Generate() + f.autoVibrato = f.settings.Generate(f.settings.Factory) return f.ResetAutoVibrato() } @@ -50,7 +50,7 @@ func (f *AutoVibratoModulator[TPeriod]) SetEnabled(enabled bool) { // ConfigureAutoVibrato sets the AutoVibrato oscillator settings func (f *AutoVibratoModulator[TPeriod]) ConfigureAutoVibrato() { - f.autoVibrato = f.settings.Generate() + f.autoVibrato = f.settings.Generate(f.settings.Factory) } // ResetAutoVibrato resets the current AutoVibrato diff --git a/voice/pcm/bit16.go b/voice/pcm/bit16.go index 56214d7..6920a35 100755 --- a/voice/pcm/bit16.go +++ b/voice/pcm/bit16.go @@ -39,9 +39,9 @@ func (s Sample16BitSigned) ReadAt(d *SampleData, ofs int64) (volume.Volume, erro func (Sample16BitSigned) Format(d *SampleData) SampleDataFormat { if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { - return SampleDataFormat16BitLESigned - } else { return SampleDataFormat16BitBESigned + } else { + return SampleDataFormat16BitLESigned } } @@ -73,8 +73,8 @@ func (s Sample16BitUnsigned) ReadAt(d *SampleData, ofs int64) (volume.Volume, er func (Sample16BitUnsigned) Format(d *SampleData) SampleDataFormat { if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { - return SampleDataFormat16BitLEUnsigned - } else { return SampleDataFormat16BitBEUnsigned + } else { + return SampleDataFormat16BitLEUnsigned } } diff --git a/voice/pcm/bit32float.go b/voice/pcm/bit32float.go index 301dafb..d92b755 100755 --- a/voice/pcm/bit32float.go +++ b/voice/pcm/bit32float.go @@ -35,8 +35,8 @@ func (s Sample32BitFloat) ReadAt(d *SampleData, ofs int64) (volume.Volume, error func (Sample32BitFloat) Format(d *SampleData) SampleDataFormat { if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { - return SampleDataFormat32BitLEFloat - } else { return SampleDataFormat32BitBEFloat + } else { + return SampleDataFormat32BitLEFloat } } diff --git a/voice/pcm/bit64float.go b/voice/pcm/bit64float.go index 6065b48..7f7d298 100755 --- a/voice/pcm/bit64float.go +++ b/voice/pcm/bit64float.go @@ -35,8 +35,8 @@ func (s Sample64BitFloat) ReadAt(d *SampleData, ofs int64) (volume.Volume, error func (Sample64BitFloat) Format(d *SampleData) SampleDataFormat { if d.byteOrder.Uint16([]byte{0x01, 0x02}) == 0x0102 { - return SampleDataFormat64BitLEFloat - } else { return SampleDataFormat64BitBEFloat + } else { + return SampleDataFormat64BitLEFloat } } 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/sampledata.go b/voice/pcm/sampledata.go index a40b2ea..eec5e5d 100755 --- a/voice/pcm/sampledata.go +++ b/voice/pcm/sampledata.go @@ -64,6 +64,7 @@ func NewSample(data []byte, length int, channels int, format SampleDataFormat) S length: length, channels: channels, } + switch format { case SampleDataFormat8BitSigned: return &PCMReader[Sample8BitSigned]{ @@ -227,5 +228,8 @@ func NewSampleFromBase64(channels int, format SampleDataFormat, data string) Sam panic(err) } - return NewSample(d, len(d), channels, format) + sb := getSampleBytes(format) + length := len(d) / (sb * channels) + + return NewSample(d, length, channels, format) } 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 +} From 2325489afaf8c56dd5e34c4c4345a8fd72428e7b Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sat, 20 Jan 2024 20:08:21 -0800 Subject: [PATCH 57/63] fix instrument mapping and semitone remap --- format/common/basesong.go | 17 +-- format/it/channel/data.go | 18 +-- format/it/channel/sampleid.go | 17 +-- format/it/layout/song.go | 36 +++++- format/it/layout/stringrow.go | 143 ++++++++++++++++++++++++ format/it/load/itformat.go | 37 +++--- format/it/note/note.go | 2 +- format/it/voice/voice.go | 7 +- format/it/volume/volume.go | 2 +- format/s3m/channel/data.go | 7 +- format/s3m/channel/instid.go | 7 +- format/s3m/layout/stringrow.go | 2 +- format/s3m/load/modconv/modconverter.go | 2 +- format/s3m/voice/voice.go | 9 +- format/xm/channel/data.go | 17 +-- format/xm/channel/sampid.go | 17 +-- format/xm/layout/song.go | 6 +- format/xm/load/xmformat.go | 2 +- format/xm/voice/voice.go | 7 +- instrument/util.go | 3 +- note/note.go | 11 ++ player/machine/channel_notedecode.go | 17 ++- player/machine/machine.go | 2 +- player/machine/machine_channel.go | 8 +- song/channeldata.go | 3 +- song/song.go | 3 +- 26 files changed, 279 insertions(+), 123 deletions(-) create mode 100644 format/it/layout/stringrow.go diff --git a/format/common/basesong.go b/format/common/basesong.go index fc56f40..ba5f2e0 100644 --- a/format/common/basesong.go +++ b/format/common/basesong.go @@ -141,21 +141,12 @@ func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) NumI return len(s.Instruments) } -// IsValidInstrumentID returns true if the instrument exists -func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) IsValidInstrumentID(instNum instrument.ID) bool { - if instNum.IsEmpty() { - return false - } - idx, _ := instNum.GetIndexAndSemitone() - return idx > 0 && idx <= len(s.Instruments) -} - // GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s BaseSong[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetInstrument(instID instrument.ID) (instrument.InstrumentIntf, note.Semitone) { - if instID.IsEmpty() { - return nil, note.UnchangedSemitone +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, st := instID.GetIndexAndSemitone() + idx := instID - 1 if idx >= len(s.Instruments) { return nil, st } diff --git a/format/it/channel/data.go b/format/it/channel/data.go index d25fcf7..ad78b7d 100644 --- a/format/it/channel/data.go +++ b/format/it/channel/data.go @@ -6,14 +6,12 @@ import ( itfile "github.com/gotracker/goaudiofile/music/tracked/it" "github.com/gotracker/gomixing/volume" - "github.com/heucuva/optional" "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/index" - "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" @@ -63,18 +61,8 @@ func (d Data[TPeriod]) HasInstrument() bool { } // GetInstrument returns the instrument for the channel -func (d Data[TPeriod]) 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: optional.NewValue(st), - } +func (d Data[TPeriod]) GetInstrument() int { + return int(d.Instrument) } // HasVolume returns true if there exists a volume on the channel @@ -127,6 +115,8 @@ func (Data[TPeriod]) getNoteString(n note.Note) string { return "===" case note.SpecialTypeStop: return "^^^" + case note.SpecialTypeFadeout: + return "vvv" case note.SpecialTypeNormal: return n.String() default: diff --git a/format/it/channel/sampleid.go b/format/it/channel/sampleid.go index 71767e2..7f6512c 100644 --- a/format/it/channel/sampleid.go +++ b/format/it/channel/sampleid.go @@ -2,15 +2,12 @@ package channel import ( "fmt" - - "github.com/gotracker/playback/note" - "github.com/heucuva/optional" ) // SampleID is an InstrumentID that is a combination of InstID and SampID type SampleID struct { - InstID uint8 - Semitone optional.Value[note.Semitone] + InstID uint8 + SampID uint8 } // IsEmpty returns true if the sample ID is empty @@ -18,14 +15,10 @@ func (s SampleID) IsEmpty() bool { return s.InstID == 0 } -func (s SampleID) GetIndexAndSemitone() (int, note.Semitone) { - st := note.UnchangedSemitone - if ost, set := s.Semitone.Get(); set { - st = ost - } - return int(s.InstID) - 1, st +func (s SampleID) GetIndexAndSample() (int, int) { + return int(s.InstID) - 1, int(s.SampID) } func (s SampleID) String() string { - return fmt.Sprintf("%d(%v)", s.InstID, s.Semitone) + return fmt.Sprintf("%d(%d)", s.InstID, s.SampID) } diff --git a/format/it/layout/song.go b/format/it/layout/song.go index fe4979e..24fdd17 100644 --- a/format/it/layout/song.go +++ b/format/it/layout/song.go @@ -7,17 +7,30 @@ 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/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/song" ) +type SemitoneSample uint16 + +func (s SemitoneSample) Split() (int, note.Semitone) { + return int(s >> 8), note.Semitone(s & 0xFF) +} + +func NewSemitoneSample(sampIdx int, remap note.Semitone) SemitoneSample { + return SemitoneSample(uint16(sampIdx<<8) | uint16(remap)) +} + +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] - InstrumentNoteMap map[uint8]map[note.Semitone]NoteInstrument[TPeriod] + InstrumentNoteMap map[uint8]SemitoneSamples ChannelSettings []ChannelSetting FilterPlugins map[int]filter.Info } @@ -32,6 +45,27 @@ func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.Channel return s.ChannelSettings[channelNum] } +// 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, st + } + + idx := instID - 1 + + if inm, ok := s.InstrumentNoteMap[uint8(instID)]; ok { + if rm := inm[st]; rm != 0 { + idx, st = rm.Split() + } + } + + 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) 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/itformat.go b/format/it/load/itformat.go index b04c6cc..fc042cf 100644 --- a/format/it/load/itformat.go +++ b/format/it/load/itformat.go @@ -122,11 +122,11 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] GlobalVolume: h.GlobalVolume, MixingVolume: h.MixingVolume, InitialOrder: h.InitialOrder, - Instruments: make([]*instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], f.Head.InstrumentCount), + 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]map[note.Semitone]layout.NoteInstrument[TPeriod]), + InstrumentNoteMap: make(map[uint8]layout.SemitoneSamples), FilterPlugins: make(map[int]filter.Info), } @@ -160,7 +160,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] } for _, ci := range instMap { - addSampleWithNoteMapToSong(songData, ci.Inst, ci.NR, instNum) + addSampleWithNoteMapToSong(songData, instNum, ci.Inst, ci.NR) } case *itfile.IMPIInstrument: @@ -170,7 +170,7 @@ func convertItFileToTypedSong[TPeriod period.Period](f *itfile.File, features [] } for _, ci := range instMap { - addSampleWithNoteMapToSong(songData, ci.Inst, ci.NR, instNum) + addSampleWithNoteMapToSong(songData, instNum, ci.Inst, ci.NR) } } } @@ -246,31 +246,32 @@ type noteRemap struct { Remap note.Semitone } -func addSampleWithNoteMapToSong[TPeriod period.Period](song *layout.Song[TPeriod], sample *instrument.Instrument[TPeriod, itVolume.FineVolume, itVolume.Volume, itPanning.Panning], 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[instNum] = 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[TPeriod]) - song.InstrumentNoteMap[id.InstID] = inm - } + inm, _ := song.InstrumentNoteMap[id.InstID] + hasRemap := false for _, st := range sts { - inm[st.Orig] = layout.NoteInstrument[TPeriod]{ - 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) (song.Data, error) { 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/voice/voice.go b/format/it/voice/voice.go index 067e057..657723e 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -35,6 +35,7 @@ type itVoice[TPeriod Period] struct { component.KeyModulator + stopped bool voicer component.Voicer[TPeriod, itVolume.FineVolume, itVolume.Volume] amp component.AmpModulator[itVolume.FineVolume, itVolume.Volume] fadeout component.FadeoutModulator @@ -251,6 +252,7 @@ func (v *itVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, itVolume.F func (v *itVoice[TPeriod]) Reset() error { v.KeyModulator.Release() + v.stopped = false return errors.Join( v.amp.Reset(), v.fadeout.Reset(), @@ -268,12 +270,12 @@ func (v *itVoice[TPeriod]) Reset() error { } func (v *itVoice[TPeriod]) Stop() { - v.voicer = nil + v.stopped = true v.updateFinal() } func (v itVoice[TPeriod]) IsDone() bool { - if v.voicer == nil { + if v.voicer == nil || v.stopped { return true } @@ -342,6 +344,7 @@ func (v *itVoice[TPeriod]) Clone(background bool) voice.Voice { pitchAndFilterEnvShared: v.pitchAndFilterEnvShared, filterEnvActive: v.filterEnvActive, fadeoutMode: v.fadeoutMode, + stopped: v.stopped, amp: v.amp.Clone(), fadeout: v.fadeout.Clone(), freq: v.freq.Clone(), diff --git a/format/it/volume/volume.go b/format/it/volume/volume.go index 26f3827..7c0426e 100644 --- a/format/it/volume/volume.go +++ b/format/it/volume/volume.go @@ -10,7 +10,7 @@ import ( var ( MaxItVolume = itfile.Volume(0x40) - MaxItFineVolume = FineVolume(0x7f) + MaxItFineVolume = FineVolume(0x80) DefaultItVolume = itfile.DefaultVolume // DefaultVolume is the default volume value for most everything in IT format diff --git a/format/s3m/channel/data.go b/format/s3m/channel/data.go index 76ac98f..5116c53 100644 --- a/format/s3m/channel/data.go +++ b/format/s3m/channel/data.go @@ -11,7 +11,6 @@ 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/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" @@ -26,7 +25,7 @@ type DataEffect uint8 type Data struct { What s3mfile.PatternFlags Note s3mfile.Note - Instrument InstID + Instrument uint8 Volume s3mVolume.Volume Command uint8 Info DataEffect @@ -48,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 diff --git a/format/s3m/channel/instid.go b/format/s3m/channel/instid.go index 5b3f904..cc55a47 100644 --- a/format/s3m/channel/instid.go +++ b/format/s3m/channel/instid.go @@ -2,8 +2,6 @@ package channel import ( "fmt" - - "github.com/gotracker/playback/note" ) // InstID is an instrument ID in S3M world @@ -14,8 +12,9 @@ func (s InstID) IsEmpty() bool { return s == 0 } -func (s InstID) GetIndexAndSemitone() (int, note.Semitone) { - return int(s) - 1, note.UnchangedSemitone +func (s InstID) GetIndexAndSample() (int, int) { + idx := int(s) - 1 + return idx, idx } func (s InstID) String() string { diff --git a/format/s3m/layout/stringrow.go b/format/s3m/layout/stringrow.go index 880a617..02d6c88 100644 --- a/format/s3m/layout/stringrow.go +++ b/format/s3m/layout/stringrow.go @@ -101,7 +101,7 @@ func (StringRow) decodeChannel(cstr string) (channel.Data, error) { if i > 0 { d.What |= s3mfile.PatternFlagNote - d.Instrument = channel.InstID(i) + d.Instrument = uint8(i) } } diff --git a/format/s3m/load/modconv/modconverter.go b/format/s3m/load/modconv/modconverter.go index 1e3bf23..c5876c3 100644 --- a/format/s3m/load/modconv/modconverter.go +++ b/format/s3m/load/modconv/modconverter.go @@ -30,7 +30,7 @@ func convertMODPatternToS3M(mp *modfile.Pattern) (*s3mfile.PackedPattern, error) *u = channel.Data{ What: s3mfile.PatternFlags(c & 0x1F), Note: s3mfile.EmptyNote, - Instrument: channel.InstID(sampleNumber), + Instrument: sampleNumber, Volume: s3mVolume.Volume(s3mfile.EmptyVolume), Command: uint8(0), Info: channel.DataEffect(0), diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index c06f543..1d79868 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -26,7 +26,8 @@ type s3mVoice struct { component.KeyModulator - voicer component.Voicer[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume] + 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] @@ -171,6 +172,7 @@ func (v *s3mVoice) Setup(inst *instrument.Instrument[period.Amiga, s3mVolume.Fin } func (v *s3mVoice) Reset() error { + v.stopped = false return errors.Join( v.AmpModulator.Reset(), v.FreqModulator.Reset(), @@ -180,7 +182,7 @@ func (v *s3mVoice) Reset() error { } func (v *s3mVoice) Stop() { - v.voicer = nil + v.stopped = true } func (v s3mVoice) IsMuted() bool { @@ -188,7 +190,7 @@ func (v s3mVoice) IsMuted() bool { } func (v s3mVoice) IsDone() bool { - if v.voicer == nil { + if v.voicer == nil || v.stopped { return true } @@ -221,6 +223,7 @@ func (v *s3mVoice) Clone(bool) voice.Voice { inst: v.inst, opl2Chip: v.opl2Chip, opl2Channel: v.opl2Channel, + stopped: v.stopped, AmpModulator: v.AmpModulator.Clone(), FreqModulator: v.FreqModulator.Clone(), PanModulator: v.PanModulator.Clone(), diff --git a/format/xm/channel/data.go b/format/xm/channel/data.go index 12cab92..9263aeb 100644 --- a/format/xm/channel/data.go +++ b/format/xm/channel/data.go @@ -6,14 +6,12 @@ import ( xmfile "github.com/gotracker/goaudiofile/music/tracked/xm" "github.com/gotracker/gomixing/volume" - "github.com/heucuva/optional" "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/index" - "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine" @@ -63,19 +61,8 @@ func (d Data[TPeriod]) HasInstrument() bool { } // GetInstrument returns the instrument for the channel -func (d Data[TPeriod]) 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: optional.NewValue[note.Semitone](st), - } +func (d Data[TPeriod]) GetInstrument() int { + return int(d.Instrument) } // HasVolume returns true if there exists a volume on the channel diff --git a/format/xm/channel/sampid.go b/format/xm/channel/sampid.go index 8a449d6..7f6512c 100644 --- a/format/xm/channel/sampid.go +++ b/format/xm/channel/sampid.go @@ -2,15 +2,12 @@ package channel import ( "fmt" - - "github.com/gotracker/playback/note" - "github.com/heucuva/optional" ) // SampleID is an InstrumentID that is a combination of InstID and SampID type SampleID struct { - InstID uint8 - Semitone optional.Value[note.Semitone] + InstID uint8 + SampID uint8 } // IsEmpty returns true if the sample ID is empty @@ -18,14 +15,10 @@ func (s SampleID) IsEmpty() bool { return s.InstID == 0 } -func (s SampleID) GetIndexAndSemitone() (int, note.Semitone) { - st := note.UnchangedSemitone - if ost, set := s.Semitone.Get(); set { - st = ost - } - return int(s.InstID) - 1, st +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/layout/song.go b/format/xm/layout/song.go index 891deeb..da4fd9c 100644 --- a/format/xm/layout/song.go +++ b/format/xm/layout/song.go @@ -34,12 +34,12 @@ func (s Song[TPeriod]) GetChannelSettings(channelNum index.Channel) song.Channel } // GetInstrument returns the instrument interface indexed by `instNum` (0-based) -func (s Song[TPeriod]) GetInstrument(instID instrument.ID) (instrument.InstrumentIntf, note.Semitone) { - if instID.IsEmpty() { +func (s Song[TPeriod]) GetInstrument(instID int, st note.Semitone) (instrument.InstrumentIntf, note.Semitone) { + if instID == 0 { return nil, note.UnchangedSemitone } - idx, st := instID.GetIndexAndSemitone() + idx := instID - 1 i := idx if inm, ok := s.InstrumentNoteMap[uint8(idx)]; ok { diff --git a/format/xm/load/xmformat.go b/format/xm/load/xmformat.go index 425f0d2..7cbb85f 100644 --- a/format/xm/load/xmformat.go +++ b/format/xm/load/xmformat.go @@ -342,7 +342,7 @@ func convertXmFileToTypedSong[TPeriod period.Period](f *xmfile.File, features [] for i, sts := range noteMap { sample := samples[i] inm, _ := s.InstrumentNoteMap[uint8(instNum)] - idx, _ := sample.Static.ID.GetIndexAndSemitone() + idx, _ := sample.Static.ID.GetIndexAndSample() var remapped bool for _, st := range sts { diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 20272e5..9db441a 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -30,6 +30,7 @@ type xmVoice[TPeriod Period] struct { component.KeyModulator + stopped bool voicer component.Voicer[TPeriod, xmVolume.XmVolume, xmVolume.XmVolume] amp component.AmpModulator[xmVolume.XmVolume, xmVolume.XmVolume] fadeout component.FadeoutModulator @@ -206,6 +207,7 @@ func (v *xmVoice[TPeriod]) Setup(inst *instrument.Instrument[TPeriod, xmVolume.X func (v *xmVoice[TPeriod]) Reset() error { v.KeyModulator.Release() + v.stopped = false return errors.Join( v.amp.Reset(), v.fadeout.Reset(), @@ -219,11 +221,11 @@ func (v *xmVoice[TPeriod]) Reset() error { } func (v *xmVoice[TPeriod]) Stop() { - v.voicer = nil + v.stopped = true } func (v xmVoice[TPeriod]) IsDone() bool { - if v.voicer == nil { + if v.voicer == nil || v.stopped { return true } @@ -272,6 +274,7 @@ 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(), diff --git a/instrument/util.go b/instrument/util.go index ee4088e..15baab9 100644 --- a/instrument/util.go +++ b/instrument/util.go @@ -4,13 +4,12 @@ import ( "fmt" "github.com/gotracker/gomixing/sampling" - "github.com/gotracker/playback/note" ) // ID is an identifier for an instrument/sample that means something to the format type ID interface { IsEmpty() bool - GetIndexAndSemitone() (int, note.Semitone) + GetIndexAndSample() (int, int) fmt.Stringer } 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/player/machine/channel_notedecode.go b/player/machine/channel_notedecode.go index d8706e9..b14b25f 100644 --- a/player/machine/channel_notedecode.go +++ b/player/machine/channel_notedecode.go @@ -46,17 +46,21 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco 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(c.prev.Semitone.Coalesce(st)) + i := d.GetInstrument() - ii, _ := m.songData.GetInstrument(i) + 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 { + } else if st != 0 && c.target.Inst != nil { // retrigger same instrument - inst = c.target.Inst + 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 } @@ -72,6 +76,11 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco } 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) } diff --git a/player/machine/machine.go b/player/machine/machine.go index 991a5d5..8313336 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -84,7 +84,7 @@ type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann 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 instrument.ID) 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 diff --git a/player/machine/machine_channel.go b/player/machine/machine_channel.go index 3af3da7..7c4fa94 100644 --- a/player/machine/machine_channel.go +++ b/player/machine/machine_channel.go @@ -129,15 +129,15 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) GetC }) } -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) SetChannelInstrumentByID(ch index.Channel, i instrument.ID) error { +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 instrument.ID + var oldID int if c.target.Inst != nil { - oldID = c.target.Inst.GetID() + oldID, _ = c.target.Inst.GetID().GetIndexAndSample() } traceChannelValueChangeWithComment(m, ch, "target.Inst", oldID, i, "SetChannelInstrumentByID") - inst, _ := m.songData.GetInstrument(i) + 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 { diff --git a/song/channeldata.go b/song/channeldata.go index e722e47..8009ca3 100644 --- a/song/channeldata.go +++ b/song/channeldata.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/gotracker/gomixing/volume" - "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" ) @@ -14,7 +13,7 @@ type ChannelDataIntf interface { GetNote() note.Note HasInstrument() bool - GetInstrument(note.Semitone) instrument.ID + GetInstrument() int HasVolume() bool GetVolumeGeneric() volume.Volume diff --git a/song/song.go b/song/song.go index 2a36f7f..92888d3 100644 --- a/song/song.go +++ b/song/song.go @@ -29,8 +29,7 @@ type Data interface { GetNumChannels() int GetChannelSettings(index.Channel) ChannelSettings NumInstruments() int - IsValidInstrumentID(instrument.ID) bool - GetInstrument(instrument.ID) (instrument.InstrumentIntf, note.Semitone) + GetInstrument(int, note.Semitone) (instrument.InstrumentIntf, note.Semitone) GetName() string GetPatternByOrder(index.Order) (Pattern, error) GetPattern(index.Pattern) (Pattern, error) From f6d03b7b40472a30c292c927a6c0f31ea6f420bd Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 21 Jan 2024 09:53:45 -0800 Subject: [PATCH 58/63] todo cleanup --- format/it/channel/effectfactory.go | 2 +- format/s3m/channel/effect_surroundon.go | 2 +- pan/util.go | 55 ------------------------ player/machine/channel_notedecode.go | 2 +- voice/autovibrato/config.go | 7 ++- voice/component/envelope.go | 2 +- voice/component/modulator_autovibrato.go | 11 +++-- voice/component/opl2.go | 2 +- 8 files changed, 13 insertions(+), 70 deletions(-) delete mode 100644 pan/util.go diff --git a/format/it/channel/effectfactory.go b/format/it/channel/effectfactory.go index 44be180..b243bb5 100644 --- a/format/it/channel/effectfactory.go +++ b/format/it/channel/effectfactory.go @@ -236,7 +236,7 @@ func soundControlEffect[TPeriod period.Period](data Data[TPeriod]) EffectIT { case 0x0: // Surround Off case 0x1: // Surround On // only S91 is supported directly by IT - return nil // TODO: SurroundOn + return SurroundOn(data.EffectParameter) case 0x8: // Reverb Off case 0x9: // Reverb On case 0xA: // Center Surround diff --git a/format/s3m/channel/effect_surroundon.go b/format/s3m/channel/effect_surroundon.go index 55789d5..8fe638d 100644 --- a/format/s3m/channel/effect_surroundon.go +++ b/format/s3m/channel/effect_surroundon.go @@ -18,7 +18,7 @@ func (e SurroundOn) String() string { } func (e SurroundOn) RowStart(ch index.Channel, m machine.Machine[period.Amiga, s3mVolume.Volume, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning]) error { - // TODO: support for center rear panning + // TODO: support for surround function return nil } 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/player/machine/channel_notedecode.go b/player/machine/channel_notedecode.go index b14b25f..6b79060 100644 --- a/player/machine/channel_notedecode.go +++ b/player/machine/channel_notedecode.go @@ -129,7 +129,7 @@ func (c *channel[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) deco } case *instrument.OPL2: - // TODO - is there anything to do? + // TODO - is there anything to actually do here? } return nil } diff --git a/voice/autovibrato/config.go b/voice/autovibrato/config.go index acc0f84..91c37af 100644 --- a/voice/autovibrato/config.go +++ b/voice/autovibrato/config.go @@ -18,17 +18,16 @@ type AutoVibratoConfig[TPeriod types.Period] struct { } // 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 { +func (a AutoVibratoConfig[TPeriod]) Generate(factory func(string) (oscillator.Oscillator, error)) (oscillator.Oscillator, error) { if factory == nil { return nil } o, err := factory(a.FactoryName) if err != nil { - // TODO: maybe return an error? maybe panic? - return nil + return err } o.SetWaveform(oscillator.WaveTableSelect(a.WaveformSelection)) - return o + return o, nil } diff --git a/voice/component/envelope.go b/voice/component/envelope.go index d18852a..c3ff266 100755 --- a/voice/component/envelope.go +++ b/voice/component/envelope.go @@ -88,7 +88,7 @@ func (e *baseEnvelope[TIn, TOut]) SetEnvelopePosition(pos int) (voice.Callback, e.keyed.active = true e.keyed.done = false e.stateReset() - // TODO: this is gross, but currently the most optimal way to find the correct position + // 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 diff --git a/voice/component/modulator_autovibrato.go b/voice/component/modulator_autovibrato.go index d4ecca7..bbfdff8 100644 --- a/voice/component/modulator_autovibrato.go +++ b/voice/component/modulator_autovibrato.go @@ -39,7 +39,11 @@ func (f AutoVibratoModulator[TPeriod]) Clone() AutoVibratoModulator[TPeriod] { func (f *AutoVibratoModulator[TPeriod]) Reset() error { f.keyed.age = 0 - f.autoVibrato = f.settings.Generate(f.settings.Factory) + var err error + f.autoVibrato, err = f.settings.Generate(f.settings.Factory) + if err != nil { + return err + } return f.ResetAutoVibrato() } @@ -48,11 +52,6 @@ func (f *AutoVibratoModulator[TPeriod]) SetEnabled(enabled bool) { f.unkeyed.enabled = enabled } -// ConfigureAutoVibrato sets the AutoVibrato oscillator settings -func (f *AutoVibratoModulator[TPeriod]) ConfigureAutoVibrato() { - f.autoVibrato = f.settings.Generate(f.settings.Factory) -} - // ResetAutoVibrato resets the current AutoVibrato func (f *AutoVibratoModulator[TPeriod]) ResetAutoVibrato() error { if f.autoVibrato != nil { diff --git a/voice/component/opl2.go b/voice/component/opl2.go index 1533311..ca857d4 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -214,5 +214,5 @@ func (o *OPL2[TPeriod, TMixingVolume, TVolume]) freqToFnumBlock(freq float64) (u } func (o OPL2[TPeriod, TMixingVolume, TVolume]) DumpState(ch index.Channel, t tracing.Tracer, comment string) { - // TODO + // TODO - add state dumper } From 4be63e4d2dae928e50f242535d7a11ca8cfc6b2a Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 21 Jan 2024 09:55:12 -0800 Subject: [PATCH 59/63] final todo work --- format/xm/channel/effectfactory_standard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/format/xm/channel/effectfactory_standard.go b/format/xm/channel/effectfactory_standard.go index 4c419cc..111aebd 100644 --- a/format/xm/channel/effectfactory_standard.go +++ b/format/xm/channel/effectfactory_standard.go @@ -87,7 +87,7 @@ func specialEffectFactory[TPeriod period.Period](mem *Memory, ce Command, cp Dat 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 From ce41a19b9fd89c95916379b3e339edebcaf7d68d Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 21 Jan 2024 10:20:44 -0800 Subject: [PATCH 60/63] clean up issues with opl2 linking, update gomod --- format/it/voice/voice.go | 4 ---- format/s3m/voice/voice.go | 5 +++-- format/xm/voice/voice.go | 4 ---- go.mod | 14 +++----------- go.sum | 23 ++++++----------------- player/machine/machine.go | 4 ++-- player/machine/machine_factory.go | 5 +++-- player/machine/machine_opl2.go | 9 +++++---- player/render/channel.go | 4 ++-- voice/component/opl2.go | 9 ++++----- voice/config.go | 5 +++-- voice/opl2/opl2intf.go | 21 --------------------- voice/voice.go | 8 ++++++-- 13 files changed, 37 insertions(+), 78 deletions(-) delete mode 100644 voice/opl2/opl2intf.go diff --git a/format/it/voice/voice.go b/format/it/voice/voice.go index 657723e..a94df8d 100644 --- a/format/it/voice/voice.go +++ b/format/it/voice/voice.go @@ -18,7 +18,6 @@ import ( "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/opl2" ) type Period interface { @@ -102,9 +101,6 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, itVolume.FineVolume, return v } -func (v *itVoice[TPeriod]) SetOPL2Chip(chip opl2.Chip) { -} - func (v *itVoice[TPeriod]) doAttack() { v.vol0Opt.Reset() v.autoVibrato.Reset() diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index 1d79868..4ca3916 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -4,6 +4,8 @@ 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" @@ -16,12 +18,11 @@ import ( "github.com/gotracker/playback/period" "github.com/gotracker/playback/voice" "github.com/gotracker/playback/voice/component" - "github.com/gotracker/playback/voice/opl2" ) type s3mVoice struct { inst *instrument.Instrument[period.Amiga, s3mVolume.FineVolume, s3mVolume.Volume, s3mPanning.Panning] - opl2Chip opl2.Chip + opl2Chip *opl2.Chip opl2Channel index.OPLChannel component.KeyModulator diff --git a/format/xm/voice/voice.go b/format/xm/voice/voice.go index 9db441a..9c48da2 100644 --- a/format/xm/voice/voice.go +++ b/format/xm/voice/voice.go @@ -16,7 +16,6 @@ import ( "github.com/gotracker/playback/voice/autovibrato" "github.com/gotracker/playback/voice/component" "github.com/gotracker/playback/voice/fadeout" - "github.com/gotracker/playback/voice/opl2" ) type Period interface { @@ -84,9 +83,6 @@ func New[TPeriod Period](config voice.VoiceConfig[TPeriod, xmVolume.XmVolume, xm return v } -func (v *xmVoice[TPeriod]) SetOPL2Chip(chip opl2.Chip) { -} - func (v *xmVoice[TPeriod]) doAttack() { v.vol0Opt.Reset() v.autoVibrato.ResetAutoVibrato() diff --git a/go.mod b/go.mod index 56e78ab..d0ea5ad 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,10 @@ module github.com/gotracker/playback go 1.21 require ( - github.com/fatih/color v1.16.0 - github.com/gotracker/goaudiofile v1.0.15 - 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-20231219180239-dc181d75b848 -) - -require ( - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pkg/errors v0.9.1 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a ) diff --git a/go.sum b/go.sum index add20e9..ed098c8 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,16 @@ -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/gotracker/goaudiofile v1.0.15 h1:90MtPQB1i99DSTradMol49VZlV4smJ5iLYGrdecfIps= -github.com/gotracker/goaudiofile v1.0.15/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= github.com/heucuva/comparison v1.0.0/go.mod h1:5l0Va1uxFyy7S4DgdflnayxV2HStFwWI2rzbrlNNNMk= github.com/heucuva/optional v0.0.1 h1:tLbVBMQBKzQVfe43bHQFSxjhTzYcRK8frnTBG6FLksM= github.com/heucuva/optional v0.0.1/go.mod h1:2AtE/X9279wzrHLkCNvKl0xP7AiEIj3RijGKwbO8R3M= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/player/machine/machine.go b/player/machine/machine.go index 8313336..4e54be5 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -6,6 +6,7 @@ import ( "github.com/gotracker/gomixing/sampling" "github.com/gotracker/gomixing/volume" + "github.com/gotracker/opl2" "github.com/gotracker/playback/filter" "github.com/gotracker/playback/index" @@ -18,7 +19,6 @@ import ( "github.com/gotracker/playback/player/render" "github.com/gotracker/playback/player/sampler" "github.com/gotracker/playback/song" - "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/oscillator" "github.com/gotracker/playback/voice/types" ) @@ -139,7 +139,7 @@ type machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPann songData song.Data ms *settings.MachineSettings[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning] us settings.UserSettings - opl2 opl2.Chip + opl2 *opl2.Chip opl2Enabled bool rowStringer render.RowStringer diff --git a/player/machine/machine_factory.go b/player/machine/machine_factory.go index 52557bb..f71d6bb 100644 --- a/player/machine/machine_factory.go +++ b/player/machine/machine_factory.go @@ -5,13 +5,14 @@ import ( "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" - "github.com/gotracker/playback/voice/opl2" ) type typeLookup struct { @@ -135,7 +136,7 @@ func RegisterMachine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volum rc.OutputFilter = filt } - rc.GetOPL2Chip = func() opl2.Chip { + rc.GetOPL2Chip = func() *opl2.Chip { return m.opl2 } diff --git a/player/machine/machine_opl2.go b/player/machine/machine_opl2.go index a6c83a4..de3e668 100644 --- a/player/machine/machine_opl2.go +++ b/player/machine/machine_opl2.go @@ -6,8 +6,9 @@ import ( "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/opl2" + "github.com/gotracker/playback/voice" ) func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setupOPL2(s *sampler.Sampler) error { @@ -15,7 +16,7 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setu return errors.New("sampler is nil") } - o := opl2.NewOPL2Chip(uint32(s.SampleRate)) + 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 @@ -24,14 +25,14 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) setu for i := range m.actualOutputs { rc := &m.actualOutputs[i] - if v := rc.GetVoice(); v != nil { + 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(); v != nil { + if v, _ := rc.GetVoice().(voice.VoiceOPL2er); v != nil { v.SetOPL2Chip(m.opl2) } } diff --git a/player/render/channel.go b/player/render/channel.go index 15d816e..21c30ed 100644 --- a/player/render/channel.go +++ b/player/render/channel.go @@ -3,12 +3,12 @@ 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" "github.com/gotracker/playback/voice/mixer" - "github.com/gotracker/playback/voice/opl2" ) type ChannelIntf interface { @@ -20,7 +20,7 @@ type ChannelIntf interface { type Channel[TPeriod period.Period] struct { PluginFilter filter.Filter OutputFilter filter.Filter - GetOPL2Chip func() opl2.Chip + GetOPL2Chip func() *opl2.Chip GlobalVolume volume.Volume // this is the channel's version of the GlobalVolume v voice.Voice diff --git a/voice/component/opl2.go b/voice/component/opl2.go index ca857d4..250bb05 100755 --- a/voice/component/opl2.go +++ b/voice/component/opl2.go @@ -2,13 +2,12 @@ package component import ( "github.com/gotracker/gomixing/volume" - opl2Impl "github.com/gotracker/opl2" + "github.com/gotracker/opl2" "github.com/gotracker/playback/frequency" "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" "github.com/gotracker/playback/tracing" - "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/types" ) @@ -35,7 +34,7 @@ func (o OPL2Registers) Clone() OPL2Registers { // OPL2 is an OPL2 component type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { - chip opl2.Chip + chip *opl2.Chip channel int reg OPL2Registers baseFreq frequency.Frequency @@ -45,7 +44,7 @@ type OPL2[TPeriod types.Period, TMixingVolume, TVolume types.Volume] struct { } // Setup sets up the OPL2 component -func (o *OPL2[TPeriod, TMixingVolume, TVolume]) Setup(chip opl2.Chip, channel int, reg OPL2Registers, pc period.PeriodConverter[TPeriod], baseFreq frequency.Frequency, defaultVolume TVolume) { +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 @@ -208,7 +207,7 @@ func (o *OPL2[TPeriod, TMixingVolume, TVolume]) freqToFnumBlock(freq float64) (u } else { block = 0 } - fnum := uint16(freq * float64(int(1)<<(20-block)) / opl2Impl.OPLRATE) + fnum := uint16(freq * float64(int(1)<<(20-block)) / opl2.OPLRATE) return fnum, block } diff --git a/voice/config.go b/voice/config.go index 2f3cf9b..82ed08a 100644 --- a/voice/config.go +++ b/voice/config.go @@ -1,9 +1,10 @@ package voice import ( + "github.com/gotracker/opl2" + "github.com/gotracker/playback/index" "github.com/gotracker/playback/period" - "github.com/gotracker/playback/voice/opl2" "github.com/gotracker/playback/voice/types" "github.com/gotracker/playback/voice/vol0optimization" ) @@ -16,7 +17,7 @@ type ( type VoiceConfig[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] struct { PC period.PeriodConverter[TPeriod] - OPLChip opl2.Chip + OPLChip *opl2.Chip OPLChannel index.OPLChannel InitialVolume TVolume InitialMixing TMixingVolume diff --git a/voice/opl2/opl2intf.go b/voice/opl2/opl2intf.go deleted file mode 100644 index 75af19b..0000000 --- a/voice/opl2/opl2intf.go +++ /dev/null @@ -1,21 +0,0 @@ -package opl2 - -import ( - "github.com/gotracker/opl2" -) - -// Chip sets up a contract that the chip definition will contain these interfaces -type Chip interface { - WriteReg(uint32, uint8) - GenerateBlock2(uint, []int32) -} - -// OPL2Intf is an interface to get the active OPL2 chip -type OPL2Intf interface { - GetOPL2Chip() Chip -} - -// NewOPL2Chip generates a new OPL2 chip -func NewOPL2Chip(rate uint32) Chip { - return opl2.NewChip(rate, false) -} diff --git a/voice/voice.go b/voice/voice.go index a06eb8d..87d5706 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -4,13 +4,14 @@ import ( "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/opl2" "github.com/gotracker/playback/voice/types" ) @@ -21,7 +22,6 @@ type Voice interface { // Configuration Reset() error SetPlaybackRate(outputRate frequency.Frequency) error - SetOPL2Chip(chip opl2.Chip) // Actions Attack() @@ -40,6 +40,10 @@ type Voice interface { GetSampleRate() frequency.Frequency } +type VoiceOPL2er interface { + SetOPL2Chip(chip opl2.Chip) +} + type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { Voice From 83be14413b56fd50bc2ba990209153185a8fd254 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 21 Jan 2024 10:30:42 -0800 Subject: [PATCH 61/63] fix autovibrato errors --- voice/autovibrato/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice/autovibrato/config.go b/voice/autovibrato/config.go index 91c37af..12f7512 100644 --- a/voice/autovibrato/config.go +++ b/voice/autovibrato/config.go @@ -20,12 +20,12 @@ type AutoVibratoConfig[TPeriod types.Period] struct { // 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 + return nil, nil } o, err := factory(a.FactoryName) if err != nil { - return err + return nil, err } o.SetWaveform(oscillator.WaveTableSelect(a.WaveformSelection)) From 84b0769c0fd1e95a18894b01490c87703a06a771 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 21 Jan 2024 10:33:31 -0800 Subject: [PATCH 62/63] final cleanups --- format/it/channel/effect_surroundon.go | 27 ++++++++++++++++++++++++++ format/it/channel/effectfactory.go | 2 +- format/s3m/voice/voice.go | 2 +- voice/voice.go | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 format/it/channel/effect_surroundon.go 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/effectfactory.go b/format/it/channel/effectfactory.go index b243bb5..d4fbe0b 100644 --- a/format/it/channel/effectfactory.go +++ b/format/it/channel/effectfactory.go @@ -236,7 +236,7 @@ func soundControlEffect[TPeriod period.Period](data Data[TPeriod]) EffectIT { case 0x0: // Surround Off case 0x1: // Surround On // only S91 is supported directly by IT - return SurroundOn(data.EffectParameter) + return SurroundOn[TPeriod](data.EffectParameter) case 0x8: // Reverb Off case 0x9: // Reverb On case 0xA: // Center Surround diff --git a/format/s3m/voice/voice.go b/format/s3m/voice/voice.go index 4ca3916..f4803bc 100644 --- a/format/s3m/voice/voice.go +++ b/format/s3m/voice/voice.go @@ -78,7 +78,7 @@ func New(config voice.VoiceConfig[period.Amiga, s3mVolume.Volume, s3mVolume.Fine return v } -func (v *s3mVoice) SetOPL2Chip(chip opl2.Chip) { +func (v *s3mVoice) SetOPL2Chip(chip *opl2.Chip) { v.opl2Chip = chip } diff --git a/voice/voice.go b/voice/voice.go index 87d5706..14e00f5 100755 --- a/voice/voice.go +++ b/voice/voice.go @@ -41,7 +41,7 @@ type Voice interface { } type VoiceOPL2er interface { - SetOPL2Chip(chip opl2.Chip) + SetOPL2Chip(chip *opl2.Chip) } type RenderVoice[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { From 409462077deab8fd26e1940485452f4a3c9d8ecb Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Sun, 21 Jan 2024 11:05:03 -0800 Subject: [PATCH 63/63] clean up generation interface --- internal/examples/bufferload/example.go | 43 +++++++++++++------------ internal/examples/fileload/example.go | 34 ++++++++++--------- playback.go | 15 +++------ player/machine/machine.go | 4 +-- player/machine/machine_tick.go | 30 +++++++++-------- player/sampler/sampler.go | 5 ++- 6 files changed, 68 insertions(+), 63 deletions(-) diff --git a/internal/examples/bufferload/example.go b/internal/examples/bufferload/example.go index 240a931..9673a2e 100644 --- a/internal/examples/bufferload/example.go +++ b/internal/examples/bufferload/example.go @@ -76,20 +76,10 @@ func ExamplePlayBufferToStdout() { 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. - out := sampler.NewSampler(sampleRate, channels) - if out == nil { - panic(errors.New("could not create sampler")) - } - - // The player's nearly ready to start playing! Now that the player is configured, we can allocate - // a channel is filled with bundles of pre-mix data (i.e.: data which is ready to be converted into - // the final, mixed version). We want to use a buffered channel with a bit of room in it. It doesn't - // have to be huge, but the more space you supply, the more likely the player will not underflow the - // channel. + // Now that the player is configured, we can allocate a channel is filled with bundles of + // pre-mix data (i.e.: data which is ready to be converted into the final, mixed version). + // We want to use a buffered channel with a bit of room in it. It doesn't have to be huge, + // but the more space you supply, the more likely the player will not underflow the channel. const premixChannelSize = 8 premixDataChannel := make(chan *output.PremixData, premixChannelSize) @@ -100,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. @@ -128,10 +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 - premix, err := player.Generate(out) - if 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) { @@ -140,8 +145,6 @@ playerUpdateLoop: // If we get here, then we don't know what the error is... panic(err) } - - premixDataChannel <- premix } // We're done! diff --git a/internal/examples/fileload/example.go b/internal/examples/fileload/example.go index 7464e09..6cfb539 100644 --- a/internal/examples/fileload/example.go +++ b/internal/examples/fileload/example.go @@ -75,15 +75,6 @@ func ExamplePlayFileToStdout() { 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. - out := sampler.NewSampler(sampleRate, channels) - if out == nil { - panic(errors.New("could not create sampler")) - } - // Now that the player is configured, we can allocate a channel is filled with bundles of // pre-mix data (i.e.: data which is ready to be converted into the final, mixed version). // We want to use a buffered channel with a bit of room in it. It doesn't have to be huge, @@ -98,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. @@ -126,10 +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 - premix, err := player.Generate(out) - if 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) { @@ -138,8 +144,6 @@ playerUpdateLoop: // If we get here, then we don't know what the error is... panic(err) } - - premixDataChannel <- premix } // We're done! diff --git a/playback.go b/playback.go index c81ea75..3709baf 100644 --- a/playback.go +++ b/playback.go @@ -1,23 +1,18 @@ package playback import ( - "time" - - "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/feature" + "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 Configure([]feature.Feature) error - // runs the internal ticker and returns sample output - Update(time.Duration, chan<- *output.PremixData) error - Generate(time.Duration) (*output.PremixData, error) - - // runs the internal ticker without caring about the sample output - Tick() error + // 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 GetNumOrders() int CanOrderLoop() bool diff --git a/player/machine/machine.go b/player/machine/machine.go index 4e54be5..2094eea 100644 --- a/player/machine/machine.go +++ b/player/machine/machine.go @@ -12,7 +12,6 @@ import ( "github.com/gotracker/playback/index" "github.com/gotracker/playback/instrument" "github.com/gotracker/playback/note" - "github.com/gotracker/playback/output" "github.com/gotracker/playback/period" "github.com/gotracker/playback/player/machine/instruction" "github.com/gotracker/playback/player/machine/settings" @@ -46,8 +45,7 @@ type MachineInfo interface { type MachineTicker interface { MachineInfo - Generate(s *sampler.Sampler) (*output.PremixData, error) - Tick() error + Tick(s *sampler.Sampler) error } type Machine[TPeriod Period, TGlobalVolume, TMixingVolume, TVolume Volume, TPanning Panning] interface { diff --git a/player/machine/machine_tick.go b/player/machine/machine_tick.go index 70be690..e5fdef3 100644 --- a/player/machine/machine_tick.go +++ b/player/machine/machine_tick.go @@ -1,27 +1,19 @@ package machine import ( - "errors" - "github.com/gotracker/playback/index" - "github.com/gotracker/playback/output" "github.com/gotracker/playback/player/sampler" ) -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Generate(s *sampler.Sampler) (*output.PremixData, error) { - if m.opl2Enabled && m.opl2 == nil && m.ms.OPL2Enabled { - if err := m.setupOPL2(s); err != nil { - return nil, err +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 + } } } - tickErr := m.Tick() - - premix, err := m.render(s) - return premix, errors.Join(tickErr, err) -} - -func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick() error { if err := m.songData.ForEachChannel(true, func(ch index.Channel) (bool, error) { c := &m.channels[ch] if err := c.DoNoteAction(ch, m); err != nil { @@ -38,6 +30,16 @@ func (m *machine[TPeriod, TGlobalVolume, TMixingVolume, TVolume, TPanning]) Tick } m.age++ + + if s != nil { + premix, err := m.render(s) + if err != nil { + return err + } + if s.OnGenerate != nil { + s.OnGenerate(premix) + } + } return nil } diff --git a/player/sampler/sampler.go b/player/sampler/sampler.go index d31d555..6cedefc 100644 --- a/player/sampler/sampler.go +++ b/player/sampler/sampler.go @@ -3,20 +3,23 @@ package sampler import ( "github.com/gotracker/gomixing/mixing" "github.com/gotracker/playback/frequency" + "github.com/gotracker/playback/output" ) // Sampler is a container of sampler/mixer settings type Sampler struct { SampleRate int 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) *Sampler { +func NewSampler(samplesPerSec, channels int, onGenerate func(premix *output.PremixData)) *Sampler { s := Sampler{ SampleRate: samplesPerSec, + OnGenerate: onGenerate, mixer: mixing.Mixer{ Channels: channels, },