feat(asm&go4k): Rewrote both library & player to use text/template compiler

There is no more plain .asms, both library & player are created from the templates using go text/template package.
This commit is contained in:
Veikko Sariola
2020-12-14 15:46:12 +02:00
parent 2ad61ff6b2
commit d0bd877b3f
141 changed files with 1195 additions and 5542 deletions

View File

@ -1,4 +1,4 @@
{{- if .Opcode "pop"}}
{{- if .HasOp "pop"}}
;-------------------------------------------------------------------------------
; POP opcode: remove (discard) the topmost signal from the stack
;-------------------------------------------------------------------------------
@ -24,7 +24,7 @@ su_op_pop_mono:
{{end}}
{{- if .Opcode "add"}}
{{- if .HasOp "add"}}
;-------------------------------------------------------------------------------
; ADD opcode: add the two top most signals on the stack
;-------------------------------------------------------------------------------
@ -58,7 +58,7 @@ su_op_add_mono:
{{end}}
{{- if .Opcode "addp"}}
{{- if .HasOp "addp"}}
;-------------------------------------------------------------------------------
; ADDP opcode: add the two top most signals on the stack and pop
;-------------------------------------------------------------------------------
@ -84,7 +84,7 @@ su_op_addp_mono:
{{end}}
{{- if .Opcode "loadnote"}}
{{- if .HasOp "loadnote"}}
;-------------------------------------------------------------------------------
; LOADNOTE opcode: load the current note, scaled to [-1,1]
;-------------------------------------------------------------------------------
@ -109,7 +109,7 @@ su_op_addp_mono:
{{end}}
{{- if .Opcode "mul"}}
{{- if .HasOp "mul"}}
;-------------------------------------------------------------------------------
; MUL opcode: multiply the two top most signals on the stack
;-------------------------------------------------------------------------------
@ -129,7 +129,7 @@ su_op_mul_mono:
{{end}}
{{- if .Opcode "mulp"}}
{{- if .HasOp "mulp"}}
;-------------------------------------------------------------------------------
; MULP opcode: multiply the two top most signals on the stack and pop
;-------------------------------------------------------------------------------
@ -155,7 +155,7 @@ su_op_mulp_mono:
{{end}}
{{- if .Opcode "push"}}
{{- if .HasOp "push"}}
;-------------------------------------------------------------------------------
; PUSH opcode: push the topmost signal on the stack
;-------------------------------------------------------------------------------
@ -181,7 +181,7 @@ su_op_push_mono:
{{end}}
{{- if .Opcode "xch"}}
{{- if .HasOp "xch"}}
;-------------------------------------------------------------------------------
; XCH opcode: exchange the signals on the stack
;-------------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
{{- if .Opcode "distort"}}
{{- if .HasOp "distort"}}
;-------------------------------------------------------------------------------
; DISTORT opcode: apply distortion on the signal
;-------------------------------------------------------------------------------
@ -14,7 +14,7 @@
{{end}}
{{- if .Opcode "hold"}}
{{- if .HasOp "hold"}}
;-------------------------------------------------------------------------------
; HOLD opcode: sample and hold the signal, reducing sample rate
;-------------------------------------------------------------------------------
@ -46,7 +46,7 @@ su_op_hold_holding:
{{end}}
{{- if .Opcode "crush"}}
{{- if .HasOp "crush"}}
;-------------------------------------------------------------------------------
; CRUSH opcode: quantize the signal to finite number of levels
;-------------------------------------------------------------------------------
@ -64,7 +64,7 @@ su_op_hold_holding:
{{end}}
{{- if .Opcode "gain"}}
{{- if .HasOp "gain"}}
;-------------------------------------------------------------------------------
; GAIN opcode: apply gain on the signal
;-------------------------------------------------------------------------------
@ -88,7 +88,7 @@ su_op_gain_mono:
{{end}}
{{- if .Opcode "invgain"}}
{{- if .HasOp "invgain"}}
;-------------------------------------------------------------------------------
; INVGAIN opcode: apply inverse gain on the signal
;-------------------------------------------------------------------------------
@ -112,7 +112,7 @@ su_op_invgain_mono:
{{end}}
{{- if .Opcode "filter"}}
{{- if .HasOp "filter"}}
;-------------------------------------------------------------------------------
; FILTER opcode: perform low/high/band-pass/notch etc. filtering on the signal
;-------------------------------------------------------------------------------
@ -139,31 +139,31 @@ su_op_invgain_mono:
fadd dword [{{.WRK}}+8] ; f2*h'+b
fstp dword [{{.WRK}}+8] ; b'=f2*h'+b
fldz ; 0
{{- if .HasParamValue "filter" "lowpass" 1}}
{{- if .SupportsParamValue "filter" "lowpass" 1}}
test al, byte 0x40
jz short su_op_filter_skiplowpass
fadd dword [{{.WRK}}]
su_op_filter_skiplowpass:
{{- end}}
{{- if .HasParamValue "filter" "bandpass" 1}}
{{- 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 .HasParamValue "filter" "highpass" 1}}
{{- 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 .HasParamValue "filter" "negbandpass" 1}}
{{- 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 .HasParamValue "filter" "neghighpass" 1}}
{{- if .SupportsParamValue "filter" "neghighpass" 1}}
test al, byte 0x04
jz short su_op_filter_skipneghighpass
fsub dword [{{.WRK}}+4]
@ -173,7 +173,7 @@ su_op_filter_skipneghighpass:
{{end}}
{{- if .Opcode "clip"}}
{{- if .HasOp "clip"}}
;-------------------------------------------------------------------------------
; CLIP opcode: clips the signal into [-1,1] range
;-------------------------------------------------------------------------------
@ -188,7 +188,7 @@ su_op_filter_skipneghighpass:
{{end}}
{{- if .Opcode "pan" -}}
{{- if .HasOp "pan" -}}
;-------------------------------------------------------------------------------
; PAN opcode: pan the signal
;-------------------------------------------------------------------------------
@ -220,7 +220,7 @@ su_op_pan_do:
{{end}}
{{- if .Opcode "delay"}}
{{- if .HasOp "delay"}}
;-------------------------------------------------------------------------------
; DELAY opcode: adds delay effect to the signal
;-------------------------------------------------------------------------------
@ -234,12 +234,13 @@ su_op_pan_do:
lodsw ; al = delay index, ah = delay count
{{- .PushRegs .VAL "DelayVal" .COM "DelayCom" | indent 4}}
movzx ebx, al
; %ifdef RUNTIME_TABLES ; when using runtime tables, delaytimes is pulled from the stack so can be a pointer to heap
; mov _SI, [{{.SP}} + su_stack.delaytimes + PUSH_REG_SIZE(2)]
; lea _BX, [_SI + _BX*2]
; %else
{{.Prepare "su_delay_times" | indent 4}}
{{- 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"}}
@ -256,7 +257,7 @@ 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 {{.PTRWORD}} [{{.Stack "DelayWorkSpace"}}],{{.CX}} ; move delay workspace pointer back to stack.
{{- .PopRegs .VAL .COM | indent 4}}
{{- if .UsesDelayModulation}}
{{- if .SupportsModulation "delay" "delaytime"}}
xor eax, eax
mov dword [{{.Modulation "delay" "delaytime"}}], eax
{{- end}}
@ -281,9 +282,9 @@ su_op_delay_mono: ; flow into mono delay
fxch ; y p*p*x
fmul dword [{{.Input "delay" "dry"}}] ; dr*y p*p*x
su_op_delay_loop:
{{- if or .UsesDelayModulation (.HasParamValue "delay" "notetracking" 1)}} ; delaytime modulation or note syncing require computing the delay time in floats
{{- 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 .HasParamValue "delay" "notetracking" 1}}
{{- 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]
@ -293,7 +294,7 @@ su_op_delay_loop:
fdivp st1, st0 ; use 10787 for delaytime to have neutral transpose
su_op_delay_skipnotesync:
{{- end}}
{{- if .UsesDelayModulation}}
{{- 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
@ -339,7 +340,7 @@ su_op_delay_loop:
{{end}}
{{- if .Opcode "compressor"}}
{{- if .HasOp "compressor"}}
;-------------------------------------------------------------------------------
; COMPRES opcode: push compressor gain to stack
;-------------------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
{{- if .Opcode "speed" -}}
{{- if .HasOp "speed" -}}
;-------------------------------------------------------------------------------
; SPEED opcode: modulate the speed (bpm) of the song based on ST0
;-------------------------------------------------------------------------------

View File

@ -1,3 +1,5 @@
{{- if .SupportsParamValue "oscillator" "type" .Sample}}
{{- if eq .OS "windows"}}
{{.ExportFunc "su_load_gmdls"}}
{{- if .Amd64}}
@ -24,7 +26,7 @@
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}}
{{else}}
mov edx, su_sample_table
mov ecx, su_gmdls_path1
su_gmdls_pathloop:
@ -50,9 +52,9 @@ extern _ReadFile@20 ; requires windows
db 'drivers/gm.dls',0
su_gmdls_path2:
db 'drivers/etc/gm.dls',0
{{end}}
{{.SectBss "susamtable"}}
su_sample_table:
resb 3440660 ; size of gmdls.
{{end}}

View File

@ -1,113 +1,10 @@
; source file for compiling sointu as a library
%define SU_DISABLE_PLAYER
%include "sointu/header.inc"
; use every opcode
USE_ADD
USE_ADDP
USE_POP
USE_LOADNOTE
USE_MUL
USE_MULP
USE_PUSH
USE_XCH
USE_DISTORT
USE_HOLD
USE_CRUSH
USE_GAIN
USE_INVGAIN
USE_FILTER
USE_CLIP
USE_PAN
USE_DELAY
USE_COMPRES
USE_SPEED
USE_OUT
USE_OUTAUX
USE_AUX
USE_SEND
USE_ENVELOPE
USE_NOISE
USE_OSCILLAT
USE_LOAD_VAL
USE_RECEIVE
USE_IN
; include stereo variant of each opcode
%define INCLUDE_STEREO_ADD
%define INCLUDE_STEREO_ADDP
%define INCLUDE_STEREO_POP
%define INCLUDE_STEREO_LOADNOTE
%define INCLUDE_STEREO_MUL
%define INCLUDE_STEREO_MULP
%define INCLUDE_STEREO_PUSH
%define INCLUDE_STEREO_XCH
%define INCLUDE_STEREO_DISTORT
%define INCLUDE_STEREO_HOLD
%define INCLUDE_STEREO_CRUSH
%define INCLUDE_STEREO_GAIN
%define INCLUDE_STEREO_INVGAIN
%define INCLUDE_STEREO_FILTER
%define INCLUDE_STEREO_CLIP
%define INCLUDE_STEREO_PAN
%define INCLUDE_STEREO_DELAY
%define INCLUDE_STEREO_COMPRES
%define INCLUDE_STEREO_SPEED
%define INCLUDE_STEREO_OUT
%define INCLUDE_STEREO_OUTAUX
%define INCLUDE_STEREO_AUX
%define INCLUDE_STEREO_SEND
%define INCLUDE_STEREO_ENVELOPE
%define INCLUDE_STEREO_NOISE
%define INCLUDE_STEREO_OSCILLAT
%define INCLUDE_STEREO_LOADVAL
%define INCLUDE_STEREO_RECEIVE
%define INCLUDE_STEREO_IN
; include all features inside all opcodes
%define INCLUDE_TRISAW
%define INCLUDE_SINE
%define INCLUDE_PULSE
%define INCLUDE_GATE
%define INCLUDE_UNISONS
%define INCLUDE_POLYPHONY
%define INCLUDE_MULTIVOICE_TRACKS
%define INCLUDE_DELAY_MODULATION
%define INCLUDE_LOWPASS
%define INCLUDE_BANDPASS
%define INCLUDE_HIGHPASS
%define INCLUDE_NEGBANDPASS
%define INCLUDE_NEGHIGHPASS
%define INCLUDE_GLOBAL_SEND
%define INCLUDE_DELAY_NOTETRACKING
%define INCLUDE_DELAY_FLOAT_TIME
%ifidn __OUTPUT_FORMAT__,win32
%define INCLUDE_SAMPLES
%define INCLUDE_GMDLS
%endif
%ifidn __OUTPUT_FORMAT__,win64
%define INCLUDE_SAMPLES
%define INCLUDE_GMDLS
%endif
%include "sointu/footer.inc"
section .text
struc su_sampleoff
.start resd 1
.loopstart resw 1
.looplength resw 1
.size:
endstruc
{{template "structs.asm" .}}
struc su_synth
.synthwrk resb su_synthworkspace.size
.delaywrks resb su_delayline_wrk.size * 64
.synth_wrk resb su_synthworkspace.size
.delay_wrks resb su_delayline_wrk.size * 64
.delaytimes resw 768
.sampleoffs resb su_sampleoff.size * 256
.sampleoffs resb su_sample_offset.size * 256
.randseed resd 1
.globaltime resd 1
.commands resb 32 * 64
@ -116,189 +13,122 @@ struc su_synth
.numvoices resd 1
endstruc
SECT_TEXT(sursampl)
EXPORT MANGLE_FUNC(su_render,16)
%if BITS == 32 ; stdcall
pushad ; push registers
mov ecx, [esp + 4 + 32] ; ecx = &synthState
mov edx, [esp + 8 + 32] ; edx = &buffer
mov esi, [esp + 12 + 32] ; esi = &samples
mov ebx, [esp + 16 + 32] ; ebx = &time
%else
%ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rcx = &synth, rdx = &buffer, r8 = &bufsize, r9 = &time
push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
mov rsi, r8 ; rsi = &samples
mov rbx, r9 ; rbx = &time
%else ; System V ABI: rdi = &synth, rsi = &buffer, rdx = &samples, rcx = &time
push_registers rbx, rbp ; System V ABI: these registers are non-volatile
mov rbx, rcx ; rbx points to time
xchg rsi, rdx ; rdx points to buffer, rsi points to samples
mov rcx, rdi ; rcx = &Synthstate
%endif
%endif
sub _SP,108 ; allocate space on stack for the FPU state
fsave [_SP] ; save the FPU state to stack & reset the FPU
push _SI ; push the pointer to samples
push _BX ; push the pointer to time
{{.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 ; push samplenumber to stack
mov esi, [_SI] ; zero extend dereferenced pointer
push _SI ; push bufsize
push _DX ; push bufptr
push _CX ; this takes place of the voicetrack
lea _AX, [_CX + su_synth.sampleoffs]
push _AX
lea _AX, [_CX + su_synth.delaytimes]
push _AX
mov eax, [_CX + su_synth.randseed]
push _AX ; randseed
mov eax, [_CX + su_synth.globaltime]
push _AX ; global tick time
mov ebx, dword [_BX] ; zero extend dereferenced pointer
push _BX ; the nominal rowlength should be time_in
{{.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, 0011100001000101b ; mask TOP pointer, stack error, zero divide and invalid operation
test _DI,_DI ; all the aforementioned bits should be 0!
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, [_SP] ; if rowtick >= maxtime
cmp eax, [{{.Stack "RowLength"}}] ; if rowtick >= maxtime
jge su_render_samples_time_finish ; goto finish
mov ecx, [_SP + PTRSIZE*7] ; ecx = buffer length in samples
cmp [_SP + PTRSIZE*8], ecx ; if samples >= maxsamples
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 [_SP + PTRSIZE*8] ; samples++
mov _CX, [_SP + PTRSIZE*5]
push _AX ; push rowtick
mov eax, [_CX + su_synth.polyphony]
push _AX ;polyphony
mov eax, [_CX + su_synth.numvoices]
push _AX ;numvoices
lea _DX, [_CX+ su_synth.synthwrk]
lea COM, [_CX+ su_synth.commands]
lea VAL, [_CX+ su_synth.values]
lea WRK, [_DX + su_synthworkspace.voices]
lea _CX, [_CX+ su_synth.delaywrks - su_delayline_wrk.filtstate]
call MANGLE_FUNC(su_run_vm,0)
pop _AX
pop _AX
mov _DI, [_SP + PTRSIZE*7] ; edi containts buffer ptr
mov _CX, [_SP + PTRSIZE*6]
lea _SI, [_CX + su_synth.synthwrk + su_synthworkspace.left]
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 [_SP + PTRSIZE*7], _DI ; save back the updated ptr
lea _DI, [_SI-8]
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 [_SP + PTRSIZE] ; increment global time, used by delays
{{.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 ; discard delaytimes ptr
pop _CX ; discard samplesoffs ptr
pop _CX
mov [_CX + su_synth.randseed], edx
mov [_CX + su_synth.globaltime], ebx
pop _BX
pop _BX
pop _DX
pop _BX ; pop the pointer to time
pop _SI ; pop the pointer to samples
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 value
frstor [_SP] ; restore fpu state
add _SP,108 ; rewind the stack allocate for FPU state
%if BITS == 32 ; stdcall
mov [_SP + 28],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop
popad
ret 16
%else
%ifidn __OUTPUT_FORMAT__,win64
pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
%else
pop_registers rbx, rbp ; System V ABI: these registers are non-volatile
%endif
{{.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
%endif
{{- 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}}
SECT_DATA(opcodeid)
; Arithmetic opcode ids
EXPORT MANGLE_DATA(su_add_id)
dd ADD_ID
EXPORT MANGLE_DATA(su_addp_id)
dd ADDP_ID
EXPORT MANGLE_DATA(su_pop_id)
dd POP_ID
EXPORT MANGLE_DATA(su_loadnote_id)
dd LOADNOTE_ID
EXPORT MANGLE_DATA(su_mul_id)
dd MUL_ID
EXPORT MANGLE_DATA(su_mulp_id)
dd MULP_ID
EXPORT MANGLE_DATA(su_push_id)
dd PUSH_ID
EXPORT MANGLE_DATA(su_xch_id)
dd XCH_ID
{{template "patch.asm" .}}
; Effect opcode ids
EXPORT MANGLE_DATA(su_distort_id)
dd DISTORT_ID
EXPORT MANGLE_DATA(su_hold_id)
dd HOLD_ID
EXPORT MANGLE_DATA(su_crush_id)
dd CRUSH_ID
EXPORT MANGLE_DATA(su_gain_id)
dd GAIN_ID
EXPORT MANGLE_DATA(su_invgain_id)
dd INVGAIN_ID
EXPORT MANGLE_DATA(su_filter_id)
dd FILTER_ID
EXPORT MANGLE_DATA(su_clip_id)
dd CLIP_ID
EXPORT MANGLE_DATA(su_pan_id)
dd PAN_ID
EXPORT MANGLE_DATA(su_delay_id)
dd DELAY_ID
EXPORT MANGLE_DATA(su_compres_id)
dd COMPRES_ID
; Flowcontrol opcode ids
EXPORT MANGLE_DATA(su_advance_id)
dd SU_ADVANCE_ID
EXPORT MANGLE_DATA(su_speed_id)
dd SPEED_ID
; Sink opcode ids
EXPORT MANGLE_DATA(su_out_id)
dd OUT_ID
EXPORT MANGLE_DATA(su_outaux_id)
dd OUTAUX_ID
EXPORT MANGLE_DATA(su_aux_id)
dd AUX_ID
EXPORT MANGLE_DATA(su_send_id)
dd SEND_ID
; Source opcode ids
EXPORT MANGLE_DATA(su_envelope_id)
dd ENVELOPE_ID
EXPORT MANGLE_DATA(su_noise_id)
dd NOISE_ID
EXPORT MANGLE_DATA(su_oscillat_id)
dd OSCILLAT_ID
EXPORT MANGLE_DATA(su_loadval_id)
dd LOADVAL_ID
EXPORT MANGLE_DATA(su_receive_id)
dd RECEIVE_ID
EXPORT MANGLE_DATA(su_in_id)
dd IN_ID
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
{{.SectData "constants"}}
{{.Constants}}

100
templates/library.h Normal file
View File

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

View File

@ -25,8 +25,8 @@ su_run_vm_loop: ; loop until all voices done
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" | indent 4}}
cmp cl, byte [{{.Use "su_vm_transformcounts"}}+{{.DI}}] ; compare the counter to the value in the param count table
{{- .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
@ -43,11 +43,12 @@ su_transform_values_loop:
su_transform_values_out:
bt dword [{{.COM}}-1],0 ; LSB of COM = stereo bit => carry
{{- .SaveStack "Opcode"}}
{{- .Prepare "su_vm_jumptable" | indent 4}}
call [{{.Use "su_vm_jumptable"}}+{{.DI}}*{{.PTRSIZE}}] ; call the function corresponding to the instruction
{{- $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 .Polyphony}}
{{- 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
@ -173,18 +174,15 @@ su_clip_do:
; 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_offset"}}
su_vm_jumptable equ $ - {{.PTRSIZE}} ; Advance is not in the opcode table
{{- $x := .}}
{{- range .Opcodes}}
{{$x.DPTR}} su_op_{{.Type}}
{{.Data "su_vm_jumptable"}}
{{- range .Instructions}}
{{$.DPTR}} su_op_{{.}}
{{- end}}
;-------------------------------------------------------------------------------
; The number of transformed parameters each opcode takes
;-------------------------------------------------------------------------------
{{.Data "su_vm_transformcounts_offset"}}
su_vm_transformcounts equ $ - 1 ; Advance is not in the opcode table
{{- range .Opcodes}}
db {{.NumParams}}
{{.Data "su_vm_transformcounts"}}
{{- range .Instructions}}
db {{$.TransformCount .}}
{{- end}}

View File

@ -26,7 +26,7 @@ su_synth_obj:
{{- end}}
{{- $prologsize := len .Stacklocs}}
xor eax, eax
{{- if .MultivoiceTracks}}
{{- if ne .VoiceTrackBitmask 0}}
{{.Push (.VoiceTrackBitmask | printf "%v") "VoiceTrackBitmask"}}
{{- end}}
{{.Push "1" "RandSeed"}}
@ -37,20 +37,20 @@ su_render_rowloop: ; loop through every row in the song
xor eax, eax ; ecx is the current sample within row
su_render_sampleloop: ; loop through every sample in the row
{{.Push .AX "Sample"}}
{{- if .Polyphony}}
{{- if .SupportsPolyphony}}
{{.Push (.PolyphonyBitmask | printf "%v") "PolyphonyBitmask"}} ; does the next voice reuse the current opcodes?
{{- end}}
{{.Push (.Song.Patch.TotalVoices | 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 .Opcode "delay"}}
{{- if .HasOp "delay"}}
lea {{.CX}}, [{{.DX}} + 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 .Polyphony}}
{{- if .SupportsPolyphony}}
{{.Pop .AX}}
{{- end}}
{{- template "output_sound.asm" .}} ; *ptr++ = left, *ptr++ = right
@ -64,9 +64,8 @@ su_render_sampleloop: ; loop through every sample in the row
cmp eax, {{.Song.TotalRows}}
jl su_render_rowloop
; rewind the stack the entropy of multiple pop {{.AX}} is probably lower than add
{{- $x := .}}
{{- range (.Sub (len .Stacklocs) $prologsize | .Count)}}
{{$x.Pop $x.AX}}
{{- range slice .Stacklocs $prologsize}}
{{$.Pop $.AX}}
{{- end}}
{{- if .Amd64}}
{{- if eq .OS "windows"}}
@ -89,7 +88,7 @@ su_render_sampleloop: ; loop through every sample in the row
; Dirty: pretty much everything
;-------------------------------------------------------------------------------
{{.Func "su_update_voices"}}
{{- if .MultivoiceTracks}}
{{- if ne .VoiceTrackBitmask 0}}
; The more complicated implementation: one track can trigger multiple voices
xor edx, edx
mov ebx, {{.Song.PatternRows}} ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256...
@ -198,31 +197,31 @@ su_update_voices_skipadd:
db {{.Sequence | toStrings | join ","}}
{{- end}}
{{- if gt (.Song.Patch.SampleOffsets | len) 0}}
{{- if gt (.SampleOffsets | len) 0}}
;-------------------------------------------------------------------------------
; Sample offsets
;-------------------------------------------------------------------------------
{{.Data "su_sample_offsets"}}
{{- range .Song.Patch.SampleOffsets}}
{{- range .SampleOffsets}}
dd {{.Start}}
dw {{.LoopStart}}
dw {{.LoopLength}}
{{- end}}
{{end}}
{{- if gt (.Song.Patch.DelayTimes | len ) 0}}
{{- if gt (.DelayTimes | len ) 0}}
;-------------------------------------------------------------------------------
; Delay times
;-------------------------------------------------------------------------------
{{.Data "su_delay_times"}}
dw {{.Song.Patch.DelayTimes | toStrings | join ","}}
dw {{.DelayTimes | toStrings | join ","}}
{{end}}
;-------------------------------------------------------------------------------
; The code for this patch, basically indices to vm jump table
;-------------------------------------------------------------------------------
{{.Data "su_patch_code"}}
db {{.Code | toStrings | join ","}}
db {{.Commands | toStrings | join ","}}
;-------------------------------------------------------------------------------
; The parameters / inputs to each opcode

50
templates/player.h Normal file
View File

@ -0,0 +1,50 @@
// auto-generated by Sointu, editing not recommended
#ifndef SU_RENDER_H
#define SU_RENDER_H
#define SU_MAX_SAMPLES {{.MaxSamples}}
#define SU_BUFFER_LENGTH (SU_MAX_SAMPLES*2)
#define SU_SAMPLE_RATE 44100
#define SU_BPM {{.Song.BPM}}
#define SU_PATTERN_SIZE {{.Song.PatternRows}}
#define SU_MAX_PATTERNS {{.Song.SequenceLength}}
#define SU_TOTAL_ROWS (SU_MAX_PATTERNS*SU_PATTERN_SIZE)
#define SU_SAMPLES_PER_ROW (SU_SAMPLE_RATE*4*60/(SU_BPM*16))
#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
{{- else}}
typedef float SUsample;
#define SU_SAMPLE_RANGE 1.0
{{- end}}
#ifdef __cplusplus
extern "C" {
#endif
void SU_CALLCONV su_render_song(SUsample *buffer);
{{- if gt (.SampleOffsets | len) 0}}
void SU_CALLCONV su_load_gmdls();
#define SU_LOAD_GMDLS
{{- end}}
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,4 +1,4 @@
{{- if .Opcode "out"}}
{{- if .HasOp "out"}}
;-------------------------------------------------------------------------------
; OUT opcode: outputs and pops the signal
;-------------------------------------------------------------------------------
@ -26,7 +26,7 @@ su_op_out_mono:
{{end}}
{{- if .Opcode "outaux"}}
{{- if .HasOp "outaux"}}
;-------------------------------------------------------------------------------
; OUTAUX opcode: outputs to main and aux1 outputs and pops the signal
;-------------------------------------------------------------------------------
@ -54,7 +54,7 @@ su_op_outaux_mono:
{{end}}
{{- if .Opcode "aux"}}
{{- if .HasOp "aux"}}
;-------------------------------------------------------------------------------
; AUX opcode: outputs the signal to aux (or main) port and pops the signal
;-------------------------------------------------------------------------------
@ -79,7 +79,7 @@ su_op_aux_mono:
{{end}}
{{- if .Opcode "send"}}
{{- if .HasOp "send"}}
;-------------------------------------------------------------------------------
; SEND opcode: adds the signal to a port
;-------------------------------------------------------------------------------
@ -103,7 +103,7 @@ su_op_aux_mono:
fxch ; swap them back: l r
su_op_send_mono:
{{- end}}
{{- if .HasParamValueOtherThan "send" "voice" 0}}
{{- if .SupportsParamValueOtherThan "send" "voice" 0}}
test {{.AX}}, 0x8000
jz su_op_send_skipglobal
mov {{.CX}}, [{{.Stack "Synth"}}]

View File

@ -1,4 +1,4 @@
{{if .Opcode "envelope" -}}
{{if .HasOp "envelope" -}}
;-------------------------------------------------------------------------------
; ENVELOPE opcode: pushes an ADSR envelope value on stack [0,1]
;-------------------------------------------------------------------------------
@ -62,7 +62,7 @@ su_op_envelope_leave2:
{{end}}
{{- if .Opcode "noise"}}
{{- if .HasOp "noise"}}
;-------------------------------------------------------------------------------
; NOISE opcode: creates noise
;-------------------------------------------------------------------------------
@ -91,7 +91,7 @@ su_op_noise_mono:
{{end}}
{{- if .Opcode "oscillator"}}
{{- if .HasOp "oscillator"}}
;-------------------------------------------------------------------------------
; OSCILLAT opcode: oscillator, the heart of the synth
;-------------------------------------------------------------------------------
@ -100,11 +100,10 @@ su_op_noise_mono:
;-------------------------------------------------------------------------------
{{.Func "su_op_oscillator" "Opcode"}}
lodsb ; load the flags
%ifdef RUNTIME_TABLES
%ifdef INCLUDE_SAMPLES
mov {{.DI}}, [{{.SP}} + su_stack.sampleoffs]; we need to put this in a register, as the stereo & unisons screw the stack positions
%endif ; ain't we lucky that {{.DI}} was unused throughout
%endif
{{- 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
@ -120,7 +119,7 @@ su_op_noise_mono:
fchs ; -d r, negate the detune for second round
su_op_oscillat_mono:
{{- end}}
{{- if .HasParamValueOtherThan "oscillator" "unison" 0}}
{{- if .SupportsParamValueOtherThan "oscillator" "unison" 0}}
{{.PushRegs .AX "" .WRK "OscWRK" .AX "OscFlags"}}
fldz ; 0 d
fxch ; d a=0, "accumulated signal"
@ -171,7 +170,7 @@ su_op_oscillat_normalized:
fadd dword [{{.WRK}}]
fst dword [{{.WRK}}]
fadd dword [{{.Input "oscillator" "phase"}}]
{{- if .HasParamValue "oscillator" "type" .Sample}}
{{- if .SupportsParamValue "oscillator" "type" .Sample}}
test al, byte 0x80
jz short su_op_oscillat_not_sample
{{.Call "su_oscillat_sample"}}
@ -185,25 +184,25 @@ su_op_oscillat_not_sample:
fstp st1
fld dword [{{.Input "oscillator" "color"}}] ; // c p
; every oscillator test included if needed
{{- if .HasParamValue "oscillator" "type" .Sine}}
{{- 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 .HasParamValue "oscillator" "type" .Trisaw}}
{{- 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 .HasParamValue "oscillator" "type" .Pulse}}
{{- 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 .HasParamValue "oscillator" "type" .Gate}}
{{- if .SupportsParamValue "oscillator" "type" .Gate}}
test al, byte 0x04
jz short su_op_oscillat_not_gate
{{.Call "su_oscillat_gate"}}
@ -300,12 +299,12 @@ go4kVCO_gate_bit: ; stack: 0/1, let's call it x
{{- .PushRegs .AX "SampleAx" .DX "SampleDx" .CX "SampleCx" .BX "SampleBx" | 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
%ifdef RUNTIME_TABLES ; when using RUNTIME_TABLES, assumed the sample_offset ptr is in {{.DI}}
do{lea {{.DI}}, [}, {{.DI}}, {{.AX}}*8,] ; edi points now to the sample table entry
%else
{{- 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
%endif
{{- end}}
{{- .Float 84.28074964676522 | .Prepare | indent 4}}
fmul dword [{{.Float 84.28074964676522 | .Use}}] ; p*r
fistp dword [{{.SP}}]
@ -329,7 +328,7 @@ su_oscillat_sample_not_looping:
{{end}}
{{- if .Opcode "loadval"}}
{{- if .HasOp "loadval"}}
;-------------------------------------------------------------------------------
; LOADVAL opcode
;-------------------------------------------------------------------------------
@ -356,7 +355,7 @@ su_op_loadval_mono:
{{end}}
{{- if .Opcode "receive"}}
{{- if .HasOp "receive"}}
;-------------------------------------------------------------------------------
; RECEIVE opcode
;-------------------------------------------------------------------------------
@ -387,7 +386,7 @@ su_op_receive_mono:
{{end}}
{{- if .Opcode "in"}}
{{- if .HasOp "in"}}
;-------------------------------------------------------------------------------
; IN opcode: inputs and clears a global port
;-------------------------------------------------------------------------------

View File

@ -41,3 +41,13 @@ struc su_delayline_wrk
.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