fix(vm): avoid NaNs in trisaw oscillator

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2026-02-28 16:41:39 +02:00
parent 92859a5e58
commit 4d29a191c8
3 changed files with 19 additions and 13 deletions

View File

@ -40,7 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- VSTi queries the host sample rate more robustly. Cubase previously reported - VSTi queries the host sample rate more robustly. Cubase previously reported
the sample rate as 0 Hz, leading to persistent error message about the sample the sample rate as 0 Hz, leading to persistent error message about the sample
rate not being 44100 Hz. ([#222][i222]) rate not being 44100 Hz. ([#222][i222])
- Occasional NaNs in the Trisaw oscillator when the color was 0 in the Go VM. - Occasional NaNs in the Trisaw oscillator when color was = 0 or color = 128
- The tracker thought that "sync" unit pops the value from stack, even if the VM - The tracker thought that "sync" unit pops the value from stack, even if the VM
did not, resulting it claiming errors in patches that worked once compiled. did not, resulting it claiming errors in patches that worked once compiled.

View File

@ -269,7 +269,10 @@ su_oscillat_pulse_up:
{{- if .HasCall "su_oscillat_trisaw"}} {{- if .HasCall "su_oscillat_trisaw"}}
{{.Func "su_oscillat_trisaw"}} {{.Func "su_oscillat_trisaw"}}
fucomi st1 ; // c p fucomi st1 ; // c p
jnc short su_oscillat_trisaw_up ; we do jnbe instead of jnc to avoid NaN. phase cannot be < 0 or >= 1, so the important cases are
; c = 0 => the branch is never taken, because c > p can never happen, and thus results in .../(1-c)
; c = 1 => the branch is always taken, because p < c and thus always gives .../c
jnbe short su_oscillat_trisaw_up
fld1 ; // 1 c p fld1 ; // 1 c p
fsubr st2, st0 ; // 1 c 1-p fsubr st2, st0 ; // 1 c 1-p
fsubrp st1, st0 ; // 1-c 1-p fsubrp st1, st0 ; // 1-c 1-p

View File

@ -481,10 +481,10 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, r
} }
omega += float64(unit.ports[6]) // add frequency modulation omega += float64(unit.ports[6]) // add frequency modulation
var amplitude float32 var amplitude float32
*statevar += float32(omega) phase := float64(*statevar) + omega
if flags&0x80 == 0x80 { // if this is a sample oscillator if flags&0x80 == 0x80 { // if this is a sample oscillator
phase := *statevar *statevar = float32(phase)
phase += params[2] phase += float64(params[2])
sampleno := operandsAtTransform[3] // reuse color as the sample number sampleno := operandsAtTransform[3] // reuse color as the sample number
sampleoffset := s.bytecode.SampleOffsets[sampleno] sampleoffset := s.bytecode.SampleOffsets[sampleno]
sampleindex := int(phase*84.28074964676522 + 0.5) sampleindex := int(phase*84.28074964676522 + 0.5)
@ -497,22 +497,25 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, r
sampleindex += int(sampleoffset.Start) sampleindex += int(sampleoffset.Start)
amplitude = float32(int16(binary.LittleEndian.Uint16(su_sample_table[sampleindex*2:]))) / 32767.0 amplitude = float32(int16(binary.LittleEndian.Uint16(su_sample_table[sampleindex*2:]))) / 32767.0
} else { } else {
*statevar -= float32(int(*statevar+1) - 1) // at this point, the native synth actually uses 80-bit precision, so emulate that as closely as possible by using 64-bit math here
phase := *statevar phase += 1
phase += params[2] phase -= float64(int(phase))
phase -= float32(int(phase+1) - 1) *statevar = float32(phase)
color := params[3] phase += float64(params[2])
phase += 1
phase -= float64(int(phase)) // this should guaranteee that phase is [0,1), so that the Trisaw should not nan even if color = 1
color := float64(params[3])
switch { switch {
case flags&0x40 == 0x40: // Sine case flags&0x40 == 0x40: // Sine
if phase < color { if phase < color {
amplitude = float32(math.Sin(2 * math.Pi * float64(phase/color))) amplitude = float32(math.Sin(2 * math.Pi * phase / color))
} }
case flags&0x20 == 0x20: // Trisaw case flags&0x20 == 0x20: // Trisaw
if phase >= color { if phase >= color { // since phase cannot be 1, if color = 1, then this condition never fires
phase = 1 - phase phase = 1 - phase
color = 1 - color color = 1 - color
} }
amplitude = phase/color*2 - 1 amplitude = float32(phase/color*2 - 1)
case flags&0x10 == 0x10: // Pulse case flags&0x10 == 0x10: // Pulse
if phase >= color { if phase >= color {
amplitude = -1 amplitude = -1