mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
feat(vm/compiler): embed templates to executable
This commit is contained in:
parent
d2ddba3944
commit
8ffe4a70dd
205
vm/compiler/templates/amd64-386/arithmetic.asm
Normal file
205
vm/compiler/templates/amd64-386/arithmetic.asm
Normal 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}}
|
390
vm/compiler/templates/amd64-386/effects.asm
Normal file
390
vm/compiler/templates/amd64-386/effects.asm
Normal 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}}
|
44
vm/compiler/templates/amd64-386/flowcontrol.asm
Normal file
44
vm/compiler/templates/amd64-386/flowcontrol.asm
Normal 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}}
|
50
vm/compiler/templates/amd64-386/gmdls.asm
Normal file
50
vm/compiler/templates/amd64-386/gmdls.asm
Normal 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}}
|
134
vm/compiler/templates/amd64-386/library.asm
Normal file
134
vm/compiler/templates/amd64-386/library.asm
Normal 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}}
|
100
vm/compiler/templates/amd64-386/library.h
Normal file
100
vm/compiler/templates/amd64-386/library.h
Normal 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
|
44
vm/compiler/templates/amd64-386/output_sound.asm
Normal file
44
vm/compiler/templates/amd64-386/output_sound.asm
Normal 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 }}
|
215
vm/compiler/templates/amd64-386/patch.asm
Normal file
215
vm/compiler/templates/amd64-386/patch.asm
Normal 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}}
|
254
vm/compiler/templates/amd64-386/player.asm
Normal file
254
vm/compiler/templates/amd64-386/player.asm
Normal 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}}
|
66
vm/compiler/templates/amd64-386/player.h
Normal file
66
vm/compiler/templates/amd64-386/player.h
Normal 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
|
128
vm/compiler/templates/amd64-386/sinks.asm
Normal file
128
vm/compiler/templates/amd64-386/sinks.asm
Normal 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}}
|
431
vm/compiler/templates/amd64-386/sources.asm
Normal file
431
vm/compiler/templates/amd64-386/sources.asm
Normal 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}}
|
53
vm/compiler/templates/amd64-386/structs.asm
Normal file
53
vm/compiler/templates/amd64-386/structs.asm
Normal 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
|
239
vm/compiler/templates/wasm/arithmetic.wat
Normal file
239
vm/compiler/templates/wasm/arithmetic.wat
Normal file
@ -0,0 +1,239 @@
|
||||
{{- if .HasOp "pop"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; POP opcode: remove (discard) the topmost signal from the stack
|
||||
;;-------------------------------------------------------------------------------
|
||||
{{- if .Mono "pop" -}}
|
||||
;; Mono: a -> (empty)
|
||||
{{- end}}
|
||||
{{- if .Stereo "pop" -}}
|
||||
;; Stereo: a b -> (empty)
|
||||
{{- end}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_pop (param $stereo i32)
|
||||
{{- if .Stereo "pop"}}
|
||||
(if (local.get $stereo) (then
|
||||
(drop (call $pop))
|
||||
))
|
||||
{{- end}}
|
||||
(drop (call $pop))
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "add"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; ADD opcode: add the two top most signals on the stack
|
||||
;;-------------------------------------------------------------------------------
|
||||
{{- if .Mono "add"}}
|
||||
;; Mono: a b -> a+b b
|
||||
{{- end}}
|
||||
{{- if .Stereo "add" -}}
|
||||
;; Stereo: a b c d -> a+c b+d c d
|
||||
{{- end}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_add (param $stereo i32)
|
||||
{{- if .StereoAndMono "add"}}
|
||||
(if (local.get $stereo) (then
|
||||
{{- end}}
|
||||
{{- if .Stereo "add"}}
|
||||
call $pop ;; F: b c d P: a
|
||||
call $pop ;; F: c d P: b a
|
||||
call $peek2;; F: c d P: d b a
|
||||
f32.add ;; F: c d P: b+d a
|
||||
call $push ;; F: b+d c d P: a
|
||||
call $peek2;; F: b+d c d P: c a
|
||||
f32.add ;; F: b+d c d P: a+c
|
||||
call $push ;; F: a+c b+d c d P:
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "add"}}
|
||||
)(else
|
||||
{{- end}}
|
||||
{{- if .Mono "add"}}
|
||||
(call $push (f32.add (call $pop) (call $peek)))
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "add"}}
|
||||
))
|
||||
{{- end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "addp"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; ADDP opcode: add the two top most signals on the stack and pop
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: a b -> a+b
|
||||
;; Stereo: a b c d -> a+c b+d
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_addp (param $stereo i32)
|
||||
{{- if .StereoAndMono "addp"}}
|
||||
(if (local.get $stereo) (then
|
||||
{{- end}}
|
||||
{{- if .Stereo "addp"}}
|
||||
call $pop ;; a
|
||||
call $pop ;; b a
|
||||
call $swap ;; a b
|
||||
call $pop ;; c a b
|
||||
f32.add ;; c+a b
|
||||
call $swap ;; b c+a
|
||||
call $pop ;; d b c+a
|
||||
f32.add ;; d+b c+a
|
||||
call $push ;; c+a
|
||||
call $push
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "addp"}}
|
||||
)(else
|
||||
{{- end}}
|
||||
{{- if .Mono "addp"}}
|
||||
(call $push (f32.add (call $pop) (call $pop)))
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "addp"}}
|
||||
))
|
||||
{{- end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "loadnote"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; LOADNOTE opcode: load the current note, scaled to [-1,1]
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_loadnote (param $stereo i32)
|
||||
{{- if .Stereo "loadnote"}}
|
||||
(if (local.get $stereo) (then
|
||||
(call $su_op_loadnote (i32.const 0))
|
||||
))
|
||||
{{- end}}
|
||||
(f32.convert_i32_u (i32.load (global.get $voice)))
|
||||
(f32.mul (f32.const 0.015625))
|
||||
(f32.sub (f32.const 1))
|
||||
(call $push)
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{- if .HasOp "mul"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; MUL opcode: multiply the two top most signals on the stack
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: a b -> a*b a
|
||||
;; Stereo: a b c d -> a*c b*d c d
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_mul (param $stereo i32)
|
||||
{{- if .StereoAndMono "mul"}}
|
||||
(if (local.get $stereo) (then
|
||||
{{- end}}
|
||||
{{- if .Stereo "mul"}}
|
||||
call $pop ;; F: b c d P: a
|
||||
call $pop ;; F: c d P: b a
|
||||
call $peek2;; F: c d P: d b a
|
||||
f32.mul ;; F: c d P: b*d a
|
||||
call $push ;; F: b*d c d P: a
|
||||
call $peek2;; F: b*d c d P: c a
|
||||
f32.mul ;; F: b*d c d P: a*c
|
||||
call $push ;; F: a*c b*d c d P:
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "mul"}}
|
||||
)(else
|
||||
{{- end}}
|
||||
{{- if .Mono "mul"}}
|
||||
(call $push (f32.mul (call $pop) (call $peek)))
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "mul"}}
|
||||
))
|
||||
{{- end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "mulp"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; MULP opcode: multiply the two top most signals on the stack and pop
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: a b -> a*b
|
||||
;; Stereo: a b c d -> a*c b*d
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_mulp (param $stereo i32)
|
||||
{{- if .StereoAndMono "mulp"}}
|
||||
(if (local.get $stereo) (then
|
||||
{{- end}}
|
||||
{{- if .Stereo "mulp"}}
|
||||
call $pop ;; a
|
||||
call $pop ;; b a
|
||||
call $swap ;; a b
|
||||
call $pop ;; c a b
|
||||
f32.mul ;; c*a b
|
||||
call $swap ;; b c*a
|
||||
call $pop ;; d b c*a
|
||||
f32.mul ;; d*b c*a
|
||||
call $push ;; c*a
|
||||
call $push
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "mulp"}}
|
||||
)(else
|
||||
{{- end}}
|
||||
{{- if .Mono "mulp"}}
|
||||
(call $push (f32.mul (call $pop) (call $pop)))
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "mulp"}}
|
||||
))
|
||||
{{- end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if .HasOp "push"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; PUSH opcode: push the topmost signal on the stack
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: a -> a a
|
||||
;; Stereo: a b -> a b a b
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_push (param $stereo i32)
|
||||
{{- if .Stereo "push"}}
|
||||
(if (local.get $stereo) (then
|
||||
(call $push (call $peek))
|
||||
))
|
||||
{{- end}}
|
||||
(call $push (call $peek))
|
||||
)
|
||||
{{end}}
|
||||
|
||||
|
||||
{{- if or (.HasOp "xch") (.Stereo "delay")}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; XCH opcode: exchange the signals on the stack
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: a b -> b a
|
||||
;; stereo: a b c d -> c d a b
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_xch (param $stereo i32)
|
||||
call $pop
|
||||
call $pop
|
||||
{{- if .StereoAndMono "xch"}}
|
||||
(if (local.get $stereo) (then
|
||||
{{- end}}
|
||||
{{- if .Stereo "xch"}}
|
||||
call $pop ;; F: d P: c b a
|
||||
call $swap ;; F: d P: b c a
|
||||
call $pop ;; F: P: d b c a
|
||||
call $swap ;; F: P: b d c a
|
||||
call $push ;; F: b P: d c a
|
||||
call $push ;; F: d b P: c a
|
||||
call $swap ;; F: d b P: a c
|
||||
call $pop ;; F: b P: d a c
|
||||
call $swap ;; F: b P: a d c
|
||||
call $push ;; F: a b P: d c
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "xch"}}
|
||||
)(else
|
||||
{{- end}}
|
||||
{{- if or (.Mono "xch") (.Stereo "delay")}}
|
||||
call $swap
|
||||
{{- end}}
|
||||
{{- if .StereoAndMono "xch"}}
|
||||
))
|
||||
{{- end}}
|
||||
call $push
|
||||
call $push
|
||||
)
|
||||
{{end}}
|
478
vm/compiler/templates/wasm/effects.wat
Normal file
478
vm/compiler/templates/wasm/effects.wat
Normal 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}}
|
19
vm/compiler/templates/wasm/output_sound.wat
Normal file
19
vm/compiler/templates/wasm/output_sound.wat
Normal 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
|
144
vm/compiler/templates/wasm/patch.wat
Normal file
144
vm/compiler/templates/wasm/patch.wat
Normal file
@ -0,0 +1,144 @@
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; su_run_vm function: runs the entire virtual machine once, creating 1 sample
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_run_vm (local $opcodeWithStereo i32) (local $opcode i32) (local $paramNum i32) (local $paramX4 i32) (local $WRKplusparam i32)
|
||||
loop $vm_loop
|
||||
(local.set $opcodeWithStereo (i32.load8_u (global.get $COM)))
|
||||
(global.set $COM (i32.add (global.get $COM) (i32.const 1))) ;; move to next instruction
|
||||
(global.set $WRK (i32.add (global.get $WRK) (i32.const 64))) ;; move WRK to next unit
|
||||
(if (local.tee $opcode (i32.shr_u (local.get $opcodeWithStereo) (i32.const 1)))(then ;; if $opcode = $opcodeStereo >> 1; $opcode != 0 {
|
||||
(local.set $paramNum (i32.const 0))
|
||||
(local.set $paramX4 (i32.const 0))
|
||||
loop $transform_values_loop
|
||||
{{- $addr := sub (index .Labels "su_vm_transformcounts") 1}}
|
||||
(if (i32.lt_u (local.get $paramNum) (i32.load8_u offset={{$addr}} (local.get $opcode)))(then ;;(i32.ge (local.get $paramNum) (i32.load8_u (local.get $opcode))) /*TODO: offset to transformvalues
|
||||
(local.set $WRKplusparam (i32.add (global.get $WRK) (local.get $paramX4)))
|
||||
(f32.store offset={{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}}
|
||||
)
|
335
vm/compiler/templates/wasm/player.wat
Normal file
335
vm/compiler/templates/wasm/player.wat
Normal 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
|
198
vm/compiler/templates/wasm/sinks.wat
Normal file
198
vm/compiler/templates/wasm/sinks.wat
Normal file
@ -0,0 +1,198 @@
|
||||
{{- if .HasOp "outaux"}}
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; OUTAUX opcode: outputs to main and aux1 outputs and pops the signal
|
||||
;;-------------------------------------------------------------------------------
|
||||
;; Mono: add outgain*ST0 to main left port and auxgain*ST0 to aux1 left
|
||||
;; Stereo: also add outgain*ST1 to main right port and auxgain*ST1 to aux1 right
|
||||
;;-------------------------------------------------------------------------------
|
||||
(func $su_op_outaux (param $stereo i32) (local $addr i32)
|
||||
(local.set $addr (i32.const {{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}}
|
350
vm/compiler/templates/wasm/sources.wat
Normal file
350
vm/compiler/templates/wasm/sources.wat
Normal 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}}
|
Reference in New Issue
Block a user