mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
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.
This commit is contained in:
parent
7e4bcf18e4
commit
e4490faa2e
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@ -42,6 +42,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: lukka/get-cmake@v3.18.3
|
- uses: lukka/get-cmake@v3.18.3
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: ilammy/setup-nasm@v1.2.0
|
- uses: ilammy/setup-nasm@v1.2.0
|
||||||
- name: Install libasound2-dev # sointu-cli has alsa as dependency for playing sound and
|
- name: Install libasound2-dev # sointu-cli has alsa as dependency for playing sound and
|
||||||
|
@ -28,6 +28,27 @@ IF(APPLE)
|
|||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie")
|
||||||
endif()
|
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)
|
enable_language(ASM_NASM)
|
||||||
|
|
||||||
# The normal NASM compile object does not include <DEFINES>
|
# The normal NASM compile object does not include <DEFINES>
|
||||||
@ -41,7 +62,8 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# the tests include the entire ASM but we still want to rebuild when they change
|
# 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 sointusrc "${PROJECT_SOURCE_DIR}/*.go")
|
||||||
file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/compiler/*.go")
|
file(GLOB compilersrc "${PROJECT_SOURCE_DIR}/compiler/*.go")
|
||||||
file(GLOB compilecmdsrc "${PROJECT_SOURCE_DIR}/cmd/sointu-compile/*.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
|
# Build sointu-cli only once because go run has everytime quite a bit of delay when
|
||||||
# starting
|
# starting
|
||||||
add_custom_command(
|
add_custom_target(
|
||||||
OUTPUT ${compilecmd}
|
sointu-compiler
|
||||||
COMMAND go build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
|
COMMAND ${GO} build -o ${compilecmd} ${PROJECT_SOURCE_DIR}/cmd/sointu-compile/main.go
|
||||||
DEPENDS "${sointusrc}" "${compilersrc}" "${compilecmdsrc}"
|
SOURCES "${sointusrc}" "${compilersrc}" "${compilecmdsrc}"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${sointuasm}
|
OUTPUT ${sointuasm}
|
||||||
COMMAND ${compilecmd} -arch=${arch} -a -o ${CMAKE_CURRENT_BINARY_DIR}
|
COMMAND ${compilecmd} -arch=${arch} -a -o ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
DEPENDS "${templates}" ${compilecmd}
|
DEPENDS "${templates}" sointu-compiler
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${STATICLIB} ${sointuasm})
|
add_library(${STATICLIB} ${sointuasm})
|
||||||
|
35
README.md
35
README.md
@ -1,8 +1,10 @@
|
|||||||
# Sointu
|
# Sointu
|
||||||

|

|
||||||
|
|
||||||
A cross-platform modular software synthesizer for small intros, forked from
|
A cross-architecture and cross-platform modular software synthesizer for small
|
||||||
[4klang](https://github.com/hzdgopher/4klang). Supports win32/win64/linux/mac.
|
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
|
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
|
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
|
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
|
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
|
engine can already be fitted in 600 bytes (386, compressed), with another few
|
||||||
bytes for the patch and pattern data.
|
hundred bytes for the patch and pattern data.
|
||||||
|
|
||||||
Sointu consists of two core elements:
|
Sointu consists of two core elements:
|
||||||
- A cross-platform synth-tracker app for composing music, written in
|
- 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
|
[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.
|
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
|
- 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
|
to compile these .yml files into .asm or .wat code. For x86 platforms, the
|
||||||
be then compiled by [nasm](https://www.nasm.us/) or
|
resulting .asm can be then compiled by [nasm](https://www.nasm.us/) or
|
||||||
[yasm](https://yasm.tortall.net).
|
[yasm](https://yasm.tortall.net). For browsers, the resulting .wat can be
|
||||||
|
compiled by [wat2wasm](https://github.com/WebAssembly/wabt).
|
||||||
|
|
||||||
Building
|
Building
|
||||||
--------
|
--------
|
||||||
@ -53,6 +56,13 @@ sointu-compile -o . -arch=386 tests/test_chords.yml
|
|||||||
nasm -f win32 test_chords.asm
|
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 and running the tests as executables
|
||||||
|
|
||||||
Building the [regression tests](tests/) as executables (testing that they work
|
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
|
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.
|
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
|
New features since fork
|
||||||
-----------------------
|
-----------------------
|
||||||
- **Compiler**. Written in go. The input is a .yml file and the output is an
|
- **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
|
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
|
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.
|
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
|
- **Supports Windows, Linux and MacOS**. On all three 64-bit platforms, all
|
||||||
tests are passing. Additionally, all tests are passing on windows 32.
|
tests are passing. Additionally, all tests are passing on windows 32.
|
||||||
- **New units**. For example: bit-crusher, gain, inverse gain, clip, modulate
|
- **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
|
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
|
single envelope and a slow filter can even be used as a cheap smooth
|
||||||
interpolation mechanism.
|
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
|
- **Hack deeper into audio sources from the OS**. Speech synthesis, I'm eyeing
|
||||||
at you.
|
at you.
|
||||||
|
|
||||||
|
@ -21,7 +21,15 @@ type Compiler struct {
|
|||||||
// New returns a new compiler using the default .asm templates
|
// New returns a new compiler using the default .asm templates
|
||||||
func New(os string, arch string) (*Compiler, error) {
|
func New(os string, arch string) (*Compiler, error) {
|
||||||
_, myname, _, _ := runtime.Caller(0)
|
_, 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)
|
compiler, err := NewFromTemplates(os, arch, templateDir)
|
||||||
return compiler, err
|
return compiler, err
|
||||||
}
|
}
|
||||||
@ -43,9 +51,16 @@ func (com *Compiler) Library() (map[string]string, error) {
|
|||||||
features := AllFeatures{}
|
features := AllFeatures{}
|
||||||
retmap := map[string]string{}
|
retmap := map[string]string{}
|
||||||
for _, templateName := range templates {
|
for _, templateName := range templates {
|
||||||
macros := NewMacros(*com, features)
|
compilerMacros := *NewCompilerMacros(*com)
|
||||||
macros.Library = true
|
compilerMacros.Library = true
|
||||||
populatedTemplate, extension, err := com.compile(templateName, macros)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
|
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) {
|
func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
|
||||||
if com.Arch != "386" && com.Arch != "amd64" {
|
if com.Arch != "386" && com.Arch != "amd64" && com.Arch != "wasm" {
|
||||||
return nil, fmt.Errorf(`compiling a song player is supported only on 386 and amd64 architectures (targeted architecture was %v)`, com.Arch)
|
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)
|
features := NecessaryFeaturesFor(song.Patch)
|
||||||
retmap := map[string]string{}
|
retmap := map[string]string{}
|
||||||
encodedPatch, err := Encode(&song.Patch, features)
|
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)
|
return nil, fmt.Errorf(`could not encode patch: %v`, err)
|
||||||
}
|
}
|
||||||
for _, templateName := range templates {
|
for _, templateName := range templates {
|
||||||
macros := NewPlayerMacros(*com, features, song, encodedPatch)
|
compilerMacros := *NewCompilerMacros(*com)
|
||||||
populatedTemplate, extension, err := com.compile(templateName, macros)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
|
return nil, fmt.Errorf(`could not execute template "%v": %v`, templateName, err)
|
||||||
}
|
}
|
||||||
|
28
compiler/compiler_macros.go
Normal file
28
compiler/compiler_macros.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
27
compiler/featureset_macros.go
Normal file
27
compiler/featureset_macros.go
Normal file
@ -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)
|
||||||
|
}
|
39
compiler/song_macros.go
Normal file
39
compiler/song_macros.go
Normal file
@ -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)
|
||||||
|
}
|
50
compiler/wasm_macros.go
Normal file
50
compiler/wasm_macros.go
Normal file
@ -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()
|
||||||
|
}
|
@ -4,74 +4,36 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OplistEntry struct {
|
type X86Macros struct {
|
||||||
Type string
|
|
||||||
NumParams int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Macros struct {
|
|
||||||
Stacklocs []string
|
Stacklocs []string
|
||||||
Output16Bit bool
|
|
||||||
Clip bool
|
|
||||||
Library bool
|
|
||||||
Amd64 bool
|
Amd64 bool
|
||||||
|
OS string
|
||||||
DisableSections bool
|
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
|
usesFloatConst map[float32]bool
|
||||||
usesIntConst map[int]bool
|
usesIntConst map[int]bool
|
||||||
floatConsts []float32
|
floatConsts []float32
|
||||||
intConsts []int
|
intConsts []int
|
||||||
calls map[string]bool
|
calls map[string]bool
|
||||||
stackframes map[string][]string
|
stackframes map[string][]string
|
||||||
FeatureSet
|
features FeatureSet
|
||||||
Compiler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMacros(c Compiler, f FeatureSet) *Macros {
|
func NewX86Macros(os string, Amd64 bool, features FeatureSet, DisableSections bool) *X86Macros {
|
||||||
return &Macros{
|
return &X86Macros{
|
||||||
calls: map[string]bool{},
|
calls: map[string]bool{},
|
||||||
usesFloatConst: map[float32]bool{},
|
usesFloatConst: map[float32]bool{},
|
||||||
usesIntConst: map[int]bool{},
|
usesIntConst: map[int]bool{},
|
||||||
stackframes: map[string][]string{},
|
stackframes: map[string][]string{},
|
||||||
Sine: sointu.Sine,
|
Amd64: Amd64,
|
||||||
Trisaw: sointu.Trisaw,
|
OS: os,
|
||||||
Pulse: sointu.Pulse,
|
DisableSections: DisableSections,
|
||||||
Gate: sointu.Gate,
|
features: features,
|
||||||
Sample: sointu.Sample,
|
|
||||||
Amd64: c.Arch == "amd64",
|
|
||||||
Compiler: c,
|
|
||||||
FeatureSet: f,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) HasOp(instruction string) bool {
|
func (p *X86Macros) Float(value float32) string {
|
||||||
_, 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 {
|
|
||||||
if _, ok := p.usesFloatConst[value]; !ok {
|
if _, ok := p.usesFloatConst[value]; !ok {
|
||||||
p.usesFloatConst[value] = true
|
p.usesFloatConst[value] = true
|
||||||
p.floatConsts = append(p.floatConsts, value)
|
p.floatConsts = append(p.floatConsts, value)
|
||||||
@ -79,7 +41,7 @@ func (p *Macros) Float(value float32) string {
|
|||||||
return nameForFloat(value)
|
return nameForFloat(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) Int(value int) string {
|
func (p *X86Macros) Int(value int) string {
|
||||||
if _, ok := p.usesIntConst[value]; !ok {
|
if _, ok := p.usesIntConst[value]; !ok {
|
||||||
p.usesIntConst[value] = true
|
p.usesIntConst[value] = true
|
||||||
p.intConsts = append(p.intConsts, value)
|
p.intConsts = append(p.intConsts, value)
|
||||||
@ -87,7 +49,7 @@ func (p *Macros) Int(value int) string {
|
|||||||
return nameForInt(value)
|
return nameForInt(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) Constants() string {
|
func (p *X86Macros) Constants() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for _, v := range p.floatConsts {
|
for _, v := range p.floatConsts {
|
||||||
fmt.Fprintf(&b, "%-23s dd 0x%x\n", nameForFloat(v), math.Float32bits(v))
|
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)
|
return "ICONST_" + fmt.Sprintf("%d", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) PTRSIZE() int {
|
func (p *X86Macros) PTRSIZE() int {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return 8
|
return 8
|
||||||
}
|
}
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) DPTR() string {
|
func (p *X86Macros) DPTR() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "dq"
|
return "dq"
|
||||||
}
|
}
|
||||||
return "dd"
|
return "dd"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) PTRWORD() string {
|
func (p *X86Macros) PTRWORD() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "qword"
|
return "qword"
|
||||||
}
|
}
|
||||||
return "dword"
|
return "dword"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) AX() string {
|
func (p *X86Macros) AX() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rax"
|
return "rax"
|
||||||
}
|
}
|
||||||
return "eax"
|
return "eax"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) BX() string {
|
func (p *X86Macros) BX() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rbx"
|
return "rbx"
|
||||||
}
|
}
|
||||||
return "ebx"
|
return "ebx"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) CX() string {
|
func (p *X86Macros) CX() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rcx"
|
return "rcx"
|
||||||
}
|
}
|
||||||
return "ecx"
|
return "ecx"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) DX() string {
|
func (p *X86Macros) DX() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rdx"
|
return "rdx"
|
||||||
}
|
}
|
||||||
return "edx"
|
return "edx"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) SI() string {
|
func (p *X86Macros) SI() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rsi"
|
return "rsi"
|
||||||
}
|
}
|
||||||
return "esi"
|
return "esi"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) DI() string {
|
func (p *X86Macros) DI() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rdi"
|
return "rdi"
|
||||||
}
|
}
|
||||||
return "edi"
|
return "edi"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) SP() string {
|
func (p *X86Macros) SP() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rsp"
|
return "rsp"
|
||||||
}
|
}
|
||||||
return "esp"
|
return "esp"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) BP() string {
|
func (p *X86Macros) BP() string {
|
||||||
if p.Amd64 {
|
if p.Amd64 {
|
||||||
return "rbp"
|
return "rbp"
|
||||||
}
|
}
|
||||||
return "ebp"
|
return "ebp"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) WRK() string {
|
func (p *X86Macros) WRK() string {
|
||||||
return p.BP()
|
return p.BP()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) VAL() string {
|
func (p *X86Macros) VAL() string {
|
||||||
return p.SI()
|
return p.SI()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) COM() string {
|
func (p *X86Macros) COM() string {
|
||||||
return p.BX()
|
return p.BX()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) INP() string {
|
func (p *X86Macros) INP() string {
|
||||||
return p.DX()
|
return p.DX()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) SaveStack(scope string) string {
|
func (p *X86Macros) SaveStack(scope string) string {
|
||||||
p.stackframes[scope] = p.Stacklocs
|
p.stackframes[scope] = p.Stacklocs
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) Call(funcname string) (string, error) {
|
func (p *X86Macros) Call(funcname string) (string, error) {
|
||||||
p.calls[funcname] = true
|
p.calls[funcname] = true
|
||||||
var s = make([]string, len(p.Stacklocs))
|
var s = make([]string, len(p.Stacklocs))
|
||||||
copy(s, p.Stacklocs)
|
copy(s, p.Stacklocs)
|
||||||
@ -216,13 +178,13 @@ func (p *Macros) Call(funcname string) (string, error) {
|
|||||||
return "call " + funcname, nil
|
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.calls[funcname] = true
|
||||||
p.stackframes[funcname] = p.Stacklocs
|
p.stackframes[funcname] = p.Stacklocs
|
||||||
return "jmp " + funcname, nil
|
return "jmp " + funcname, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) SectText(name string) string {
|
func (p *X86Macros) SectText(name string) string {
|
||||||
if p.OS == "windows" {
|
if p.OS == "windows" {
|
||||||
if p.DisableSections {
|
if p.DisableSections {
|
||||||
return "section .code align=1"
|
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.OS == "darwin" {
|
||||||
if p.OS == "windows" && !p.DisableSections {
|
if p.OS == "windows" && !p.DisableSections {
|
||||||
return fmt.Sprintf("section .%v data align=1", name)
|
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.OS == "darwin" {
|
||||||
if p.OS == "windows" && !p.DisableSections {
|
if p.OS == "windows" && !p.DisableSections {
|
||||||
return fmt.Sprintf("section .%v bss align=256", name)
|
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)
|
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
|
scopeName := funcname
|
||||||
if len(scope) > 1 {
|
if len(scope) > 1 {
|
||||||
return "", fmt.Errorf(`Func macro "%v" can take only one additional scope parameter, "%v" were given`, funcname, scope)
|
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
|
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]
|
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)
|
p.Stacklocs = append(p.Stacklocs, name)
|
||||||
return fmt.Sprintf("push %v ; Stack: %v ", value, p.FmtStack())
|
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 {
|
if p.Amd64 {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i := 0; i < len(params); i = i + 2 {
|
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 {
|
if p.Amd64 {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i := len(params) - 1; i >= 0; i-- {
|
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]
|
last := p.Stacklocs[len(p.Stacklocs)-1]
|
||||||
p.Stacklocs = 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())
|
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
|
i := 0
|
||||||
for ; i < 108; i += p.PTRSIZE() {
|
for ; i < 108; i += p.PTRSIZE() {
|
||||||
p.Stacklocs = append(p.Stacklocs, fmt.Sprintf("F%v", i))
|
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)
|
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
|
i := 0
|
||||||
for ; i < 108; i += p.PTRSIZE() {
|
for ; i < 108; i += p.PTRSIZE() {
|
||||||
p.Stacklocs = p.Stacklocs[:len(p.Stacklocs)-1]
|
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)
|
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 {
|
for i, k := range p.Stacklocs {
|
||||||
if k == name {
|
if k == name {
|
||||||
pos := len(p.Stacklocs) - i - 1
|
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)
|
return "", fmt.Errorf("unknown symbol %v", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) FmtStack() string {
|
func (p *X86Macros) FmtStack() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
last := len(p.Stacklocs) - 1
|
last := len(p.Stacklocs) - 1
|
||||||
for i := range p.Stacklocs {
|
for i := range p.Stacklocs {
|
||||||
@ -390,7 +352,7 @@ func (p *Macros) FmtStack() string {
|
|||||||
return b.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 {
|
if !p.Amd64 {
|
||||||
reverseParams := make([]string, len(params))
|
reverseParams := make([]string, len(params))
|
||||||
for i, param := range 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)
|
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) {
|
func (p *X86Macros) Input(unit string, port string) (string, error) {
|
||||||
i := p.InputNumber(unit, port)
|
i := p.features.InputNumber(unit, port)
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
return fmt.Sprintf("%v + %v", p.INP(), i*4), nil
|
return fmt.Sprintf("%v + %v", p.INP(), i*4), nil
|
||||||
}
|
}
|
||||||
return p.INP(), nil
|
return p.INP(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Macros) Modulation(unit string, port string) (string, error) {
|
func (p *X86Macros) Modulation(unit string, port string) (string, error) {
|
||||||
i := p.InputNumber(unit, port)
|
i := p.features.InputNumber(unit, port)
|
||||||
return fmt.Sprintf("%v + %v", p.WRK(), i*4+32), nil
|
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 p.Amd64 {
|
||||||
if len(regs) > 1 {
|
if len(regs) > 1 {
|
||||||
return "", fmt.Errorf("macro Prepare cannot accept more than one register parameter")
|
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
|
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 {
|
if p.Amd64 {
|
||||||
return "r9", nil
|
return "r9", nil
|
||||||
}
|
}
|
||||||
@ -443,39 +405,3 @@ func (p *Macros) Use(value string, regs ...string) (string, error) {
|
|||||||
}
|
}
|
||||||
return value, nil
|
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)
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
{{- if not .Output16Bit }}
|
{{- if not .Song.Output16Bit }}
|
||||||
{{- if not .Clip }}
|
{{- if not .Clip }}
|
||||||
mov {{.DI}}, [{{.Stack "OutputBufPtr"}}] ; edi containts ptr
|
mov {{.DI}}, [{{.Stack "OutputBufPtr"}}] ; edi containts ptr
|
||||||
mov {{.SI}}, {{.PTRWORD}} su_synth_obj + su_synthworkspace.left
|
mov {{.SI}}, {{.PTRWORD}} su_synth_obj + su_synthworkspace.left
|
@ -23,7 +23,7 @@
|
|||||||
#define SU_CALLCONV
|
#define SU_CALLCONV
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
{{- if .Output16Bit}}
|
{{- if .Song.Output16Bit}}
|
||||||
typedef short SUsample;
|
typedef short SUsample;
|
||||||
#define SU_SAMPLE_RANGE 32767.0
|
#define SU_SAMPLE_RANGE 32767.0
|
||||||
{{- else}}
|
{{- else}}
|
@ -114,7 +114,9 @@ su_op_noise_mono:
|
|||||||
{{- if .Stereo "oscillator"}}
|
{{- if .Stereo "oscillator"}}
|
||||||
fld st0 ; d d
|
fld st0 ; d d
|
||||||
call su_op_oscillat_mono ; r 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
|
fxch ; d r
|
||||||
fchs ; -d r, negate the detune for second round
|
fchs ; -d r, negate the detune for second round
|
||||||
su_op_oscillat_mono:
|
su_op_oscillat_mono:
|
||||||
@ -129,10 +131,12 @@ su_op_oscillat_unison_loop:
|
|||||||
faddp st1, st0 ; a+=s
|
faddp st1, st0 ; a+=s
|
||||||
test al, 3
|
test al, 3
|
||||||
je su_op_oscillat_unison_out
|
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
|
add {{.WRK}}, 8
|
||||||
fld dword [{{.Input "oscillator" "phase"}}] ; p s
|
fld dword [{{.Input "oscillator" "phase"}}] ; p s
|
||||||
{{.Int 0x3DAAAAAA | .Prepare}}
|
{{.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
|
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
|
fld dword [{{.SP}}] ; d s
|
||||||
{{.Float 0.5 | .Prepare}}
|
{{.Float 0.5 | .Prepare}}
|
239
templates/wasm/arithmetic.wat
Normal file
239
templates/wasm/arithmetic.wat
Normal file
@ -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}}
|
482
templates/wasm/effects.wat
Normal file
482
templates/wasm/effects.wat
Normal file
@ -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<level) (let's call it c)
|
||||||
|
(local.tee $level (f32.add ;; l'=l + c*(x^2-l)
|
||||||
|
(f32.mul ;; c was already on stack, so c*(x^2-l)
|
||||||
|
(f32.sub ;; x^2-l
|
||||||
|
(local.get $x2)
|
||||||
|
(local.get $level)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(local.get $level)
|
||||||
|
))
|
||||||
|
(local.tee $t2 (f32.mul ;; t^2
|
||||||
|
(call $input (i32.const {{.InputNumber "compressor" "threshold"}}))
|
||||||
|
(call $input (i32.const {{.InputNumber "compressor" "threshold"}}))
|
||||||
|
))
|
||||||
|
(if (f32.gt) (then ;; if $level > $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}}
|
19
templates/wasm/output_sound.wat
Normal file
19
templates/wasm/output_sound.wat
Normal file
@ -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
|
144
templates/wasm/patch.wat
Normal file
144
templates/wasm/patch.wat
Normal file
@ -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}}
|
||||||
|
)
|
288
templates/wasm/player.wat
Normal file
288
templates/wasm/player.wat
Normal file
@ -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
|
198
templates/wasm/sinks.wat
Normal file
198
templates/wasm/sinks.wat
Normal file
@ -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}}
|
336
templates/wasm/sources.wat
Normal file
336
templates/wasm/sources.wat
Normal file
@ -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}}
|
@ -8,11 +8,23 @@ function(regression_test testname)
|
|||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${asmfile}
|
OUTPUT ${asmfile}
|
||||||
COMMAND ${compilecmd} -arch=${arch} -o ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${source}
|
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})
|
add_executable(${testname} test_renderer.c ${asmfile})
|
||||||
target_compile_definitions(${testname} PUBLIC TEST_HEADER=<${testname}.h>)
|
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()
|
else()
|
||||||
set(source ${ARGV3})
|
set(source ${ARGV3})
|
||||||
add_executable(${testname} ${source} test_renderer.c)
|
add_executable(${testname} ${source} test_renderer.c)
|
||||||
|
@ -34,6 +34,17 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
su_render_song(buf);
|
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");
|
snprintf(filename, sizeof filename, "%s%s%s", expected_output_folder, test_name, ".raw");
|
||||||
|
|
||||||
f = fopen(filename, "rb");
|
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);
|
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;
|
return 0;
|
||||||
|
|
||||||
fail:
|
fail:
|
||||||
|
95
tests/wasm_test_renderer.es6
Normal file
95
tests/wasm_test_renderer.es6
Normal file
@ -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));
|
Loading…
x
Reference in New Issue
Block a user