mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-27 10:50:23 -04:00
There is a new "sync" opcode that saves the top-most signal every 256 samples to the new "syncBuffer" output. Additionally, you can enable saving the current fractional row as sync[0], avoiding calculating the beat in the shader, but also calculating the beat correctly when the beat is modulated.
588 lines
16 KiB
Go
588 lines
16 KiB
Go
package vm
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/vsariola/sointu"
|
|
)
|
|
|
|
//go:generate go run generate/generate.go
|
|
|
|
// Interpreter 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 Interpreter.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 Interpreter struct {
|
|
bytePatch BytePatch
|
|
stack []float32
|
|
synth synth
|
|
delaylines []delayline
|
|
}
|
|
|
|
type SynthService struct {
|
|
}
|
|
|
|
const MAX_VOICES = 32
|
|
const MAX_UNITS = 63
|
|
|
|
type unit struct {
|
|
state [8]float32
|
|
ports [8]float32
|
|
}
|
|
|
|
type voice struct {
|
|
note byte
|
|
release 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
|
|
)
|
|
|
|
func Synth(patch sointu.Patch) (sointu.Synth, error) {
|
|
bytePatch, err := Encode(patch, AllFeatures{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error compiling %v", err)
|
|
}
|
|
ret := &Interpreter{bytePatch: *bytePatch, stack: make([]float32, 0, 4), delaylines: make([]delayline, patch.NumDelayLines())}
|
|
ret.synth.randSeed = 1
|
|
return ret, nil
|
|
}
|
|
|
|
func (s SynthService) Compile(patch sointu.Patch) (sointu.Synth, error) {
|
|
synth, err := Synth(patch)
|
|
return synth, err
|
|
}
|
|
|
|
func (s *Interpreter) Trigger(voiceIndex int, note byte) {
|
|
s.synth.voices[voiceIndex] = voice{}
|
|
s.synth.voices[voiceIndex].note = note
|
|
}
|
|
|
|
func (s *Interpreter) Release(voiceIndex int) {
|
|
s.synth.voices[voiceIndex].release = true
|
|
}
|
|
|
|
func (s *Interpreter) Update(patch sointu.Patch) error {
|
|
bytePatch, err := Encode(patch, AllFeatures{})
|
|
if err != nil {
|
|
return fmt.Errorf("error compiling %v", err)
|
|
}
|
|
needsRefresh := len(bytePatch.Commands) != len(s.bytePatch.Commands)
|
|
if !needsRefresh {
|
|
for i, c := range bytePatch.Commands {
|
|
if s.bytePatch.Commands[i] != c {
|
|
needsRefresh = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
s.bytePatch = *bytePatch
|
|
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 *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (samples int, syncs 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) > 1 {
|
|
commandInstr := s.bytePatch.Commands
|
|
valuesInstr := s.bytePatch.Values
|
|
commands, values := commandInstr, valuesInstr
|
|
delaylines := s.delaylines
|
|
voicesRemaining := s.bytePatch.NumVoices
|
|
voices := s.synth.voices[:]
|
|
units := voices[0].units[:]
|
|
if byte(s.synth.globalTime) == 0 { // every 256 samples
|
|
syncBuf[0], syncBuf = float32(time), syncBuf[1:]
|
|
syncs++
|
|
}
|
|
for voicesRemaining > 0 {
|
|
op := commands[0]
|
|
commands = commands[1:]
|
|
channels := int((op & 1) + 1)
|
|
stereo := channels == 2
|
|
opNoStereo := (op & 0xFE) >> 1
|
|
if opNoStereo == 0 {
|
|
voices = voices[1:]
|
|
units = voices[0].units[:]
|
|
voicesRemaining--
|
|
if mask := uint32(1) << uint32(voicesRemaining); s.bytePatch.PolyphonyBitmask&mask == mask {
|
|
commands, values = commandInstr, valuesInstr
|
|
} else {
|
|
commandInstr, valuesInstr = commands, values
|
|
}
|
|
continue
|
|
}
|
|
tcount := transformCounts[opNoStereo-1]
|
|
if len(values) < tcount {
|
|
return samples, syncs, time, errors.New("value stream ended prematurely")
|
|
}
|
|
voice := &voices[0]
|
|
unit := &units[0]
|
|
valuesAtTransform := values
|
|
for i := 0; i < tcount; i++ {
|
|
params[i] = float32(values[0])/128.0 + unit.ports[i]
|
|
unit.ports[i] = 0
|
|
values = values[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, values = values[0], values[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, values = values[0], values[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].release {
|
|
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, values = values[0], values[1], values[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, values = values[0], values[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, values = values[0], values[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
|
|
}
|
|
*statevar += float32(omega)
|
|
*statevar -= float32(int(*statevar+1) - 1)
|
|
phase := *statevar
|
|
phase += params[2]
|
|
phase -= float32(int(phase))
|
|
color := params[3]
|
|
var amplitude float32
|
|
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 := valuesAtTransform[3], valuesAtTransform[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
|
|
}
|
|
case opDelay:
|
|
pregain2 := params[0] * params[0]
|
|
damp := params[3]
|
|
feedback := params[2]
|
|
var index, count byte
|
|
index, count, values = values[0], values[1], values[2:]
|
|
t := uint16(s.synth.globalTime)
|
|
for i := 0; i < channels; i++ {
|
|
var d *delayline
|
|
signal := stack[l-1-i]
|
|
output := params[1] * signal // dry output
|
|
for j := byte(0); j < count; j += 2 {
|
|
d, delaylines = &delaylines[0], delaylines[1:]
|
|
delay := float32(s.bytePatch.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[l-1-i] = d.dcFiltState
|
|
}
|
|
unit.ports[4] = 0
|
|
case opCompressor:
|
|
stack[l-1] /= params[2] // apply inverse gain
|
|
signalLevel := stack[l-1] * stack[l-1] // square the signal to get power
|
|
if stereo {
|
|
stack[l-2] /= params[2] // apply inverse gain
|
|
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)))
|
|
}
|
|
stack = append(stack, gain)
|
|
if stereo {
|
|
stack = append(stack, gain)
|
|
}
|
|
case opSync:
|
|
if byte(s.synth.globalTime) == 0 { // every 256 samples
|
|
syncBuf[0], syncBuf = float32(stack[l-1]), syncBuf[1:]
|
|
}
|
|
default:
|
|
return samples, syncs, time, errors.New("invalid / unimplemented opcode")
|
|
}
|
|
units = units[1:]
|
|
}
|
|
if len(stack) < 4 {
|
|
return samples, syncs, time, errors.New("stack underflow")
|
|
}
|
|
if len(stack) > 4 {
|
|
return samples, syncs, time, errors.New("stack not empty")
|
|
}
|
|
buffer[0] = synth.outputs[0]
|
|
buffer[1] = synth.outputs[1]
|
|
synth.outputs[0] = 0
|
|
synth.outputs[1] = 0
|
|
buffer = buffer[2:]
|
|
samples++
|
|
time++
|
|
s.synth.globalTime++
|
|
}
|
|
s.stack = stack[:0]
|
|
return samples, syncs, 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 {
|
|
return float32(math.Round(float64(value/amount)) * float64(amount))
|
|
}
|
|
|
|
func waveshape(value, amount float32) float32 {
|
|
absVal := value
|
|
if absVal < 0 {
|
|
absVal = -absVal
|
|
}
|
|
return value * amount / (1 - amount + (2*amount-1)*absVal)
|
|
}
|