feat(asm&go4k): Preprocess asm code using go text/template

The preprocessing is done sointu-cli and (almost) nothing is done by the NASM preprocessor anymore (some .strucs are still there.
Now, sointu-cli loads the .yml song, defines bunch of macros (go functions / variables) and passes the struct to text/template parses.
This a lot more powerful way to generate .asm code than trying to fight with the nasm preprocessor.

At the moment, tests pass but the repository is a bit of monster, as the library is still compiled using the old approach. Go should
generate the library also from the templates.
This commit is contained in:
Veikko Sariola
2020-12-14 15:44:16 +02:00
parent 92c8b70fd2
commit 2ad61ff6b2
19 changed files with 2934 additions and 212 deletions

View File

@ -9,6 +9,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"gopkg.in/yaml.v3"
@ -24,6 +25,7 @@ func main() {
help := flag.Bool("h", false, "Show help.")
play := flag.Bool("p", false, "Play the input songs.")
asmOut := flag.Bool("a", false, "Output the song as .asm file, to standard output unless otherwise specified.")
tmplDir := flag.String("t", "", "Output the song as by parsing the templates in directory, to standard output unless otherwise specified.")
jsonOut := flag.Bool("j", false, "Output the song as .json file, to standard output unless otherwise specified.")
yamlOut := flag.Bool("y", false, "Output the song as .yml file, to standard output unless otherwise specified.")
headerOut := flag.Bool("c", false, "Output .h C header file, to standard output unless otherwise specified.")
@ -31,13 +33,15 @@ func main() {
rawOut := flag.Bool("r", false, "Output the rendered song as .raw stereo float32 buffer, to standard output unless otherwise specified.")
directory := flag.String("d", "", "Directory where to output all files. The directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
hold := flag.Int("o", -1, "New value to be used as the hold value")
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to OS architecture. Possible values: 386, amd64")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current OS. Possible values: windows, darwin, linux (anything else is assumed linuxy)")
flag.Usage = printUsage
flag.Parse()
if flag.NArg() == 0 || *help {
flag.Usage()
os.Exit(0)
}
if !*asmOut && !*jsonOut && !*rawOut && !*headerOut && !*play && !*yamlOut {
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
@ -129,7 +133,7 @@ func main() {
}
}
if *asmOut {
asmCode, err := go4k.FormatAsm(&song)
asmCode, err := go4k.Compile(&song, *targetArch, *targetOs)
if err != nil {
return fmt.Errorf("Could not format the song as asm file: %v", err)
}

View File

@ -79,6 +79,26 @@ func Render(synth Synth, buffer []float32) error {
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
}
// UnitParameter documents one parameter that an unit takes
type UnitParameter struct {
Name string // thould be found with this name in the Unit.Parameters map
@ -88,185 +108,175 @@ type UnitParameter struct {
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
}
// UnitType documents the supported behaviour of one type of unit (oscillator, envelope etc.)
type UnitType struct {
Name string
Parameters []UnitParameter
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 = []UnitType{
{
Name: "add",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "addp",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "pop",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "loadnote",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "mul",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "mulp",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "push",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "xch",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "distort",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "hold",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "crush",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "gain",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "invgain",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "filter",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "clip",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "pan",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "delay",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{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: true},
{Name: "count", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
}},
{
Name: "compressor",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "speed",
Parameters: []UnitParameter{}},
{
Name: "out",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "outaux",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "aux",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false},
}},
{
Name: "send",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
{Name: "unit", MinValue: 0, MaxValue: 63, CanSet: true, CanModulate: false},
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
}},
{
Name: "envelope",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "noise",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "oscillator",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{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: "loadval",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "receive",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
}},
{
Name: "in",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false},
}},
var UnitTypes = map[string]([]UnitParameter){
"add": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"addp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"pop": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"loadnote": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"mul": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"mulp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"push": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"xch": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"distort": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"hold": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"crush": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"gain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"invgain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"filter": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"clip": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"pan": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"delay": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{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},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"speed": []UnitParameter{},
"out": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"outaux": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"aux": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
"send": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
{Name: "unit", MinValue: 0, MaxValue: 63, CanSet: true, CanModulate: false},
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"envelope": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"noise": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"oscillator": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{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}},
"loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"receive": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"in": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
}

627
go4k/macros.go Normal file
View File

@ -0,0 +1,627 @@
package go4k
import (
"bytes"
"fmt"
"math"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/Masterminds/sprig"
)
type OplistEntry struct {
Type string
NumParams int
}
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)
}
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
}
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) Stereo(t string) bool {
return p.stereo[t]
}
func (p *Macros) Mono(t string) bool {
return p.mono[t]
}
func (p *Macros) StereoAndMono(t string) bool {
return p.stereo[t] && p.mono[t]
}
// Macros and functions to accumulate constants automagically
func (p *Macros) Float(value float32) string {
if _, ok := p.usesFloatConst[value]; !ok {
p.usesFloatConst[value] = true
p.floatConsts = append(p.floatConsts, value)
}
return nameForFloat(value)
}
func (p *Macros) Int(value int) string {
if _, ok := p.usesIntConst[value]; !ok {
p.usesIntConst[value] = true
p.intConsts = append(p.intConsts, value)
}
return nameForInt(value)
}
func (p *Macros) Constants() string {
var b strings.Builder
for _, v := range p.floatConsts {
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForFloat(v), math.Float32bits(v))
}
for _, v := range p.intConsts {
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForInt(v), v)
}
return b.String()
}
func nameForFloat(value float32) string {
s := fmt.Sprintf("%#g", value)
s = strings.Replace(s, ".", "_", 1)
s = strings.Replace(s, "-", "m", 1)
s = strings.Replace(s, "+", "p", 1)
return "FCONST_" + s
}
func nameForInt(value int) string {
return "ICONST_" + fmt.Sprintf("%d", value)
}
func (p *Macros) PTRSIZE() int {
if p.Amd64 {
return 8
}
return 4
}
func (p *Macros) DPTR() string {
if p.Amd64 {
return "dq"
}
return "dd"
}
func (p *Macros) PTRWORD() string {
if p.Amd64 {
return "qword"
}
return "dword"
}
func (p *Macros) AX() string {
if p.Amd64 {
return "rax"
}
return "eax"
}
func (p *Macros) BX() string {
if p.Amd64 {
return "rbx"
}
return "ebx"
}
func (p *Macros) CX() string {
if p.Amd64 {
return "rcx"
}
return "ecx"
}
func (p *Macros) DX() string {
if p.Amd64 {
return "rdx"
}
return "edx"
}
func (p *Macros) SI() string {
if p.Amd64 {
return "rsi"
}
return "esi"
}
func (p *Macros) DI() string {
if p.Amd64 {
return "rdi"
}
return "edi"
}
func (p *Macros) SP() string {
if p.Amd64 {
return "rsp"
}
return "esp"
}
func (p *Macros) BP() string {
if p.Amd64 {
return "rbp"
}
return "ebp"
}
func (p *Macros) WRK() string {
return p.BP()
}
func (p *Macros) VAL() string {
return p.SI()
}
func (p *Macros) COM() string {
return p.BX()
}
func (p *Macros) INP() string {
return p.DX()
}
func (p *Macros) SaveStack(scope string) string {
p.stackframes[scope] = p.Stacklocs
return ""
}
func (p *Macros) Call(funcname string) (string, error) {
p.calls[funcname] = true
var s = make([]string, len(p.Stacklocs))
copy(s, p.Stacklocs)
p.stackframes[funcname] = s
return "call " + funcname, nil
}
func (p *Macros) TailCall(funcname string) (string, error) {
p.calls[funcname] = true
p.stackframes[funcname] = p.Stacklocs
return "jmp " + funcname, nil
}
func (p *Macros) SectText(name string) string {
if p.OS == "windows" {
if p.DisableSections {
return "section .code align=1"
}
return fmt.Sprintf("section .%v code align=1", name)
} else if p.OS == "darwin" {
return "section .text align=1"
} else {
if p.DisableSections {
return "section .text. progbits alloc exec nowrite align=1"
}
return fmt.Sprintf("section .text.%v progbits alloc exec nowrite align=1", name)
}
}
func (p *Macros) SectData(name string) string {
if p.OS == "windows" || p.OS == "darwin" {
if p.OS == "windows" && !p.DisableSections {
return fmt.Sprintf("section .%v data align=1", name)
}
return "section .data align=1"
} else {
if !p.DisableSections {
return fmt.Sprintf("section .data.%v progbits alloc noexec write align=1", name)
}
return "section .data. progbits alloc exec nowrite align=1"
}
}
func (p *Macros) SectBss(name string) string {
if p.OS == "windows" || p.OS == "darwin" {
if p.OS == "windows" && !p.DisableSections {
return fmt.Sprintf("section .%v bss align=256", name)
}
return "section .bss align=256"
} else {
if !p.DisableSections {
return fmt.Sprintf("section .bss.%v progbits alloc noexec write align=256", name)
}
return "section .bss. progbits alloc exec nowrite align=256"
}
}
func (p *Macros) Data(label string) string {
return fmt.Sprintf("%v\n%v:", p.SectData(label), label)
}
func (p *Macros) Func(funcname string, scope ...string) (string, error) {
scopeName := funcname
if len(scope) > 1 {
return "", fmt.Errorf(`Func macro "%v" can take only one additional scope parameter, "%v" were given`, funcname, scope)
} else if len(scope) > 0 {
scopeName = scope[0]
}
p.Stacklocs = append(p.stackframes[scopeName], "retaddr_"+funcname)
return fmt.Sprintf("%v\n%v:", p.SectText(funcname), funcname), nil
}
func (p *Macros) HasCall(funcname string) bool {
return p.calls[funcname]
}
func (p *Macros) Push(value string, name string) string {
p.Stacklocs = append(p.Stacklocs, name)
return fmt.Sprintf("push %v ; Stack: %v ", value, p.FmtStack())
}
func (p *Macros) PushRegs(params ...string) string {
if p.Amd64 {
var b strings.Builder
for i := 0; i < len(params); i = i + 2 {
b.WriteRune('\n')
b.WriteString(p.Push(params[i], params[i+1]))
}
return b.String()
} else {
var pushadOrder = [...]string{"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"}
for _, name := range pushadOrder {
for j := 0; j < len(params); j = j + 2 {
if params[j] == name {
name = params[j+1]
}
}
p.Stacklocs = append(p.Stacklocs, name)
}
return fmt.Sprintf("\npushad ; Stack: %v", p.FmtStack())
}
}
func (p *Macros) PopRegs(params ...string) string {
if p.Amd64 {
var b strings.Builder
for i := len(params) - 1; i >= 0; i-- {
b.WriteRune('\n')
b.WriteString(p.Pop(params[i]))
}
return b.String()
} else {
var regs = [...]string{"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"}
var b strings.Builder
for i, name := range p.Stacklocs[len(p.Stacklocs)-8:] {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(regs[i])
if regs[i] != name {
b.WriteString(" = ")
b.WriteString(name)
}
}
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-8]
return fmt.Sprintf("\npopad ; Popped: %v. Stack: %v", b.String(), p.FmtStack())
}
}
func (p *Macros) Pop(register string) string {
last := p.Stacklocs[len(p.Stacklocs)-1]
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1]
return fmt.Sprintf("pop %v ; %v = %v, Stack: %v ", register, register, last, p.FmtStack())
}
func (p *Macros) Stack(name string) (string, error) {
for i, k := range p.Stacklocs {
if k == name {
pos := len(p.Stacklocs) - i - 1
if p.Amd64 {
pos = pos * 8
} else {
pos = pos * 4
}
if pos != 0 {
return fmt.Sprintf("%v + %v", p.SP(), pos), nil
}
return p.SP(), nil
}
}
return "", fmt.Errorf("unknown symbol %v", name)
}
func (p *Macros) FmtStack() string {
var b strings.Builder
last := len(p.Stacklocs) - 1
for i := range p.Stacklocs {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(p.Stacklocs[last-i])
}
return b.String()
}
func (p *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
if p.OS == "windows" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4)
}
}
if p.OS == "darwin" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name)
}
return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name)
}
func (p *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)
}
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)
}
return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil
}
func (p *Macros) Prepare(value string, regs ...string) (string, error) {
if p.Amd64 {
if len(regs) > 1 {
return "", fmt.Errorf("macro Prepare cannot accept more than one register parameter")
} else if len(regs) > 0 {
return fmt.Sprintf("\nmov r9, qword %v\nlea r9, [r9 + %v]", value, regs[0]), nil
}
return fmt.Sprintf("\nmov r9, qword %v", value), nil
}
return "", nil
}
func (p *Macros) Use(value string, regs ...string) (string, error) {
if p.Amd64 {
return "r9", nil
}
if len(regs) > 1 {
return "", fmt.Errorf("macro Use cannot accept more than one register parameter")
} else if len(regs) > 0 {
return value + " + " + regs[0], nil
}
return value, nil
}
func (p *PlayerMacros) NumDelayLines() string {
total := 0
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == "delay" {
total += unit.Parameters["count"] * (1 + unit.Parameters["stereo"])
}
}
}
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
}