mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-19 05:24:48 -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:
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}}
|
Reference in New Issue
Block a user