From e6c2e196a5747d1a8fdd3c34191444ae639dac0d Mon Sep 17 00:00:00 2001 From: jesseward Date: Sun, 10 Aug 2025 21:50:25 -0400 Subject: [PATCH 1/2] creating a common effects.go --- .gitignore | 1 + internal/player/effects.go | 195 +++++++++++++++++++++++++++ internal/player/player.go | 1 + internal/player/protracker_ticker.go | 109 ++------------- internal/player/s3m_ticker.go | 153 ++------------------- 5 files changed, 223 insertions(+), 236 deletions(-) create mode 100644 internal/player/effects.go diff --git a/.gitignore b/.gitignore index f8913c9..9afe986 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ vendor/ dist/ impulse.log +.env diff --git a/internal/player/effects.go b/internal/player/effects.go new file mode 100644 index 0000000..33521ec --- /dev/null +++ b/internal/player/effects.go @@ -0,0 +1,195 @@ +package player + +// PeriodGetter is an interface for getting a period value for a note. +type PeriodGetter interface { + GetPeriod(basePeriod uint16, semitoneOffset int) uint16 +} + +// sin_table is a 32-entry sine table for vibrato and tremolo effects. +var sin_table = [32]float64{ + 0, 24, 49, 74, 97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253, + 255, 253, 250, 244, 235, 224, 212, 197, 180, 161, 141, 120, 97, 74, 49, 24, +} + +// applyArpeggio applies the arpeggio effect to a channel's state. +func applyArpeggio(state *channelState, param byte, tick int, getter PeriodGetter) { + if tick == 0 { + return + } + + basePeriod := state.notePeriod + x := int(param >> 4) + y := int(param & 0x0F) + + switch tick % 3 { + case 0: + state.period = basePeriod + case 1: + state.period = getter.GetPeriod(basePeriod, x) + case 2: + state.period = getter.GetPeriod(basePeriod, y) + } +} + +// applyVibrato applies the vibrato effect to a channel's state. +func applyVibrato(state *channelState) { + if state.vibratoDepth == 0 { + return + } + var delta float64 + pos := state.vibratoPos + wave := state.vibratoWave & 3 + switch wave { + case 0: // Sine + delta = sin_table[pos&31] + if pos >= 32 { + delta = -delta + } + case 1: // Ramp down (sawtooth) + delta = float64(255 - (pos * 4)) + case 2: // Square + if pos < 32 { + delta = 255 + } else { + delta = -255 + } + } + delta = delta * float64(state.vibratoDepth) / 128.0 + state.period += uint16(delta) + state.vibratoPos = (state.vibratoPos + state.vibratoSpeed) & 63 +} + +// applyTremolo applies the tremolo effect to a channel's state. +func applyTremolo(state *channelState) { + if state.tremoloDepth == 0 { + return + } + var delta float64 + pos := state.tremoloPos + wave := state.tremoloWave & 3 + switch wave { + case 0: // Sine + delta = sin_table[pos&31] + if pos >= 32 { + delta = -delta + } + case 1: // Ramp down (sawtooth) + delta = float64(255 - (pos * 4)) + case 2: // Square + if pos < 32 { + delta = 255 + } else { + delta = -255 + } + } + delta = delta * float64(state.tremoloDepth) / 64.0 + state.volume += delta / 64.0 + if state.volume < 0 { + state.volume = 0 + } + if state.volume > 1.0 { + state.volume = 1.0 + } + state.tremoloPos = (state.tremoloPos + state.tremoloSpeed) & 63 +} + +// applyVolumeSlide applies the volume slide effect to a channel's state. +func applyVolumeSlide(state *channelState, param byte, tick int, isS3M bool) { + if param > 0 { + state.lastVolSlide = param + } else { + param = state.lastVolSlide + } + + x := param >> 4 + y := param & 0x0F + + if isS3M { + if y == 0xF && x > 0 { // Fine slide up + if tick == 0 { + state.volume += float64(x) / 64.0 + } + return + } else if x == 0xF && y > 0 { // Fine slide down + if tick == 0 { + state.volume -= float64(y) / 64.0 + } + return + } + } + + if tick > 0 { + if x > 0 { + state.volume += float64(x) / 64.0 + } else { + state.volume -= float64(y) / 64.0 + } + } + + if state.volume > 1.0 { + state.volume = 1.0 + } + if state.volume < 0 { + state.volume = 0 + } +} + +// applyPortamentoUp applies the portamento up effect to a channel's state. +func applyPortamentoUp(state *channelState, param byte, tick int, isS3M bool) { + if tick == 0 { + if isS3M { + x := param >> 4 + y := param & 0x0F + if x == 0xE { // Extra fine + state.period -= uint16(y) + } else if x == 0xF { // Fine + state.period -= uint16(y) * 4 + } + } + return + } + + var speed uint16 + if isS3M { + if param > 0 { + state.lastPorta = param + } + speed = uint16(state.lastPorta) * 4 + } else { + if param > 0 { + state.lastPortaUp = param + } + speed = uint16(state.lastPortaUp) + } + state.period -= speed +} + +// applyPortamentoDown applies the portamento down effect to a channel's state. +func applyPortamentoDown(state *channelState, param byte, tick int, isS3M bool) { + if tick == 0 { + if isS3M { + x := param >> 4 + y := param & 0x0F + if x == 0xE { // Extra fine + state.period += uint16(y) + } else if x == 0xF { // Fine + state.period += uint16(y) * 4 + } + } + return + } + + var speed uint16 + if isS3M { + if param > 0 { + state.lastPorta = param + } + speed = uint16(state.lastPorta) * 4 + } else { + if param > 0 { + state.lastPortaDown = param + } + speed = uint16(state.lastPortaDown) + } + state.period += speed +} diff --git a/internal/player/player.go b/internal/player/player.go index 2c28bb3..7c384ca 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -223,6 +223,7 @@ type channelState struct { sample module.Sample sampleIndex int period uint16 + notePeriod uint16 samplePos float64 volume float64 portaTarget uint16 diff --git a/internal/player/protracker_ticker.go b/internal/player/protracker_ticker.go index 55d0751..983c109 100644 --- a/internal/player/protracker_ticker.go +++ b/internal/player/protracker_ticker.go @@ -21,10 +21,7 @@ var periodTable = [16 * 36]uint16{ 862, 814, 768, 725, 684, 646, 610, 575, 543, 513, 484, 457, 431, 407, 384, 363, 342, 323, 305, 288, 272, 256, 242, 228, 216, 203, 192, 181, 171, 161, 152, 144, 136, 128, 121, 114, } -var sin_table = [32]float64{ - 0, 24, 49, 74, 97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253, - 255, 253, 250, 244, 235, 224, 212, 197, 180, 161, 141, 120, 97, 74, 49, 24, -} + type ProtrackerTicker struct{} @@ -49,6 +46,7 @@ func (t *ProtrackerTicker) handleTickZero(p *Player, cell *module.Cell, state *c state.portaTarget = cell.Period } else { state.period = cell.Period + state.notePeriod = cell.Period } } } @@ -59,37 +57,20 @@ func (t *ProtrackerTicker) handleEffect(p *Player, state *channelState, cell *mo switch effect { // Arpeggio alternates between the base note and two other notes, creating a chord-like effect. case 0x00: // Arpeggio - if val > 0 && tick > 0 { - arp_note := state.period - switch tick % 3 { - case 1: - arp_note = t.getPeriod(state.period, int(val>>4)) - case 2: - arp_note = t.getPeriod(state.period, int(val&0x0F)) - } - state.period = arp_note + if val > 0 { + applyArpeggio(state, val, tick, t) } // Porta Up slides the pitch of the note up. case 0x01: // Porta Up - if tick > 0 { - if val > 0 { - state.lastPortaUp = val - } - state.period -= uint16(state.lastPortaUp) - if state.period < 113 { - state.period = 113 - } + applyPortamentoUp(state, val, tick, false) + if state.period < 113 { + state.period = 113 } // Porta Down slides the pitch of the note down. case 0x02: // Porta Down - if tick > 0 { - if val > 0 { - state.lastPortaDown = val - } - state.period += uint16(state.lastPortaDown) - if state.period > 856 { - state.period = 856 - } + applyPortamentoDown(state, val, tick, false) + if state.period > 856 { + state.period = 856 } // Tone Portamento slides the pitch from the previous note to the new note. case 0x03: // Tone Portamento @@ -107,7 +88,7 @@ func (t *ProtrackerTicker) handleEffect(p *Player, state *channelState, cell *mo if val&0x0F > 0 { state.vibratoDepth = val & 0x0F } - t.applyVibrato(state) + applyVibrato(state) // Tone Portamento + Volume Slide combines a tone portamento with a volume slide. case 0x05: // Tone Portamento + Volume Slide t.handleEffect(p, state, &module.Cell{Effect: 0x03}, speed, bpm, nextRow, nextOrder, currentOrder, tick, playerState) @@ -124,7 +105,7 @@ func (t *ProtrackerTicker) handleEffect(p *Player, state *channelState, cell *mo if val&0x0F > 0 { state.tremoloDepth = val & 0x0F } - t.applyTremolo(state) + applyTremolo(state) // Set Panning sets the stereo panning of the channel. case 0x08: // Set Panning state.panning = float64(val) / 255.0 @@ -136,21 +117,7 @@ func (t *ProtrackerTicker) handleEffect(p *Player, state *channelState, cell *mo state.samplePos = float64(state.lastSampleOffset * 256) // Volume Slide slides the volume up or down. case 0x0A: // Volume Slide - if tick > 0 { - if val>>4 > 0 { - state.lastVolSlide = val >> 4 - state.volume += float64(state.lastVolSlide) / 64.0 - } else { - state.lastVolSlide = val & 0x0F - state.volume -= float64(state.lastVolSlide) / 64.0 - } - if state.volume < 0 { - state.volume = 0 - } - if state.volume > 1 { - state.volume = 1 - } - } + applyVolumeSlide(state, val, tick, false) // Position Jump jumps to a specific pattern in the order list. case 0x0B: // Position Jump *nextOrder = int(val) @@ -252,57 +219,9 @@ func (t *ProtrackerTicker) handleExtendedEffect(state *channelState, command, va } } -func (t *ProtrackerTicker) applyVibrato(state *channelState) { - var delta float64 - switch state.vibratoWave & 3 { - case 0: // Sine - delta = sin_table[state.vibratoPos&31] - if state.vibratoPos >= 32 { - delta = -delta - } - case 1: // Ramp down - delta = float64(255 - (state.vibratoPos * 8)) - case 2: // Square - if state.vibratoPos < 32 { - delta = 255 - } else { - delta = -255 - } - } - delta = delta * float64(state.vibratoDepth) / 128.0 - state.period += uint16(delta) - state.vibratoPos = (state.vibratoPos + state.vibratoSpeed) & 63 -} -func (t *ProtrackerTicker) applyTremolo(state *channelState) { - var delta float64 - switch state.tremoloWave & 3 { - case 0: // Sine - delta = sin_table[state.tremoloPos&31] - if state.tremoloPos >= 32 { - delta = -delta - } - case 1: // Ramp down - delta = float64(255 - (state.tremoloPos * 8)) - case 2: // Square - if state.vibratoPos < 32 { - delta = 255 - } else { - delta = -255 - } - } - delta = delta * float64(state.tremoloDepth) / 64.0 - state.volume += delta / 64.0 - if state.volume < 0 { - state.volume = 0 - } - if state.volume > 1.0 { - state.volume = 1.0 - } - state.tremoloPos = (state.tremoloPos + state.tremoloSpeed) & 63 -} -func (t *ProtrackerTicker) getPeriod(period uint16, offset int) uint16 { +func (t *ProtrackerTicker) GetPeriod(period uint16, offset int) uint16 { finetune := 0 note := 0 for i := range periodTable { diff --git a/internal/player/s3m_ticker.go b/internal/player/s3m_ticker.go index 26194f8..6d444f5 100644 --- a/internal/player/s3m_ticker.go +++ b/internal/player/s3m_ticker.go @@ -33,8 +33,8 @@ func (t *S3MTicker) ProcessTick(p *Player, playerState *playerState, channelStat } t.handleEffect(p, channelState, cell, speed, bpm, nextRow, nextOrder, currentOrder, tick, playerState) if tick > 0 { - t.applyVibrato(channelState) - t.applyTremolo(channelState) + applyVibrato(channelState) + applyTremolo(channelState) } } @@ -65,8 +65,10 @@ func (t *S3MTicker) handleTickZero(p *Player, cell *module.Cell, state *channelS c2spd = 8363 } state.period = uint16(float64(period) * 8363.0 / float64(c2spd)) + state.notePeriod = state.period } else { state.period = period + state.notePeriod = period } } } else if cell.Note == 254 { @@ -94,11 +96,11 @@ func (t *S3MTicker) handleEffect(p *Player, state *channelState, cell *module.Ce *nextRow = int(param>>4)*10 + int(param&0x0F) } case 4: // Dxy: Volume slide - t.volumeSlide(state, param, tick) + applyVolumeSlide(state, param, tick, true) case 5: // Exx: Portamento Down - t.portamentoDown(state, param, tick) + applyPortamentoDown(state, param, tick, true) case 6: // Fxx: Portamento Up - t.portamentoUp(state, param, tick) + applyPortamentoUp(state, param, tick, true) case 7: // Gxx: Tone portamento t.tonePortamento(state, param) case 8: // Hxy: Vibrato @@ -106,28 +108,13 @@ func (t *S3MTicker) handleEffect(p *Player, state *channelState, cell *module.Ce case 9: // Ixy: Tremor t.tremor(state, param, tick) case 10: // Jxy: Arpeggio - if tick > 0 { - x := param >> 4 - y := param & 0x0F - var arp_note uint16 - switch tick % 3 { - case 0: - arp_note = state.period - case 1: - arp_note = t.getPeriod(state.period, int(x)) - case 2: - arp_note = t.getPeriod(state.period, int(y)) - } - state.arpPeriod = arp_note - } else { - state.arpPeriod = 0 - } + applyArpeggio(state, param, tick, t) case 11: // Kxy: Vibrato + Volume slide t.vibrato(state, 0) - t.volumeSlide(state, param, tick) + applyVolumeSlide(state, param, tick, true) case 12: // Lxy: Porta + Volume slide t.tonePortamento(state, 0) - t.volumeSlide(state, param, tick) + applyVolumeSlide(state, param, tick, true) case 15: // Oxy: Set sample offset if tick == 0 { if param > 0 { @@ -138,7 +125,7 @@ func (t *S3MTicker) handleEffect(p *Player, state *channelState, cell *module.Ce case 17: // Qxy: Retrig + Volume slide if tick > 0 && tick%int(param&0x0F) == 0 { state.samplePos = 0 - t.volumeSlide(state, param>>4, 0) + applyVolumeSlide(state, param>>4, 0, true) } case 18: // Rxy: Tremolo t.tremolo(state, param) @@ -159,82 +146,9 @@ func (t *S3MTicker) handleEffect(p *Player, state *channelState, cell *module.Ce } } -func (t *S3MTicker) volumeSlide(state *channelState, param byte, tick int) { - if param > 0 { - state.lastVolSlide = param - } else { - param = state.lastVolSlide - } - x := param >> 4 - y := param & 0x0F - if y == 0xF && x > 0 { // Fine slide up - if tick == 0 { - state.volume += float64(x) / 64.0 - } - } else if x == 0xF && y > 0 { // Fine slide down - if tick == 0 { - state.volume -= float64(y) / 64.0 - } - } else if tick > 0 { - if x > 0 { - state.volume += float64(x) / 64.0 - } else { - state.volume -= float64(y) / 64.0 - } - } - if state.volume > 1.0 { - state.volume = 1.0 - } - if state.volume < 0 { - state.volume = 0 - } -} -func (t *S3MTicker) portamentoUp(state *channelState, param byte, tick int) { - if param > 0 { - state.lastPorta = param - } else { - param = state.lastPorta - } - x := param >> 4 - y := param & 0x0F - - if x == 0xE { // Extra fine - if tick == 0 { - state.period -= uint16(y) - } - } else if x == 0xF { // Fine - if tick == 0 { - state.period -= uint16(y) * 4 - } - } else if tick > 0 { - state.period -= uint16(param) * 4 - } -} - -func (t *S3MTicker) portamentoDown(state *channelState, param byte, tick int) { - if param > 0 { - state.lastPorta = param - } else { - param = state.lastPorta - } - x := param >> 4 - y := param & 0x0F - - if x == 0xE { // Extra fine - if tick == 0 { - state.period += uint16(y) - } - } else if x == 0xF { // Fine - if tick == 0 { - state.period += uint16(y) * 4 - } - } else if tick > 0 { - state.period += uint16(param) * 4 - } -} func (t *S3MTicker) tonePortamento(state *channelState, param byte) { if param > 0 { @@ -324,52 +238,9 @@ func (t *S3MTicker) specialEffect(state *channelState, param byte, nextRow *int, } } -func (t *S3MTicker) applyVibrato(state *channelState) { - var delta float64 - pos := state.vibratoPos & 31 - switch state.vibratoWave & 3 { - case 0: // Sine - delta = sin_table[pos] - case 1: // Ramp down - delta = float64(255 - pos*8) - case 2: // Square - delta = 255 - } - if state.vibratoPos >= 32 { - delta = -delta - } - - delta = delta * float64(state.vibratoDepth) / 128.0 - state.period += uint16(delta) - state.vibratoPos = (state.vibratoPos + state.vibratoSpeed) & 63 -} -func (t *S3MTicker) applyTremolo(state *channelState) { - var delta float64 - pos := state.tremoloPos & 31 - switch state.tremoloWave & 3 { - case 0: // Sine - delta = sin_table[pos] - case 1: // Ramp down - delta = float64(255 - pos*8) - case 2: // Square - delta = 255 - } - if state.tremoloPos >= 32 { - delta = -delta - } - delta = delta * float64(state.tremoloDepth) / 64.0 - state.volume += delta / 64.0 - if state.volume < 0 { - state.volume = 0 - } - if state.volume > 1.0 { - state.volume = 1.0 - } - state.tremoloPos = (state.tremoloPos + state.tremoloSpeed) & 63 -} -func (t *S3MTicker) getPeriod(period uint16, offset int) uint16 { +func (t *S3MTicker) GetPeriod(period uint16, offset int) uint16 { // this is not correct. // find the note from the period var note, octave int From 8ffc2e4a96d2f17f8191f57c87658085232f353b Mon Sep 17 00:00:00 2001 From: jesseward Date: Mon, 11 Aug 2025 09:37:56 -0400 Subject: [PATCH 2/2] Adding notecut , sampleoffset to effects.go --- README.md | 3 ++- internal/player/effects.go | 15 +++++++++++++++ internal/player/protracker_ticker.go | 9 ++------- internal/player/s3m_ticker.go | 9 ++------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3ab8b8e..75bbf41 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,5 @@ The project is written in Go. ## References -* XM libary in C https://github.com/Artefact2/libxm \ No newline at end of file +* XM libary in C https://github.com/Artefact2/libxm +* Effect comparision between formats https://wiki.openmpt.org/Manual:_Effect_Reference diff --git a/internal/player/effects.go b/internal/player/effects.go index 33521ec..8f67278 100644 --- a/internal/player/effects.go +++ b/internal/player/effects.go @@ -193,3 +193,18 @@ func applyPortamentoDown(state *channelState, param byte, tick int, isS3M bool) } state.period += speed } + +// applyNoteCut cuts the note after a specified number of ticks. +func applyNoteCut(state *channelState, param byte, tick int) { + if tick == int(param) { + state.volume = 0 + } +} + +// handleSampleOffset handles the set sample offset effect. +func handleSampleOffset(state *channelState, param byte) { + if param > 0 { + state.lastSampleOffset = uint16(param) + } + state.samplePos = float64(state.lastSampleOffset * 256) +} diff --git a/internal/player/protracker_ticker.go b/internal/player/protracker_ticker.go index 983c109..f287487 100644 --- a/internal/player/protracker_ticker.go +++ b/internal/player/protracker_ticker.go @@ -111,10 +111,7 @@ func (t *ProtrackerTicker) handleEffect(p *Player, state *channelState, cell *mo state.panning = float64(val) / 255.0 // Set Sample Offset starts the sample from a specific offset. case 0x09: // Set Sample Offset - if val > 0 { - state.lastSampleOffset = uint16(val) - } - state.samplePos = float64(state.lastSampleOffset * 256) + handleSampleOffset(state, val) // Volume Slide slides the volume up or down. case 0x0A: // Volume Slide applyVolumeSlide(state, val, tick, false) @@ -205,9 +202,7 @@ func (t *ProtrackerTicker) handleExtendedEffect(state *channelState, command, va } // Note Cut cuts the note after a specified number of ticks. case 0x0C: // Note Cut - if tick == int(value) { - state.volume = 0 - } + applyNoteCut(state, value, tick) // Note Delay delays the start of the note by a specified number of ticks. case 0x0D: // Note Delay if tick < int(value) { diff --git a/internal/player/s3m_ticker.go b/internal/player/s3m_ticker.go index 6d444f5..5646662 100644 --- a/internal/player/s3m_ticker.go +++ b/internal/player/s3m_ticker.go @@ -117,10 +117,7 @@ func (t *S3MTicker) handleEffect(p *Player, state *channelState, cell *module.Ce applyVolumeSlide(state, param, tick, true) case 15: // Oxy: Set sample offset if tick == 0 { - if param > 0 { - state.lastSampleOffset = uint16(param) - } - state.samplePos = float64(state.lastSampleOffset * 256) + handleSampleOffset(state, param) } case 17: // Qxy: Retrig + Volume slide if tick > 0 && tick%int(param&0x0F) == 0 { @@ -222,9 +219,7 @@ func (t *S3MTicker) specialEffect(state *channelState, param byte, nextRow *int, } } case 0xC: // SCx: Note Cut - if tick == int(val) { - state.volume = 0 - } + applyNoteCut(state, val, tick) case 0xD: // SDx: Note Delay if tick < int(val) { // requires restructuring note handling