sointu/vm/go_synth.go
5684185+vsariola@users.noreply.github.com 01bf409929 refactor(vm): rename Commands/Values to Opcodes/Operands
The commands and values were not very good names to what the
byte sequences actually are: opcodes and their operands. In
many other places, we were already calling the byte in the Command
stream as Opcode, so a logical name for a sequence of these is
Opcodes. Values is such a generic name that it's not immediately
clear that this sequence is related to the opcodes. Operands is not
perfect but clearly suggests that this sequence is related to
the Opcodes.
2023-10-18 19:53:47 +03:00

628 lines
17 KiB
Go

package vm
import (
"encoding/binary"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"github.com/vsariola/sointu"
)
//go:generate go run generate/generate.go
// GoSynth is a pure-Go bytecode interpreter for the Sointu VM bytecode. It
// can only simulate bytecode compiled for AllFeatures, as the opcodes hard
// coded in it for speed. If you are interested exactly how opcodes / units
// work, studying GoSynth.Render is a good place to start.
//
// Internally, it uses software stack with practically no limitations in the
// number of signals, so be warned that if you compose patches for it, they
// might not work with the x87 implementation, as it has only 8-level stack.
type GoSynth struct {
bytecode Bytecode
stack []float32
synth synth
delaylines []delayline
}
type GoSynther struct {
}
const MAX_VOICES = 32
const MAX_UNITS = 63
type unit struct {
state [8]float32
ports [8]float32
}
type voice struct {
note byte
sustain bool
units [MAX_UNITS]unit
}
type synth struct {
outputs [8]float32
randSeed uint32
globalTime uint32
voices [MAX_VOICES]voice
}
type delayline struct {
buffer [65536]float32
dampState float32
dcIn float32
dcFiltState float32
}
const (
envStateAttack = iota
envStateDecay
envStateSustain
envStateRelease
)
var su_sample_table [3440660]byte
func init() {
var f *os.File
var err error
if f, err = os.Open("gm.dls"); err == nil { // try to open from current directory first
goto success
}
if f, err = os.Open(filepath.Join(os.Getenv("SystemRoot"), "system32", "drivers", "gm.dls")); err == nil {
goto success
}
if f, err = os.Open(filepath.Join(os.Getenv("SystemRoot"), "SysWOW64", "drivers", "gm.dls")); err == nil {
goto success
}
return
success:
defer f.Close()
// read file, ignoring errors
f.Read(su_sample_table[:])
}
func Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
bytecode, err := Encode(patch, AllFeatures{}, bpm)
if err != nil {
return nil, fmt.Errorf("error compiling %v", err)
}
ret := &GoSynth{bytecode: *bytecode, stack: make([]float32, 0, 4), delaylines: make([]delayline, patch.NumDelayLines())}
ret.synth.randSeed = 1
return ret, nil
}
func (s GoSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
synth, err := Synth(patch, bpm)
return synth, err
}
func (s *GoSynth) Trigger(voiceIndex int, note byte) {
s.synth.voices[voiceIndex] = voice{}
s.synth.voices[voiceIndex].note = note
s.synth.voices[voiceIndex].sustain = true
}
func (s *GoSynth) Release(voiceIndex int) {
s.synth.voices[voiceIndex].sustain = false
}
func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
bytecode, err := Encode(patch, AllFeatures{}, bpm)
if err != nil {
return fmt.Errorf("error compiling %v", err)
}
needsRefresh := len(bytecode.Opcodes) != len(s.bytecode.Opcodes)
if !needsRefresh {
for i, c := range bytecode.Opcodes {
if s.bytecode.Opcodes[i] != c {
needsRefresh = true
break
}
}
}
s.bytecode = *bytecode
for len(s.delaylines) < patch.NumDelayLines() {
s.delaylines = append(s.delaylines, delayline{})
}
if needsRefresh {
for i := range s.synth.voices {
for j := range s.synth.voices[i].units {
s.synth.voices[i].units[j] = unit{}
}
}
}
return nil
}
func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, time int, renderError error) {
defer func() {
if err := recover(); err != nil {
renderError = fmt.Errorf("render panicced: %v", err)
}
}()
var params [8]float32
stack := s.stack[:]
stack = append(stack, []float32{0, 0, 0, 0}...)
synth := &s.synth
for time < maxtime && len(buffer) > 0 {
opcodesInstr := s.bytecode.Opcodes
operandsInstr := s.bytecode.Operands
opcodes, operands := opcodesInstr, operandsInstr
delaylines := s.delaylines
voicesRemaining := s.bytecode.NumVoices
voices := s.synth.voices[:]
units := voices[0].units[:]
for voicesRemaining > 0 {
op := opcodes[0]
opcodes = opcodes[1:]
channels := int((op & 1) + 1)
stereo := channels == 2
opNoStereo := (op & 0xFE) >> 1
if opNoStereo == 0 {
voicesRemaining--
if voicesRemaining > 0 {
voices = voices[1:]
units = voices[0].units[:]
}
if mask := uint32(1) << uint32(voicesRemaining); s.bytecode.PolyphonyBitmask&mask == mask {
opcodes, operands = opcodesInstr, operandsInstr
} else {
opcodesInstr, operandsInstr = opcodes, operands
}
continue
}
tcount := transformCounts[opNoStereo-1]
if len(operands) < tcount {
return samples, time, errors.New("operand stream ended prematurely")
}
voice := &voices[0]
unit := &units[0]
operandsAtTransform := operands
for i := 0; i < tcount; i++ {
params[i] = float32(operands[0])/128.0 + unit.ports[i]
unit.ports[i] = 0
operands = operands[1:]
}
l := len(stack)
switch opNoStereo {
case opAdd:
if stereo {
stack[l-1] += stack[l-3]
stack[l-2] += stack[l-4]
} else {
stack[l-1] += stack[l-2]
}
case opAddp:
if stereo {
stack[l-3] += stack[l-1]
stack[l-4] += stack[l-2]
stack = stack[:l-2]
} else {
stack[l-2] += stack[l-1]
stack = stack[:l-1]
}
case opMul:
if stereo {
stack[l-1] *= stack[l-3]
stack[l-2] *= stack[l-4]
} else {
stack[l-1] *= stack[l-2]
}
case opMulp:
if stereo {
stack[l-3] *= stack[l-1]
stack[l-4] *= stack[l-2]
stack = stack[:l-2]
} else {
stack[l-2] *= stack[l-1]
stack = stack[:l-1]
}
case opXch:
if stereo {
stack[l-3], stack[l-1] = stack[l-1], stack[l-3]
stack[l-4], stack[l-2] = stack[l-2], stack[l-4]
} else {
stack[l-2], stack[l-1] = stack[l-1], stack[l-2]
}
case opPush:
if stereo {
stack = append(stack, stack[l-2])
}
stack = append(stack, stack[l-1])
case opPop:
if stereo {
stack = stack[:l-2]
} else {
stack = stack[:l-1]
}
case opDistort:
amount := params[0]
if stereo {
stack[l-2] = waveshape(stack[l-2], amount)
}
stack[l-1] = waveshape(stack[l-1], amount)
case opLoadval:
val := params[0]*2 - 1
if stereo {
stack = append(stack, val)
}
stack = append(stack, val)
case opOut:
if stereo {
synth.outputs[0] += params[0] * stack[l-1]
synth.outputs[1] += params[0] * stack[l-2]
stack = stack[:l-2]
} else {
synth.outputs[0] += params[0] * stack[l-1]
stack = stack[:l-1]
}
case opOutaux:
if stereo {
synth.outputs[0] += params[0] * stack[l-1]
synth.outputs[1] += params[0] * stack[l-2]
synth.outputs[2] += params[1] * stack[l-1]
synth.outputs[3] += params[1] * stack[l-2]
stack = stack[:l-2]
} else {
synth.outputs[0] += params[0] * stack[l-1]
synth.outputs[2] += params[1] * stack[l-1]
stack = stack[:l-1]
}
case opAux:
var channel byte
channel, operands = operands[0], operands[1:]
if stereo {
synth.outputs[channel+1] += params[0] * stack[l-2]
}
synth.outputs[channel] += params[0] * stack[l-1]
stack = stack[:l-channels]
case opSpeed:
r := unit.state[0] + float32(math.Exp2(float64(stack[l-1]*2.206896551724138))-1)
w := int(r+1.5) - 1
unit.state[0] = r - float32(w)
time += w
stack = stack[:l-1]
case opIn:
var channel byte
channel, operands = operands[0], operands[1:]
if stereo {
stack = append(stack, synth.outputs[channel+1])
synth.outputs[channel+1] = 0
}
stack = append(stack, synth.outputs[channel])
synth.outputs[channel] = 0
case opEnvelope:
if !voices[0].sustain {
unit.state[0] = envStateRelease // set state to release
}
state := unit.state[0]
level := unit.state[1]
switch state {
case envStateAttack:
level += nonLinearMap(params[0])
if level >= 1 {
level = 1
state = envStateDecay
}
case envStateDecay:
level -= nonLinearMap(params[1])
if sustain := params[2]; level <= sustain {
level = sustain
}
case envStateRelease:
level -= nonLinearMap(params[3])
if level <= 0 {
level = 0
}
}
unit.state[0] = state
unit.state[1] = level
output := level * params[4]
stack = append(stack, output)
if stereo {
stack = append(stack, output)
}
case opNoise:
if stereo {
value := waveshape(synth.rand(), params[0]) * params[1]
stack = append(stack, value)
}
value := waveshape(synth.rand(), params[0]) * params[1]
stack = append(stack, value)
case opGain:
if stereo {
stack[l-2] *= params[0]
}
stack[l-1] *= params[0]
case opInvgain:
if stereo {
stack[l-2] /= params[0]
}
stack[l-1] /= params[0]
case opClip:
if stereo {
stack[l-2] = clip(stack[l-2])
}
stack[l-1] = clip(stack[l-1])
case opCrush:
if stereo {
stack[l-2] = crush(stack[l-2], params[0])
}
stack[l-1] = crush(stack[l-1], params[0])
case opHold:
freq2 := params[0] * params[0]
for i := 0; i < channels; i++ {
phase := unit.state[i] - freq2
if phase <= 0 {
unit.state[2+i] = stack[l-1-i]
phase += 1.0
}
stack[l-1-i] = unit.state[2+i]
unit.state[i] = phase
}
case opSend:
var addrLow, addrHigh byte
addrLow, addrHigh, operands = operands[0], operands[1], operands[2:]
addr := (uint16(addrHigh) << 8) + uint16(addrLow)
targetVoice := voice
if addr&0x8000 == 0x8000 {
addr -= 0x8010
targetVoice = &synth.voices[addr>>10]
}
unitIndex := ((addr & 0x01F0) >> 4) - 1
port := addr & 7
amount := params[0]*2 - 1
for i := 0; i < channels; i++ {
targetVoice.units[unitIndex].ports[int(port)+i] += stack[l-1-i] * amount
}
if addr&0x8 == 0x8 {
stack = stack[:l-channels]
}
case opReceive:
if stereo {
stack = append(stack, unit.ports[1])
unit.ports[1] = 0
}
stack = append(stack, unit.ports[0])
unit.ports[0] = 0
case opLoadnote:
noteFloat := float32(voice.note)/64 - 1
stack = append(stack, noteFloat)
if stereo {
stack = append(stack, noteFloat)
}
case opPan:
if !stereo {
stack = append(stack, stack[l-1])
l++
}
stack[l-2] *= params[0]
stack[l-1] *= 1 - params[0]
case opFilter:
freq2 := params[0] * params[0]
res := params[1]
var flags byte
flags, operands = operands[0], operands[1:]
for i := 0; i < channels; i++ {
low, band := unit.state[0+i], unit.state[2+i]
low += freq2 * band
high := stack[l-1-i] - low - res*band
band += freq2 * high
unit.state[0+i], unit.state[2+i] = low, band
var output float32
if flags&0x40 == 0x40 {
output += low
}
if flags&0x20 == 0x20 {
output += band
}
if flags&0x10 == 0x10 {
output += high
}
if flags&0x08 == 0x08 {
output -= band
}
if flags&0x04 == 0x04 {
output -= high
}
stack[l-1-i] = output
}
case opOscillator:
var flags byte
flags, operands = operands[0], operands[1:]
detuneStereo := params[1]*2 - 1
unison := flags & 3
for i := 0; i < channels; i++ {
detune := detuneStereo
var output float32
for j := byte(0); j <= unison; j++ {
statevar := &unit.state[byte(i)+j*2]
pitch := float64(64*(params[0]*2-1) + detune)
if flags&0x8 == 0 { // if lfo is disable, add note to oscillator transpose
pitch += float64(voice.note)
}
pitch *= 0.083333333333 // from semitones to octaves
omega := math.Exp2(pitch)
if flags&0x8 == 0 {
omega *= 0.000092696138 // scaling coefficient to get middle-C where it should be
} else {
omega *= 0.000038 // pretty random scaling constant to get LFOs into reasonable range. Historical reasons, goes all the way back to 4klang
}
omega += float64(unit.ports[6]) // add frequency modulation
var amplitude float32
*statevar += float32(omega)
if flags&0x80 == 0x80 { // if this is a sample oscillator
phase := *statevar
phase += params[2]
sampleno := operandsAtTransform[3] // reuse color as the sample number
sampleoffset := s.bytecode.SampleOffsets[sampleno]
sampleindex := int(phase*84.28074964676522 + 0.5)
loopstart := int(sampleoffset.LoopStart)
if sampleindex >= loopstart {
sampleindex -= loopstart
sampleindex %= int(sampleoffset.LoopLength)
sampleindex += loopstart
}
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))
color := params[3]
switch {
case flags&0x40 == 0x40: // Sine
if phase < color {
amplitude = float32(math.Sin(2 * math.Pi * float64(phase/color)))
}
case flags&0x20 == 0x20: // Trisaw
if phase >= color {
phase = 1 - phase
color = 1 - color
}
amplitude = phase/color*2 - 1
case flags&0x10 == 0x10: // Pulse
if phase >= color {
amplitude = -1
} else {
amplitude = 1
}
case flags&0x4 == 0x4: // Gate
maskLow, maskHigh := operandsAtTransform[3], operandsAtTransform[4]
gateBits := (int(maskHigh) << 8) + int(maskLow)
amplitude = float32((gateBits >> (int(phase*16+.5) & 15)) & 1)
g := unit.state[4+i] // warning: still fucks up with unison = 3
amplitude += 0.99609375 * (g - amplitude)
unit.state[4+i] = amplitude
}
}
if flags&0x4 == 0 {
output += waveshape(amplitude, params[4]) * params[5]
} else {
output += amplitude * params[5]
}
if j < unison {
params[2] += 0.08333333 // 1/12, add small phase shift so all oscillators don't start in phase
}
detune = -detune * 0.5
}
stack = append(stack, output)
detuneStereo = -detuneStereo
}
unit.ports[6] = 0
case opDelay:
pregain2 := params[0] * params[0]
damp := params[3]
feedback := params[2]
var index, count byte
index, count, operands = operands[0], operands[1], operands[2:]
t := uint16(s.synth.globalTime)
stackIndex := l - channels
for i := 0; i < channels; i++ {
var d *delayline
signal := stack[stackIndex]
output := params[1] * signal // dry output
for j := byte(0); j < count; j += 2 {
d, delaylines = &delaylines[0], delaylines[1:]
delay := float32(s.bytecode.DelayTimes[index]) + unit.ports[4]*32767
if count&1 == 0 {
delay /= float32(math.Exp2(float64(voice.note) * 0.083333333333))
}
delSignal := d.buffer[t-uint16(delay+0.5)]
output += delSignal
d.dampState = damp*d.dampState + (1-damp)*delSignal
d.buffer[t] = feedback*d.dampState + pregain2*signal
index++
}
d.dcFiltState = output + (0.99609375*d.dcFiltState - d.dcIn)
d.dcIn = output
stack[stackIndex] = d.dcFiltState
stackIndex++
}
unit.ports[4] = 0
case opCompressor:
signalLevel := stack[l-1] * stack[l-1] // square the signal to get power
if stereo {
signalLevel += stack[l-2] * stack[l-2]
}
currentLevel := unit.state[0]
paramIndex := 0 // compressor attacking
if signalLevel < currentLevel {
paramIndex = 1 // compressor releasing
}
alpha := nonLinearMap(params[paramIndex]) // map attack or release to a smoothing coefficient
currentLevel += (signalLevel - currentLevel) * alpha
unit.state[0] = currentLevel
var gain float32 = 1
if threshold2 := params[3] * params[3]; currentLevel > threshold2 {
gain = float32(math.Pow(float64(threshold2/currentLevel), float64(params[4]/2)))
}
gain /= params[2] // apply inverse gain
stack = append(stack, gain)
if stereo {
stack = append(stack, gain)
}
case opSync:
break
default:
return samples, time, errors.New("invalid / unimplemented opcode")
}
units = units[1:]
}
if len(stack) < 4 {
return samples, time, errors.New("stack underflow")
}
if len(stack) > 4 {
return samples, time, errors.New("stack not empty")
}
buffer[0][0], buffer[0][1] = synth.outputs[0], synth.outputs[1]
synth.outputs[0] = 0
synth.outputs[1] = 0
buffer = buffer[1:]
samples++
time++
s.synth.globalTime++
}
s.stack = stack[:0]
return samples, time, nil
}
func (s *synth) rand() float32 {
s.randSeed *= 16007
return float32(int32(s.randSeed)) / -2147483648.0
}
func nonLinearMap(value float32) float32 {
return float32(math.Exp2(float64(-24 * value)))
}
func clip(value float32) float32 {
if value < -1 {
return -1
}
if value > 1 {
return 1
}
return value
}
func crush(value, amount float32) float32 {
n := nonLinearMap(amount)
return float32(math.Round(float64(value/n)) * float64(n))
}
func waveshape(value, amount float32) float32 {
absVal := value
if absVal < 0 {
absVal = -absVal
}
return value * amount / (1 - amount + (2*amount-1)*absVal)
}