feat(vm/compiler): embed templates to executable

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2023-07-08 16:39:41 +03:00
parent d2ddba3944
commit 8ffe4a70dd
21 changed files with 9 additions and 6 deletions

View File

@ -0,0 +1,205 @@
{{- 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" "Opcode"}}
{{- if .StereoAndMono "pop"}}
jnc su_op_pop_mono
{{- end}}
{{- if .Stereo "pop"}}
fstp st0
{{- end}}
{{- if .StereoAndMono "pop"}}
su_op_pop_mono:
{{- end}}
fstp st0
ret
{{end}}
{{- if .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" "Opcode"}}
{{- if .StereoAndMono "add"}}
jnc su_op_add_mono
{{- end}}
{{- if .Stereo "add"}}
fadd st0, st2
fxch
fadd st0, st3
fxch
ret
{{- end}}
{{- if .StereoAndMono "add"}}
su_op_add_mono:
{{- end}}
{{- if .Mono "add"}}
fadd st1
{{- end}}
{{- if .Mono "add"}}
ret
{{- end}}
{{end}}
{{- if .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" "Opcode"}}
{{- if .StereoAndMono "addp"}}
jnc su_op_addp_mono
{{- end}}
{{- if .Stereo "addp"}}
faddp st2, st0
faddp st2, st0
ret
{{- end}}
{{- if .StereoAndMono "addp"}}
su_op_addp_mono:
{{- end}}
{{- if (.Mono "addp")}}
faddp st1, st0
ret
{{- end}}
{{end}}
{{- if .HasOp "loadnote"}}
;-------------------------------------------------------------------------------
; LOADNOTE opcode: load the current note, scaled to [-1,1]
;-------------------------------------------------------------------------------
{{if (.Mono "loadnote") -}} ; Mono: (empty) -> n, where n is the note{{end}}
{{if (.Stereo "loadnote") -}}; Stereo: (empty) -> n n{{end}}
;-------------------------------------------------------------------------------
{{.Func "su_op_loadnote" "Opcode"}}
{{- if .StereoAndMono "loadnote"}}
jnc su_op_loadnote_mono
{{- end}}
{{- if .Stereo "loadnote"}}
call su_op_loadnote_mono
su_op_loadnote_mono:
{{- end}}
fild dword [{{.INP}}-su_voice.inputs+su_voice.note]
{{.Prepare (.Float 0.0078125)}}
fmul dword [{{.Use (.Float 0.0078125)}}] ; s=n/128.0
{{.Prepare (.Float 0.5)}}
fsub dword [{{.Use (.Float 0.5)}}] ; s-.5
fadd st0, st0 ; 2*s-1
ret
{{end}}
{{- if .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" "Opcode"}}
jnc su_op_mul_mono
fmul st0, st2
fxch
fadd st0, st3
fxch
ret
su_op_mul_mono:
fmul st1
ret
{{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" "Opcode"}}
{{- if .StereoAndMono "mulp"}}
jnc su_op_mulp_mono
{{- end}}
{{- if .Stereo "mulp"}}
fmulp st2, st0
fmulp st2, st0
ret
{{- end}}
{{- if .StereoAndMono "mulp"}}
su_op_mulp_mono:
{{- end}}
{{- if .Mono "mulp"}}
fmulp st1
ret
{{- end}}
{{end}}
{{- if .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" "Opcode"}}
{{- if .StereoAndMono "push"}}
jnc su_op_push_mono
{{- end}}
{{- if .Stereo "push"}}
fld st1
fld st1
ret
{{- end}}
{{- if .StereoAndMono "push"}}
su_op_push_mono:
{{- end}}
{{- if .Mono "push"}}
fld st0
ret
{{- end}}
{{end}}
{{- if .HasOp "xch"}}
;-------------------------------------------------------------------------------
; XCH opcode: exchange the signals on the stack
;-------------------------------------------------------------------------------
; Mono: a b -> b a
; stereo: a b c d -> c d a b
;-------------------------------------------------------------------------------
{{.Func "su_op_xch" "Opcode"}}
{{- if .StereoAndMono "xch"}}
jnc su_op_xch_mono
{{- end}}
{{- if .Stereo "xch"}}
fxch st0, st2 ; c b a d
fxch st0, st1 ; b c a d
fxch st0, st3 ; d c a b
{{- end}}
{{- if .StereoAndMono "xch"}}
su_op_xch_mono:
{{- end}}
fxch st0, st1
ret
{{end}}

View File

@ -0,0 +1,390 @@
{{- 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" "Opcode"}}
{{- if .Stereo "hold"}}
{{.Call "su_effects_stereohelper"}}
{{- end}}
fld dword [{{.Input "hold" "holdfreq"}}] ; f x
fmul st0, st0 ; f^2 x
fchs ; -f^2 x
fadd dword [{{.WRK}}] ; p-f^2 x
fst dword [{{.WRK}}] ; p <- p-f^2
fldz ; 0 p x
fucomip st1 ; p x
fstp dword [{{.SP}}-4] ; t=p, x
jc short su_op_hold_holding ; if (0 < p) goto holding
fld1 ; 1 x
fadd dword [{{.SP}}-4] ; 1+t x
fstp dword [{{.WRK}}] ; x
fst dword [{{.WRK}}+4] ; save holded value
ret ; x
su_op_hold_holding:
fstp st0 ;
fld dword [{{.WRK}}+4] ; x
ret
{{end}}
{{- if .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" "Opcode"}}
{{- if .Stereo "crush"}}
{{.Call "su_effects_stereohelper"}}
{{- end}}
fdiv dword [{{.Input "crush" "resolution"}}]
frndint
fmul dword [{{.Input "crush" "resolution"}}]
ret
{{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" "Opcode"}}
{{- if .Stereo "gain"}}
fld dword [{{.Input "gain" "gain"}}] ; g l (r)
{{- if .Mono "invgain"}}
jnc su_op_gain_mono
{{- end}}
fmul st2, st0 ; g l r/g
su_op_gain_mono:
fmulp st1, st0 ; l/g (r/)
ret
{{- else}}
fmul dword [{{.Input "gain" "gain"}}]
ret
{{- end}}
{{end}}
{{- if .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" "Opcode"}}
{{- if .Stereo "invgain"}}
fld dword [{{.Input "invgain" "invgain"}}] ; g l (r)
{{- if .Mono "invgain"}}
jnc su_op_invgain_mono
{{- end}}
fdiv st2, st0 ; g l r/g
su_op_invgain_mono:
fdivp st1, st0 ; l/g (r/)
ret
{{- else}}
fdiv dword [{{.Input "invgain" "invgain"}}]
ret
{{- end}}
{{end}}
{{- if .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" "Opcode"}}
lodsb ; load the flags to al
{{- if .Stereo "filter"}}
{{.Call "su_effects_stereohelper"}}
{{- end}}
fld dword [{{.Input "filter" "resonance"}}] ; r x
fld dword [{{.Input "filter" "frequency"}}]; f r x
fmul st0, st0 ; f2 x (square the input so we never get negative and also have a smoother behaviour in the lower frequencies)
fst dword [{{.WRK}}+12] ; f2 r x
fmul dword [{{.WRK}}+8] ; f2*b r x
fadd dword [{{.WRK}}] ; f2*b+l r x
fst dword [{{.WRK}}] ; l'=f2*b+l r x
fsubp st2, st0 ; r x-l'
fmul dword [{{.WRK}}+8] ; r*b x-l'
fsubp st1, st0 ; x-l'-r*b
fst dword [{{.WRK}}+4] ; h'=x-l'-r*b
fmul dword [{{.WRK}}+12] ; f2*h'
fadd dword [{{.WRK}}+8] ; f2*h'+b
fstp dword [{{.WRK}}+8] ; b'=f2*h'+b
fldz ; 0
{{- if .SupportsParamValue "filter" "lowpass" 1}}
test al, byte 0x40
jz short su_op_filter_skiplowpass
fadd dword [{{.WRK}}]
su_op_filter_skiplowpass:
{{- end}}
{{- if .SupportsParamValue "filter" "bandpass" 1}}
test al, byte 0x20
jz short su_op_filter_skipbandpass
fadd dword [{{.WRK}}+8]
su_op_filter_skipbandpass:
{{- end}}
{{- if .SupportsParamValue "filter" "highpass" 1}}
test al, byte 0x10
jz short su_op_filter_skiphighpass
fadd dword [{{.WRK}}+4]
su_op_filter_skiphighpass:
{{- end}}
{{- if .SupportsParamValue "filter" "negbandpass" 1}}
test al, byte 0x08
jz short su_op_filter_skipnegbandpass
fsub dword [{{.WRK}}+8]
su_op_filter_skipnegbandpass:
{{- end}}
{{- if .SupportsParamValue "filter" "neghighpass" 1}}
test al, byte 0x04
jz short su_op_filter_skipneghighpass
fsub dword [{{.WRK}}+4]
su_op_filter_skipneghighpass:
{{- end}}
ret
{{end}}
{{- if .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" "Opcode"}}
{{- if .Stereo "clip"}}
{{.Call "su_effects_stereohelper"}}
{{- end}}
{{.TailCall "su_clip"}}
{{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" "Opcode"}}
{{- if .Stereo "pan"}}
jc su_op_pan_do ; this time, if this is mono op...
fld st0 ; ...we duplicate the mono into stereo first
su_op_pan_do:
fld dword [{{.Input "pan" "panning"}}] ; p l r
fld1 ; 1 p l r
fsub st1 ; 1-p p l r
fmulp st2 ; p (1-p)*l r
fmulp st2 ; (1-p)*l p*r
ret
{{- else}}
fld dword [{{.Input "pan" "panning"}}] ; p s
fmul st1 ; p*s s
fsub st1, st0 ; p*s s-p*s
; Equal to
; s*p s*(1-p)
fxch ; s*(1-p) s*p SHOULD PROBABLY DELETE, WHY BOTHER
ret
{{- end}}
{{end}}
{{- if .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" "Opcode"}}
lodsw ; al = delay index, ah = delay count
{{- .PushRegs .VAL "DelayVal" .COM "DelayCom" | indent 4}}
movzx ebx, al
{{- if .Library}}
mov {{.SI}}, [{{.Stack "DelayTable"}}] ; when using runtime tables, delaytimes is pulled from the stack so can be a pointer to heap
lea {{.BX}}, [{{.SI}} + {{.BX}}*2]
{{- else}}
{{- .Prepare "su_delay_times" | indent 4}}
lea {{.BX}},[{{.Use "su_delay_times"}} + {{.BX}}*2] ; BX now points to the right position within delay time table
{{- end}}
movzx esi, word [{{.Stack "GlobalTick"}}] ; notice that we load word, so we wrap at 65536
mov {{.CX}}, {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}] ; {{.WRK}} is now the separate delay workspace, as they require a lot more space
{{- if .StereoAndMono "delay"}}
jnc su_op_delay_mono
{{- end}}
{{- if .Stereo "delay"}}
push {{.AX}} ; save _ah (delay count)
fxch ; r l
call su_op_delay_do ; D(r) l process delay for the right channel
pop {{.AX}} ; restore the count for second run
fxch ; l D(r)
su_op_delay_mono: ; flow into mono delay
{{- end}}
call su_op_delay_do ; when stereo delay is not enabled, we could inline this to save 5 bytes, but I expect stereo delay to be farely popular so maybe not worth the hassle
mov {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}],{{.CX}} ; move delay workspace pointer back to stack.
{{- .PopRegs .VAL .COM | indent 4}}
{{- if .SupportsModulation "delay" "delaytime"}}
xor eax, eax
mov dword [{{.Modulation "delay" "delaytime"}}], eax
{{- end}}
ret
;-------------------------------------------------------------------------------
; su_op_delay_do: executes the actual delay
;-------------------------------------------------------------------------------
; Pseudocode:
; q = dr*x
; for (i = 0;i < count;i++)
; s = b[(t-delaytime[i+offset])&65535]
; q += s
; o[i] = o[i]*da+s*(1-da)
; b[t] = f*o[i] +p^2*x
; Perform dc-filtering q and output q
;-------------------------------------------------------------------------------
{{.Func "su_op_delay_do"}} ; x y
fld st0
fmul dword [{{.Input "delay" "pregain"}}] ; p*x y
fmul dword [{{.Input "delay" "pregain"}}] ; p*p*x y
fxch ; y p*p*x
fmul dword [{{.Input "delay" "dry"}}] ; dr*y p*p*x
su_op_delay_loop:
{{- if or (.SupportsModulation "delay" "delaytime") (.SupportsParamValue "delay" "notetracking" 1)}} ; delaytime modulation or note syncing require computing the delay time in floats
fild word [{{.BX}}] ; k dr*y p*p*x, where k = delay time
{{- if .SupportsParamValue "delay" "notetracking" 1}}
test ah, 1 ; note syncing is the least significant bit of ah, 0 = ON, 1 = OFF
jne su_op_delay_skipnotesync
fild dword [{{.INP}}-su_voice.inputs+su_voice.note]
{{.Int 0x3DAAAAAA | .Prepare | indent 8}}
fmul dword [{{.Int 0x3DAAAAAA | .Use}}]
{{.Call "su_power"}}
fdivp st1, st0 ; use 10787 for delaytime to have neutral transpose
su_op_delay_skipnotesync:
{{- end}}
{{- if .SupportsModulation "delay" "delaytime"}}
fld dword [{{.Modulation "delay" "delaytime"}}]
{{- .Float 32767.0 | .Prepare | indent 8}}
fmul dword [{{.Float 32767.0 | .Use}}] ; scale it up, as the modulations would be too small otherwise
faddp st1, st0
{{- end}}
fistp dword [{{.SP}}-4] ; dr*y p*p*x, dword [{{.SP}}-4] = integer amount of delay (samples)
mov edi, esi ; edi = esi = current time
sub di, word [{{.SP}}-4] ; we perform the math in 16-bit to wrap around
{{- else}}
mov edi, esi
sub di, word [{{.BX}}] ; we perform the math in 16-bit to wrap around
{{- end}}
fld dword [{{.CX}}+su_delayline_wrk.buffer+{{.DI}}*4]; s dr*y p*p*x, where s is the sample from delay buffer
fadd st1, st0 ; s dr*y+s p*p*x (add comb output to current output)
fld1 ; 1 s dr*y+s p*p*x
fsub dword [{{.Input "delay" "damp"}}] ; 1-da s dr*y+s p*p*x
fmulp st1, st0 ; s*(1-da) dr*y+s p*p*x
fld dword [{{.Input "delay" "damp"}}] ; da s*(1-da) dr*y+s p*p*x
fmul dword [{{.CX}}+su_delayline_wrk.filtstate] ; o*da s*(1-da) dr*y+s p*p*x, where o is stored
faddp st1, st0 ; o*da+s*(1-da) dr*y+s p*p*x
{{- .Float 0.5 | .Prepare | indent 4}}
fadd dword [{{.Float 0.5 | .Use}}] ; add and sub small offset to prevent denormalization. WARNING: this is highly important, as the damp filters might denormalize and give 100x CPU penalty
fsub dword [{{.Float 0.5 | .Use}}] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
fst dword [{{.CX}}+su_delayline_wrk.filtstate] ; o'=o*da+s*(1-da), o' dr*y+s p*p*x
fmul dword [{{.Input "delay" "feedback"}}] ; f*o' dr*y+s p*p*x
fadd st0, st2 ; f*o'+p*p*x dr*y+s p*p*x
fstp dword [{{.CX}}+su_delayline_wrk.buffer+{{.SI}}*4]; save f*o'+p*p*x to delay buffer
add {{.BX}},2 ; move to next index
add {{.CX}}, su_delayline_wrk.size ; go to next delay delay workspace
sub ah, 2
jg su_op_delay_loop ; if ah > 0, goto loop
fstp st1 ; dr*y+s1+s2+s3+...
; DC-filtering
fld dword [{{.CX}}+su_delayline_wrk.dcout] ; o s
{{- .Float 0.99609375 | .Prepare | indent 4}}
fmul dword [{{.Float 0.99609375 | .Use}}] ; c*o s
fsub dword [{{.CX}}+su_delayline_wrk.dcin] ; c*o-i s
fxch ; s c*o-i
fst dword [{{.CX}}+su_delayline_wrk.dcin] ; i'=s, s c*o-i
faddp st1 ; s+c*o-i
{{- .Float 0.5 | .Prepare | indent 4}}
fadd dword [{{.Float 0.5 | .Use}}] ; add and sub small offset to prevent denormalization. WARNING: this is highly important, as low pass filters might denormalize and give 100x CPU penalty
fsub dword [{{.Float 0.5 | .Use}}] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
fst dword [{{.CX}}+su_delayline_wrk.dcout] ; o'=s+c*o-i
ret
{{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" "Opcode"}}
fld st0 ; x x
fmul st0, st0 ; x^2 x
{{- if .StereoAndMono "compressor"}}
jnc su_op_compressor_mono
{{- end}}
{{- if .Stereo "compressor"}}
fld st2 ; r x^2 l r
fst st3 ; y x^2 l r
fmul st0, st0 ; y^2 x^2 l r
faddp st1, st0 ; y^2+x^2 l r
{{- if .StereoAndMono "compressor"}}
call su_op_compressor_mono ; So, for stereo, we square both left & right and add them up
fld st0 ; and return the computed gain two times, ready for MULP STEREO
ret
su_op_compressor_mono:
{{- end}}
{{- end}}
fld dword [{{.WRK}}] ; l x^2 x
fucomi st0, st1
setnb al ; if (st0 >= st1) al = 1; else al = 0;
fsubp st1, st0 ; x^2-l x
{{.Call "su_nonlinear_map"}} ; c x^2-l x, c is either attack or release parameter mapped in a nonlinear way
fmulp st1, st0 ; c*(x^2-l) x
fadd dword [{{.WRK}}] ; l+c*(x^2-l) x // we could've kept level in the stack and save a few bytes, but su_env_map uses 3 stack (c + 2 temp), so the stack was getting quite big.
; TODO: make this denormalization optional, if the user wants to save some space
{{- .Float 0.5 | .Prepare | indent 4}}
fadd dword [{{.Float 0.5 | .Use}}] ; add and sub small offset to prevent denormalization. WARNING: this is highly important, as the damp filters might denormalize and give 100x CPU penalty
fsub dword [{{.Float 0.5 | .Use}}] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
fst dword [{{.WRK}}] ; l'=l+c*(x^2-l), l' x
fld dword [{{.Input "compressor" "threshold"}}] ; t l' x
fmul st0, st0 ; t*t l' x
fxch ; l' t*t x
fucomi st0, st1 ; if l' < t*t
fcmovb st0, st1 ; l'=t*t
fdivp st1, st0 ; t*t/l' x
fld dword [{{.Input "compressor" "ratio"}}] ; r t*t/l' x
{{.Float 0.5 | .Prepare | indent 4}}
fmul dword [{{.Float 0.5 | .Use}}] ; p=r/2 t*t/l' x
fxch ; t*t/l' p x
fyl2x ; p*log2(t*t/l') x
{{.Call "su_power"}} ; 2^(p*log2(t*t/l')) x
; Equal to:
; (t*t/l')^p x
; if ratio is at minimum => p=0 => 1 x
; if ratio is at maximum => p=0.5 => t/x => t/x*x=t
fdiv dword [{{.Input "compressor" "invgain"}}]; this used to be pregain but that ran into problems with getting back up to 0 dB so postgain should be better at that
{{- if and (.Stereo "compressor") (not (.Mono "compressor"))}}
fld st0 ; and return the computed gain two times, ready for MULP STEREO
{{- end}}
ret
{{- end}}

View File

@ -0,0 +1,44 @@
{{- 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" "Opcode"}}
{{- .Float 2.206896551724138 | .Prepare | indent 4}}
fmul dword [{{.Float 2.206896551724138 | .Use}}] ; (2*s-1)*64/24, let's call this p from now on
{{.Call "su_power"}}
fld1 ; 1 2^p
fsubp st1, st0 ; 2^p-1, the player is advancing 1 tick by its own
fadd dword [{{.WRK}}] ; t+2^p-1, t is the remainder from previous rounds as ticks have to be rounded to 1
push {{.AX}}
fist dword [{{.SP}}] ; Main stack: k=int(t+2^p-1)
fisub dword [{{.SP}}] ; t+2^p-1-k, the remainder
pop {{.AX}}
add dword [{{.Stack "Sample"}}], eax ; add the whole ticks to row tick count
fstp dword [{{.WRK}}] ; save the remainder for future
ret
{{end}}
{{- if or .RowSync (.HasOp "sync")}}
;-------------------------------------------------------------------------------
; SYNC opcode: save the stack top to sync buffer
;-------------------------------------------------------------------------------
{{.Func "su_op_sync" "Opcode"}}
{{- if not .Library}}
; TODO: syncs are NOPs when compiling as library, should figure out a way to
; make them work when compiling to use the native track also
mov {{.AX}}, [{{.Stack "GlobalTick"}}]
test al, al
jne su_op_sync_skip
xchg {{.AX}}, [{{.Stack "SyncBufPtr"}}]
fst dword [{{.AX}}]
add {{.AX}}, 4
xchg {{.AX}}, [{{.Stack "SyncBufPtr"}}]
su_op_sync_skip:
{{- end}}
ret
{{end}}

View File

@ -0,0 +1,50 @@
{{- if .SupportsParamValue "oscillator" "type" .Sample}}
{{- if eq .OS "windows"}}
{{.ExportFunc "su_load_gmdls"}}
{{- if .Amd64}}
extern OpenFile ; requires windows
extern ReadFile ; requires windows
; Win64 ABI: RCX, RDX, R8, and R9
sub rsp, 40 ; Win64 ABI requires "shadow space" + space for one parameter.
mov rdx, qword su_sample_table
mov rcx, qword su_gmdls_path1
xor r8,r8 ; OF_READ
push rdx ; &ofstruct, blatantly reuse the sample table
push rcx
call OpenFile ; eax = OpenFile(path,&ofstruct,OF_READ)
pop rcx
pop rdx
movsxd rcx, eax
mov qword [rsp+32], 0
mov r9, rdx
mov r8d, 3440660 ; number of bytes to read
call ReadFile ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
add rsp, 40 ; shadow space, as required by Win64 ABI
ret
{{else}}
mov ebx, su_sample_table
push 0 ; OF_READ
push ebx ; &ofstruct, blatantly reuse the sample table
push su_gmdls_path1 ; path
call dword [__imp__OpenFile@12]; eax = OpenFile(path,&ofstruct,OF_READ) // should not touch ebx according to calling convention
push 0 ; NULL
push ebx ; &bytes_read, reusing sample table again; it does not matter that the first four bytes are trashed
push 3440660 ; number of bytes to read
push ebx ; here we actually pass the sample table to readfile
push eax ; handle to file
call dword [__imp__ReadFile@20] ; Readfile(handle,&su_sample_table,SAMPLE_TABLE_SIZE,&bytes_read,NULL)
ret
extern __imp__OpenFile@12 ; requires windows
extern __imp__ReadFile@20
; requires windows
{{end}}
{{.Data "su_gmdls_path1"}}
db 'drivers/gm.dls',0
{{end}}
{{.SectBss "susamtable"}}
su_sample_table:
resb 3440660 ; size of gmdls.
{{end}}

View File

@ -0,0 +1,134 @@
{{template "structs.asm" .}}
struc su_synth
.synth_wrk resb su_synthworkspace.size
.delay_wrks resb su_delayline_wrk.size * 64
.delaytimes resw 768
.sampleoffs resb su_sample_offset.size * 256
.randseed resd 1
.globaltime resd 1
.commands resb 32 * 64
.values resb 32 * 64 * 8
.polyphony resd 1
.numvoices resd 1
endstruc
{{.ExportFunc "su_render" "SynthStateParam" "BufferPtrParam" "SamplesParam" "TimeParam"}}
{{- if .Amd64}}
{{- if eq .OS "windows"}}
{{- .PushRegs "rdi" "NonVolatileRDI" "rsi" "NonVolatileRSI" "rbx" "NonVolatileRBX" "rbp" "NonVolatileRBP" | indent 4}}
mov rsi, r8 ; rsi = &samples
mov rbx, r9 ; rbx = &time
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
{{- .PushRegs "rbx" "NonVolatileRBX" "rbp" "NonVolatileRBP" | indent 4}}
mov rbx, rcx ; rbx points to time
xchg rsi, rdx ; rdx points to buffer, rsi points to samples
mov rcx, rdi ; rcx = &Synthstate
{{- end}}
{{- else}}
{{- .PushRegs | indent 4 }} ; push registers
mov ecx, [{{.Stack "SynthStateParam"}}] ; ecx = &synthState
mov edx, [{{.Stack "BufferPtrParam"}}] ; edx = &buffer
mov esi, [{{.Stack "SamplesParam"}}] ; esi = &samples
mov ebx, [{{.Stack "TimeParam"}}] ; ebx = &time
{{- end}}
{{.SaveFPUState | indent 4}} ; save the FPU state to stack & reset the FPU
{{.Push .SI "Samples"}}
{{.Push .BX "Time"}}
xor eax, eax ; samplenumber starts at 0
{{.Push .AX "BufSample"}}
mov esi, [{{.SI}}] ; zero extend dereferenced pointer
{{.Push .SI "BufSize"}}
{{.Push .DX "BufPtr"}}
{{.Push .CX "SynthState"}}
lea {{.AX}}, [{{.CX}} + su_synth.sampleoffs]
{{.Push .AX "SampleTable"}}
lea {{.AX}}, [{{.CX}} + su_synth.delaytimes]
{{.Push .AX "DelayTable"}}
mov eax, [{{.CX}} + su_synth.randseed]
{{.Push .AX "RandSeed"}}
mov eax, [{{.CX}} + su_synth.globaltime]
{{.Push .AX "GlobalTick"}}
mov ebx, dword [{{.BX}}] ; zero extend dereferenced pointer
{{.Push .BX "RowLength"}} ; the nominal rowlength should be time_in
xor eax, eax ; rowtick starts at 0
su_render_samples_loop:
push {{.DI}}
fnstsw [{{.SP}}] ; store the FPU status flag to stack top
pop {{.DI}} ; {{.DI}} = FPU status flag
and {{.DI}}, 0b0011100001000101 ; mask TOP pointer, stack error, zero divide and in{{.VAL}}id operation
test {{.DI}},{{.DI}} ; all the aforementioned bits should be 0!
jne su_render_samples_time_finish ; otherwise, we exit due to error
cmp eax, [{{.Stack "RowLength"}}] ; if rowtick >= maxtime
jge su_render_samples_time_finish ; goto finish
mov ecx, [{{.Stack "BufSize"}}] ; ecx = buffer length in samples
cmp [{{.Stack "BufSample"}}], ecx ; if samples >= maxsamples
jge su_render_samples_time_finish ; goto finish
inc eax ; time++
inc dword [{{.Stack "BufSample"}}] ; samples++
mov {{.CX}}, [{{.Stack "SynthState"}}]
{{.Push .AX "Sample"}}
mov eax, [{{.CX}} + su_synth.polyphony]
{{.Push .AX "PolyphonyBitmask"}}
mov eax, [{{.CX}} + su_synth.numvoices]
{{.Push .AX "VoicesRemain"}}
lea {{.DX}}, [{{.CX}}+ su_synth.synth_wrk]
lea {{.COM}}, [{{.CX}}+ su_synth.commands]
lea {{.VAL}}, [{{.CX}}+ su_synth.values]
lea {{.WRK}}, [{{.DX}} + su_synthworkspace.voices]
lea {{.CX}}, [{{.CX}}+ su_synth.delay_wrks - su_delayline_wrk.filtstate]
{{.Call "su_run_vm"}}
{{.Pop .AX}}
{{.Pop .AX}}
mov {{.DI}}, [{{.Stack "BufPtr"}}] ; edi containts buffer ptr
mov {{.CX}}, [{{.Stack "SynthState"}}]
lea {{.SI}}, [{{.CX}} + su_synth.synth_wrk + su_synthworkspace.left]
movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer
mov [{{.Stack "BufPtr"}}], {{.DI}} ; save back the updated ptr
lea {{.DI}}, [{{.SI}}-8]
xor eax, eax
stosd ; clear left channel so the VM is ready to write them again
stosd ; clear right channel so the VM is ready to write them again
{{.Pop .AX}}
inc dword [{{.Stack "GlobalTick"}}] ; increment global time, used by delays
jmp su_render_samples_loop
su_render_samples_time_finish:
{{.Pop .CX}}
{{.Pop .BX}}
{{.Pop .DX}}
{{.Pop .CX}}
{{.Pop .CX}}
{{.Pop .CX}}
mov [{{.CX}} + su_synth.randseed], edx
mov [{{.CX}} + su_synth.globaltime], ebx
{{.Pop .BX}}
{{.Pop .BX}}
{{.Pop .DX}}
{{.Pop .BX}}
{{.Pop .SI}}
mov dword [{{.SI}}], edx ; *samples = samples rendered
mov dword [{{.BX}}], eax ; *time = time ticks rendered
mov {{.AX}},{{.DI}} ; {{.DI}} was the masked FPU status flag, {{.AX}} is return {{.VAL}}ue
{{.LoadFPUState | indent 4}} ; load the FPU state from stack
{{- if .Amd64}}
{{- if eq .OS "windows"}}
{{- .PopRegs "rdi" "rsi" "rbx" "rbp" | indent 4}}
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
{{- .PopRegs "rbx" "rbp" | indent 4}}
{{- end}}
ret
{{- else}}
mov [{{.Stack "eax"}}],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop
{{- .PopRegs | indent 4 }} ; popad
ret 16
{{- end}}
{{template "patch.asm" .}}
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
{{.SectData "constants"}}
{{.Constants}}

View File

@ -0,0 +1,100 @@
#ifndef _SOINTU_H
#define _SOINTU_H
#pragma pack(push,1) // this should be fine for both Go and assembly
typedef struct Unit {
float State[8];
float Ports[8];
} Unit;
typedef struct Voice {
int Note;
int Release;
float Inputs[8];
float Reserved[6];
struct Unit Units[63];
} Voice;
typedef struct DelayWorkspace {
float Buffer[65536];
float Dcin;
float Dcout;
float Filtstate;
} DelayWorkspace;
typedef struct SynthWorkspace {
unsigned char Curvoices[32];
float Left;
float Right;
float Aux[6];
struct Voice Voices[32];
} SynthWorkspace;
typedef struct SampleOffset {
unsigned int Start;
unsigned short LoopStart;
unsigned short LoopLength;
} SampleOffset;
typedef struct Synth {
struct SynthWorkspace SynthWrk;
struct DelayWorkspace DelayWrks[64]; // let's keep this as 64 for now, so the delays take 16 meg. If that's too little or too much, we can change this in future.
unsigned short DelayTimes[768];
struct SampleOffset SampleOffsets[256];
unsigned int RandSeed;
unsigned int GlobalTick;
unsigned char Commands[32 * 64];
unsigned char Values[32 * 64 * 8];
unsigned int Polyphony;
unsigned int NumVoices;
} Synth;
#pragma pack(pop)
#if UINTPTR_MAX == 0xffffffff // are we 32-bit?
#if defined(__clang__) || defined(__GNUC__)
#define CALLCONV __attribute__ ((stdcall))
#elif defined(_WIN32)
#define CALLCONV __stdcall // on 32-bit platforms, we just use stdcall, as all know it
#endif
#else // 64-bit
#define CALLCONV // the asm will use honor honor correct x64 ABI on all 64-bit platforms
#endif
void CALLCONV su_load_gmdls(void);
// int su_render(Synth* synth, float* buffer, int* samples, int* time):
// Renders samples until 'samples' number of samples are reached or 'time' number of
// modulated time ticks are reached, whichever happens first. 'samples' and 'time' are
// are passed by reference as the function modifies to tell how many samples were
// actually rendered and how many time ticks were actually advanced.
//
// Parameters:
// synth pointer to the synthesizer used. RandSeed should be > 0 e.g. 1
// buffer audio sample buffer, L R L R ...
// samples pointer to the maximum number of samples to be rendered.
// buffer should have a length of 2 * maxsamples as the audio
// is stereo.
// time maximum modulated time rendered.
//
// The value referred by samples is changed to contain the actual number of samples rendered
// Similarly, the value referred by time is changed to contain the number of time ticks advanced.
// If samples_out == samples_in, then is must be that time_in <= time_out.
// If samples_out < samples_in, then time_out >= time_in. Note that it could happen that
// time_out > time_in, as it is modulated and the time could advance by 2 or more, so the loop
// exit condition would fire when the current time is already past time_in
//
// Returns an error code, which is actually just masked version of the FPU Status Word
// On a succesful run, the return value should be 0
// Error code bits:
// bit 0 FPU invalid operation (stack over/underflow OR invalid arithmetic e.g. NaNs)
// bit 2 Divide by zero occurred
// bit 6 Stack overflow or underflow occurred
// bits 11-13 The top pointer of the fpu stack. Any other value than 0 indicates that some values were left on the stack.
int CALLCONV su_render(Synth* synth, float* buffer, int* samples, int* time);
#define SU_ADVANCE_ID 0
{{- range $index, $element := .Instructions}}
#define {{printf "su_%v_id" $element | upper | printf "%-20v"}}{{add1 $index | mul 2}}
{{- end}}
#endif // _SOINTU_H

View File

@ -0,0 +1,44 @@
{{- if not .Output16Bit }}
{{- if not .Clip }}
mov {{.DI}}, [{{.Stack "OutputBufPtr"}}] ; edi containts ptr
mov {{.SI}}, {{.PTRWORD}} su_synth_obj + su_synthworkspace.left
movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer
mov [{{.Stack "OutputBufPtr"}}], {{.DI}} ; save back the updated ptr
lea {{.DI}}, [{{.SI}}-8]
xor eax, eax
stosd ; clear left channel so the VM is ready to write them again
stosd ; clear right channel so the VM is ready to write them again
{{ else }}
mov {{.SI}}, qword [{{.Stack "OutputBufPtr"}}] ; esi points to the output buffer
xor ecx,ecx
xor eax,eax
%%loop: ; loop over two channels, left & right
do fld dword [,su_synth_obj+su_synthworkspace.left,_CX*4,]
{{.Call "su_clip"}}
fstp dword [_SI]
do mov dword [,su_synth_obj+su_synthworkspace.left,_CX*4,{],eax} ; clear the sample so the VM is ready to write it
add _SI,4
cmp ecx,2
jl %%loop
mov dword [_SP+su_stack.bufferptr - su_stack.output_sound], _SI ; save esi back to stack
{{ end }}
{{- else}}
mov {{.SI}}, [{{.Stack "OutputBufPtr"}}] ; esi points to the output buffer
mov {{.DI}}, {{.PTRWORD}} su_synth_obj+su_synthworkspace.left
mov ecx, 2
output_sound16bit_loop: ; loop over two channels, left & right
fld dword [{{.DI}}]
{{.Call "su_clip"}}
{{- .Float 32767.0 | .Prepare | indent 16}}
fmul dword [{{.Float 32767.0 | .Use}}]
push {{.AX}}
fistp dword [{{.SP}}]
pop {{.AX}}
mov word [{{.SI}}],ax ; // store integer converted right sample
xor eax,eax
stosd
add {{.SI}},2
loop output_sound16bit_loop
mov [{{.Stack "OutputBufPtr"}}], {{.SI}} ; save esi back to stack
{{- end }}

View File

@ -0,0 +1,215 @@
;-------------------------------------------------------------------------------
; su_run_vm function: runs the entire virtual machine once, creating 1 sample
;-------------------------------------------------------------------------------
; Input: su_synth_obj.left : Set to 0 before calling
; su_synth_obj.right : Set to 0 before calling
; _CX : Pointer to delay workspace (if needed)
; _DX : Pointer to synth object
; COM : Pointer to command stream
; VAL : Pointer to value stream
; WRK : Pointer to the last workspace processed
; Output: su_synth_obj.left : left sample
; su_synth_obj.right : right sample
; Dirty: everything
;-------------------------------------------------------------------------------
{{.Func "su_run_vm"}}
{{- .PushRegs .CX "DelayWorkSpace" .DX "Synth" .COM "CommandStream" .WRK "Voice" .VAL "ValueStream" | indent 4}}
{{- if .RowSync}}
fild dword [{{.Stack "Sample"}}]
{{.Int .Song.SamplesPerRow | .Prepare | indent 8}}
fidiv dword [{{.Int .Song.SamplesPerRow | .Use}}]
fiadd dword [{{.Stack "Row"}}]
{{.Call "su_op_sync"}}
fstp st0
{{- end}}
su_run_vm_loop: ; loop until all voices done
movzx edi, byte [{{.COM}}] ; edi = command byte
inc {{.COM}} ; move to next instruction
add {{.WRK}}, su_unit.size ; move WRK to next unit
shr edi, 1 ; shift out the LSB bit = stereo bit
je su_run_vm_advance ; the opcode is zero, jump to advance
mov {{.INP}}, [{{.Stack "Voice"}}] ; reset INP to point to the inputs part of voice
pushf ; push flags to save carry = stereo bit
add {{.INP}}, su_voice.inputs
xor ecx, ecx ; counter = 0
xor eax, eax ; clear out high bits of eax, as lodsb only sets al
su_transform_values_loop:
{{- .Prepare "su_vm_transformcounts-1" | indent 4}}
cmp cl, byte [{{.Use "su_vm_transformcounts-1"}}+{{.DI}}] ; compare the counter to the value in the param count table
je su_transform_values_out
lodsb ; load the byte value from VAL stream
push {{.AX}} ; push it to memory so FPU can read it
fild dword [{{.SP}}] ; load the value to FPU stack
{{- .Prepare (.Float 0.0078125) | indent 4}}
fmul dword [{{.Use (.Float 0.0078125)}}] ; divide it by 128 (0 => 0, 128 => 1.0)
fadd dword [{{.WRK}}+su_unit.ports+{{.CX}}*4] ; add the modulations in the current workspace
fstp dword [{{.INP}}+{{.CX}}*4] ; store the modulated value in the inputs section of voice
xor eax, eax
mov dword [{{.WRK}}+su_unit.ports+{{.CX}}*4], eax ; clear out the modulation ports
pop {{.AX}}
inc ecx
jmp su_transform_values_loop
su_transform_values_out:
popf ; pop flags for the carry bit = stereo bit
{{- .SaveStack "Opcode"}}
{{- $x := printf "su_vm_jumptable-%v" .PTRSIZE}}
{{- .Prepare $x | indent 4}}
call [{{.Use $x}}+{{.DI}}*{{.PTRSIZE}}] ; call the function corresponding to the instruction
jmp su_run_vm_loop
su_run_vm_advance:
{{- if .SupportsPolyphony}}
mov {{.WRK}}, [{{.Stack "Voice"}}] ; WRK points to start of current voice
add {{.WRK}}, su_voice.size ; move to next voice
mov [{{.Stack "Voice"}}], {{.WRK}} ; update the pointer in the stack to point to the new voice
mov ecx, [{{.Stack "VoicesRemain"}}] ; ecx = how many voices remain to process
dec ecx ; decrement number of voices to process
bt dword [{{.Stack "PolyphonyBitmask"}}], ecx ; if voice bit of su_polyphonism not set
jnc su_op_advance_next_instrument ; goto next_instrument
mov {{.VAL}}, [{{.Stack "ValueStream"}}] ; if it was set, then repeat the opcodes for the current voice
mov {{.COM}}, [{{.Stack "CommandStream"}}]
su_op_advance_next_instrument:
mov [{{.Stack "ValueStream"}}], {{.VAL}} ; save current VAL as a checkpoint
mov [{{.Stack "CommandStream"}}], {{.COM}} ; save current COM as a checkpoint
su_op_advance_finish:
mov [{{.Stack "VoicesRemain"}}], ecx
jne su_run_vm_loop ; ZF was set by dec ecx
{{- else}}
mov {{.WRK}}, {{.PTRWORD}} [{{.Stack "Voice"}}] ; load pointer to voice to register
add {{.WRK}}, su_voice.size ; shift it to point to following voice
mov {{.PTRWORD}} [{{.Stack "Voice"}}], {{.WRK}} ; save back to stack
dec dword [{{.Stack "VoicesRemain"}}] ; voices--
jne su_run_vm_loop ; if there's more voices to process, goto vm_loop
{{- end}}
{{- .PopRegs .CX .DX .COM .WRK .VAL | indent 4}}
ret
{{- template "arithmetic.asm" .}}
{{- template "effects.asm" .}}
{{- template "flowcontrol.asm" .}}
{{- template "sinks.asm" .}}
{{- template "sources.asm" .}}
{{- template "gmdls.asm" .}}
{{- if .HasCall "su_nonlinear_map"}}
;-------------------------------------------------------------------------------
; su_nonlinear_map function: returns 2^(-24*x) of parameter number _AX
;-------------------------------------------------------------------------------
; Input: _AX : parameter number (e.g. for envelope: 0 = attac, 1 = decay...)
; INP : pointer to transformed values
; Output: st0 : 2^(-24*x), where x is the parameter in the range 0-1
;-------------------------------------------------------------------------------
{{.Func "su_nonlinear_map"}}
fld dword [{{.INP}}+{{.AX}}*4] ; x, where x is the parameter in the range 0-1
{{.Prepare (.Int 24)}}
fimul dword [{{.Use (.Int 24)}}] ; 24*x
fchs ; -24*x
{{end}}
{{- if or (.HasCall "su_power") (.HasCall "su_nonlinear_map")}}
;-------------------------------------------------------------------------------
; su_power function: computes 2^x
;-------------------------------------------------------------------------------
; Input: st0 : x
; Output: st0 : 2^x
;-------------------------------------------------------------------------------
{{- if not (.HasCall "su_nonlinear_map")}}{{.SectText "su_power"}}{{end}}
su_power:
fld1 ; 1 x
fld st1 ; x 1 x
fprem ; mod(x,1) 1 x
f2xm1 ; 2^mod(x,1)-1 1 x
faddp st1,st0 ; 2^mod(x,1) x
fscale ; 2^mod(x,1)*2^trunc(x) x
; Equal to:
; 2^x x
fstp st1 ; 2^x
ret
{{end}}
{{- if .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))
; This is placed here to be able to flow into waveshaper & also include
; wave shaper if needed by some other function; need to investigate the
; best way to do this
;-------------------------------------------------------------------------------
{{.Func "su_op_distort" "Opcode"}}
{{- if .Stereo "distort" -}}
{{.Call "su_effects_stereohelper"}}
{{- end}}
fld dword [{{.Input "distort" "drive"}}]
{{end}}
{{- if or (.HasCall "su_waveshaper") (.HasOp "distort")}}
{{- if .HasOp "distort"}}
su_waveshaper:
{{- else}}
{{.Func "su_waveshaper"}}
{{- end}}
fld st0 ; a a x
{{.Prepare (.Float 0.5)}}
fsub dword [{{.Use (.Float 0.5)}}] ; a-.5 a x
fadd st0 ; 2*a-1 a x
fld st2 ; x 2*a-1 a x
fabs ; abs(x) 2*a-1 a x
fmulp st1 ; (2*a-1)*abs(x) a x
fld1 ; 1 (2*a-1)*abs(x) a x
faddp st1 ; 1+(2*a-1)*abs(x) a x
fsub st1 ; 1-a+(2*a-1)*abs(x) a x
fdivp st1, st0 ; a/(1-a+(2*a-1)*abs(x)) x
fmulp st1 ; x*a/(1-a+(2*a-1)*abs(x))
ret
{{end}}
{{- if .HasCall "su_effects_stereohelper" }}
;-------------------------------------------------------------------------------
; su_effects_stereohelper: moves the workspace to next, does the filtering for
; right channel (pulling the calling address from stack), rewinds the
; workspace and returns
;-------------------------------------------------------------------------------
{{.Func "su_effects_stereohelper"}}
jnc su_effects_stereohelper_mono ; carry is still the stereo bit
add {{.WRK}}, 16
fxch ; r l
call [{{.SP}}] ; call whoever called me...
fxch ; l r
sub {{.WRK}}, 16 ; move WRK back to where it was
su_effects_stereohelper_mono:
ret ; return to process l/mono sound
{{end}}
{{- if .HasCall "su_clip"}}
{{.Func "su_clip"}}
fld1 ; 1 x a
fucomi st1 ; if (1 <= x)
jbe short su_clip_do ; goto Clip_Do
fchs ; -1 x a
fucomi st1 ; if (-1 < x)
fcmovb st0, st1 ; x x a
su_clip_do:
fstp st1 ; x' a, where x' = clamp(x)
ret
{{end}}
;-------------------------------------------------------------------------------
; The opcode table jump table. This is constructed to only include the opcodes
; that are used so that the jump table is as small as possible.
;-------------------------------------------------------------------------------
{{.Data "su_vm_jumptable"}}
{{- range .Instructions}}
{{$.DPTR}} su_op_{{.}}
{{- end}}
;-------------------------------------------------------------------------------
; The number of transformed parameters each opcode takes
;-------------------------------------------------------------------------------
{{.Data "su_vm_transformcounts"}}
{{- range .Instructions}}
db {{$.TransformCount .}}
{{- end}}

View File

@ -0,0 +1,254 @@
{{template "structs.asm" .}}
;-------------------------------------------------------------------------------
; Uninitialized data: The synth object
;-------------------------------------------------------------------------------
{{.SectBss "synth_object"}}
su_synth_obj:
resb su_synthworkspace.size
resb {{.Song.Patch.NumDelayLines}}*su_delayline_wrk.size
{{- if or .RowSync (.HasOp "sync")}}
{{- if or (and (eq .OS "windows") (not .Amd64)) (eq .OS "darwin")}}
extern _syncBuf
{{- else}}
extern syncBuf
{{- end}}
{{- end}}
;-------------------------------------------------------------------------------
; su_render_song function: the entry point for the synth
;-------------------------------------------------------------------------------
; Has the signature su_render_song(void *ptr), where ptr is a pointer to
; the output buffer. Renders the compile time hard-coded song to the buffer.
; Stack: output_ptr
;-------------------------------------------------------------------------------
{{.ExportFunc "su_render_song" "OutputBufPtr"}}
{{- if .Amd64}}
{{- if eq .OS "windows"}}
{{- .PushRegs "rcx" "OutputBufPtr" "rdi" "NonVolatileRsi" "rsi" "NonVolatile" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}} ; rcx = ptr to buf. rdi,rsi,rbx,rbp nonvolatile
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
{{- .PushRegs "rdi" "OutputBufPtr" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}}
{{- end}}
{{- else}}
{{- .PushRegs | indent 4}}
{{- end}}
{{- $prologsize := len .Stacklocs}}
{{- if or .RowSync (.HasOp "sync")}}
{{- if or (and (eq .OS "windows") (not .Amd64)) (eq .OS "darwin")}}
{{- .Prepare "_syncBuf"}}
{{.Push (.Use "_syncBuf") "SyncBufPtr"}}
{{- else}}
{{- .Prepare "syncBuf"}}
{{.Push (.Use "syncBuf") "SyncBufPtr"}}
{{- end}}
{{- end}}
xor eax, eax
{{- if ne .VoiceTrackBitmask 0}}
{{.Push (.VoiceTrackBitmask | printf "%v") "VoiceTrackBitmask"}}
{{- end}}
{{.Push "1" "RandSeed"}}
{{.Push .AX "GlobalTick"}}
su_render_rowloop: ; loop through every row in the song
{{.Push .AX "Row"}}
{{.Call "su_update_voices"}} ; update instruments for the new row
xor eax, eax ; ecx is the current sample within row
su_render_sampleloop: ; loop through every sample in the row
{{.Push .AX "Sample"}}
{{- if .SupportsPolyphony}}
{{.Push (.PolyphonyBitmask | printf "%v") "PolyphonyBitmask"}} ; does the next voice reuse the current opcodes?
{{- end}}
{{.Push (.Song.Patch.NumVoices | printf "%v") "VoicesRemain"}}
mov {{.DX}}, {{.PTRWORD}} su_synth_obj ; {{.DX}} points to the synth object
mov {{.COM}}, {{.PTRWORD}} su_patch_code ; COM points to vm code
mov {{.VAL}}, {{.PTRWORD}} su_patch_parameters ; VAL points to unit params
{{- if .HasOp "delay"}}
mov {{.CX}}, {{.PTRWORD}} su_synth_obj + su_synthworkspace.size - su_delayline_wrk.filtstate
{{- end}}
lea {{.WRK}}, [{{.DX}} + su_synthworkspace.voices] ; WRK points to the first voice
{{.Call "su_run_vm"}} ; run through the VM code
{{.Pop .AX}}
{{- if .SupportsPolyphony}}
{{.Pop .AX}}
{{- end}}
{{- template "output_sound.asm" .}} ; *ptr++ = left, *ptr++ = right
{{.Pop .AX}}
inc dword [{{.Stack "GlobalTick"}}] ; increment global time, used by delays
inc eax
cmp eax, {{.Song.SamplesPerRow}}
jl su_render_sampleloop
{{.Pop .AX}} ; Stack: pushad ptr
inc eax
cmp eax, {{mul .PatternLength .SequenceLength}}
jl su_render_rowloop
; rewind the stack the entropy of multiple pop {{.AX}} is probably lower than add
{{- range slice .Stacklocs $prologsize}}
{{$.Pop $.AX}}
{{- end}}
{{- if .Amd64}}
{{- if eq .OS "windows"}}
; Windows64 ABI, rdi rsi rbx rbp non-volatile
{{- .PopRegs "rcx" "rdi" "rsi" "rbx" "rbp" | indent 4}}
{{- else}}
; SystemV64 ABI (linux mac or hopefully something similar), rbx rbp non-volatile
{{- .PopRegs "rdi" "rbx" "rbp" | indent 4}}
{{- end}}
ret
{{- else}}
{{- .PopRegs | indent 4}}
ret 4
{{- end}}
;-------------------------------------------------------------------------------
; su_update_voices function: polyphonic & chord implementation
;-------------------------------------------------------------------------------
; Input: eax : current row within song
; Dirty: pretty much everything
;-------------------------------------------------------------------------------
{{.Func "su_update_voices"}}
{{- if ne .VoiceTrackBitmask 0}}
; The more complicated implementation: one track can trigger multiple voices
xor edx, edx
mov ebx, {{.PatternLength}} ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256...
div ebx ; eax = current pattern, edx = current row in pattern
{{.Prepare "su_tracks"}}
lea {{.SI}}, [{{.Use "su_tracks"}}+{{.AX}}] ; esi points to the pattern data for current track
xor eax, eax ; eax is the first voice of next track
xor ebx, ebx ; ebx is the first voice of current track
mov {{.BP}}, {{.PTRWORD}} su_synth_obj ; ebp points to the current_voiceno array
su_update_voices_trackloop:
movzx eax, byte [{{.SI}}] ; eax = current pattern
imul eax, {{.PatternLength}} ; eax = offset to current pattern data
{{- .Prepare "su_patterns" .AX | indent 4}}
movzx eax,byte [{{.Use "su_patterns" .AX}},{{.DX}}] ; eax = note
push {{.DX}} ; Stack: ptrnrow
xor edx, edx ; edx=0
mov ecx, ebx ; ecx=first voice of the track to be done
su_calculate_voices_loop: ; do {
bt dword [{{.Stack "VoiceTrackBitmask"}} + {{.PTRSIZE}}],ecx ; test voicetrack_bitmask// notice that the incs don't set carry
inc edx ; edx++ // edx=numvoices
inc ecx ; ecx++ // ecx=the first voice of next track
jc su_calculate_voices_loop ; } while bit ecx-1 of bitmask is on
push {{.CX}} ; Stack: next_instr ptrnrow
cmp al, {{.Hold}} ; anything but hold causes action
je short su_update_voices_nexttrack
mov cl, byte [{{.BP}}]
mov edi, ecx
add edi, ebx
shl edi, 12 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header
{{- .Prepare "su_synth_obj" | indent 4}}
inc dword [{{.Use "su_synth_obj"}} + su_synthworkspace.voices + su_voice.release + {{.DI}}] ; set the voice currently active to release; notice that it could increment any number of times
cmp al, {{.Hold}} ; if cl < HLD (no new note triggered)
jl su_update_voices_nexttrack ; goto nexttrack
inc ecx ; curvoice++
cmp ecx, edx ; if (curvoice >= num_voices)
jl su_update_voices_skipreset
xor ecx,ecx ; curvoice = 0
su_update_voices_skipreset:
mov byte [{{.BP}}],cl
add ecx, ebx
shl ecx, 12 ; each unit = 64 bytes and there are 1<<6 units + small header
lea {{.DI}},[{{.Use "su_synth_obj"}} + su_synthworkspace.voices + {{.CX}}]
stosd ; save note
mov ecx, (su_voice.size - su_voice.release)/4
xor eax, eax
rep stosd ; clear the workspace of the new voice, retriggering oscillators
su_update_voices_nexttrack:
pop {{.BX}} ; ebx=first voice of next instrument, Stack: ptrnrow
pop {{.DX}} ; edx=patrnrow
add {{.SI}}, {{.SequenceLength}}
inc {{.BP}}
{{- $addrname := len .Song.Score.Tracks | printf "su_synth_obj + %v"}}
{{- .Prepare $addrname | indent 8}}
cmp {{.BP}},{{.Use $addrname}}
jl su_update_voices_trackloop
ret
{{- else}}
; The simple implementation: each track triggers always the same voice
xor edx, edx
xor ebx, ebx
mov bl, {{.PatternLength}} ; rows per pattern
div ebx ; eax = current pattern, edx = current row in pattern
{{- .Prepare "su_tracks" | indent 4}}
lea {{.SI}}, [{{.Use "su_tracks"}}+{{.AX}}]; esi points to the pattern data for current track
mov {{.DI}}, {{.PTRWORD}} su_synth_obj+su_synthworkspace.voices
mov bl, {{len .Song.Score.Tracks}} ; MAX_TRACKS is always <= 32 so this is ok
su_update_voices_trackloop:
movzx eax, byte [{{.SI}}] ; eax = current pattern
imul eax, {{.PatternLength}} ; multiply by rows per pattern, eax = offset to current pattern data
{{- .Prepare "su_patterns" .AX | indent 8}}
movzx eax, byte [{{.Use "su_patterns" .AX}} + {{.DX}}] ; ecx = note
cmp al, {{.Hold}} ; anything but hold causes action
je short su_update_voices_nexttrack
inc dword [{{.DI}}+su_voice.release] ; set the voice currently active to release; notice that it could increment any number of times
jb su_update_voices_nexttrack ; if cl < HLD (no new note triggered) goto nexttrack
su_update_voices_retrigger:
stosd ; save note
mov ecx, (su_voice.size - su_voice.release)/4 ; could be xor ecx, ecx; mov ch,...>>8, but will it actually be smaller after compression?
xor eax, eax
rep stosd ; clear the workspace of the new voice, retriggering oscillators
jmp short su_update_voices_skipadd
su_update_voices_nexttrack:
add {{.DI}}, su_voice.size
su_update_voices_skipadd:
add {{.SI}}, {{.SequenceLength}}
dec ebx
jnz short su_update_voices_trackloop
ret
{{- end}}
{{template "patch.asm" .}}
;-------------------------------------------------------------------------------
; Patterns
;-------------------------------------------------------------------------------
{{.Data "su_patterns"}}
{{- range .Patterns}}
db {{. | toStrings | join ","}}
{{- end}}
;-------------------------------------------------------------------------------
; Tracks
;-------------------------------------------------------------------------------
{{.Data "su_tracks"}}
{{- range .Sequences}}
db {{. | toStrings | join ","}}
{{- end}}
{{- if gt (.SampleOffsets | len) 0}}
;-------------------------------------------------------------------------------
; Sample offsets
;-------------------------------------------------------------------------------
{{.Data "su_sample_offsets"}}
{{- range .SampleOffsets}}
dd {{.Start}}
dw {{.LoopStart}}
dw {{.LoopLength}}
{{- end}}
{{end}}
{{- if gt (.DelayTimes | len ) 0}}
;-------------------------------------------------------------------------------
; Delay times
;-------------------------------------------------------------------------------
{{.Data "su_delay_times"}}
dw {{.DelayTimes | toStrings | join ","}}
{{end}}
;-------------------------------------------------------------------------------
; The code for this patch, basically indices to vm jump table
;-------------------------------------------------------------------------------
{{.Data "su_patch_code"}}
db {{.Commands | toStrings | join ","}}
;-------------------------------------------------------------------------------
; The parameters / inputs to each opcode
;-------------------------------------------------------------------------------
{{.Data "su_patch_parameters"}}
db {{.Values | toStrings | join ","}}
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
{{.SectData "constants"}}
{{.Constants}}

View File

@ -0,0 +1,66 @@
// auto-generated by Sointu, editing not recommended
#ifndef SU_RENDER_H
#define SU_RENDER_H
#define SU_LENGTH_IN_SAMPLES {{.MaxSamples}}
#define SU_BUFFER_LENGTH (SU_LENGTH_IN_SAMPLES*2)
#define SU_SAMPLE_RATE 44100
#define SU_BPM {{.Song.BPM}}
#define SU_ROWS_PER_BEAT {{.Song.RowsPerBeat}}
#define SU_ROWS_PER_PATTERN {{.Song.Score.RowsPerPattern}}
#define SU_LENGTH_IN_PATTERNS {{.Song.Score.Length}}
#define SU_LENGTH_IN_ROWS (SU_LENGTH_IN_PATTERNS*SU_PATTERN_SIZE)
#define SU_SAMPLES_PER_ROW (SU_SAMPLE_RATE*60/(SU_BPM*SU_ROWS_PER_BEAT))
{{- if or .RowSync (.HasOp "sync")}}
{{- if .RowSync}}
#define SU_NUMSYNCS {{add1 .Song.Patch.NumSyncs}}
{{- else}}
#define SU_NUMSYNCS {{.Song.Patch.NumSyncs}}
{{- end}}
#define SU_SYNCBUFFER_LENGTH ((SU_LENGTH_IN_SAMPLES+255)>>8)*SU_NUMSYNCS
{{- end}}
#include <stdint.h>
#if UINTPTR_MAX == 0xffffffff
#if defined(__clang__) || defined(__GNUC__)
#define SU_CALLCONV __attribute__ ((stdcall))
#elif defined(_WIN32)
#define SU_CALLCONV __stdcall
#endif
#else
#define SU_CALLCONV
#endif
{{- if .Output16Bit}}
typedef short SUsample;
#define SU_SAMPLE_RANGE 32767.0
#define SU_SAMPLE_PCM16
{{- else}}
typedef float SUsample;
#define SU_SAMPLE_RANGE 1.0
#define SU_SAMPLE_FLOAT
{{- end}}
#ifdef __cplusplus
extern "C" {
#endif
{{- if or .RowSync (.HasOp "sync")}}
#define SU_SYNC
{{- end}}
void SU_CALLCONV su_render_song(SUsample *buffer);
{{- if gt (.SampleOffsets | len) 0}}
void SU_CALLCONV su_load_gmdls();
#define SU_LOAD_GMDLS
{{- end}}
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,128 @@
{{- 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" "Opcode"}} ; l r
mov {{.DI}}, [{{.Stack "Synth"}}] ; DI points to the synth object, use DI consistently in sinks/sources presumably to increase compression rate
{{- if .StereoAndMono "out" }}
jnc su_op_out_mono
{{- end }}
{{- if .Stereo "out" }}
call su_op_out_mono
add {{.DI}}, 4 ; shift from left to right channel
su_op_out_mono:
{{- end}}
fmul dword [{{.Input "out" "gain"}}] ; multiply by gain
fadd dword [{{.DI}} + su_synthworkspace.left] ; add current value of the output
fstp dword [{{.DI}} + su_synthworkspace.left] ; store the new value of the output
ret
{{end}}
{{- 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" "Opcode"}} ; l r
mov {{.DI}}, [{{.Stack "Synth"}}]
{{- if .StereoAndMono "outaux" }}
jnc su_op_outaux_mono
{{- end}}
{{- if .Stereo "outaux" }}
call su_op_outaux_mono
add {{.DI}}, 4
su_op_outaux_mono:
{{- end}}
fld st0 ; l l
fmul dword [{{.Input "outaux" "outgain"}}] ; g*l
fadd dword [{{.DI}} + su_synthworkspace.left] ; g*l+o
fstp dword [{{.DI}} + su_synthworkspace.left] ; o'=g*l+o
fmul dword [{{.Input "outaux" "auxgain"}}] ; h*l
fadd dword [{{.DI}} + su_synthworkspace.aux] ; h*l+a
fstp dword [{{.DI}} + su_synthworkspace.aux] ; a'=h*l+a
ret
{{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" "Opcode"}} ; l r
lodsb
mov {{.DI}}, [{{.Stack "Synth"}}]
{{- if .StereoAndMono "aux" }}
jnc su_op_aux_mono
{{- end}}
{{- if .Stereo "aux" }}
call su_op_aux_mono
add {{.DI}}, 4
su_op_aux_mono:
{{- end}}
fmul dword [{{.Input "aux" "gain"}}] ; g*l
fadd dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4] ; g*l+o
fstp dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4] ; o'=g*l+o
ret
{{end}}
{{- if .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" "Opcode"}}
lodsw
mov {{.CX}}, [{{.Stack "Voice"}}] ; load pointer to voice
{{- if .SupportsGlobalSend}}
pushf ; uh ugly: we save the flags just for the stereo carry bit. Doing the .CX loading later crashed the synth for stereo sends as loading the synth address from stack was f'd up by the "call su_op_send_mono"
test ah, 0x80
jz su_op_send_skipglobal
mov {{.CX}}, [{{.Stack "Synth"}} + {{.PTRSIZE}}]
su_op_send_skipglobal:
popf
{{- end}}
{{- if .StereoAndMono "send"}}
jnc su_op_send_mono
{{- end}}
{{- if .Stereo "send"}}
mov {{.DI}}, {{.AX}}
inc {{.AX}} ; send the right channel first
fxch ; r l
call su_op_send_mono ; (r) l
mov {{.AX}}, {{.DI}} ; move back to original address
test al, 0x8 ; if r was not popped and is still in the stack
jnz su_op_send_mono
fxch ; swap them back: l r
su_op_send_mono:
{{- end}}
test al, 0x8 ; if the SEND_POP bit is not set
jnz su_op_send_skippush
fld st0 ; duplicate the signal on stack: s s
su_op_send_skippush: ; there is signal s, but maybe also another: s (s)
fld dword [{{.Input "send" "amount"}}] ; a l (l)
{{- .Float 0.5 | .Prepare | indent 4}}
fsub dword [{{.Float 0.5 | .Use}}] ; a-.5 l (l)
fadd st0 ; g=2*a-1 l (l)
and ah, 0x7f ; eax = send address, clear the global bit
or al, 0x8 ; set the POP bit always, at the same time shifting to ports instead of wrk
fmulp st1, st0 ; g*l (l)
fadd dword [{{.CX}} + {{.AX}}*4] ; g*l+L (l),where L is the current value
fstp dword [{{.CX}} + {{.AX}}*4] ; (l)
ret
{{end}}

View File

@ -0,0 +1,431 @@
{{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" "Opcode"}}
{{- if .StereoAndMono "envelope"}}
jnc su_op_envelope_mono
{{- end}}
{{- if .Stereo "envelope"}}
call su_op_envelope_mono
fld st0
ret
su_op_envelope_mono:
{{- end}}
mov eax, dword [{{.INP}}-su_voice.inputs+su_voice.release] ; eax = su_instrument.release
test eax, eax ; if (eax == 0)
je su_op_envelope_process ; goto process
mov al, {{.InputNumber "envelope" "release"}} ; [state]=RELEASE
mov dword [{{.WRK}}], eax ; note that mov al, XXX; mov ..., eax is less bytes than doing it directly
su_op_envelope_process:
mov eax, dword [{{.WRK}}] ; al=[state]
fld dword [{{.WRK}}+4] ; x=[level]
cmp al, {{.InputNumber "envelope" "sustain"}} ; if (al==SUSTAIN)
je short su_op_envelope_leave2 ; goto leave2
su_op_envelope_attac:
cmp al, {{.InputNumber "envelope" "attack"}} ; if (al!=ATTAC)
jne short su_op_envelope_decay ; goto decay
{{.Call "su_nonlinear_map"}} ; a x, where a=attack
faddp st1, st0 ; a+x
fld1 ; 1 a+x
fucomi st1 ; if (a+x<=1) // is attack complete?
fcmovnb st0, st1 ; a+x a+x
jbe short su_op_envelope_statechange ; else goto statechange
su_op_envelope_decay:
cmp al, {{.InputNumber "envelope" "decay"}} ; if (al!=DECAY)
jne short su_op_envelope_release ; goto release
{{.Call "su_nonlinear_map"}} ; d x, where d=decay
fsubp st1, st0 ; x-d
fld dword [{{.Input "envelope" "sustain"}}] ; s x-d, where s=sustain
fucomi st1 ; if (x-d>s) // is decay complete?
fcmovb st0, st1 ; x-d x-d
jnc short su_op_envelope_statechange ; else goto statechange
su_op_envelope_release:
cmp al, {{.InputNumber "envelope" "release"}} ; if (al!=RELEASE)
jne short su_op_envelope_leave ; goto leave
{{.Call "su_nonlinear_map"}} ; r x, where r=release
fsubp st1, st0 ; x-r
fldz ; 0 x-r
fucomi st1 ; if (x-r>0) // is release complete?
fcmovb st0, st1 ; x-r x-r, then goto leave
jc short su_op_envelope_leave
su_op_envelope_statechange:
inc dword [{{.WRK}}] ; [state]++
su_op_envelope_leave:
fstp st1 ; x', where x' is the new value
fst dword [{{.WRK}}+4] ; [level]=x'
su_op_envelope_leave2:
fmul dword [{{.Input "envelope" "gain"}}] ; [gain]*x'
ret
{{end}}
{{- if .HasOp "noise"}}
;-------------------------------------------------------------------------------
; NOISE opcode: creates noise
;-------------------------------------------------------------------------------
; Mono: push a random value [-1,1] value on stack
; Stereo: push two (differeent) random values on stack
;-------------------------------------------------------------------------------
{{.Func "su_op_noise" "Opcode"}}
lea {{.CX}},[{{.Stack "RandSeed"}}]
{{- if .StereoAndMono "noise"}}
jnc su_op_noise_mono
{{- end}}
{{- if .Stereo "noise"}}
call su_op_noise_mono
su_op_noise_mono:
{{- end}}
imul eax, [{{.CX}}],16007
mov [{{.CX}}],eax
fild dword [{{.CX}}]
{{- .Prepare (.Int 2147483648)}}
fidiv dword [{{.Use (.Int 2147483648)}}] ; 65536*32768
fld dword [{{.Input "noise" "shape"}}]
{{.Call "su_waveshaper"}}
fmul dword [{{.Input "noise" "gain"}}]
ret
{{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" "Opcode"}}
lodsb ; load the flags
{{- if .Library}}
mov {{.DI}}, [{{.Stack "SampleTable"}}]; we need to put this in a register, as the stereo & unisons screw the stack positions
; ain't we lucky that {{.DI}} was unused throughout
{{- end}}
fld dword [{{.Input "oscillator" "detune"}}] ; e, where e is the detune [0,1]
{{- .Prepare (.Float 0.5)}}
fsub dword [{{.Use (.Float 0.5)}}] ; e-.5
fadd st0, st0 ; d=2*e-.5, where d is the detune [-1,1]
{{- if .StereoAndMono "oscillator"}}
jnc su_op_oscillat_mono
{{- end}}
{{- if .Stereo "oscillator"}}
fld st0 ; d d
add {{.WRK}}, 4 ; move wrk...
call su_op_oscillat_mono ; r d
sub {{.WRK}}, 4 ; ...restore wrk
fxch ; d r
fchs ; -d r, negate the detune for second round
su_op_oscillat_mono:
{{- end}}
{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}}
{{.PushRegs .AX "" .WRK "OscWRK" .AX "OscFlags"}}
fldz ; 0 d
fxch ; d a=0, "accumulated signal"
su_op_oscillat_unison_loop:
fst dword [{{.SP}}] ; save the current detune, d. We could keep it in fpu stack but it was getting big.
call su_op_oscillat_single ; s a
faddp st1, st0 ; a+=s
test al, 3
je su_op_oscillat_unison_out
add {{.WRK}}, 8 ; this is ok after all, as there's a pop in the end of unison loop
fld dword [{{.Input "oscillator" "phase"}}] ; p s
{{.Int 0x3DAAAAAA | .Prepare}}
fadd dword [{{.Int 0x3DAAAAAA | .Use}}] ; 1/12 p s, add some little phase offset to unison oscillators so they don't start in sync
fstp dword [{{.Input "oscillator" "phase"}}] ; s note that this changes the phase for second, possible stereo run. That's probably ok
fld dword [{{.SP}}] ; d s
{{.Float 0.5 | .Prepare}}
fmul dword [{{.Float 0.5 | .Use}}] ; .5*d s // negate and halve the detune of each oscillator
fchs ; -.5*d s // negate and halve the detune of each oscillator
dec eax
jmp short su_op_oscillat_unison_loop
su_op_oscillat_unison_out:
{{.PopRegs .AX .WRK .AX}}
ret
su_op_oscillat_single:
{{- end}}
fld dword [{{.Input "oscillator" "transpose"}}]
{{- .Float 0.5 | .Prepare}}
fsub dword [{{.Float 0.5 | .Use}}]
{{- .Float 0.0078125 | .Prepare}}
fdiv dword [{{.Float 0.0078125 | .Use}}]
faddp st1
{{- if .SupportsParamValue "oscillator" "lfo" 1}}
test al, byte 0x08
jnz su_op_oscillat_skipnote
{{- end}}
fiadd dword [{{.INP}}-su_voice.inputs+su_voice.note] ; // st0 is note, st1 is t+d offset
{{- if .SupportsParamValue "oscillator" "lfo" 1}}
su_op_oscillat_skipnote:
{{- end}}
{{- .Int 0x3DAAAAAA | .Prepare}}
fmul dword [{{.Int 0x3DAAAAAA | .Use}}]
{{.Call "su_power"}}
{{- if .SupportsParamValue "oscillator" "lfo" 1}}
test al, byte 0x08
jz short su_op_oscillat_normalize_note
{{- .Float 0.000038 | .Prepare}}
fmul dword [{{.Float 0.000038 | .Use}}] ; // st0 is now frequency for lfo
jmp short su_op_oscillat_normalized
su_op_oscillat_normalize_note:
{{- end}}
{{- .Float 0.000092696138 | .Prepare}}
fmul dword [{{.Float 0.000092696138 | .Use}}] ; // st0 is now frequency
su_op_oscillat_normalized:
fadd dword [{{.WRK}}]
{{- if .SupportsParamValue "oscillator" "type" .Sample}}
test al, byte 0x80
jz short su_op_oscillat_not_sample
fst dword [{{.WRK}}] ; for samples, we store the phase without mod(p,1)
{{- if or (.SupportsParamValueOtherThan "oscillator" "phase" 0) (.SupportsModulation "oscillator" "phase")}}
fadd dword [{{.Input "oscillator" "phase"}}]
{{- end}}
{{.Call "su_oscillat_sample"}}
jmp su_op_oscillat_shaping ; skip the rest to avoid color phase normalization and colorloading
su_op_oscillat_not_sample:
{{- end}}
fld1 ; we need to take mod(p,1) so the frequency does not drift as the float
fadd st1, st0 ; make no mistake: without this, there is audible drifts in oscillator pitch
fxch ; as the actual period changes once the phase becomes too big
fprem ; we actually computed mod(p+1,1) instead of mod(p,1) as the fprem takes mod
fstp st1 ; towards zero
fst dword [{{.WRK}}] ; store back the updated phase
{{- if or (.SupportsParamValueOtherThan "oscillator" "phase" 0) (.SupportsModulation "oscillator" "phase")}}
fadd dword [{{.Input "oscillator" "phase"}}]
fld1 ; this is a bit stupid, but we need to take mod(x,1) again after phase modulations
fadd st1, st0 ; as the actual oscillator functions expect x in [0,1]
fxch
fprem
fstp st1
{{- end}}
fld dword [{{.Input "oscillator" "color"}}] ; // c p
; every oscillator test included if needed
{{- if .SupportsParamValue "oscillator" "type" .Sine}}
test al, byte 0x40
jz short su_op_oscillat_notsine
{{.Call "su_oscillat_sine"}}
su_op_oscillat_notsine:
{{- end}}
{{- if .SupportsParamValue "oscillator" "type" .Trisaw}}
test al, byte 0x20
jz short su_op_oscillat_not_trisaw
{{.Call "su_oscillat_trisaw"}}
su_op_oscillat_not_trisaw:
{{- end}}
{{- if .SupportsParamValue "oscillator" "type" .Pulse}}
test al, byte 0x10
jz short su_op_oscillat_not_pulse
{{.Call "su_oscillat_pulse"}}
su_op_oscillat_not_pulse:
{{- end}}
{{- if .SupportsParamValue "oscillator" "type" .Gate}}
test al, byte 0x04
jz short su_op_oscillat_not_gate
{{.Call "su_oscillat_gate"}}
jmp su_op_oscillat_gain ; skip waveshaping as the shape parameter is reused for gateshigh
su_op_oscillat_not_gate:
{{- end}}
su_op_oscillat_shaping:
; finally, shape the oscillator and apply gain
fld dword [{{.Input "oscillator" "shape"}}]
{{.Call "su_waveshaper"}}
su_op_oscillat_gain:
fmul dword [{{.Input "oscillator" "gain"}}]
ret
{{end}}
{{- if .HasCall "su_oscillat_pulse"}}
{{.Func "su_oscillat_pulse"}}
fucomi st1 ; // c p
fld1
jnc short su_oscillat_pulse_up ; // +1 c p
fchs ; // -1 c p
su_oscillat_pulse_up:
fstp st1 ; // +-1 p
fstp st1 ; // +-1
ret
{{end}}
{{- if .HasCall "su_oscillat_trisaw"}}
{{.Func "su_oscillat_trisaw"}}
fucomi st1 ; // c p
jnc short su_oscillat_trisaw_up
fld1 ; // 1 c p
fsubr st2, st0 ; // 1 c 1-p
fsubrp st1, st0 ; // 1-c 1-p
su_oscillat_trisaw_up:
fdivp st1, st0 ; // tp'/tc
fadd st0 ; // 2*''
fld1 ; // 1 2*''
fsubp st1, st0 ; // 2*''-1
ret
{{end}}
{{- if .HasCall "su_oscillat_sine"}}
{{.Func "su_oscillat_sine"}}
fucomi st1 ; // c p
jnc short su_oscillat_sine_do
fstp st1
fsub st0, st0 ; // 0
ret
su_oscillat_sine_do:
fdivp st1, st0 ; // p/c
fldpi ; // pi p
fadd st0 ; // 2*pi p
fmulp st1, st0 ; // 2*pi*p
fsin ; // sin(2*pi*p)
ret
{{end}}
{{- if .HasCall "su_oscillat_gate"}}
{{.Func "su_oscillat_gate"}}
fxch ; p c
fstp st1 ; p
{{- .Float 16.0 | .Prepare | indent 4}}
fmul dword [{{.Float 16.0 | .Use}}] ; 16*p
push {{.AX}}
push {{.AX}}
fistp dword [{{.SP}}] ; s=int(16*p), stack empty
fld1 ; 1
pop {{.AX}}
and al, 0xf ; ax=int(16*p) & 15, stack: 1
bt word [{{.VAL}}-4],ax ; if bit ax of the gate word is set
jc su_oscillat_gate_bit ; goto gate_bit
fsub st0, st0 ; stack: 0
su_oscillat_gate_bit: ; stack: 0/1, let's call it x
fld dword [{{.WRK}}+16] ; g x, g is gatestate, x is the input to this filter 0/1
fsub st1 ; g-x x
{{- .Float 0.99609375 | .Prepare | indent 4}}
fmul dword [{{.Float 0.99609375 | .Use}}] ; c(g-x) x
faddp st1, st0 ; x+c(g-x)
fst dword [{{.WRK}}+16]; g'=x+c(g-x) NOTE THAT UNISON 2 & UNISON 3 ALSO USE {{.WRK}}+16, so gate and unison 2 & 3 don't work. Probably should delete that low pass altogether
pop {{.AX}} ; Another way to see this (c~0.996)
ret ; g'=cg+(1-c)x
; This is a low-pass to smooth the gate transitions
{{end}}
{{- if .HasCall "su_oscillat_sample"}}
{{.Func "su_oscillat_sample"}}
{{- .PushRegs .AX "SampleAx" .DX "SampleDx" .CX "SampleCx" .BX "SampleBx" .DI "SampleDi" | indent 4}} ; edx must be saved, eax & ecx if this is stereo osc
push {{.AX}}
mov al, byte [{{.VAL}}-4] ; reuse "color" as the sample number
{{- if .Library}}
lea {{.DI}}, [{{.DI}} + {{.AX}}*8] ; edi points now to the sample table entry
{{- else}}
{{- .Prepare "su_sample_offsets" | indent 4}}
lea {{.DI}}, [{{.Use "su_sample_offsets"}} + {{.AX}}*8]; edi points now to the sample table entry
{{- end}}
{{- .Float 84.28074964676522 | .Prepare | indent 4}}
fmul dword [{{.Float 84.28074964676522 | .Use}}] ; p*r
fistp dword [{{.SP}}]
pop {{.DX}} ; edx is now the sample number
movzx ebx, word [{{.DI}} + 4] ; ecx = loopstart
sub edx, ebx ; if sample number < loop start
jl su_oscillat_sample_not_looping ; then we're not looping yet
mov eax, edx ; eax = sample number
movzx ecx, word [{{.DI}} + 6] ; edi is now the loop length
xor edx, edx ; div wants edx to be empty
div ecx ; edx is now the remainder
su_oscillat_sample_not_looping:
add edx, ebx ; sampleno += loopstart
add edx, dword [{{.DI}}]
{{- .Prepare "su_sample_table" | indent 4}}
fild word [{{.Use "su_sample_table"}} + {{.DX}}*2]
{{- .Float 32767.0 | .Prepare | indent 4}}
fdiv dword [{{.Float 32767.0 | .Use}}]
{{- .PopRegs .AX .DX .CX .BX .DI | indent 4}}
ret
{{end}}
{{- 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" "Opcode"}}
{{- if .StereoAndMono "loadval" }}
jnc su_op_loadval_mono
{{- end}}
{{- if .Stereo "loadval" }}
call su_op_loadval_mono
su_op_loadval_mono:
{{- end }}
fld dword [{{.Input "loadval" "value"}}] ; v
{{- .Float 0.5 | .Prepare | indent 4}}
fsub dword [{{.Float 0.5 | .Use}}]
fadd st0 ; 2*v-1
ret
{{end}}
{{- if .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" "Opcode"}}
lea {{.DI}}, [{{.WRK}}+su_unit.ports]
{{- if .StereoAndMono "receive"}}
jnc su_op_receive_mono
{{- end}}
{{- if .Stereo "receive"}}
xor ecx,ecx
fld dword [{{.DI}}+4]
mov dword [{{.DI}}+4],ecx
{{- end}}
{{- if .StereoAndMono "receive"}}
su_op_receive_mono:
xor ecx,ecx
{{- end}}
fld dword [{{.DI}}]
mov dword [{{.DI}}],ecx
ret
{{end}}
{{- if .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" "Opcode"}}
lodsb
mov {{.DI}}, [{{.Stack "Synth"}}]
{{- if .StereoAndMono "in"}}
jnc su_op_in_mono
{{- end}}
{{- if .Stereo "in"}}
xor ecx, ecx ; we cannot xor before jnc, so we have to do it mono & stereo. LAHF / SAHF could do it, but is the same number of bytes with more entropy
fld dword [{{.DI}} + su_synthworkspace.right + {{.AX}}*4]
mov dword [{{.DI}} + su_synthworkspace.right + {{.AX}}*4], ecx
{{- end}}
{{- if .StereoAndMono "in"}}
su_op_in_mono:
xor ecx, ecx
{{- end}}
fld dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4]
mov dword [{{.DI}} + su_synthworkspace.left + {{.AX}}*4], ecx
ret
{{end}}

View File

@ -0,0 +1,53 @@
;-------------------------------------------------------------------------------
; unit struct
;-------------------------------------------------------------------------------
struc su_unit
.state resd 8
.ports resd 8
.size:
endstruc
;-------------------------------------------------------------------------------
; voice struct
;-------------------------------------------------------------------------------
struc su_voice
.note resd 1
.release resd 1
.inputs resd 8
.reserved resd 6 ; this is done to so the whole voice is 2^n long, see polyphonic player
.workspace resb 63 * su_unit.size
.size:
endstruc
;-------------------------------------------------------------------------------
; synthworkspace struct
;-------------------------------------------------------------------------------
struc su_synthworkspace
.curvoices resb 32 ; these are used by the multitrack player to store which voice is playing on which track
.left resd 1
.right resd 1
.aux resd 6 ; 3 auxiliary signals
.voices resb 32 * su_voice.size
.size:
endstruc
;-------------------------------------------------------------------------------
; su_delayline_wrk struct
;-------------------------------------------------------------------------------
struc su_delayline_wrk
.dcin resd 1
.dcout resd 1
.filtstate resd 1
.buffer resd 65536
.size:
endstruc
;-------------------------------------------------------------------------------
; su_sample_offset struct
;-------------------------------------------------------------------------------
struc su_sample_offset ; length conveniently 8 bytes, so easy to index
.start resd 1
.loopstart resw 1
.looplength resw 1
.size:
endstruc

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

View File

@ -0,0 +1,478 @@
{{- 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)
{{- if .Stereo "compressor"}}
(local.set $x2 (f32.mul
(call $peek)
(call $peek)
))
(if (local.get $stereo)(then
(call $pop)
(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
))
(call $push (f32.div ;; apply post-gain ("make up gain")
(call $pop)
(call $input (i32.const {{.InputNumber "compressor" "invgain"}}))
))
{{- 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}}

View File

@ -0,0 +1,19 @@
{{- if not .Output16Bit }}
(i64.store (global.get $outputBufPtr) (i64.load (i32.const {{index .Labels "su_globalports"}}))) ;; 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={{index .Labels "su_globalports"}} (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 {{index .Labels "su_globalports"}}) (i64.const 0)) ;; clear the left and right ports

View 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={{index .Labels "su_transformedvalues"}}
(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={{index .Labels "su_transformedvalues"}} (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}} funcref)
(elem (i32.const 1) ;; start the indices at 1, as 0 is reserved for advance
{{- range .Instructions}}
$su_op_{{.}}
{{- end}}
)

View File

@ -0,0 +1,335 @@
(module
{{- /*
;------------------------------------------------------------------------------
; Patterns
;-------------------------------------------------------------------------------
*/}}
{{- .SetDataLabel "su_patterns"}}
{{- $m := .}}
{{- range .Patterns}}
{{- range .}}
{{- $.DataB .}}
{{- end}}
{{- end}}
{{- /*
;------------------------------------------------------------------------------
; Tracks
;-------------------------------------------------------------------------------
*/}}
{{- .SetDataLabel "su_tracks"}}
{{- $m := .}}
{{- range .Sequences}}
{{- range .}}
{{- $.DataB .}}
{{- end}}
{{- end}}
{{- /*
;------------------------------------------------------------------------------
; The code for this patch, basically indices to vm jump table
;-------------------------------------------------------------------------------
*/}}
{{- .SetDataLabel "su_patch_code"}}
{{- range .Commands}}
{{- $.DataB .}}
{{- end}}
{{- /*
;-------------------------------------------------------------------------------
; The parameters / inputs to each opcode
;-------------------------------------------------------------------------------
*/}}
{{- .SetDataLabel "su_patch_parameters"}}
{{- range .Values}}
{{- $.DataB .}}
{{- end}}
{{- /*
;-------------------------------------------------------------------------------
; Delay times
;-------------------------------------------------------------------------------
*/}}
{{- .SetDataLabel "su_delay_times"}}
{{- range .DelayTimes}}
{{- $.DataW .}}
{{- end}}
{{- /*
;-------------------------------------------------------------------------------
; The number of transformed parameters each opcode takes
;-------------------------------------------------------------------------------
*/}}
{{- .SetDataLabel "su_vm_transformcounts"}}
{{- range .Instructions}}
{{- $.TransformCount . | $.ToByte | $.DataB}}
{{- end}}
{{- /*
;-------------------------------------------------------------------------------
; Allocate memory for stack.
; Stack of 64 float signals is enough for everybody... right?
; Note: as the stack grows _downwards_ the label is _after_ stack
;-------------------------------------------------------------------------------
*/}}
{{- .Align}}
{{- .Block 256}}
{{- .SetBlockLabel "su_stack"}}
{{- /*
;-------------------------------------------------------------------------------
; Allocate memory for transformed values.
;-------------------------------------------------------------------------------
*/}}
{{- .Align}}
{{- .SetBlockLabel "su_transformedvalues"}}
{{- .Block 32}}
{{- /*
;-------------------------------------------------------------------------------
; Uninitialized memory for synth, delaylines & outputbuffer
;-------------------------------------------------------------------------------
*/}}
{{- .Align}}
{{- if ne .VoiceTrackBitmask 0}}
{{- .SetBlockLabel "su_trackcurrentvoice"}}
{{- .Block 32}}
{{- end}}
{{- .Align}}
{{- .SetBlockLabel "su_synth"}}
{{- .Block 32}}
{{- .SetBlockLabel "su_globalports"}}
{{- .Block 32}}
{{- .SetBlockLabel "su_voices"}}
{{- .Block 131072}}
{{- .Align}}
{{- .SetBlockLabel "su_delaylines"}}
{{- .Block (int (mul 262156 .Song.Patch.NumDelayLines))}}
{{- .Align}}
{{- .SetBlockLabel "su_outputbuffer"}}
{{- if .Output16Bit}}
{{- .Block (int (mul .PatternLength .SequenceLength .Song.SamplesPerRow 4))}}
{{- else}}
{{- .Block (int (mul .PatternLength .SequenceLength .Song.SamplesPerRow 8))}}
{{- end}}
{{- .SetBlockLabel "su_outputend"}}
;;------------------------------------------------------------------------------
;; 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
;;------------------------------------------------------------------------------
(memory (export "m") {{.MemoryPages}})
;;------------------------------------------------------------------------------
;; 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 {{index .Labels "su_stack"}}))
(global $outputBufPtr (mut i32) (i32.const {{index .Labels "su_outputbuffer"}}))
;; 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 {{index .Labels "su_outputbuffer"}}))
(global $outputLength (export "l") i32 (i32.const {{if .Output16Bit}}{{mul .PatternLength .SequenceLength .Song.SamplesPerRow 4}}{{else}}{{mul .PatternLength .SequenceLength .Song.SamplesPerRow 8}}{{end}}))
(global $output16bit (export "t") i32 (i32.const {{if .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 .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 {{index .Labels "su_voices"}}))
(global.set $voice (i32.const {{index .Labels "su_voices"}}))
(global.set $voicesRemain (i32.const {{.Song.Patch.NumVoices | printf "%v"}}))
{{- if .HasOp "delay"}}
(global.set $delayWRK (i32.const {{index .Labels "su_delaylines"}}))
{{- 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 {{.PatternLength}})))
end
(global.set $pattern (i32.add (global.get $pattern) (i32.const 1)))
(br_if $pattern_loop (i32.lt_s (global.get $pattern) (i32.const {{.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 .Sequences}}))
(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 {{.PatternLength}}))
(i32.add (global.get $row))
(i32.load8_u offset={{index .Labels "su_patterns"}})
(local.tee $note)
(if (i32.ne (i32.const {{.Hold}}))(then
(i32.store offset={{add (index .Labels "su_voices") 4}}
(i32.mul
(i32.add
(local.tee $voiceNo (i32.load8_u offset={{index .Labels "su_trackcurrentvoice"}} (local.get $tracksRemaining)))
(local.get $firstVoice)
)
(i32.const 4096)
)
(i32.const 1)
) ;; release the note
(if (i32.gt_u (local.get $note) (i32.const {{.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 {{index .Labels "su_voices"}})
))
(memory.fill (local.get $di) (i32.const 0) (i32.const 4096))
(i32.store (local.get $di) (local.get $note))
(i32.store8 offset={{index .Labels "su_trackcurrentvoice"}} (local.get $tracksRemaining) (local.get $voiceNo))
))
))
(local.set $si (i32.add (local.get $si) (i32.const {{.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 .Sequences}}))
(local.set $si (global.get $pattern))
(local.set $di (i32.const {{index .Labels "su_voices"}}))
loop $track_loop
(i32.load8_u offset={{index .Labels "su_tracks"}} (local.get $si))
(i32.mul (i32.const {{.PatternLength}}))
(i32.add (global.get $row))
(i32.load8_u offset={{index .Labels "su_patterns"}})
(local.tee $note)
(if (i32.ne (i32.const {{.Hold}}))(then
(i32.store offset=4 (local.get $di) (i32.const 1)) ;; release the note
(if (i32.gt_u (local.get $note) (i32.const {{.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 {{.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

View 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 {{index .Labels "su_globalports"}}))
{{- 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 {{index .Labels "su_globalports"}})))
{{- 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 .SupportsGlobalSend}}
(select
(i32.const {{index .Labels "su_synth"}})
{{- end}}
(global.get $voice)
{{- if .SupportsGlobalSend}}
(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 {{index .Labels "su_globalports"}})) ;; synth.left
(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 {{add (index .Labels "su_globalports") 4}})) ;; synth.right, note that ATM does not seem to support mono ocpode at all
(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}}

View File

@ -0,0 +1,350 @@
{{- 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}}
{{- if .Stereo "oscillator"}}
(local $WRK_stereostash i32)
(local.set $WRK_stereostash (global.get $WRK))
{{- 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)
(local.tee $phase
(f32.sub
(local.tee $phase
;; 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.floor (local.get $phase))
)
)
)
(f32.add (local.get $phase) (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 (index .Labels "su_transformedvalues")}} (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)))
(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)))
(br_if $stereoLoop (i32.eqz (local.tee $stereo (i32.eqz (local.get $stereo)))))
end
(global.set $WRK (local.get $WRK_stereostash))
;; TODO: all this "save WRK to local variable, modify it and then restore it" could be better thought out
;; however, it is now done like this as a quick bug fix to the issue of stereo oscillators touching WRK and not restoring it
{{- 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 {{index .Labels "su_globalports"}})))
{{- 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}}