mirror of
https://github.com/vsariola/sointu.git
synced 2025-11-29 05:53:28 -05:00
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:
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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, ", ")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package bridge
|
||||
|
||||
func Init() {
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
8
go4k/bridge/init_windows.go
Normal file
8
go4k/bridge/init_windows.go
Normal file
@ -0,0 +1,8 @@
|
||||
package bridge
|
||||
|
||||
// #include "sointu.h"
|
||||
import "C"
|
||||
|
||||
func init() {
|
||||
C.su_load_gmdls() // GM.DLS is an windows specific sound bank so samples work currently only on windows
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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
81
go4k/compiler/compiler.go
Normal 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
|
||||
}
|
||||
144
go4k/compiler/encoded_patch.go
Normal file
144
go4k/compiler/encoded_patch.go
Normal 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
206
go4k/compiler/featureset.go
Normal 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
61
go4k/compiler/generate.go
Normal 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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
240
go4k/go4k.go
240
go4k/go4k.go
@ -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
|
||||
}
|
||||
|
||||
136
go4k/song.go
136
go4k/song.go
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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}},
|
||||
}}}},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user