mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
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.
628 lines
17 KiB
Go
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)
|
|
}
|