diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dd7c926..fed265a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,6 +42,7 @@ jobs: steps: - uses: lukka/get-cmake@v3.18.3 - uses: actions/setup-go@v2 + - uses: actions/setup-node@v1 - uses: actions/checkout@v2 - uses: ilammy/setup-nasm@v1.2.0 - name: Install libasound2-dev # sointu-cli has alsa as dependency for playing sound and diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a669fe..89e094d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,27 @@ IF(APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie") endif() +find_program(GO NAMES go) +if(NOT GO) + message(FATAL_ERROR "go not found. Get it from: https://golang.org") +else() + message("go found at: ${GO}") +endif() + +find_program(NODE NAMES node) +if(NOT NODE) + message( WARNING "node not found, cannot run WebAssembly tests. Get it from: https://nodejs.org/" ) +else() + message("node found at: ${NODE}") +endif() + +find_program(WAT2WASM NAMES wat2wasm) +if(NOT WAT2WASM ) + message( WARNING "wat2wasm not found, cannot build wasm tests. Get it from: https://github.com/WebAssembly/wabt)" ) +else() + message("wat2wasm found at: ${WAT2WASM}") +endif() + enable_language(ASM_NASM) # The normal NASM compile object does not include @@ -41,7 +62,8 @@ else() 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 x86templates ${PROJECT_SOURCE_DIR}/templates/amd64-386/*.asm) +file(GLOB wasmtemplates ${PROJECT_SOURCE_DIR}/templates/wasm/*.wat) file(GLOB sointusrc "${PROJECT_SOURCE_DIR}/*.go") file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/compiler/*.go") file(GLOB compilecmdsrc "${PROJECT_SOURCE_DIR}/cmd/sointu-compile/*.go") @@ -61,16 +83,16 @@ set(sointuasm sointu.asm) # Build sointu-cli only once because go run has everytime quite a bit of delay when # starting -add_custom_command( - OUTPUT ${compilecmd} - COMMAND go build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go - DEPENDS "${sointusrc}" "${compilersrc}" "${compilecmdsrc}" +add_custom_target( + sointu-compiler + COMMAND ${GO} build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go + SOURCES "${sointusrc}" "${compilersrc}" "${compilecmdsrc}" ) add_custom_command( OUTPUT ${sointuasm} COMMAND ${compilecmd} -arch=${arch} -a -o ${CMAKE_CURRENT_BINARY_DIR} - DEPENDS "${templates}" ${compilecmd} + DEPENDS "${templates}" sointu-compiler ) add_library(${STATICLIB} ${sointuasm}) diff --git a/README.md b/README.md index 2f6b256..3802dfc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Sointu ![Tests](https://github.com/vsariola/sointu/workflows/Tests/badge.svg) -A cross-platform modular software synthesizer for small intros, forked from -[4klang](https://github.com/hzdgopher/4klang). Supports win32/win64/linux/mac. +A cross-architecture and cross-platform modular software synthesizer for small +intros, forked from [4klang](https://github.com/hzdgopher/4klang). Targetable +architectures include 386, amd64, and WebAssembly; targetable platforms include +Windows, Mac, Linux (and related) + browser. Summary ------- @@ -15,17 +17,18 @@ sound is produced by a virtual machine that executes small bytecode to produce the audio; however, by now the internal virtual machine has been heavily rewritten and extended. It is actually extended so much that you will never fit all the features at the same time in a 4k intro, but a fairly capable synthesis -engine can already be fitted in 600 bytes (compressed), with another few hundred -bytes for the patch and pattern data. +engine can already be fitted in 600 bytes (386, compressed), with another few +hundred bytes for the patch and pattern data. Sointu consists of two core elements: - A cross-platform synth-tracker app for composing music, written in [go](https://golang.org/). The app is not working yet, but a prototype is existing. The app exports (will export) the projects as .yml files. - A compiler, likewise written in go, which can be invoked from the command line - to compile these .yml files into .asm code. The resulting single file .asm can - be then compiled by [nasm](https://www.nasm.us/) or - [yasm](https://yasm.tortall.net). + to compile these .yml files into .asm or .wat code. For x86 platforms, the + resulting .asm can be then compiled by [nasm](https://www.nasm.us/) or + [yasm](https://yasm.tortall.net). For browsers, the resulting .wat can be + compiled by [wat2wasm](https://github.com/WebAssembly/wabt). Building -------- @@ -53,6 +56,13 @@ sointu-compile -o . -arch=386 tests/test_chords.yml nasm -f win32 test_chords.asm ``` +WebAssembly example: + +``` +sointu-compile -o . -arch=wasm tests/test_chords.yml +wat2wasm --enable-bulk-memory test_chords.wat +``` + ### Building and running the tests as executables Building the [regression tests](tests/) as executables (testing that they work @@ -140,6 +150,11 @@ and the fix Use a newer nightly build of yasm that includes the fix. The linker had placed our synth object overlapping with DLL call addresses; very funny stuff to debug. +### Building and running the WebAssembly tests + +These are automatically invoked by CTest if [node](https://nodejs.org) and +[wat2wasm](https://github.com/WebAssembly/wabt) are found in the path. + New features since fork ----------------------- - **Compiler**. Written in go. The input is a .yml file and the output is an @@ -154,6 +169,8 @@ New features since fork changes to get it work, using template macros to change the lines between 32-bit and 64-bit modes. Mostly, it's as easy as writing {{.AX}} instead of eax; the macro {{.AX}} compiles to eax in 32-bit and rax in 64-bit. + - **Supports compiling into WebAssembly**. This is a complete reimplementation + of the core, written in WebAssembly text format (.wat). - **Supports Windows, Linux and MacOS**. On all three 64-bit platforms, all tests are passing. Additionally, all tests are passing on windows 32. - **New units**. For example: bit-crusher, gain, inverse gain, clip, modulate @@ -270,10 +287,6 @@ Crazy ideas uniforms to shader. A track with two voices, triggering an instrument with a single envelope and a slow filter can even be used as a cheap smooth interpolation mechanism. - - **Support WASM targets with the compiler**. It should not be impossible to - reimplement the x86 core with WAT equivalent. It would be nice to make it - work (almost) exactly like the x86 version, so one could just track the song - with Sointu tools and export the song to WAT using sointu-cli. - **Hack deeper into audio sources from the OS**. Speech synthesis, I'm eyeing at you. diff --git a/compiler/compiler.go b/compiler/compiler.go index 0fe06ed..ad87057 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -21,7 +21,15 @@ type Compiler struct { // New returns a new compiler using the default .asm templates func New(os string, arch string) (*Compiler, error) { _, myname, _, _ := runtime.Caller(0) - templateDir := filepath.Join(path.Dir(myname), "..", "templates") + var subdir string + if arch == "386" || arch == "amd64" { + subdir = "amd64-386" + } else if arch == "wasm" { + subdir = "wasm" + } else { + return nil, fmt.Errorf("compiler.New failed, because only amd64, 386 and wasm archs are supported (targeted architecture was %v)", arch) + } + templateDir := filepath.Join(path.Dir(myname), "..", "templates", subdir) compiler, err := NewFromTemplates(os, arch, templateDir) return compiler, err } @@ -43,9 +51,16 @@ func (com *Compiler) Library() (map[string]string, error) { features := AllFeatures{} retmap := map[string]string{} for _, templateName := range templates { - macros := NewMacros(*com, features) - macros.Library = true - populatedTemplate, extension, err := com.compile(templateName, macros) + compilerMacros := *NewCompilerMacros(*com) + compilerMacros.Library = true + featureSetMacros := FeatureSetMacros{features} + x86Macros := *NewX86Macros(com.OS, com.Arch == "amd64", features, false) + data := struct { + CompilerMacros + FeatureSetMacros + X86Macros + }{compilerMacros, featureSetMacros, x86Macros} + populatedTemplate, extension, err := com.compile(templateName, &data) if err != nil { return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err) } @@ -55,10 +70,15 @@ func (com *Compiler) Library() (map[string]string, error) { } func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) { - if com.Arch != "386" && com.Arch != "amd64" { - return nil, fmt.Errorf(`compiling a song player is supported only on 386 and amd64 architectures (targeted architecture was %v)`, com.Arch) + if com.Arch != "386" && com.Arch != "amd64" && com.Arch != "wasm" { + return nil, fmt.Errorf(`compiling a song player is supported only on 386, amd64 and wasm architectures (targeted architecture was %v)`, com.Arch) + } + var templates []string + if com.Arch == "386" || com.Arch == "amd64" { + templates = []string{"player.asm", "player.h"} + } else if com.Arch == "wasm" { + templates = []string{"player.wat"} } - templates := []string{"player.asm", "player.h"} features := NecessaryFeaturesFor(song.Patch) retmap := map[string]string{} encodedPatch, err := Encode(&song.Patch, features) @@ -66,8 +86,32 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) { return nil, fmt.Errorf(`could not encode patch: %v`, err) } for _, templateName := range templates { - macros := NewPlayerMacros(*com, features, song, encodedPatch) - populatedTemplate, extension, err := com.compile(templateName, macros) + compilerMacros := *NewCompilerMacros(*com) + featureSetMacros := FeatureSetMacros{features} + songMacros := *NewSongMacros(song) + var populatedTemplate, extension string + var err error + if com.Arch == "386" || com.Arch == "amd64" { + x86Macros := *NewX86Macros(com.OS, com.Arch == "amd64", features, false) + data := struct { + CompilerMacros + FeatureSetMacros + X86Macros + SongMacros + *EncodedPatch + }{compilerMacros, featureSetMacros, x86Macros, songMacros, encodedPatch} + populatedTemplate, extension, err = com.compile(templateName, &data) + } else if com.Arch == "wasm" { + wasmMacros := *NewWasmMacros() + data := struct { + CompilerMacros + FeatureSetMacros + WasmMacros + SongMacros + *EncodedPatch + }{compilerMacros, featureSetMacros, wasmMacros, songMacros, encodedPatch} + populatedTemplate, extension, err = com.compile(templateName, &data) + } if err != nil { return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err) } diff --git a/compiler/compiler_macros.go b/compiler/compiler_macros.go new file mode 100644 index 0000000..0369bb1 --- /dev/null +++ b/compiler/compiler_macros.go @@ -0,0 +1,28 @@ +package compiler + +import ( + "github.com/vsariola/sointu" +) + +type CompilerMacros struct { + Clip bool + Library 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 + Compiler +} + +func NewCompilerMacros(c Compiler) *CompilerMacros { + return &CompilerMacros{ + Sine: sointu.Sine, + Trisaw: sointu.Trisaw, + Pulse: sointu.Pulse, + Gate: sointu.Gate, + Sample: sointu.Sample, + Compiler: c, + } +} diff --git a/compiler/featureset_macros.go b/compiler/featureset_macros.go new file mode 100644 index 0000000..c1f460c --- /dev/null +++ b/compiler/featureset_macros.go @@ -0,0 +1,27 @@ +package compiler + +type FeatureSetMacros struct { + FeatureSet +} + +func (p *FeatureSetMacros) HasOp(instruction string) bool { + _, ok := p.Opcode(instruction) + return ok +} + +func (p *FeatureSetMacros) GetOp(instruction string) int { + v, _ := p.Opcode(instruction) + return v +} + +func (p *FeatureSetMacros) Stereo(unitType string) bool { + return p.SupportsParamValue(unitType, "stereo", 1) +} + +func (p *FeatureSetMacros) Mono(unitType string) bool { + return p.SupportsParamValue(unitType, "stereo", 0) +} + +func (p *FeatureSetMacros) StereoAndMono(unitType string) bool { + return p.Stereo(unitType) && p.Mono(unitType) +} diff --git a/compiler/song_macros.go b/compiler/song_macros.go new file mode 100644 index 0000000..8472a1e --- /dev/null +++ b/compiler/song_macros.go @@ -0,0 +1,39 @@ +package compiler + +import ( + "fmt" + + "github.com/vsariola/sointu" +) + +type SongMacros struct { + Song *sointu.Song + VoiceTrackBitmask int + MaxSamples int +} + +func NewSongMacros(s *sointu.Song) *SongMacros { + maxSamples := s.SamplesPerRow() * s.TotalRows() + p := SongMacros{Song: s, MaxSamples: maxSamples} + trackVoiceNumber := 0 + for _, t := range s.Tracks { + for b := 0; b < t.NumVoices-1; b++ { + p.VoiceTrackBitmask += 1 << trackVoiceNumber + trackVoiceNumber++ + } + trackVoiceNumber++ // set all bits except last one + } + return &p +} + +func (p *SongMacros) 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) +} diff --git a/compiler/wasm_macros.go b/compiler/wasm_macros.go new file mode 100644 index 0000000..92db1e5 --- /dev/null +++ b/compiler/wasm_macros.go @@ -0,0 +1,50 @@ +package compiler + +import ( + "bytes" + "encoding/binary" +) + +type WasmMacros struct { + data *bytes.Buffer + Labels map[string]int +} + +func NewWasmMacros() *WasmMacros { + return &WasmMacros{ + data: new(bytes.Buffer), + Labels: map[string]int{}, + } +} + +func (wm *WasmMacros) SetLabel(label string) string { + wm.Labels[label] = wm.data.Len() + return "" +} + +func (wm *WasmMacros) GetLabel(label string) int { + return wm.Labels[label] +} + +func (wm *WasmMacros) DataB(value byte) string { + binary.Write(wm.data, binary.LittleEndian, value) + return "" +} + +func (wm *WasmMacros) DataW(value uint16) string { + binary.Write(wm.data, binary.LittleEndian, value) + return "" +} + +func (wm *WasmMacros) DataD(value uint32) string { + binary.Write(wm.data, binary.LittleEndian, value) + return "" +} + +func (wm *WasmMacros) ToByte(value int) byte { + return byte(value) +} + +func (wm *WasmMacros) Data() []byte { + return wm.data.Bytes() +} diff --git a/compiler/macros.go b/compiler/x86_macros.go similarity index 63% rename from compiler/macros.go rename to compiler/x86_macros.go index 068a671..5f30d56 100644 --- a/compiler/macros.go +++ b/compiler/x86_macros.go @@ -4,74 +4,36 @@ import ( "fmt" "math" "strings" - - "github.com/vsariola/sointu" ) -type OplistEntry struct { - Type string - NumParams int -} - -type Macros struct { +type X86Macros struct { Stacklocs []string - Output16Bit bool - Clip bool - Library 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 stackframes map[string][]string - FeatureSet - Compiler + features FeatureSet } -func NewMacros(c Compiler, f FeatureSet) *Macros { - return &Macros{ - calls: map[string]bool{}, - usesFloatConst: map[float32]bool{}, - usesIntConst: map[int]bool{}, - stackframes: map[string][]string{}, - Sine: sointu.Sine, - Trisaw: sointu.Trisaw, - Pulse: sointu.Pulse, - Gate: sointu.Gate, - Sample: sointu.Sample, - Amd64: c.Arch == "amd64", - Compiler: c, - FeatureSet: f, +func NewX86Macros(os string, Amd64 bool, features FeatureSet, DisableSections bool) *X86Macros { + return &X86Macros{ + calls: map[string]bool{}, + usesFloatConst: map[float32]bool{}, + usesIntConst: map[int]bool{}, + stackframes: map[string][]string{}, + Amd64: Amd64, + OS: os, + DisableSections: DisableSections, + features: features, } } -func (p *Macros) HasOp(instruction string) bool { - _, ok := p.Opcode(instruction) - return ok -} - -func (p *Macros) Stereo(unitType string) bool { - return p.SupportsParamValue(unitType, "stereo", 1) -} - -func (p *Macros) Mono(unitType string) bool { - return p.SupportsParamValue(unitType, "stereo", 0) -} - -func (p *Macros) StereoAndMono(unitType string) bool { - return p.Stereo(unitType) && p.Mono(unitType) -} - -// Macros and functions to accumulate constants automagically - -func (p *Macros) Float(value float32) string { +func (p *X86Macros) Float(value float32) string { if _, ok := p.usesFloatConst[value]; !ok { p.usesFloatConst[value] = true p.floatConsts = append(p.floatConsts, value) @@ -79,7 +41,7 @@ func (p *Macros) Float(value float32) string { return nameForFloat(value) } -func (p *Macros) Int(value int) string { +func (p *X86Macros) Int(value int) string { if _, ok := p.usesIntConst[value]; !ok { p.usesIntConst[value] = true p.intConsts = append(p.intConsts, value) @@ -87,7 +49,7 @@ func (p *Macros) Int(value int) string { return nameForInt(value) } -func (p *Macros) Constants() string { +func (p *X86Macros) Constants() string { var b strings.Builder for _, v := range p.floatConsts { fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForFloat(v), math.Float32bits(v)) @@ -110,105 +72,105 @@ func nameForInt(value int) string { return "ICONST_" + fmt.Sprintf("%d", value) } -func (p *Macros) PTRSIZE() int { +func (p *X86Macros) PTRSIZE() int { if p.Amd64 { return 8 } return 4 } -func (p *Macros) DPTR() string { +func (p *X86Macros) DPTR() string { if p.Amd64 { return "dq" } return "dd" } -func (p *Macros) PTRWORD() string { +func (p *X86Macros) PTRWORD() string { if p.Amd64 { return "qword" } return "dword" } -func (p *Macros) AX() string { +func (p *X86Macros) AX() string { if p.Amd64 { return "rax" } return "eax" } -func (p *Macros) BX() string { +func (p *X86Macros) BX() string { if p.Amd64 { return "rbx" } return "ebx" } -func (p *Macros) CX() string { +func (p *X86Macros) CX() string { if p.Amd64 { return "rcx" } return "ecx" } -func (p *Macros) DX() string { +func (p *X86Macros) DX() string { if p.Amd64 { return "rdx" } return "edx" } -func (p *Macros) SI() string { +func (p *X86Macros) SI() string { if p.Amd64 { return "rsi" } return "esi" } -func (p *Macros) DI() string { +func (p *X86Macros) DI() string { if p.Amd64 { return "rdi" } return "edi" } -func (p *Macros) SP() string { +func (p *X86Macros) SP() string { if p.Amd64 { return "rsp" } return "esp" } -func (p *Macros) BP() string { +func (p *X86Macros) BP() string { if p.Amd64 { return "rbp" } return "ebp" } -func (p *Macros) WRK() string { +func (p *X86Macros) WRK() string { return p.BP() } -func (p *Macros) VAL() string { +func (p *X86Macros) VAL() string { return p.SI() } -func (p *Macros) COM() string { +func (p *X86Macros) COM() string { return p.BX() } -func (p *Macros) INP() string { +func (p *X86Macros) INP() string { return p.DX() } -func (p *Macros) SaveStack(scope string) string { +func (p *X86Macros) SaveStack(scope string) string { p.stackframes[scope] = p.Stacklocs return "" } -func (p *Macros) Call(funcname string) (string, error) { +func (p *X86Macros) Call(funcname string) (string, error) { p.calls[funcname] = true var s = make([]string, len(p.Stacklocs)) copy(s, p.Stacklocs) @@ -216,13 +178,13 @@ func (p *Macros) Call(funcname string) (string, error) { return "call " + funcname, nil } -func (p *Macros) TailCall(funcname string) (string, error) { +func (p *X86Macros) 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 { +func (p *X86Macros) SectText(name string) string { if p.OS == "windows" { if p.DisableSections { return "section .code align=1" @@ -238,7 +200,7 @@ func (p *Macros) SectText(name string) string { } } -func (p *Macros) SectData(name string) string { +func (p *X86Macros) 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) @@ -252,7 +214,7 @@ func (p *Macros) SectData(name string) string { } } -func (p *Macros) SectBss(name string) string { +func (p *X86Macros) 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) @@ -266,11 +228,11 @@ func (p *Macros) SectBss(name string) string { } } -func (p *Macros) Data(label string) string { +func (p *X86Macros) Data(label string) string { return fmt.Sprintf("%v\n%v:", p.SectData(label), label) } -func (p *Macros) Func(funcname string, scope ...string) (string, error) { +func (p *X86Macros) 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) @@ -281,16 +243,16 @@ func (p *Macros) Func(funcname string, scope ...string) (string, error) { return fmt.Sprintf("%v\n%v:", p.SectText(funcname), funcname), nil } -func (p *Macros) HasCall(funcname string) bool { +func (p *X86Macros) HasCall(funcname string) bool { return p.calls[funcname] } -func (p *Macros) Push(value string, name string) string { +func (p *X86Macros) 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 { +func (p *X86Macros) PushRegs(params ...string) string { if p.Amd64 { var b strings.Builder for i := 0; i < len(params); i = i + 2 { @@ -312,7 +274,7 @@ func (p *Macros) PushRegs(params ...string) string { } } -func (p *Macros) PopRegs(params ...string) string { +func (p *X86Macros) PopRegs(params ...string) string { if p.Amd64 { var b strings.Builder for i := len(params) - 1; i >= 0; i-- { @@ -338,13 +300,13 @@ func (p *Macros) PopRegs(params ...string) string { } } -func (p *Macros) Pop(register string) string { +func (p *X86Macros) 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) SaveFPUState() string { +func (p *X86Macros) SaveFPUState() string { i := 0 for ; i < 108; i += p.PTRSIZE() { p.Stacklocs = append(p.Stacklocs, fmt.Sprintf("F%v", i)) @@ -352,7 +314,7 @@ func (p *Macros) SaveFPUState() string { return fmt.Sprintf("sub %[1]v, %[2]v\nfsave [%[1]v]", p.SP(), i) } -func (p *Macros) LoadFPUState() string { +func (p *X86Macros) LoadFPUState() string { i := 0 for ; i < 108; i += p.PTRSIZE() { p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1] @@ -360,7 +322,7 @@ func (p *Macros) LoadFPUState() string { return fmt.Sprintf("frstor [%[1]v]\nadd %[1]v, %[2]v", p.SP(), i) } -func (p *Macros) Stack(name string) (string, error) { +func (p *X86Macros) Stack(name string) (string, error) { for i, k := range p.Stacklocs { if k == name { pos := len(p.Stacklocs) - i - 1 @@ -378,7 +340,7 @@ func (p *Macros) Stack(name string) (string, error) { return "", fmt.Errorf("unknown symbol %v", name) } -func (p *Macros) FmtStack() string { +func (p *X86Macros) FmtStack() string { var b strings.Builder last := len(p.Stacklocs) - 1 for i := range p.Stacklocs { @@ -390,7 +352,7 @@ func (p *Macros) FmtStack() string { return b.String() } -func (p *Macros) ExportFunc(name string, params ...string) string { +func (p *X86Macros) ExportFunc(name string, params ...string) string { if !p.Amd64 { reverseParams := make([]string, len(params)) for i, param := range params { @@ -407,20 +369,20 @@ func (p *Macros) ExportFunc(name string, params ...string) string { return fmt.Sprintf("%[1]v\nglobal %[2]v\n%[2]v:", p.SectText(name), name) } -func (p *Macros) Input(unit string, port string) (string, error) { - i := p.InputNumber(unit, port) +func (p *X86Macros) Input(unit string, port string) (string, error) { + i := p.features.InputNumber(unit, port) if i != 0 { return fmt.Sprintf("%v + %v", p.INP(), i*4), nil } return p.INP(), nil } -func (p *Macros) Modulation(unit string, port string) (string, error) { - i := p.InputNumber(unit, port) +func (p *X86Macros) Modulation(unit string, port string) (string, error) { + i := p.features.InputNumber(unit, port) return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil } -func (p *Macros) Prepare(value string, regs ...string) (string, error) { +func (p *X86Macros) 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") @@ -432,7 +394,7 @@ func (p *Macros) Prepare(value string, regs ...string) (string, error) { return "", nil } -func (p *Macros) Use(value string, regs ...string) (string, error) { +func (p *X86Macros) Use(value string, regs ...string) (string, error) { if p.Amd64 { return "r9", nil } @@ -443,39 +405,3 @@ func (p *Macros) Use(value string, regs ...string) (string, error) { } return value, nil } - -type PlayerMacros struct { - Song *sointu.Song - VoiceTrackBitmask int - MaxSamples int - Macros - EncodedPatch -} - -func NewPlayerMacros(c Compiler, f FeatureSet, s *sointu.Song, e *EncodedPatch) *PlayerMacros { - maxSamples := s.SamplesPerRow() * s.TotalRows() - macros := *NewMacros(c, f) - macros.Output16Bit = s.Output16Bit // TODO: should we actually store output16bit in Songs or not? - p := PlayerMacros{Song: s, Macros: macros, MaxSamples: maxSamples, EncodedPatch: *e} - trackVoiceNumber := 0 - for _, t := range s.Tracks { - for b := 0; b < t.NumVoices-1; b++ { - p.VoiceTrackBitmask += 1 << trackVoiceNumber - trackVoiceNumber++ - } - trackVoiceNumber++ // set all bits except last one - } - return &p -} - -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) -} diff --git a/templates/arithmetic.asm b/templates/amd64-386/arithmetic.asm similarity index 100% rename from templates/arithmetic.asm rename to templates/amd64-386/arithmetic.asm diff --git a/templates/effects.asm b/templates/amd64-386/effects.asm similarity index 100% rename from templates/effects.asm rename to templates/amd64-386/effects.asm diff --git a/templates/flowcontrol.asm b/templates/amd64-386/flowcontrol.asm similarity index 100% rename from templates/flowcontrol.asm rename to templates/amd64-386/flowcontrol.asm diff --git a/templates/gmdls.asm b/templates/amd64-386/gmdls.asm similarity index 100% rename from templates/gmdls.asm rename to templates/amd64-386/gmdls.asm diff --git a/templates/library.asm b/templates/amd64-386/library.asm similarity index 100% rename from templates/library.asm rename to templates/amd64-386/library.asm diff --git a/templates/library.h b/templates/amd64-386/library.h similarity index 100% rename from templates/library.h rename to templates/amd64-386/library.h diff --git a/templates/output_sound.asm b/templates/amd64-386/output_sound.asm similarity index 98% rename from templates/output_sound.asm rename to templates/amd64-386/output_sound.asm index 4306c0f..ecfee0c 100644 --- a/templates/output_sound.asm +++ b/templates/amd64-386/output_sound.asm @@ -1,4 +1,4 @@ -{{- if not .Output16Bit }} +{{- if not .Song.Output16Bit }} {{- if not .Clip }} mov {{.DI}}, [{{.Stack "OutputBufPtr"}}] ; edi containts ptr mov {{.SI}}, {{.PTRWORD}} su_synth_obj + su_synthworkspace.left diff --git a/templates/patch.asm b/templates/amd64-386/patch.asm similarity index 100% rename from templates/patch.asm rename to templates/amd64-386/patch.asm diff --git a/templates/player.asm b/templates/amd64-386/player.asm similarity index 100% rename from templates/player.asm rename to templates/amd64-386/player.asm diff --git a/templates/player.h b/templates/amd64-386/player.h similarity index 97% rename from templates/player.h rename to templates/amd64-386/player.h index 5811671..ff50133 100644 --- a/templates/player.h +++ b/templates/amd64-386/player.h @@ -23,7 +23,7 @@ #define SU_CALLCONV #endif -{{- if .Output16Bit}} +{{- if .Song.Output16Bit}} typedef short SUsample; #define SU_SAMPLE_RANGE 32767.0 {{- else}} diff --git a/templates/sinks.asm b/templates/amd64-386/sinks.asm similarity index 100% rename from templates/sinks.asm rename to templates/amd64-386/sinks.asm diff --git a/templates/sources.asm b/templates/amd64-386/sources.asm similarity index 95% rename from templates/sources.asm rename to templates/amd64-386/sources.asm index 323c6e5..04602bd 100644 --- a/templates/sources.asm +++ b/templates/amd64-386/sources.asm @@ -114,7 +114,9 @@ su_op_noise_mono: {{- 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 + ;; WARNING: this is a bug. WRK should be nonvolatile, but we are changing it. It does not cause immediate problems but modulations will be off. + ;; Figure out how to do this; maybe $WRK should be volatile (pushed by the virtual machine) + 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: @@ -129,10 +131,12 @@ su_op_oscillat_unison_loop: faddp st1, st0 ; a+=s test al, 3 je su_op_oscillat_unison_out + ;; WARNING: this is a bug. WRK should be nonvolatile, but we are changing it. It does not cause immediate problems but modulations will be off. + ;; Figure out how to do this; maybe $WRK should be volatile (pushed by the virtual machine) 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 + fadd dword [{{.Int 0x3DAAAAAA | .Use}}] ; 1/12 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}} diff --git a/templates/structs.asm b/templates/amd64-386/structs.asm similarity index 100% rename from templates/structs.asm rename to templates/amd64-386/structs.asm diff --git a/templates/wasm/arithmetic.wat b/templates/wasm/arithmetic.wat new file mode 100644 index 0000000..95ec612 --- /dev/null +++ b/templates/wasm/arithmetic.wat @@ -0,0 +1,239 @@ +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "pop"}} + (if (local.get $stereo) (then + (drop (call $pop)) + )) +{{- end}} + (drop (call $pop)) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .StereoAndMono "add"}} + (if (local.get $stereo) (then +{{- end}} +{{- if .Stereo "add"}} + call $pop ;; F: b c d P: a + call $pop ;; F: c d P: b a + call $peek2;; F: c d P: d b a + f32.add ;; F: c d P: b+d a + call $push ;; F: b+d c d P: a + call $peek2;; F: b+d c d P: c a + f32.add ;; F: b+d c d P: a+c + call $push ;; F: a+c b+d c d P: +{{- end}} +{{- if .StereoAndMono "add"}} + )(else +{{- end}} +{{- if .Mono "add"}} + (call $push (f32.add (call $pop) (call $peek))) +{{- end}} +{{- if .StereoAndMono "add"}} + )) +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .StereoAndMono "addp"}} + (if (local.get $stereo) (then +{{- end}} +{{- if .Stereo "addp"}} + call $pop ;; a + call $pop ;; b a + call $swap ;; a b + call $pop ;; c a b + f32.add ;; c+a b + call $swap ;; b c+a + call $pop ;; d b c+a + f32.add ;; d+b c+a + call $push ;; c+a + call $push +{{- end}} +{{- if .StereoAndMono "addp"}} + )(else +{{- end}} +{{- if .Mono "addp"}} + (call $push (f32.add (call $pop) (call $pop))) +{{- end}} +{{- if .StereoAndMono "addp"}} + )) +{{- end}} +) +{{end}} + + +{{- if .HasOp "loadnote"}} +;;------------------------------------------------------------------------------- +;; LOADNOTE opcode: load the current note, scaled to [-1,1] +;;------------------------------------------------------------------------------- +(func $su_op_loadnote (param $stereo i32) +{{- if .Stereo "loadnote"}} + (if (local.get $stereo) (then + (call $su_op_loadnote (i32.const 0)) + )) +{{- end}} + (f32.convert_i32_u (i32.load (global.get $voice))) + (f32.mul (f32.const 0.015625)) + (f32.sub (f32.const 1)) + (call $push) +) +{{end}} + +{{- if .HasOp "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 (param $stereo i32) +{{- if .StereoAndMono "mul"}} + (if (local.get $stereo) (then +{{- end}} +{{- if .Stereo "mul"}} + call $pop ;; F: b c d P: a + call $pop ;; F: c d P: b a + call $peek2;; F: c d P: d b a + f32.mul ;; F: c d P: b*d a + call $push ;; F: b*d c d P: a + call $peek2;; F: b*d c d P: c a + f32.mul ;; F: b*d c d P: a*c + call $push ;; F: a*c b*d c d P: +{{- end}} +{{- if .StereoAndMono "mul"}} + )(else +{{- end}} +{{- if .Mono "mul"}} + (call $push (f32.mul (call $pop) (call $peek))) +{{- end}} +{{- if .StereoAndMono "mul"}} + )) +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .StereoAndMono "mulp"}} + (if (local.get $stereo) (then +{{- end}} +{{- if .Stereo "mulp"}} + call $pop ;; a + call $pop ;; b a + call $swap ;; a b + call $pop ;; c a b + f32.mul ;; c*a b + call $swap ;; b c*a + call $pop ;; d b c*a + f32.mul ;; d*b c*a + call $push ;; c*a + call $push +{{- end}} +{{- if .StereoAndMono "mulp"}} + )(else +{{- end}} +{{- if .Mono "mulp"}} + (call $push (f32.mul (call $pop) (call $pop))) +{{- end}} +{{- if .StereoAndMono "mulp"}} + )) +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "push"}} + (if (local.get $stereo) (then + (call $push (call $peek)) + )) +{{- end}} + (call $push (call $peek)) +) +{{end}} + + +{{- if or (.HasOp "xch") (.Stereo "delay")}} +;;------------------------------------------------------------------------------- +;; 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 (param $stereo i32) + call $pop + call $pop +{{- if .StereoAndMono "xch"}} + (if (local.get $stereo) (then +{{- end}} +{{- if .Stereo "xch"}} + call $pop ;; F: d P: c b a + call $swap ;; F: d P: b c a + call $pop ;; F: P: d b c a + call $swap ;; F: P: b d c a + call $push ;; F: b P: d c a + call $push ;; F: d b P: c a + call $swap ;; F: d b P: a c + call $pop ;; F: b P: d a c + call $swap ;; F: b P: a d c + call $push ;; F: a b P: d c +{{- end}} +{{- if .StereoAndMono "xch"}} + )(else +{{- end}} +{{- if or (.Mono "xch") (.Stereo "delay")}} + call $swap +{{- end}} +{{- if .StereoAndMono "xch"}} + )) +{{- end}} + call $push + call $push +) +{{end}} diff --git a/templates/wasm/effects.wat b/templates/wasm/effects.wat new file mode 100644 index 0000000..5ce485e --- /dev/null +++ b/templates/wasm/effects.wat @@ -0,0 +1,482 @@ +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "distort"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "distort") 2}})) +{{- end}} + (call $pop) + (call $waveshaper (call $input (i32.const {{.InputNumber "distort" "drive"}}))) + (call $push) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $phase f32) +{{- if .Stereo "hold"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "hold") 2}})) +{{- end}} + (local.set $phase + (f32.sub + (f32.load (global.get $WRK)) + (f32.mul + (call $input (i32.const {{.InputNumber "hold" "holdfreq"}})) + (call $input (i32.const {{.InputNumber "hold" "holdfreq"}})) ;; if we ever implement $dup, replace with that + ) + ) + ) + (if (f32.ge (f32.const 0) (local.get $phase)) (then + (f32.store offset=4 (global.get $WRK) (call $peek)) ;; we start holding a new value + (local.set $phase (f32.add (local.get $phase) (f32.const 1))) + )) + (drop (call $pop)) ;; we replace the top most signal + (call $push (f32.load offset=4 (global.get $WRK))) ;; with the held value + (f32.store (global.get $WRK) (local.get $phase)) ;; save back new phase +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "crush"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "crush") 2}})) +{{- end}} + call $pop + (f32.div (call $input (i32.const {{.InputNumber "crush" "resolution"}}))) + f32.nearest + (f32.mul (call $input (i32.const {{.InputNumber "crush" "resolution"}}))) + call $push +) +{{end}} + + +{{- if .HasOp "gain"}} +;;------------------------------------------------------------------------------- +;; GAIN opcode: apply gain on the signal +;;------------------------------------------------------------------------------- +;; Mono: x -> x*g +;; Stereo: l r -> l*g r*g +;;------------------------------------------------------------------------------- +(func $su_op_gain (param $stereo i32) +{{- if .Stereo "gain"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "gain") 2}})) +{{- end}} + (call $push (f32.mul (call $pop) (call $input (i32.const {{.InputNumber "gain" "gain"}})))) +) +{{end}} + + +{{- if .HasOp "invgain"}} +;;------------------------------------------------------------------------------- +;; INVGAIN opcode: apply inverse gain on the signal +;;------------------------------------------------------------------------------- +;; Mono: x -> x/g +;; Stereo: l r -> l/g r/g +;;------------------------------------------------------------------------------- +(func $su_op_invgain (param $stereo i32) +{{- if .Stereo "invgain"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "invgain") 2}})) +{{- end}} + (call $push (f32.div (call $pop) (call $input (i32.const {{.InputNumber "invgain" "invgain"}})))) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $flags i32) (local $freq f32) (local $high f32) (local $low f32) (local $band f32) (local $retval f32) +{{- if .Stereo "filter"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "filter") 2}})) + (if (local.get $stereo)(then + ;; This is hacky: rewind the $VAL one byte backwards as the right channel already + ;; scanned it once. Find a way to avoid rewind + (global.set $VAL (i32.sub (global.get $VAL) (i32.const 1))) + )) +{{- end}} + (local.set $flags (call $scanValueByte)) + (local.set $freq (f32.mul + (call $input (i32.const {{.InputNumber "filter" "frequency"}})) + (call $input (i32.const {{.InputNumber "filter" "frequency"}})) + )) + (local.set $low ;; l' = f2*b + l + (f32.add ;; f2*b+l + (f32.mul ;; f2*b + (local.tee $band (f32.load offset=4 (global.get $WRK))) ;; b + (local.get $freq) ;; f2 + ) + (f32.load (global.get $WRK)) ;; l + ) + ) + (local.set $high ;; h' = x - l' - r*b + (f32.sub ;; x - l' - r*b + (f32.sub ;; x - l' + (call $pop) ;; x (signal) + (local.get $low) ;; l' + ) + (f32.mul ;; r*b + (call $input (i32.const {{.InputNumber "filter" "resonance"}})) ;; r + (local.get $band) ;; b + ) + ) + ) + (local.set $band ;; b' = f2 * h' + b + (f32.add ;; f2 * h' + b + (f32.mul ;; f2 * h' + (local.get $freq) ;; f2 + (local.get $high) ;; h' + ) + (local.get $band) ;; b + ) + ) + (local.set $retval (f32.const 0)) +{{- if .SupportsParamValue "filter" "lowpass" 1}} + (if (i32.and (local.get $flags) (i32.const 0x40)) (then + (local.set $retval (f32.add (local.get $retval) (local.get $low))) + )) +{{- end}} +{{- if .SupportsParamValue "filter" "bandpass" 1}} + (if (i32.and (local.get $flags) (i32.const 0x20)) (then + (local.set $retval (f32.add (local.get $retval) (local.get $band))) + )) +{{- end}} +{{- if .SupportsParamValue "filter" "highpass" 1}} + (if (i32.and (local.get $flags) (i32.const 0x10)) (then + (local.set $retval (f32.add (local.get $retval) (local.get $high))) + )) +{{- end}} +{{- if .SupportsParamValue "filter" "negbandpass" 1}} + (if (i32.and (local.get $flags) (i32.const 0x08)) (then + (local.set $retval (f32.sub (local.get $retval) (local.get $band))) + )) +{{- end}} +{{- if .SupportsParamValue "filter" "neghighpass" 1}} + (if (i32.and (local.get $flags) (i32.const 0x04)) (then + (local.set $retval (f32.sub (local.get $retval) (local.get $high))) + )) +{{- end}} + (f32.store (global.get $WRK) (local.get $low)) + (f32.store offset=4 (global.get $WRK) (local.get $band)) + (call $push (local.get $retval)) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "clip"}} + (call $stereoHelper (local.get $stereo) (i32.const {{div (.GetOp "clip") 2}})) +{{- end}} + (call $push (call $clip (call $pop))) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "pan"}} + (if (i32.eqz (local.get $stereo)) (then ;; this time, if this is mono op... + call $peek ;; ...we duplicate the mono into stereo first + call $push + )) + (call $pop) ;; F: r P: l + (call $pop) ;; F: P: r l + (call $input (i32.const {{.InputNumber "pan" "panning"}})) ;; F: P: p r l + f32.mul ;; F: P: p*r l + (call $push) ;; F: p*r P: l + f32.const 1 + (call $input (i32.const {{.InputNumber "pan" "panning"}})) ;; F: p*r P: p 1 l + f32.sub ;; F: p*r P: 1-p l + f32.mul ;; F: p*r P: (1-p)*l + (call $push) ;; F: (1-p)*l p*r +{{- else}} + (call $peek) ;; F: s P: s + (f32.mul + (call $input (i32.const {{.InputNumber "pan" "panning"}})) + (call $pop) + ) ;; F: P: p*s s + (call $push) ;; F: p*s P: s + (call $peek) ;; F: p*s P: p*s s + f32.sub ;; F: p*s P: s-p*s + (call $push) ;; F: (1-p)*s p*s +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $delayIndex i32) (local $delayCount i32) (local $output f32) (local $s f32) (local $filtstate f32) +{{- if .Stereo "delay"}} (local $delayCountStash i32) {{- end}} +{{- if or (.SupportsModulation "delay" "delaytime") (.SupportsParamValue "delay" "notetracking" 1)}} (local $delayTime f32) {{- end}} + (local.set $delayIndex (i32.mul (call $scanValueByte) (i32.const 2))) +{{- if .Stereo "delay"}} + (local.set $delayCountStash (call $scanValueByte)) + (if (local.get $stereo)(then + (call $su_op_xch (i32.const 0)) + )) + loop $stereoLoop + (local.set $delayCount (local.get $delayCountStash)) +{{- else}} + (local.set $delayCount (call $scanValueByte)) +{{- end}} + (local.set $output (f32.mul + (call $input (i32.const {{.InputNumber "delay" "dry"}})) + (call $peek) + )) + loop $delayLoop + (local.tee $s (f32.load offset=12 + (i32.add ;; delayWRK + ((globalTick-delaytimes[delayIndex])&65535)*4 + (i32.mul ;; ((globalTick-delaytimes[delayIndex])&65535)*4 + (i32.and ;; (globalTick-delaytimes[delayIndex])&65535 + (i32.sub ;; globalTick-delaytimes[delayIndex] + (global.get $globaltick) +{{- if or (.SupportsModulation "delay" "delaytime") (.SupportsParamValue "delay" "notetracking" 1)}} ;; delaytime modulation or note syncing require computing the delay time in floats + (local.set $delayTime (f32.convert_i32_u (i32.load16_u + offset={{index .Labels "su_delay_times"}} + (local.get $delayIndex) + ))) +{{- if .SupportsParamValue "delay" "notetracking" 1}} + (if (i32.eqz (i32.and (local.get $delayCount) (i32.const 1)))(then + (local.set $delayTime (f32.div + (local.get $delayTime) + (call $pow2 + (f32.mul + (f32.convert_i32_u (i32.load (global.get $voice))) + (f32.const 0.08333333) + ) + ) + )) + )) +{{- end}} +{{- if .SupportsModulation "delay" "delaytime"}} + (i32.trunc_f32_u (f32.add + (f32.add + (local.get $delayTime) + (f32.mul + (f32.load offset={{.InputNumber "delay" "delaytime" | mul 4 | add 32}} (global.get $WRK)) + (f32.const 32767) + ) + ) + (f32.const 0.5) + )) +{{- else}} + (i32.trunc_f32_u (f32.add (local.get $delayTime) (f32.const 0.5))) +{{- end}} +{{- else}} + (i32.load16_u + offset={{index .Labels "su_delay_times"}} + (local.get $delayIndex) + ) +{{- end}} + ) + (i32.const 65535) + ) + (i32.const 4) + ) + (global.get $delayWRK) + ) + )) + (local.set $output (f32.add (local.get $output))) + (f32.store + (global.get $delayWRK) + (local.tee $filtstate + (f32.add + (f32.mul + (f32.sub + (f32.load (global.get $delayWRK)) + (local.get $s) + ) + (call $input (i32.const {{.InputNumber "delay" "damp"}})) + ) + (local.get $s) + ) + ) + ) + (f32.store offset=12 + (i32.add ;; delayWRK + globalTick*4 + (i32.mul ;; globalTick)&65535)*4 + (i32.and ;; globalTick&65535 + (global.get $globaltick) + (i32.const 65535) + ) + (i32.const 4) + ) + (global.get $delayWRK) + ) + (f32.add + (f32.mul + (call $input (i32.const {{.InputNumber "delay" "feedback"}})) + (local.get $filtstate) + ) + (f32.mul + (f32.mul + (call $input (i32.const {{.InputNumber "delay" "pregain"}})) + (call $input (i32.const {{.InputNumber "delay" "pregain"}})) + ) + (call $peek) + ) + ) + ) + (global.set $delayWRK (i32.add (global.get $delayWRK) (i32.const 262156))) + (local.set $delayIndex (i32.add (local.get $delayIndex) (i32.const 2))) + (br_if $delayLoop (i32.gt_s (local.tee $delayCount (i32.sub (local.get $delayCount) (i32.const 2))) (i32.const 0))) + end + (f32.store offset=4 + (global.get $delayWRK) + (local.tee $filtstate + (f32.add + (local.get $output) + (f32.sub + (f32.mul + (f32.const 0.99609375) + (f32.load offset=4 (global.get $delayWRK)) + ) + (f32.load offset=8 (global.get $delayWRK)) + ) + ) + ) + ) + (f32.store offset=8 + (global.get $delayWRK) + (local.get $output) + ) + (drop (call $pop)) + (call $push (local.get $filtstate)) +{{- if .Stereo "delay"}} + (call $su_op_xch (i32.const 0)) + (br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo))))) + end + (call $su_op_xch (i32.const 0)) +{{- end}} +{{- if .SupportsModulation "delay" "delaytime"}} + (f32.store offset={{.InputNumber "delay" "delaytime" | mul 4 | add 32}} (global.get $WRK) (f32.const 0)) +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $x2 f32) (local $level f32) (local $t2 f32) + (call $push (f32.div ;; the inverse gain is applied on this signal, even if the gain is side-chained somewhere else + (call $pop) + (call $input (i32.const {{.InputNumber "compressor" "invgain"}})) + )) +{{- if .Stereo "compressor"}} + (local.set $x2 (f32.mul + (call $peek) + (call $peek) + )) + (if (local.get $stereo)(then + (call $pop) + (call $push (f32.div + (call $pop) + (call $input (i32.const {{.InputNumber "compressor" "invgain"}})) + )) + (local.set $x2 (f32.add + (local.get $x2) + (f32.mul + (call $peek) + (call $peek) + ) + )) + (call $push) + )) + (local.get $x2) +{{- else}} + (local.tee $x2 (f32.mul + (call $peek) + (call $peek) + )) +{{- end}} + (local.tee $level (f32.load (global.get $WRK))) + f32.lt + call $nonLinearMap ;; $nonlinearMap(x^2 $threshold, note the local.tees + (call $push + (call $pow ;; (t^2/l)^(r/2) + (f32.div ;; t^2/l + (local.get $t2) + (local.get $level) + ) + (f32.mul ;; r/2 + (call $input (i32.const {{.InputNumber "compressor" "ratio"}})) ;; r + (f32.const 0.5) ;; 0.5 + ) + ) + ) + )(else + (call $push (f32.const 1)) ;; unity gain if we are below threshold + )) +{{- if .Stereo "compressor"}} + (if (local.get $stereo)(then + (call $push (call $peek)) + )) +{{- end}} + (f32.store (global.get $WRK) (local.get $level)) ;; save the updated levels +) +{{- end}} diff --git a/templates/wasm/output_sound.wat b/templates/wasm/output_sound.wat new file mode 100644 index 0000000..9e29b30 --- /dev/null +++ b/templates/wasm/output_sound.wat @@ -0,0 +1,19 @@ +{{- if not .Song.Output16Bit }} + (i64.store (global.get $outputBufPtr) (i64.load (i32.const 4128))) ;; load the sample from left & right channels as one 64bit int and store it in the address pointed by outputBufPtr + (global.set $outputBufPtr (i32.add (global.get $outputBufPtr) (i32.const 8))) ;; advance outputbufptr +{{- else }} + (local.set $channel (i32.const 0)) + loop $channelLoop + (i32.store16 (global.get $outputBufPtr) (i32.trunc_f32_s + (f32.mul + (call $clip + (f32.load offset=4128 (i32.mul (local.get $channel) (i32.const 4))) + ) + (f32.const 32767) + ) + )) + (global.set $outputBufPtr (i32.add (global.get $outputBufPtr) (i32.const 2))) + (br_if $channelLoop (local.tee $channel (i32.eqz (local.get $channel)))) + end +{{- end }} + (i64.store (i32.const 4128) (i64.const 0)) ;; clear the left and right ports \ No newline at end of file diff --git a/templates/wasm/patch.wat b/templates/wasm/patch.wat new file mode 100644 index 0000000..ffc82d1 --- /dev/null +++ b/templates/wasm/patch.wat @@ -0,0 +1,144 @@ +;;------------------------------------------------------------------------------- +;; su_run_vm function: runs the entire virtual machine once, creating 1 sample +;;------------------------------------------------------------------------------- +(func $su_run_vm (local $opcodeWithStereo i32) (local $opcode i32) (local $paramNum i32) (local $paramX4 i32) (local $WRKplusparam i32) + loop $vm_loop + (local.set $opcodeWithStereo (i32.load8_u (global.get $COM))) + (global.set $COM (i32.add (global.get $COM) (i32.const 1))) ;; move to next instruction + (global.set $WRK (i32.add (global.get $WRK) (i32.const 64))) ;; move WRK to next unit + (if (local.tee $opcode (i32.shr_u (local.get $opcodeWithStereo) (i32.const 1)))(then ;; if $opcode = $opcodeStereo >> 1; $opcode != 0 { + (local.set $paramNum (i32.const 0)) + (local.set $paramX4 (i32.const 0)) + loop $transform_values_loop + {{- $addr := sub (index .Labels "su_vm_transformcounts") 1}} + (if (i32.lt_u (local.get $paramNum) (i32.load8_u offset={{$addr}} (local.get $opcode)))(then ;;(i32.ge (local.get $paramNum) (i32.load8_u (local.get $opcode))) /*TODO: offset to transformvalues + (local.set $WRKplusparam (i32.add (global.get $WRK) (local.get $paramX4))) + (f32.store offset=512 + (local.get $paramX4) + (f32.add + (f32.mul + (f32.convert_i32_u (call $scanValueByte)) + (f32.const 0.0078125) ;; scale from 0-128 to 0.0 - 1.0 + ) + (f32.load offset=32 (local.get $WRKplusparam)) ;; add modulation + ) + ) + (f32.store offset=32 (local.get $WRKplusparam) (f32.const 0.0)) ;; clear modulations + (local.set $paramNum (i32.add (local.get $paramNum) (i32.const 1))) ;; $paramNum++ + (local.set $paramX4 (i32.add (local.get $paramX4) (i32.const 4))) + br $transform_values_loop ;; continue looping + )) + ;; paramNum was >= the number of parameters to transform, exiting loop + end + (call_indirect (type $opcode_func_signature) (i32.and (local.get $opcodeWithStereo) (i32.const 1)) (local.get $opcode)) + )(else ;; advance to next voice + (global.set $voice (i32.add (global.get $voice) (i32.const 4096))) ;; advance to next voice + (global.set $WRK (global.get $voice)) ;; set WRK point to beginning of voice + (global.set $voicesRemain (i32.sub (global.get $voicesRemain) (i32.const 1))) +{{- if .SupportsPolyphony}} + (if (i32.and (i32.shr_u (i32.const {{.PolyphonyBitmask | printf "%v"}}) (global.get $voicesRemain)) (i32.const 1))(then + (global.set $VAL (global.get $VAL_instr_start)) + (global.set $COM (global.get $COM_instr_start)) + )) + (global.set $VAL_instr_start (global.get $VAL)) + (global.set $COM_instr_start (global.get $COM)) +{{- end}} + (br_if 2 (i32.eqz (global.get $voicesRemain))) ;; if no more voices remain, return from function + )) + br $vm_loop + end +) + +{{- template "arithmetic.wat" .}} +{{- template "effects.wat" .}} +{{- template "sources.wat" .}} +{{- template "sinks.wat" .}} + +;;------------------------------------------------------------------------------- +;; $input returns the float value of a transformed to 0.0 - 1.0f range. +;; The transformed values start at 512 (TODO: change magic constants somehow) +;;------------------------------------------------------------------------------- +(func $input (param $inputNumber i32) (result f32) + (f32.load offset=512 (i32.mul (local.get $inputNumber) (i32.const 4))) +) + +;;------------------------------------------------------------------------------- +;; $inputSigned returns the float value of a transformed to -1.0 - 1.0f range. +;;------------------------------------------------------------------------------- +(func $inputSigned (param $inputNumber i32) (result f32) + (f32.sub (f32.mul (call $input (local.get $inputNumber)) (f32.const 2)) (f32.const 1)) +) + +;;------------------------------------------------------------------------------- +;; $nonLinearMap: x -> 2^(-24*input[x]) +;;------------------------------------------------------------------------------- +(func $nonLinearMap (param $value i32) (result f32) + (call $pow2 + (f32.mul + (f32.const -24) + (call $input (local.get $value)) + ) + ) +) + +;;------------------------------------------------------------------------------- +;; $pow2: x -> 2^x +;;------------------------------------------------------------------------------- +(func $pow2 (param $value f32) (result f32) + (call $pow (f32.const 2) (local.get $value)) +) + +;;------------------------------------------------------------------------------- +;; Waveshaper(x,a): "distorts" signal x by amount a +;; Returns x*a/(1-a+(2*a-1)*abs(x)) +;;------------------------------------------------------------------------------- +(func $waveshaper (param $signal f32) (param $amount f32) (result f32) + (local.set $signal (call $clip (local.get $signal))) + (f32.mul + (local.get $signal) + (f32.div + (local.get $amount) + (f32.add + (f32.const 1) + (f32.sub + (f32.mul + (f32.sub + (f32.add (local.get $amount) (local.get $amount)) + (f32.const 1) + ) + (f32.abs (local.get $signal)) + ) + (local.get $amount) + ) + ) + ) + ) +) + +;;------------------------------------------------------------------------------- +;; Clip(a : f32) returns min(max(a,-1),1) +;;------------------------------------------------------------------------------- +(func $clip (param $value f32) (result f32) + (f32.min (f32.max (local.get $value) (f32.const -1.0)) (f32.const 1.0)) +) + +(func $stereoHelper (param $stereo i32) (param $tableIndex i32) + (if (local.get $stereo)(then + (call $pop) + (global.set $WRK (i32.add (global.get $WRK) (i32.const 16))) + (call_indirect (type $opcode_func_signature) (i32.const 0) (local.get $tableIndex)) + (global.set $WRK (i32.sub (global.get $WRK) (i32.const 16))) + (call $push) + )) +) + +;;------------------------------------------------------------------------------- +;; 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. +;;------------------------------------------------------------------------------- +(table {{.Instructions | len | add 1}} anyfunc) +(elem (i32.const 1) ;; start the indices at 1, as 0 is reserved for advance +{{- range .Instructions}} + $su_op_{{.}} +{{- end}} +) diff --git a/templates/wasm/player.wat b/templates/wasm/player.wat new file mode 100644 index 0000000..9497b96 --- /dev/null +++ b/templates/wasm/player.wat @@ -0,0 +1,288 @@ +(module + +{{- /* +;------------------------------------------------------------------------------ +; Patterns +;------------------------------------------------------------------------------- +*/}} +{{- .SetLabel "su_patterns"}} +{{- $m := .}} +{{- range .Song.Patterns}} + {{- range .}} + {{- $.DataB .}} + {{- end}} +{{- end}} + +{{- /* +;------------------------------------------------------------------------------ +; Tracks +;------------------------------------------------------------------------------- +*/}} +{{- .SetLabel "su_tracks"}} +{{- $m := .}} +{{- range .Song.Tracks}} + {{- range .Sequence}} + {{- $.DataB .}} + {{- end}} +{{- end}} + +{{- /* +;------------------------------------------------------------------------------ +; The code for this patch, basically indices to vm jump table +;------------------------------------------------------------------------------- +*/}} +{{- .SetLabel "su_patch_code"}} +{{- range .Commands}} +{{- $.DataB .}} +{{- end}} + +{{- /* +;------------------------------------------------------------------------------- +; The parameters / inputs to each opcode +;------------------------------------------------------------------------------- +*/}} +{{- .SetLabel "su_patch_parameters"}} +{{- range .Values}} +{{- $.DataB .}} +{{- end}} + +{{- /* +;------------------------------------------------------------------------------- +; Delay times +;------------------------------------------------------------------------------- +*/}} +{{- .SetLabel "su_delay_times"}} +{{- range .DelayTimes}} +{{- $.DataW .}} +{{- end}} + +{{- /* +;------------------------------------------------------------------------------- +; The number of transformed parameters each opcode takes +;------------------------------------------------------------------------------- +*/}} +{{- .SetLabel "su_vm_transformcounts"}} +{{- range .Instructions}} +{{- $.TransformCount . | $.ToByte | $.DataB}} +{{- end}} + +;;------------------------------------------------------------------------------ +;; Import the difficult math functions from javascript +;; (seriously now, it's 2020) +;;------------------------------------------------------------------------------ +(func $pow (import "m" "pow") (param f32) (param f32) (result f32)) +(func $log2 (import "m" "log2") (param f32) (result f32)) +(func $sin (import "m" "sin") (param f32) (result f32)) + +;;------------------------------------------------------------------------------ +;; Types. Only useful to define the jump table type, which is +;; (int stereo) void +;;------------------------------------------------------------------------------ +(type $opcode_func_signature (func (param i32))) + +;;------------------------------------------------------------------------------ +;; The one and only memory +;; TODO: Its size should be calculated just to fit, but not more +;;------------------------------------------------------------------------------ +(memory (export "m") 256) + +;;------------------------------------------------------------------------------ +;; Globals. Putting all with same initialization value should compress most +;;------------------------------------------------------------------------------ +(global $WRK (mut i32) (i32.const 0)) +(global $COM (mut i32) (i32.const 0)) +(global $VAL (mut i32) (i32.const 0)) +{{- if .SupportsPolyphony}} +(global $COM_instr_start (mut i32) (i32.const 0)) +(global $VAL_instr_start (mut i32) (i32.const 0)) +{{- end}} +{{- if .HasOp "delay"}} +(global $delayWRK (mut i32) (i32.const 0)) +{{- end}} +(global $globaltick (mut i32) (i32.const 0)) +(global $row (mut i32) (i32.const 0)) +(global $pattern (mut i32) (i32.const 0)) +(global $sample (mut i32) (i32.const 0)) +(global $voice (mut i32) (i32.const 0)) +(global $voicesRemain (mut i32) (i32.const 0)) +(global $randseed (mut i32) (i32.const 1)) +(global $sp (mut i32) (i32.const 2048)) +(global $outputBufPtr (mut i32) (i32.const 8388608)) +;; TODO: only export start and length with certain compiler options; in demo use, they can be hard coded +;; in the intro +(global $outputStart (export "s") i32 (i32.const 8388608)) ;; TODO: do not hard code, layout memory somehow intelligently +(global $outputLength (export "l") i32 (i32.const {{if .Song.Output16Bit}}{{mul .Song.TotalRows .Song.SamplesPerRow 4}}{{else}}{{mul .Song.TotalRows .Song.SamplesPerRow 8}}{{end}})) +(global $output16bit (export "t") i32 (i32.const {{if .Song.Output16Bit}}1{{else}}0{{end}})) + + +;;------------------------------------------------------------------------------ +;; Functions to emulate FPU stack in software +;;------------------------------------------------------------------------------ +(func $peek (result f32) + (f32.load (global.get $sp)) +) + +(func $peek2 (result f32) + (f32.load offset=4 (global.get $sp)) +) + +(func $pop (result f32) + (call $peek) + (global.set $sp (i32.add (global.get $sp) (i32.const 4))) +) + +(func $push (param $value f32) + (global.set $sp (i32.sub (global.get $sp) (i32.const 4))) + (f32.store (global.get $sp) (local.get $value)) +) + +;;------------------------------------------------------------------------------ +;; Helper functions +;;------------------------------------------------------------------------------ +(func $swap (param f32 f32) (result f32 f32) ;; x,y -> y,x + local.get 1 + local.get 0 +) + +(func $scanValueByte (result i32) ;; scans positions $VAL for a byte, incrementing $VAL afterwards + (i32.load8_u (global.get $VAL)) ;; in other words: returns byte [$VAL++] + (global.set $VAL (i32.add (global.get $VAL) (i32.const 1))) ;; $VAL++ +) + +;;------------------------------------------------------------------------------ +;; "Entry point" for the player +;;------------------------------------------------------------------------------ +(start $render) ;; we run render automagically when the module is instantiated + +(func $render (param) +{{- if .Song.Output16Bit }} (local $channel i32) {{- end }} + loop $pattern_loop + (global.set $row (i32.const 0)) + loop $row_loop + (call $su_update_voices) + (global.set $sample (i32.const 0)) + loop $sample_loop + (global.set $COM (i32.const {{index .Labels "su_patch_code"}})) + (global.set $VAL (i32.const {{index .Labels "su_patch_parameters"}})) +{{- if .SupportsPolyphony}} + (global.set $COM_instr_start (global.get $COM)) + (global.set $VAL_instr_start (global.get $VAL)) +{{- end}} + (global.set $WRK (i32.const 4160)) + (global.set $voice (i32.const 4160)) + (global.set $voicesRemain (i32.const {{.Song.Patch.TotalVoices | printf "%v"}})) +{{- if .HasOp "delay"}} + (global.set $delayWRK (i32.const 262144)) ;; BAD IDEA: we are limited to something like 30 delay lines + ;; after that, the delay lines start to overwrite the outputbuffer. Find a way to layout the memory + ;; based on the song, instead of hard coding addressed. +{{- end}} + (call $su_run_vm) + {{- template "output_sound.wat" .}} + (global.set $sample (i32.add (global.get $sample) (i32.const 1))) + (global.set $globaltick (i32.add (global.get $globaltick) (i32.const 1))) + (br_if $sample_loop (i32.lt_s (global.get $sample) (i32.const {{.Song.SamplesPerRow}}))) + end + (global.set $row (i32.add (global.get $row) (i32.const 1))) + (br_if $row_loop (i32.lt_s (global.get $row) (i32.const {{.Song.PatternRows}}))) + end + (global.set $pattern (i32.add (global.get $pattern) (i32.const 1))) + (br_if $pattern_loop (i32.lt_s (global.get $pattern) (i32.const {{.Song.SequenceLength}}))) + end +) + +{{- if ne .VoiceTrackBitmask 0}} +;; the complex implementation of update_voices: at least one track has more than one voice +(func $su_update_voices (local $si i32) (local $di i32) (local $tracksRemaining i32) (local $note i32) (local $firstVoice i32) (local $nextTrackStartsAt i32) (local $numVoices i32) (local $voiceNo i32) + (local.set $tracksRemaining (i32.const {{len .Song.Tracks}})) + (local.set $si (global.get $pattern)) + (local.set $nextTrackStartsAt (i32.const 0)) + loop $track_loop + (local.set $numVoices (i32.const 0)) + (local.set $firstVoice (local.get $nextTrackStartsAt)) + loop $voiceLoop + (i32.and + (i32.shr_u + (i32.const {{.VoiceTrackBitmask | printf "%v"}}) + (local.get $nextTrackStartsAt) + ) + (i32.const 1) + ) + (local.set $nextTrackStartsAt (i32.add (local.get $nextTrackStartsAt) (i32.const 1))) + (local.set $numVoices (i32.add (local.get $numVoices) (i32.const 1))) + br_if $voiceLoop + end + (i32.load8_u offset={{index .Labels "su_tracks"}} (local.get $si)) + (i32.mul (i32.const {{.Song.PatternRows}})) + (i32.add (global.get $row)) + (i32.load8_u offset={{index .Labels "su_patterns"}}) + (local.tee $note) + (if (i32.ne (i32.const {{.Song.Hold}}))(then + (i32.store offset=4164 + (i32.mul + (i32.add + (local.tee $voiceNo (i32.load8_u offset=768 (local.get $tracksRemaining))) + (local.get $firstVoice) + ) + (i32.const 4096) + ) + (i32.const 1) + ) ;; release the note + (if (i32.gt_u (local.get $note) (i32.const {{.Song.Hold}}))(then + (local.set $di (i32.add + (i32.mul + (i32.add + (local.tee $voiceNo (i32.rem_u + (i32.add (local.get $voiceNo) (i32.const 1)) + (local.get $numVoices) + )) + (local.get $firstVoice) + ) + (i32.const 4096) + ) + (i32.const 4160) + )) + (memory.fill (local.get $di) (i32.const 0) (i32.const 4096)) + (i32.store (local.get $di) (local.get $note)) + (i32.store8 offset=768 (local.get $tracksRemaining) (local.get $voiceNo)) + )) + )) + (local.set $si (i32.add (local.get $si) (i32.const {{.Song.SequenceLength}}))) + (br_if $track_loop (local.tee $tracksRemaining (i32.sub (local.get $tracksRemaining) (i32.const 1)))) + end +) + +{{- else}} +;; the simple implementation of update_voices: each track has exactly one voice +(func $su_update_voices (local $si i32) (local $di i32) (local $tracksRemaining i32) (local $note i32) + (local.set $tracksRemaining (i32.const {{len .Song.Tracks}})) + (local.set $si (global.get $pattern)) + (local.set $di (i32.const 4160)) + loop $track_loop + (i32.load8_u offset={{index .Labels "su_tracks"}} (local.get $si)) + (i32.mul (i32.const {{.Song.PatternRows}})) + (i32.add (global.get $row)) + (i32.load8_u offset={{index .Labels "su_patterns"}}) + (local.tee $note) + (if (i32.ne (i32.const {{.Song.Hold}}))(then + (i32.store offset=4 (local.get $di) (i32.const 1)) ;; release the note + (if (i32.gt_u (local.get $note) (i32.const {{.Song.Hold}}))(then + (memory.fill (local.get $di) (i32.const 0) (i32.const 4096)) + (i32.store (local.get $di) (local.get $note)) + )) + )) + (local.set $di (i32.add (local.get $di) (i32.const 4096))) + (local.set $si (i32.add (local.get $si) (i32.const {{.Song.SequenceLength}}))) + (br_if $track_loop (local.tee $tracksRemaining (i32.sub (local.get $tracksRemaining) (i32.const 1)))) + end +) +{{- end}} + +{{template "patch.wat" .}} + + +;; All data is collected into a byte buffer and emitted at once +(data (i32.const 0) "{{range .Data}}\{{. | printf "%02x"}}{{end}}") + +;;(data (i32.const 8388610) "\52\49\46\46\b2\eb\0c\20\57\41\56\45\66\6d\74\20\12\20\20\20\03\20\02\20\44\ac\20\20\20\62\05\20\08\20\20\20\20\20\66\61\63\74\04\20\20\20\e0\3a\03\20\64\61\74\61\80\eb\0c\20") + +) ;; END MODULE diff --git a/templates/wasm/sinks.wat b/templates/wasm/sinks.wat new file mode 100644 index 0000000..7ea4310 --- /dev/null +++ b/templates/wasm/sinks.wat @@ -0,0 +1,198 @@ +{{- if .HasOp "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 (param $stereo i32) (local $addr i32) + (local.set $addr (i32.const 4128)) +{{- if .Stereo "outaux"}} + loop $stereoLoop +{{- end}} + (f32.store ;; send + (local.get $addr) + (f32.add + (f32.mul + (call $peek) + (call $input (i32.const {{.InputNumber "outaux" "outgain"}})) + ) + (f32.load (local.get $addr)) + ) + ) + (f32.store offset=8 + (local.get $addr) + (f32.add + (f32.mul + (call $pop) + (call $input (i32.const {{.InputNumber "outaux" "auxgain"}})) + ) + (f32.load offset=8 (local.get $addr)) + ) + ) +{{- if .Stereo "outaux"}} + (local.set $addr (i32.add (local.get $addr) (i32.const 4))) + (br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo))))) + end +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $addr i32) + (local.set $addr (i32.add (i32.mul (call $scanValueByte) (i32.const 4)) (i32.const 4128))) +{{- if .Stereo "aux"}} + loop $stereoLoop +{{- end}} + (f32.store + (local.get $addr) + (f32.add + (f32.mul + (call $pop) + (call $input (i32.const {{.InputNumber "aux" "gain"}})) + ) + (f32.load (local.get $addr)) + ) + ) +{{- if .Stereo "aux"}} + (local.set $addr (i32.add (local.get $addr) (i32.const 4))) + (br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo))))) + end +{{- end}} +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $address i32) (local $scaledAddress i32) + (local.set $address (i32.add (call $scanValueByte) (i32.shl (call $scanValueByte) (i32.const 8)))) + (if (i32.eqz (i32.and (local.get $address) (i32.const 8)))(then +{{- if .Stereo "send"}} + (if (local.get $stereo)(then + (call $push (call $peek2)) + (call $push (call $peek2)) + )(else +{{- end}} + (call $push (call $peek)) +{{- if .Stereo "send"}} + )) +{{- end}} + )) +{{- if .Stereo "send"}} + loop $stereoLoop +{{- end}} + (local.set $scaledAddress (i32.add (i32.mul (i32.and (local.get $address) (i32.const 0x7FF7)) (i32.const 4)) +{{- if .SupportsParamValueOtherThan "send" "voice" 0}} + (select + (i32.const 4096) +{{- end}} + (global.get $voice) +{{- if .SupportsParamValueOtherThan "send" "voice" 0}} + (i32.and (local.get $address)(i32.const 0x8000)) + ) +{{- end}} + )) + (f32.store offset=32 + (local.get $scaledAddress) + (f32.add + (f32.load offset=32 (local.get $scaledAddress)) + (f32.mul + (call $inputSigned (i32.const {{.InputNumber "send" "amount"}})) + (call $pop) + ) + ) + ) + {{- if .Stereo "send"}} + (local.set $address (i32.add (local.get $address) (i32.const 1))) + (br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo))))) + end + {{- end}} +) +{{end}} + + + +{{- if .HasOp "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 (param $stereo i32) (local $ptr i32) + (local.set $ptr (i32.const 4128)) ;; synth.left, but should not be magic constant + (f32.store (local.get $ptr) + (f32.add + (f32.mul + (call $pop) + (call $input (i32.const {{.InputNumber "out" "gain"}})) + ) + (f32.load (local.get $ptr)) + ) + ) + (local.set $ptr (i32.const 4132)) ;; synth.right, but should not be magic constant + (f32.store (local.get $ptr) + (f32.add + (f32.mul + (call $pop) + (call $input (i32.const {{.InputNumber "out" "gain"}})) + ) + (f32.load (local.get $ptr)) + ) + ) +) +{{end}} + +{{- if .HasOp "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 (param $stereo i32) (local $r f32) (local $w i32) + (f32.store + (global.get $WRK) + (local.tee $r + (f32.sub + (local.tee $r + (f32.add + (f32.load (global.get $WRK)) + (f32.sub + (call $pow2 + (f32.mul + (call $pop) + (f32.const 2.206896551724138) + ) + ) + (f32.const 1) + ) + ) + ) + (f32.convert_i32_s + (local.tee $w (i32.trunc_f32_s (local.get $r))) ;; note: small difference from x86, as this is trunc; x86 rounds to nearest) + ) + ) + ) + ) + (global.set $sample (i32.add (global.get $sample) (local.get $w))) +) +{{end}} diff --git a/templates/wasm/sources.wat b/templates/wasm/sources.wat new file mode 100644 index 0000000..aa13f9d --- /dev/null +++ b/templates/wasm/sources.wat @@ -0,0 +1,336 @@ +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "loadval"}} + (if (local.get $stereo) (then + (call $su_op_loadval (i32.const 0)) + )) +{{- end}} + (f32.sub (call $input (i32.const {{.InputNumber "loadval" "value"}})) (f32.const 0.5)) + (f32.mul (f32.const 2.0)) + (call $push) +) +{{end}} + + +{{if .HasOp "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 (param $stereo i32) (local $state i32) (local $level f32) (local $delta f32) + (if (i32.load offset=4 (global.get $voice)) (then ;; if voice.release > 0 + (i32.store (global.get $WRK) (i32.const {{.InputNumber "envelope" "release"}})) ;; set envelope state to release + )) + (local.set $state (i32.load (global.get $WRK))) + (local.set $level (f32.load offset=4 (global.get $WRK))) + (local.set $delta (call $nonLinearMap (local.get $state))) + (if (local.get $state) (then + (if (i32.eq (local.get $state) (i32.const 1))(then ;; state is 1 aka decay + (local.set $level (f32.sub (local.get $level) (local.get $delta))) + (if (f32.le (local.get $level) (call $input (i32.const 2)))(then + (local.set $level (call $input (i32.const 2))) + (local.set $state (i32.const {{.InputNumber "envelope" "sustain"}})) + )) + )) + (if (i32.eq (local.get $state) (i32.const {{.InputNumber "envelope" "release"}}))(then ;; state is 3 aka release + (local.set $level (f32.sub (local.get $level) (local.get $delta))) + (if (f32.le (local.get $level) (f32.const 0)) (then + (local.set $level (f32.const 0)) + )) + )) + )(else ;; the state is 0 aka attack + (local.set $level (f32.add (local.get $level) (local.get $delta))) + (if (f32.ge (local.get $level) (f32.const 1))(then + (local.set $level (f32.const 1)) + (local.set $state (i32.const 1)) + )) + )) + (i32.store (global.get $WRK) (local.get $state)) + (f32.store offset=4 (global.get $WRK) (local.get $level)) + (call $push (f32.mul (local.get $level) (call $input (i32.const {{.InputNumber "envelope" "gain"}})))) +{{- if .Stereo "envelope"}} + (if (local.get $stereo)(then + (call $push (call $peek)) + )) +{{- end}} +) +{{end}} + + +{{- if .HasOp "noise"}} +;;------------------------------------------------------------------------------- +;; NOISE opcode: creates noise +;;------------------------------------------------------------------------------- +;; Mono: push a random value [-1,1] value on stack +;; Stereo: push two (different) random values on stack +;;------------------------------------------------------------------------------- +(func $su_op_noise (param $stereo i32) +{{- if .Stereo "noise" }} + (if (local.get $stereo) (then + (call $su_op_noise (i32.const 0)) + )) +{{- end}} + (global.set $randseed (i32.mul (global.get $randseed) (i32.const 16007))) + (f32.mul + (call $waveshaper + ;; Note: in x86 code, the constant looks like a positive integer, but has actually the MSB set i.e. is considered negative by the FPU. This tripped me big time. + (f32.div (f32.convert_i32_s (global.get $randseed)) (f32.const -2147483648)) + (call $input (i32.const {{.InputNumber "noise" "shape"}})) + ) + (call $input (i32.const {{.InputNumber "noise" "gain"}})) + ) + (call $push) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $flags i32) (local $detune f32) (local $phase f32) (local $color f32) (local $amplitude f32) +{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}} + (local $unison i32) (local $WRK_stash i32) (local $detune_stash f32) +{{- end}} + (local.set $flags (call $scanValueByte)) + (local.set $detune (call $inputSigned (i32.const {{.InputNumber "oscillator" "detune"}}))) +{{- if .Stereo "oscillator"}} + loop $stereoLoop +{{- end}} +{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}} + (local.set $unison (i32.add (i32.and (local.get $flags) (i32.const 3)) (i32.const 1))) + (local.set $WRK_stash (global.get $WRK)) + (local.set $detune_stash (local.get $detune)) + (call $push (f32.const 0)) + loop $unisonLoop +{{- end}} + (f32.store ;; update phase + (global.get $WRK) + ;; Transpose calculation starts + (f32.div + (call $inputSigned (i32.const {{.InputNumber "oscillator" "transpose"}})) + (f32.const 0.015625) + ) ;; scale back to 0 - 128 + (f32.add (local.get $detune)) ;; add detune. detune is -1 to 1 so can detune a full note up or down at max + (f32.add (select + (f32.const 0) + (f32.convert_i32_u (i32.load (global.get $voice))) + (i32.and (local.get $flags) (i32.const 0x8)) + )) ;; if lfo is not enabled, add the note number to it + (f32.mul (f32.const 0.0833333)) ;; /12, in full octaves + (call $pow2) + (f32.mul (select + (f32.const 0.000038) ;; pretty random scaling constant to get LFOs into reasonable range. Historical reasons, goes all the way back to 4klang + (f32.const 0.000092696138) ;; scaling constant to get middle-C to where it should be + (i32.and (local.get $flags) (i32.const 0x8)) + )) + (f32.add (f32.load (global.get $WRK))) ;; add the current phase of the oscillator + ) + (f32.add (f32.load (global.get $WRK)) (call $input (i32.const {{.InputNumber "oscillator" "phase"}}))) + (local.set $phase (f32.sub (local.tee $phase) (f32.floor (local.get $phase)))) ;; phase = phase mod 1.0 + (local.set $color (call $input (i32.const {{.InputNumber "oscillator" "color"}}))) +{{- if .SupportsParamValue "oscillator" "type" .Sine}} + (if (i32.and (local.get $flags) (i32.const 0x40)) (then + (local.set $amplitude (call $oscillator_sine (local.get $phase) (local.get $color))) + )) +{{- end}} +{{- if .SupportsParamValue "oscillator" "type" .Trisaw}} + (if (i32.and (local.get $flags) (i32.const 0x20)) (then + (local.set $amplitude (call $oscillator_trisaw (local.get $phase) (local.get $color))) + )) +{{- end}} +{{- if .SupportsParamValue "oscillator" "type" .Pulse}} + (if (i32.and (local.get $flags) (i32.const 0x10)) (then + (local.set $amplitude (call $oscillator_pulse (local.get $phase) (local.get $color))) + )) +{{- end}} +{{- if .SupportsParamValue "oscillator" "type" .Gate}} + (if (i32.and (local.get $flags) (i32.const 0x04)) (then + (local.set $amplitude (call $oscillator_gate (local.get $phase))) + ;; wave shaping is skipped with gate + )(else + (local.set $amplitude (call $waveshaper (local.get $amplitude) (call $input (i32.const {{.InputNumber "oscillator" "shape"}})))) + )) + (local.get $amplitude) +{{- else}} + (call $waveshaper (local.get $amplitude) (call $input (i32.const {{.InputNumber "oscillator" "shape"}}))) +{{- end}} + (call $push (f32.mul + (call $input (i32.const {{.InputNumber "oscillator" "gain"}})) + )) +{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}} + (call $push (f32.add (call $pop) (call $pop))) + (if (local.tee $unison (i32.sub (local.get $unison) (i32.const 1)))(then + (f32.store offset={{.InputNumber "oscillator" "phase" | mul 4 | add 512}} (i32.const 0) + (f32.add + (call $input (i32.const {{.InputNumber "oscillator" "phase"}})) + (f32.const 0.08333333) ;; 1/12, add small phase shift so all oscillators don't start in phase + ) + ) + (global.set $WRK (i32.add (global.get $WRK) (i32.const 8))) ;; WARNING: this is a bug. WRK should be nonvolatile, but we are changing it. It does not cause immediate problems but modulations will be off. + (local.set $detune (f32.neg (f32.mul + (local.get $detune) ;; each unison oscillator has a detune with flipped sign and halved amount... this creates detunes that concentrate around the fundamental + (f32.const 0.5) + ))) + br $unisonLoop + )) + end + (global.set $WRK (local.get $WRK_stash)) + (local.set $detune (local.get $detune_stash)) +{{- end}} +{{- if .Stereo "oscillator"}} + (local.set $detune (f32.neg (local.get $detune))) ;; flip the detune for secon round + (global.set $WRK (i32.add (global.get $WRK) (i32.const 4))) ;; WARNING: this is a bug. WRK should be nonvolatile, but we are changing it. It does not cause immediate problems but modulations will be off. + (br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo))))) + end +{{- end}} +) + +{{- if .SupportsParamValue "oscillator" "type" .Pulse}} +(func $oscillator_pulse (param $phase f32) (param $color f32) (result f32) + (select + (f32.const -1) + (f32.const 1) + (f32.ge (local.get $phase) (local.get $color)) + ) +) +{{end}} + +{{- if .SupportsParamValue "oscillator" "type" .Sine}} +(func $oscillator_sine (param $phase f32) (param $color f32) (result f32) + (select + (f32.const 0) + (call $sin (f32.mul + (f32.div + (local.get $phase) + (local.get $color) + ) + (f32.const 6.28318530718) + )) + (f32.ge (local.get $phase) (local.get $color)) + ) +) +{{end}} + +{{- if .SupportsParamValue "oscillator" "type" .Trisaw}} +(func $oscillator_trisaw (param $phase f32) (param $color f32) (result f32) + (if (f32.ge (local.get $phase) (local.get $color)) (then + (local.set $phase (f32.sub (f32.const 1) (local.get $phase))) + (local.set $color (f32.sub (f32.const 1) (local.get $color))) + )) + (f32.div (local.get $phase) (local.get $color)) + (f32.mul (f32.const 2)) + (f32.sub (f32.const 1)) +) +{{end}} + +{{- if .SupportsParamValue "oscillator" "type" .Gate}} +(func $oscillator_gate (param $phase f32) (result f32) (local $x f32) + (f32.store offset=16 (global.get $WRK) + (local.tee $x + (f32.add ;; c*(g-x)+x + (f32.mul ;; c*(g-x) + (f32.sub ;; g - x + (f32.load offset=16 (global.get $WRK)) ;; g + (local.tee $x + (f32.convert_i32_u ;; 'x' gate bit = float((gatebits >> (int(p*16+.5)&15)) & 1) + (i32.and ;; (int(p*16+.5)&15)&1 + (i32.shr_u ;; int(p*16+.5)&15 + (i32.load16_u (i32.sub (global.get $VAL) (i32.const 4))) + (i32.and ;; int(p*16+.5) & 15 + (i32.trunc_f32_s (f32.add + (f32.mul + (local.get $phase) + (f32.const 16.0) + ) + (f32.const 0.5) ;; well, x86 rounds to integer by default; on wasm, we have only trunc. + )) ;; This is just for rendering similar to x86, should probably delete when optimizing size. + (i32.const 15) + ) + ) + (i32.const 1) + ) + ) + ) + ) + (f32.const 0.99609375) ;; 'c' + ) + (local.get $x) + ) + ) + ) + local.get $x +) +{{end}} + +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) +{{- if .Stereo "receive"}} + (if (local.get $stereo) (then + (call $push + (f32.load offset=36 (global.get $WRK)) + ) + (f32.store offset=36 (global.get $WRK) (f32.const 0)) + )) +{{- end}} + (call $push + (f32.load offset=32 (global.get $WRK)) + ) + (f32.store offset=32 (global.get $WRK) (f32.const 0)) +) +{{end}} + + +{{- if .HasOp "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 (param $stereo i32) (local $addr i32) + call $scanValueByte +{{- if .Stereo "in"}} + (i32.add (local.get $stereo)) ;; start from right channel if stereo +{{- end}} + (local.set $addr (i32.add (i32.mul (i32.const 4)) (i32.const 4128))) +{{- if .Stereo "in"}} + loop $stereoLoop +{{- end}} + (call $push (f32.load (local.get $addr))) + (f32.store (local.get $addr) (f32.const 0)) +{{- if .Stereo "in"}} + (local.set $addr (i32.sub (local.get $addr) (i32.const 4))) + (br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo))))) + end +{{- end}} +) +{{end}} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7800f90..11752f7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,11 +8,23 @@ function(regression_test testname) add_custom_command( OUTPUT ${asmfile} COMMAND ${compilecmd} -arch=${arch} -o ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source} - DEPENDS ${source} ${compilecmd} ${templates} + DEPENDS ${source} ${x86templates} sointu-compiler ) add_executable(${testname} test_renderer.c ${asmfile}) target_compile_definitions(${testname} PUBLIC TEST_HEADER=<${testname}.h>) + + if (NODE AND WAT2WASM AND NOT ${testname} MATCHES "sample") + set(wasmfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wasm) + set(watfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wat) + set(wasmtarget wasm_${testname}) + add_custom_target(${wasmtarget} ALL + COMMAND ${compilecmd} -arch=wasm -o ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source} && ${WAT2WASM} --enable-bulk-memory -o ${wasmfile} ${watfile} + SOURCES "${source}" "${wasmtemplates}" + DEPENDS sointu-compiler + ) + add_test(${wasmtarget} ${NODE} ${CMAKE_CURRENT_SOURCE_DIR}/wasm_test_renderer.es6 ${wasmfile} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw) + endif() else() set(source ${ARGV3}) add_executable(${testname} ${source} test_renderer.c) diff --git a/tests/test_renderer.c b/tests/test_renderer.c index 8637103..89380b1 100644 --- a/tests/test_renderer.c +++ b/tests/test_renderer.c @@ -34,6 +34,17 @@ int main(int argc, char* argv[]) { su_render_song(buf); +#if defined (_WIN32) + CreateDirectory(actual_output_folder, NULL); +#else + mkdir(actual_output_folder, 0777); +#endif + + snprintf(filename, sizeof filename, "%s%s%s", actual_output_folder, test_name, ".raw"); + f = fopen(filename, "wb"); + fwrite((void*)buf, sizeof(SUsample), SU_BUFFER_LENGTH, f); + fclose(f); + snprintf(filename, sizeof filename, "%s%s%s", expected_output_folder, test_name, ".raw"); f = fopen(filename, "rb"); @@ -79,16 +90,6 @@ int main(int argc, char* argv[]) { printf("Warning: Sointu rendered almost correct wave, but a small maximum error of %f\n",max_diff); } -#if defined (_WIN32) - CreateDirectory(actual_output_folder, NULL); -#else - mkdir(actual_output_folder, 0777); -#endif - - snprintf(filename, sizeof filename, "%s%s%s", actual_output_folder, test_name, ".raw"); - f = fopen(filename, "wb"); - fwrite((void*)buf, sizeof(SUsample), SU_BUFFER_LENGTH, f); - fclose(f); return 0; fail: diff --git a/tests/wasm_test_renderer.es6 b/tests/wasm_test_renderer.es6 new file mode 100644 index 0000000..56964ca --- /dev/null +++ b/tests/wasm_test_renderer.es6 @@ -0,0 +1,95 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const { exit } = require('process'); + +if (process.argv.length <= 3) { + console.log("Usage: wasm_test_renderer.es6 path/to/compiled_wasm_song.wasm path/to/expected_output.raw") + console.log("The test renderer needs to know the location and length of the output buffer in wasm memory; remember to sointu-compile the .wat with TBW") + exit(2) +} + +(async () => { + var file,wasm,instance + try { + file = fs.readFileSync(process.argv[2]) + } catch (err) { + console.error("could not read wasmfile "+process.argv[2]+": "+err); + return 1 + } + + try { + wasm = await WebAssembly.compile(file); + } catch (err) { + console.error("could not compile wasmfile "+process.argv[2]+": "+err); + return 1 + } + + try { + instance = await WebAssembly.instantiate(wasm,{m:Math}); + } catch (err) { + console.error("could not instantiate wasmfile "+process.argv[2]+": "+err); + return 1 + } + + let gotBuffer = instance.exports.t.value ? + new Int16Array(instance.exports.m.buffer,instance.exports.s.value,instance.exports.l.value/2) : + new Float32Array(instance.exports.m.buffer,instance.exports.s.value,instance.exports.l.value/4); + + + const gotFileName = path.join(path.parse(process.argv[2]).dir,"wasm_got_" + path.parse(process.argv[3]).name+".raw"); + try { + const gotByteBuffer = Buffer.from(instance.exports.m.buffer,instance.exports.s.value,instance.exports.l.value); + fs.writeFileSync(gotFileName, gotByteBuffer); + } catch (err) { + console.error("could not save the buffer we got to disk "+gotFileName+": "+err); + return 1 + } + + const expectedFile = fs.readFileSync(process.argv[3]); + let expectedBuffer = instance.exports.t.value ? + new Int16Array(expectedFile.buffer, expectedFile.offset, expectedFile.byteLength/2) : + new Float32Array(expectedFile.buffer, expectedFile.offset, expectedFile.byteLength/4); + + if (gotBuffer.length < expectedBuffer.length) + { + console.error("got shorter buffer than expected"); + return 1 + } + + if (gotBuffer.length > expectedBuffer.length) + { + console.error("got longer buffer than expected"); + return 1 + } + + let margin = 1e-2 * (instance.exports.t.value ? 32767 : 1); + + var firstError = true, firstErrorPos, errorCount = 0 + // we still have occasional sample wrong here or there. We only consider this a true error + // if the total number of errors is too high + for (var i = 2; i < gotBuffer.length-2; i++) { + // Pulse oscillators with their sharp changes can sometimes be one sample late + // due to rounding errors, causing the test fail. So, we test three samples + // and if none match, then this sample is really wrong. Note that this is stereo + // buffer so -2 index is the previous sample. + // Also, we're pretty liberal on the accuracy, as small rounding errors + // in frequency cause tests fails as the waves developed a phase shift over time + // (or rounding errors in delay buffers etc.) + if (Math.abs(gotBuffer[i] - expectedBuffer[i-2]) > margin && + Math.abs(gotBuffer[i] - expectedBuffer[i]) > margin && + Math.abs(gotBuffer[i] - expectedBuffer[i+2]) > margin) { + if (firstError) { + firstErrorPos = i + firstError = false + } + errorCount++ + } + if (errorCount > 100) { + console.error("got different buffer than expected. First error at: "+(firstErrorPos/2|0)+(firstErrorPos%1," right"," left")); + return 1; + } + } + + return 0; +})().then(retval => exit(retval));