reorganize things into different packages

This commit is contained in:
vsariola
2021-03-02 17:19:45 +02:00
parent e46ece3648
commit a035845b81
19 changed files with 43 additions and 38 deletions

198
vm/bytepatch.go Normal file
View 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)
}
}

View 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, ", ")
}

View 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
}

View 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
View 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
}

View 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,
}
}

View 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)
}

View 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)
}

View 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
View 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
View 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
View 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
View 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)
}
}