Implement .asm parsing in go4k, and run succesfully almost all tests/ through the bridge.

Delays and samples are not implemented yet and thus the tests are skipped, as these require parsing the delay and sample tables also. Various macronames were changed to be more sensible and consistent i.e. ATTAC was changed to ATTACK. GatesLow and GatesHigh was removed for the time being and the tracker will just have to know they are the SHAPE and COLOR parameters. SU_SPEED was changed to take a parameter so the parser picks it up.
This commit is contained in:
Veikko Sariola
2020-11-07 19:50:44 +02:00
parent f584138572
commit 9209aa1cba
66 changed files with 569 additions and 129 deletions

226
go4k/asmformat.go Normal file
View File

@ -0,0 +1,226 @@
package go4k
import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"strconv"
"strings"
)
func ParseAsm(reader io.Reader) (*Song, error) {
var bpm int
scanner := bufio.NewScanner(reader)
patterns := make([][]byte, 0)
tracks := make([]Track, 0)
var patch Patch
var instr Instrument
paramReg, err := regexp.Compile(`([a-zA-Z]\w*)\s*\(\s*([0-9]+)\s*\)`) // matches FOO(42), groups "FOO" and "42"
if err != nil {
return nil, err
}
parseParams := func(s string) (map[string]int, error) {
matches := paramReg.FindAllStringSubmatch(s, 256)
ret := map[string]int{}
for _, match := range matches {
val, err := strconv.Atoi(match[2])
if err != nil {
return nil, fmt.Errorf("Error converting %v to integer, which is unexpected as regexp matches only numbers", match[2])
}
ret[strings.ToLower(match[1])] = val
}
return ret, nil
}
flagsReg, err := regexp.Compile(`FLAGS\s*\(\s*(\s*\w+(?:\s*\+\s*\w+)*)\s*\)`) // matches FLAGS ( FOO + FAA), groups "FOO + FAA"
if err != nil {
return nil, err
}
flagNameReg, err := regexp.Compile(`\w+`) // matches any alphanumeric word
if err != nil {
return nil, err
}
parseFlags := func(s string) map[string]bool {
match := flagsReg.FindStringSubmatch(s)
if match == nil {
return nil
}
ret := map[string]bool{}
flagmatches := flagNameReg.FindAllString(match[1], 256)
for _, f := range flagmatches {
if f != "NONE" {
ret[f] = true
}
}
return ret
}
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
}
numberReg, err := regexp.Compile(`-?[0-9]+|HLD`) // finds integer numbers, possibly with a sign in front. HLD is the magic value used by sointu, will be interpreted as 1
if err != nil {
return nil, err
}
parseNumbers := func(s string) ([]int, error) {
matches := numberReg.FindAllString(s, 256)
ret := []int{}
for _, str := range matches {
var i int
var err error
if str == "HLD" {
i = 1
} else {
i, err = strconv.Atoi(str)
if err != nil {
return nil, err
}
}
ret = append(ret, i)
}
return ret, nil
}
toBytes := func(ints []int) []byte {
ret := []byte{}
for _, v := range ints {
ret = append(ret, byte(v))
}
return ret
}
unitNameMap := map[string]string{
"SU_ADD": "add",
"SU_ADDP": "addp",
"SU_POP": "pop",
"SU_LOADNOTE": "loadnote",
"SU_MUL": "mul",
"SU_MULP": "mulp",
"SU_PUSH": "push",
"SU_XCH": "xch",
"SU_DISTORT": "distort",
"SU_HOLD": "hold",
"SU_CRUSH": "crush",
"SU_GAIN": "gain",
"SU_INVGAIN": "invgain",
"SU_FILTER": "filter",
"SU_CLIP": "clip",
"SU_PAN": "pan",
"SU_DELAY": "delay",
"SU_COMPRES": "compressor",
"SU_SPEED": "speed",
"SU_OUT": "out",
"SU_OUTAUX": "outaux",
"SU_AUX": "aux",
"SU_SEND": "send",
"SU_ENVELOPE": "envelope",
"SU_NOISE": "noise",
"SU_OSCILLAT": "oscillator",
"SU_LOADVAL": "loadval",
"SU_RECEIVE": "receive",
"SU_IN": "in",
}
for scanner.Scan() {
line := scanner.Text()
macroMatch := wordReg.FindStringSubmatch(line)
if macroMatch != nil {
word, rest := macroMatch[1], macroMatch[2]
switch word {
case "define":
defineMatch := wordReg.FindStringSubmatch(rest)
if defineMatch != nil {
defineName, defineRest := defineMatch[1], defineMatch[2]
if defineName == "BPM" {
ints, err := parseNumbers(defineRest)
if err != nil {
return nil, err
}
bpm = ints[0]
}
}
case "PATTERN":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
patterns = append(patterns, toBytes(ints))
case "TRACK":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
track := Track{ints[0], toBytes(ints[1:])}
tracks = append(tracks, track)
case "BEGIN_INSTRUMENT":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
instr = Instrument{NumVoices: ints[0], Units: []Unit{}}
case "END_INSTRUMENT":
patch = append(patch, instr)
}
if unittype, ok := unitNameMap[word]; ok {
instrMatch := wordReg.FindStringSubmatch(rest)
if instrMatch != nil {
stereoMono, instrRest := instrMatch[1], instrMatch[2]
stereo := stereoMono == "STEREO"
parameters, err := parseParams(instrRest)
if err != nil {
return nil, fmt.Errorf("Error parsing parameters: %v", err)
}
flags := parseFlags(instrRest)
if unittype == "oscillator" {
if flags["SINE"] {
parameters["type"] = Sine
} else if flags["TRISAW"] {
parameters["type"] = Trisaw
} else if flags["PULSE"] {
parameters["type"] = Pulse
} else if flags["GATE"] {
parameters["type"] = Gate
} else if flags["SAMPLE"] {
parameters["type"] = Sample
} else {
return nil, errors.New("Invalid oscillator type")
}
if flags["UNISON4"] {
parameters["unison"] = 4
} else if flags["UNISON3"] {
parameters["unison"] = 3
} else if flags["UNISON2"] {
parameters["unison"] = 2
} else {
parameters["unison"] = 1
}
if flags["LFO"] {
parameters["lfo"] = 1
} else {
parameters["lfo"] = 0
}
} else if unittype == "filter" {
for _, flag := range []string{"LOWPASS", "BANDPASS", "HIGHPASS", "NEGBANDPASS", "NEGHIGHPASS"} {
if flags[flag] {
parameters[strings.ToLower(flag)] = 1
} else {
parameters[strings.ToLower(flag)] = 0
}
}
} else if unittype == "send" {
if _, ok := parameters["voice"]; !ok {
parameters["voice"] = -1
}
if flags["SEND_POP"] {
parameters["pop"] = 1
} else {
parameters["pop"] = 0
}
}
unit := Unit{Type: unittype, Stereo: stereo, Parameters: parameters}
instr.Units = append(instr.Units, unit)
}
}
}
}
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch, SongLength: -1}
return &s, nil
}

95
go4k/asmformat_test.go Normal file
View File

@ -0,0 +1,95 @@
package go4k_test
import (
"bytes"
"encoding/binary"
"io/ioutil"
"log"
"math"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/vsariola/sointu/go4k"
"github.com/vsariola/sointu/go4k/bridge"
)
func TestAllAsmFiles(t *testing.T) {
_, 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 strings.Contains(testname, "delay") || strings.Contains(testname, "sample") {
return // delays and samples are not implemented yet in the bridge, so skip them for now
}
file, err := os.Open(filename)
if err != nil {
t.Fatalf("cannot read the .asm file: %v", filename)
}
song, err := go4k.ParseAsm(file)
if err != nil {
t.Fatalf("could not parse the .asm 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)
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)
}
}
compareToRaw(t, buffer, testname+".raw")
})
}
}
func compareToRaw(t *testing.T, buffer []float32, rawname string) {
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
expected := make([]float32, len(expectedb)/4)
buf := bytes.NewReader(expectedb)
err = binary.Read(buf, binary.LittleEndian, &expected)
if err != nil {
t.Fatalf("error converting expected buffer: %v", err)
}
if len(expected) != len(buffer) {
t.Fatalf("buffer length mismatch, got %v, expected %v", len(buffer), len(expected))
}
for i, v := range expected {
if math.IsNaN(float64(buffer[i])) || math.Abs(float64(v-buffer[i])) > 1e-6 {
t.Fatalf("error bigger than 1e-6 detected, at sample position %v", i)
}
}
}

View File

@ -26,24 +26,24 @@ var opcodeTable = map[string]opTableEntry{
"mulp": opTableEntry{C.su_mulp_id, []string{}},
"push": opTableEntry{C.su_push_id, []string{}},
"xch": opTableEntry{C.su_xch_id, []string{}},
"distortion": opTableEntry{C.su_distort_id, []string{"drive"}},
"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", "flags"}},
"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", "depth", "damp", "delay", "count"}},
"delay": opTableEntry{C.su_delay_id, []string{"pregain", "dry", "feedback", "damp", "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", "port"}},
"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", "flags"}},
"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"}},
@ -93,18 +93,71 @@ func Synth(patch go4k.Patch) (*C.Synth, error) {
}
for unitid, unit := range instr.Units {
if val, ok := opcodeTable[unit.Type]; ok {
opCode := val.opcode
if unit.Stereo {
commands = append(commands, byte(val.opcode+1))
} else {
commands = append(commands, byte(val.opcode))
opCode++
}
commands = append(commands, byte(opCode))
for _, paramname := range val.parameterList {
if pval, ok := unit.Parameters[paramname]; ok {
if unit.Type == "delay" && paramname == "count" {
pval = pval*2 - 1
if val, ok := unit.Parameters["notetracking"]; ok && val == 1 {
pval++
}
}
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"] - 1
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"] != -1 {
address += 0x4000 + 16 + unit.Parameters["voice"]*1024 // global send, address is computed relative to synthworkspace
}
if unit.Parameters["pop"] == 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)
}

View File

@ -12,6 +12,14 @@ type Unit struct {
Parameters map[string]int
}
const (
Sine = iota
Trisaw = iota
Pulse = iota
Gate = iota
Sample = iota
)
// Instrument includes a list of units consisting of the instrument, and the number of polyphonic voices for this instrument
type Instrument struct {
NumVoices int

View File

@ -25,10 +25,10 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
func TestPlayer(t *testing.T) {
patch := go4k.Patch{go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "flags": 0x40}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 1}},
go4k.Unit{"mulp", false, map[string]int{}},
go4k.Unit{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "flags": 0x40}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 1}},
go4k.Unit{"mulp", false, map[string]int{}},
go4k.Unit{"out", true, map[string]int{"gain": 128}},
}}}