From e4490faa2e52d0a6bee7d8dd7450fdc87b8434e2 Mon Sep 17 00:00:00 2001 From: Veikko Sariola Date: Sat, 26 Dec 2020 23:16:18 +0200 Subject: [PATCH] feat(compiler): Add support for targeting WebAssembly. The working principle is similar as before with x86, but instead of outputting .asm, it outputs .wat. This can be compiled into .wasm by using the wat2wasm assembler. --- .github/workflows/tests.yml | 1 + CMakeLists.txt | 34 +- README.md | 35 +- compiler/compiler.go | 62 ++- compiler/compiler_macros.go | 28 ++ compiler/featureset_macros.go | 27 ++ compiler/song_macros.go | 39 ++ compiler/wasm_macros.go | 50 +++ compiler/{macros.go => x86_macros.go} | 184 +++----- templates/{ => amd64-386}/arithmetic.asm | 0 templates/{ => amd64-386}/effects.asm | 0 templates/{ => amd64-386}/flowcontrol.asm | 0 templates/{ => amd64-386}/gmdls.asm | 0 templates/{ => amd64-386}/library.asm | 0 templates/{ => amd64-386}/library.h | 0 templates/{ => amd64-386}/output_sound.asm | 2 +- templates/{ => amd64-386}/patch.asm | 0 templates/{ => amd64-386}/player.asm | 0 templates/{ => amd64-386}/player.h | 2 +- templates/{ => amd64-386}/sinks.asm | 0 templates/{ => amd64-386}/sources.asm | 8 +- templates/{ => amd64-386}/structs.asm | 0 templates/wasm/arithmetic.wat | 239 ++++++++++ templates/wasm/effects.wat | 482 +++++++++++++++++++++ templates/wasm/output_sound.wat | 19 + templates/wasm/patch.wat | 144 ++++++ templates/wasm/player.wat | 288 ++++++++++++ templates/wasm/sinks.wat | 198 +++++++++ templates/wasm/sources.wat | 336 ++++++++++++++ tests/CMakeLists.txt | 14 +- tests/test_renderer.c | 21 +- tests/wasm_test_renderer.es6 | 95 ++++ 32 files changed, 2138 insertions(+), 170 deletions(-) create mode 100644 compiler/compiler_macros.go create mode 100644 compiler/featureset_macros.go create mode 100644 compiler/song_macros.go create mode 100644 compiler/wasm_macros.go rename compiler/{macros.go => x86_macros.go} (63%) rename templates/{ => amd64-386}/arithmetic.asm (100%) rename templates/{ => amd64-386}/effects.asm (100%) rename templates/{ => amd64-386}/flowcontrol.asm (100%) rename templates/{ => amd64-386}/gmdls.asm (100%) rename templates/{ => amd64-386}/library.asm (100%) rename templates/{ => amd64-386}/library.h (100%) rename templates/{ => amd64-386}/output_sound.asm (98%) rename templates/{ => amd64-386}/patch.asm (100%) rename templates/{ => amd64-386}/player.asm (100%) rename templates/{ => amd64-386}/player.h (97%) rename templates/{ => amd64-386}/sinks.asm (100%) rename templates/{ => amd64-386}/sources.asm (95%) rename templates/{ => amd64-386}/structs.asm (100%) create mode 100644 templates/wasm/arithmetic.wat create mode 100644 templates/wasm/effects.wat create mode 100644 templates/wasm/output_sound.wat create mode 100644 templates/wasm/patch.wat create mode 100644 templates/wasm/player.wat create mode 100644 templates/wasm/sinks.wat create mode 100644 templates/wasm/sources.wat create mode 100644 tests/wasm_test_renderer.es6 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));