mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
The sample-based oscillators converted the samplepos to an integer and did samplepos < loop_end comparison to check if we are past looping. Unfortunately, the < comparison was done in signed math. Normally, this should never happen, but if the x87 FPU stack overflowed exactly at right position, we then got 0x80000000 in samplepos, which is equal to -2147483648. Thus, we considered that sample is not looping and read the sample table at position -2147483648, well out of bound. TL;DR changing jl to jb makes sure we always wrap within to sample table, no matter what. Fixes #149.
442 lines
19 KiB
NASM
442 lines
19 KiB
NASM
{{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.sustain] ; eax = su_instrument.sustain
|
|
test eax, eax ; if (eax != 0)
|
|
jne 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"}}
|
|
{{- if .SupportsModulation "oscillator" "frequency"}}
|
|
push 0
|
|
pop {{.CX}} ; clear cx without affecting flags
|
|
xchg ecx, dword [{{.Modulation "oscillator" "frequency"}}]
|
|
{{- end}}
|
|
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 .SupportsModulation "oscillator" "frequency"}}
|
|
push {{.CX}}
|
|
fadd dword [{{.SP}}]
|
|
pop {{.CX}}
|
|
{{- end}}
|
|
{{- 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") (.SupportsParamValueOtherThan "oscillator" "unison" 0)}}
|
|
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") (.SupportsParamValueOtherThan "oscillator" "unison" 0)}}
|
|
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
|
|
jb 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}}
|