mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
reorganize things into different packages
This commit is contained in:
198
vm/bytepatch.go
Normal file
198
vm/bytepatch.go
Normal file
@ -0,0 +1,198 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type BytePatch struct {
|
||||
Commands []byte
|
||||
Values []byte
|
||||
DelayTimes []uint16
|
||||
SampleOffsets []SampleOffset
|
||||
PolyphonyBitmask uint32
|
||||
NumVoices uint32
|
||||
}
|
||||
|
||||
type SampleOffset struct {
|
||||
Start uint32
|
||||
LoopStart uint16
|
||||
LoopLength uint16
|
||||
}
|
||||
|
||||
func Encode(patch sointu.Patch, featureSet FeatureSet) (*BytePatch, error) {
|
||||
var c BytePatch
|
||||
sampleOffsetMap := map[SampleOffset]int{}
|
||||
globalAddrs := map[int]uint16{}
|
||||
globalFixups := map[int]([]int){}
|
||||
voiceNo := 0
|
||||
for instrIndex, instr := range patch {
|
||||
if len(instr.Units) > 63 {
|
||||
return nil, errors.New("An instrument can have a maximum of 63 units")
|
||||
}
|
||||
if instr.NumVoices < 1 {
|
||||
return nil, errors.New("Each instrument must have at least 1 voice")
|
||||
}
|
||||
localAddrs := map[int]uint16{}
|
||||
localFixups := map[int]([]int){}
|
||||
localUnitNo := 0
|
||||
for _, unit := range instr.Units {
|
||||
if unit.Type == "" { // empty units are just ignored & skipped
|
||||
continue
|
||||
}
|
||||
if unit.Type == "oscillator" && unit.Parameters["type"] == 4 {
|
||||
s := SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
|
||||
if s.LoopLength == 0 {
|
||||
// hacky quick fix: looplength 0 causes div by zero so avoid crashing
|
||||
s.LoopLength = 1
|
||||
}
|
||||
index, ok := sampleOffsetMap[s]
|
||||
if !ok {
|
||||
index = len(c.SampleOffsets)
|
||||
sampleOffsetMap[s] = index
|
||||
c.SampleOffsets = append(c.SampleOffsets, s)
|
||||
}
|
||||
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)
|
||||
}
|
||||
var values []byte
|
||||
for _, v := range sointu.UnitTypes[unit.Type] {
|
||||
if v.CanModulate && v.CanSet {
|
||||
values = append(values, byte(unit.Parameters[v.Name]))
|
||||
}
|
||||
}
|
||||
if unit.Type == "aux" {
|
||||
values = append(values, byte(unit.Parameters["channel"]))
|
||||
} else if unit.Type == "in" {
|
||||
values = append(values, byte(unit.Parameters["channel"]))
|
||||
} else if unit.Type == "oscillator" {
|
||||
flags := 0
|
||||
switch unit.Parameters["type"] {
|
||||
case sointu.Sine:
|
||||
flags = 0x40
|
||||
case sointu.Trisaw:
|
||||
flags = 0x20
|
||||
case sointu.Pulse:
|
||||
flags = 0x10
|
||||
case sointu.Gate:
|
||||
flags = 0x04
|
||||
case sointu.Sample:
|
||||
flags = 0x80
|
||||
}
|
||||
if unit.Parameters["lfo"] == 1 {
|
||||
flags += 0x08
|
||||
}
|
||||
flags += unit.Parameters["unison"]
|
||||
values = append(values, byte(flags))
|
||||
} else if unit.Type == "filter" {
|
||||
flags := 0
|
||||
if unit.Parameters["lowpass"] == 1 {
|
||||
flags += 0x40
|
||||
}
|
||||
if unit.Parameters["bandpass"] == 1 {
|
||||
flags += 0x20
|
||||
}
|
||||
if unit.Parameters["highpass"] == 1 {
|
||||
flags += 0x10
|
||||
}
|
||||
if unit.Parameters["negbandpass"] == 1 {
|
||||
flags += 0x08
|
||||
}
|
||||
if unit.Parameters["neghighpass"] == 1 {
|
||||
flags += 0x04
|
||||
}
|
||||
values = append(values, byte(flags))
|
||||
} else if unit.Type == "send" {
|
||||
targetID := unit.Parameters["target"]
|
||||
targetInstrIndex, _, err := patch.FindSendTarget(targetID)
|
||||
targetVoice := unit.Parameters["voice"]
|
||||
var addr uint16 = uint16(unit.Parameters["port"]) & 7
|
||||
if unit.Parameters["sendpop"] == 1 {
|
||||
addr += 0x8
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// local send is only possible if targetVoice is "auto" (0) and
|
||||
// the targeted unit is in the same instrument as send
|
||||
if targetInstrIndex == instrIndex && targetVoice == 0 {
|
||||
if v, ok := localAddrs[targetID]; ok {
|
||||
addr += v
|
||||
} else {
|
||||
localFixups[targetID] = append(localFixups[targetID], len(c.Values)+len(values))
|
||||
}
|
||||
} else {
|
||||
addr += 0x8000
|
||||
if targetVoice > 0 { // "auto" (0) means for global send that it targets voice 0 of that instrument
|
||||
addr += uint16((targetVoice - 1) * 0x400)
|
||||
}
|
||||
if v, ok := globalAddrs[targetID]; ok {
|
||||
addr += v
|
||||
} else {
|
||||
globalFixups[targetID] = append(globalFixups[targetID], len(c.Values)+len(values))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if no target will be found, the send will trash some of
|
||||
// the last values of the last port of the last voice, which
|
||||
// is unlikely to cause issues. We still honor the POP bit.
|
||||
addr &= 0x8
|
||||
addr |= 0xFFF7
|
||||
}
|
||||
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))
|
||||
}
|
||||
c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"]))
|
||||
c.Values = append(c.Values, values...)
|
||||
if unit.ID != 0 {
|
||||
localAddr := uint16((localUnitNo + 1) << 4)
|
||||
fixUp(c.Values, localFixups[unit.ID], localAddr)
|
||||
localFixups[unit.ID] = nil
|
||||
localAddrs[unit.ID] = localAddr
|
||||
globalAddr := localAddr + 16 + uint16(voiceNo)*1024
|
||||
fixUp(c.Values, globalFixups[unit.ID], globalAddr)
|
||||
globalFixups[unit.ID] = nil
|
||||
globalAddrs[unit.ID] = globalAddr
|
||||
}
|
||||
localUnitNo++ // a command in command stream means the wrkspace addr gets also increased
|
||||
}
|
||||
c.Commands = append(c.Commands, byte(0)) // advance
|
||||
voiceNo += instr.NumVoices
|
||||
c.NumVoices += uint32(instr.NumVoices)
|
||||
for k := 0; k < instr.NumVoices-1; k++ {
|
||||
c.PolyphonyBitmask = (c.PolyphonyBitmask << 1) + 1
|
||||
}
|
||||
c.PolyphonyBitmask <<= 1
|
||||
}
|
||||
if c.NumVoices > 32 {
|
||||
return nil, fmt.Errorf("Sointu does not support more than 32 concurrent voices; patch uses %v", c.NumVoices)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func fixUp(values []byte, positions []int, delta uint16) {
|
||||
for _, pos := range positions {
|
||||
orig := (uint16(values[pos+1]) << 8) + uint16(values[pos])
|
||||
new := orig + delta
|
||||
values[pos] = byte(new & 255)
|
||||
values[pos+1] = byte(new >> 8)
|
||||
}
|
||||
}
|
169
vm/compiler/bridge/bridge.go
Normal file
169
vm/compiler/bridge/bridge.go
Normal file
@ -0,0 +1,169 @@
|
||||
package bridge
|
||||
|
||||
// #cgo CFLAGS: -I"${SRCDIR}/../../../build/"
|
||||
// #cgo LDFLAGS: "${SRCDIR}/../../../build/libsointu.a"
|
||||
// #include <sointu.h>
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type BridgeService struct {
|
||||
}
|
||||
|
||||
func (s BridgeService) Compile(patch sointu.Patch) (sointu.Synth, error) {
|
||||
synth, err := Synth(patch)
|
||||
return synth, err
|
||||
}
|
||||
|
||||
func Synth(patch sointu.Patch) (*C.Synth, error) {
|
||||
s := new(C.Synth)
|
||||
comPatch, err := vm.Encode(patch, vm.AllFeatures{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error compiling patch: %v", err)
|
||||
}
|
||||
if len(comPatch.Commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
|
||||
return nil, errors.New("bridge supports at most 2048 commands; the compiled patch has more")
|
||||
}
|
||||
if len(comPatch.Values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
|
||||
return nil, errors.New("bridge supports at most 16384 values; the compiled patch has more")
|
||||
}
|
||||
for i, v := range comPatch.Commands {
|
||||
s.Commands[i] = (C.uchar)(v)
|
||||
}
|
||||
for i, v := range comPatch.Values {
|
||||
s.Values[i] = (C.uchar)(v)
|
||||
}
|
||||
for i, v := range comPatch.DelayTimes {
|
||||
s.DelayTimes[i] = (C.ushort)(v)
|
||||
}
|
||||
for i, v := range comPatch.SampleOffsets {
|
||||
s.SampleOffsets[i].Start = (C.uint)(v.Start)
|
||||
s.SampleOffsets[i].LoopStart = (C.ushort)(v.LoopStart)
|
||||
s.SampleOffsets[i].LoopLength = (C.ushort)(v.LoopLength)
|
||||
}
|
||||
s.NumVoices = C.uint(comPatch.NumVoices)
|
||||
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
|
||||
s.RandSeed = 1
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Render renders until the buffer is full or the modulated time is reached, whichever
|
||||
// happens first.
|
||||
// Parameters:
|
||||
// buffer float32 slice to fill with rendered samples. Stereo signal, so
|
||||
// should have even length.
|
||||
// maxtime how long nominal time to render in samples. Speed unit might modulate time
|
||||
// so the actual number of samples rendered depends on the modulation and if
|
||||
// buffer is full before maxtime is reached.
|
||||
// Returns a tuple (int, int, error), consisting of:
|
||||
// samples number of samples rendered in the buffer
|
||||
// time how much the time advanced
|
||||
// error potential error
|
||||
// In practice, if nsamples = len(buffer)/2, then time <= maxtime. If maxtime was reached
|
||||
// first, then nsamples <= len(buffer)/2 and time >= maxtime. Note that it could happen that
|
||||
// time > maxtime, as it is modulated and the time could advance by 2 or more, so the loop
|
||||
// exit condition would fire when the time is already past maxtime.
|
||||
// Under no conditions, nsamples >= len(buffer)/2 i.e. guaranteed to never overwrite the buffer.
|
||||
func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) {
|
||||
if len(buffer)%1 == 1 {
|
||||
return -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length")
|
||||
}
|
||||
samples := C.int(len(buffer) / 2)
|
||||
time := C.int(maxtime)
|
||||
errcode := int(C.su_render(synth, (*C.float)(&buffer[0]), &samples, &time))
|
||||
if errcode > 0 {
|
||||
return int(samples), int(time), &RenderError{errcode: errcode}
|
||||
}
|
||||
return int(samples), int(time), nil
|
||||
}
|
||||
|
||||
// Trigger is part of C.Synths' implementation of sointu.Synth interface
|
||||
func (s *C.Synth) Trigger(voice int, note byte) {
|
||||
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
||||
return
|
||||
}
|
||||
s.SynthWrk.Voices[voice] = C.Voice{}
|
||||
s.SynthWrk.Voices[voice].Note = C.int(note)
|
||||
}
|
||||
|
||||
// Release is part of C.Synths' implementation of sointu.Synth interface
|
||||
func (s *C.Synth) Release(voice int) {
|
||||
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
||||
return
|
||||
}
|
||||
s.SynthWrk.Voices[voice].Release = 1
|
||||
}
|
||||
|
||||
// Update
|
||||
func (s *C.Synth) Update(patch sointu.Patch) error {
|
||||
comPatch, err := vm.Encode(patch, vm.AllFeatures{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error compiling patch: %v", err)
|
||||
}
|
||||
if len(comPatch.Commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
|
||||
return errors.New("bridge supports at most 2048 commands; the compiled patch has more")
|
||||
}
|
||||
if len(comPatch.Values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
|
||||
return errors.New("bridge supports at most 16384 values; the compiled patch has more")
|
||||
}
|
||||
needsRefresh := false
|
||||
for i, v := range comPatch.Commands {
|
||||
if cmdChar := (C.uchar)(v); s.Commands[i] != cmdChar {
|
||||
s.Commands[i] = cmdChar
|
||||
needsRefresh = true // if any of the commands change, we retrigger all units
|
||||
}
|
||||
}
|
||||
for i, v := range comPatch.Values {
|
||||
s.Values[i] = (C.uchar)(v)
|
||||
}
|
||||
for i, v := range comPatch.DelayTimes {
|
||||
s.DelayTimes[i] = (C.ushort)(v)
|
||||
}
|
||||
for i, v := range comPatch.SampleOffsets {
|
||||
s.SampleOffsets[i].Start = (C.uint)(v.Start)
|
||||
s.SampleOffsets[i].LoopStart = (C.ushort)(v.LoopStart)
|
||||
s.SampleOffsets[i].LoopLength = (C.ushort)(v.LoopLength)
|
||||
}
|
||||
s.NumVoices = C.uint(comPatch.NumVoices)
|
||||
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
|
||||
if needsRefresh {
|
||||
for i := range s.SynthWrk.Voices {
|
||||
// if any of the commands change, we retrigger all units
|
||||
// note that we don't change the notes or release states, just the units
|
||||
for j := range s.SynthWrk.Voices[i].Units {
|
||||
s.SynthWrk.Voices[i].Units[j] = C.Unit{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render error stores the exact errorcode, which is actually just the x87 FPU flags,
|
||||
// with only the critical failure flags masked. Useful if you are interested exactly
|
||||
// what went wrong with the patch.
|
||||
type RenderError struct {
|
||||
errcode int
|
||||
}
|
||||
|
||||
func (e *RenderError) Error() string {
|
||||
var reasons []string
|
||||
if e.errcode&0x40 != 0 {
|
||||
reasons = append(reasons, "FPU stack over/underflow")
|
||||
}
|
||||
if e.errcode&0x04 != 0 {
|
||||
reasons = append(reasons, "FPU divide by zero")
|
||||
}
|
||||
if e.errcode&0x01 != 0 {
|
||||
reasons = append(reasons, "FPU invalid operation")
|
||||
}
|
||||
if e.errcode&0x3800 != 0 {
|
||||
reasons = append(reasons, "FPU stack push/pops are not balanced")
|
||||
}
|
||||
return "RenderError: " + strings.Join(reasons, ", ")
|
||||
}
|
268
vm/compiler/bridge/bridge_test.go
Normal file
268
vm/compiler/bridge/bridge_test.go
Normal file
@ -0,0 +1,268 @@
|
||||
package bridge_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/compiler/bridge"
|
||||
"gopkg.in/yaml.v2"
|
||||
// TODO: test the song using a mocks instead
|
||||
)
|
||||
|
||||
const BPM = 100
|
||||
const SAMPLE_RATE = 44100
|
||||
const TOTAL_ROWS = 16
|
||||
const SAMPLES_PER_ROW = SAMPLE_RATE * 4 * 60 / (BPM * 16)
|
||||
|
||||
const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
|
||||
|
||||
// const bufsize = su_max_samples * 2
|
||||
|
||||
func TestOscillatSine(t *testing.T) {
|
||||
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
|
||||
sointu.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
|
||||
sointu.Unit{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": sointu.Sine, "lfo": 0, "unison": 0}},
|
||||
sointu.Unit{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
||||
sointu.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
|
||||
sointu.Unit{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": sointu.Sine, "lfo": 0, "unison": 0}},
|
||||
sointu.Unit{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
||||
sointu.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
|
||||
}}}
|
||||
tracks := []sointu.Track{{NumVoices: 1, Order: []int{0}, Patterns: [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}}}
|
||||
song := sointu.Song{BPM: 100, RowsPerBeat: 4, Score: sointu.Score{RowsPerPattern: 16, Length: 1, Tracks: tracks}, Patch: patch}
|
||||
synth, err := bridge.Synth(patch)
|
||||
if err != nil {
|
||||
t.Fatalf("Compiling patch failed: %v", err)
|
||||
}
|
||||
buffer, err := sointu.Play(synth, song)
|
||||
if err != nil {
|
||||
t.Fatalf("Render failed: %v", err)
|
||||
}
|
||||
compareToRawFloat32(t, buffer, "test_oscillat_sine.raw")
|
||||
}
|
||||
|
||||
func TestRenderSamples(t *testing.T) {
|
||||
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
|
||||
sointu.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
|
||||
sointu.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
|
||||
sointu.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
|
||||
}}}
|
||||
|
||||
synth, err := bridge.Synth(patch)
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
synth.Trigger(0, 64)
|
||||
buffer := make([]float32, 2*su_max_samples)
|
||||
err = sointu.Render(synth, buffer[:len(buffer)/2])
|
||||
if err != nil {
|
||||
t.Fatalf("first render gave an error")
|
||||
}
|
||||
synth.Release(0)
|
||||
err = sointu.Render(synth, buffer[len(buffer)/2:])
|
||||
if err != nil {
|
||||
t.Fatalf("first render gave an error")
|
||||
}
|
||||
compareToRawFloat32(t, buffer, "test_render_samples.raw")
|
||||
}
|
||||
|
||||
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 runtime.GOOS != "windows" && strings.Contains(testname, "sample") {
|
||||
t.Skip("Samples (gm.dls) available only on Windows")
|
||||
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 := bridge.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 := bridge.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 := bridge.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 TestStackOverflow(t *testing.T) {
|
||||
patch := sointu.Patch{
|
||||
sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
}}}
|
||||
synth, err := bridge.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 overflow, despite balanced push/pops")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDivideByZero(t *testing.T) {
|
||||
patch := sointu.Patch{sointu.Instrument{NumVoices: 1, Units: []sointu.Unit{
|
||||
sointu.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
|
||||
sointu.Unit{Type: "invgain", Parameters: map[string]int{"invgain": 0}},
|
||||
sointu.Unit{Type: "pop", Parameters: map[string]int{}},
|
||||
}}}
|
||||
synth, err := bridge.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 divide by zero")
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
for i, v := range expected {
|
||||
if math.IsNaN(float64(buffer[i])) || math.Abs(float64(v-buffer[i])) > 1e-6 {
|
||||
t.Fatalf("error bigger than 1e-6 detected, at sample position %v", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
8
vm/compiler/bridge/init_windows.go
Normal file
8
vm/compiler/bridge/init_windows.go
Normal file
@ -0,0 +1,8 @@
|
||||
package bridge
|
||||
|
||||
// #include "sointu.h"
|
||||
import "C"
|
||||
|
||||
func init() {
|
||||
C.su_load_gmdls() // GM.DLS is an windows specific sound bank so samples work currently only on windows
|
||||
}
|
144
vm/compiler/compiler.go
Normal file
144
vm/compiler/compiler.go
Normal file
@ -0,0 +1,144 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type Compiler struct {
|
||||
Template *template.Template
|
||||
OS string
|
||||
Arch string
|
||||
Output16Bit bool
|
||||
}
|
||||
|
||||
// New returns a new compiler using the default .asm templates
|
||||
func New(os string, arch string, output16Bit bool) (*Compiler, error) {
|
||||
_, myname, _, _ := runtime.Caller(0)
|
||||
var subdir string
|
||||
if arch == "386" || arch == "amd64" {
|
||||
subdir = "amd64-386"
|
||||
} else if arch == "wasm" {
|
||||
subdir = "wasm"
|
||||
} else {
|
||||
return nil, fmt.Errorf("compiler.New failed, because only amd64, 386 and wasm archs are supported (targeted architecture was %v)", arch)
|
||||
}
|
||||
templateDir := filepath.Join(path.Dir(myname), "..", "..", "templates", subdir)
|
||||
compiler, err := NewFromTemplates(os, arch, output16Bit, templateDir)
|
||||
return compiler, err
|
||||
}
|
||||
|
||||
func NewFromTemplates(os string, arch string, output16Bit bool, templateDirectory string) (*Compiler, error) {
|
||||
globPtrn := filepath.Join(templateDirectory, "*.*")
|
||||
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(globPtrn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not create template based on directory "%v": %v`, templateDirectory, err)
|
||||
}
|
||||
return &Compiler{Template: tmpl, OS: os, Arch: arch, Output16Bit: output16Bit}, nil
|
||||
}
|
||||
|
||||
func (com *Compiler) Library() (map[string]string, error) {
|
||||
if com.Arch != "386" && com.Arch != "amd64" {
|
||||
return nil, fmt.Errorf(`compiling as a library is supported only on 386 and amd64 architectures (targeted architecture was %v)`, com.Arch)
|
||||
}
|
||||
templates := []string{"library.asm", "library.h"}
|
||||
features := vm.AllFeatures{}
|
||||
retmap := map[string]string{}
|
||||
for _, templateName := range templates {
|
||||
compilerMacros := *NewCompilerMacros(*com)
|
||||
compilerMacros.Library = true
|
||||
featureSetMacros := FeatureSetMacros{features}
|
||||
x86Macros := *NewX86Macros(com.OS, com.Arch == "amd64", features, false)
|
||||
data := struct {
|
||||
CompilerMacros
|
||||
FeatureSetMacros
|
||||
X86Macros
|
||||
}{compilerMacros, featureSetMacros, x86Macros}
|
||||
populatedTemplate, extension, err := com.compile(templateName, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
|
||||
}
|
||||
retmap[extension] = populatedTemplate
|
||||
}
|
||||
return retmap, nil
|
||||
}
|
||||
|
||||
func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
|
||||
if com.Arch != "386" && com.Arch != "amd64" && com.Arch != "wasm" {
|
||||
return nil, fmt.Errorf(`compiling a song player is supported only on 386, amd64 and wasm architectures (targeted architecture was %v)`, com.Arch)
|
||||
}
|
||||
var templates []string
|
||||
if com.Arch == "386" || com.Arch == "amd64" {
|
||||
templates = []string{"player.asm", "player.h"}
|
||||
} else if com.Arch == "wasm" {
|
||||
templates = []string{"player.wat"}
|
||||
}
|
||||
features := vm.NecessaryFeaturesFor(song.Patch)
|
||||
retmap := map[string]string{}
|
||||
encodedPatch, err := vm.Encode(song.Patch, features)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not encode patch: %v`, err)
|
||||
}
|
||||
patterns, sequences, err := vm.ConstructPatterns(song)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not encode song: %v`, err)
|
||||
}
|
||||
for _, templateName := range templates {
|
||||
compilerMacros := *NewCompilerMacros(*com)
|
||||
featureSetMacros := FeatureSetMacros{features}
|
||||
songMacros := *NewSongMacros(song)
|
||||
var populatedTemplate, extension string
|
||||
var err error
|
||||
if com.Arch == "386" || com.Arch == "amd64" {
|
||||
x86Macros := *NewX86Macros(com.OS, com.Arch == "amd64", features, false)
|
||||
data := struct {
|
||||
CompilerMacros
|
||||
FeatureSetMacros
|
||||
X86Macros
|
||||
SongMacros
|
||||
*vm.BytePatch
|
||||
Patterns [][]byte
|
||||
Sequences [][]byte
|
||||
PatternLength int
|
||||
SequenceLength int
|
||||
Hold int
|
||||
}{compilerMacros, featureSetMacros, x86Macros, songMacros, encodedPatch, patterns, sequences, len(patterns[0]), len(sequences[0]), 1}
|
||||
populatedTemplate, extension, err = com.compile(templateName, &data)
|
||||
} else if com.Arch == "wasm" {
|
||||
wasmMacros := *NewWasmMacros()
|
||||
data := struct {
|
||||
CompilerMacros
|
||||
FeatureSetMacros
|
||||
WasmMacros
|
||||
SongMacros
|
||||
*vm.BytePatch
|
||||
Patterns [][]byte
|
||||
Sequences [][]byte
|
||||
PatternLength int
|
||||
SequenceLength int
|
||||
Hold int
|
||||
}{compilerMacros, featureSetMacros, wasmMacros, songMacros, encodedPatch, patterns, sequences, len(patterns[0]), len(sequences[0]), 1}
|
||||
populatedTemplate, extension, err = com.compile(templateName, &data)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
|
||||
}
|
||||
retmap[extension] = populatedTemplate
|
||||
}
|
||||
return retmap, nil
|
||||
}
|
||||
|
||||
func (com *Compiler) compile(templateName string, data interface{}) (string, string, error) {
|
||||
result := bytes.NewBufferString("")
|
||||
err := com.Template.ExecuteTemplate(result, templateName, data)
|
||||
extension := filepath.Ext(templateName)
|
||||
return result.String(), extension, err
|
||||
}
|
28
vm/compiler/compiler_macros.go
Normal file
28
vm/compiler/compiler_macros.go
Normal file
@ -0,0 +1,28 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type CompilerMacros struct {
|
||||
Clip bool
|
||||
Library bool
|
||||
|
||||
Sine int // TODO: how can we elegantly access global constants in template, without wrapping each one by one
|
||||
Trisaw int
|
||||
Pulse int
|
||||
Gate int
|
||||
Sample int
|
||||
Compiler
|
||||
}
|
||||
|
||||
func NewCompilerMacros(c Compiler) *CompilerMacros {
|
||||
return &CompilerMacros{
|
||||
Sine: sointu.Sine,
|
||||
Trisaw: sointu.Trisaw,
|
||||
Pulse: sointu.Pulse,
|
||||
Gate: sointu.Gate,
|
||||
Sample: sointu.Sample,
|
||||
Compiler: c,
|
||||
}
|
||||
}
|
29
vm/compiler/featureset_macros.go
Normal file
29
vm/compiler/featureset_macros.go
Normal file
@ -0,0 +1,29 @@
|
||||
package compiler
|
||||
|
||||
import "github.com/vsariola/sointu/vm"
|
||||
|
||||
type FeatureSetMacros struct {
|
||||
vm.FeatureSet
|
||||
}
|
||||
|
||||
func (p *FeatureSetMacros) HasOp(instruction string) bool {
|
||||
_, ok := p.Opcode(instruction)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *FeatureSetMacros) GetOp(instruction string) int {
|
||||
v, _ := p.Opcode(instruction)
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *FeatureSetMacros) Stereo(unitType string) bool {
|
||||
return p.SupportsParamValue(unitType, "stereo", 1)
|
||||
}
|
||||
|
||||
func (p *FeatureSetMacros) Mono(unitType string) bool {
|
||||
return p.SupportsParamValue(unitType, "stereo", 0)
|
||||
}
|
||||
|
||||
func (p *FeatureSetMacros) StereoAndMono(unitType string) bool {
|
||||
return p.Stereo(unitType) && p.Mono(unitType)
|
||||
}
|
39
vm/compiler/song_macros.go
Normal file
39
vm/compiler/song_macros.go
Normal file
@ -0,0 +1,39 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type SongMacros struct {
|
||||
Song *sointu.Song
|
||||
VoiceTrackBitmask int
|
||||
MaxSamples int
|
||||
}
|
||||
|
||||
func NewSongMacros(s *sointu.Song) *SongMacros {
|
||||
maxSamples := s.SamplesPerRow() * s.Score.LengthInRows()
|
||||
p := SongMacros{Song: s, MaxSamples: maxSamples}
|
||||
trackVoiceNumber := 0
|
||||
for _, t := range s.Score.Tracks {
|
||||
for b := 0; b < t.NumVoices-1; b++ {
|
||||
p.VoiceTrackBitmask += 1 << trackVoiceNumber
|
||||
trackVoiceNumber++
|
||||
}
|
||||
trackVoiceNumber++ // set all bits except last one
|
||||
}
|
||||
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)
|
||||
}
|
50
vm/compiler/wasm_macros.go
Normal file
50
vm/compiler/wasm_macros.go
Normal file
@ -0,0 +1,50 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type WasmMacros struct {
|
||||
data *bytes.Buffer
|
||||
Labels map[string]int
|
||||
}
|
||||
|
||||
func NewWasmMacros() *WasmMacros {
|
||||
return &WasmMacros{
|
||||
data: new(bytes.Buffer),
|
||||
Labels: map[string]int{},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) SetLabel(label string) string {
|
||||
wm.Labels[label] = wm.data.Len()
|
||||
return ""
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) GetLabel(label string) int {
|
||||
return wm.Labels[label]
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) DataB(value byte) string {
|
||||
binary.Write(wm.data, binary.LittleEndian, value)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) DataW(value uint16) string {
|
||||
binary.Write(wm.data, binary.LittleEndian, value)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) DataD(value uint32) string {
|
||||
binary.Write(wm.data, binary.LittleEndian, value)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) ToByte(value int) byte {
|
||||
return byte(value)
|
||||
}
|
||||
|
||||
func (wm *WasmMacros) Data() []byte {
|
||||
return wm.data.Bytes()
|
||||
}
|
408
vm/compiler/x86_macros.go
Normal file
408
vm/compiler/x86_macros.go
Normal file
@ -0,0 +1,408 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type X86Macros struct {
|
||||
Stacklocs []string
|
||||
Amd64 bool
|
||||
OS string
|
||||
DisableSections bool
|
||||
usesFloatConst map[float32]bool
|
||||
usesIntConst map[int]bool
|
||||
floatConsts []float32
|
||||
intConsts []int
|
||||
calls map[string]bool
|
||||
stackframes map[string][]string
|
||||
features vm.FeatureSet
|
||||
}
|
||||
|
||||
func NewX86Macros(os string, Amd64 bool, features vm.FeatureSet, DisableSections bool) *X86Macros {
|
||||
return &X86Macros{
|
||||
calls: map[string]bool{},
|
||||
usesFloatConst: map[float32]bool{},
|
||||
usesIntConst: map[int]bool{},
|
||||
stackframes: map[string][]string{},
|
||||
Amd64: Amd64,
|
||||
OS: os,
|
||||
DisableSections: DisableSections,
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *X86Macros) Float(value float32) string {
|
||||
if _, ok := p.usesFloatConst[value]; !ok {
|
||||
p.usesFloatConst[value] = true
|
||||
p.floatConsts = append(p.floatConsts, value)
|
||||
}
|
||||
return nameForFloat(value)
|
||||
}
|
||||
|
||||
func (p *X86Macros) Int(value int) string {
|
||||
if _, ok := p.usesIntConst[value]; !ok {
|
||||
p.usesIntConst[value] = true
|
||||
p.intConsts = append(p.intConsts, value)
|
||||
}
|
||||
return nameForInt(value)
|
||||
}
|
||||
|
||||
func (p *X86Macros) Constants() string {
|
||||
var b strings.Builder
|
||||
for _, v := range p.floatConsts {
|
||||
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForFloat(v), math.Float32bits(v))
|
||||
}
|
||||
for _, v := range p.intConsts {
|
||||
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForInt(v), v)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func nameForFloat(value float32) string {
|
||||
s := fmt.Sprintf("%#g", value)
|
||||
s = strings.Replace(s, ".", "_", 1)
|
||||
s = strings.Replace(s, "-", "m", 1)
|
||||
s = strings.Replace(s, "+", "p", 1)
|
||||
return "FCONST_" + s
|
||||
}
|
||||
|
||||
func nameForInt(value int) string {
|
||||
return "ICONST_" + fmt.Sprintf("%d", value)
|
||||
}
|
||||
|
||||
func (p *X86Macros) PTRSIZE() int {
|
||||
if p.Amd64 {
|
||||
return 8
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
func (p *X86Macros) DPTR() string {
|
||||
if p.Amd64 {
|
||||
return "dq"
|
||||
}
|
||||
return "dd"
|
||||
}
|
||||
|
||||
func (p *X86Macros) PTRWORD() string {
|
||||
if p.Amd64 {
|
||||
return "qword"
|
||||
}
|
||||
return "dword"
|
||||
}
|
||||
|
||||
func (p *X86Macros) AX() string {
|
||||
if p.Amd64 {
|
||||
return "rax"
|
||||
}
|
||||
return "eax"
|
||||
}
|
||||
|
||||
func (p *X86Macros) BX() string {
|
||||
if p.Amd64 {
|
||||
return "rbx"
|
||||
}
|
||||
return "ebx"
|
||||
}
|
||||
|
||||
func (p *X86Macros) CX() string {
|
||||
if p.Amd64 {
|
||||
return "rcx"
|
||||
}
|
||||
return "ecx"
|
||||
}
|
||||
|
||||
func (p *X86Macros) DX() string {
|
||||
if p.Amd64 {
|
||||
return "rdx"
|
||||
}
|
||||
return "edx"
|
||||
}
|
||||
|
||||
func (p *X86Macros) SI() string {
|
||||
if p.Amd64 {
|
||||
return "rsi"
|
||||
}
|
||||
return "esi"
|
||||
}
|
||||
|
||||
func (p *X86Macros) DI() string {
|
||||
if p.Amd64 {
|
||||
return "rdi"
|
||||
}
|
||||
return "edi"
|
||||
}
|
||||
|
||||
func (p *X86Macros) SP() string {
|
||||
if p.Amd64 {
|
||||
return "rsp"
|
||||
}
|
||||
return "esp"
|
||||
}
|
||||
|
||||
func (p *X86Macros) BP() string {
|
||||
if p.Amd64 {
|
||||
return "rbp"
|
||||
}
|
||||
return "ebp"
|
||||
}
|
||||
|
||||
func (p *X86Macros) WRK() string {
|
||||
return p.BP()
|
||||
}
|
||||
|
||||
func (p *X86Macros) VAL() string {
|
||||
return p.SI()
|
||||
}
|
||||
|
||||
func (p *X86Macros) COM() string {
|
||||
return p.BX()
|
||||
}
|
||||
|
||||
func (p *X86Macros) INP() string {
|
||||
return p.DX()
|
||||
}
|
||||
|
||||
func (p *X86Macros) SaveStack(scope string) string {
|
||||
p.stackframes[scope] = p.Stacklocs
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *X86Macros) Call(funcname string) (string, error) {
|
||||
p.calls[funcname] = true
|
||||
var s = make([]string, len(p.Stacklocs))
|
||||
copy(s, p.Stacklocs)
|
||||
p.stackframes[funcname] = s
|
||||
return "call " + funcname, nil
|
||||
}
|
||||
|
||||
func (p *X86Macros) TailCall(funcname string) (string, error) {
|
||||
p.calls[funcname] = true
|
||||
p.stackframes[funcname] = p.Stacklocs
|
||||
return "jmp " + funcname, nil
|
||||
}
|
||||
|
||||
func (p *X86Macros) SectText(name string) string {
|
||||
if p.OS == "windows" {
|
||||
if p.DisableSections {
|
||||
return "section .code align=1"
|
||||
}
|
||||
return fmt.Sprintf("section .%v code align=1", name)
|
||||
} else if p.OS == "darwin" {
|
||||
return "section .text align=1"
|
||||
} else {
|
||||
if p.DisableSections {
|
||||
return "section .text. progbits alloc exec nowrite align=1"
|
||||
}
|
||||
return fmt.Sprintf("section .text.%v progbits alloc exec nowrite align=1", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *X86Macros) SectData(name string) string {
|
||||
if p.OS == "windows" || p.OS == "darwin" {
|
||||
if p.OS == "windows" && !p.DisableSections {
|
||||
return fmt.Sprintf("section .%v data align=1", name)
|
||||
}
|
||||
return "section .data align=1"
|
||||
} else {
|
||||
if !p.DisableSections {
|
||||
return fmt.Sprintf("section .data.%v progbits alloc noexec write align=1", name)
|
||||
}
|
||||
return "section .data progbits alloc exec nowrite align=1"
|
||||
}
|
||||
}
|
||||
|
||||
func (p *X86Macros) SectBss(name string) string {
|
||||
if p.OS == "windows" || p.OS == "darwin" {
|
||||
if p.OS == "windows" && !p.DisableSections {
|
||||
return fmt.Sprintf("section .%v bss align=256", name)
|
||||
}
|
||||
} else {
|
||||
if !p.DisableSections {
|
||||
return fmt.Sprintf("section .bss.%v nobits alloc noexec write align=256", name)
|
||||
}
|
||||
}
|
||||
return "section .bss align=256"
|
||||
}
|
||||
|
||||
func (p *X86Macros) Data(label string) string {
|
||||
return fmt.Sprintf("%v\n%v:", p.SectData(label), label)
|
||||
}
|
||||
|
||||
func (p *X86Macros) Func(funcname string, scope ...string) (string, error) {
|
||||
scopeName := funcname
|
||||
if len(scope) > 1 {
|
||||
return "", fmt.Errorf(`Func macro "%v" can take only one additional scope parameter, "%v" were given`, funcname, scope)
|
||||
} else if len(scope) > 0 {
|
||||
scopeName = scope[0]
|
||||
}
|
||||
p.Stacklocs = append(p.stackframes[scopeName], "retaddr_"+funcname)
|
||||
return fmt.Sprintf("%v\n%v:", p.SectText(funcname), funcname), nil
|
||||
}
|
||||
|
||||
func (p *X86Macros) HasCall(funcname string) bool {
|
||||
return p.calls[funcname]
|
||||
}
|
||||
|
||||
func (p *X86Macros) Push(value string, name string) string {
|
||||
p.Stacklocs = append(p.Stacklocs, name)
|
||||
return fmt.Sprintf("push %v ; Stack: %v ", value, p.FmtStack())
|
||||
}
|
||||
|
||||
func (p *X86Macros) PushRegs(params ...string) string {
|
||||
if p.Amd64 {
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(params); i = i + 2 {
|
||||
b.WriteRune('\n')
|
||||
b.WriteString(p.Push(params[i], params[i+1]))
|
||||
}
|
||||
return b.String()
|
||||
} else {
|
||||
var pushadOrder = [...]string{"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"}
|
||||
for _, name := range pushadOrder {
|
||||
for j := 0; j < len(params); j = j + 2 {
|
||||
if params[j] == name {
|
||||
name = params[j+1]
|
||||
}
|
||||
}
|
||||
p.Stacklocs = append(p.Stacklocs, name)
|
||||
}
|
||||
return fmt.Sprintf("\npushad ; Stack: %v", p.FmtStack())
|
||||
}
|
||||
}
|
||||
|
||||
func (p *X86Macros) PopRegs(params ...string) string {
|
||||
if p.Amd64 {
|
||||
var b strings.Builder
|
||||
for i := len(params) - 1; i >= 0; i-- {
|
||||
b.WriteRune('\n')
|
||||
b.WriteString(p.Pop(params[i]))
|
||||
}
|
||||
return b.String()
|
||||
} else {
|
||||
var regs = [...]string{"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"}
|
||||
var b strings.Builder
|
||||
for i, name := range p.Stacklocs[len(p.Stacklocs)-8:] {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(regs[i])
|
||||
if regs[i] != name {
|
||||
b.WriteString(" = ")
|
||||
b.WriteString(name)
|
||||
}
|
||||
}
|
||||
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-8]
|
||||
return fmt.Sprintf("\npopad ; Popped: %v. Stack: %v", b.String(), p.FmtStack())
|
||||
}
|
||||
}
|
||||
|
||||
func (p *X86Macros) Pop(register string) string {
|
||||
last := p.Stacklocs[len(p.Stacklocs)-1]
|
||||
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1]
|
||||
return fmt.Sprintf("pop %v ; %v = %v, Stack: %v ", register, register, last, p.FmtStack())
|
||||
}
|
||||
|
||||
func (p *X86Macros) SaveFPUState() string {
|
||||
i := 0
|
||||
for ; i < 108; i += p.PTRSIZE() {
|
||||
p.Stacklocs = append(p.Stacklocs, fmt.Sprintf("F%v", i))
|
||||
}
|
||||
return fmt.Sprintf("sub %[1]v, %[2]v\nfsave [%[1]v]", p.SP(), i)
|
||||
}
|
||||
|
||||
func (p *X86Macros) LoadFPUState() string {
|
||||
i := 0
|
||||
for ; i < 108; i += p.PTRSIZE() {
|
||||
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1]
|
||||
}
|
||||
return fmt.Sprintf("frstor [%[1]v]\nadd %[1]v, %[2]v", p.SP(), i)
|
||||
}
|
||||
|
||||
func (p *X86Macros) Stack(name string) (string, error) {
|
||||
for i, k := range p.Stacklocs {
|
||||
if k == name {
|
||||
pos := len(p.Stacklocs) - i - 1
|
||||
if p.Amd64 {
|
||||
pos = pos * 8
|
||||
} else {
|
||||
pos = pos * 4
|
||||
}
|
||||
if pos != 0 {
|
||||
return fmt.Sprintf("%v + %v", p.SP(), pos), nil
|
||||
}
|
||||
return p.SP(), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("unknown symbol %v", name)
|
||||
}
|
||||
|
||||
func (p *X86Macros) FmtStack() string {
|
||||
var b strings.Builder
|
||||
last := len(p.Stacklocs) - 1
|
||||
for i := range p.Stacklocs {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(p.Stacklocs[last-i])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (p *X86Macros) ExportFunc(name string, params ...string) string {
|
||||
if !p.Amd64 {
|
||||
reverseParams := make([]string, len(params))
|
||||
for i, param := range params {
|
||||
reverseParams[len(params)-1-i] = param
|
||||
}
|
||||
p.Stacklocs = append(reverseParams, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
|
||||
if p.OS == "windows" {
|
||||
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4)
|
||||
}
|
||||
}
|
||||
if p.OS == "darwin" {
|
||||
return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name)
|
||||
}
|
||||
return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name)
|
||||
}
|
||||
|
||||
func (p *X86Macros) Input(unit string, port string) (string, error) {
|
||||
i := p.features.InputNumber(unit, port)
|
||||
if i != 0 {
|
||||
return fmt.Sprintf("%v + %v", p.INP(), i*4), nil
|
||||
}
|
||||
return p.INP(), nil
|
||||
}
|
||||
|
||||
func (p *X86Macros) Modulation(unit string, port string) (string, error) {
|
||||
i := p.features.InputNumber(unit, port)
|
||||
return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil
|
||||
}
|
||||
|
||||
func (p *X86Macros) Prepare(value string, regs ...string) (string, error) {
|
||||
if p.Amd64 {
|
||||
if len(regs) > 1 {
|
||||
return "", fmt.Errorf("macro Prepare cannot accept more than one register parameter")
|
||||
} else if len(regs) > 0 {
|
||||
return fmt.Sprintf("\nmov r9, qword %v\nlea r9, [r9 + %v]", value, regs[0]), nil
|
||||
}
|
||||
return fmt.Sprintf("\nmov r9, qword %v", value), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *X86Macros) Use(value string, regs ...string) (string, error) {
|
||||
if p.Amd64 {
|
||||
return "r9", nil
|
||||
}
|
||||
if len(regs) > 1 {
|
||||
return "", fmt.Errorf("macro Use cannot accept more than one register parameter")
|
||||
} else if len(regs) > 0 {
|
||||
return value + " + " + regs[0], nil
|
||||
}
|
||||
return value, nil
|
||||
}
|
209
vm/featureset.go
Normal file
209
vm/featureset.go
Normal file
@ -0,0 +1,209 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
// FeatureSet defines what opcodes / parameters are included in the compiled virtual machine
|
||||
// It is used by the compiler to decide how to encode opcodes
|
||||
type FeatureSet interface {
|
||||
Opcode(unitType string) (int, bool)
|
||||
TransformCount(unitType string) int
|
||||
Instructions() []string
|
||||
InputNumber(unitType string, paramName string) int
|
||||
SupportsParamValue(unitType string, paramName string, value int) bool
|
||||
SupportsParamValueOtherThan(unitType string, paramName string, value int) bool
|
||||
SupportsModulation(unitType string, paramName string) bool
|
||||
SupportsPolyphony() bool
|
||||
SupportsGlobalSend() bool
|
||||
}
|
||||
|
||||
type Instruction struct {
|
||||
Name string
|
||||
TransformCount int
|
||||
}
|
||||
|
||||
type paramKey struct {
|
||||
Unit string
|
||||
Param string
|
||||
}
|
||||
|
||||
type paramValueKey struct {
|
||||
Unit string
|
||||
Param string
|
||||
Value int
|
||||
}
|
||||
|
||||
// AllFeatures is used by the library compilation / bridging to configure a virtual machine
|
||||
// that supports every conceivable parameter, so it needs no members and just returns "true" to all
|
||||
// queries about what it supports. Contrast this NecessaryFeatures that only returns true if the patch
|
||||
// needs support for that feature
|
||||
type AllFeatures struct {
|
||||
}
|
||||
|
||||
func (_ AllFeatures) SupportsParamValue(unit string, paramName string, value int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (_ AllFeatures) SupportsParamValueOtherThan(unit string, paramName string, value int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (_ AllFeatures) SupportsModulation(unit string, port string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (_ AllFeatures) SupportsPolyphony() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (_ AllFeatures) SupportsGlobalSend() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (_ AllFeatures) Opcode(unitType string) (int, bool) {
|
||||
code, ok := allOpcodes[unitType]
|
||||
return code, ok
|
||||
}
|
||||
|
||||
func (_ AllFeatures) TransformCount(unitType string) int {
|
||||
return allTransformCounts[unitType]
|
||||
}
|
||||
|
||||
func (_ AllFeatures) Instructions() []string {
|
||||
return allInstructions
|
||||
}
|
||||
|
||||
func (_ AllFeatures) InputNumber(unitType string, paramName string) int {
|
||||
return allInputs[paramKey{unitType, paramName}]
|
||||
}
|
||||
|
||||
var allOpcodes map[string]int
|
||||
var allInstructions []string
|
||||
var allInputs map[paramKey]int
|
||||
var allTransformCounts map[string]int
|
||||
|
||||
func init() {
|
||||
allInstructions = make([]string, len(sointu.UnitTypes))
|
||||
allOpcodes = map[string]int{}
|
||||
allTransformCounts = map[string]int{}
|
||||
allInputs = map[paramKey]int{}
|
||||
i := 0
|
||||
for k, v := range sointu.UnitTypes {
|
||||
inputCount := 0
|
||||
transformCount := 0
|
||||
for _, t := range v {
|
||||
if t.CanModulate {
|
||||
allInputs[paramKey{k, t.Name}] = inputCount
|
||||
inputCount++
|
||||
}
|
||||
if t.CanModulate && t.CanSet {
|
||||
transformCount++
|
||||
}
|
||||
}
|
||||
allInstructions[i] = k // Opcode 0 is reserved for instrument advance, so opcodes start from 1
|
||||
allTransformCounts[k] = transformCount
|
||||
i++
|
||||
}
|
||||
sort.Strings(allInstructions) // sort the opcodes to have predictable ordering, as maps don't guarantee the order the items
|
||||
for i, instruction := range allInstructions {
|
||||
allOpcodes[instruction] = (i + 1) * 2 // make a map to find out the opcode number based on the type
|
||||
}
|
||||
}
|
||||
|
||||
// NecessaryFeatures returns true only if the patch actually needs the support for the feature
|
||||
type NecessaryFeatures struct {
|
||||
opcodes map[string]int
|
||||
instructions []string
|
||||
supportsParamValue map[paramKey](map[int]bool)
|
||||
supportsModulation map[paramKey]bool
|
||||
globalSend bool
|
||||
polyphony bool
|
||||
}
|
||||
|
||||
func NecessaryFeaturesFor(patch sointu.Patch) NecessaryFeatures {
|
||||
features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
|
||||
for instrIndex, instrument := range patch {
|
||||
for _, unit := range instrument.Units {
|
||||
if _, ok := features.opcodes[unit.Type]; !ok {
|
||||
features.instructions = append(features.instructions, unit.Type)
|
||||
features.opcodes[unit.Type] = len(features.instructions) * 2 // note that the first opcode gets value 1, as 0 is always reserved for advance
|
||||
}
|
||||
for k, v := range unit.Parameters {
|
||||
key := paramKey{unit.Type, k}
|
||||
if features.supportsParamValue[key] == nil {
|
||||
features.supportsParamValue[key] = map[int]bool{}
|
||||
}
|
||||
features.supportsParamValue[key][v] = true
|
||||
}
|
||||
if unit.Type == "send" {
|
||||
targetInstrIndex, targetUnitIndex, err := patch.FindSendTarget(unit.Parameters["target"])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
targetUnit := patch[targetInstrIndex].Units[targetUnitIndex]
|
||||
portList := sointu.Ports[targetUnit.Type]
|
||||
portIndex := unit.Parameters["port"]
|
||||
if portIndex < 0 || portIndex >= len(portList) {
|
||||
continue
|
||||
}
|
||||
if targetInstrIndex != instrIndex || unit.Parameters["voice"] > 0 {
|
||||
features.globalSend = true
|
||||
}
|
||||
features.supportsModulation[paramKey{targetUnit.Type, portList[portIndex]}] = true
|
||||
}
|
||||
}
|
||||
if instrument.NumVoices > 1 {
|
||||
features.polyphony = true
|
||||
}
|
||||
}
|
||||
return features
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) SupportsParamValue(unit string, paramName string, value int) bool {
|
||||
m, ok := n.supportsParamValue[paramKey{unit, paramName}]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return m[value]
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) SupportsParamValueOtherThan(unit string, paramName string, value int) bool {
|
||||
for paramValue := range n.supportsParamValue[paramKey{unit, paramName}] {
|
||||
if paramValue != value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) SupportsModulation(unit string, param string) bool {
|
||||
return n.supportsModulation[paramKey{unit, param}]
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) SupportsPolyphony() bool {
|
||||
return n.polyphony
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) Opcode(unitType string) (int, bool) {
|
||||
code, ok := n.opcodes[unitType]
|
||||
return code, ok
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) Instructions() []string {
|
||||
return n.instructions
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) InputNumber(unitType string, paramName string) int {
|
||||
return allInputs[paramKey{unitType, paramName}]
|
||||
}
|
||||
|
||||
func (_ NecessaryFeatures) TransformCount(unitType string) int {
|
||||
return allTransformCounts[unitType]
|
||||
}
|
||||
|
||||
func (n NecessaryFeatures) SupportsGlobalSend() bool {
|
||||
return n.globalSend
|
||||
}
|
203
vm/patterns.go
Normal file
203
vm/patterns.go
Normal file
@ -0,0 +1,203 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
// fixPatternLength makes sure that every pattern is the same length. During
|
||||
// composing. Patterns shorter than the given length are padded with 1 / "hold";
|
||||
// patterns longer than the given length are cropped.
|
||||
func fixPatternLength(patterns [][]byte, fixedLength int) [][]int {
|
||||
patternData := make([]int, len(patterns)*fixedLength)
|
||||
ret := make([][]int, len(patterns))
|
||||
for i, pat := range patterns {
|
||||
for j, note := range pat {
|
||||
patternData[j] = int(note)
|
||||
}
|
||||
for j := len(pat); j < fixedLength; j++ {
|
||||
patternData[j] = 1 // pad with hold
|
||||
}
|
||||
ret[i], patternData = patternData[:fixedLength], patternData[fixedLength:]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// flattenSequence looks up a sequence of patterns and concatenates them into a
|
||||
// single linear array of notes. Note that variable length patterns are
|
||||
// concatenated as such; call fixPatternLength first if you want every pattern
|
||||
// to be constant length.
|
||||
func flattenSequence(patterns [][]int, sequence []int) []int {
|
||||
sumLen := 0
|
||||
for _, patIndex := range sequence {
|
||||
if patIndex < 0 || patIndex >= len(patterns) {
|
||||
continue
|
||||
}
|
||||
sumLen += len(patterns[patIndex])
|
||||
}
|
||||
notes := make([]int, sumLen)
|
||||
window := notes
|
||||
for _, patIndex := range sequence {
|
||||
if patIndex < 0 || patIndex >= len(patterns) {
|
||||
continue
|
||||
}
|
||||
elementsCopied := copy(window, patterns[patIndex])
|
||||
window = window[elementsCopied:]
|
||||
}
|
||||
return notes
|
||||
}
|
||||
|
||||
// markDontCares goes through a linear array of notes and marks every hold (1)
|
||||
// or release (0) after the first release (0) as -1 or "don't care". This means
|
||||
// that for -1:s, we don't care if it's a hold or release; it does not affect
|
||||
// the sound as the note has been already released.
|
||||
func markDontCares(notes []int) []int {
|
||||
notesWithDontCares := make([]int, len(notes))
|
||||
dontCare := false
|
||||
for i, n := range notes {
|
||||
if dontCare && n <= 1 {
|
||||
notesWithDontCares[i] = -1
|
||||
} else {
|
||||
notesWithDontCares[i] = n
|
||||
dontCare = n == 0
|
||||
}
|
||||
}
|
||||
return notesWithDontCares
|
||||
}
|
||||
|
||||
// replaceInt replaces all occurrences of needle in the haystack with the value
|
||||
// "with"
|
||||
func replaceInts(haystack []int, needle int, with int) {
|
||||
for i, v := range haystack {
|
||||
if v == needle {
|
||||
haystack[i] = with
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splitSequence splits a linear sequence of notes into patternLength size
|
||||
// chunks. If the last chunk is shorter than the patternLength, then it is
|
||||
// padded with dontCares (-1).
|
||||
func splitSequence(sequence []int, patternLength int) [][]int {
|
||||
numChunksRoundedUp := (len(sequence) + patternLength - 1) / patternLength
|
||||
chunks := make([][]int, numChunksRoundedUp)
|
||||
for i := range chunks {
|
||||
if len(sequence) >= patternLength {
|
||||
chunks[i], sequence = sequence[:patternLength], sequence[patternLength:]
|
||||
} else {
|
||||
padded := make([]int, patternLength)
|
||||
j := copy(padded, sequence)
|
||||
for ; j < patternLength; j++ {
|
||||
padded[j] = -1
|
||||
}
|
||||
chunks[i] = padded
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
// addPatternsToTable adds given patterns to the table, checking if existing
|
||||
// pattern could be used. DontCares are taken into account so a pattern that has
|
||||
// don't care where another has a hold or release is ok. It returns a 1D
|
||||
// sequence of indices of each added pattern in the updated pattern table & the
|
||||
// updated pattern table.
|
||||
func addPatternsToTable(patterns [][]int, table [][]int) ([]int, [][]int) {
|
||||
updatedTable := make([][]int, len(table))
|
||||
copy(updatedTable, table) // avoid updating the underlying slices for concurrency safety
|
||||
sequence := make([]int, len(patterns))
|
||||
for i, pat := range patterns {
|
||||
// go through the current pattern table to see if there's already a
|
||||
// pattern that could be used
|
||||
patternIndex := -1
|
||||
for j, p := range updatedTable {
|
||||
match := true
|
||||
identical := true
|
||||
for k, n := range p {
|
||||
if (n > -1 && pat[k] > -1 && n != pat[k]) ||
|
||||
(n == -1 && pat[k] > 1) ||
|
||||
(n > 1 && pat[k] == -1) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
if n != pat[i] {
|
||||
identical = false
|
||||
}
|
||||
}
|
||||
if match {
|
||||
if !identical {
|
||||
// the patterns were not identical; one of them had don't
|
||||
// cares where another had hold or release so we make a new
|
||||
// copy with merged data, that essentially is a max of the
|
||||
// two patterns
|
||||
mergedPat := make([]int, len(p))
|
||||
copy(mergedPat, p) // make a copy instead of updating existing, for concurrency safety
|
||||
for k, n := range pat {
|
||||
if n != -1 {
|
||||
mergedPat[k] = n
|
||||
}
|
||||
}
|
||||
updatedTable[j] = mergedPat
|
||||
}
|
||||
patternIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if patternIndex == -1 {
|
||||
patternIndex = len(updatedTable)
|
||||
updatedTable = append(updatedTable, pat)
|
||||
}
|
||||
sequence[i] = patternIndex
|
||||
}
|
||||
return sequence, updatedTable
|
||||
}
|
||||
|
||||
func intsToBytes(array []int) ([]byte, error) {
|
||||
ret := make([]byte, len(array))
|
||||
for i, v := range array {
|
||||
if v < 0 || v > 255 {
|
||||
return nil, fmt.Errorf("when converting intsToBytes, all values should be 0 .. 255 (was: %v)", v)
|
||||
}
|
||||
ret[i] = byte(v)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func bytesToInts(array []byte) []int {
|
||||
ret := make([]int, len(array))
|
||||
for i, v := range array {
|
||||
ret[i] = int(v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func ConstructPatterns(song *sointu.Song) ([][]byte, [][]byte, error) {
|
||||
patLength := song.Score.RowsPerPattern
|
||||
sequences := make([][]byte, len(song.Score.Tracks))
|
||||
var patterns [][]int
|
||||
for i, t := range song.Score.Tracks {
|
||||
fixed := fixPatternLength(t.Patterns, patLength)
|
||||
flat := flattenSequence(fixed, t.Order)
|
||||
dontCares := markDontCares(flat)
|
||||
// TODO: we could give the user the possibility to use another length during encoding that during composing
|
||||
chunks := splitSequence(dontCares, patLength)
|
||||
var sequence []int
|
||||
sequence, patterns = addPatternsToTable(chunks, patterns)
|
||||
var err error
|
||||
sequences[i], err = intsToBytes(sequence)
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("the constructed pattern table would result in > 256 unique patterns; only 256 unique patterns are supported")
|
||||
}
|
||||
}
|
||||
bytePatterns := make([][]byte, len(patterns))
|
||||
for i, pat := range patterns {
|
||||
var err error
|
||||
replaceInts(pat, -1, 0) // replace don't cares with releases
|
||||
bytePatterns[i], err = intsToBytes(pat)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid note in pattern, notes should be 0 .. 255: %v", err)
|
||||
}
|
||||
}
|
||||
return bytePatterns, sequences, nil
|
||||
}
|
90
vm/patterns_test.go
Normal file
90
vm/patterns_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package vm_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
func TestPatternReusing(t *testing.T) {
|
||||
song := sointu.Song{
|
||||
Score: sointu.Score{
|
||||
RowsPerPattern: 8,
|
||||
Tracks: []sointu.Track{{
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}},
|
||||
Order: []int{0, 1},
|
||||
}, {
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}},
|
||||
Order: []int{0, 1},
|
||||
}},
|
||||
},
|
||||
}
|
||||
patterns, sequences, err := vm.ConstructPatterns(&song)
|
||||
if err != nil {
|
||||
t.Fatalf("erorr constructing patterns: %v", err)
|
||||
}
|
||||
expectedSequences := [][]byte{{0, 1}, {0, 2}}
|
||||
expectedPatterns := [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}}
|
||||
if !reflect.DeepEqual(patterns, expectedPatterns) {
|
||||
t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns)
|
||||
}
|
||||
if !reflect.DeepEqual(sequences, expectedSequences) {
|
||||
t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnnecessaryHolds(t *testing.T) {
|
||||
song := sointu.Song{
|
||||
Score: sointu.Score{
|
||||
RowsPerPattern: 8,
|
||||
Tracks: []sointu.Track{{
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 1, 0, 0}, {72, 0, 1, 0, 1, 0, 0, 0}},
|
||||
Order: []int{0, 1},
|
||||
}, {
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 1, 0}, {84, 0, 0, 0, 1, 1, 0, 0}},
|
||||
Order: []int{0, 1},
|
||||
}}},
|
||||
}
|
||||
patterns, sequences, err := vm.ConstructPatterns(&song)
|
||||
if err != nil {
|
||||
t.Fatalf("erorr constructing patterns: %v", err)
|
||||
}
|
||||
expectedSequences := [][]byte{{0, 1}, {0, 2}}
|
||||
expectedPatterns := [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}}
|
||||
if !reflect.DeepEqual(patterns, expectedPatterns) {
|
||||
t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns)
|
||||
}
|
||||
if !reflect.DeepEqual(sequences, expectedSequences) {
|
||||
t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDontCares(t *testing.T) {
|
||||
song := sointu.Song{
|
||||
Score: sointu.Score{
|
||||
Length: 2,
|
||||
RowsPerPattern: 8,
|
||||
Tracks: []sointu.Track{{
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
Order: []int{0, 1},
|
||||
}, {
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 0, 0, 0, 0}},
|
||||
Order: []int{0, 1},
|
||||
}},
|
||||
},
|
||||
}
|
||||
patterns, sequences, err := vm.ConstructPatterns(&song)
|
||||
if err != nil {
|
||||
t.Fatalf("erorr constructing patterns: %v", err)
|
||||
}
|
||||
expectedSequences := [][]byte{{0, 1}, {2, 1}}
|
||||
expectedPatterns := [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 0, 0, 0, 0, 0}, {64, 1, 1, 1, 1, 1, 1, 1}}
|
||||
if !reflect.DeepEqual(patterns, expectedPatterns) {
|
||||
t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns)
|
||||
}
|
||||
if !reflect.DeepEqual(sequences, expectedSequences) {
|
||||
t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user