From 4d29a191c84d3ff87663913877c5e65166e92ff4 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:41:39 +0200 Subject: [PATCH] fix(vm): avoid NaNs in trisaw oscillator --- CHANGELOG.md | 2 +- vm/compiler/templates/amd64-386/sources.asm | 5 ++++- vm/go_synth.go | 25 ++++++++++++--------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bd163..fff1209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 the sample rate as 0 Hz, leading to persistent error message about the sample 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 did not, resulting it claiming errors in patches that worked once compiled. diff --git a/vm/compiler/templates/amd64-386/sources.asm b/vm/compiler/templates/amd64-386/sources.asm index 558c46c..7e6efda 100644 --- a/vm/compiler/templates/amd64-386/sources.asm +++ b/vm/compiler/templates/amd64-386/sources.asm @@ -269,7 +269,10 @@ su_oscillat_pulse_up: {{- if .HasCall "su_oscillat_trisaw"}} {{.Func "su_oscillat_trisaw"}} 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 fsubr st2, st0 ; // 1 c 1-p fsubrp st1, st0 ; // 1-c 1-p diff --git a/vm/go_synth.go b/vm/go_synth.go index 2391dd4..262d8ed 100644 --- a/vm/go_synth.go +++ b/vm/go_synth.go @@ -481,10 +481,10 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, r } omega += float64(unit.ports[6]) // add frequency modulation var amplitude float32 - *statevar += float32(omega) + phase := float64(*statevar) + omega if flags&0x80 == 0x80 { // if this is a sample oscillator - phase := *statevar - phase += params[2] + *statevar = float32(phase) + phase += float64(params[2]) sampleno := operandsAtTransform[3] // reuse color as the sample number sampleoffset := s.bytecode.SampleOffsets[sampleno] 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) amplitude = float32(int16(binary.LittleEndian.Uint16(su_sample_table[sampleindex*2:]))) / 32767.0 } else { - *statevar -= float32(int(*statevar+1) - 1) - phase := *statevar - phase += params[2] - phase -= float32(int(phase+1) - 1) - color := params[3] + // 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 += 1 + phase -= float64(int(phase)) + *statevar = float32(phase) + 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 { case flags&0x40 == 0x40: // Sine 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 - if phase >= color { + if phase >= color { // since phase cannot be 1, if color = 1, then this condition never fires phase = 1 - phase color = 1 - color } - amplitude = phase/color*2 - 1 + amplitude = float32(phase/color*2 - 1) case flags&0x10 == 0x10: // Pulse if phase >= color { amplitude = -1