Files
4kintro/4klang.asm
2024-06-02 21:24:14 +03:00

995 lines
51 KiB
NASM

%define SU_LENGTH_IN_SAMPLES 3335488
%define SU_SAMPLE_RATE 44100
%define SU_BPM 165.
;-------------------------------------------------------------------------------
; unit struct
;-------------------------------------------------------------------------------
struc su_unit
.state resd 8
.ports resd 8
.size:
endstruc
;-------------------------------------------------------------------------------
; voice struct
;-------------------------------------------------------------------------------
struc su_voice
.note resd 1
.sustain 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
;-------------------------------------------------------------------------------
; Uninitialized data: The synth object
;-------------------------------------------------------------------------------
section .synth_object bss align=256
su_synth_obj:
resb su_synthworkspace.size
resb 34*su_delayline_wrk.size
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_render_song code align=1
global _su_render_song@4
_su_render_song@4:
pushad ; Stack: edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
xor eax, eax
push 1 ; Stack: RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
push eax ; Stack: GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
su_render_rowloop: ; loop through every row in the song
push eax ; Stack: Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
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 eax ; Stack: Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
push 7 ; Stack: VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
mov edx, dword su_synth_obj ; edx points to the synth object
mov ebx, dword su_patch_opcodes ; COM points to vm code
mov esi, dword su_patch_operands ; VAL points to unit params
mov ecx, dword su_synth_obj + su_synthworkspace.size - su_delayline_wrk.filtstate
lea ebp, [edx + su_synthworkspace.voices] ; WRK points to the first voice
call su_run_vm ; run through the VM code
pop eax ; eax = VoicesRemain, Stack: Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
mov esi, [esp + 52] ; esi points to the output buffer
mov edi, dword su_synth_obj+su_synthworkspace.left
mov ecx, 2
output_sound16bit_loop: ; loop over two channels, left & right
fld dword [edi]
call su_clip
fmul dword [FCONST_32767_0]
push eax
fistp dword [esp]
pop eax
mov word [esi],ax ; // store integer converted right sample
xor eax,eax
stosd
add esi,2
loop output_sound16bit_loop
mov [esp + 52], esi ; save esi back to stack ; *ptr++ = left, *ptr++ = right
pop eax ; eax = Sample, Stack: Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
inc dword [esp + 4] ; increment global time, used by delays
inc eax
cmp eax, 4009
jl su_render_sampleloop
pop eax ; eax = Row, Stack: GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr ; Stack: pushad ptr
inc eax
cmp eax, 832
jl su_render_rowloop
; rewind the stack the entropy of multiple pop eax is probably lower than add
pop eax ; eax = GlobalTick, Stack: RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
pop eax ; eax = RandSeed, Stack: edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
popad ; Popped: eax, ecx, edx, ebx, esp, ebp, esi, edi. Stack: retaddr_su_render_song, OutputBufPtr
ret 4
;-------------------------------------------------------------------------------
; su_update_voices function: polyphonic & chord implementation
;-------------------------------------------------------------------------------
; Input: eax : current row within song
; Dirty: pretty much everything
;-------------------------------------------------------------------------------
section .su_update_voices code align=1
su_update_voices:
; The simple implementation: each track triggers always the same voice
xor edx, edx
xor ebx, ebx
mov bl, 16 ; rows per pattern
div ebx ; eax = current pattern, edx = current row in pattern
lea esi, [su_tracks+eax]; esi points to the pattern data for current track
mov edi, dword su_synth_obj+su_synthworkspace.voices
mov bl, 6 ; MAX_TRACKS is always <= 32 so this is ok
su_update_voices_trackloop:
movzx eax, byte [esi] ; eax = current pattern
imul eax, 16 ; multiply by rows per pattern, eax = offset to current pattern data
movzx eax, byte [su_patterns + eax + edx] ; ecx = note
cmp al, 1 ; anything but hold causes action
je short su_update_voices_nexttrack
mov dword [edi+su_voice.sustain], eax ; set the voice currently active to release
jb su_update_voices_nexttrack ; if cl < HLD (no new note triggered) goto nexttrack
su_update_voices_retrigger:
stosd ; save note
stosd ; save sustain
mov ecx, (su_voice.size - su_voice.inputs)/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 edi, su_voice.size
su_update_voices_skipadd:
add esi, 52
dec ebx
jnz short su_update_voices_trackloop
ret
;-------------------------------------------------------------------------------
; 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 opcode stream
; VAL : Pointer to operand stream
; WRK : Pointer to the last workspace processed
; Output: su_synth_obj.left : left sample
; su_synth_obj.right : right sample
; Dirty: everything
;-------------------------------------------------------------------------------
section .su_run_vm code align=1
su_run_vm:
pushad ; Stack: edi, OperandStream, Voice, esp, OpcodeStream, Synth, DelayWorkSpace, eax, retaddr_su_run_vm, VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
su_run_vm_loop: ; loop until all voices done
movzx edi, byte [ebx] ; edi = command byte
inc ebx ; move to next instruction
add ebp, 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 edx, [esp + 8] ; reset INP to point to the inputs part of voice
pushf ; push flags to save carry = stereo bit
add edx, su_voice.inputs
xor ecx, ecx ; counter = 0
xor eax, eax ; clear out high bits of eax, as lodsb only sets al
su_transform_operands_loop:
cmp cl, byte [su_vm_transformcounts-1+edi] ; compare the counter to the value in the param count table
je su_transform_operands_out
lodsb ; load the operand from VAL stream
push eax ; push it to memory so FPU can read it
fild dword [esp] ; load the operand value to FPU stack
fmul dword [FCONST_0_00781250] ; divide it by 128 (0 => 0, 128 => 1.0)
fadd dword [ebp+su_unit.ports+ecx*4] ; add the modulations in the current workspace
fstp dword [edx+ecx*4] ; store the modulated value in the inputs section of voice
xor eax, eax
mov dword [ebp+su_unit.ports+ecx*4], eax ; clear out the modulation ports
pop eax
inc ecx
jmp su_transform_operands_loop
su_transform_operands_out:
popf ; pop flags for the carry bit = stereo bit
call [su_vm_jumptable-4+edi*4] ; call the function corresponding to the instruction
jmp su_run_vm_loop
su_run_vm_advance:
mov ebp, dword [esp + 8] ; load pointer to voice to register
add ebp, su_voice.size ; shift it to point to following voice
mov dword [esp + 8], ebp ; save back to stack
dec dword [esp + 36] ; voices--
jne su_run_vm_loop ; if there's more voices to process, goto vm_loop
popad ; Popped: eax, ecx = DelayWorkSpace, edx = Synth, ebx = OpcodeStream, esp, ebp = Voice, esi = OperandStream, edi. Stack: retaddr_su_run_vm, VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_addp code align=1
su_op_addp:
jnc su_op_addp_mono
faddp st2, st0
faddp st2, st0
ret
su_op_addp_mono:
faddp st1, st0
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_mulp code align=1
su_op_mulp:
jnc su_op_mulp_mono
fmulp st2, st0
fmulp st2, st0
ret
su_op_mulp_mono:
fmulp st1
ret
;-------------------------------------------------------------------------------
; XCH opcode: exchange the signals on the stack
;-------------------------------------------------------------------------------
; Mono: a b -> b a
; stereo: a b c d -> c d a b
;-------------------------------------------------------------------------------
section .su_op_xch code align=1
su_op_xch:
fxch st0, st1
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_hold code align=1
su_op_hold:
fld dword [edx] ; f x
fmul st0, st0 ; f^2 x
fchs ; -f^2 x
fadd dword [ebp] ; p-f^2 x
fst dword [ebp] ; p <- p-f^2
fldz ; 0 p x
fucomip st1 ; p x
fstp dword [esp-4] ; t=p, x
jc short su_op_hold_holding ; if (0 < p) goto holding
fld1 ; 1 x
fadd dword [esp-4] ; 1+t x
fstp dword [ebp] ; x
fst dword [ebp+4] ; save holded value
ret ; x
su_op_hold_holding:
fstp st0 ;
fld dword [ebp+4] ; x
ret
;-------------------------------------------------------------------------------
; FILTER opcode: perform low/high/band-pass/notch etc. filtering on the signal
;-------------------------------------------------------------------------------
; Mono: x -> filtered(x)
; Stereo: l r -> filtered(l) filtered(r)
;-------------------------------------------------------------------------------
section .su_op_filter code align=1
su_op_filter:
lodsb ; load the flags to al
call su_effects_stereohelper
fld dword [edx + 4] ; r x
fld dword [edx]; 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 [ebp+12] ; f2 r x
fmul dword [ebp+8] ; f2*b r x
fadd dword [ebp] ; f2*b+l r x
fst dword [ebp] ; l'=f2*b+l r x
fsubp st2, st0 ; r x-l'
fmul dword [ebp+8] ; r*b x-l'
fsubp st1, st0 ; x-l'-r*b
fst dword [ebp+4] ; h'=x-l'-r*b
fmul dword [ebp+12] ; f2*h'
fadd dword [ebp+8] ; f2*h'+b
fstp dword [ebp+8] ; b'=f2*h'+b
fldz ; 0
test al, byte 0x40
jz short su_op_filter_skiplowpass
fadd dword [ebp]
su_op_filter_skiplowpass:
test al, byte 0x20
jz short su_op_filter_skipbandpass
fadd dword [ebp+8]
su_op_filter_skipbandpass:
test al, byte 0x10
jz short su_op_filter_skiphighpass
fadd dword [ebp+4]
su_op_filter_skiphighpass:
test al, byte 0x08
jz short su_op_filter_skipnegbandpass
fsub dword [ebp+8]
su_op_filter_skipnegbandpass:
test al, byte 0x04
jz short su_op_filter_skipneghighpass
fsub dword [ebp+4]
su_op_filter_skipneghighpass:
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_pan code align=1
su_op_pan:
fld dword [edx] ; 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
;-------------------------------------------------------------------------------
; 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)
;-------------------------------------------------------------------------------
section .su_op_delay code align=1
su_op_delay:
lodsw ; al = delay index, ah = delay count
pushad ; Stack: edi, DelayVal, ebp, esp, DelayCom, edx, ecx, eax, retaddr_su_op_delay, edi, OperandStream, Voice, esp, OpcodeStream, Synth, DelayWorkSpace, eax, retaddr_su_run_vm, VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
movzx ebx, al
lea ebx,[su_delay_times + ebx*2] ; BX now points to the right position within delay time table
movzx esi, word [esp + 84] ; notice that we load word, so we wrap at 65536
mov ecx, dword [esp + 60] ; ebp is now the separate delay workspace, as they require a lot more space
jnc su_op_delay_mono
push eax ; save _ah (delay count)
fxch ; r l
call su_op_delay_do ; D(r) l process delay for the right channel
pop eax ; restore the count for second run
fxch ; l D(r)
su_op_delay_mono: ; flow into mono delay
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 dword [esp + 60],ecx ; move delay workspace pointer back to stack.
popad ; Popped: eax, ecx, edx, ebx = DelayCom, esp, ebp, esi = DelayVal, edi. Stack: retaddr_su_op_delay, edi, OperandStream, Voice, esp, OpcodeStream, Synth, DelayWorkSpace, eax, retaddr_su_run_vm, VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
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
;-------------------------------------------------------------------------------
section .su_op_delay_do code align=1
su_op_delay_do: ; x y
fld st0
fmul dword [edx] ; p*x y
fmul dword [edx] ; p*p*x y
fxch ; y p*p*x
fmul dword [edx + 4] ; dr*y p*p*x
su_op_delay_loop:
mov edi, esi
sub di, word [ebx] ; we perform the math in 16-bit to wrap around
fld dword [ecx+su_delayline_wrk.buffer+edi*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 [edx + 12] ; 1-da s dr*y+s p*p*x
fmulp st1, st0 ; s*(1-da) dr*y+s p*p*x
fld dword [edx + 12] ; da s*(1-da) dr*y+s p*p*x
fmul dword [ecx+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
fadd dword [FCONST_0_500000] ; 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 [FCONST_0_500000] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
fst dword [ecx+su_delayline_wrk.filtstate] ; o'=o*da+s*(1-da), o' dr*y+s p*p*x
fmul dword [edx + 8] ; f*o' dr*y+s p*p*x
fadd st0, st2 ; f*o'+p*p*x dr*y+s p*p*x
fstp dword [ecx+su_delayline_wrk.buffer+esi*4]; save f*o'+p*p*x to delay buffer
add ebx,2 ; move to next index
add ecx, 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 [ecx+su_delayline_wrk.dcout] ; o s
fmul dword [FCONST_0_99609375] ; c*o s
fsub dword [ecx+su_delayline_wrk.dcin] ; c*o-i s
fxch ; s c*o-i
fst dword [ecx+su_delayline_wrk.dcin] ; i'=s, s c*o-i
faddp st1 ; s+c*o-i
fadd dword [FCONST_0_500000] ; 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 [FCONST_0_500000] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
fst dword [ecx+su_delayline_wrk.dcout] ; o'=s+c*o-i
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_compressor code align=1
su_op_compressor:
fld st0 ; x x
fmul st0, st0 ; x^2 x
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
fld dword [ebp] ; 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 [ebp] ; 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
fadd dword [FCONST_0_500000] ; 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 [FCONST_0_500000] ; See for example: https://stackoverflow.com/questions/36781881/why-denormalized-floats-are-so-much-slower-than-other-floats-from-hardware-arch
fst dword [ebp] ; l'=l+c*(x^2-l), l' x
fld dword [edx + 12] ; 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 [edx + 16] ; r t*t/l' x
fmul dword [FCONST_0_500000] ; 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 [edx + 8]; this used to be pregain but that ran into problems with getting back up to 0 dB so postgain should be better at that
fld st0 ; and return the computed gain two times, ready for MULP STEREO
ret
;-------------------------------------------------------------------------------
; OUT opcode: outputs and pops the signal
;-------------------------------------------------------------------------------
; Stereo: add ST0 to left out and ST1 to right out, then pop
;-------------------------------------------------------------------------------
section .su_op_out code align=1
su_op_out: ; l r
mov edi, [esp + 24] ; DI points to the synth object, use DI consistently in sinks/sources presumably to increase compression rate
call su_op_out_mono
add edi, 4 ; shift from left to right channel
su_op_out_mono:
fmul dword [edx] ; multiply by gain
fadd dword [edi + su_synthworkspace.left] ; add current value of the output
fstp dword [edi + su_synthworkspace.left] ; store the new value of the output
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_outaux code align=1
su_op_outaux: ; l r
mov edi, [esp + 24]
call su_op_outaux_mono
add edi, 4
su_op_outaux_mono:
fld st0 ; l l
fmul dword [edx] ; g*l
fadd dword [edi + su_synthworkspace.left] ; g*l+o
fstp dword [edi + su_synthworkspace.left] ; o'=g*l+o
fmul dword [edx + 4] ; h*l
fadd dword [edi + su_synthworkspace.aux] ; h*l+a
fstp dword [edi + su_synthworkspace.aux] ; a'=h*l+a
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_send code align=1
su_op_send:
lodsw
mov ecx, [esp + 12] ; load pointer to voice
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 ecx, [esp + 24 + 4]
su_op_send_skipglobal:
popf
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 [edx] ; a l (l)
fsub dword [FCONST_0_500000] ; 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 [ecx + eax*4] ; g*l+L (l),where L is the current value
fstp dword [ecx + eax*4] ; (l)
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_envelope code align=1
su_op_envelope:
jnc su_op_envelope_mono
call su_op_envelope_mono
fld st0
ret
su_op_envelope_mono:
mov eax, dword [edx-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, 3 ; [state]=RELEASE
mov dword [ebp], eax ; note that mov al, XXX; mov ..., eax is less bytes than doing it directly
su_op_envelope_process:
mov eax, dword [ebp] ; al=[state]
fld dword [ebp+4] ; x=[level]
cmp al, 2 ; if (al==SUSTAIN)
je short su_op_envelope_leave2 ; goto leave2
su_op_envelope_attac:
cmp al, 0 ; 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, 1 ; 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 [edx + 8] ; 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, 3 ; 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 [ebp] ; [state]++
su_op_envelope_leave:
fstp st1 ; x', where x' is the new value
fst dword [ebp+4] ; [level]=x'
su_op_envelope_leave2:
fmul dword [edx + 16] ; [gain]*x'
ret
;-------------------------------------------------------------------------------
; NOISE opcode: creates noise
;-------------------------------------------------------------------------------
; Mono: push a random value [-1,1] value on stack
; Stereo: push two (differeent) random values on stack
;-------------------------------------------------------------------------------
section .su_op_noise code align=1
su_op_noise:
lea ecx,[esp + 56]
imul eax, [ecx],16007
mov [ecx],eax
fild dword [ecx]
fidiv dword [ICONST_2147483648] ; 65536*32768
fld dword [edx]
call su_waveshaper
fmul dword [edx + 4]
ret
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_oscillator code align=1
su_op_oscillator:
lodsb ; load the flags
fld dword [edx + 4] ; e, where e is the detune [0,1]
fsub dword [FCONST_0_500000] ; e-.5
fadd st0, st0 ; d=2*e-.5, where d is the detune [-1,1]
jnc su_op_oscillat_mono
fld st0 ; d d
add ebp, 4 ; move wrk...
call su_op_oscillat_mono ; r d
sub ebp, 4 ; ...restore wrk
fxch ; d r
fchs ; -d r, negate the detune for second round
su_op_oscillat_mono:
pushad ; Stack: edi, esi, OscWRK, esp, ebx, edx, ecx, , retaddr_su_op_oscillator, edi, OperandStream, Voice, esp, OpcodeStream, Synth, DelayWorkSpace, eax, retaddr_su_run_vm, VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
fldz ; 0 d
fxch ; d a=0, "accumulated signal"
su_op_oscillat_unison_loop:
fst dword [esp] ; 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 ebp, 8 ; this is ok after all, as there's a pop in the end of unison loop
fld dword [edx + 8] ; p s
fadd dword [ICONST_1034594986] ; 1/12 p s, add some little phase offset to unison oscillators so they don't start in sync
fstp dword [edx + 8] ; s note that this changes the phase for second, possible stereo run. That's probably ok
fld dword [esp] ; d s
fmul dword [FCONST_0_500000] ; .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:
popad ; Popped: eax = , ecx, edx, ebx, esp, ebp = OscWRK, esi, edi. Stack: retaddr_su_op_oscillator, edi, OperandStream, Voice, esp, OpcodeStream, Synth, DelayWorkSpace, eax, retaddr_su_run_vm, VoicesRemain, Sample, Row, GlobalTick, RandSeed, edi, esi, ebp, esp, ebx, edx, ecx, eax, retaddr_su_render_song, OutputBufPtr
ret
su_op_oscillat_single:
fld dword [edx]
fsub dword [FCONST_0_500000]
fdiv dword [FCONST_0_00781250]
faddp st1
test al, byte 0x08
jnz su_op_oscillat_skipnote
fiadd dword [edx-su_voice.inputs+su_voice.note] ; // st0 is note, st1 is t+d offset
su_op_oscillat_skipnote:
fmul dword [ICONST_1034594986]
call su_power
test al, byte 0x08
jz short su_op_oscillat_normalize_note
fmul dword [FCONST_3_80000em05] ; // st0 is now frequency for lfo
jmp short su_op_oscillat_normalized
su_op_oscillat_normalize_note:
fmul dword [FCONST_9_269614em05] ; // st0 is now frequency
su_op_oscillat_normalized:
fadd dword [ebp]
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 [ebp] ; store back the updated phase
fadd dword [edx + 8]
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
fld dword [edx + 12] ; // c p
; every oscillator test included if needed
test al, byte 0x40
jz short su_op_oscillat_notsine
call su_oscillat_sine
su_op_oscillat_notsine:
test al, byte 0x20
jz short su_op_oscillat_not_trisaw
call su_oscillat_trisaw
su_op_oscillat_not_trisaw:
su_op_oscillat_shaping:
; finally, shape the oscillator and apply gain
fld dword [edx + 16]
call su_waveshaper
su_op_oscillat_gain:
fmul dword [edx + 20]
ret
section .su_oscillat_trisaw code align=1
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
section .su_oscillat_sine code align=1
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
;-------------------------------------------------------------------------------
; 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)
;-------------------------------------------------------------------------------
section .su_op_in code align=1
su_op_in:
lodsb
mov edi, [esp + 24]
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 [edi + su_synthworkspace.right + eax*4]
mov dword [edi + su_synthworkspace.right + eax*4], ecx
fld dword [edi + su_synthworkspace.left + eax*4]
mov dword [edi + su_synthworkspace.left + eax*4], ecx
ret
;-------------------------------------------------------------------------------
; 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 operands
; Output: st0 : 2^(-24*x), where x is the parameter in the range 0-1
;-------------------------------------------------------------------------------
section .su_nonlinear_map code align=1
su_nonlinear_map:
fld dword [edx+eax*4] ; x, where x is the parameter in the range 0-1
fimul dword [ICONST_24] ; 24*x
fchs ; -24*x
;-------------------------------------------------------------------------------
; su_power function: computes 2^x
;-------------------------------------------------------------------------------
; Input: st0 : x
; Output: st0 : 2^x
;-------------------------------------------------------------------------------
global _su_pow@0
_su_pow@0:
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
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_op_distort code align=1
su_op_distort:call su_effects_stereohelper
fld dword [edx]
su_waveshaper:
fld st0 ; a a x
fsub dword [FCONST_0_500000] ; 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
;-------------------------------------------------------------------------------
; 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
;-------------------------------------------------------------------------------
section .su_effects_stereohelper code align=1
su_effects_stereohelper:
jnc su_effects_stereohelper_mono ; carry is still the stereo bit
add ebp, 16
fxch ; r l
call [esp] ; call whoever called me...
fxch ; l r
sub ebp, 16 ; move WRK back to where it was
su_effects_stereohelper_mono:
ret ; return to process l/mono sound
section .su_clip code align=1
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
;-------------------------------------------------------------------------------
; 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.
;-------------------------------------------------------------------------------
section .su_vm_jumptable data align=1
su_vm_jumptable:
dd su_op_envelope
dd su_op_oscillator
dd su_op_distort
dd su_op_hold
dd su_op_mulp
dd su_op_noise
dd su_op_filter
dd su_op_addp
dd su_op_pan
dd su_op_delay
dd su_op_xch
dd su_op_outaux
dd su_op_send
dd su_op_compressor
dd su_op_in
dd su_op_out
;-------------------------------------------------------------------------------
; The number of transformed parameters each opcode takes
;-------------------------------------------------------------------------------
section .su_vm_transformcounts data align=1
su_vm_transformcounts:
db 5
db 6
db 1
db 1
db 0
db 2
db 2
db 0
db 1
db 4
db 0
db 2
db 1
db 5
db 0
db 1
;-------------------------------------------------------------------------------
; Patterns
;-------------------------------------------------------------------------------
section .su_patterns data align=1
su_patterns:
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,57,1,1,1,57,1,1,1
db 1,1,1,1,57,1,1,1,1,1,1,1,57,1,1,1
db 1,1,1,1,57,1,1,1,1,1,1,57,57,1,1,1
db 1,1,1,1,57,57,57,57,1,1,1,1,57,1,1,1
db 1,1,1,1,57,1,1,1,57,57,1,1,57,1,1,1
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,46,1,1,1,46,1
db 46,1,1,1,1,1,1,1,1,1,46,1,1,1,1,1
db 46,1,1,1,1,1,1,1,46,1,46,1,1,1,1,1
db 46,1,1,1,1,1,1,1,46,46,46,46,1,1,1,1
db 46,1,1,1,1,1,1,1,1,1,46,46,1,1,1,1
db 48,1,48,1,1,1,48,1,48,1,48,1,1,1,48,1
db 50,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 48,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 46,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,1,1,1,1,1,1,1,1,48,1,50,1,53,1
db 43,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,1,1,1,1,1,1,43,1,45,1,46,1,48,1
db 1,1,1,1,1,1,1,1,1,1,1,1,48,1,50,1
db 52,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 45,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0
db 62,0,64,0,65,0,69,0,65,0,64,0,62,0,64,0
db 65,0,72,0,65,0,64,0,62,0,64,0,65,0,71,0
db 65,0,69,0,65,0,64,0,62,0,64,0,65,0,72,0
db 50,0,50,1,50,0,50,1,50,0,50,1,50,0,50,1
db 48,0,48,1,48,0,48,1,48,0,48,1,48,0,48,1
db 46,0,46,1,46,0,46,1,46,0,46,1,46,0,46,1
db 43,0,43,1,43,0,43,1,43,0,43,1,43,0,43,1
db 43,0,43,1,43,0,43,1,43,0,43,1,43,0,43,0
;-------------------------------------------------------------------------------
; Tracks
;-------------------------------------------------------------------------------
section .su_tracks data align=1
su_tracks:
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,2,3,2,2,2,4,2,2,2,2,2,2,2,5,2,2,2,3,2,2,2,4,2,2,2,2,2,2,2,5,6,6,7,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,9,9,9,10,9,9,9,11,9,9,9,10,9,9,9,12,9,9,9,10,9,9,9,11,9,9,9,10,9,9,9,12,6,6,7,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,6,6,7,0
db 14,6,15,6,16,17,18,19,16,20,21,22,14,6,6,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,24,25,24,26,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,27,27,28,28,29,29,30,30,27,27,28,28,29,29,30,30,27,27,28,28,29,29,30,30,27,27,28,28,29,29,30,31,0,0,0,0
;-------------------------------------------------------------------------------
; Delay times
;-------------------------------------------------------------------------------
section .su_delay_times data align=1
su_delay_times:
dw 1116,1188,1276,1356,1422,1492,1556,1618,1140,1212,1300,1380,1446,1516,1580,1642,21381,12027
;-------------------------------------------------------------------------------
; The code for this patch, basically indices to vm jump table
;-------------------------------------------------------------------------------
section .su_patch_opcodes data align=1
su_patch_opcodes:
db 2,4,6,8,10,2,12,10,14,14,16,14,14,14,14,14,6,8,18,20,22,20,22,25,2,26,2,26,2,26,0,2,26,4,6,8,10,2,12,14,14,10,16,14,14,14,14,18,25,2,2,10,26,0,2,12,10,14,14,14,18,25,0,5,5,17,5,17,15,14,22,14,7,15,3,11,25,4,26,4,26,26,4,26,4,26,4,26,0,2,4,4,16,10,18,20,22,20,25,0,2,26,26,2,4,4,16,10,14,14,18,7,29,11,25,0,31,21,33,0
;-------------------------------------------------------------------------------
; The parameters / inputs to each opcode
;-------------------------------------------------------------------------------
section .su_patch_operands data align=1
su_patch_operands:
db 22,64,0,0,38,77,64,19,128,66,70,64,78,128,0,68,0,0,70,2,128,97,128,68,108,128,68,100,128,68,19,98,16,15,128,80,40,74,80,23,42,16,97,128,59,27,128,112,0,0,15,27,128,112,0,8,15,128,0,38,49,0,0,128,70,40,0,47,54,0,0,128,19,152,0,54,68,70,0,128,26,200,0,39,71,0,0,128,7,0,149,88,64,42,115,114,54,33,58,100,28,45,0,0,128,84,102,93,63,68,75,87,64,11,66,68,53,128,80,7,76,16,13,54,16,64,128,0,71,128,74,0,128,49,71,121,0,128,0,56,0,4,53,0,0,128,102,128,87,128,16,95,128,80,89,71,32,61,45,0,64,87,64,128,65,128,34,64,60,87,128,64,128,35,64,69,0,128,106,128,66,62,91,64,103,26,88,85,31,88,96,12,128,88,25,59,106,60,64,29,21,64,64,4,128,64,128,40,52,25,0,41,42,55,54,63,128,72,75,33,0,59,43,0,74,64,0,128,64,128,72,69,73,0,40,64,0,128,64,128,72,71,120,0,40,64,0,128,64,128,72,86,152,0,39,62,0,30,71,88,62,14,53,27,72,64,88,66,64,113,64,128,64,64,71,128,86,0,16,1,73,128,90,0,17,1,84,52,39,81,0,0,128,87,144,0,90,107,0,33,57,67,57,128,76,64,0,98,64,101,64,64,48,85,78,97,64,35,39,82,64,15,128,68,64,80,48,56,87,46,54,85,35,2,50,128,114,77,0,15,128
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
section .constants data align=1
FCONST_32767_0 dd 0x46fffe00
FCONST_0_00781250 dd 0x3c000000
FCONST_0_500000 dd 0x3f000000
FCONST_0_99609375 dd 0x3f7f0000
FCONST_3_80000em05 dd 0x381f6230
FCONST_9_269614em05 dd 0x38c265dc
ICONST_2147483648 dd 0x80000000
ICONST_1034594986 dd 0x3daaaaaa
ICONST_24 dd 0x18