mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-25 18:00:37 -04:00
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:
parent
a035845b81
commit
6d2b63a5e9
21
cmd/sointu-nativetrack/main.go
Normal file
21
cmd/sointu-nativetrack/main.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
12
patch.go
12
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] {
|
||||
|
28
song.go
28
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
|
||||
|
5
synth.go
5
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 {
|
||||
|
@ -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
|
||||
|
@ -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...)
|
||||
|
@ -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
47
vm/generate/generate.go
Normal 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
579
vm/interpreter.go
Normal 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
169
vm/interpreter_test.go
Normal 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
36
vm/opcodes.go
Normal 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}
|
Loading…
Reference in New Issue
Block a user