sointu/go4k/asmformat.go
Veikko Sariola 95c8c9c2b7 refactor(go4k): Remove all special treatment from samples and map Song 1-1 to what's in the .asm file.
Whoever uses it, probably wants their own Patch format, as now it is pretty cumbersome to work with sampleoffsets and delays, as the user needs to construct the delaytimes tables and sampleoffset tables.
2020-11-20 22:21:21 +02:00

372 lines
11 KiB
Go

package go4k
import (
"bufio"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
func DeserializeAsm(asmcode string) (*Song, error) {
var bpm int
scanner := bufio.NewScanner(strings.NewReader(asmcode))
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
}
typeReg, err := regexp.Compile(`TYPE\s*\(\s*(SINE|TRISAW|PULSE|GATE|SAMPLE)\s*\)`) // matches TYPE(TRISAW), groups "TRISAW"
if err != nil {
return nil, err
}
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
}
inInstrument := false
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{}}
inInstrument = true
case "END_INSTRUMENT":
patch.Instruments = append(patch.Instruments, instr)
inInstrument = false
case "DELTIME":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
for _, v := range ints {
patch.DelayTimes = append(patch.DelayTimes, v)
}
case "SAMPLE_OFFSET":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
patch.SampleOffsets = append(patch.SampleOffsets, SampleOffset{
Start: ints[0],
LoopStart: ints[1],
LoopLength: ints[2]})
}
if inInstrument && strings.HasPrefix(word, "SU_") {
unittype := strings.ToLower(word[3:])
parameters, err := parseParams(rest)
if err != nil {
return nil, fmt.Errorf("Error parsing parameters: %v", err)
}
if unittype == "oscillator" {
match := typeReg.FindStringSubmatch(rest)
if match == nil {
return nil, errors.New("Oscillator should define a type")
}
switch match[1] {
case "SINE":
parameters["type"] = Sine
case "TRISAW":
parameters["type"] = Trisaw
case "PULSE":
parameters["type"] = Pulse
case "GATE":
parameters["type"] = Gate
case "SAMPLE":
parameters["type"] = Sample
}
}
unit := Unit{Type: unittype, Parameters: parameters}
instr.Units = append(instr.Units, unit)
}
}
}
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch, SongLength: -1}
return &s, nil
}
func SerializeAsm(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--
}
// The actual printing starts here
println("%%define BPM %d", song.BPM)
// 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 := false
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 = true
}
}
}
}
if delaymod {
println("%%define INCLUDE_DELAY_MODULATION")
}
println("")
println("%%include \"sointu/header.inc\"\n")
var patternTable [][]string
for _, pattern := range song.Patterns {
row := []string{"PATTERN"}
for _, v := range pattern {
if v == 1 {
row = append(row, "HLD")
} else {
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 unit.Type == "oscillator" && parname == "type" {
switch unit.Parameters["type"] {
case Sine:
row = append(row, "TYPE(SINE)")
case Trisaw:
row = append(row, "TYPE(TRISAW)")
case Pulse:
row = append(row, "TYPE(PULSE)")
case Gate:
row = append(row, "TYPE(GATE)")
case Sample:
row = append(row, "TYPE(SAMPLE)")
}
} else 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("%%include \"sointu/footer.inc\"")
ret := b.String()
return ret, nil
}