From 2ad61ff6b2caea65cdd9506be27d634e8fac454f Mon Sep 17 00:00:00 2001 From: Veikko Sariola Date: Mon, 14 Dec 2020 15:44:16 +0200 Subject: [PATCH] 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. --- go.mod | 7 + go.sum | 18 + go4k/cmd/sointu-cli/main.go | 8 +- go4k/go4k.go | 364 ++++++++++---------- go4k/macros.go | 627 +++++++++++++++++++++++++++++++++++ templates/arithmetic.asm | 205 ++++++++++++ templates/effects.asm | 393 ++++++++++++++++++++++ templates/flowcontrol.asm | 23 ++ templates/gmdls.asm | 58 ++++ templates/library.asm | 304 +++++++++++++++++ templates/output_sound.asm | 44 +++ templates/patch.asm | 190 +++++++++++ templates/player.asm | 237 +++++++++++++ templates/sinks.asm | 126 +++++++ templates/sources.asm | 415 +++++++++++++++++++++++ templates/structs.asm | 43 +++ tests/CMakeLists.txt | 64 ++-- tests/test_delay_flanger.yml | 2 +- tests/test_renderer.c | 18 +- 19 files changed, 2934 insertions(+), 212 deletions(-) create mode 100644 go4k/macros.go create mode 100644 templates/arithmetic.asm create mode 100644 templates/effects.asm create mode 100644 templates/flowcontrol.asm create mode 100644 templates/gmdls.asm create mode 100644 templates/library.asm create mode 100644 templates/output_sound.asm create mode 100644 templates/patch.asm create mode 100644 templates/player.asm create mode 100644 templates/sinks.asm create mode 100644 templates/sources.asm create mode 100644 templates/structs.asm diff --git a/go.mod b/go.mod index dd93ec7..bbe8ee6 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,13 @@ go 1.15 require ( gioui.org v0.0.0-20201106195654-dbc0796d0207 + github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible + github.com/google/uuid v1.1.2 // indirect github.com/hajimehoshi/oto v0.6.6 + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 ) diff --git a/go.sum b/go.sum index 9626742..3faf131 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,27 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 gioui.org v0.0.0-20201106195654-dbc0796d0207 h1:tB+woXgNaCiudnpU7QmRD7J92YrBz7R4NAGgEjOnEzQ= gioui.org v0.0.0-20201106195654-dbc0796d0207/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hajimehoshi/oto v0.6.6 h1:HYSZ8cYZqOL4iHugvbcfhNN2smiSOsBMaoSBi4nnWcw= github.com/hajimehoshi/oto v0.6.6/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= @@ -31,5 +48,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go4k/cmd/sointu-cli/main.go b/go4k/cmd/sointu-cli/main.go index 92c1df8..4285768 100644 --- a/go4k/cmd/sointu-cli/main.go +++ b/go4k/cmd/sointu-cli/main.go @@ -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) } diff --git a/go4k/go4k.go b/go4k/go4k.go index 5275fe5..5559164 100644 --- a/go4k/go4k.go +++ b/go4k/go4k.go @@ -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}}, } diff --git a/go4k/macros.go b/go4k/macros.go new file mode 100644 index 0000000..24d1716 --- /dev/null +++ b/go4k/macros.go @@ -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 +} diff --git a/templates/arithmetic.asm b/templates/arithmetic.asm new file mode 100644 index 0000000..dc05dfb --- /dev/null +++ b/templates/arithmetic.asm @@ -0,0 +1,205 @@ +{{- if .Opcode "pop"}} +;------------------------------------------------------------------------------- +; POP opcode: remove (discard) the topmost signal from the stack +;------------------------------------------------------------------------------- +{{- if .Mono "pop" -}} +; Mono: a -> (empty) +{{- end}} +{{- if .Stereo "pop" -}} +; Stereo: a b -> (empty) +{{- end}} +;------------------------------------------------------------------------------- +{{.Func "su_op_pop" "Opcode"}} +{{- if .StereoAndMono "pop"}} + jnc su_op_pop_mono +{{- end}} +{{- if .Stereo "pop"}} + fstp st0 +{{- end}} +{{- if .StereoAndMono "pop"}} +su_op_pop_mono: +{{- end}} + fstp st0 + ret +{{end}} + + +{{- if .Opcode "add"}} +;------------------------------------------------------------------------------- +; ADD opcode: add the two top most signals on the stack +;------------------------------------------------------------------------------- +{{- if .Mono "add"}} +; Mono: a b -> a+b b +{{- end}} +{{- if .Stereo "add" -}} +; Stereo: a b c d -> a+c b+d c d +{{- end}} +;------------------------------------------------------------------------------- +{{.Func "su_op_add" "Opcode"}} +{{- if .StereoAndMono "add"}} + jnc su_op_add_mono +{{- end}} +{{- if .Stereo "add"}} + fadd st0, st2 + fxch + fadd st0, st3 + fxch + ret +{{- end}} +{{- if .StereoAndMono "add"}} +su_op_add_mono: +{{- end}} +{{- if .Mono "add"}} + fadd st1 +{{- end}} +{{- if .Mono "add"}} + ret + {{- end}} +{{end}} + + +{{- if .Opcode "addp"}} +;------------------------------------------------------------------------------- +; ADDP opcode: add the two top most signals on the stack and pop +;------------------------------------------------------------------------------- +; Mono: a b -> a+b +; Stereo: a b c d -> a+c b+d +;------------------------------------------------------------------------------- +{{.Func "su_op_addp" "Opcode"}} +{{- if .StereoAndMono "addp"}} + jnc su_op_addp_mono +{{- end}} +{{- if .Stereo "addp"}} + faddp st2, st0 + faddp st2, st0 + ret +{{- end}} +{{- if .StereoAndMono "addp"}} +su_op_addp_mono: +{{- end}} +{{- if (.Mono "addp")}} + faddp st1, st0 + ret +{{- end}} +{{end}} + + +{{- if .Opcode "loadnote"}} +;------------------------------------------------------------------------------- +; LOADNOTE opcode: load the current note, scaled to [-1,1] +;------------------------------------------------------------------------------- +{{if (.Mono "loadnote") -}} ; Mono: (empty) -> n, where n is the note{{end}} +{{if (.Stereo "loadnote") -}}; Stereo: (empty) -> n n{{end}} +;------------------------------------------------------------------------------- +{{.Func "su_op_loadnote" "Opcode"}} +{{- if .StereoAndMono "loadnote"}} + jnc su_op_loadnote_mono +{{- end}} +{{- if .Stereo "loadnote"}} + call su_op_loadnote_mono + su_op_loadnote_mono: +{{- end}} + fild dword [{{.INP}}-su_voice.inputs+su_voice.note] + {{.Prepare (.Float 0.0078125)}} + fmul dword [{{.Use (.Float 0.0078125)}}] ; s=n/128.0 + {{.Prepare (.Float 0.5)}} + fsub dword [{{.Use (.Float 0.5)}}] ; s-.5 + fadd st0, st0 ; 2*s-1 + ret +{{end}} + + +{{- if .Opcode "mul"}} +;------------------------------------------------------------------------------- +; MUL opcode: multiply the two top most signals on the stack +;------------------------------------------------------------------------------- +; Mono: a b -> a*b a +; Stereo: a b c d -> a*c b*d c d +;------------------------------------------------------------------------------- +{{.Func "su_op_mul" "Opcode"}} + jnc su_op_mul_mono + fmul st0, st2 + fxch + fadd st0, st3 + fxch + ret +su_op_mul_mono: + fmul st1 + ret +{{end}} + + +{{- if .Opcode "mulp"}} +;------------------------------------------------------------------------------- +; MULP opcode: multiply the two top most signals on the stack and pop +;------------------------------------------------------------------------------- +; Mono: a b -> a*b +; Stereo: a b c d -> a*c b*d +;------------------------------------------------------------------------------- +{{.Func "su_op_mulp" "Opcode"}} +{{- if .StereoAndMono "mulp"}} + jnc su_op_mulp_mono +{{- end}} +{{- if .Stereo "mulp"}} + fmulp st2, st0 + fmulp st2, st0 + ret +{{- end}} +{{- if .StereoAndMono "mulp"}} +su_op_mulp_mono: +{{- end}} +{{- if .Mono "mulp"}} + fmulp st1 + ret +{{- end}} +{{end}} + + +{{- if .Opcode "push"}} +;------------------------------------------------------------------------------- +; PUSH opcode: push the topmost signal on the stack +;------------------------------------------------------------------------------- +; Mono: a -> a a +; Stereo: a b -> a b a b +;------------------------------------------------------------------------------- +{{.Func "su_op_push" "Opcode"}} +{{- if .StereoAndMono "push"}} + jnc su_op_push_mono +{{- end}} +{{- if .Stereo "push"}} + fld st1 + fld st1 + ret +{{- end}} +{{- if .StereoAndMono "push"}} +su_op_push_mono: +{{- end}} +{{- if .Mono "push"}} + fld st0 + ret + {{- end}} +{{end}} + + +{{- if .Opcode "xch"}} +;------------------------------------------------------------------------------- +; XCH opcode: exchange the signals on the stack +;------------------------------------------------------------------------------- +; Mono: a b -> b a +; stereo: a b c d -> c d a b +;------------------------------------------------------------------------------- +{{.Func "su_op_xch" "Opcode"}} +{{- if .StereoAndMono "xch"}} + jnc su_op_xch_mono +{{- end}} +{{- if .Stereo "xch"}} + fxch st0, st2 ; c b a d + fxch st0, st1 ; b c a d + fxch st0, st3 ; d c a b +{{- end}} +{{- if .StereoAndMono "xch"}} +su_op_xch_mono: +{{- end}} + fxch st0, st1 + ret +{{end}} diff --git a/templates/effects.asm b/templates/effects.asm new file mode 100644 index 0000000..6af5c0e --- /dev/null +++ b/templates/effects.asm @@ -0,0 +1,393 @@ +{{- if .Opcode "distort"}} +;------------------------------------------------------------------------------- +; DISTORT opcode: apply distortion on the signal +;------------------------------------------------------------------------------- +; Mono: x -> x*a/(1-a+(2*a-1)*abs(x)) where x is clamped first +; Stereo: l r -> l*a/(1-a+(2*a-1)*abs(l)) r*a/(1-a+(2*a-1)*abs(r)) +;------------------------------------------------------------------------------- +{{.Func "su_op_distort" "Opcode"}} +{{- if .Stereo "distort" -}} + {{.Call "su_effects_stereohelper"}} +{{- end}} + fld dword [{{.Input "distort" "drive"}}] + {{.TailCall "su_waveshaper"}} +{{end}} + + +{{- if .Opcode "hold"}} +;------------------------------------------------------------------------------- +; HOLD opcode: sample and hold the signal, reducing sample rate +;------------------------------------------------------------------------------- +; Mono version: holds the signal at a rate defined by the freq parameter +; Stereo version: holds both channels +;------------------------------------------------------------------------------- +{{.Func "su_op_hold" "Opcode"}} +{{- if .Stereo "hold"}} + {{.Call "su_effects_stereohelper"}} +{{- end}} + fld dword [{{.Input "hold" "holdfreq"}}] ; f x + fmul st0, st0 ; f^2 x + fchs ; -f^2 x + fadd dword [{{.WRK}}] ; p-f^2 x + fst dword [{{.WRK}}] ; p <- p-f^2 + fldz ; 0 p x + fucomip st1 ; p x + fstp dword [{{.SP}}-4] ; t=p, x + jc short su_op_hold_holding ; if (0 < p) goto holding + fld1 ; 1 x + fadd dword [{{.SP}}-4] ; 1+t x + fstp dword [{{.WRK}}] ; x + fst dword [{{.WRK}}+4] ; save holded value + ret ; x +su_op_hold_holding: + fstp st0 ; + fld dword [{{.WRK}}+4] ; x + ret +{{end}} + + +{{- if .Opcode "crush"}} +;------------------------------------------------------------------------------- +; CRUSH opcode: quantize the signal to finite number of levels +;------------------------------------------------------------------------------- +; Mono: x -> e*int(x/e) +; Stereo: l r -> e*int(l/e) e*int(r/e) +;------------------------------------------------------------------------------- +{{.Func "su_op_crush" "Opcode"}} +{{- if .Stereo "crush"}} + {{.Call "su_effects_stereohelper"}} +{{- end}} + fdiv dword [{{.Input "crush" "resolution"}}] + frndint + fmul dword [{{.Input "crush" "resolution"}}] + ret +{{end}} + + +{{- if .Opcode "gain"}} +;------------------------------------------------------------------------------- +; GAIN opcode: apply gain on the signal +;------------------------------------------------------------------------------- +; Mono: x -> x*g +; Stereo: l r -> l*g r*g +;------------------------------------------------------------------------------- +{{.Func "su_op_gain" "Opcode"}} +{{- if .Stereo "gain"}} + fld dword [{{.Input "gain" "gain"}}] ; g l (r) +{{- if .Mono "invgain"}} + jnc su_op_gain_mono +{{- end}} + fmul st2, st0 ; g l r/g +su_op_gain_mono: + fmulp st1, st0 ; l/g (r/) + ret +{{- else}} + fmul dword [{{.Input "gain" "gain"}}] + ret +{{- end}} +{{end}} + + +{{- if .Opcode "invgain"}} +;------------------------------------------------------------------------------- +; INVGAIN opcode: apply inverse gain on the signal +;------------------------------------------------------------------------------- +; Mono: x -> x/g +; Stereo: l r -> l/g r/g +;------------------------------------------------------------------------------- +{{.Func "su_op_invgain" "Opcode"}} +{{- if .Stereo "invgain"}} + fld dword [{{.Input "invgain" "invgain"}}] ; g l (r) +{{- if .Mono "invgain"}} + jnc su_op_invgain_mono +{{- end}} + fdiv st2, st0 ; g l r/g +su_op_invgain_mono: + fdivp st1, st0 ; l/g (r/) + ret +{{- else}} + fdiv dword [{{.Input "invgain" "invgain"}}] + ret +{{- end}} +{{end}} + + +{{- if .Opcode "filter"}} +;------------------------------------------------------------------------------- +; FILTER opcode: perform low/high/band-pass/notch etc. filtering on the signal +;------------------------------------------------------------------------------- +; Mono: x -> filtered(x) +; Stereo: l r -> filtered(l) filtered(r) +;------------------------------------------------------------------------------- +{{.Func "su_op_filter" "Opcode"}} + lodsb ; load the flags to al +{{- if .Stereo "filter"}} + {{.Call "su_effects_stereohelper"}} +{{- end}} + fld dword [{{.Input "filter" "resonance"}}] ; r x + fld dword [{{.Input "filter" "frequency"}}]; f r x + fmul st0, st0 ; f2 x (square the input so we never get negative and also have a smoother behaviour in the lower frequencies) + fst dword [{{.SP}}-4] ; f2 r x + fmul dword [{{.WRK}}+8] ; f2*b r x + fadd dword [{{.WRK}}] ; f2*b+l r x + fst dword [{{.WRK}}] ; l'=f2*b+l r x + fsubp st2, st0 ; r x-l' + fmul dword [{{.WRK}}+8] ; r*b x-l' + fsubp st1, st0 ; x-l'-r*b + fst dword [{{.WRK}}+4] ; h'=x-l'-r*b + fmul dword [{{.SP}}-4] ; f2*h' + fadd dword [{{.WRK}}+8] ; f2*h'+b + fstp dword [{{.WRK}}+8] ; b'=f2*h'+b + fldz ; 0 +{{- if .HasParamValue "filter" "lowpass" 1}} + test al, byte 0x40 + jz short su_op_filter_skiplowpass + fadd dword [{{.WRK}}] +su_op_filter_skiplowpass: +{{- end}} +{{- if .HasParamValue "filter" "bandpass" 1}} + test al, byte 0x20 + jz short su_op_filter_skipbandpass + fadd dword [{{.WRK}}+8] +su_op_filter_skipbandpass: +{{- end}} +{{- if .HasParamValue "filter" "highpass" 1}} + test al, byte 0x10 + jz short su_op_filter_skiphighpass + fadd dword [{{.WRK}}+4] +su_op_filter_skiphighpass: +{{- end}} +{{- if .HasParamValue "filter" "negbandpass" 1}} + test al, byte 0x08 + jz short su_op_filter_skipnegbandpass + fsub dword [{{.WRK}}+8] +su_op_filter_skipnegbandpass: +{{- end}} +{{- if .HasParamValue "filter" "neghighpass" 1}} + test al, byte 0x04 + jz short su_op_filter_skipneghighpass + fsub dword [{{.WRK}}+4] +su_op_filter_skipneghighpass: +{{- end}} + ret +{{end}} + + +{{- if .Opcode "clip"}} +;------------------------------------------------------------------------------- +; CLIP opcode: clips the signal into [-1,1] range +;------------------------------------------------------------------------------- +; Mono: x -> min(max(x,-1),1) +; Stereo: l r -> min(max(l,-1),1) min(max(r,-1),1) +;------------------------------------------------------------------------------- +{{.Func "su_op_clip" "Opcode"}} +{{- if .Stereo "clip"}} + {{.Call "su_effects_stereohelper"}} +{{- end}} + {{.TailCall "su_clip"}} +{{end}} + + +{{- if .Opcode "pan" -}} +;------------------------------------------------------------------------------- +; PAN opcode: pan the signal +;------------------------------------------------------------------------------- +; Mono: s -> s*(1-p) s*p +; Stereo: l r -> l*(1-p) r*p +; +; where p is the panning in [0,1] range +;------------------------------------------------------------------------------- +{{.Func "su_op_pan" "Opcode"}} +{{- if .Stereo "pan"}} + jc su_op_pan_do ; this time, if this is mono op... + fld st0 ; ...we duplicate the mono into stereo first +su_op_pan_do: + fld dword [{{.Input "pan" "panning"}}] ; p l r + fld1 ; 1 p l r + fsub st1 ; 1-p p l r + fmulp st2 ; p (1-p)*l r + fmulp st2 ; (1-p)*l p*r + ret +{{- else}} + fld dword [{{.Input "pan" "panning"}}] ; p s + fmul st1 ; p*s s + fsub st1, st0 ; p*s s-p*s + ; Equal to + ; s*p s*(1-p) + fxch ; s*(1-p) s*p SHOULD PROBABLY DELETE, WHY BOTHER + ret +{{- end}} +{{end}} + + +{{- if .Opcode "delay"}} +;------------------------------------------------------------------------------- +; DELAY opcode: adds delay effect to the signal +;------------------------------------------------------------------------------- +; Mono: perform delay on ST0, using delaycount delaylines starting +; at delayindex from the delaytable +; Stereo: perform delay on ST1, using delaycount delaylines starting +; at delayindex + delaycount from the delaytable (so the right delays +; can be different) +;------------------------------------------------------------------------------- +{{.Func "su_op_delay" "Opcode"}} + lodsw ; al = delay index, ah = delay count + {{- .PushRegs .VAL "DelayVal" .COM "DelayCom" | indent 4}} + movzx ebx, al + ; %ifdef RUNTIME_TABLES ; when using runtime tables, delaytimes is pulled from the stack so can be a pointer to heap + ; mov _SI, [{{.SP}} + su_stack.delaytimes + PUSH_REG_SIZE(2)] + ; lea _BX, [_SI + _BX*2] + ; %else +{{.Prepare "su_delay_times" | indent 4}} + lea {{.BX}},[{{.Use "su_delay_times"}} + {{.BX}}*2] ; BX now points to the right position within delay time table + movzx esi, word [{{.Stack "GlobalTick"}}] ; notice that we load word, so we wrap at 65536 + mov {{.CX}}, {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}] ; {{.WRK}} is now the separate delay workspace, as they require a lot more space +{{- if .StereoAndMono "delay"}} + jnc su_op_delay_mono +{{- end}} +{{- if .Stereo "delay"}} + push {{.AX}} ; save _ah (delay count) + fxch ; r l + call su_op_delay_do ; D(r) l process delay for the right channel + pop {{.AX}} ; restore the count for second run + fxch ; l D(r) +su_op_delay_mono: ; flow into mono delay +{{- end}} + call su_op_delay_do ; when stereo delay is not enabled, we could inline this to save 5 bytes, but I expect stereo delay to be farely popular so maybe not worth the hassle + mov {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}],{{.CX}} ; move delay workspace pointer back to stack. + {{- .PopRegs .VAL .COM | indent 4}} +{{- if .UsesDelayModulation}} + xor eax, eax + mov dword [{{.Modulation "delay" "delaytime"}}], eax +{{- end}} + ret + +;------------------------------------------------------------------------------- +; su_op_delay_do: executes the actual delay +;------------------------------------------------------------------------------- +; Pseudocode: +; q = dr*x +; for (i = 0;i < count;i++) +; s = b[(t-delaytime[i+offset])&65535] +; q += s +; o[i] = o[i]*da+s*(1-da) +; b[t] = f*o[i] +p^2*x +; Perform dc-filtering q and output q +;------------------------------------------------------------------------------- +{{.Func "su_op_delay_do"}} ; x y + fld st0 + fmul dword [{{.Input "delay" "pregain"}}] ; p*x y + fmul dword [{{.Input "delay" "pregain"}}] ; p*p*x y + fxch ; y p*p*x + fmul dword [{{.Input "delay" "dry"}}] ; dr*y p*p*x +su_op_delay_loop: + {{- if or .UsesDelayModulation (.HasParamValue "delay" "notetracking" 1)}} ; delaytime modulation or note syncing require computing the delay time in floats + fild word [{{.BX}}] ; k dr*y p*p*x, where k = delay time + {{- if .HasParamValue "delay" "notetracking" 1}} + test ah, 1 ; note syncing is the least significant bit of ah, 0 = ON, 1 = OFF + jne su_op_delay_skipnotesync + fild dword [{{.INP}}-su_voice.inputs+su_voice.note] + {{.Int 0x3DAAAAAA | .Prepare | indent 8}} + fmul dword [{{.Int 0x3DAAAAAA | .Use}}] + {{.Call "su_power"}} + fdivp st1, st0 ; use 10787 for delaytime to have neutral transpose + su_op_delay_skipnotesync: + {{- end}} + {{- if .UsesDelayModulation}} + fld dword [{{.Modulation "delay" "delaytime"}}] + {{- .Float 32767.0 | .Prepare | indent 8}} + fmul dword [{{.Float 32767.0 | .Use}}] ; scale it up, as the modulations would be too small otherwise + faddp st1, st0 + {{- end}} + fistp dword [{{.SP}}-4] ; dr*y p*p*x, dword [{{.SP}}-4] = integer amount of delay (samples) + mov edi, esi ; edi = esi = current time + sub di, word [{{.SP}}-4] ; we perform the math in 16-bit to wrap around + {{- else}} + mov edi, esi + sub di, word [{{.BX}}] ; we perform the math in 16-bit to wrap around + {{- end}} + fld dword [{{.CX}}+su_delayline_wrk.buffer+{{.DI}}*4]; s dr*y p*p*x, where s is the sample from delay buffer + fadd st1, st0 ; s dr*y+s p*p*x (add comb output to current output) + fld1 ; 1 s dr*y+s p*p*x + fsub dword [{{.Input "delay" "damp"}}] ; 1-da s dr*y+s p*p*x + fmulp st1, st0 ; s*(1-da) dr*y+s p*p*x + fld dword [{{.Input "delay" "damp"}}] ; da s*(1-da) dr*y+s p*p*x + fmul dword [{{.CX}}+su_delayline_wrk.filtstate] ; o*da s*(1-da) dr*y+s p*p*x, where o is stored + faddp st1, st0 ; o*da+s*(1-da) dr*y+s p*p*x + fst dword [{{.CX}}+su_delayline_wrk.filtstate] ; o'=o*da+s*(1-da), o' dr*y+s p*p*x + fmul dword [{{.Input "delay" "feedback"}}] ; f*o' dr*y+s p*p*x + fadd st0, st2 ; f*o'+p*p*x dr*y+s p*p*x + fstp dword [{{.CX}}+su_delayline_wrk.buffer+{{.SI}}*4]; save f*o'+p*p*x to delay buffer + add {{.BX}},2 ; move to next index + add {{.CX}}, su_delayline_wrk.size ; go to next delay delay workspace + sub ah, 2 + jg su_op_delay_loop ; if ah > 0, goto loop + fstp st1 ; dr*y+s1+s2+s3+... + ; DC-filtering + fld dword [{{.CX}}+su_delayline_wrk.dcout] ; o s +{{- .Float 0.99609375 | .Prepare | indent 4}} + fmul dword [{{.Float 0.99609375 | .Use}}] ; c*o s + fsub dword [{{.CX}}+su_delayline_wrk.dcin] ; c*o-i s + fxch ; s c*o-i + fst dword [{{.CX}}+su_delayline_wrk.dcin] ; i'=s, s c*o-i + faddp st1 ; s+c*o-i +{{- .Float 0.5 | .Prepare | indent 4}} + fadd dword [{{.Float 0.5 | .Use}}] ; add and sub small offset to prevent denormalization + fsub dword [{{.Float 0.5 | .Use}}] + fst dword [{{.CX}}+su_delayline_wrk.dcout] ; o'=s+c*o-i + ret +{{end}} + + +{{- if .Opcode "compressor"}} +;------------------------------------------------------------------------------- +; COMPRES opcode: push compressor gain to stack +;------------------------------------------------------------------------------- +; Mono: push g on stack, where g is a suitable gain for the signal +; you can either MULP to compress the signal or SEND it to a GAIN +; somewhere else for compressor side-chaining. +; Stereo: push g g on stack, where g is calculated using l^2 + r^2 +;------------------------------------------------------------------------------- +{{.Func "su_op_compressor" "Opcode"}} + fdiv dword [{{.Input "compressor" "invgain"}}]; l/g, we'll call this pre inverse gained signal x from now on + fld st0 ; x x + fmul st0, st0 ; x^2 x +{{- if .StereoAndMono "compressor"}} + jnc su_op_compressor_mono +{{- end}} +{{- if .Stereo "compressor"}} + fld st2 ; r x^2 l/g r + fdiv dword [{{.Input "compressor" "invgain"}}]; r/g, we'll call this pre inverse gained signal y from now on + fst st3 ; y x^2 l/g r/g + fmul st0, st0 ; y^2 x^2 l/g r/g + faddp st1, st0 ; y^2+x^2 l/g r/g + call su_op_compressor_mono ; So, for stereo, we square both left & right and add them up + fld st0 ; and return the computed gain two times, ready for MULP STEREO + ret +su_op_compressor_mono: +{{- end}} + fld dword [{{.WRK}}] ; l x^2 x + fucomi st0, st1 + setnb al ; if (st0 >= st1) al = 1; else al = 0; + fsubp st1, st0 ; x^2-l x + {{.Call "su_nonlinear_map"}} ; c x^2-l x, c is either attack or release parameter mapped in a nonlinear way + fmulp st1, st0 ; c*(x^2-l) x + fadd dword [{{.WRK}}] ; l+c*(x^2-l) x // we could've kept level in the stack and save a few bytes, but su_env_map uses 3 stack (c + 2 temp), so the stack was getting quite big. + fst dword [{{.WRK}}] ; l'=l+c*(x^2-l), l' x + fld dword [{{.Input "compressor" "threshold"}}] ; t l' x + fmul st0, st0 ; t*t l' x + fxch ; l' t*t x + fucomi st0, st1 ; if l' < t*t + fcmovb st0, st1 ; l'=t*t + fdivp st1, st0 ; t*t/l' x + fld dword [{{.Input "compressor" "ratio"}}] ; r t*t/l' x +{{.Float 0.5 | .Prepare | indent 4}} + fmul dword [{{.Float 0.5 | .Use}}] ; p=r/2 t*t/l' x + fxch ; t*t/l' p x + fyl2x ; p*log2(t*t/l') x + {{.TailCall "su_power"}} ; 2^(p*log2(t*t/l')) x + ; tail call ; Equal to: + ; (t*t/l')^p x + ; if ratio is at minimum => p=0 => 1 x + ; if ratio is at maximum => p=0.5 => t/x => t/x*x=t +{{- end}} diff --git a/templates/flowcontrol.asm b/templates/flowcontrol.asm new file mode 100644 index 0000000..1dbbfe0 --- /dev/null +++ b/templates/flowcontrol.asm @@ -0,0 +1,23 @@ +{{- if .Opcode "speed" -}} +;------------------------------------------------------------------------------- +; SPEED opcode: modulate the speed (bpm) of the song based on ST0 +;------------------------------------------------------------------------------- +; Mono: adds or subtracts the ticks, a value of 0.5 is neutral & will7 +; result in no speed change. +; There is no STEREO version. +;------------------------------------------------------------------------------- +{{.Func "su_op_speed" "Opcode"}} +{{- .Float 2.206896551724138 | .Prepare | indent 4}} + fmul dword [{{.Float 2.206896551724138 | .Use}}] ; (2*s-1)*64/24, let's call this p from now on + {{.Call "su_power"}} + fld1 ; 1 2^p + fsubp st1, st0 ; 2^p-1, the player is advancing 1 tick by its own + fadd dword [{{.WRK}}] ; t+2^p-1, t is the remainder from previous rounds as ticks have to be rounded to 1 + push {{.AX}} + fist dword [{{.SP}}] ; Main stack: k=int(t+2^p-1) + fisub dword [{{.SP}}] ; t+2^p-1-k, the remainder + pop {{.AX}} + add dword [{{.Stack "Sample"}}], eax ; add the whole ticks to row tick count + fstp dword [{{.WRK}}] ; save the remainder for future + ret +{{end}} diff --git a/templates/gmdls.asm b/templates/gmdls.asm new file mode 100644 index 0000000..2d831a2 --- /dev/null +++ b/templates/gmdls.asm @@ -0,0 +1,58 @@ +{{- if eq .OS "windows"}} +{{.ExportFunc "su_load_gmdls"}} +{{- if .Amd64}} + extern OpenFile ; requires windows + extern ReadFile ; requires windows + ; Win64 ABI: RCX, RDX, R8, and R9 + sub rsp, 40 ; Win64 ABI requires "shadow space" + space for one parameter. + mov rdx, qword su_sample_table + mov rcx, qword su_gmdls_path1 + su_gmdls_pathloop: + xor r8,r8 ; OF_READ + push rdx ; &ofstruct, blatantly reuse the sample table + push rcx + call OpenFile ; eax = OpenFile(path,&ofstruct,OF_READ) + pop rcx + add rcx, su_gmdls_path2 - su_gmdls_path1 ; if we ever get to third, then crash + pop rdx + cmp eax, -1 ; ecx == INVALID? + je su_gmdls_pathloop + movsxd rcx, eax + mov qword [rsp+32], 0 + mov r9, rdx + mov r8d, 3440660 ; number of bytes to read + call ReadFile ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL) + add rsp, 40 ; shadow space, as required by Win64 ABI + ret +{{- else}} + mov edx, su_sample_table + mov ecx, su_gmdls_path1 + su_gmdls_pathloop: + push 0 ; OF_READ + push edx ; &ofstruct, blatantly reuse the sample table + push ecx ; path + call _OpenFile@12 ; eax = OpenFile(path,&ofstruct,OF_READ) + add ecx, su_gmdls_path2 - su_gmdls_path1 ; if we ever get to third, then crash + cmp eax, -1 ; eax == INVALID? + je su_gmdls_pathloop + push 0 ; NULL + push edx ; &bytes_read, reusing sample table again; it does not matter that the first four bytes are trashed + push 3440660 ; number of bytes to read + push edx ; here we actually pass the sample table to readfile + push eax ; handle to file + call _ReadFile@20 ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL) + ret +extern _OpenFile@12 ; requires windows +extern _ReadFile@20 ; requires windows +{{end}} + +{{.Data "su_gmdls_path1"}} + db 'drivers/gm.dls',0 +su_gmdls_path2: + db 'drivers/etc/gm.dls',0 + +{{.SectBss "susamtable"}} +su_sample_table: + resb 3440660 ; size of gmdls. + +{{end}} diff --git a/templates/library.asm b/templates/library.asm new file mode 100644 index 0000000..8ea2a76 --- /dev/null +++ b/templates/library.asm @@ -0,0 +1,304 @@ +; source file for compiling sointu as a library +%define SU_DISABLE_PLAYER + +%include "sointu/header.inc" + +; use every opcode +USE_ADD +USE_ADDP +USE_POP +USE_LOADNOTE +USE_MUL +USE_MULP +USE_PUSH +USE_XCH +USE_DISTORT +USE_HOLD +USE_CRUSH +USE_GAIN +USE_INVGAIN +USE_FILTER +USE_CLIP +USE_PAN +USE_DELAY +USE_COMPRES +USE_SPEED +USE_OUT +USE_OUTAUX +USE_AUX +USE_SEND +USE_ENVELOPE +USE_NOISE +USE_OSCILLAT +USE_LOAD_VAL +USE_RECEIVE +USE_IN + +; include stereo variant of each opcode +%define INCLUDE_STEREO_ADD +%define INCLUDE_STEREO_ADDP +%define INCLUDE_STEREO_POP +%define INCLUDE_STEREO_LOADNOTE +%define INCLUDE_STEREO_MUL +%define INCLUDE_STEREO_MULP +%define INCLUDE_STEREO_PUSH +%define INCLUDE_STEREO_XCH +%define INCLUDE_STEREO_DISTORT +%define INCLUDE_STEREO_HOLD +%define INCLUDE_STEREO_CRUSH +%define INCLUDE_STEREO_GAIN +%define INCLUDE_STEREO_INVGAIN +%define INCLUDE_STEREO_FILTER +%define INCLUDE_STEREO_CLIP +%define INCLUDE_STEREO_PAN +%define INCLUDE_STEREO_DELAY +%define INCLUDE_STEREO_COMPRES +%define INCLUDE_STEREO_SPEED +%define INCLUDE_STEREO_OUT +%define INCLUDE_STEREO_OUTAUX +%define INCLUDE_STEREO_AUX +%define INCLUDE_STEREO_SEND +%define INCLUDE_STEREO_ENVELOPE +%define INCLUDE_STEREO_NOISE +%define INCLUDE_STEREO_OSCILLAT +%define INCLUDE_STEREO_LOADVAL +%define INCLUDE_STEREO_RECEIVE +%define INCLUDE_STEREO_IN + +; include all features inside all opcodes +%define INCLUDE_TRISAW +%define INCLUDE_SINE +%define INCLUDE_PULSE +%define INCLUDE_GATE +%define INCLUDE_UNISONS +%define INCLUDE_POLYPHONY +%define INCLUDE_MULTIVOICE_TRACKS +%define INCLUDE_DELAY_MODULATION +%define INCLUDE_LOWPASS +%define INCLUDE_BANDPASS +%define INCLUDE_HIGHPASS +%define INCLUDE_NEGBANDPASS +%define INCLUDE_NEGHIGHPASS +%define INCLUDE_GLOBAL_SEND +%define INCLUDE_DELAY_NOTETRACKING +%define INCLUDE_DELAY_FLOAT_TIME + +%ifidn __OUTPUT_FORMAT__,win32 + %define INCLUDE_SAMPLES + %define INCLUDE_GMDLS +%endif +%ifidn __OUTPUT_FORMAT__,win64 + %define INCLUDE_SAMPLES + %define INCLUDE_GMDLS +%endif + +%include "sointu/footer.inc" + +section .text + +struc su_sampleoff + .start resd 1 + .loopstart resw 1 + .looplength resw 1 + .size: +endstruc + +struc su_synth + .synthwrk resb su_synthworkspace.size + .delaywrks resb su_delayline_wrk.size * 64 + .delaytimes resw 768 + .sampleoffs resb su_sampleoff.size * 256 + .randseed resd 1 + .globaltime resd 1 + .commands resb 32 * 64 + .values resb 32 * 64 * 8 + .polyphony resd 1 + .numvoices resd 1 +endstruc + +SECT_TEXT(sursampl) + +EXPORT MANGLE_FUNC(su_render,16) +%if BITS == 32 ; stdcall + pushad ; push registers + mov ecx, [esp + 4 + 32] ; ecx = &synthState + mov edx, [esp + 8 + 32] ; edx = &buffer + mov esi, [esp + 12 + 32] ; esi = &samples + mov ebx, [esp + 16 + 32] ; ebx = &time +%else + %ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rcx = &synth, rdx = &buffer, r8 = &bufsize, r9 = &time + push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile + mov rsi, r8 ; rsi = &samples + mov rbx, r9 ; rbx = &time + %else ; System V ABI: rdi = &synth, rsi = &buffer, rdx = &samples, rcx = &time + push_registers rbx, rbp ; System V ABI: these registers are non-volatile + mov rbx, rcx ; rbx points to time + xchg rsi, rdx ; rdx points to buffer, rsi points to samples + mov rcx, rdi ; rcx = &Synthstate + %endif +%endif + sub _SP,108 ; allocate space on stack for the FPU state + fsave [_SP] ; save the FPU state to stack & reset the FPU + push _SI ; push the pointer to samples + push _BX ; push the pointer to time + xor eax, eax ; samplenumber starts at 0 + push _AX ; push samplenumber to stack + mov esi, [_SI] ; zero extend dereferenced pointer + push _SI ; push bufsize + push _DX ; push bufptr + push _CX ; this takes place of the voicetrack + lea _AX, [_CX + su_synth.sampleoffs] + push _AX + lea _AX, [_CX + su_synth.delaytimes] + push _AX + mov eax, [_CX + su_synth.randseed] + push _AX ; randseed + mov eax, [_CX + su_synth.globaltime] + push _AX ; global tick time + mov ebx, dword [_BX] ; zero extend dereferenced pointer + push _BX ; the nominal rowlength should be time_in + xor eax, eax ; rowtick starts at 0 +su_render_samples_loop: + push _DI + fnstsw [_SP] ; store the FPU status flag to stack top + pop _DI ; _DI = FPU status flag + and _DI, 0011100001000101b ; mask TOP pointer, stack error, zero divide and invalid operation + test _DI,_DI ; all the aforementioned bits should be 0! + jne su_render_samples_time_finish ; otherwise, we exit due to error + cmp eax, [_SP] ; if rowtick >= maxtime + jge su_render_samples_time_finish ; goto finish + mov ecx, [_SP + PTRSIZE*7] ; ecx = buffer length in samples + cmp [_SP + PTRSIZE*8], ecx ; if samples >= maxsamples + jge su_render_samples_time_finish ; goto finish + inc eax ; time++ + inc dword [_SP + PTRSIZE*8] ; samples++ + mov _CX, [_SP + PTRSIZE*5] + push _AX ; push rowtick + mov eax, [_CX + su_synth.polyphony] + push _AX ;polyphony + mov eax, [_CX + su_synth.numvoices] + push _AX ;numvoices + lea _DX, [_CX+ su_synth.synthwrk] + lea COM, [_CX+ su_synth.commands] + lea VAL, [_CX+ su_synth.values] + lea WRK, [_DX + su_synthworkspace.voices] + lea _CX, [_CX+ su_synth.delaywrks - su_delayline_wrk.filtstate] + call MANGLE_FUNC(su_run_vm,0) + pop _AX + pop _AX + mov _DI, [_SP + PTRSIZE*7] ; edi containts buffer ptr + mov _CX, [_SP + PTRSIZE*6] + lea _SI, [_CX + su_synth.synthwrk + su_synthworkspace.left] + movsd ; copy left channel to output buffer + movsd ; copy right channel to output buffer + mov [_SP + PTRSIZE*7], _DI ; save back the updated ptr + lea _DI, [_SI-8] + xor eax, eax + stosd ; clear left channel so the VM is ready to write them again + stosd ; clear right channel so the VM is ready to write them again + pop _AX + inc dword [_SP + PTRSIZE] ; increment global time, used by delays + jmp su_render_samples_loop +su_render_samples_time_finish: + pop _CX + pop _BX + pop _DX + pop _CX ; discard delaytimes ptr + pop _CX ; discard samplesoffs ptr + pop _CX + mov [_CX + su_synth.randseed], edx + mov [_CX + su_synth.globaltime], ebx + pop _BX + pop _BX + pop _DX + pop _BX ; pop the pointer to time + pop _SI ; pop the pointer to samples + mov dword [_SI], edx ; *samples = samples rendered + mov dword [_BX], eax ; *time = time ticks rendered + mov _AX,_DI ; _DI was the masked FPU status flag, _AX is return value + frstor [_SP] ; restore fpu state + add _SP,108 ; rewind the stack allocate for FPU state +%if BITS == 32 ; stdcall + mov [_SP + 28],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop + popad + ret 16 +%else + %ifidn __OUTPUT_FORMAT__,win64 + pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile + %else + pop_registers rbx, rbp ; System V ABI: these registers are non-volatile + %endif + ret +%endif + +SECT_DATA(opcodeid) + +; Arithmetic opcode ids +EXPORT MANGLE_DATA(su_add_id) + dd ADD_ID +EXPORT MANGLE_DATA(su_addp_id) + dd ADDP_ID +EXPORT MANGLE_DATA(su_pop_id) + dd POP_ID +EXPORT MANGLE_DATA(su_loadnote_id) + dd LOADNOTE_ID +EXPORT MANGLE_DATA(su_mul_id) + dd MUL_ID +EXPORT MANGLE_DATA(su_mulp_id) + dd MULP_ID +EXPORT MANGLE_DATA(su_push_id) + dd PUSH_ID +EXPORT MANGLE_DATA(su_xch_id) + dd XCH_ID + +; Effect opcode ids +EXPORT MANGLE_DATA(su_distort_id) + dd DISTORT_ID +EXPORT MANGLE_DATA(su_hold_id) + dd HOLD_ID +EXPORT MANGLE_DATA(su_crush_id) + dd CRUSH_ID +EXPORT MANGLE_DATA(su_gain_id) + dd GAIN_ID +EXPORT MANGLE_DATA(su_invgain_id) + dd INVGAIN_ID +EXPORT MANGLE_DATA(su_filter_id) + dd FILTER_ID +EXPORT MANGLE_DATA(su_clip_id) + dd CLIP_ID +EXPORT MANGLE_DATA(su_pan_id) + dd PAN_ID +EXPORT MANGLE_DATA(su_delay_id) + dd DELAY_ID +EXPORT MANGLE_DATA(su_compres_id) + dd COMPRES_ID + +; Flowcontrol opcode ids +EXPORT MANGLE_DATA(su_advance_id) + dd SU_ADVANCE_ID +EXPORT MANGLE_DATA(su_speed_id) + dd SPEED_ID + +; Sink opcode ids +EXPORT MANGLE_DATA(su_out_id) + dd OUT_ID +EXPORT MANGLE_DATA(su_outaux_id) + dd OUTAUX_ID +EXPORT MANGLE_DATA(su_aux_id) + dd AUX_ID +EXPORT MANGLE_DATA(su_send_id) + dd SEND_ID + +; Source opcode ids +EXPORT MANGLE_DATA(su_envelope_id) + dd ENVELOPE_ID +EXPORT MANGLE_DATA(su_noise_id) + dd NOISE_ID +EXPORT MANGLE_DATA(su_oscillat_id) + dd OSCILLAT_ID +EXPORT MANGLE_DATA(su_loadval_id) + dd LOADVAL_ID +EXPORT MANGLE_DATA(su_receive_id) + dd RECEIVE_ID +EXPORT MANGLE_DATA(su_in_id) + dd IN_ID diff --git a/templates/output_sound.asm b/templates/output_sound.asm new file mode 100644 index 0000000..4306c0f --- /dev/null +++ b/templates/output_sound.asm @@ -0,0 +1,44 @@ +{{- if not .Output16Bit }} + {{- if not .Clip }} + mov {{.DI}}, [{{.Stack "OutputBufPtr"}}] ; edi containts ptr + mov {{.SI}}, {{.PTRWORD}} su_synth_obj + su_synthworkspace.left + movsd ; copy left channel to output buffer + movsd ; copy right channel to output buffer + mov [{{.Stack "OutputBufPtr"}}], {{.DI}} ; save back the updated ptr + lea {{.DI}}, [{{.SI}}-8] + xor eax, eax + stosd ; clear left channel so the VM is ready to write them again + stosd ; clear right channel so the VM is ready to write them again + {{ else }} + mov {{.SI}}, qword [{{.Stack "OutputBufPtr"}}] ; esi points to the output buffer + xor ecx,ecx + xor eax,eax + %%loop: ; loop over two channels, left & right + do fld dword [,su_synth_obj+su_synthworkspace.left,_CX*4,] + {{.Call "su_clip"}} + fstp dword [_SI] + do mov dword [,su_synth_obj+su_synthworkspace.left,_CX*4,{],eax} ; clear the sample so the VM is ready to write it + add _SI,4 + cmp ecx,2 + jl %%loop + mov dword [_SP+su_stack.bufferptr - su_stack.output_sound], _SI ; save esi back to stack + {{ end }} +{{- else}} + mov {{.SI}}, [{{.Stack "OutputBufPtr"}}] ; esi points to the output buffer + mov {{.DI}}, {{.PTRWORD}} su_synth_obj+su_synthworkspace.left + mov ecx, 2 + output_sound16bit_loop: ; loop over two channels, left & right + fld dword [{{.DI}}] + {{.Call "su_clip"}} + {{- .Float 32767.0 | .Prepare | indent 16}} + fmul dword [{{.Float 32767.0 | .Use}}] + push {{.AX}} + fistp dword [{{.SP}}] + pop {{.AX}} + mov word [{{.SI}}],ax ; // store integer converted right sample + xor eax,eax + stosd + add {{.SI}},2 + loop output_sound16bit_loop + mov [{{.Stack "OutputBufPtr"}}], {{.SI}} ; save esi back to stack +{{- end }} \ No newline at end of file diff --git a/templates/patch.asm b/templates/patch.asm new file mode 100644 index 0000000..aff51c8 --- /dev/null +++ b/templates/patch.asm @@ -0,0 +1,190 @@ +;------------------------------------------------------------------------------- +; su_run_vm function: runs the entire virtual machine once, creating 1 sample +;------------------------------------------------------------------------------- +; Input: su_synth_obj.left : Set to 0 before calling +; su_synth_obj.right : Set to 0 before calling +; _CX : Pointer to delay workspace (if needed) +; _DX : Pointer to synth object +; COM : Pointer to command stream +; VAL : Pointer to value stream +; WRK : Pointer to the last workspace processed +; Output: su_synth_obj.left : left sample +; su_synth_obj.right : right sample +; Dirty: everything +;------------------------------------------------------------------------------- +{{.Func "su_run_vm"}} + {{- .PushRegs .CX "DelayWorkSpace" .DX "Synth" .COM "CommandStream" .WRK "Voice" .VAL "ValueStream" | indent 4}} +su_run_vm_loop: ; loop until all voices done + movzx edi, byte [{{.COM}}] ; edi = command byte + inc {{.COM}} ; move to next instruction + add {{.WRK}}, su_unit.size ; move WRK to next unit + shr edi, 1 ; shift out the LSB bit = stereo bit + je su_run_vm_advance ; the opcode is zero, jump to advance + mov {{.INP}}, [{{.Stack "Voice"}}] ; reset INP to point to the inputs part of voice + add {{.INP}}, su_voice.inputs + xor ecx, ecx ; counter = 0 + xor eax, eax ; clear out high bits of eax, as lodsb only sets al +su_transform_values_loop: + {{- .Prepare "su_vm_transformcounts" | indent 4}} + cmp cl, byte [{{.Use "su_vm_transformcounts"}}+{{.DI}}] ; compare the counter to the value in the param count table + je su_transform_values_out + lodsb ; load the byte value from VAL stream + push {{.AX}} ; push it to memory so FPU can read it + fild dword [{{.SP}}] ; load the value to FPU stack + {{- .Prepare (.Float 0.0078125) | indent 4}} + fmul dword [{{.Use (.Float 0.0078125)}}] ; divide it by 128 (0 => 0, 128 => 1.0) + fadd dword [{{.WRK}}+su_unit.ports+{{.CX}}*4] ; add the modulations in the current workspace + fstp dword [{{.INP}}+{{.CX}}*4] ; store the modulated value in the inputs section of voice + xor eax, eax + mov dword [{{.WRK}}+su_unit.ports+{{.CX}}*4], eax ; clear out the modulation ports + pop {{.AX}} + inc ecx + jmp su_transform_values_loop +su_transform_values_out: + bt dword [{{.COM}}-1],0 ; LSB of COM = stereo bit => carry + {{- .SaveStack "Opcode"}} + {{- .Prepare "su_vm_jumptable" | indent 4}} + call [{{.Use "su_vm_jumptable"}}+{{.DI}}*{{.PTRSIZE}}] ; call the function corresponding to the instruction + jmp su_run_vm_loop +su_run_vm_advance: + {{- if .Polyphony}} + mov {{.WRK}}, [{{.Stack "Voice"}}] ; WRK points to start of current voice + add {{.WRK}}, su_voice.size ; move to next voice + mov [{{.Stack "Voice"}}], {{.WRK}} ; update the pointer in the stack to point to the new voice + mov ecx, [{{.Stack "VoicesRemain"}}] ; ecx = how many voices remain to process + dec ecx ; decrement number of voices to process + bt dword [{{.Stack "PolyphonyBitmask"}}], ecx ; if voice bit of su_polyphonism not set + jnc su_op_advance_next_instrument ; goto next_instrument + mov {{.VAL}}, [{{.Stack "ValueStream"}}] ; if it was set, then repeat the opcodes for the current voice + mov {{.COM}}, [{{.Stack "CommandStream"}}] +su_op_advance_next_instrument: + mov [{{.Stack "ValueStream"}}], {{.VAL}} ; save current VAL as a checkpoint + mov [{{.Stack "CommandStream"}}], {{.COM}} ; save current COM as a checkpoint +su_op_advance_finish: + mov [{{.Stack "VoicesRemain"}}], ecx + jne su_run_vm_loop ; ZF was set by dec ecx + {{- else}} + mov {{.WRK}}, {{.PTRWORD}} [{{.Stack "Voice"}}] ; load pointer to voice to register + add {{.WRK}}, su_voice.size ; shift it to point to following voice + mov {{.PTRWORD}} [{{.Stack "Voice"}}], {{.WRK}} ; save back to stack + dec dword [{{.Stack "VoicesRemain"}}] ; voices-- + jne su_run_vm_loop ; if there's more voices to process, goto vm_loop + {{- end}} + {{- .PopRegs .CX .DX .COM .WRK .VAL | indent 4}} + ret + +{{- template "arithmetic.asm" .}} +{{- template "effects.asm" .}} +{{- template "flowcontrol.asm" .}} +{{- template "sinks.asm" .}} +{{- template "sources.asm" .}} +{{- template "gmdls.asm" .}} + +{{- if .HasCall "su_nonlinear_map"}} +;------------------------------------------------------------------------------- +; su_nonlinear_map function: returns 2^(-24*x) of parameter number _AX +;------------------------------------------------------------------------------- +; Input: _AX : parameter number (e.g. for envelope: 0 = attac, 1 = decay...) +; INP : pointer to transformed values +; Output: st0 : 2^(-24*x), where x is the parameter in the range 0-1 +;------------------------------------------------------------------------------- +{{.Func "su_nonlinear_map"}} + fld dword [{{.INP}}+{{.AX}}*4] ; x, where x is the parameter in the range 0-1 + {{.Prepare (.Int 24)}} + fimul dword [{{.Use (.Int 24)}}] ; 24*x + fchs ; -24*x + +{{end}} + +{{- if or (.HasCall "su_power") (.HasCall "su_nonlinear_map")}} +;------------------------------------------------------------------------------- +; su_power function: computes 2^x +;------------------------------------------------------------------------------- +; Input: st0 : x +; Output: st0 : 2^x +;------------------------------------------------------------------------------- +{{- if not (.HasCall "su_nonlinear_map")}}{{.SectText "su_power"}}{{end}} +su_power: + fld1 ; 1 x + fld st1 ; x 1 x + fprem ; mod(x,1) 1 x + f2xm1 ; 2^mod(x,1)-1 1 x + faddp st1,st0 ; 2^mod(x,1) x + fscale ; 2^mod(x,1)*2^trunc(x) x + ; Equal to: + ; 2^x x + fstp st1 ; 2^x + ret + +{{end}} + +{{- if .HasCall "su_effects_stereohelper" }} +;------------------------------------------------------------------------------- +; su_effects_stereohelper: moves the workspace to next, does the filtering for +; right channel (pulling the calling address from stack), rewinds the +; workspace and returns +;------------------------------------------------------------------------------- +{{.Func "su_effects_stereohelper"}} + jnc su_effects_stereohelper_mono ; carry is still the stereo bit + add {{.WRK}}, 16 + fxch ; r l + call [{{.SP}}] ; call whoever called me... + fxch ; l r + sub {{.WRK}}, 16 ; move WRK back to where it was +su_effects_stereohelper_mono: + ret ; return to process l/mono sound + +{{end}} + +{{- if .HasCall "su_waveshaper" }} +{{.Func "su_waveshaper"}} + fxch ; x a + {{.Call "su_clip"}} + fxch ; a x' (from now on just called x) + fld st0 ; a a x + {{.Prepare (.Float 0.5)}} + fsub dword [{{.Use (.Float 0.5)}}] ; a-.5 a x + fadd st0 ; 2*a-1 a x + fld st2 ; x 2*a-1 a x + fabs ; abs(x) 2*a-1 a x + fmulp st1 ; (2*a-1)*abs(x) a x + fld1 ; 1 (2*a-1)*abs(x) a x + faddp st1 ; 1+(2*a-1)*abs(x) a x + fsub st1 ; 1-a+(2*a-1)*abs(x) a x + fdivp st1, st0 ; a/(1-a+(2*a-1)*abs(x)) x + fmulp st1 ; x*a/(1-a+(2*a-1)*abs(x)) + ret +{{end}} + +{{- if .HasCall "su_clip"}} +{{.Func "su_clip"}} + fld1 ; 1 x a + fucomi st1 ; if (1 <= x) + jbe short su_clip_do ; goto Clip_Do + fchs ; -1 x a + fucomi st1 ; if (-1 < x) + fcmovb st0, st1 ; x x a +su_clip_do: + fstp st1 ; x' a, where x' = clamp(x) + ret +{{end}} + +;------------------------------------------------------------------------------- +; The opcode table jump table. This is constructed to only include the opcodes +; that are used so that the jump table is as small as possible. +;------------------------------------------------------------------------------- +{{.Data "su_vm_jumptable_offset"}} +su_vm_jumptable equ $ - {{.PTRSIZE}} ; Advance is not in the opcode table +{{- $x := .}} +{{- range .Opcodes}} + {{$x.DPTR}} su_op_{{.Type}} +{{- end}} + +;------------------------------------------------------------------------------- +; The number of transformed parameters each opcode takes +;------------------------------------------------------------------------------- +{{.Data "su_vm_transformcounts_offset"}} +su_vm_transformcounts equ $ - 1 ; Advance is not in the opcode table +{{- range .Opcodes}} + db {{.NumParams}} +{{- end}} diff --git a/templates/player.asm b/templates/player.asm new file mode 100644 index 0000000..e85a1d2 --- /dev/null +++ b/templates/player.asm @@ -0,0 +1,237 @@ +{{template "structs.asm" .}} +;------------------------------------------------------------------------------- +; Uninitialized data: The synth object +;------------------------------------------------------------------------------- +{{.SectBss "synth_object"}} +su_synth_obj: + resb su_synthworkspace.size + resb {{.NumDelayLines}}*su_delayline_wrk.size + +;------------------------------------------------------------------------------- +; su_render_song function: the entry point for the synth +;------------------------------------------------------------------------------- +; Has the signature su_render_song(void *ptr), where ptr is a pointer to +; the output buffer. Renders the compile time hard-coded song to the buffer. +; Stack: output_ptr +;------------------------------------------------------------------------------- +{{.ExportFunc "su_render_song" "OutputBufPtr"}} + {{- if .Amd64}} + {{- if eq .OS "windows"}} + {{- .PushRegs "rcx" "OutputBufPtr" "rdi" "NonVolatileRsi" "rsi" "NonVolatile" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}} ; rcx = ptr to buf. rdi,rsi,rbx,rbp nonvolatile + {{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar + {{- .PushRegs "rdi" "OutputBufPtr" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}} + {{- end}} + {{- else}} + {{- .PushRegs | indent 4}} + {{- end}} + {{- $prologsize := len .Stacklocs}} + xor eax, eax + {{- if .MultivoiceTracks}} + {{.Push (.VoiceTrackBitmask | printf "%v") "VoiceTrackBitmask"}} + {{- end}} + {{.Push "1" "RandSeed"}} + {{.Push .AX "GlobalTick"}} +su_render_rowloop: ; loop through every row in the song + {{.Push .AX "Row"}} + {{.Call "su_update_voices"}} ; update instruments for the new row + xor eax, eax ; ecx is the current sample within row +su_render_sampleloop: ; loop through every sample in the row + {{.Push .AX "Sample"}} + {{- if .Polyphony}} + {{.Push (.PolyphonyBitmask | printf "%v") "PolyphonyBitmask"}} ; does the next voice reuse the current opcodes? + {{- end}} + {{.Push (.Song.Patch.TotalVoices | printf "%v") "VoicesRemain"}} + mov {{.DX}}, {{.PTRWORD}} su_synth_obj ; {{.DX}} points to the synth object + mov {{.COM}}, {{.PTRWORD}} su_patch_code ; COM points to vm code + mov {{.VAL}}, {{.PTRWORD}} su_patch_parameters ; VAL points to unit params + {{- if .Opcode "delay"}} + lea {{.CX}}, [{{.DX}} + su_synthworkspace.size - su_delayline_wrk.filtstate] + {{- end}} + lea {{.WRK}}, [{{.DX}} + su_synthworkspace.voices] ; WRK points to the first voice + {{.Call "su_run_vm"}} ; run through the VM code + {{.Pop .AX}} + {{- if .Polyphony}} + {{.Pop .AX}} + {{- end}} + {{- template "output_sound.asm" .}} ; *ptr++ = left, *ptr++ = right + {{.Pop .AX}} + inc dword [{{.Stack "GlobalTick"}}] ; increment global time, used by delays + inc eax + cmp eax, {{.Song.SamplesPerRow}} + jl su_render_sampleloop + {{.Pop .AX}} ; Stack: pushad ptr + inc eax + cmp eax, {{.Song.TotalRows}} + jl su_render_rowloop + ; rewind the stack the entropy of multiple pop {{.AX}} is probably lower than add + {{- $x := .}} + {{- range (.Sub (len .Stacklocs) $prologsize | .Count)}} + {{$x.Pop $x.AX}} + {{- end}} + {{- if .Amd64}} + {{- if eq .OS "windows"}} + ; Windows64 ABI, rdi rsi rbx rbp non-volatile + {{- .PopRegs "rcx" "rdi" "rsi" "rbx" "rbp" | indent 4}} + {{- else}} + ; SystemV64 ABI (linux mac or hopefully something similar), rbx rbp non-volatile + {{- .PopRegs "rdi" "rbx" "rbp" | indent 4}} + {{- end}} + ret + {{- else}} + {{- .PopRegs | indent 4}} + ret 4 + {{- end}} + +;------------------------------------------------------------------------------- +; su_update_voices function: polyphonic & chord implementation +;------------------------------------------------------------------------------- +; Input: eax : current row within song +; Dirty: pretty much everything +;------------------------------------------------------------------------------- +{{.Func "su_update_voices"}} +{{- if .MultivoiceTracks}} +; The more complicated implementation: one track can trigger multiple voices + xor edx, edx + mov ebx, {{.Song.PatternRows}} ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256... + div ebx ; eax = current pattern, edx = current row in pattern + {{.Prepare "su_tracks"}} + lea {{.SI}}, [{{.Use "su_tracks"}}+{{.AX}}] ; esi points to the pattern data for current track + xor eax, eax ; eax is the first voice of next track + xor ebx, ebx ; ebx is the first voice of current track + mov {{.BP}}, {{.PTRWORD}} su_synth_obj ; ebp points to the current_voiceno array +su_update_voices_trackloop: + movzx eax, byte [{{.SI}}] ; eax = current pattern + imul eax, {{.Song.PatternRows}} ; eax = offset to current pattern data +{{- .Prepare "su_patterns" .AX | indent 4}} + movzx eax,byte [{{.Use "su_patterns" .AX}},{{.DX}}] ; eax = note + push {{.DX}} ; Stack: ptrnrow + xor edx, edx ; edx=0 + mov ecx, ebx ; ecx=first voice of the track to be done +su_calculate_voices_loop: ; do { + bt dword [{{.Stack "VoiceTrackBitmask"}}],ecx ; test voicetrack_bitmask// notice that the incs don't set carry + inc edx ; edx++ // edx=numvoices + inc ecx ; ecx++ // ecx=the first voice of next track + jc su_calculate_voices_loop ; } while bit ecx-1 of bitmask is on + push {{.CX}} ; Stack: next_instr ptrnrow + cmp al, {{.Song.Hold}} ; anything but hold causes action + je short su_update_voices_nexttrack + mov cl, byte [{{.BP}}] + mov edi, ecx + add edi, ebx + shl edi, 12 ; each unit = 64 bytes and there are 1<= num_voices) + jl su_update_voices_skipreset + xor ecx,ecx ; curvoice = 0 +su_update_voices_skipreset: + mov byte [{{.BP}}],cl + add ecx, ebx + shl ecx, 12 ; each unit = 64 bytes and there are 1<<6 units + small header + lea {{.DI}},[{{.Use "su_synth_obj"}} + su_synthworkspace.voices + {{.CX}}] + stosd ; save note + mov ecx, (su_voice.size - su_voice.release)/4 + xor eax, eax + rep stosd ; clear the workspace of the new voice, retriggering oscillators +su_update_voices_nexttrack: + pop {{.BX}} ; ebx=first voice of next instrument, Stack: ptrnrow + pop {{.DX}} ; edx=patrnrow + add {{.SI}}, {{.Song.SequenceLength}} + inc {{.BP}} +{{- $addrname := len .Song.Tracks | printf "su_synth_obj + %v"}} +{{- .Prepare $addrname | indent 8}} + cmp {{.BP}},{{.Use $addrname}} + jl su_update_voices_trackloop + ret +{{- else}} +; The simple implementation: each track triggers always the same voice + xor edx, edx + xor ebx, ebx + mov bl, {{.Song.PatternRows}} ; rows per pattern + div ebx ; eax = current pattern, edx = current row in pattern +{{- .Prepare "su_tracks" | indent 4}} + lea {{.SI}}, [{{.Use "su_tracks"}}+{{.AX}}]; esi points to the pattern data for current track + mov {{.DI}}, {{.PTRWORD}} su_synth_obj+su_synthworkspace.voices + mov bl, {{len .Song.Tracks}} ; MAX_TRACKS is always <= 32 so this is ok +su_update_voices_trackloop: + movzx eax, byte [{{.SI}}] ; eax = current pattern + imul eax, {{.Song.PatternRows}} ; multiply by rows per pattern, eax = offset to current pattern data +{{- .Prepare "su_patterns" .AX | indent 8}} + movzx eax, byte [{{.Use "su_patterns" .AX}} + {{.DX}}] ; ecx = note + cmp al, {{.Song.Hold}} ; anything but hold causes action + je short su_update_voices_nexttrack + inc dword [{{.DI}}+su_voice.release] ; set the voice currently active to release; notice that it could increment any number of times + jb su_update_voices_nexttrack ; if cl < HLD (no new note triggered) goto nexttrack +su_update_voices_retrigger: + stosd ; save note + mov ecx, (su_voice.size - su_voice.release)/4 ; could be xor ecx, ecx; mov ch,...>>8, but will it actually be smaller after compression? + xor eax, eax + rep stosd ; clear the workspace of the new voice, retriggering oscillators + jmp short su_update_voices_skipadd +su_update_voices_nexttrack: + add {{.DI}}, su_voice.size +su_update_voices_skipadd: + add {{.SI}}, {{.Song.SequenceLength}} + dec ebx + jnz short su_update_voices_trackloop + ret +{{- end}} + +{{template "patch.asm" .}} + +;------------------------------------------------------------------------------- +; Patterns +;------------------------------------------------------------------------------- +{{.Data "su_patterns"}} +{{- range .Song.Patterns}} + db {{. | toStrings | join ","}} +{{- end}} + +;------------------------------------------------------------------------------- +; Tracks +;------------------------------------------------------------------------------- +{{.Data "su_tracks"}} +{{- range .Song.Tracks}} + db {{.Sequence | toStrings | join ","}} +{{- end}} + +{{- if gt (.Song.Patch.SampleOffsets | len) 0}} +;------------------------------------------------------------------------------- +; Sample offsets +;------------------------------------------------------------------------------- +{{.Data "su_sample_offsets"}} +{{- range .Song.Patch.SampleOffsets}} + dd {{.Start}} + dw {{.LoopStart}} + dw {{.LoopLength}} +{{- end}} +{{end}} + +{{- if gt (.Song.Patch.DelayTimes | len ) 0}} +;------------------------------------------------------------------------------- +; Delay times +;------------------------------------------------------------------------------- +{{.Data "su_delay_times"}} + dw {{.Song.Patch.DelayTimes | toStrings | join ","}} +{{end}} + +;------------------------------------------------------------------------------- +; The code for this patch, basically indices to vm jump table +;------------------------------------------------------------------------------- +{{.Data "su_patch_code"}} + db {{.Code | toStrings | join ","}} + +;------------------------------------------------------------------------------- +; The parameters / inputs to each opcode +;------------------------------------------------------------------------------- +{{.Data "su_patch_parameters"}} + db {{.Values | toStrings | join ","}} + +;------------------------------------------------------------------------------- +; Constants +;------------------------------------------------------------------------------- +{{.SectData "constants"}} +{{.Constants}} diff --git a/templates/sinks.asm b/templates/sinks.asm new file mode 100644 index 0000000..c83dbd3 --- /dev/null +++ b/templates/sinks.asm @@ -0,0 +1,126 @@ +{{- if .Opcode "out"}} +;------------------------------------------------------------------------------- +; OUT opcode: outputs and pops the signal +;------------------------------------------------------------------------------- +{{- if .Mono "out"}} +; Mono: add ST0 to main left port, then pop +{{- end}} +{{- if .Stereo "out"}} +; Stereo: add ST0 to left out and ST1 to right out, then pop +{{- end}} +;------------------------------------------------------------------------------- +{{.Func "su_op_out" "Opcode"}} ; l r + mov {{.AX}}, [{{.Stack "Synth"}}] ; AX points to the synth object +{{- if .StereoAndMono "out" }} + jnc su_op_out_mono +{{- end }} +{{- if .Stereo "out" }} + call su_op_out_mono + add {{.AX}}, 4 ; shift from left to right channel +su_op_out_mono: +{{- end}} + fmul dword [{{.Input "out" "gain"}}] ; multiply by gain + fadd dword [{{.AX}} + su_synthworkspace.left] ; add current value of the output + fstp dword [{{.AX}} + su_synthworkspace.left] ; store the new value of the output + ret +{{end}} + + +{{- if .Opcode "outaux"}} +;------------------------------------------------------------------------------- +; OUTAUX opcode: outputs to main and aux1 outputs and pops the signal +;------------------------------------------------------------------------------- +; Mono: add outgain*ST0 to main left port and auxgain*ST0 to aux1 left +; Stereo: also add outgain*ST1 to main right port and auxgain*ST1 to aux1 right +;------------------------------------------------------------------------------- +{{.Func "su_op_outaux" "Opcode"}} ; l r + mov {{.AX}}, [{{.Stack "Synth"}}] +{{- if .StereoAndMono "outaux" }} + jnc su_op_outaux_mono +{{- end}} +{{- if .Stereo "outaux" }} + call su_op_outaux_mono + add {{.AX}}, 4 +su_op_outaux_mono: +{{- end}} + fld st0 ; l l + fmul dword [{{.Input "outaux" "outgain"}}] ; g*l + fadd dword [{{.AX}} + su_synthworkspace.left] ; g*l+o + fstp dword [{{.AX}} + su_synthworkspace.left] ; o'=g*l+o + fmul dword [{{.Input "outaux" "auxgain"}}] ; h*l + fadd dword [{{.AX}} + su_synthworkspace.aux] ; h*l+a + fstp dword [{{.AX}} + su_synthworkspace.aux] ; a'=h*l+a + ret +{{end}} + + +{{- if .Opcode "aux"}} +;------------------------------------------------------------------------------- +; AUX opcode: outputs the signal to aux (or main) port and pops the signal +;------------------------------------------------------------------------------- +; Mono: add gain*ST0 to left port +; Stereo: also add gain*ST1 to right port +;------------------------------------------------------------------------------- +{{.Func "su_op_aux" "Opcode"}} ; l r + lodsb + mov {{.DI}}, [{{.Stack "Synth"}}] +{{- if .StereoAndMono "aux" }} + jnc su_op_aux_mono +{{- end}} +{{- if .Stereo "aux" }} + call su_op_aux_mono + add {{.DI}}, 4 +su_op_aux_mono: +{{- end}} + fmul dword [{{.Input "aux" "gain"}}] ; g*l + fadd dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4] ; g*l+o + fstp dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4] ; o'=g*l+o + ret +{{end}} + + +{{- if .Opcode "send"}} +;------------------------------------------------------------------------------- +; SEND opcode: adds the signal to a port +;------------------------------------------------------------------------------- +; Mono: adds signal to a memory address, defined by a word in VAL stream +; Stereo: also add right signal to the following address +;------------------------------------------------------------------------------- +{{.Func "su_op_send" "Opcode"}} + lodsw + mov {{.CX}}, [{{.Stack "Voice"}}] ; load pointer to voice +{{- if .StereoAndMono "send"}} + jnc su_op_send_mono +{{- end}} +{{- if .Stereo "send"}} + mov {{.DI}}, {{.AX}} + inc {{.AX}} ; send the right channel first + fxch ; r l + call su_op_send_mono ; (r) l + mov {{.AX}}, {{.DI}} ; move back to original address + test {{.AX}}, 0x8 ; if r was not popped and is still in the stack + jnz su_op_send_mono + fxch ; swap them back: l r +su_op_send_mono: +{{- end}} +{{- if .HasParamValueOtherThan "send" "voice" 0}} + test {{.AX}}, 0x8000 + jz su_op_send_skipglobal + mov {{.CX}}, [{{.Stack "Synth"}}] +su_op_send_skipglobal: +{{- end}} + test {{.AX}}, 0x8 ; if the SEND_POP bit is not set + jnz su_op_send_skippush + fld st0 ; duplicate the signal on stack: s s +su_op_send_skippush: ; there is signal s, but maybe also another: s (s) + fld dword [{{.Input "send" "amount"}}] ; a l (l) +{{- .Float 0.5 | .Prepare | indent 4}} + fsub dword [{{.Float 0.5 | .Use}}] ; a-.5 l (l) + fadd st0 ; g=2*a-1 l (l) + and ah, 0x7f ; eax = send address, clear the global bit + or al, 0x8 ; set the POP bit always, at the same time shifting to ports instead of wrk + fmulp st1, st0 ; g*l (l) + fadd dword [{{.CX}} + {{.AX}}*4] ; g*l+L (l),where L is the current value + fstp dword [{{.CX}} + {{.AX}}*4] ; (l) + ret +{{end}} diff --git a/templates/sources.asm b/templates/sources.asm new file mode 100644 index 0000000..ba241b4 --- /dev/null +++ b/templates/sources.asm @@ -0,0 +1,415 @@ +{{if .Opcode "envelope" -}} +;------------------------------------------------------------------------------- +; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1] +;------------------------------------------------------------------------------- +; Mono: push the envelope value on stack +; Stereo: push the envelope valeu on stack twice +;------------------------------------------------------------------------------- +{{.Func "su_op_envelope" "Opcode"}} +{{- if .StereoAndMono "envelope"}} + jnc su_op_envelope_mono +{{- end}} +{{- if .Stereo "envelope"}} + call su_op_envelope_mono + fld st0 + ret +su_op_envelope_mono: +{{- end}} + mov eax, dword [{{.INP}}-su_voice.inputs+su_voice.release] ; eax = su_instrument.release + test eax, eax ; if (eax == 0) + je su_op_envelope_process ; goto process + mov dword [{{.WRK}}], {{.InputNumber "envelope" "release"}} ; [state]=RELEASE +su_op_envelope_process: + mov eax, dword [{{.WRK}}] ; al=[state] + fld dword [{{.WRK}}+4] ; x=[level] + cmp al, {{.InputNumber "envelope" "sustain"}} ; if (al==SUSTAIN) + je short su_op_envelope_leave2 ; goto leave2 +su_op_envelope_attac: + cmp al, {{.InputNumber "envelope" "attack"}} ; if (al!=ATTAC) + jne short su_op_envelope_decay ; goto decay + {{.Call "su_nonlinear_map"}} ; a x, where a=attack + faddp st1, st0 ; a+x + fld1 ; 1 a+x + fucomi st1 ; if (a+x<=1) // is attack complete? + fcmovnb st0, st1 ; a+x a+x + jbe short su_op_envelope_statechange ; else goto statechange +su_op_envelope_decay: + cmp al, {{.InputNumber "envelope" "decay"}} ; if (al!=DECAY) + jne short su_op_envelope_release ; goto release + {{.Call "su_nonlinear_map"}} ; d x, where d=decay + fsubp st1, st0 ; x-d + fld dword [{{.Input "envelope" "sustain"}}] ; s x-d, where s=sustain + fucomi st1 ; if (x-d>s) // is decay complete? + fcmovb st0, st1 ; x-d x-d + jnc short su_op_envelope_statechange ; else goto statechange +su_op_envelope_release: + cmp al, {{.InputNumber "envelope" "release"}} ; if (al!=RELEASE) + jne short su_op_envelope_leave ; goto leave + {{.Call "su_nonlinear_map"}} ; r x, where r=release + fsubp st1, st0 ; x-r + fldz ; 0 x-r + fucomi st1 ; if (x-r>0) // is release complete? + fcmovb st0, st1 ; x-r x-r, then goto leave + jc short su_op_envelope_leave +su_op_envelope_statechange: + inc dword [{{.WRK}}] ; [state]++ +su_op_envelope_leave: + fstp st1 ; x', where x' is the new value + fst dword [{{.WRK}}+4] ; [level]=x' +su_op_envelope_leave2: + fmul dword [{{.Input "envelope" "gain"}}] ; [gain]*x' + ret +{{end}} + + +{{- if .Opcode "noise"}} +;------------------------------------------------------------------------------- +; NOISE opcode: creates noise +;------------------------------------------------------------------------------- +; Mono: push a random value [-1,1] value on stack +; Stereo: push two (differeent) random values on stack +;------------------------------------------------------------------------------- +{{.Func "su_op_noise" "Opcode"}} + lea {{.CX}},[{{.Stack "RandSeed"}}] +{{- if .StereoAndMono "noise"}} + jnc su_op_noise_mono +{{- end}} +{{- if .Stereo "noise"}} + call su_op_noise_mono +su_op_noise_mono: +{{- end}} + imul eax, [{{.CX}}],16007 + mov [{{.CX}}],eax + fild dword [{{.CX}}] +{{- .Prepare (.Int 2147483648)}} + fidiv dword [{{.Use (.Int 2147483648)}}] ; 65536*32768 + fld dword [{{.Input "noise" "shape"}}] + {{.Call "su_waveshaper"}} + fld dword [{{.Input "noise" "gain"}}] + fmulp st1, st0 + ret +{{end}} + + +{{- if .Opcode "oscillator"}} +;------------------------------------------------------------------------------- +; OSCILLAT opcode: oscillator, the heart of the synth +;------------------------------------------------------------------------------- +; Mono: push oscillator value on stack +; Stereo: push l r on stack, where l has opposite detune compared to r +;------------------------------------------------------------------------------- +{{.Func "su_op_oscillator" "Opcode"}} + lodsb ; load the flags +%ifdef RUNTIME_TABLES + %ifdef INCLUDE_SAMPLES + mov {{.DI}}, [{{.SP}} + su_stack.sampleoffs]; we need to put this in a register, as the stereo & unisons screw the stack positions + %endif ; ain't we lucky that {{.DI}} was unused throughout +%endif + fld dword [{{.Input "oscillator" "detune"}}] ; e, where e is the detune [0,1] +{{- .Prepare (.Float 0.5)}} + fsub dword [{{.Use (.Float 0.5)}}] ; e-.5 + fadd st0, st0 ; d=2*e-.5, where d is the detune [-1,1] +{{- if .StereoAndMono "oscillator"}} + jnc su_op_oscillat_mono +{{- end}} +{{- if .Stereo "oscillator"}} + fld st0 ; d d + call su_op_oscillat_mono ; r d + add {{.WRK}}, 4 ; state vars: r1 l1 r2 l2 r3 l3 r4 l4, for the unison osc phases + fxch ; d r + fchs ; -d r, negate the detune for second round +su_op_oscillat_mono: +{{- end}} +{{- if .HasParamValueOtherThan "oscillator" "unison" 0}} + {{.PushRegs .AX "" .WRK "OscWRK" .AX "OscFlags"}} + fldz ; 0 d + fxch ; d a=0, "accumulated signal" +su_op_oscillat_unison_loop: + fst dword [{{.SP}}] ; save the current detune, d. We could keep it in fpu stack but it was getting big. + call su_op_oscillat_single ; s a + faddp st1, st0 ; a+=s + test al, 3 + je su_op_oscillat_unison_out + add {{.WRK}}, 8 + fld dword [{{.Input "oscillator" "phase"}}] ; p s +{{.Int 0x3DAAAAAA | .Prepare}} + fadd dword [{{.Int 0x3DAAAAAA | .Use}}] ; 1/128 p s, add some little phase offset to unison oscillators so they don't start in sync + fstp dword [{{.Input "oscillator" "phase"}}] ; s note that this changes the phase for second, possible stereo run. That's probably ok + fld dword [{{.SP}}] ; d s +{{.Float 0.5 | .Prepare}} + fmul dword [{{.Float 0.5 | .Use}}] ; .5*d s // negate and halve the detune of each oscillator + fchs ; -.5*d s // negate and halve the detune of each oscillator + dec eax + jmp short su_op_oscillat_unison_loop +su_op_oscillat_unison_out: + {{.PopRegs .AX .WRK .AX}} + ret +su_op_oscillat_single: +{{- end}} + fld dword [{{.Input "oscillator" "transpose"}}] +{{- .Float 0.5 | .Prepare}} + fsub dword [{{.Float 0.5 | .Use}}] +{{- .Float 0.0078125 | .Prepare}} + fdiv dword [{{.Float 0.0078125 | .Use}}] + faddp st1 + test al, byte 0x08 + jnz su_op_oscillat_skipnote + fiadd dword [{{.INP}}-su_voice.inputs+su_voice.note] ; // st0 is note, st1 is t+d offset +su_op_oscillat_skipnote: +{{- .Int 0x3DAAAAAA | .Prepare}} + fmul dword [{{.Int 0x3DAAAAAA | .Use}}] + {{.Call "su_power"}} + test al, byte 0x08 + jz short su_op_oscillat_normalize_note +{{- .Float 0.000038 | .Prepare}} + fmul dword [{{.Float 0.000038 | .Use}}] ; // st0 is now frequency for lfo + jmp short su_op_oscillat_normalized +su_op_oscillat_normalize_note: +{{- .Float 0.000092696138 | .Prepare}} + fmul dword [{{.Float 0.000092696138 | .Use}}] ; // st0 is now frequency +su_op_oscillat_normalized: + fadd dword [{{.WRK}}] + fst dword [{{.WRK}}] + fadd dword [{{.Input "oscillator" "phase"}}] +{{- if .HasParamValue "oscillator" "type" .Sample}} + test al, byte 0x80 + jz short su_op_oscillat_not_sample + {{.Call "su_oscillat_sample"}} + jmp su_op_oscillat_shaping ; skip the rest to avoid color phase normalization and colorloading +su_op_oscillat_not_sample: +{{- end}} + fld1 + fadd st1, st0 + fxch + fprem + fstp st1 + fld dword [{{.Input "oscillator" "color"}}] ; // c p + ; every oscillator test included if needed +{{- if .HasParamValue "oscillator" "type" .Sine}} + test al, byte 0x40 + jz short su_op_oscillat_notsine + {{.Call "su_oscillat_sine"}} +su_op_oscillat_notsine: +{{- end}} +{{- if .HasParamValue "oscillator" "type" .Trisaw}} + test al, byte 0x20 + jz short su_op_oscillat_not_trisaw + {{.Call "su_oscillat_trisaw"}} +su_op_oscillat_not_trisaw: +{{- end}} +{{- if .HasParamValue "oscillator" "type" .Pulse}} + test al, byte 0x10 + jz short su_op_oscillat_not_pulse + {{.Call "su_oscillat_pulse"}} +su_op_oscillat_not_pulse: +{{- end}} +{{- if .HasParamValue "oscillator" "type" .Gate}} + test al, byte 0x04 + jz short su_op_oscillat_not_gate + {{.Call "su_oscillat_gate"}} + jmp su_op_oscillat_gain ; skip waveshaping as the shape parameter is reused for gateshigh +su_op_oscillat_not_gate: +{{- end}} +su_op_oscillat_shaping: + ; finally, shape the oscillator and apply gain + fld dword [{{.Input "oscillator" "shape"}}] + {{.Call "su_waveshaper"}} +su_op_oscillat_gain: + fld dword [{{.Input "oscillator" "gain"}}] + fmulp st1, st0 + ret +{{end}} + + +{{- if .HasCall "su_oscillat_pulse"}} +{{.Func "su_oscillat_pulse"}} + fucomi st1 ; // c p + fld1 + jnc short su_oscillat_pulse_up ; // +1 c p + fchs ; // -1 c p +su_oscillat_pulse_up: + fstp st1 ; // +-1 p + fstp st1 ; // +-1 + ret +{{end}} + + +{{- if .HasCall "su_oscillat_trisaw"}} +{{.Func "su_oscillat_trisaw"}} + fucomi st1 ; // c p + jnc short su_oscillat_trisaw_up + fld1 ; // 1 c p + fsubr st2, st0 ; // 1 c 1-p + fsubrp st1, st0 ; // 1-c 1-p +su_oscillat_trisaw_up: + fdivp st1, st0 ; // tp'/tc + fadd st0 ; // 2*'' + fld1 ; // 1 2*'' + fsubp st1, st0 ; // 2*''-1 + ret +{{end}} + + +{{- if .HasCall "su_oscillat_sine"}} +{{.Func "su_oscillat_sine"}} + fucomi st1 ; // c p + jnc short su_oscillat_sine_do + fstp st1 + fsub st0, st0 ; // 0 + ret +su_oscillat_sine_do: + fdivp st1, st0 ; // p/c + fldpi ; // pi p + fadd st0 ; // 2*pi p + fmulp st1, st0 ; // 2*pi*p + fsin ; // sin(2*pi*p) + ret +{{end}} + + +{{- if .HasCall "su_oscillat_gate"}} +{{.Func "su_oscillat_gate"}} + fxch ; p c + fstp st1 ; p +{{- .Float 16.0 | .Prepare | indent 4}} + fmul dword [{{.Float 16.0 | .Use}}] ; 16*p + push {{.AX}} + push {{.AX}} + fistp dword [{{.SP}}] ; s=int(16*p), stack empty + fld1 ; 1 + pop {{.AX}} + and al, 0xf ; ax=int(16*p) & 15, stack: 1 + bt word [{{.VAL}}-4],ax ; if bit ax of the gate word is set + jc go4kVCO_gate_bit ; goto gate_bit + fsub st0, st0 ; stack: 0 +go4kVCO_gate_bit: ; stack: 0/1, let's call it x + fld dword [{{.WRK}}+16] ; g x, g is gatestate, x is the input to this filter 0/1 + fsub st1 ; g-x x +{{- .Float 0.99609375 | .Prepare | indent 4}} + fmul dword [{{.Float 0.99609375 | .Use}}] ; c(g-x) x + faddp st1, st0 ; x+c(g-x) + fst dword [{{.WRK}}+16]; g'=x+c(g-x) NOTE THAT UNISON 2 & UNISON 3 ALSO USE {{.WRK}}+16, so gate and unison 2 & 3 don't work. Probably should delete that low pass altogether + pop {{.AX}} ; Another way to see this (c~0.996) + ret ; g'=cg+(1-c)x + ; This is a low-pass to smooth the gate transitions +{{end}} + + +{{- if .HasCall "su_oscillat_sample"}} +{{.Func "su_oscillat_sample"}} + {{- .PushRegs .AX "SampleAx" .DX "SampleDx" .CX "SampleCx" .BX "SampleBx" | indent 4}} ; edx must be saved, eax & ecx if this is stereo osc + push {{.AX}} + mov al, byte [{{.VAL}}-4] ; reuse "color" as the sample number +%ifdef RUNTIME_TABLES ; when using RUNTIME_TABLES, assumed the sample_offset ptr is in {{.DI}} + do{lea {{.DI}}, [}, {{.DI}}, {{.AX}}*8,] ; edi points now to the sample table entry +%else +{{- .Prepare "su_sample_offsets" | indent 4}} + lea {{.DI}}, [{{.Use "su_sample_offsets"}} + {{.AX}}*8]; edi points now to the sample table entry +%endif +{{- .Float 84.28074964676522 | .Prepare | indent 4}} + fmul dword [{{.Float 84.28074964676522 | .Use}}] ; p*r + fistp dword [{{.SP}}] + pop {{.DX}} ; edx is now the sample number + movzx ebx, word [{{.DI}} + 4] ; ecx = loopstart + sub edx, ebx ; if sample number < loop start + jl su_oscillat_sample_not_looping ; then we're not looping yet + mov eax, edx ; eax = sample number + movzx ecx, word [{{.DI}} + 6] ; edi is now the loop length + xor edx, edx ; div wants edx to be empty + div ecx ; edx is now the remainder +su_oscillat_sample_not_looping: + add edx, ebx ; sampleno += loopstart + add edx, dword [{{.DI}}] +{{- .Prepare "su_sample_table" | indent 4}} + fild word [{{.Use "su_sample_table"}} + {{.DX}}*2] +{{- .Float 32767.0 | .Prepare | indent 4}} + fdiv dword [{{.Float 32767.0 | .Use}}] + {{- .PopRegs .AX .DX .CX .BX | indent 4}} + ret +{{end}} + + +{{- if .Opcode "loadval"}} +;------------------------------------------------------------------------------- +; LOADVAL opcode +;------------------------------------------------------------------------------- +{{- if .Mono "loadval"}} +; Mono: push 2*v-1 on stack, where v is the input to port "value" +{{- end}} +{{- if .Stereo "loadval"}} +; Stereo: push 2*v-1 twice on stack +{{- end}} +;------------------------------------------------------------------------------- +{{.Func "su_op_loadval" "Opcode"}} + {{- if .StereoAndMono "loadval" }} + jnc su_op_loadval_mono + {{- end}} + {{- if .Stereo "loadval" }} + call su_op_loadval_mono +su_op_loadval_mono: + {{- end }} + fld dword [{{.Input "loadval" "value"}}] ; v +{{- .Float 0.5 | .Prepare | indent 4}} + fsub dword [{{.Float 0.5 | .Use}}] + fadd st0 ; 2*v-1 + ret +{{end}} + + +{{- if .Opcode "receive"}} +;------------------------------------------------------------------------------- +; RECEIVE opcode +;------------------------------------------------------------------------------- +{{- if .Mono "receive"}} +; Mono: push l on stack, where l is the left channel received +{{- end}} +{{- if .Stereo "receive"}} +; Stereo: push l r on stack +{{- end}} +;------------------------------------------------------------------------------- +{{.Func "su_op_receive" "Opcode"}} + lea {{.DI}}, [{{.WRK}}+su_unit.ports] +{{- if .StereoAndMono "receive"}} + jnc su_op_receive_mono +{{- end}} +{{- if .Stereo "receive"}} + xor ecx,ecx + fld dword [{{.DI}}+4] + mov dword [{{.DI}}+4],ecx +{{- end}} +{{- if .StereoAndMono "receive"}} +su_op_receive_mono: + xor ecx,ecx +{{- end}} + fld dword [{{.DI}}] + mov dword [{{.DI}}],ecx + ret +{{end}} + + +{{- if .Opcode "in"}} +;------------------------------------------------------------------------------- +; IN opcode: inputs and clears a global port +;------------------------------------------------------------------------------- +; Mono: push the left channel of a global port (out or aux) +; Stereo: also push the right channel (stack in l r order) +;------------------------------------------------------------------------------- +{{.Func "su_op_in" "Opcode"}} + lodsb + mov {{.DI}}, [{{.Stack "Synth"}}] +{{- if .StereoAndMono "in"}} + jnc su_op_in_mono +{{- end}} +{{- if .Stereo "in"}} + xor ecx, ecx ; we cannot xor before jnc, so we have to do it mono & stereo. LAHF / SAHF could do it, but is the same number of bytes with more entropy + fld dword [{{.DI}} + su_synthworkspace.right + {{.AX}}*4] + mov dword [{{.DI}} + su_synthworkspace.right + {{.AX}}*4], ecx +{{- end}} +{{- if .StereoAndMono "in"}} +su_op_in_mono: + xor ecx, ecx +{{- end}} + fld dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4] + mov dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4], ecx + ret +{{end}} diff --git a/templates/structs.asm b/templates/structs.asm new file mode 100644 index 0000000..cd2c20e --- /dev/null +++ b/templates/structs.asm @@ -0,0 +1,43 @@ +;------------------------------------------------------------------------------- +; unit struct +;------------------------------------------------------------------------------- +struc su_unit + .state resd 8 + .ports resd 8 + .size: +endstruc + +;------------------------------------------------------------------------------- +; voice struct +;------------------------------------------------------------------------------- +struc su_voice + .note resd 1 + .release resd 1 + .inputs resd 8 + .reserved resd 6 ; this is done to so the whole voice is 2^n long, see polyphonic player + .workspace resb 63 * su_unit.size + .size: +endstruc + +;------------------------------------------------------------------------------- +; synthworkspace struct +;------------------------------------------------------------------------------- +struc su_synthworkspace + .curvoices resb 32 ; these are used by the multitrack player to store which voice is playing on which track + .left resd 1 + .right resd 1 + .aux resd 6 ; 3 auxiliary signals + .voices resb 32 * su_voice.size + .size: +endstruc + +;------------------------------------------------------------------------------- +; su_delayline_wrk struct +;------------------------------------------------------------------------------- +struc su_delayline_wrk + .dcin resd 1 + .dcout resd 1 + .filtstate resd 1 + .buffer resd 65536 + .size: +endstruc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 72397f4..e4a6b22 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,33 +1,30 @@ -function(regression_test testname) +function(regression_test testname) + if(${ARGC} LESS 4) set(source ${testname}.yml) set(asmfile ${testname}.asm) set (headerfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.h) - add_custom_command( - PRE_BUILD - OUTPUT ${headerfile} - COMMAND go run ${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/main.go -c -w -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source} - DEPENDS ${source} - ) - add_custom_command( - PRE_BUILD + + if(DEFINED CMAKE_C_SIZEOF_DATA_PTR AND CMAKE_C_SIZEOF_DATA_PTR EQUAL 8) + set(arch "-arch=amd64") + elseif(DEFINED CMAKE_CXX_SIZEOF_DATA_PTR AND CMAKE_CXX_SIZEOF_DATA_PTR EQUAL 8) + set(arch "-arch=amd64") + else() + set(arch "-arch=386") + endif() + + add_custom_command( OUTPUT ${asmfile} - COMMAND go run ${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/main.go -a -w -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source} - DEPENDS ${source} + COMMAND ${sointuexe} -a -c -w ${arch} -d ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source} + DEPENDS ${source} ${sointuexe} ) - add_executable(${testname} test_renderer.c ${headerfile} ${asmfile}) + + add_executable(${testname} test_renderer.c ${asmfile}) target_compile_definitions(${testname} PUBLIC TEST_HEADER=<${testname}.h>) else() set(source ${ARGV3}) add_executable(${testname} ${source} test_renderer.c) - endif() - - # the tests include the entire ASM but we still want to rebuild when they change - file(GLOB SOINTU ${PROJECT_SOURCE_DIR}/include/sointu/*.inc - ${PROJECT_SOURCE_DIR}/include/sointu/win32/*.inc - ${PROJECT_SOURCE_DIR}/include/sointu/win64/*.inc) - set_source_files_properties(${source}.asm PROPERTIES OBJECT_DEPENDS "${SOINTU}") - set_source_files_properties(${FOURKLANG} PROPERTIES HEADER_FILE_ONLY TRUE) + endif() add_test(${testname} ${testname}) target_link_libraries(${testname} ${HEADERLIB}) @@ -39,11 +36,11 @@ function(regression_test testname) COMMAND ${CMAKE_COMMAND} -E copy_if_different ${rawinput} ${rawoutput} ) - target_include_directories(${testname} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) - target_compile_definitions(${testname} PUBLIC TEST_NAME="${testname}") - add_dependencies(${testname} ${testname}_rawcopy) + target_include_directories(${testname} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + target_compile_definitions(${testname} PUBLIC TEST_NAME="${testname}") + if(ARGC GREATER 1) if (ARGV1) message("${testname} requires ${ARGV1}") @@ -59,6 +56,27 @@ function(regression_test testname) endif() endfunction(regression_test) +if(WIN32) + set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli.exe) +else() + set(sointuexe ${CMAKE_CURRENT_BINARY_DIR}/sointu-cli) +endif() + +# the tests include the entire ASM but we still want to rebuild when they change +file(GLOB templates ${PROJECT_SOURCE_DIR}/templates/*.asm) +file(GLOB go4k "${PROJECT_SOURCE_DIR}/go4k/*.go" "${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/*.go") + +message("templates=${templates}") +message("go4k=${go4k}") + +# Build sointu-cli only once because go run has everytime quite a bit of delay when +# starting +add_custom_command( + OUTPUT ${sointuexe} + COMMAND go build -o ${sointuexe} ${PROJECT_SOURCE_DIR}/go4k/cmd/sointu-cli/main.go + DEPENDS "${templates}" "${go4k}" +) + regression_test(test_envelope "" ENVELOPE) regression_test(test_envelope_stereo ENVELOPE) regression_test(test_loadval "" LOADVAL) diff --git a/tests/test_delay_flanger.yml b/tests/test_delay_flanger.yml index b4ea088..dc86e4d 100644 --- a/tests/test_delay_flanger.yml +++ b/tests/test_delay_flanger.yml @@ -24,6 +24,6 @@ patch: - type: oscillator parameters: {color: 128, detune: 64, gain: 128, lfo: 1, phase: 64, shape: 64, stereo: 0, transpose: 50, type: 0, unison: 0} - type: send - parameters: {amount: 65, port: 5, sendpop: 1, stereo: 0, unit: 3, voice: 0} + parameters: {amount: 65, port: 4, sendpop: 1, stereo: 0, unit: 3, voice: 0} delaytimes: [1000] sampleoffsets: [] diff --git a/tests/test_renderer.c b/tests/test_renderer.c index 110332d..8637103 100644 --- a/tests/test_renderer.c +++ b/tests/test_renderer.c @@ -1,16 +1,17 @@ -#include +#if defined (_WIN32) +#define _CRT_SECURE_NO_DEPRECATE +#include +#else +#include +#include +#endif #include #include #include #include #include #include -#if defined (_WIN32) -#include -#else -#include -#include -#endif +#include #include TEST_HEADER SUsample buf[SU_BUFFER_LENGTH]; @@ -20,7 +21,6 @@ int main(int argc, char* argv[]) { FILE* f; char filename[256]; int n; - int retval; char test_name[] = TEST_NAME; char expected_output_folder[] = "expected_output/"; char actual_output_folder[] = "actual_output/"; @@ -64,7 +64,7 @@ int main(int argc, char* argv[]) { max_diff = 0.0f; for (n = 0; n < SU_BUFFER_LENGTH; n++) { - diff = fabs((float)(buf[n] - filebuf[n])/SU_SAMPLE_RANGE); + diff = (float)fabs((float)(buf[n] - filebuf[n])/SU_SAMPLE_RANGE); if (diff > 1e-3f || isnan(diff)) { printf("Sointu rendered different wave than expected\n"); goto fail;