sointu/vm/go_synth.go
5684185+vsariola@users.noreply.github.com 04fbc9f6a7 feat(vm): add dbgain unit, where gain is defined in decibels
Closes #78
2023-10-23 21:57:29 +03:00

635 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
type (
// 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.
GoSynth struct {
bytecode Bytecode
stack []float32
state synthState
delaylines []delayline
}
// GoSynther is a Synther implementation that can converts patches into
// GoSynths.
GoSynther struct {
}
)
const MAX_VOICES = 32
const MAX_UNITS = 63
type (
unit struct {
state [8]float32
ports [8]float32
}
voice struct {
note byte
sustain bool
units [MAX_UNITS]unit
}
synthState struct {
outputs [8]float32
randSeed uint32
globalTime uint32
voices [MAX_VOICES]voice
}
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 (s GoSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
bytecode, err := NewBytecode(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.state.randSeed = 1
return ret, nil
}
func (s *GoSynth) Trigger(voiceIndex int, note byte) {
s.state.voices[voiceIndex] = voice{}
s.state.voices[voiceIndex].note = note
s.state.voices[voiceIndex].sustain = true
}
func (s *GoSynth) Release(voiceIndex int) {
s.state.voices[voiceIndex].sustain = false
}
func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
bytecode, err := NewBytecode(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.state.voices {
for j := range s.state.voices[i].units {
s.state.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.state
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.state.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 opDbgain:
gain := float32(math.Pow(2, float64(params[0]*2-1)*6.643856189774724))
if stereo {
stack[l-2] *= gain
}
stack[l-1] *= gain
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.state.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.state.globalTime++
}
s.stack = stack[:0]
return samples, time, nil
}
func (s *synthState) 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)
}