diff --git a/cmd/sointu-nativetrack/main.go b/cmd/sointu-nativetrack/main.go new file mode 100644 index 0000000..9920acd --- /dev/null +++ b/cmd/sointu-nativetrack/main.go @@ -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) +} diff --git a/cmd/sointu-play/main.go b/cmd/sointu-play/main.go index a37620c..2a05079 100644 --- a/cmd/sointu-play/main.go +++ b/cmd/sointu-play/main.go @@ -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) diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index 9920acd..ffbfdb0 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -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) } diff --git a/patch.go b/patch.go index dec55c5..6718d0b 100644 --- a/patch.go +++ b/patch.go @@ -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] { diff --git a/song.go b/song.go index 06a8b13..215db61 100644 --- a/song.go +++ b/song.go @@ -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 diff --git a/synth.go b/synth.go index 6ea7981..219993b 100644 --- a/synth.go +++ b/synth.go @@ -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 { diff --git a/templates/amd64-386/player.asm b/templates/amd64-386/player.asm index 9718ca9..3d84b3c 100644 --- a/templates/amd64-386/player.asm +++ b/templates/amd64-386/player.asm @@ -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 diff --git a/vm/bytepatch.go b/vm/bytepatch.go index 3ccd8b6..df4bfa9 100644 --- a/vm/bytepatch.go +++ b/vm/bytepatch.go @@ -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...) diff --git a/vm/compiler/song_macros.go b/vm/compiler/song_macros.go index c644d06..e47d5e4 100644 --- a/vm/compiler/song_macros.go +++ b/vm/compiler/song_macros.go @@ -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) -} diff --git a/vm/generate/generate.go b/vm/generate/generate.go new file mode 100644 index 0000000..ac3c42b --- /dev/null +++ b/vm/generate/generate.go @@ -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, "}") +} diff --git a/vm/interpreter.go b/vm/interpreter.go new file mode 100644 index 0000000..3751ea5 --- /dev/null +++ b/vm/interpreter.go @@ -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) +} diff --git a/vm/interpreter_test.go b/vm/interpreter_test.go new file mode 100644 index 0000000..20b97a0 --- /dev/null +++ b/vm/interpreter_test.go @@ -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 +} diff --git a/vm/opcodes.go b/vm/opcodes.go new file mode 100644 index 0000000..8d14d10 --- /dev/null +++ b/vm/opcodes.go @@ -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}