feat(sointu, vm): implement pure-Go interpreter for bytecode

The old "native" compiler bridged version is now started with cmd/sointu-nativetrack,
while the new pure-Go bytecode implemented bytecode interpreter is started with
cmd/sointu-track

Thus, you do not need any of the CMake / cgo stuff to run cmd/sointu-track
This commit is contained in:
vsariola 2021-03-02 20:47:17 +02:00
parent a035845b81
commit 6d2b63a5e9
13 changed files with 891 additions and 58 deletions

View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker/gioui"
"github.com/vsariola/sointu/vm/compiler/bridge"
)
func main() {
audioContext, err := oto.NewContext()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer audioContext.Close()
synthService := bridge.BridgeService{}
gioui.Main(audioContext, synthService)
}

View File

@ -24,6 +24,7 @@ func main() {
help := flag.Bool("h", false, "Show help.")
directory := flag.String("o", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
play := flag.Bool("p", false, "Play the input songs (default behaviour when no other output is defined).")
unreleased := flag.Bool("u", false, "Start song with all oscillators unreleased.")
//start := flag.Float64("start", 0, "Start playing from part; given in the units defined by parameter `unit`.")
//stop := flag.Float64("stop", -1, "Stop playing at part; given in the units defined by parameter `unit`. Negative values indicate render until end.")
//units := flag.String("unit", "pattern", "Units for parameters start and stop. Possible values: second, sample, pattern, beat. Warning: beat and pattern do not take SPEED modulations into account.")
@ -86,6 +87,11 @@ func main() {
if err != nil {
return fmt.Errorf("could not create synth based on the patch: %v", err)
}
if !*unreleased {
for i := 0; i < 32; i++ {
synth.Release(i)
}
}
buffer, err := sointu.Play(synth, song) // render the song to calculate its length
if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err)

View File

@ -6,7 +6,7 @@ import (
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker/gioui"
"github.com/vsariola/sointu/vm/compiler/bridge"
"github.com/vsariola/sointu/vm"
)
func main() {
@ -16,6 +16,6 @@ func main() {
os.Exit(1)
}
defer audioContext.Close()
synthService := bridge.BridgeService{}
synthService := vm.SynthService{}
gioui.Main(audioContext, synthService)
}

View File

@ -25,6 +25,18 @@ func (p Patch) NumVoices() int {
return ret
}
func (p Patch) NumDelayLines() int {
total := 0
for _, instr := range p {
for _, unit := range instr.Units {
if unit.Type == "delay" {
total += len(unit.VarArgs)
}
}
}
return total
}
func (p Patch) FirstVoiceForInstrument(instrIndex int) int {
ret := 0
for _, t := range p[:instrIndex] {

28
song.go
View File

@ -28,33 +28,7 @@ func (s *Song) Validate() error {
if len(s.Score.Tracks) == 0 {
return errors.New("song contains no tracks")
}
var patternLen int
for i, t := range s.Score.Tracks {
for j, pat := range t.Patterns {
if i == 0 && j == 0 {
patternLen = len(pat)
} else {
if len(pat) != patternLen {
return errors.New("Every pattern should have the same length")
}
}
}
}
for i := range s.Score.Tracks[:len(s.Score.Tracks)-1] {
if len(s.Score.Tracks[i].Order) != len(s.Score.Tracks[i+1].Order) {
return errors.New("Every track should have the same sequence length")
}
}
totalTrackVoices := 0
for _, track := range s.Score.Tracks {
totalTrackVoices += track.NumVoices
for _, p := range track.Order {
if p < 0 || int(p) >= len(track.Patterns) {
return errors.New("Tracks use a non-existing pattern")
}
}
}
if totalTrackVoices > s.Patch.NumVoices() {
if s.Score.NumVoices() > s.Patch.NumVoices() {
return errors.New("Tracks use too many voices")
}
return nil

View File

@ -73,7 +73,10 @@ func Play(synth Synth, song Song) ([]float32, error) {
}
tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, _ := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
samples, time, err := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
if err != nil {
return buffer, fmt.Errorf("render failed: %v", err)
}
rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...)
if tries > 100 {

View File

@ -5,7 +5,7 @@
{{.SectBss "synth_object"}}
su_synth_obj:
resb su_synthworkspace.size
resb {{.NumDelayLines}}*su_delayline_wrk.size
resb {{.Song.Patch.NumDelayLines}}*su_delayline_wrk.size
;-------------------------------------------------------------------------------
; su_render_song function: the entry point for the synth

View File

@ -56,17 +56,6 @@ func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) {
}
unit.Parameters["color"] = index
}
if unit.Type == "delay" {
unit.Parameters["delay"] = len(c.DelayTimes)
if unit.Parameters["stereo"] == 1 {
unit.Parameters["count"] = len(unit.VarArgs) / 2
} else {
unit.Parameters["count"] = len(unit.VarArgs)
}
for _, v := range unit.VarArgs {
c.DelayTimes = append(c.DelayTimes, uint16(v))
}
}
opcode, ok := featureSet.Opcode(unit.Type)
if !ok {
return nil, fmt.Errorf(`the targeted virtual machine is not configured to support unit type "%v"`, unit.Type)
@ -156,8 +145,19 @@ func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) {
}
values = append(values, byte(addr&255), byte(addr>>8))
} else if unit.Type == "delay" {
countTrack := (unit.Parameters["count"] << 1) - 1 + unit.Parameters["notetracking"] // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
values = append(values, byte(unit.Parameters["delay"]), byte(countTrack))
count := len(unit.VarArgs)
if unit.Parameters["stereo"] == 1 {
count /= 2
}
if count == 0 {
continue // skip encoding delays without any delay lines
}
countTrack := count*2 - 1 + unit.Parameters["notetracking"] // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
delStart := len(c.DelayTimes)
for _, v := range unit.VarArgs {
c.DelayTimes = append(c.DelayTimes, uint16(v))
}
values = append(values, byte(delStart), byte(countTrack))
}
c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"]))
c.Values = append(c.Values, values...)

View File

@ -1,8 +1,6 @@
package compiler
import (
"fmt"
"github.com/vsariola/sointu"
)
@ -25,15 +23,3 @@ func NewSongMacros(s *sointu.Song) *SongMacros {
}
return &p
}
func (p *SongMacros) NumDelayLines() string {
total := 0
for _, instr := range p.Song.Patch {
for _, unit := range instr.Units {
if unit.Type == "delay" {
total += unit.Parameters["count"] * (1 + unit.Parameters["stereo"])
}
}
}
return fmt.Sprintf("%v", total)
}

47
vm/generate/generate.go Normal file
View File

@ -0,0 +1,47 @@
// +build ignore
package main
import (
"fmt"
"os"
"strings"
"github.com/vsariola/sointu/vm"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
outputFile, err := os.Create("opcodes.go")
check(err)
defer outputFile.Close()
fmt.Fprintln(outputFile, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(outputFile, "package vm")
fmt.Fprintln(outputFile, "")
fmt.Fprintln(outputFile, "const (")
features := vm.AllFeatures{}
max := 0
for _, instr := range features.Instructions() {
if l := len(instr); max < l {
max = l
}
}
for i, instr := range features.Instructions() {
format := fmt.Sprintf("\top%%-%vv = %%v\n", max)
fmt.Fprintf(outputFile, format, strings.Title(instr), i+1)
}
fmt.Fprintln(outputFile, ")")
fmt.Fprintln(outputFile, "")
fmt.Fprintf(outputFile, "var transformCounts = [...]int{")
for i, instr := range features.Instructions() {
if i > 0 {
fmt.Fprintf(outputFile, ", ")
}
fmt.Fprintf(outputFile, "%v", features.TransformCount(instr))
}
fmt.Fprintln(outputFile, "}")
}

579
vm/interpreter.go Normal file
View File

@ -0,0 +1,579 @@
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, 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) > 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[:]
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, 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)
}
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] = 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, 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)
}

169
vm/interpreter_test.go Normal file
View File

@ -0,0 +1,169 @@
package vm_test
import (
"bytes"
"encoding/binary"
"io/ioutil"
"log"
"math"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
"gopkg.in/yaml.v2"
)
func TestAllRegressionTests(t *testing.T) {
_, myname, _, _ := runtime.Caller(0)
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.yml"))
if err != nil {
t.Fatalf("cannot glob files in the test directory: %v", err)
}
for _, filename := range files {
basename := filepath.Base(filename)
testname := strings.TrimSuffix(basename, path.Ext(basename))
t.Run(testname, func(t *testing.T) {
if strings.Contains(testname, "sample") {
t.Skip("Samples (gm.dls) not available in the interpreter VM at the moment")
return
}
asmcode, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("cannot read the .asm file: %v", filename)
}
var song sointu.Song
err = yaml.Unmarshal(asmcode, &song)
if err != nil {
t.Fatalf("could not parse the .yml file: %v", err)
}
synth, err := vm.Synth(song.Patch)
if err != nil {
t.Fatalf("Compiling patch failed: %v", err)
}
buffer, err := sointu.Play(synth, song)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
}
if os.Getenv("SOINTU_TEST_SAVE_OUTPUT") == "YES" {
outputpath := path.Join(path.Dir(myname), "actual_output")
if _, err := os.Stat(outputpath); os.IsNotExist(err) {
os.Mkdir(outputpath, 0755)
}
outFileName := path.Join(path.Dir(myname), "actual_output", testname+".raw")
outfile, err := os.OpenFile(outFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
defer outfile.Close()
if err != nil {
t.Fatalf("Creating file failed: %v", err)
}
var createdbuf bytes.Buffer
err = binary.Write(&createdbuf, binary.LittleEndian, buffer)
if err != nil {
t.Fatalf("error converting buffer: %v", err)
}
_, err = outfile.Write(createdbuf.Bytes())
if err != nil {
log.Fatal(err)
}
}
compareToRawFloat32(t, buffer, testname+".raw")
})
}
}
func TestStackUnderflow(t *testing.T) {
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
}}}
synth, err := vm.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to stack underflow")
}
}
func TestStackBalancing(t *testing.T) {
patch := sointu.Patch{
sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
sointu.Unit{Type: "push", Parameters: map[string]int{}},
}}}
synth, err := vm.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to unbalanced stack push/pop")
}
}
func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
expected := make([]float32, len(expectedb)/4)
buf := bytes.NewReader(expectedb)
err = binary.Read(buf, binary.LittleEndian, &expected)
if err != nil {
t.Fatalf("error converting expected buffer: %v", err)
}
if len(expected) != len(buffer) {
t.Fatalf("buffer length mismatch, got %v, expected %v", len(buffer), len(expected))
}
firsterr := -1
errs := 0
for i, v := range expected[1 : len(expected)-1] {
if math.IsNaN(float64(buffer[i])) || (math.Abs(float64(v-buffer[i])) > 1e-2 &&
math.Abs(float64(v-buffer[i+1])) > 1e-2 && math.Abs(float64(v-buffer[i+2])) > 1e-2) {
errs++
if firsterr == -1 {
firsterr = i
}
if errs > 200 { // we are again quite liberal with rounding errors, as different platforms have minor differences in floating point rounding
t.Fatalf("more than 200 errors bigger than 1e-2 detected, first at sample position %v", firsterr)
}
}
}
}
func compareToRawInt16(t *testing.T, buffer []int16, rawname string) {
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
expected := make([]int16, len(expectedb)/2)
buf := bytes.NewReader(expectedb)
err = binary.Read(buf, binary.LittleEndian, &expected)
if err != nil {
t.Fatalf("error converting expected buffer: %v", err)
}
if len(expected) != len(buffer) {
t.Fatalf("buffer length mismatch, got %v, expected %v", len(buffer), len(expected))
}
for i, v := range expected {
if math.IsNaN(float64(buffer[i])) || v != buffer[i] {
t.Fatalf("error at sample position %v", i)
}
}
}
func convertToInt16Buffer(buffer []float32) []int16 {
int16Buffer := make([]int16, len(buffer))
for i, v := range buffer {
int16Buffer[i] = int16(math.Round(math.Min(math.Max(float64(v), -1.0), 1.0) * 32767))
}
return int16Buffer
}

36
vm/opcodes.go Normal file
View File

@ -0,0 +1,36 @@
// Code generated by go generate; DO NOT EDIT.
package vm
const (
opAdd = 1
opAddp = 2
opAux = 3
opClip = 4
opCompressor = 5
opCrush = 6
opDelay = 7
opDistort = 8
opEnvelope = 9
opFilter = 10
opGain = 11
opHold = 12
opIn = 13
opInvgain = 14
opLoadnote = 15
opLoadval = 16
opMul = 17
opMulp = 18
opNoise = 19
opOscillator = 20
opOut = 21
opOutaux = 22
opPan = 23
opPop = 24
opPush = 25
opReceive = 26
opSend = 27
opSpeed = 28
opXch = 29
)
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0}