Separate Synth and SynthState: SynthState is the part that Render changes.

This should make testing easier, as Synth can be assumed to stay the same
during each call. Synth is also the part that we can parse from .asm/.json file
and a Patch can be compiled into a synth. Synth can be eventually made
quite opaque to the user. The user should not need to worry about opcodes
etc.
This commit is contained in:
Veikko Sariola
2020-10-28 13:44:34 +02:00
parent 64afa9fb48
commit 8183c698da
13 changed files with 196 additions and 156 deletions

View File

@ -10,6 +10,8 @@ import (
import "C" import "C"
// SynthState contains the entire state of sointu sound engine // SynthState contains the entire state of sointu sound engine
type Synth C.Synth // hide C.Synth, explicit cast is still possible if needed
type SynthState C.SynthState // hide C.Synthstate, explicit cast is still possible if needed type SynthState C.SynthState // hide C.Synthstate, explicit cast is still possible if needed
// Opcode is a single byte, representing the virtual machine commands used in Sointu // Opcode is a single byte, representing the virtual machine commands used in Sointu
@ -87,12 +89,12 @@ func (o Opcode) Mono() Opcode {
// buffer float32 slice to fill with rendered samples. Stereo signal, so // buffer float32 slice to fill with rendered samples. Stereo signal, so
// should have even length. // should have even length.
// Returns an error if something went wrong. // Returns an error if something went wrong.
func (s *SynthState) Render(buffer []float32) error { func (synth *Synth) Render(state *SynthState, buffer []float32) error {
if len(buffer)%1 == 1 { if len(buffer)%1 == 1 {
return errors.New("Render writes stereo signals, so buffer should have even length") return errors.New("Render writes stereo signals, so buffer should have even length")
} }
maxSamples := len(buffer) / 2 maxSamples := len(buffer) / 2
errcode := C.su_render((*C.SynthState)(s), (*C.float)(&buffer[0]), C.int(maxSamples)) errcode := C.su_render((*C.Synth)(synth), (*C.SynthState)(state), (*C.float)(&buffer[0]), C.int(maxSamples))
if errcode > 0 { if errcode > 0 {
return errors.New("Render failed") return errors.New("Render failed")
} }
@ -116,30 +118,30 @@ func (s *SynthState) Render(buffer []float32) error {
// time > maxtime, as it is modulated and the time could advance by 2 or more, so the loop // time > maxtime, as it is modulated and the time could advance by 2 or more, so the loop
// exit condition would fire when the time is already past maxtime. // exit condition would fire when the time is already past maxtime.
// Under no conditions, nsamples >= len(buffer)/2 i.e. guaranteed to never overwrite the buffer. // Under no conditions, nsamples >= len(buffer)/2 i.e. guaranteed to never overwrite the buffer.
func (s *SynthState) RenderTime(buffer []float32, maxtime int) (int, int, error) { func (synth *Synth) RenderTime(state *SynthState, buffer []float32, maxtime int) (int, int, error) {
if len(buffer)%1 == 1 { if len(buffer)%1 == 1 {
return -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length") return -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length")
} }
samples := C.int(len(buffer) / 2) samples := C.int(len(buffer) / 2)
time := C.int(maxtime) time := C.int(maxtime)
errcode := int(C.su_render_time((*C.SynthState)(s), (*C.float)(&buffer[0]), &samples, &time)) errcode := int(C.su_render_time((*C.Synth)(synth), (*C.SynthState)(state), (*C.float)(&buffer[0]), &samples, &time))
if errcode > 0 { if errcode > 0 {
return -1, -1, errors.New("RenderTime failed") return -1, -1, errors.New("RenderTime failed")
} }
return int(samples), int(time), nil return int(samples), int(time), nil
} }
func (s *SynthState) SetPatch(patch Patch) error { func Compile(patch Patch) (*Synth, error) {
totalVoices := 0 totalVoices := 0
commands := make([]Opcode, 0) commands := make([]Opcode, 0)
values := make([]byte, 0) values := make([]byte, 0)
polyphonyBitmask := 0 polyphonyBitmask := 0
for _, instr := range patch { for _, instr := range patch {
if len(instr.Units) > 63 { if len(instr.Units) > 63 {
return errors.New("An instrument can have a maximum of 63 units") return nil, errors.New("An instrument can have a maximum of 63 units")
} }
if instr.NumVoices < 1 { if instr.NumVoices < 1 {
return errors.New("Each instrument must have at least 1 voice") return nil, errors.New("Each instrument must have at least 1 voice")
} }
for _, unit := range instr.Units { for _, unit := range instr.Units {
commands = append(commands, unit.Command) commands = append(commands, unit.Command)
@ -153,35 +155,35 @@ func (s *SynthState) SetPatch(patch Patch) error {
polyphonyBitmask <<= 1 polyphonyBitmask <<= 1
} }
if totalVoices > 32 { if totalVoices > 32 {
return errors.New("Sointu does not support more than 32 concurrent voices") return nil, errors.New("Sointu does not support more than 32 concurrent voices")
} }
if len(commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo if len(commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
return errors.New("The patch would result in more than 2048 commands") return nil, errors.New("The patch would result in more than 2048 commands")
} }
if len(values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo if len(values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return errors.New("The patch would result in more than 16384 values") return nil, errors.New("The patch would result in more than 16384 values")
} }
cs := (*C.SynthState)(s) s := new(Synth)
for i := range commands { for i := range commands {
cs.Commands[i] = (C.uchar)(commands[i]) s.Commands[i] = (C.uchar)(commands[i])
} }
for i := range values { for i := range values {
cs.Values[i] = (C.uchar)(values[i]) s.Values[i] = (C.uchar)(values[i])
} }
cs.NumVoices = C.uint(totalVoices) s.NumVoices = C.uint(totalVoices)
cs.Polyphony = C.uint(polyphonyBitmask) s.Polyphony = C.uint(polyphonyBitmask)
return nil return s, nil
} }
func (s *SynthState) Trigger(voice int, note byte) { func (s *SynthState) Trigger(voice int, note byte) {
cs := (*C.SynthState)(s) cs := (*C.SynthState)(s)
cs.Synth.Voices[voice] = C.Voice{} cs.SynthWrk.Voices[voice] = C.Voice{}
cs.Synth.Voices[voice].Note = C.int(note) cs.SynthWrk.Voices[voice].Note = C.int(note)
} }
func (s *SynthState) Release(voice int) { func (s *SynthState) Release(voice int) {
cs := (*C.SynthState)(s) cs := (*C.SynthState)(s)
cs.Synth.Voices[voice].Release = 1 cs.SynthWrk.Voices[voice].Release = 1
} }
func NewSynthState() *SynthState { func NewSynthState() *SynthState {

View File

@ -21,22 +21,25 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
// const bufsize = su_max_samples * 2 // const bufsize = su_max_samples * 2
func TestBridge(t *testing.T) { func TestBridge(t *testing.T) {
s := bridge.NewSynthState() patch := []bridge.Instrument{
s.SetPatch([]bridge.Instrument{
bridge.Instrument{1, []bridge.Unit{ bridge.Instrument{1, []bridge.Unit{
bridge.Unit{bridge.Envelope, []byte{64, 64, 64, 80, 128}}, bridge.Unit{bridge.Envelope, []byte{64, 64, 64, 80, 128}},
bridge.Unit{bridge.Envelope, []byte{95, 64, 64, 80, 128}}, bridge.Unit{bridge.Envelope, []byte{95, 64, 64, 80, 128}},
bridge.Unit{bridge.Out.Stereo(), []byte{128}}, bridge.Unit{bridge.Out.Stereo(), []byte{128}},
}}, }}}
}) synth, err := bridge.Compile(patch)
s.Trigger(0, 64) if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
state := bridge.NewSynthState()
state.Trigger(0, 64)
buffer := make([]float32, 2*su_max_samples) buffer := make([]float32, 2*su_max_samples)
err := s.Render(buffer[:len(buffer)/2]) err = synth.Render(state, buffer[:len(buffer)/2])
if err != nil { if err != nil {
t.Fatalf("first render gave an error") t.Fatalf("first render gave an error")
} }
s.Release(0) state.Release(0)
err = s.Render(buffer[len(buffer)/2:]) err = synth.Render(state, buffer[len(buffer)/2:])
if err != nil { if err != nil {
t.Fatalf("first render gave an error") t.Fatalf("first render gave an error")
} }

View File

@ -15,14 +15,6 @@ typedef struct Voice {
struct Unit Units[63]; struct Unit Units[63];
} Voice; } Voice;
typedef struct Synth {
unsigned char Curvoices[32];
float Left;
float Right;
float Aux[6];
struct Voice Voices[32];
} Synth;
typedef struct DelayWorkspace { typedef struct DelayWorkspace {
float Buffer[65536]; float Buffer[65536];
float Dcin; float Dcin;
@ -30,16 +22,27 @@ typedef struct DelayWorkspace {
float Filtstate; float Filtstate;
} DelayWorkspace; } DelayWorkspace;
typedef struct SynthWorkspace {
unsigned char Curvoices[32];
float Left;
float Right;
float Aux[6];
struct Voice Voices[32];
} SynthWorkspace;
typedef struct SynthState { typedef struct SynthState {
struct Synth 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. 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 int RandSeed;
unsigned int GlobalTick;
} SynthState;
typedef struct Synth {
unsigned char Commands[32 * 64]; unsigned char Commands[32 * 64];
unsigned char Values[32 * 64 * 8]; unsigned char Values[32 * 64 * 8];
unsigned int Polyphony; unsigned int Polyphony;
unsigned int NumVoices; unsigned int NumVoices;
unsigned int RandSeed; } Synth;
unsigned int GlobalTick;
} SynthState;
#pragma pack(pop) #pragma pack(pop)
#if UINTPTR_MAX == 0xffffffff // are we 32-bit? #if UINTPTR_MAX == 0xffffffff // are we 32-bit?
@ -56,11 +59,12 @@ typedef struct SynthState {
extern void CALLCONV su_load_gmdls(void); extern void CALLCONV su_load_gmdls(void);
#endif #endif
// int su_render(SynthState* synthState, float* buffer, int samples): // int su_render(Synth* synth,SynthState* synthState, float* buffer, int samples):
// Renders 'samples' number of samples to the buffer, using and modifying // Renders 'samples' number of 'samples' to the 'buffer', using 'synth'.
// the synthesizer state in synthState. // Modifies 'synthState' and fills the 'buffer'.
// //
// Parameters: // Parameters:
// synth pointer to the synthesizer used. Won't get modified by the call.
// synthState pointer to current synthState. RandSeed should be > 0 e.g. 1 // synthState pointer to current synthState. RandSeed should be > 0 e.g. 1
// buffer audio sample buffer, L R L R ... // buffer audio sample buffer, L R L R ...
// samples maximum number of samples to be rendered. WARNING: buffer // samples maximum number of samples to be rendered. WARNING: buffer
@ -69,15 +73,16 @@ extern void CALLCONV su_load_gmdls(void);
// Returns error code: // Returns error code:
// 0 everything ok // 0 everything ok
// (returns always 0 as no errors are implemented yet) // (returns always 0 as no errors are implemented yet)
int CALLCONV su_render(SynthState* synthState, float* buffer, int samples); int CALLCONV su_render(Synth* synth,SynthState* synthState, float* buffer, int samples);
// int su_render_time(SynthState* synthState, float* buffer, int* samples, int* time): // int su_render_time(Synth* synth,SynthState* synthState, float* buffer, int* samples, int* time):
// Renders samples until 'samples' number of samples are reached or 'time' number of // 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 // 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 // are passed by reference as the function modifies to tell how many samples were
// actually rendered and how many time ticks were actually advanced. // actually rendered and how many time ticks were actually advanced.
// //
// Parameters: // Parameters:
// synth pointer to the synthesizer used. Won't get modified by the call.
// synthState pointer to current synthState. RandSeed should be > 0 e.g. 1 // synthState pointer to current synthState. RandSeed should be > 0 e.g. 1
// Also synthState->SamplesPerRow cannot be 0 or nothing will be // Also synthState->SamplesPerRow cannot be 0 or nothing will be
// rendered; either set it to INT32_MAX to always render full // rendered; either set it to INT32_MAX to always render full
@ -99,7 +104,7 @@ int CALLCONV su_render(SynthState* synthState, float* buffer, int samples);
// Returns error code: // Returns error code:
// 0 everything ok // 0 everything ok
// (no actual errors implemented yet) // (no actual errors implemented yet)
int CALLCONV su_render_time(SynthState* synthState, float* buffer, int* samples, int* time); int CALLCONV su_render_time(Synth* synth,SynthState* synthState, float* buffer, int* samples, int* time);
// Arithmetic opcode ids // Arithmetic opcode ids
extern const int su_add_id; extern const int su_add_id;

View File

@ -86,13 +86,11 @@ func (s *Song) FirstTrackVoice(track int) int {
return ret return ret
} }
func (s *Song) Render() ([]float32, error) { func (s *Song) Render(synth *bridge.Synth, state *bridge.SynthState) ([]float32, error) {
err := s.Validate() err := s.Validate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
synth := bridge.NewSynthState()
synth.SetPatch(s.Patch)
curVoices := make([]int, len(s.Tracks)) curVoices := make([]int, len(s.Tracks))
for i := range curVoices { for i := range curVoices {
curVoices[i] = s.FirstTrackVoice(i) curVoices[i] = s.FirstTrackVoice(i)
@ -113,17 +111,17 @@ func (s *Song) Render() ([]float32, error) {
if note == 1 { // anything but hold causes an action. if note == 1 { // anything but hold causes an action.
continue // TODO: can hold be actually something else than 1? continue // TODO: can hold be actually something else than 1?
} }
synth.Release(curVoices[t]) state.Release(curVoices[t])
if note > 1 { if note > 1 {
curVoices[t]++ curVoices[t]++
first := s.FirstTrackVoice(t) first := s.FirstTrackVoice(t)
if curVoices[t] >= first+s.Tracks[t].NumVoices { if curVoices[t] >= first+s.Tracks[t].NumVoices {
curVoices[t] = first curVoices[t] = first
} }
synth.Trigger(curVoices[t], note) state.Trigger(curVoices[t], note)
} }
} }
samples, _, _ := synth.RenderTime(buffer[2*totaln:], rowtime) samples, _, _ := synth.RenderTime(state, buffer[2*totaln:], rowtime)
totaln += samples totaln += samples
} }
return buffer, nil return buffer, nil

View File

@ -38,7 +38,12 @@ func TestSongRender(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("NewSong failed: %v", err) t.Fatalf("NewSong failed: %v", err)
} }
buffer, err := song.Render() synth, err := bridge.Compile(patch)
if err != nil {
t.Fatalf("Compiling patch failed: %v", err)
}
state := bridge.NewSynthState()
buffer, err := song.Render(synth, state)
if err != nil { if err != nil {
t.Fatalf("Render failed: %v", err) t.Fatalf("Render failed: %v", err)
} }

View File

@ -17,8 +17,8 @@ EXPORT MANGLE_FUNC(su_op_out,0) ; l r
su_op_out_mono: su_op_out_mono:
%endif %endif
fmul dword [INP + su_out_ports.gain] ; g*l fmul dword [INP + su_out_ports.gain] ; g*l
fadd dword [_AX + su_synth.left] ; g*l+o fadd dword [_AX + su_synthworkspace.left] ; g*l+o
fstp dword [_AX + su_synth.left] ; o'=g*l+o fstp dword [_AX + su_synthworkspace.left] ; o'=g*l+o
ret ret
%endif ; SU_OUT_ID > -1 %endif ; SU_OUT_ID > -1
@ -43,11 +43,11 @@ EXPORT MANGLE_FUNC(su_op_outaux,0) ; l r
%endif %endif
fld st0 ; l l fld st0 ; l l
fmul dword [INP + su_outaux_ports.outgain] ; g*l fmul dword [INP + su_outaux_ports.outgain] ; g*l
fadd dword [_AX + su_synth.left] ; g*l+o fadd dword [_AX + su_synthworkspace.left] ; g*l+o
fstp dword [_AX + su_synth.left] ; o'=g*l+o fstp dword [_AX + su_synthworkspace.left] ; o'=g*l+o
fmul dword [INP + su_outaux_ports.auxgain] ; h*l fmul dword [INP + su_outaux_ports.auxgain] ; h*l
fadd dword [_AX + su_synth.aux] ; h*l+a fadd dword [_AX + su_synthworkspace.aux] ; h*l+a
fstp dword [_AX + su_synth.aux] ; a'=h*l+a fstp dword [_AX + su_synthworkspace.aux] ; a'=h*l+a
ret ret
%endif ; SU_OUTAUX_ID > -1 %endif ; SU_OUTAUX_ID > -1
@ -72,8 +72,8 @@ EXPORT MANGLE_FUNC(su_op_aux,0) ; l r
su_op_aux_mono: su_op_aux_mono:
%endif %endif
fmul dword [INP + su_aux_ports.gain] ; g*l fmul dword [INP + su_aux_ports.gain] ; g*l
fadd dword [_DI + su_synth.left + _AX*4] ; g*l+o fadd dword [_DI + su_synthworkspace.left + _AX*4] ; g*l+o
fstp dword [_DI + su_synth.left + _AX*4] ; o'=g*l+o fstp dword [_DI + su_synthworkspace.left + _AX*4] ; o'=g*l+o
ret ret
%endif ; SU_AUX_ID > -1 %endif ; SU_AUX_ID > -1

View File

@ -114,7 +114,7 @@ endstruc
%define AMOUNT(val) val %define AMOUNT(val) val
%define LOCALPORT(unit,port) ((unit+1)*su_unit.size + su_unit.ports)/4 + port %define LOCALPORT(unit,port) ((unit+1)*su_unit.size + su_unit.ports)/4 + port
%define GLOBALPORT(voice,unit,port) SEND_GLOBAL + (su_synth.voices+voice*su_voice.size+su_voice.workspace+unit*su_unit.size + su_unit.ports)/4 + port %define GLOBALPORT(voice,unit,port) SEND_GLOBAL + (su_synthworkspace.voices+voice*su_voice.size+su_voice.workspace+unit*su_unit.size + su_unit.ports)/4 + port
%define OUTPORT 0 %define OUTPORT 0
%define SEND_POP 0x8000 %define SEND_POP 0x8000
%define SEND_GLOBAL 0x4000 %define SEND_GLOBAL 0x4000

View File

@ -406,13 +406,13 @@ EXPORT MANGLE_FUNC(su_op_in,0)
sub _DI, 4 sub _DI, 4
su_op_in_right: su_op_in_right:
xor ecx, ecx xor ecx, ecx
fld dword [_DI + su_synth.right + _AX*4] fld dword [_DI + su_synthworkspace.right + _AX*4]
mov dword [_DI + su_synth.right + _AX*4], ecx mov dword [_DI + su_synthworkspace.right + _AX*4], ecx
%else %else
xor ecx, ecx xor ecx, ecx
mov _DI, [_SP + su_stack.synth] mov _DI, [_SP + su_stack.synth]
fld dword [_DI + su_synth.left + _AX*4] fld dword [_DI + su_synthworkspace.left + _AX*4]
mov dword [_DI + su_synth.left + _AX*4], ecx mov dword [_DI + su_synthworkspace.left + _AX*4], ecx
%endif %endif
ret ret

View File

@ -26,37 +26,50 @@ USE_OUT
section .text section .text
struc su_synth_state struc su_synth_state
.synth resb su_synth.size .synthwrk resb su_synthworkspace.size
.delaywrks resb su_delayline_wrk.size * 64 .delaywrks resb su_delayline_wrk.size * 64
.commands resb 32 * 64
.values resb 32 * 64 * 8
.polyphony resd 1
.numvoices resd 1
.randseed resd 1 .randseed resd 1
.globaltime resd 1 .globaltime resd 1
endstruc endstruc
struc su_synth
.commands resb 32 * 64
.values resb 32 * 64 * 8
.polyphony resd 1
.numvoices resd 1
endstruc
SECT_TEXT(sursampl) SECT_TEXT(sursampl)
EXPORT MANGLE_FUNC(su_render_time,16) EXPORT MANGLE_FUNC(su_render_time,20)
%if BITS == 32 ; stdcall %if BITS == 32 ; stdcall
pushad ; push registers pushad ; push registers
mov ecx, [esp + 4 + 32] ; ecx = &synthState mov eax, [esp + 4 + 32] ; eax = &synth
mov edx, [esp + 8 + 32] ; edx = &buffer mov ecx, [esp + 8 + 32] ; ecx = &synthState
mov esi, [esp + 12 + 32] ; esi = &samples mov edx, [esp + 12 + 32] ; edx = &buffer
mov ebx, [esp + 16 + 32] ; ebx = &time mov esi, [esp + 16 + 32] ; esi = &samples
mov ebx, [esp + 20 + 32] ; ebx = &time
%else %else
%ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rcx = &synthstate, rdx = &buffer, r8 = &bufsize, r9 = &time %ifidn __OUTPUT_FORMAT__,win64
; win64 ABI parameter order: RCX, RDX, R8, R9; rest in stack (right-to-left, note shadow space!)
; here: rcx = &synth, rdx = &synthstate, r8 = &buffer, r9 = &bufsize, stack1 = &time
push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
mov rsi, r8 ; rsi = &samples mov rbx, [rsp + 0x28 + PUSH_REG_SIZE(4)] ; rbx = &time, note the shadow space so &time is at rsp + 0x28
mov rbx, r9 ; rbx = &time mov rax, rcx
%else ; System V ABI: rdi = &synthstate, rsi = &buffer, rdx = &samples, rcx = &time mov rcx, rdx
mov rdx, r8
mov rsi, r9
%else
; System V ABI parameter order: RDI, RSI, RDX, RCX, R8, R9; rest in stack (right-to-left)
; here: rdi = &synth, rsi = &synthstate, rdx = &buffer, rcx = &samples, r8 = &time
push_registers rbx, rbp ; System V ABI: these registers are non-volatile push_registers rbx, rbp ; System V ABI: these registers are non-volatile
mov rbx, rcx ; rbx points to time mov rax, rdi
xchg rsi, rdx ; rdx points to buffer, rsi points to samples mov rbx, r8
mov rcx, rdi ; rcx = &Synthstate xchg rcx, rsi
%endif %endif
%endif %endif
; _AX = &synth, _BX == &time, _CX == &synthstate, _DX == &buf, _SI == &samples,
push _AX ; push the pointer to synth to stack
push _SI ; push the pointer to samples push _SI ; push the pointer to samples
push _BX ; push the pointer to time push _BX ; push the pointer to time
xor eax, eax ; samplenumber starts at 0 xor eax, eax ; samplenumber starts at 0
@ -80,23 +93,24 @@ su_render_samples_loop:
jge su_render_samples_time_finish ; goto finish jge su_render_samples_time_finish ; goto finish
inc eax ; time++ inc eax ; time++
inc dword [_SP + PTRSIZE*6] ; samples++ inc dword [_SP + PTRSIZE*6] ; samples++
mov _CX, [_SP + PTRSIZE*3] mov _CX, [_SP + PTRSIZE*3] ; _CX = &synthstate
mov _BP, [_SP + PTRSIZE*9] ; _BP = &synth
push _AX ; push rowtick push _AX ; push rowtick
mov eax, [_CX + su_synth_state.polyphony] mov eax, [_BP + su_synth.polyphony]
push _AX ;polyphony push _AX ;polyphony
mov eax, [_CX + su_synth_state.numvoices] mov eax, [_BP + su_synth.numvoices]
push _AX ;numvoices push _AX ;numvoices
lea _DX, [_CX+ su_synth_state.synth] lea _DX, [_CX + su_synth_state.synthwrk]
lea COM, [_CX+ su_synth_state.commands] lea COM, [_BP + su_synth.commands]
lea VAL, [_CX+ su_synth_state.values] lea VAL, [_BP + su_synth.values]
lea WRK, [_DX + su_synth.voices] lea WRK, [_DX + su_synthworkspace.voices] ; _BP and WRK are actually the same thing so here _BP gets overwritten
lea _CX, [_CX + su_synth_state.delaywrks - su_delayline_wrk.filtstate] lea _CX, [_CX + su_synth_state.delaywrks - su_delayline_wrk.filtstate]
call MANGLE_FUNC(su_run_vm,0) call MANGLE_FUNC(su_run_vm,0)
pop _AX pop _AX
pop _AX pop _AX
mov _DI, [_SP + PTRSIZE*5] ; edi containts buffer ptr mov _DI, [_SP + PTRSIZE*5] ; edi containts buffer ptr
mov _CX, [_SP + PTRSIZE*4] mov _CX, [_SP + PTRSIZE*4]
lea _SI, [_CX + su_synth_state.synth + su_synth.left] lea _SI, [_CX + su_synth_state.synthwrk + su_synthworkspace.left]
movsd ; copy left channel to output buffer movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer movsd ; copy right channel to output buffer
mov [_SP + PTRSIZE*5], _DI ; save back the updated ptr mov [_SP + PTRSIZE*5], _DI ; save back the updated ptr
@ -121,11 +135,12 @@ su_render_samples_time_finish:
pop _SI ; pop the pointer to samples pop _SI ; pop the pointer to samples
mov dword [_SI], edx ; *samples = samples rendered mov dword [_SI], edx ; *samples = samples rendered
mov dword [_BX], eax ; *time = time ticks rendered mov dword [_BX], eax ; *time = time ticks rendered
pop _AX ; pop the synth pointer
xor eax, eax ; TODO: set eax to possible error code, now just 0 xor eax, eax ; TODO: set eax to possible error code, now just 0
%if BITS == 32 ; stdcall %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 mov [_SP + 28],eax ; we want to return eax, but popad pops everything, so put eax to stack for popad to pop
popad popad
ret 16 ret 20
%else %else
%ifidn __OUTPUT_FORMAT__,win64 %ifidn __OUTPUT_FORMAT__,win64
pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile
@ -135,47 +150,44 @@ su_render_samples_time_finish:
ret ret
%endif %endif
EXPORT MANGLE_FUNC(su_render,12) EXPORT MANGLE_FUNC(su_render,16)
%if BITS == 32 ; stdcall %if BITS == 32 ; stdcall
mov eax, 0x7FFFFFFF ; don't care about time, just try to fill the buffer mov eax, 0x7FFFFFFF ; don't care about time, just try to fill the buffer
push eax push eax
mov eax, [esp + 8] ; eax = &synthState push esp
mov ecx, [esp + 12] ; ecx = &buffer lea eax, [esp + 24]
mov edx, [esp + 16] ; edx = samples
push edx
lea edx, [esp + 4]
push edx
lea edx, [esp + 4]
push edx
push ecx
push eax push eax
push dword [esp + 24]
push dword [esp + 24]
push dword [esp + 24]
%else %else
%ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rdx = bufsize, r8 = &buffer, rcx = &synthstate %ifidn __OUTPUT_FORMAT__,win64
push r8 ; win64 ABI parameter order: RCX, RDX, R8, R9; rest in stack (right-to-left, note shadow space!)
mov r8, _SP ; here: rcx = &synth, rdx = &synthstate, r8 = &buffer, r9 = samples
mov r9, 0x7FFFFFFF ; don't care about time, just try to fill the buffer ; put the values in shadow space and get pointers to them
push r9 mov [_SP+0x8],r9
mov r9, _SP ; still, we have to pass a pointer to time, so pointer to stack lea r9, [_SP+0x8]
%else ; System V ABI: rdi = &synthstate, rsi = &buffer, rdx = samples mov [_SP+0x10], dword 0x7FFFFFFF ; don't care about time, just try to fill the buffer
push rdx lea r10, [_SP+0x10]
mov rdx, _SP mov [_SP+0x20], r10
mov rcx, 0x7FFFFFFF ; don't care about time, just try to fill the buffer %else
; System V ABI parameter order: RDI, RSI, RDX, RCX, R8, R9; rest in stack (right-to-left)
; here: rdi = &synth, rsi = &synthstate, rdx = &buffer, rcx = samples
push rcx push rcx
mov rcx, _SP ; still, we have to pass a pointer to time, so pointer to stack mov rcx, _SP ; pass a pointer to samples instead of direct value
mov r8, 0x7FFFFFFF ; don't care about time, just try to fill the buffer
push r8
mov r8, _SP ; still, we have to pass a pointer to time, so pointer to stack
%endif %endif
%endif %endif
call MANGLE_FUNC(su_render_time,16) call MANGLE_FUNC(su_render_time,20)
%if BITS == 32 ; stdcall %if BITS == 32 ; stdcall
pop ecx pop ecx
pop ecx ret 16
ret 12
%else
%ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rdx = bufsize, r8 = &buffer, rcx = &synthstate
pop r9
pop r8
%else %else
%ifnidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rdx = bufsize, r8 = &buffer, rcx = &synthstate
pop rcx pop rcx
pop rdx pop r8
%endif %endif
ret ret
%endif %endif

View File

@ -168,7 +168,7 @@ endstruc
;=============================================================================== ;===============================================================================
SECT_BSS(susynth) SECT_BSS(susynth)
su_synth_obj resb su_synth.size su_synth_obj resb su_synthworkspace.size
%if DELAY_ID > -1 ; if we use delay, then the synth obj should be immediately followed by the delay workspaces %if DELAY_ID > -1 ; if we use delay, then the synth obj should be immediately followed by the delay workspaces
resb NUM_DELAY_LINES*su_delayline_wrk.size resb NUM_DELAY_LINES*su_delayline_wrk.size
@ -287,7 +287,7 @@ EXPORT MANGLE_FUNC(su_power,0)
%ifndef SU_USE_16BIT_OUTPUT %ifndef SU_USE_16BIT_OUTPUT
%ifndef SU_CLIP_OUTPUT ; The modern way. No need to clip; OS can do it. %ifndef SU_CLIP_OUTPUT ; The modern way. No need to clip; OS can do it.
mov _DI, [_SP+su_stack.bufferptr - su_stack.output_sound] ; edi containts ptr mov _DI, [_SP+su_stack.bufferptr - su_stack.output_sound] ; edi containts ptr
mov _SI, PTRWORD su_synth_obj + su_synth.left mov _SI, PTRWORD su_synth_obj + su_synthworkspace.left
movsd ; copy left channel to output buffer movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer movsd ; copy right channel to output buffer
mov [_SP+su_stack.bufferptr - su_stack.output_sound], _DI ; save back the updated ptr mov [_SP+su_stack.bufferptr - su_stack.output_sound], _DI ; save back the updated ptr
@ -300,10 +300,10 @@ EXPORT MANGLE_FUNC(su_power,0)
xor _CX,_CX xor _CX,_CX
xor eax,eax xor eax,eax
%%loop: ; loop over two channels, left & right %%loop: ; loop over two channels, left & right
do fld dword [,su_synth_obj+su_synth.left,_CX*4,] do fld dword [,su_synth_obj+su_synthworkspace.left,_CX*4,]
call su_clip call su_clip
fstp dword [_SI] fstp dword [_SI]
do mov dword [,su_synth_obj+su_synth.left,_CX*4,{],eax} ; clear the sample so the VM is ready to write it do mov dword [,su_synth_obj+su_synthworkspace.left,_CX*4,{],eax} ; clear the sample so the VM is ready to write it
add _SI,4 add _SI,4
cmp ecx,2 cmp ecx,2
jl %%loop jl %%loop
@ -311,7 +311,7 @@ EXPORT MANGLE_FUNC(su_power,0)
%endif %endif
%else ; 16-bit output, always clipped. This is a bit legacy method. %else ; 16-bit output, always clipped. This is a bit legacy method.
mov _SI, [_SP+su_stack.bufferptr - su_stack.output_sound] ; esi points to the output buffer mov _SI, [_SP+su_stack.bufferptr - su_stack.output_sound] ; esi points to the output buffer
mov _DI, PTRWORD su_synth_obj+su_synth.left mov _DI, PTRWORD su_synth_obj+su_synthworkspace.left
mov ecx, 2 mov ecx, 2
%%loop: ; loop over two channels, left & right %%loop: ; loop over two channels, left & right
fld dword [_DI] fld dword [_DI]
@ -361,9 +361,9 @@ su_render_sampleloop: ; loop through every sample in the row
mov COM, PTRWORD MANGLE_DATA(su_commands) ; COM points to vm code mov COM, PTRWORD MANGLE_DATA(su_commands) ; COM points to vm code
mov VAL, PTRWORD MANGLE_DATA(su_params) ; VAL points to unit params mov VAL, PTRWORD MANGLE_DATA(su_params) ; VAL points to unit params
%if DELAY_ID > -1 %if DELAY_ID > -1
lea _CX, [_DX + su_synth.size - su_delayline_wrk.filtstate] lea _CX, [_DX + su_synthworkspace.size - su_delayline_wrk.filtstate]
%endif %endif
lea WRK, [_DX + su_synth.voices] ; WRK points to the first voice lea WRK, [_DX + su_synthworkspace.voices] ; WRK points to the first voice
call MANGLE_FUNC(su_run_vm,0) ; run through the VM code call MANGLE_FUNC(su_run_vm,0) ; run through the VM code
pop _AX pop _AX
%ifdef INCLUDE_POLYPHONY %ifdef INCLUDE_POLYPHONY
@ -424,7 +424,7 @@ su_calculate_voices_loop: ; do {
mov edi, ecx mov edi, ecx
add edi, ebx add edi, ebx
shl edi, MAX_UNITS_SHIFT + 6 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header shl edi, MAX_UNITS_SHIFT + 6 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header
do inc dword [,su_synth_obj+su_synth.voices+su_voice.release,_DI,] ; set the voice currently active to release; notice that it could increment any number of times do inc dword [,su_synth_obj+su_synthworkspace.voices+su_voice.release,_DI,] ; set the voice currently active to release; notice that it could increment any number of times
cmp al, HLD ; if cl < HLD (no new note triggered) cmp al, HLD ; if cl < HLD (no new note triggered)
jl su_update_voices_nexttrack ; goto nexttrack jl su_update_voices_nexttrack ; goto nexttrack
inc ecx ; curvoice++ inc ecx ; curvoice++
@ -435,7 +435,7 @@ su_update_voices_skipreset:
mov byte [_BP],cl mov byte [_BP],cl
add ecx, ebx add ecx, ebx
shl ecx, MAX_UNITS_SHIFT + 6 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header shl ecx, MAX_UNITS_SHIFT + 6 ; each unit = 64 bytes and there are 1<<MAX_UNITS_SHIFT units + small header
do{lea _DI,[},su_synth_obj+su_synth.voices,_CX,] do{lea _DI,[},su_synth_obj+su_synthworkspace.voices,_CX,]
stosd ; save note stosd ; save note
mov ecx, (su_voice.size - su_voice.release)/4 mov ecx, (su_voice.size - su_voice.release)/4
xor eax, eax xor eax, eax
@ -457,7 +457,7 @@ su_update_voices: ; Stack: retaddr row
mov bl, PATTERN_SIZE mov bl, PATTERN_SIZE
div ebx ; eax = current pattern, edx = current row in pattern div ebx ; eax = current pattern, edx = current row in pattern
do{lea _SI, [},MANGLE_DATA(su_tracks),_AX,]; esi points to the pattern data for current track do{lea _SI, [},MANGLE_DATA(su_tracks),_AX,]; esi points to the pattern data for current track
mov _DI, PTRWORD su_synth_obj+su_synth.voices mov _DI, PTRWORD su_synth_obj+su_synthworkspace.voices
mov bl, MAX_TRACKS ; MAX_TRACKS is always <= 32 so this is ok mov bl, MAX_TRACKS ; MAX_TRACKS is always <= 32 so this is ok
su_update_voices_trackloop: su_update_voices_trackloop:
movzx eax, byte [_SI] ; eax = current pattern movzx eax, byte [_SI] ; eax = current pattern

View File

@ -251,9 +251,9 @@ struc su_voice
endstruc endstruc
;------------------------------------------------------------------------------- ;-------------------------------------------------------------------------------
; synth struct ; synthworkspace struct
;------------------------------------------------------------------------------- ;-------------------------------------------------------------------------------
struc su_synth struc su_synthworkspace
.curvoices resb 32 ; these are used by the multitrack player to store which voice is playing on which track .curvoices resb 32 ; these are used by the multitrack player to store which voice is playing on which track
.left resd 1 .left resd 1
.right resd 1 .right resd 1

View File

@ -20,6 +20,7 @@
const int su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS; const int su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS;
void CALLCONV su_render_song(float* buffer) { void CALLCONV su_render_song(float* buffer) {
Synth* synth;
SynthState* synthState; SynthState* synthState;
const unsigned char commands[] = { su_envelope_id, // MONO const unsigned char commands[] = { su_envelope_id, // MONO
su_envelope_id, // MONO su_envelope_id, // MONO
@ -29,17 +30,23 @@ void CALLCONV su_render_song(float* buffer) {
95, 64, 64, 80, 128, // envelope 2 95, 64, 64, 80, 128, // envelope 2
128}; 128};
int retval; int retval;
synthState = malloc(sizeof(SynthState)); // initialize Synth
synth = (Synth*)malloc(sizeof(Synth));
memcpy(synth->Commands, commands, sizeof(commands));
memcpy(synth->Values, values, sizeof(values));
synth->NumVoices = 1;
synth->Polyphony = 0;
// initialize SynthState
synthState = (SynthState*)malloc(sizeof(SynthState));
memset(synthState, 0, sizeof(SynthState)); memset(synthState, 0, sizeof(SynthState));
memcpy(synthState->Commands, commands, sizeof(commands));
memcpy(synthState->Values, values, sizeof(values));
synthState->RandSeed = 1; synthState->RandSeed = 1;
synthState->NumVoices = 1; // triger first voice
synthState->Synth.Voices[0].Note = 64; synthState->SynthWrk.Voices[0].Note = 64;
retval = su_render(synthState, buffer, su_max_samples / 2); retval = su_render(synth, synthState, buffer, su_max_samples / 2);
synthState->Synth.Voices[0].Release++; synthState->SynthWrk.Voices[0].Release++;
buffer = buffer + su_max_samples; buffer = buffer + su_max_samples;
retval = su_render(synthState, buffer, su_max_samples / 2); retval = su_render(synth, synthState, buffer, su_max_samples / 2);
free(synth);
free(synthState); free(synthState);
return; return;
} }

View File

@ -11,6 +11,7 @@
const int su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS; const int su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS;
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
Synth* synth;
SynthState* synthState; SynthState* synthState;
float* buffer; float* buffer;
const unsigned char commands[] = { su_envelope_id, // MONO const unsigned char commands[] = { su_envelope_id, // MONO
@ -25,20 +26,26 @@ int main(int argc, char* argv[]) {
int samples; int samples;
int totalrendered; int totalrendered;
int retval; int retval;
// initialize Synth
synth = (Synth*)malloc(sizeof(Synth));
memcpy(synth->Commands, commands, sizeof(commands));
memcpy(synth->Values, values, sizeof(values));
synth->NumVoices = 1;
synth->Polyphony = 0;
// initialize SynthState
synthState = (SynthState*)malloc(sizeof(SynthState)); synthState = (SynthState*)malloc(sizeof(SynthState));
buffer = (float*)malloc(2 * sizeof(float) * su_max_samples);
memset(synthState, 0, sizeof(SynthState)); memset(synthState, 0, sizeof(SynthState));
memcpy(synthState->Commands, commands, sizeof(commands));
memcpy(synthState->Values, values, sizeof(values));
synthState->RandSeed = 1; synthState->RandSeed = 1;
synthState->NumVoices = 1; // initialize Buffer
synthState->Synth.Voices[0].Note = 64; buffer = (float*)malloc(2 * sizeof(float) * su_max_samples);
// triger first voice
synthState->SynthWrk.Voices[0].Note = 64;
totalrendered = 0; totalrendered = 0;
// First check that when we render using su_render_time with 0 time // First check that when we render using su_render_time with 0 time
// we get nothing done // we get nothing done
samples = su_max_samples; samples = su_max_samples;
time = 0; time = 0;
errcode = su_render_time(synthState, buffer, &samples, &time); errcode = su_render_time(synth, synthState, buffer, &samples, &time);
if (errcode != 0) if (errcode != 0)
{ {
printf("su_render_time returned error"); printf("su_render_time returned error");
@ -58,7 +65,7 @@ int main(int argc, char* argv[]) {
// we get nothing done // we get nothing done
samples = 0; samples = 0;
time = INT32_MAX; time = INT32_MAX;
errcode = su_render_time(synthState, buffer, &samples, &time); errcode = su_render_time(synth, synthState, buffer, &samples, &time);
if (errcode != 0) if (errcode != 0)
{ {
printf("su_render_time returned error"); printf("su_render_time returned error");
@ -81,7 +88,7 @@ int main(int argc, char* argv[]) {
// check that buffer full // check that buffer full
samples = 1; samples = 1;
time = INT32_MAX; time = INT32_MAX;
su_render_time(synthState, &buffer[totalrendered*2], &samples, &time); su_render_time(synth, synthState, &buffer[totalrendered*2], &samples, &time);
totalrendered += samples; totalrendered += samples;
if (samples != 1) if (samples != 1)
{ {
@ -95,7 +102,7 @@ int main(int argc, char* argv[]) {
} }
samples = SAMPLES_PER_ROW - 1; samples = SAMPLES_PER_ROW - 1;
time = INT32_MAX; time = INT32_MAX;
su_render_time(synthState, &buffer[totalrendered * 2], &samples, &time); su_render_time(synth, synthState, &buffer[totalrendered * 2], &samples, &time);
totalrendered += samples; totalrendered += samples;
if (samples != SAMPLES_PER_ROW - 1) if (samples != SAMPLES_PER_ROW - 1)
{ {
@ -108,7 +115,7 @@ int main(int argc, char* argv[]) {
goto fail; goto fail;
} }
if (i == 8) if (i == 8)
synthState->Synth.Voices[0].Release++; synthState->SynthWrk.Voices[0].Release++;
} }
if (totalrendered != su_max_samples) if (totalrendered != su_max_samples)
{ {
@ -117,6 +124,7 @@ int main(int argc, char* argv[]) {
} }
retval = 0; retval = 0;
finish: finish:
free(synth);
free(synthState); free(synthState);
free(buffer); free(buffer);
return retval; return retval;