feat(asm&go4k): Rewrote both library & player to use text/template compiler

There is no more plain .asms, both library & player are created from the templates using go text/template package.
This commit is contained in:
Veikko Sariola
2020-12-14 15:46:12 +02:00
parent 2ad61ff6b2
commit d0bd877b3f
141 changed files with 1195 additions and 5542 deletions

View File

@ -1,170 +0,0 @@
package go4k
// FindSuperIntArray finds a small super array containing all
// the subarrays passed to it. Returns the super array and indices where
// the subarrays can be found. For example:
// FindSuperIntArray([][]int{{4,5,6},{1,2,3},{3,4}})
// returns {1,2,3,4,5,6},{3,0,2}
// Implemented using a greedy search, so does not necessarily find
// the true optimal (the problem is NP-hard and analogous to traveling
// salesman problem).
//
// Used to construct a small delay time table without unnecessary repetition
// of delay times.
func FindSuperIntArray(arrays [][]int) ([]int, []int) {
// If we go past MAX_MERGES, the algorithm could get slow and hang the computer
// So this is a safety limit: after this problem size, just merge any arrays
// until we get into more manageable range
const maxMerges = 1000
min := func(a int, b int) int {
if a < b {
return a
}
return b
}
overlap := func(a []int, b []int) (int, int) {
minShift := len(a)
for shift := len(a) - 1; shift >= 0; shift-- {
overlapping := true
for k := shift; k < min(len(a), len(b)+shift); k++ {
if a[k] != b[k-shift] {
overlapping = false
break
}
}
if overlapping {
minShift = shift
}
}
overlap := min(len(a)-minShift, len(b))
return overlap, minShift
}
sliceNumbers := make([]int, len(arrays))
startIndices := make([]int, len(arrays))
var processedArrays [][]int
for i := range arrays {
if len(arrays[i]) == 0 {
// Zero length arrays do not need to be processed at all
// They will 'start' at index 0 always as they have no length.
sliceNumbers[i] = -1
} else {
sliceNumbers[i] = len(processedArrays)
processedArrays = append(processedArrays, arrays[i])
}
}
if len(processedArrays) == 0 {
return []int{}, startIndices // no arrays with len>0 to process, just return empty array and all indices as 0
}
for len(processedArrays) > 1 { // there's at least two candidates that could be be merged
maxO, maxI, maxJ, maxS := -1, -1, -1, -1
if len(processedArrays) < maxMerges {
// find the pair i,j that results in the largest overlap with array i coming first, followed by potentially overlapping array j
for i := range processedArrays {
for j := range processedArrays {
if i == j {
continue
}
overlap, shift := overlap(processedArrays[i], processedArrays[j])
if overlap > maxO {
maxI, maxJ, maxO, maxS = i, j, overlap, shift
}
}
}
} else {
// The task is daunting, we have over MAX_MERGES overlaps to test. Just merge two first ones until the task is more manageable size
overlap, shift := overlap(processedArrays[0], processedArrays[1])
maxI, maxJ, maxO, maxS = 0, 1, overlap, shift
}
for k := range sliceNumbers {
if sliceNumbers[k] == maxJ {
// update slice pointers to point maxI instead of maxJ (maxJ will be appended to maxI, taking overlap into account)
sliceNumbers[k] = maxI
startIndices[k] += maxS // the array j starts at index maxS in array i
}
if sliceNumbers[k] > maxJ {
// pointers maxJ reduced by 1 as maxJ will be deleted
sliceNumbers[k]--
}
}
// if array j was not entirely included within array j
if maxO < len(processedArrays[maxJ]) {
// append array maxJ to array maxI, without duplicating the overlapping part
processedArrays[maxI] = append(processedArrays[maxI], processedArrays[maxJ][maxO:]...)
}
// finally, remove element maxJ from processedArrays
processedArrays = append(processedArrays[:maxJ], processedArrays[maxJ+1:]...)
}
return processedArrays[0], startIndices // there should be only one slice left in the arrays after the loop
}
// ConstructDelayTimeTable tries to construct the delay times table
// abusing overlapping between different delay times tables as much
// as possible. Especially: if two delay units use exactly the same
// delay times, they appear in the table only once.
func ConstructDelayTimeTable(patch Patch) ([]int, [][]int) {
ind := make([][]int, len(patch.Instruments))
var subarrays [][]int
// flatten the delay times into one array of arrays
// saving the indices where they were placed
for i, instr := range patch.Instruments {
ind[i] = make([]int, len(instr.Units))
for j, unit := range instr.Units {
// only include delay times for delays. Only delays
// should use delay times
if unit.Type == "delay" {
ind[i][j] = len(subarrays)
end := unit.Parameters["count"]
if unit.Parameters["stereo"] > 0 {
end *= 2
}
subarrays = append(subarrays, patch.DelayTimes[unit.Parameters["delay"]:end])
}
}
}
delayTable, indices := FindSuperIntArray(subarrays)
// cancel the flattening, so unitindices can be used to
// to find the index of each delay in the delay table
unitindices := make([][]int, len(patch.Instruments))
for i, instr := range patch.Instruments {
unitindices[i] = make([]int, len(instr.Units))
for j, unit := range instr.Units {
if unit.Type == "delay" {
unitindices[i][j] = indices[ind[i][j]]
}
}
}
return delayTable, unitindices
}
// ConstructSampleOffsetTable collects the sample offests from
// all sample-based oscillators and collects them in a table,
// so that they appear in the table only once. Returns the collected
// table and [][]int array where element [i][j] is the index in the
// table by instrument i / unit j (units other than sample oscillators
// have the value 0)
func ConstructSampleOffsetTable(patch Patch) ([]SampleOffset, [][]int) {
unitindices := make([][]int, len(patch.Instruments))
var offsetTable []SampleOffset
offsetMap := map[SampleOffset]int{}
for i, instr := range patch.Instruments {
unitindices[i] = make([]int, len(instr.Units))
for j, unit := range instr.Units {
if unit.Type == "oscillator" && unit.Parameters["type"] == Sample {
offset := SampleOffset{
Start: unit.Parameters["start"],
LoopStart: unit.Parameters["loopstart"],
LoopLength: unit.Parameters["looplength"],
}
if ind, ok := offsetMap[offset]; ok {
unitindices[i][j] = ind // the sample has been already added to table, reuse the index
} else {
ind = len(offsetTable)
unitindices[i][j] = ind
offsetMap[offset] = ind
offsetTable = append(offsetTable, offset)
}
}
}
}
return offsetTable, unitindices
}

View File

@ -1,36 +0,0 @@
package go4k_test
import (
"fmt"
"reflect"
"testing"
"github.com/vsariola/sointu/go4k"
)
func TestFindSuperIntArray(t *testing.T) {
var tests = []struct {
input [][]int
wantSuper []int
wantIndices []int
}{
{[][]int{}, []int{}, []int{}},
{[][]int{nil, nil}, []int{}, []int{0, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}}, []int{1, 2, 3, 4, 5}, []int{2, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}, nil}, []int{1, 2, 3, 4, 5}, []int{2, 0, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}, {}}, []int{1, 2, 3, 4, 5}, []int{2, 0, 0}},
{[][]int{{3, 4, 5}, {1, 2, 3}, {2, 3}}, []int{1, 2, 3, 4, 5}, []int{2, 0, 1}},
{[][]int{{1, 2, 3, 4, 5}, {1, 2, 3}}, []int{1, 2, 3, 4, 5}, []int{0, 0}},
{[][]int{{1, 2, 3, 4, 5}, {2, 3}}, []int{1, 2, 3, 4, 5}, []int{0, 1}},
{[][]int{{1, 2, 3, 4, 5}, {2, 3}, {5, 6, 7}}, []int{1, 2, 3, 4, 5, 6, 7}, []int{0, 1, 4}},
{[][]int{{1, 2, 3, 4}, {3, 4, 1}, {2, 3, 4, 5}}, []int{3, 4, 1, 2, 3, 4, 5}, []int{2, 0, 3}},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("TestFindSuperIntArray %d", i), func(t *testing.T) {
super, indices := go4k.FindSuperIntArray(tt.input)
if !reflect.DeepEqual(super, tt.wantSuper) || !reflect.DeepEqual(indices, tt.wantIndices) {
t.Errorf("FindSuperIntArray(%v) got (%v,%v), want (%v,%v)", tt.input, super, indices, tt.wantSuper, tt.wantIndices)
}
})
}
}

View File

@ -1,346 +0,0 @@
package go4k
import (
"bufio"
"fmt"
"regexp"
"strconv"
"strings"
)
func ParseAsm(asmcode string) (*Song, error) {
paramReg, err := regexp.Compile(`(?:([a-zA-Z]\w*)\s*\(\s*([0-9]+)\s*\)|([0-9]+))`) // matches FOO(42), groups "FOO" and "42" OR just numbers
if err != nil {
return nil, fmt.Errorf("error compiling paramReg: %v", err)
}
parseParams := func(s string) (map[string]int, []int, error) {
matches := paramReg.FindAllStringSubmatch(s, 256)
namedParams := map[string]int{}
unnamedParams := make([]int, 0)
for _, match := range matches {
if match[1] == "" { // the second part of OR fired
val, err := strconv.Atoi(match[3])
if err != nil {
return nil, nil, fmt.Errorf("error converting %v to integer, which is unexpected as regexp matches only numbers: %v", match[3], err)
}
unnamedParams = append(unnamedParams, val)
} else {
val, err := strconv.Atoi(match[2])
if err != nil {
return nil, nil, fmt.Errorf("error converting %v to integer, which is unexpected as regexp matches only numbers: %v", match[2], err)
}
namedParams[strings.ToLower(match[1])] = val
}
}
return namedParams, unnamedParams, nil
}
wordReg, err := regexp.Compile(`\s*([a-zA-Z_][a-zA-Z0-9_]*)([^;\n]*)`) // matches a word and "the rest", until newline or a comment
if err != nil {
return nil, err
}
toBytes := func(ints []int) []byte {
ret := []byte{}
for _, v := range ints {
ret = append(ret, byte(v))
}
return ret
}
var song Song
scanner := bufio.NewScanner(strings.NewReader(asmcode))
for scanner.Scan() {
line := scanner.Text()
macroMatch := wordReg.FindStringSubmatch(line)
if macroMatch == nil {
continue
}
word, rest := macroMatch[1], macroMatch[2]
namedParams, unnamedParams, err := parseParams(rest)
if err != nil {
return nil, fmt.Errorf("error parsing parameters: %v", err)
}
switch word {
case "BEGIN_SONG":
song = Song{BPM: namedParams["bpm"], Patterns: nil, Tracks: nil, Patch: Patch{}, Output16Bit: namedParams["output_16bit"] == 1, Hold: byte(namedParams["hold"])}
case "PATTERN":
song.Patterns = append(song.Patterns, toBytes(unnamedParams))
case "TRACK":
song.Tracks = append(song.Tracks, Track{namedParams["voices"], toBytes(unnamedParams)})
case "BEGIN_INSTRUMENT":
song.Patch.Instruments = append(song.Patch.Instruments, Instrument{NumVoices: namedParams["voices"], Units: []Unit{}})
case "DELTIME":
song.Patch.DelayTimes = append(song.Patch.DelayTimes, unnamedParams...)
case "SAMPLE_OFFSET":
song.Patch.SampleOffsets = append(song.Patch.SampleOffsets, SampleOffset{
Start: namedParams["start"],
LoopStart: namedParams["loopstart"],
LoopLength: namedParams["looplength"]})
}
if strings.HasPrefix(word, "SU_") {
unittype := strings.ToLower(word[3:])
unit := Unit{Type: unittype, Parameters: namedParams}
lastIndex := len(song.Patch.Instruments) - 1
if lastIndex < 0 {
return nil, fmt.Errorf("opcode %v before BEGIN_INSTRUMENT", word)
}
song.Patch.Instruments[lastIndex].Units = append(song.Patch.Instruments[lastIndex].Units, unit)
}
}
return &song, nil
}
func FormatAsm(song *Song) (string, error) {
paramorder := map[string][]string{
"add": []string{"stereo"},
"addp": []string{"stereo"},
"pop": []string{"stereo"},
"loadnote": []string{"stereo"},
"mul": []string{"stereo"},
"mulp": []string{"stereo"},
"push": []string{"stereo"},
"xch": []string{"stereo"},
"distort": []string{"stereo", "drive"},
"hold": []string{"stereo", "holdfreq"},
"crush": []string{"stereo", "resolution"},
"gain": []string{"stereo", "gain"},
"invgain": []string{"stereo", "invgain"},
"filter": []string{"stereo", "frequency", "resonance", "lowpass", "bandpass", "highpass", "negbandpass", "neghighpass"},
"clip": []string{"stereo"},
"pan": []string{"stereo", "panning"},
"delay": []string{"stereo", "pregain", "dry", "feedback", "damp", "delay", "count", "notetracking"},
"compressor": []string{"stereo", "attack", "release", "invgain", "threshold", "ratio"},
"speed": []string{},
"out": []string{"stereo", "gain"},
"outaux": []string{"stereo", "outgain", "auxgain"},
"aux": []string{"stereo", "gain", "channel"},
"send": []string{"stereo", "amount", "voice", "unit", "port", "sendpop"},
"envelope": []string{"stereo", "attack", "decay", "sustain", "release", "gain"},
"noise": []string{"stereo", "shape", "gain"},
"oscillator": []string{"stereo", "transpose", "detune", "phase", "color", "shape", "gain", "type", "lfo", "unison"},
"loadval": []string{"stereo", "value"},
"receive": []string{"stereo"},
"in": []string{"stereo", "channel"},
}
indentation := 0
indent := func() string {
return strings.Repeat(" ", indentation*4)
}
var b strings.Builder
println := func(format string, params ...interface{}) {
if len(format) > 0 {
fmt.Fprintf(&b, "%v", indent())
fmt.Fprintf(&b, format, params...)
}
fmt.Fprintf(&b, "\n")
}
align := func(table [][]string, format string) [][]string {
var maxwidth []int
// find the maximum width of each column
for _, row := range table {
for k, elem := range row {
l := len(elem)
if len(maxwidth) <= k {
maxwidth = append(maxwidth, l)
} else {
if maxwidth[k] < l {
maxwidth[k] = l
}
}
}
}
// align each column, depending on the specified formatting
for _, row := range table {
for k, elem := range row {
l := len(elem)
var f byte
if k >= len(format) {
f = format[len(format)-1] // repeat the last format specifier for all remaining columns
} else {
f = format[k]
}
switch f {
case 'n': // no alignment
row[k] = elem
case 'l': // left align
row[k] = elem + strings.Repeat(" ", maxwidth[k]-l)
case 'r': // right align
row[k] = strings.Repeat(" ", maxwidth[k]-l) + elem
}
}
}
return table
}
printTable := func(table [][]string) {
indentation++
for _, row := range table {
println("%v %v", row[0], strings.Join(row[1:], ","))
}
indentation--
}
// delay modulation is pretty much the only %define that the asm preprocessor cannot figure out
// as the preprocessor has no clue if a SEND modulates a delay unit. So, unfortunately, for the
// time being, we need to figure during export if INCLUDE_DELAY_MODULATION needs to be defined.
delaymod := 0
for i, instrument := range song.Patch.Instruments {
for j, unit := range instrument.Units {
if unit.Type == "send" {
targetInstrument := i
if unit.Parameters["voice"] > 0 {
v, err := song.Patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
return "", fmt.Errorf("INSTRUMENT #%v / SEND #%v targets voice %v, which does not exist", i, j, unit.Parameters["voice"])
}
targetInstrument = v
}
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(song.Patch.Instruments[targetInstrument].Units) {
return "", fmt.Errorf("INSTRUMENT #%v / SEND #%v target unit %v out of range", i, j, unit.Parameters["unit"])
}
if song.Patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]].Type == "delay" && unit.Parameters["port"] == 5 {
delaymod = 1
}
}
}
}
// The actual printing starts here
output_16bit := 0
if song.Output16Bit {
output_16bit = 1
}
println("%%include \"sointu/header.inc\"\n")
println("BEGIN_SONG BPM(%v),OUTPUT_16BIT(%v),CLIP_OUTPUT(0),DELAY_MODULATION(%v),HOLD(%v)\n", song.BPM, output_16bit, delaymod, song.Hold)
var patternTable [][]string
for _, pattern := range song.Patterns {
row := []string{"PATTERN"}
for _, v := range pattern {
row = append(row, strconv.Itoa(int(v)))
}
patternTable = append(patternTable, row)
}
println("BEGIN_PATTERNS")
printTable(align(patternTable, "lr"))
println("END_PATTERNS\n")
var trackTable [][]string
for _, track := range song.Tracks {
row := []string{"TRACK", fmt.Sprintf("VOICES(%d)", track.NumVoices)}
for _, v := range track.Sequence {
row = append(row, strconv.Itoa(int(v)))
}
trackTable = append(trackTable, row)
}
println("BEGIN_TRACKS")
printTable(align(trackTable, "lr"))
println("END_TRACKS\n")
println("BEGIN_PATCH")
indentation++
for _, instrument := range song.Patch.Instruments {
var instrTable [][]string
for _, unit := range instrument.Units {
row := []string{fmt.Sprintf("SU_%v", strings.ToUpper(unit.Type))}
for _, parname := range paramorder[unit.Type] {
if v, ok := unit.Parameters[parname]; ok {
row = append(row, fmt.Sprintf("%v(%v)", strings.ToUpper(parname), strconv.Itoa(int(v))))
} else {
return "", fmt.Errorf("The parameter map for unit %v does not contain %v, even though it should", unit.Type, parname)
}
}
instrTable = append(instrTable, row)
}
println("BEGIN_INSTRUMENT VOICES(%d)", instrument.NumVoices)
printTable(align(instrTable, "ln"))
println("END_INSTRUMENT")
}
indentation--
println("END_PATCH\n")
if len(song.Patch.DelayTimes) > 0 {
var delStrTable [][]string
for _, v := range song.Patch.DelayTimes {
row := []string{"DELTIME", strconv.Itoa(int(v))}
delStrTable = append(delStrTable, row)
}
println("BEGIN_DELTIMES")
printTable(align(delStrTable, "lr"))
println("END_DELTIMES\n")
}
if len(song.Patch.SampleOffsets) > 0 {
var samStrTable [][]string
for _, v := range song.Patch.SampleOffsets {
samStrTable = append(samStrTable, []string{
"SAMPLE_OFFSET",
fmt.Sprintf("START(%d)", v.Start),
fmt.Sprintf("LOOPSTART(%d)", v.LoopStart),
fmt.Sprintf("LOOPLENGTH(%d)", v.LoopLength),
})
}
println("BEGIN_SAMPLE_OFFSETS")
printTable(align(samStrTable, "r"))
println("END_SAMPLE_OFFSETS\n")
}
println("END_SONG")
ret := b.String()
return ret, nil
}
func CHeader(song *Song, maxSamples int) string {
template :=
`// auto-generated by Sointu, editing not recommended
#ifndef SU_RENDER_H
#define SU_RENDER_H
#define SU_MAX_SAMPLES %v
#define SU_BUFFER_LENGTH (SU_MAX_SAMPLES*2)
#define SU_SAMPLE_RATE 44100
#define SU_BPM %v
#define SU_PATTERN_SIZE %v
#define SU_MAX_PATTERNS %v
#define SU_TOTAL_ROWS (SU_MAX_PATTERNS*SU_PATTERN_SIZE)
#define SU_SAMPLES_PER_ROW (SU_SAMPLE_RATE*4*60/(SU_BPM*16))
#include <stdint.h>
#if UINTPTR_MAX == 0xffffffff
#if defined(__clang__) || defined(__GNUC__)
#define SU_CALLCONV __attribute__ ((stdcall))
#elif defined(_WIN32)
#define SU_CALLCONV __stdcall
#endif
#else
#define SU_CALLCONV
#endif
typedef %v SUsample;
#define SU_SAMPLE_RANGE %v
#ifdef __cplusplus
extern "C" {
#endif
void SU_CALLCONV su_render_song(SUsample *buffer);
%v
#ifdef __cplusplus
}
#endif
#endif
`
maxSamplesText := "SU_TOTAL_ROWS*SU_SAMPLES_PER_ROW"
if maxSamples > 0 {
maxSamplesText = fmt.Sprintf("%v", maxSamples)
}
sampleType := "float"
sampleRange := "1.0f"
if song.Output16Bit {
sampleType = "short"
sampleRange = "32768"
}
defineGmdls := ""
for _, instr := range song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == "oscillator" && unit.Parameters["type"] == Sample {
defineGmdls = "\n#define SU_LOAD_GMDLS\nvoid SU_CALLCONV su_load_gmdls(void);"
break
}
}
}
header := fmt.Sprintf(template, maxSamplesText, song.BPM, song.PatternRows(), song.SequenceLength(), sampleType, sampleRange, defineGmdls)
return header
}

View File

@ -1,4 +1,4 @@
package go4k_test
package bridge_test
import (
"bytes"
@ -9,19 +9,18 @@ import (
"os"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/bridge"
"gopkg.in/yaml.v2"
)
func TestAllAsmFiles(t *testing.T) {
bridge.Init()
_, myname, _, _ := runtime.Caller(0)
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.asm"))
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)
}
@ -37,89 +36,16 @@ func TestAllAsmFiles(t *testing.T) {
if err != nil {
t.Fatalf("cannot read the .asm file: %v", filename)
}
song, err := go4k.ParseAsm(string(asmcode))
var song go4k.Song
err = yaml.Unmarshal(asmcode, &song)
if err != nil {
t.Fatalf("could not parse the .asm file: %v", err)
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 := go4k.Play(synth, *song)
buffer = buffer[:song.TotalRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
}
if os.Getenv("GO4K_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)
}
}
if song.Output16Bit {
int16Buffer := convertToInt16Buffer(buffer)
compareToRawInt16(t, int16Buffer, testname+".raw")
} else {
compareToRawFloat32(t, buffer, testname+".raw")
}
})
}
}
func TestSerializingAllAsmFiles(t *testing.T) {
bridge.Init()
_, myname, _, _ := runtime.Caller(0)
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.asm"))
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)
}
song, err := go4k.ParseAsm(string(asmcode)) // read the asm
if err != nil {
t.Fatalf("could not parse the .asm file: %v", err)
}
str, err := go4k.FormatAsm(song) // serialize again
if err != nil {
t.Fatalf("Could not serialize asm file: %v", err)
}
song2, err := go4k.ParseAsm(str) // deserialize again. The rendered song should still give same results.
if err != nil {
t.Fatalf("could not parse the serialized asm code: %v", err)
}
if !reflect.DeepEqual(song, song2) {
t.Fatalf("serialize/deserialize does not result equal songs, before: %v, after %v", song, song2)
}
synth, err := bridge.Synth(song2.Patch)
if err != nil {
t.Fatalf("Compiling patch failed: %v", err)
}
buffer, err := go4k.Play(synth, *song2)
buffer, err := go4k.Play(synth, song)
buffer = buffer[:song.TotalRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
@ -157,7 +83,7 @@ func TestSerializingAllAsmFiles(t *testing.T) {
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))
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
@ -179,7 +105,7 @@ func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
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))
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}

View File

@ -1,74 +1,48 @@
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/go4k"
"github.com/vsariola/sointu/go4k/compiler"
)
// #cgo CFLAGS: -I"${SRCDIR}/../../include/sointu"
// #cgo LDFLAGS: "${SRCDIR}/../../build/libsointu.a"
// #include <sointu.h>
import "C"
type opTableEntry struct {
opcode C.int
parameterList []string
}
var opcodeTable = map[string]opTableEntry{
"add": opTableEntry{C.su_add_id, []string{}},
"addp": opTableEntry{C.su_addp_id, []string{}},
"pop": opTableEntry{C.su_pop_id, []string{}},
"loadnote": opTableEntry{C.su_loadnote_id, []string{}},
"mul": opTableEntry{C.su_mul_id, []string{}},
"mulp": opTableEntry{C.su_mulp_id, []string{}},
"push": opTableEntry{C.su_push_id, []string{}},
"xch": opTableEntry{C.su_xch_id, []string{}},
"distort": opTableEntry{C.su_distort_id, []string{"drive"}},
"hold": opTableEntry{C.su_hold_id, []string{"holdfreq"}},
"crush": opTableEntry{C.su_crush_id, []string{"resolution"}},
"gain": opTableEntry{C.su_gain_id, []string{"gain"}},
"invgain": opTableEntry{C.su_invgain_id, []string{"invgain"}},
"filter": opTableEntry{C.su_filter_id, []string{"frequency", "resonance"}},
"clip": opTableEntry{C.su_clip_id, []string{}},
"pan": opTableEntry{C.su_pan_id, []string{"panning"}},
"delay": opTableEntry{C.su_delay_id, []string{"pregain", "dry", "feedback", "damp", "delay", "count"}},
"compressor": opTableEntry{C.su_compres_id, []string{"attack", "release", "invgain", "threshold", "ratio"}},
"speed": opTableEntry{C.su_speed_id, []string{}},
"out": opTableEntry{C.su_out_id, []string{"gain"}},
"outaux": opTableEntry{C.su_outaux_id, []string{"outgain", "auxgain"}},
"aux": opTableEntry{C.su_aux_id, []string{"gain", "channel"}},
"send": opTableEntry{C.su_send_id, []string{"amount"}},
"envelope": opTableEntry{C.su_envelope_id, []string{"attack", "decay", "sustain", "release", "gain"}},
"noise": opTableEntry{C.su_noise_id, []string{"shape", "gain"}},
"oscillator": opTableEntry{C.su_oscillat_id, []string{"transpose", "detune", "phase", "color", "shape", "gain"}},
"loadval": opTableEntry{C.su_loadval_id, []string{"value"}},
"receive": opTableEntry{C.su_receive_id, []string{}},
"in": opTableEntry{C.su_in_id, []string{"channel"}},
}
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")
func Synth(patch go4k.Patch) (*C.Synth, error) {
s := new(C.Synth)
comPatch, err := compiler.Encode(&patch, compiler.AllFeatures{})
if err != nil {
return nil, fmt.Errorf("error compiling patch: %v", err)
}
if e.errcode&0x04 != 0 {
reasons = append(reasons, "FPU divide by zero")
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 e.errcode&0x01 != 0 {
reasons = append(reasons, "FPU invalid operation")
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")
}
if e.errcode&0x3800 != 0 {
reasons = append(reasons, "FPU stack push/pops are not balanced")
for i, v := range comPatch.Commands {
s.Commands[i] = (C.uchar)(v)
}
return "RenderError: " + strings.Join(reasons, ", ")
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
@ -101,131 +75,37 @@ func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) {
return int(samples), int(time), nil
}
func Synth(patch go4k.Patch) (*C.Synth, error) {
s := new(C.Synth)
totalVoices := 0
commands := make([]byte, 0)
values := make([]byte, 0)
polyphonyBitmask := 0
for insid, instr := range patch.Instruments {
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")
}
for unitid, unit := range instr.Units {
if val, ok := opcodeTable[unit.Type]; ok {
opCode := val.opcode
if unit.Parameters["stereo"] == 1 {
opCode++
}
commands = append(commands, byte(opCode))
for _, paramname := range val.parameterList {
if unit.Type == "delay" && paramname == "count" {
count := unit.Parameters["count"]*2 - 1
if unit.Parameters["notetracking"] == 1 {
count++
}
values = append(values, byte(count))
} else if pval, ok := unit.Parameters[paramname]; ok {
values = append(values, byte(pval))
} else {
return nil, fmt.Errorf("Unit parameter undefined: %v (at instrument %v, unit %v)", paramname, insid, unitid)
}
}
if unit.Type == "oscillator" {
flags := 0
switch unit.Parameters["type"] {
case go4k.Sine:
flags = 0x40
case go4k.Trisaw:
flags = 0x20
case go4k.Pulse:
flags = 0x10
case go4k.Gate:
flags = 0x04
case go4k.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" {
address := unit.Parameters["unit"]*16 + 24 + unit.Parameters["port"]
if unit.Parameters["voice"] > 0 {
address += 0x4000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, address is computed relative to synthworkspace
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8000
}
values = append(values, byte(address&255), byte(address>>8))
}
} else {
return nil, fmt.Errorf("Unknown unit type: %v (at instrument %v, unit %v)", unit.Type, insid, unitid)
}
}
commands = append(commands, byte(C.su_advance_id))
totalVoices += instr.NumVoices
for k := 0; k < instr.NumVoices-1; k++ {
polyphonyBitmask = (polyphonyBitmask << 1) + 1
}
polyphonyBitmask <<= 1
}
if totalVoices > 32 {
return nil, errors.New("Sointu does not support more than 32 concurrent voices")
}
if len(commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
return nil, errors.New("The patch would result in more than 2048 commands")
}
if len(values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return nil, errors.New("The patch would result in more than 16384 values")
}
for i := range commands {
s.Commands[i] = (C.uchar)(commands[i])
}
for i := range values {
s.Values[i] = (C.uchar)(values[i])
}
for i, deltime := range patch.DelayTimes {
s.DelayTimes[i] = (C.ushort)(deltime)
}
for i, samoff := range patch.SampleOffsets {
s.SampleOffsets[i].Start = (C.uint)(samoff.Start)
s.SampleOffsets[i].LoopStart = (C.ushort)(samoff.LoopStart)
s.SampleOffsets[i].LoopLength = (C.ushort)(samoff.LoopLength)
}
s.NumVoices = C.uint(totalVoices)
s.Polyphony = C.uint(polyphonyBitmask)
s.RandSeed = 1
return s, nil
}
// Trigger is part of C.Synths' implementation of go4k.Synth interface
func (s *C.Synth) Trigger(voice int, note byte) {
s.SynthWrk.Voices[voice] = C.Voice{}
s.SynthWrk.Voices[voice].Note = C.int(note)
}
// Release is part of C.Synths' implementation of go4k.Synth interface
func (s *C.Synth) Release(voice int) {
s.SynthWrk.Voices[voice].Release = 1
}
// 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

@ -12,25 +12,14 @@ import (
"github.com/vsariola/sointu/go4k/bridge"
)
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 TestBridge(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
@ -64,6 +53,7 @@ func TestBridge(t *testing.T) {
for i, v := range createdb {
if expectedb[i] != v {
t.Errorf("byte mismatch @ %v, got %v, expected %v", i, v, expectedb[i])
break
}
}
}
@ -72,10 +62,8 @@ func TestStackUnderflow(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"pop", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
@ -91,10 +79,8 @@ func TestStackBalancing(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"push", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "push", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
@ -110,27 +96,25 @@ func TestStackOverflow(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
go4k.Unit{"pop", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)
@ -146,12 +130,10 @@ func TestDivideByZero(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"loadval", map[string]int{"value": 128}},
go4k.Unit{"invgain", map[string]int{"invgain": 0}},
go4k.Unit{"pop", map[string]int{}},
}}},
SampleOffsets: []go4k.SampleOffset{},
DelayTimes: []int{}}
go4k.Unit{Type: "loadval", Parameters: map[string]int{"value": 128}},
go4k.Unit{Type: "invgain", Parameters: map[string]int{"invgain": 0}},
go4k.Unit{Type: "pop", Parameters: map[string]int{}},
}}}}
synth, err := bridge.Synth(patch)
if err != nil {
t.Fatalf("bridge compile error: %v", err)

View File

@ -1,6 +0,0 @@
// +build !windows
package bridge
func Init() {
}

View File

@ -1,12 +0,0 @@
// +build windows
package bridge
// #cgo CFLAGS: -I"${SRCDIR}/../../include/sointu"
// #cgo LDFLAGS: "${SRCDIR}/../../build/libsointu.a"
// #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
}

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
}

View File

@ -1,4 +1,4 @@
package go4k_test
package bridge_test
import (
"bytes"
@ -25,16 +25,14 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
func TestPlayer(t *testing.T) {
patch := go4k.Patch{
Instruments: []go4k.Instrument{go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{"mulp", map[string]int{"stereo": 0}},
go4k.Unit{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{"mulp", map[string]int{"stereo": 0}},
go4k.Unit{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
DelayTimes: []int{},
SampleOffsets: []go4k.SampleOffset{}}
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
go4k.Unit{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
go4k.Unit{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}}
patterns := [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}
tracks := []go4k.Track{go4k.Track{1, []byte{0}}}
song := go4k.Song{BPM: 100, Patterns: patterns, Tracks: tracks, Patch: patch, Output16Bit: false, Hold: 1}
@ -47,7 +45,7 @@ func TestPlayer(t *testing.T) {
t.Fatalf("Render failed: %v", err)
}
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", "test_oscillat_sine.raw"))
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "..", "tests", "expected_output", "test_oscillat_sine.raw"))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}

View File

@ -17,6 +17,7 @@ import (
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/audio/oto"
"github.com/vsariola/sointu/go4k/bridge"
"github.com/vsariola/sointu/go4k/compiler"
)
func main() {
@ -41,12 +42,21 @@ func main() {
flag.Usage()
os.Exit(0)
}
var comp *compiler.Compiler
if !*asmOut && !*jsonOut && !*rawOut && !*headerOut && !*play && !*yamlOut && *tmplDir == "" {
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
}
needsRendering := *play || *exactLength || *rawOut
if needsRendering {
bridge.Init()
needsCompile := *headerOut || *asmOut
if needsCompile {
var err error
comp, err = compiler.New()
if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
os.Exit(1)
}
comp.Amd64 = *targetArch == "amd64"
comp.OS = *targetOs
}
process := func(filename string) error {
output := func(extension string, contents []byte) error {
@ -87,11 +97,7 @@ func main() {
var song go4k.Song
if errJSON := json.Unmarshal(inputBytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(inputBytes, &song); errYaml != nil {
song2, errAsm := go4k.ParseAsm(string(inputBytes))
if errAsm != nil {
return fmt.Errorf("The song could not be parsed as .json (%v), .yml (%v) nor .asm (%v)", errJSON, errYaml, errAsm)
}
song = *song2
return fmt.Errorf("The song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
var buffer []float32
@ -121,23 +127,26 @@ func main() {
return fmt.Errorf("error updating the hold value of the song: %v", err)
}
}
if *headerOut {
var compiledPlayer map[string]string
if needsCompile {
maxSamples := 0 // 0 means it is calculated automatically
if *exactLength {
maxSamples = len(buffer) / 2
}
header := go4k.CHeader(&song, maxSamples)
if err := output(".h", []byte(header)); err != nil {
var err error
compiledPlayer, err = comp.Player(&song, maxSamples)
if err != nil {
return fmt.Errorf("compiling player failed: %v", err)
}
}
if *headerOut {
if err := output(".h", []byte(compiledPlayer["h"])); err != nil {
return fmt.Errorf("Error outputting header file: %v", err)
}
}
if *asmOut {
asmCode, err := go4k.Compile(&song, *targetArch, *targetOs)
if err != nil {
return fmt.Errorf("Could not format the song as asm file: %v", err)
}
if err := output(".asm", []byte(asmCode)); err != nil {
if err := output(".asm", []byte(compiledPlayer["asm"])); err != nil {
return fmt.Errorf("Error outputting asm file: %v", err)
}
}

81
go4k/compiler/compiler.go Normal file
View File

@ -0,0 +1,81 @@
package compiler
import (
"bytes"
"fmt"
"path"
"path/filepath"
"runtime"
"text/template"
"github.com/Masterminds/sprig"
"github.com/vsariola/sointu/go4k"
)
//go:generate go run generate.go
type Compiler struct {
Template *template.Template
Amd64 bool
OS string
DisableSections bool
}
// New returns a new compiler using the default .asm templates
func New() (*Compiler, error) {
_, myname, _, _ := runtime.Caller(0)
templateDir := filepath.Join(path.Dir(myname), "..", "..", "templates")
compiler, err := NewFromTemplates(templateDir)
return compiler, err
}
func NewFromTemplates(directory string) (*Compiler, error) {
globPtrn := filepath.Join(directory, "*.*")
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`, directory, err)
}
return &Compiler{Template: tmpl, Amd64: runtime.GOARCH == "amd64", OS: runtime.GOOS}, nil
}
func (com *Compiler) compile(templateName string, data interface{}) (string, error) {
result := bytes.NewBufferString("")
err := com.Template.ExecuteTemplate(result, templateName, data)
return result.String(), err
}
func (com *Compiler) Library() (map[string]string, error) {
features := AllFeatures{}
m := NewMacros(*com, features)
m.Library = true
asmCode, err := com.compile("library.asm", m)
if err != nil {
return nil, fmt.Errorf(`could not execute template "library.asm": %v`, err)
}
m = NewMacros(*com, features)
m.Library = true
header, err := com.compile("library.h", &m)
if err != nil {
return nil, fmt.Errorf(`could not execute template "library.h": %v`, err)
}
return map[string]string{"asm": asmCode, "h": header}, nil
}
func (com *Compiler) Player(song *go4k.Song, maxSamples int) (map[string]string, error) {
features := NecessaryFeaturesFor(song.Patch)
encodedPatch, err := Encode(&song.Patch, features)
if err != nil {
return nil, fmt.Errorf(`could not encode patch: %v`, err)
}
asmCode, err := com.compile("player.asm", NewPlayerMacros(*com, features, song, encodedPatch, maxSamples))
if err != nil {
return nil, fmt.Errorf(`could not execute template "player.asm": %v`, err)
}
header, err := com.compile("player.h", NewPlayerMacros(*com, features, song, encodedPatch, maxSamples))
if err != nil {
return nil, fmt.Errorf(`could not execute template "player.h": %v`, err)
}
return map[string]string{"asm": asmCode, "h": header}, nil
}

View File

@ -0,0 +1,144 @@
package compiler
import (
"errors"
"fmt"
"github.com/vsariola/sointu/go4k"
)
type EncodedPatch 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 *go4k.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
var c EncodedPatch
sampleOffsetMap := map[SampleOffset]int{}
for _, instr := range patch.Instruments {
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")
}
for _, unit := range instr.Units {
if unit.Type == "oscillator" && unit.Parameters["type"] == 4 {
s := SampleOffset{Start: uint32(unit.Parameters["start"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
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))
}
}
command, values, err := EncodeUnit(unit, featureSet)
if err != nil {
return nil, fmt.Errorf(`encoding unit failed: %v`, err)
}
c.Commands = append(c.Commands, command)
c.Values = append(c.Values, values...)
}
c.Commands = append(c.Commands, byte(0)) // advance
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 EncodeUnit(unit go4k.Unit, featureSet FeatureSet) (byte, []byte, error) {
opcode, ok := featureSet.Opcode(unit.Type)
if !ok {
return 0, nil, fmt.Errorf(`the targeted virtual machine is not configured to support unit type "%v"`, unit.Type)
}
var values []byte
for _, v := range go4k.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 go4k.Sine:
flags = 0x40
case go4k.Trisaw:
flags = 0x20
case go4k.Pulse:
flags = 0x10
case go4k.Gate:
flags = 0x04
case go4k.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" {
address := ((unit.Parameters["unit"] + 1) << 4) + unit.Parameters["port"] // each unit is 16 dwords, 8 workspace followed by 8 ports. +1 is for skipping the note/release/inputs
if unit.Parameters["voice"] > 0 {
address += 0x8000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, +16 is for skipping the out/aux ports
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8
}
values = append(values, byte(address&255), byte(address>>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))
}
return byte(opcode + unit.Parameters["stereo"]), values, nil
}

206
go4k/compiler/featureset.go Normal file
View File

@ -0,0 +1,206 @@
package compiler
import (
"sort"
"github.com/vsariola/sointu/go4k"
)
// 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
}
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) 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(go4k.UnitTypes))
allOpcodes = map[string]int{}
allTransformCounts = map[string]int{}
allInputs = map[paramKey]int{}
i := 0
for k, v := range go4k.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
polyphony bool
}
func NecessaryFeaturesFor(patch go4k.Patch) NecessaryFeatures {
features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
for instrNo, instrument := range patch.Instruments {
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" {
targetInstrument := instrNo
if unit.Parameters["voice"] > 0 {
v, err := patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
continue
}
targetInstrument = v
}
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(patch.Instruments[targetInstrument].Units) {
continue // send is modulating outside the range of the target instrument; probably a bug in patch, but at least it's not modulating the uniType we're after
}
targetUnit := patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]]
modulatedPortNo := 0
for _, v := range go4k.UnitTypes[targetUnit.Type] {
if v.CanModulate {
if modulatedPortNo == unit.Parameters["port"] {
features.supportsModulation[paramKey{targetUnit.Type, v.Name}] = true
}
modulatedPortNo++
}
}
}
}
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]
}

61
go4k/compiler/generate.go Normal file
View File

@ -0,0 +1,61 @@
// The following directive is necessary to make the package coherent:
// +build ignore
// This program generates the library headers and assembly files for the library
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"github.com/vsariola/sointu/go4k/compiler"
)
func main() {
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to Go architecture. Possible values: amd64, 386 (anything else is assumed 386)")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current Go OS. Possible values: windows, darwin, linux (anything else is assumed linux)")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() != 1 {
flag.Usage()
os.Exit(0)
}
comp, err := compiler.New()
if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
os.Exit(1)
}
comp.Amd64 = *targetArch == "amd64"
comp.OS = *targetOs
library, err := comp.Library()
if err != nil {
fmt.Fprintf(os.Stderr, `error compiling library: %v`, err)
os.Exit(1)
}
filenames := map[string]string{"h": "sointu.h", "asm": "sointu.asm"}
for t, contents := range library {
filename := filenames[t]
err := ioutil.WriteFile(filepath.Join(flag.Args()[0], filename), []byte(contents), os.ModePerm)
if err != nil {
fmt.Fprintf(os.Stderr, `could not write to file "%v": %v`, filename, err)
os.Exit(1)
}
}
os.Exit(0)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Sointu command line utility for generating the library .asm and .h files.\nUsage: %s [flags] outputDirectory\n", os.Args[0])
flag.PrintDefaults()
}

View File

@ -1,16 +1,11 @@
package go4k
package compiler
import (
"bytes"
"fmt"
"math"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/Masterminds/sprig"
"github.com/vsariola/sointu/go4k"
)
type OplistEntry struct {
@ -19,141 +14,56 @@ type OplistEntry struct {
}
type Macros struct {
Opcodes []OplistEntry
Polyphony bool
MultivoiceTracks bool
PolyphonyBitmask int
Stacklocs []string
Output16Bit bool
Clip bool
Amd64 bool
OS string
DisableSections 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
usesFloatConst map[float32]bool
usesIntConst map[int]bool
floatConsts []float32
intConsts []int
calls map[string]bool
stereo map[string]bool
mono map[string]bool
ops map[string]bool
stackframes map[string][]string
unitInputMap map[string](map[string]int)
Stacklocs []string
Output16Bit bool
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
usesFloatConst map[float32]bool
usesIntConst map[int]bool
floatConsts []float32
intConsts []int
calls map[string]bool
stackframes map[string][]string
FeatureSet
Compiler
}
type PlayerMacros struct {
Song *Song
VoiceTrackBitmask int
JumpTable []string
Code []byte
Values []byte
Macros
}
func NewPlayerMacros(song *Song, targetArch string, targetOS string) *PlayerMacros {
unitInputMap := map[string](map[string]int){}
for k, v := range UnitTypes {
inputMap := map[string]int{}
inputCount := 0
for _, t := range v {
if t.CanModulate {
inputMap[t.Name] = inputCount
inputCount++
}
}
unitInputMap[k] = inputMap
func NewMacros(c Compiler, f FeatureSet) *Macros {
return &Macros{
calls: map[string]bool{},
usesFloatConst: map[float32]bool{},
usesIntConst: map[int]bool{},
stackframes: map[string][]string{},
Sine: go4k.Sine,
Trisaw: go4k.Trisaw,
Pulse: go4k.Pulse,
Gate: go4k.Gate,
Sample: go4k.Sample,
Compiler: c,
FeatureSet: f,
}
jumpTable, code, values := song.Patch.Encode()
amd64 := targetArch == "amd64"
p := &PlayerMacros{
Song: song,
JumpTable: jumpTable,
Code: code,
Values: values,
Macros: Macros{
mono: map[string]bool{},
stereo: map[string]bool{},
calls: map[string]bool{},
ops: map[string]bool{},
usesFloatConst: map[float32]bool{},
usesIntConst: map[int]bool{},
stackframes: map[string][]string{},
unitInputMap: unitInputMap,
Amd64: amd64,
OS: targetOS,
Sine: Sine,
Trisaw: Trisaw,
Pulse: Pulse,
Gate: Gate,
Sample: Sample,
}}
for _, track := range song.Tracks {
if track.NumVoices > 1 {
p.MultivoiceTracks = true
}
}
trackVoiceNumber := 0
for _, t := range song.Tracks {
for b := 0; b < t.NumVoices-1; b++ {
p.VoiceTrackBitmask += 1 << trackVoiceNumber
trackVoiceNumber++
}
trackVoiceNumber++ // set all bits except last one
}
totalVoices := 0
for _, instr := range song.Patch.Instruments {
if instr.NumVoices > 1 {
p.Polyphony = true
}
for _, unit := range instr.Units {
if !p.ops[unit.Type] {
p.ops[unit.Type] = true
numParams := 0
for _, v := range UnitTypes[unit.Type] {
if v.CanSet && v.CanModulate {
numParams++
}
}
p.Opcodes = append(p.Opcodes, OplistEntry{
Type: unit.Type,
NumParams: numParams,
})
}
if unit.Parameters["stereo"] == 1 {
p.stereo[unit.Type] = true
} else {
p.mono[unit.Type] = true
}
}
totalVoices += instr.NumVoices
for k := 0; k < instr.NumVoices-1; k++ {
p.PolyphonyBitmask = (p.PolyphonyBitmask << 1) + 1
}
p.PolyphonyBitmask <<= 1
}
p.Output16Bit = song.Output16Bit
return p
}
func (p *Macros) Opcode(t string) bool {
return p.ops[t]
func (p *Macros) HasOp(instruction string) bool {
_, ok := p.Opcode(instruction)
return ok
}
func (p *Macros) Stereo(t string) bool {
return p.stereo[t]
func (p *Macros) Stereo(unitType string) bool {
return p.SupportsParamValue(unitType, "stereo", 1)
}
func (p *Macros) Mono(t string) bool {
return p.mono[t]
func (p *Macros) Mono(unitType string) bool {
return p.SupportsParamValue(unitType, "stereo", 0)
}
func (p *Macros) StereoAndMono(t string) bool {
return p.stereo[t] && p.mono[t]
func (p *Macros) StereoAndMono(unitType string) bool {
return p.Stereo(unitType) && p.Mono(unitType)
}
// Macros and functions to accumulate constants automagically
@ -431,6 +341,22 @@ func (p *Macros) Pop(register string) string {
return fmt.Sprintf("pop %v ; %v = %v, Stack: %v ", register, register, last, p.FmtStack())
}
func (p *Macros) 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 *Macros) 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 *Macros) Stack(name string) (string, error) {
for i, k := range p.Stacklocs {
if k == name {
@ -463,7 +389,11 @@ func (p *Macros) FmtStack() string {
func (p *Macros) ExportFunc(name string, params ...string) string {
if !p.Amd64 {
p.Stacklocs = append(params, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
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)
}
@ -474,54 +404,16 @@ func (p *Macros) ExportFunc(name string, params ...string) string {
return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name)
}
func (p *Macros) Count(count int) []int {
s := make([]int, count)
for i := range s {
s[i] = i
}
return s
}
func (p *Macros) Sub(a int, b int) int {
return a - b
}
func (p *Macros) Input(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown input "%v" for unit "%v"`, port, unit)
}
i := p.InputNumber(unit, port)
if i != 0 {
return fmt.Sprintf("%v + %v", p.INP(), i*4), nil
}
return p.INP(), nil
}
func (p *Macros) InputNumber(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find InputNumber for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find InputNumber for unknown input "%v" for unit "%v"`, port, unit)
}
return fmt.Sprintf("%v", i), nil
}
func (p *Macros) Modulation(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown input "%v" for unit "%v"`, port, unit)
}
i := p.InputNumber(unit, port)
return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil
}
@ -549,6 +441,32 @@ func (p *Macros) Use(value string, regs ...string) (string, error) {
return value, nil
}
type PlayerMacros struct {
Song *go4k.Song
VoiceTrackBitmask int
MaxSamples int
Macros
EncodedPatch
}
func NewPlayerMacros(c Compiler, f FeatureSet, s *go4k.Song, e *EncodedPatch, maxSamples int) *PlayerMacros {
if maxSamples == 0 {
maxSamples = s.SamplesPerRow() * s.TotalRows()
}
macros := *NewMacros(c, f)
macros.Output16Bit = s.Output16Bit // TODO: should we actually store output16bit in Songs or not?
p := PlayerMacros{Song: s, Macros: macros, MaxSamples: maxSamples, EncodedPatch: *e}
trackVoiceNumber := 0
for _, t := range s.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 *PlayerMacros) NumDelayLines() string {
total := 0
for _, instr := range p.Song.Patch.Instruments {
@ -560,68 +478,3 @@ func (p *PlayerMacros) NumDelayLines() string {
}
return fmt.Sprintf("%v", total)
}
func (p *PlayerMacros) UsesDelayModulation() (bool, error) {
for i, instrument := range p.Song.Patch.Instruments {
for j, unit := range instrument.Units {
if unit.Type == "send" {
targetInstrument := i
if unit.Parameters["voice"] > 0 {
v, err := p.Song.Patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
return false, fmt.Errorf("INSTRUMENT #%v / SEND #%v targets voice %v, which does not exist", i, j, unit.Parameters["voice"])
}
targetInstrument = v
}
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(p.Song.Patch.Instruments[targetInstrument].Units) {
return false, fmt.Errorf("INSTRUMENT #%v / SEND #%v target unit %v out of range", i, j, unit.Parameters["unit"])
}
if p.Song.Patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]].Type == "delay" && unit.Parameters["port"] == 4 {
return true, nil
}
}
}
}
return false, nil
}
func (p *PlayerMacros) HasParamValue(unitType string, paramName string, value int) bool {
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == unitType {
if unit.Parameters[paramName] == value {
return true
}
}
}
}
return false
}
func (p *PlayerMacros) HasParamValueOtherThan(unitType string, paramName string, value int) bool {
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == unitType {
if unit.Parameters[paramName] != value {
return true
}
}
}
}
return false
}
func Compile(song *Song, targetArch string, targetOs string) (string, error) {
_, myname, _, _ := runtime.Caller(0)
templateDir := filepath.Join(path.Dir(myname), "..", "templates", "*.asm")
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(templateDir)
if err != nil {
return "", fmt.Errorf(`could not create template based on dir "%v": %v`, templateDir, err)
}
b := bytes.NewBufferString("")
err = tmpl.ExecuteTemplate(b, "player.asm", NewPlayerMacros(song, targetArch, targetOs))
if err != nil {
return "", fmt.Errorf(`could not execute template "player.asm": %v`, err)
}
return b.String(), nil
}

View File

@ -2,6 +2,7 @@ package go4k
import (
"errors"
"fmt"
"math"
)
@ -9,6 +10,7 @@ import (
type Unit struct {
Type string
Parameters map[string]int `yaml:",flow"`
VarArgs []int
}
const (
@ -25,17 +27,9 @@ type Instrument struct {
Units []Unit
}
type SampleOffset struct {
Start int
LoopStart int
LoopLength int
}
// Patch is simply a list of instruments used in a song
type Patch struct {
Instruments []Instrument
DelayTimes []int `yaml:",flow"`
SampleOffsets []SampleOffset
Instruments []Instrument
}
func (p Patch) TotalVoices() int {
@ -73,30 +67,13 @@ type Synth interface {
func Render(synth Synth, buffer []float32) error {
s, _, err := synth.Render(buffer, math.MaxInt32)
if err != nil {
return fmt.Errorf("go4k.Render failed: %v", err)
}
if s != len(buffer)/2 {
return errors.New("synth.Render should have filled the whole buffer")
return errors.New("in go4k.Render, synth.Render should have filled the whole buffer but did not")
}
return err
}
func (p *Patch) Encode() ([]string, []byte, []byte) {
var code []byte
var values []byte
var jumpTable []string
assignedIds := map[string]byte{}
for _, instr := range p.Instruments {
for _, unit := range instr.Units {
if _, ok := assignedIds[unit.Type]; !ok {
jumpTable = append(jumpTable, unit.Type)
assignedIds[unit.Type] = byte(len(jumpTable) * 2)
}
stereo, unitValues := Encode(unit)
code = append(code, stereo+assignedIds[unit.Type])
values = append(values, unitValues...)
}
code = append(code, 0)
}
return jumpTable, code, values
return nil
}
// UnitParameter documents one parameter that an unit takes
@ -108,70 +85,6 @@ type UnitParameter struct {
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
}
func Encode(unit Unit) (byte, []byte) {
var values []byte
for _, v := range UnitTypes[unit.Type] {
if v.CanSet && v.CanModulate {
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 Sine:
flags = 0x40
case Trisaw:
flags = 0x20
case Pulse:
flags = 0x10
case Gate:
flags = 0x04
case 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" {
address := ((unit.Parameters["unit"] + 1) << 4) + unit.Parameters["port"] // each unit is 16 dwords, 8 workspace followed by 8 ports. +1 is for skipping the note/release/inputs
if unit.Parameters["voice"] > 0 {
address += 0x8000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, +16 is for skipping the out/aux ports
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8
}
values = append(values, byte(address&255), byte(address>>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))
}
return byte(unit.Parameters["stereo"]), values
}
// UnitTypes documents all the available unit types and if they support stereo variant
// and what parameters they take.
var UnitTypes = map[string]([]UnitParameter){
@ -218,8 +131,6 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "delay", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
{Name: "count", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"compressor": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
@ -268,7 +179,10 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false}},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
{Name: "samplestart", MinValue: 0, MaxValue: 3440659, CanSet: true, CanModulate: false},
{Name: "loopstart", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false},
{Name: "looplength", MinValue: 0, MaxValue: 65535, CanSet: true, CanModulate: false}},
"loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
@ -280,3 +194,133 @@ var UnitTypes = map[string]([]UnitParameter){
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
}
type Song struct {
BPM int
Output16Bit bool
Hold byte
Patterns [][]byte `yaml:",flow"`
Tracks []Track
Patch Patch
}
func (s *Song) PatternRows() int {
return len(s.Patterns[0])
}
func (s *Song) SequenceLength() int {
return len(s.Tracks[0].Sequence)
}
func (s *Song) TotalRows() int {
return s.PatternRows() * s.SequenceLength()
}
func (s *Song) SamplesPerRow() int {
return 44100 * 60 / (s.BPM * 4)
}
func (s *Song) FirstTrackVoice(track int) int {
ret := 0
for _, t := range s.Tracks[:track] {
ret += t.NumVoices
}
return ret
}
// TBD: Where shall we put methods that work on pure domain types and have no dependencies
// e.g. Validate here
func (s *Song) Validate() error {
if s.BPM < 1 {
return errors.New("BPM should be > 0")
}
for i := range s.Patterns[:len(s.Patterns)-1] {
if len(s.Patterns[i]) != len(s.Patterns[i+1]) {
return errors.New("Every pattern should have the same length")
}
}
for i := range s.Tracks[:len(s.Tracks)-1] {
if len(s.Tracks[i].Sequence) != len(s.Tracks[i+1].Sequence) {
return errors.New("Every track should have the same sequence length")
}
}
totalTrackVoices := 0
for _, track := range s.Tracks {
totalTrackVoices += track.NumVoices
for _, p := range track.Sequence {
if p < 0 || int(p) >= len(s.Patterns) {
return errors.New("Tracks use a non-existing pattern")
}
}
}
if totalTrackVoices > s.Patch.TotalVoices() {
return errors.New("Tracks use too many voices")
}
return nil
}
func Play(synth Synth, song Song) ([]float32, error) {
err := song.Validate()
if err != nil {
return nil, err
}
curVoices := make([]int, len(song.Tracks))
for i := range curVoices {
curVoices[i] = song.FirstTrackVoice(i)
}
initialCapacity := song.TotalRows() * song.SamplesPerRow() * 2
buffer := make([]float32, 0, initialCapacity)
rowbuffer := make([]float32, song.SamplesPerRow()*2)
for row := 0; row < song.TotalRows(); row++ {
patternRow := row % song.PatternRows()
pattern := row / song.PatternRows()
for t := range song.Tracks {
patternIndex := song.Tracks[t].Sequence[pattern]
note := song.Patterns[patternIndex][patternRow]
if note > 0 && note <= song.Hold { // anything but hold causes an action.
continue
}
synth.Release(curVoices[t])
if note > song.Hold {
curVoices[t]++
first := song.FirstTrackVoice(t)
if curVoices[t] >= first+song.Tracks[t].NumVoices {
curVoices[t] = first
}
synth.Trigger(curVoices[t], note)
}
}
tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, _ := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...)
if tries > 100 {
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
}
}
}
return buffer, nil
}
func (s *Song) UpdateHold(newHold byte) error {
if newHold == 0 {
return errors.New("hold value cannot be 0, 0 is reserved for release")
}
for _, pat := range s.Patterns {
for _, v := range pat {
if v > s.Hold && v <= newHold {
return errors.New("song uses note values greater or equal to the new hold value")
}
}
}
for _, pat := range s.Patterns {
for i, v := range pat {
if v > 0 && v <= s.Hold {
pat[i] = newHold
}
}
}
s.Hold = newHold
return nil
}

View File

@ -1,136 +0,0 @@
package go4k
import (
"errors"
"fmt"
)
type Song struct {
BPM int
Output16Bit bool
Hold byte
Patterns [][]byte `yaml:",flow"`
Tracks []Track
Patch Patch
}
func (s *Song) PatternRows() int {
return len(s.Patterns[0])
}
func (s *Song) SequenceLength() int {
return len(s.Tracks[0].Sequence)
}
func (s *Song) TotalRows() int {
return s.PatternRows() * s.SequenceLength()
}
func (s *Song) SamplesPerRow() int {
return 44100 * 60 / (s.BPM * 4)
}
func (s *Song) FirstTrackVoice(track int) int {
ret := 0
for _, t := range s.Tracks[:track] {
ret += t.NumVoices
}
return ret
}
// TBD: Where shall we put methods that work on pure domain types and have no dependencies
// e.g. Validate here
func (s *Song) Validate() error {
if s.BPM < 1 {
return errors.New("BPM should be > 0")
}
for i := range s.Patterns[:len(s.Patterns)-1] {
if len(s.Patterns[i]) != len(s.Patterns[i+1]) {
return errors.New("Every pattern should have the same length")
}
}
for i := range s.Tracks[:len(s.Tracks)-1] {
if len(s.Tracks[i].Sequence) != len(s.Tracks[i+1].Sequence) {
return errors.New("Every track should have the same sequence length")
}
}
totalTrackVoices := 0
for _, track := range s.Tracks {
totalTrackVoices += track.NumVoices
for _, p := range track.Sequence {
if p < 0 || int(p) >= len(s.Patterns) {
return errors.New("Tracks use a non-existing pattern")
}
}
}
if totalTrackVoices > s.Patch.TotalVoices() {
return errors.New("Tracks use too many voices")
}
return nil
}
func Play(synth Synth, song Song) ([]float32, error) {
err := song.Validate()
if err != nil {
return nil, err
}
curVoices := make([]int, len(song.Tracks))
for i := range curVoices {
curVoices[i] = song.FirstTrackVoice(i)
}
initialCapacity := song.TotalRows() * song.SamplesPerRow() * 2
buffer := make([]float32, 0, initialCapacity)
rowbuffer := make([]float32, song.SamplesPerRow()*2)
for row := 0; row < song.TotalRows(); row++ {
patternRow := row % song.PatternRows()
pattern := row / song.PatternRows()
for t := range song.Tracks {
patternIndex := song.Tracks[t].Sequence[pattern]
note := song.Patterns[patternIndex][patternRow]
if note > 0 && note <= song.Hold { // anything but hold causes an action.
continue
}
synth.Release(curVoices[t])
if note > song.Hold {
curVoices[t]++
first := song.FirstTrackVoice(t)
if curVoices[t] >= first+song.Tracks[t].NumVoices {
curVoices[t] = first
}
synth.Trigger(curVoices[t], note)
}
}
tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, _ := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime)
rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...)
if tries > 100 {
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
}
}
}
return buffer, nil
}
func (s *Song) UpdateHold(newHold byte) error {
if newHold == 0 {
return errors.New("hold value cannot be 0, 0 is reserved for release")
}
for _, pat := range s.Patterns {
for _, v := range pat {
if v > s.Hold && v <= newHold {
return errors.New("song uses note values greater or equal to the new hold value")
}
}
}
for _, pat := range s.Patterns {
for i, v := range pat {
if v > 0 && v <= s.Hold {
pat[i] = newHold
}
}
}
s.Hold = newHold
return nil
}

View File

@ -1,54 +0,0 @@
package go4k_test
import (
"encoding/json"
"reflect"
"testing"
"github.com/vsariola/sointu/go4k"
)
const expectedMarshaled = `{"BPM":100,"Output16Bit":false,"Hold":1,"Patterns":["QABEACAAAABLAE4AAAAAAA=="],"Tracks":[{"NumVoices":1,"Sequence":"AA=="}],"Patch":{"Instruments":[{"NumVoices":1,"Units":[{"Type":"envelope","Parameters":{"attack":32,"decay":32,"gain":128,"release":64,"stereo":0,"sustain":64}},{"Type":"oscillator","Parameters":{"color":96,"detune":64,"flags":64,"gain":128,"phase":0,"shape":64,"stereo":0,"transpose":64}},{"Type":"mulp","Parameters":{"stereo":0}},{"Type":"envelope","Parameters":{"attack":32,"decay":32,"gain":128,"release":64,"stereo":0,"sustain":64}},{"Type":"oscillator","Parameters":{"color":64,"detune":64,"flags":64,"gain":128,"phase":64,"shape":96,"stereo":0,"transpose":72}},{"Type":"mulp","Parameters":{"stereo":0}},{"Type":"out","Parameters":{"gain":128,"stereo":1}}]}],"DelayTimes":[],"SampleOffsets":[]}}`
var testSong = go4k.Song{
BPM: 100,
Patterns: [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}},
Tracks: []go4k.Track{
{NumVoices: 1, Sequence: []byte{0}},
},
Patch: go4k.Patch{
Instruments: []go4k.Instrument{{NumVoices: 1, Units: []go4k.Unit{
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "flags": 0x40}},
{"mulp", map[string]int{"stereo": 0}},
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "flags": 0x40}},
{"mulp", map[string]int{"stereo": 0}},
{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
DelayTimes: []int{},
SampleOffsets: []go4k.SampleOffset{},
},
Hold: 1,
}
func TestSongMarshalJSON(t *testing.T) {
songbytes, err := json.Marshal(testSong)
if err != nil {
t.Fatalf("cannot marshal song: %v", err)
}
if string(songbytes) != expectedMarshaled {
t.Fatalf("expectedMarshaled song to unexpected result, got %v, expected %v", string(songbytes), expectedMarshaled)
}
}
func TestSongUnmarshalJSON(t *testing.T) {
var song go4k.Song
err := json.Unmarshal([]byte(expectedMarshaled), &song)
if err != nil {
t.Fatalf("cannot unmarshal song: %v", err)
}
if !reflect.DeepEqual(song, testSong) {
t.Fatalf("unmarshaled song to unexpected result, got %#v, expected %#v", song, testSong)
}
}

View File

@ -14,15 +14,12 @@ var defaultSong = go4k.Song{
},
Patch: go4k.Patch{
Instruments: []go4k.Instrument{{NumVoices: 2, Units: []go4k.Unit{
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine}},
{"mulp", map[string]int{"stereo": 0}},
{"envelope", map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine}},
{"mulp", map[string]int{"stereo": 0}},
{"out", map[string]int{"stereo": 1, "gain": 128}},
}}},
DelayTimes: []int{},
SampleOffsets: []go4k.SampleOffset{},
},
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine}},
{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
{Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine}},
{Type: "mulp", Parameters: map[string]int{"stereo": 0}},
{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
}}}},
}