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

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

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

7
go.mod
View File

@ -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
)

18
go.sum
View File

@ -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=

View File

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

View File

@ -79,6 +79,26 @@ func Render(synth Synth, buffer []float32) error {
return err
}
func (p *Patch) Encode() ([]string, []byte, []byte) {
var code []byte
var values []byte
var jumpTable []string
assignedIds := map[string]byte{}
for _, instr := range p.Instruments {
for _, unit := range instr.Units {
if _, ok := assignedIds[unit.Type]; !ok {
jumpTable = append(jumpTable, unit.Type)
assignedIds[unit.Type] = byte(len(jumpTable) * 2)
}
stereo, unitValues := Encode(unit)
code = append(code, stereo+assignedIds[unit.Type])
values = append(values, unitValues...)
}
code = append(code, 0)
}
return jumpTable, code, values
}
// UnitParameter documents one parameter that an unit takes
type UnitParameter struct {
Name string // thould be found with this name in the Unit.Parameters map
@ -88,185 +108,175 @@ type UnitParameter struct {
CanModulate bool // if this parameter can be modulated i.e. has a port number in "send" unit
}
// UnitType documents the supported behaviour of one type of unit (oscillator, envelope etc.)
type UnitType struct {
Name string
Parameters []UnitParameter
func Encode(unit Unit) (byte, []byte) {
var values []byte
for _, v := range UnitTypes[unit.Type] {
if v.CanSet && v.CanModulate {
values = append(values, byte(unit.Parameters[v.Name]))
}
}
if unit.Type == "aux" {
values = append(values, byte(unit.Parameters["channel"]))
} else if unit.Type == "in" {
values = append(values, byte(unit.Parameters["channel"]))
} else if unit.Type == "oscillator" {
flags := 0
switch unit.Parameters["type"] {
case Sine:
flags = 0x40
case Trisaw:
flags = 0x20
case Pulse:
flags = 0x10
case Gate:
flags = 0x04
case Sample:
flags = 0x80
}
if unit.Parameters["lfo"] == 1 {
flags += 0x08
}
flags += unit.Parameters["unison"]
values = append(values, byte(flags))
} else if unit.Type == "filter" {
flags := 0
if unit.Parameters["lowpass"] == 1 {
flags += 0x40
}
if unit.Parameters["bandpass"] == 1 {
flags += 0x20
}
if unit.Parameters["highpass"] == 1 {
flags += 0x10
}
if unit.Parameters["negbandpass"] == 1 {
flags += 0x08
}
if unit.Parameters["neghighpass"] == 1 {
flags += 0x04
}
values = append(values, byte(flags))
} else if unit.Type == "send" {
address := ((unit.Parameters["unit"] + 1) << 4) + unit.Parameters["port"] // each unit is 16 dwords, 8 workspace followed by 8 ports. +1 is for skipping the note/release/inputs
if unit.Parameters["voice"] > 0 {
address += 0x8000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, +16 is for skipping the out/aux ports
}
if unit.Parameters["sendpop"] == 1 {
address += 0x8
}
values = append(values, byte(address&255), byte(address>>8))
} else if unit.Type == "delay" {
countTrack := (unit.Parameters["count"] << 1) - 1 + unit.Parameters["notetracking"] // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
values = append(values, byte(unit.Parameters["delay"]), byte(countTrack))
}
return byte(unit.Parameters["stereo"]), values
}
// UnitTypes documents all the available unit types and if they support stereo variant
// and what parameters they take.
var UnitTypes = []UnitType{
{
Name: "add",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "addp",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "pop",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "loadnote",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "mul",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "mulp",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "push",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "xch",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "distort",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "hold",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "crush",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "gain",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "invgain",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "filter",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "clip",
Parameters: []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}},
{
Name: "pan",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "delay",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "delay", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: true},
{Name: "count", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
}},
{
Name: "compressor",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "speed",
Parameters: []UnitParameter{}},
{
Name: "out",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}}},
{
Name: "outaux",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "aux",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false},
}},
{
Name: "send",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
{Name: "unit", MinValue: 0, MaxValue: 63, CanSet: true, CanModulate: false},
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
}},
{
Name: "envelope",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "noise",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "oscillator",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false},
}},
{
Name: "loadval",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
}},
{
Name: "receive",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
}},
{
Name: "in",
Parameters: []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false},
}},
var UnitTypes = map[string]([]UnitParameter){
"add": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"addp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"pop": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"loadnote": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"mul": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"mulp": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"push": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"xch": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"distort": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "drive", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"hold": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "holdfreq", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"crush": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "resolution", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"gain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"invgain": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"filter": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "frequency", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "resonance", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "lowpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "bandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "highpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "negbandpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "neghighpass", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"clip": []UnitParameter{{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"pan": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "panning", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"delay": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "pregain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "dry", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "feedback", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "damp", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "notetracking", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "delay", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
{Name: "count", MinValue: 0, MaxValue: 255, CanSet: true, CanModulate: false},
{Name: "delaytime", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"compressor": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "invgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "threshold", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "ratio", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"speed": []UnitParameter{},
"out": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"outaux": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "outgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "auxgain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"aux": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
"send": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false},
{Name: "unit", MinValue: 0, MaxValue: 63, CanSet: true, CanModulate: false},
{Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false},
{Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}},
"envelope": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "attack", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "decay", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "sustain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "release", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"noise": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"oscillator": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "transpose", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "detune", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "phase", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "color", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "shape", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "gain", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true},
{Name: "type", MinValue: int(Sine), MaxValue: int(Sample), CanSet: true, CanModulate: false},
{Name: "lfo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "unison", MinValue: 0, MaxValue: 3, CanSet: true, CanModulate: false}},
"loadval": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "value", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true}},
"receive": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "left", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true},
{Name: "right", MinValue: 0, MaxValue: -1, CanSet: false, CanModulate: true}},
"in": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
}

627
go4k/macros.go Normal file
View File

@ -0,0 +1,627 @@
package go4k
import (
"bytes"
"fmt"
"math"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/Masterminds/sprig"
)
type OplistEntry struct {
Type string
NumParams int
}
type Macros struct {
Opcodes []OplistEntry
Polyphony bool
MultivoiceTracks bool
PolyphonyBitmask int
Stacklocs []string
Output16Bit bool
Clip bool
Amd64 bool
OS string
DisableSections bool
Sine int // TODO: how can we elegantly access global constants in template, without wrapping each one by one
Trisaw int
Pulse int
Gate int
Sample int
usesFloatConst map[float32]bool
usesIntConst map[int]bool
floatConsts []float32
intConsts []int
calls map[string]bool
stereo map[string]bool
mono map[string]bool
ops map[string]bool
stackframes map[string][]string
unitInputMap map[string](map[string]int)
}
type PlayerMacros struct {
Song *Song
VoiceTrackBitmask int
JumpTable []string
Code []byte
Values []byte
Macros
}
func NewPlayerMacros(song *Song, targetArch string, targetOS string) *PlayerMacros {
unitInputMap := map[string](map[string]int){}
for k, v := range UnitTypes {
inputMap := map[string]int{}
inputCount := 0
for _, t := range v {
if t.CanModulate {
inputMap[t.Name] = inputCount
inputCount++
}
}
unitInputMap[k] = inputMap
}
jumpTable, code, values := song.Patch.Encode()
amd64 := targetArch == "amd64"
p := &PlayerMacros{
Song: song,
JumpTable: jumpTable,
Code: code,
Values: values,
Macros: Macros{
mono: map[string]bool{},
stereo: map[string]bool{},
calls: map[string]bool{},
ops: map[string]bool{},
usesFloatConst: map[float32]bool{},
usesIntConst: map[int]bool{},
stackframes: map[string][]string{},
unitInputMap: unitInputMap,
Amd64: amd64,
OS: targetOS,
Sine: Sine,
Trisaw: Trisaw,
Pulse: Pulse,
Gate: Gate,
Sample: Sample,
}}
for _, track := range song.Tracks {
if track.NumVoices > 1 {
p.MultivoiceTracks = true
}
}
trackVoiceNumber := 0
for _, t := range song.Tracks {
for b := 0; b < t.NumVoices-1; b++ {
p.VoiceTrackBitmask += 1 << trackVoiceNumber
trackVoiceNumber++
}
trackVoiceNumber++ // set all bits except last one
}
totalVoices := 0
for _, instr := range song.Patch.Instruments {
if instr.NumVoices > 1 {
p.Polyphony = true
}
for _, unit := range instr.Units {
if !p.ops[unit.Type] {
p.ops[unit.Type] = true
numParams := 0
for _, v := range UnitTypes[unit.Type] {
if v.CanSet && v.CanModulate {
numParams++
}
}
p.Opcodes = append(p.Opcodes, OplistEntry{
Type: unit.Type,
NumParams: numParams,
})
}
if unit.Parameters["stereo"] == 1 {
p.stereo[unit.Type] = true
} else {
p.mono[unit.Type] = true
}
}
totalVoices += instr.NumVoices
for k := 0; k < instr.NumVoices-1; k++ {
p.PolyphonyBitmask = (p.PolyphonyBitmask << 1) + 1
}
p.PolyphonyBitmask <<= 1
}
p.Output16Bit = song.Output16Bit
return p
}
func (p *Macros) Opcode(t string) bool {
return p.ops[t]
}
func (p *Macros) Stereo(t string) bool {
return p.stereo[t]
}
func (p *Macros) Mono(t string) bool {
return p.mono[t]
}
func (p *Macros) StereoAndMono(t string) bool {
return p.stereo[t] && p.mono[t]
}
// Macros and functions to accumulate constants automagically
func (p *Macros) Float(value float32) string {
if _, ok := p.usesFloatConst[value]; !ok {
p.usesFloatConst[value] = true
p.floatConsts = append(p.floatConsts, value)
}
return nameForFloat(value)
}
func (p *Macros) Int(value int) string {
if _, ok := p.usesIntConst[value]; !ok {
p.usesIntConst[value] = true
p.intConsts = append(p.intConsts, value)
}
return nameForInt(value)
}
func (p *Macros) Constants() string {
var b strings.Builder
for _, v := range p.floatConsts {
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForFloat(v), math.Float32bits(v))
}
for _, v := range p.intConsts {
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForInt(v), v)
}
return b.String()
}
func nameForFloat(value float32) string {
s := fmt.Sprintf("%#g", value)
s = strings.Replace(s, ".", "_", 1)
s = strings.Replace(s, "-", "m", 1)
s = strings.Replace(s, "+", "p", 1)
return "FCONST_" + s
}
func nameForInt(value int) string {
return "ICONST_" + fmt.Sprintf("%d", value)
}
func (p *Macros) PTRSIZE() int {
if p.Amd64 {
return 8
}
return 4
}
func (p *Macros) DPTR() string {
if p.Amd64 {
return "dq"
}
return "dd"
}
func (p *Macros) PTRWORD() string {
if p.Amd64 {
return "qword"
}
return "dword"
}
func (p *Macros) AX() string {
if p.Amd64 {
return "rax"
}
return "eax"
}
func (p *Macros) BX() string {
if p.Amd64 {
return "rbx"
}
return "ebx"
}
func (p *Macros) CX() string {
if p.Amd64 {
return "rcx"
}
return "ecx"
}
func (p *Macros) DX() string {
if p.Amd64 {
return "rdx"
}
return "edx"
}
func (p *Macros) SI() string {
if p.Amd64 {
return "rsi"
}
return "esi"
}
func (p *Macros) DI() string {
if p.Amd64 {
return "rdi"
}
return "edi"
}
func (p *Macros) SP() string {
if p.Amd64 {
return "rsp"
}
return "esp"
}
func (p *Macros) BP() string {
if p.Amd64 {
return "rbp"
}
return "ebp"
}
func (p *Macros) WRK() string {
return p.BP()
}
func (p *Macros) VAL() string {
return p.SI()
}
func (p *Macros) COM() string {
return p.BX()
}
func (p *Macros) INP() string {
return p.DX()
}
func (p *Macros) SaveStack(scope string) string {
p.stackframes[scope] = p.Stacklocs
return ""
}
func (p *Macros) Call(funcname string) (string, error) {
p.calls[funcname] = true
var s = make([]string, len(p.Stacklocs))
copy(s, p.Stacklocs)
p.stackframes[funcname] = s
return "call " + funcname, nil
}
func (p *Macros) TailCall(funcname string) (string, error) {
p.calls[funcname] = true
p.stackframes[funcname] = p.Stacklocs
return "jmp " + funcname, nil
}
func (p *Macros) SectText(name string) string {
if p.OS == "windows" {
if p.DisableSections {
return "section .code align=1"
}
return fmt.Sprintf("section .%v code align=1", name)
} else if p.OS == "darwin" {
return "section .text align=1"
} else {
if p.DisableSections {
return "section .text. progbits alloc exec nowrite align=1"
}
return fmt.Sprintf("section .text.%v progbits alloc exec nowrite align=1", name)
}
}
func (p *Macros) SectData(name string) string {
if p.OS == "windows" || p.OS == "darwin" {
if p.OS == "windows" && !p.DisableSections {
return fmt.Sprintf("section .%v data align=1", name)
}
return "section .data align=1"
} else {
if !p.DisableSections {
return fmt.Sprintf("section .data.%v progbits alloc noexec write align=1", name)
}
return "section .data. progbits alloc exec nowrite align=1"
}
}
func (p *Macros) SectBss(name string) string {
if p.OS == "windows" || p.OS == "darwin" {
if p.OS == "windows" && !p.DisableSections {
return fmt.Sprintf("section .%v bss align=256", name)
}
return "section .bss align=256"
} else {
if !p.DisableSections {
return fmt.Sprintf("section .bss.%v progbits alloc noexec write align=256", name)
}
return "section .bss. progbits alloc exec nowrite align=256"
}
}
func (p *Macros) Data(label string) string {
return fmt.Sprintf("%v\n%v:", p.SectData(label), label)
}
func (p *Macros) Func(funcname string, scope ...string) (string, error) {
scopeName := funcname
if len(scope) > 1 {
return "", fmt.Errorf(`Func macro "%v" can take only one additional scope parameter, "%v" were given`, funcname, scope)
} else if len(scope) > 0 {
scopeName = scope[0]
}
p.Stacklocs = append(p.stackframes[scopeName], "retaddr_"+funcname)
return fmt.Sprintf("%v\n%v:", p.SectText(funcname), funcname), nil
}
func (p *Macros) HasCall(funcname string) bool {
return p.calls[funcname]
}
func (p *Macros) Push(value string, name string) string {
p.Stacklocs = append(p.Stacklocs, name)
return fmt.Sprintf("push %v ; Stack: %v ", value, p.FmtStack())
}
func (p *Macros) PushRegs(params ...string) string {
if p.Amd64 {
var b strings.Builder
for i := 0; i < len(params); i = i + 2 {
b.WriteRune('\n')
b.WriteString(p.Push(params[i], params[i+1]))
}
return b.String()
} else {
var pushadOrder = [...]string{"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"}
for _, name := range pushadOrder {
for j := 0; j < len(params); j = j + 2 {
if params[j] == name {
name = params[j+1]
}
}
p.Stacklocs = append(p.Stacklocs, name)
}
return fmt.Sprintf("\npushad ; Stack: %v", p.FmtStack())
}
}
func (p *Macros) PopRegs(params ...string) string {
if p.Amd64 {
var b strings.Builder
for i := len(params) - 1; i >= 0; i-- {
b.WriteRune('\n')
b.WriteString(p.Pop(params[i]))
}
return b.String()
} else {
var regs = [...]string{"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"}
var b strings.Builder
for i, name := range p.Stacklocs[len(p.Stacklocs)-8:] {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(regs[i])
if regs[i] != name {
b.WriteString(" = ")
b.WriteString(name)
}
}
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-8]
return fmt.Sprintf("\npopad ; Popped: %v. Stack: %v", b.String(), p.FmtStack())
}
}
func (p *Macros) Pop(register string) string {
last := p.Stacklocs[len(p.Stacklocs)-1]
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1]
return fmt.Sprintf("pop %v ; %v = %v, Stack: %v ", register, register, last, p.FmtStack())
}
func (p *Macros) Stack(name string) (string, error) {
for i, k := range p.Stacklocs {
if k == name {
pos := len(p.Stacklocs) - i - 1
if p.Amd64 {
pos = pos * 8
} else {
pos = pos * 4
}
if pos != 0 {
return fmt.Sprintf("%v + %v", p.SP(), pos), nil
}
return p.SP(), nil
}
}
return "", fmt.Errorf("unknown symbol %v", name)
}
func (p *Macros) FmtStack() string {
var b strings.Builder
last := len(p.Stacklocs) - 1
for i := range p.Stacklocs {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(p.Stacklocs[last-i])
}
return b.String()
}
func (p *Macros) ExportFunc(name string, params ...string) string {
if !p.Amd64 {
p.Stacklocs = append(params, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
if p.OS == "windows" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4)
}
}
if p.OS == "darwin" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name)
}
return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name)
}
func (p *Macros) Count(count int) []int {
s := make([]int, count)
for i := range s {
s[i] = i
}
return s
}
func (p *Macros) Sub(a int, b int) int {
return a - b
}
func (p *Macros) Input(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown input "%v" for unit "%v"`, port, unit)
}
if i != 0 {
return fmt.Sprintf("%v + %v", p.INP(), i*4), nil
}
return p.INP(), nil
}
func (p *Macros) InputNumber(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find InputNumber for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find InputNumber for unknown input "%v" for unit "%v"`, port, unit)
}
return fmt.Sprintf("%v", i), nil
}
func (p *Macros) Modulation(unit string, port string) (string, error) {
umap, ok := p.unitInputMap[unit]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown unit "%v"`, unit)
}
i, ok := umap[port]
if !ok {
return "", fmt.Errorf(`trying to find input for unknown input "%v" for unit "%v"`, port, unit)
}
return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil
}
func (p *Macros) Prepare(value string, regs ...string) (string, error) {
if p.Amd64 {
if len(regs) > 1 {
return "", fmt.Errorf("macro Prepare cannot accept more than one register parameter")
} else if len(regs) > 0 {
return fmt.Sprintf("\nmov r9, qword %v\nlea r9, [r9 + %v]", value, regs[0]), nil
}
return fmt.Sprintf("\nmov r9, qword %v", value), nil
}
return "", nil
}
func (p *Macros) Use(value string, regs ...string) (string, error) {
if p.Amd64 {
return "r9", nil
}
if len(regs) > 1 {
return "", fmt.Errorf("macro Use cannot accept more than one register parameter")
} else if len(regs) > 0 {
return value + " + " + regs[0], nil
}
return value, nil
}
func (p *PlayerMacros) NumDelayLines() string {
total := 0
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == "delay" {
total += unit.Parameters["count"] * (1 + unit.Parameters["stereo"])
}
}
}
return fmt.Sprintf("%v", total)
}
func (p *PlayerMacros) UsesDelayModulation() (bool, error) {
for i, instrument := range p.Song.Patch.Instruments {
for j, unit := range instrument.Units {
if unit.Type == "send" {
targetInstrument := i
if unit.Parameters["voice"] > 0 {
v, err := p.Song.Patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
return false, fmt.Errorf("INSTRUMENT #%v / SEND #%v targets voice %v, which does not exist", i, j, unit.Parameters["voice"])
}
targetInstrument = v
}
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(p.Song.Patch.Instruments[targetInstrument].Units) {
return false, fmt.Errorf("INSTRUMENT #%v / SEND #%v target unit %v out of range", i, j, unit.Parameters["unit"])
}
if p.Song.Patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]].Type == "delay" && unit.Parameters["port"] == 4 {
return true, nil
}
}
}
}
return false, nil
}
func (p *PlayerMacros) HasParamValue(unitType string, paramName string, value int) bool {
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == unitType {
if unit.Parameters[paramName] == value {
return true
}
}
}
}
return false
}
func (p *PlayerMacros) HasParamValueOtherThan(unitType string, paramName string, value int) bool {
for _, instr := range p.Song.Patch.Instruments {
for _, unit := range instr.Units {
if unit.Type == unitType {
if unit.Parameters[paramName] != value {
return true
}
}
}
}
return false
}
func Compile(song *Song, targetArch string, targetOs string) (string, error) {
_, myname, _, _ := runtime.Caller(0)
templateDir := filepath.Join(path.Dir(myname), "..", "templates", "*.asm")
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(templateDir)
if err != nil {
return "", fmt.Errorf(`could not create template based on dir "%v": %v`, templateDir, err)
}
b := bytes.NewBufferString("")
err = tmpl.ExecuteTemplate(b, "player.asm", NewPlayerMacros(song, targetArch, targetOs))
if err != nil {
return "", fmt.Errorf(`could not execute template "player.asm": %v`, err)
}
return b.String(), nil
}

205
templates/arithmetic.asm Normal file
View File

@ -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}}

393
templates/effects.asm Normal file
View File

@ -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}}

23
templates/flowcontrol.asm Normal file
View File

@ -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}}

58
templates/gmdls.asm Normal file
View File

@ -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}}

304
templates/library.asm Normal file
View File

@ -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

View File

@ -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 }}

190
templates/patch.asm Normal file
View File

@ -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}}

237
templates/player.asm Normal file
View File

@ -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<<MAX_UNITS_SHIFT units + small header
{{- .Prepare "su_synth_obj" | indent 4}}
inc dword [{{.Use "su_synth_obj"}} + su_synthworkspace.voices + su_voice.release + {{.DI}}] ; set the voice currently active to release; notice that it could increment any number of times
cmp al, {{.Song.Hold}} ; if cl < HLD (no new note triggered)
jl su_update_voices_nexttrack ; goto nexttrack
inc ecx ; curvoice++
cmp ecx, edx ; if (curvoice >= 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}}

126
templates/sinks.asm Normal file
View File

@ -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}}

415
templates/sources.asm Normal file
View File

@ -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}}

43
templates/structs.asm Normal file
View File

@ -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

View File

@ -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)

View File

@ -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: []

View File

@ -1,16 +1,17 @@
#include <stdio.h>
#if defined (_WIN32)
#define _CRT_SECURE_NO_DEPRECATE
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/stat.h>
#endif
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#if defined (_WIN32)
#include <windows.h>
#else
#include <sys/types.h>
#include <sys/stat.h>
#endif
#include <stdio.h>
#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;