From 64afa9fb48a79263b458417d3503dd7b1432b7d4 Mon Sep 17 00:00:00 2001 From: Veikko Sariola Date: Tue, 27 Oct 2020 20:15:06 +0200 Subject: [PATCH] Change the C-API to roughly match the new Go-API. The parameter order is now so that all the in/out int parameters are in the end of the signature. --- bridge/bridge.go | 35 ++++------ include/sointu.h | 71 ++++++++++--------- src/sointu.asm | 118 +++++++++++++++++++++++--------- src/sointu_footer.inc | 10 +-- tests/test_render_samples.c | 9 ++- tests/test_render_samples_api.c | 100 ++++++++++++++++++--------- tests/test_renderer.c | 9 ++- 7 files changed, 217 insertions(+), 135 deletions(-) diff --git a/bridge/bridge.go b/bridge/bridge.go index b30f1ee..4be0067 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -2,7 +2,6 @@ package bridge import ( "errors" - "math" ) // #cgo CFLAGS: -I"${SRCDIR}/../include" @@ -93,10 +92,10 @@ func (s *SynthState) Render(buffer []float32) error { return errors.New("Render writes stereo signals, so buffer should have even length") } maxSamples := len(buffer) / 2 - cs := (*C.SynthState)(s) - cs.SamplesPerRow = C.uint(math.MaxInt32) - cs.RowTick = 0 - C.su_render_samples((*C.SynthState)(s), C.int(maxSamples), (*C.float)(&buffer[0])) + errcode := C.su_render((*C.SynthState)(s), (*C.float)(&buffer[0]), C.int(maxSamples)) + if errcode > 0 { + return errors.New("Render failed") + } return nil } @@ -106,7 +105,8 @@ func (s *SynthState) Render(buffer []float32) error { // buffer float32 slice to fill with rendered samples. Stereo signal, so // should have even length. // maxtime how long nominal time to render in samples. Speed unit might modulate time -// so the actual number of samples rendered is not the +// so the actual number of samples rendered depends on the modulation and if +// buffer is full before maxtime is reached. // Returns a tuple (int, int, error), consisting of: // samples number of samples rendered in the buffer // time how much the time advanced @@ -118,20 +118,15 @@ func (s *SynthState) Render(buffer []float32) error { // 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) { if len(buffer)%1 == 1 { - return -1, -1, errors.New("Render writes stereo signals, so buffer should have even length") + return -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length") } - maxSamples := len(buffer) / 2 - cs := (*C.SynthState)(s) - cs.SamplesPerRow = C.uint(maxtime) // these two lines are here just because the C-API is not - cs.RowTick = 0 // updated. SamplesPerRow should be "maxtime" and passed as a parameter - retval := int(C.su_render_samples((*C.SynthState)(s), C.int(maxSamples), (*C.float)(&buffer[0]))) - if retval < 0 { // this ugliness is just because the C-API is not updated yet - return maxSamples, int(cs.RowTick), nil - } else if retval == 0 { - return maxSamples, int(cs.RowTick), nil - } else { - return maxSamples - retval, int(cs.RowTick), nil + samples := C.int(len(buffer) / 2) + time := C.int(maxtime) + errcode := int(C.su_render_time((*C.SynthState)(s), (*C.float)(&buffer[0]), &samples, &time)) + if errcode > 0 { + return -1, -1, errors.New("RenderTime failed") } + return int(samples), int(time), nil } func (s *SynthState) SetPatch(patch Patch) error { @@ -192,9 +187,5 @@ func (s *SynthState) Release(voice int) { func NewSynthState() *SynthState { s := new(SynthState) s.RandSeed = 1 - // The default behaviour will be to have rows/beats disabled i.e. - // fill the whole buffer every call. This is a lot better default - // behaviour than leaving this 0 (Render would never render anything) - s.SamplesPerRow = math.MaxInt32 return s } diff --git a/include/sointu.h b/include/sointu.h index 22ecc3a..74ca9b0 100644 --- a/include/sointu.h +++ b/include/sointu.h @@ -38,9 +38,7 @@ typedef struct SynthState { unsigned int Polyphony; unsigned int NumVoices; unsigned int RandSeed; - unsigned int GlobalTick; - unsigned int RowTick; - unsigned int SamplesPerRow; // nominal value, actual rows could be more or less due to speed modulation + unsigned int GlobalTick; } SynthState; #pragma pack(pop) @@ -58,9 +56,26 @@ typedef struct SynthState { extern void CALLCONV su_load_gmdls(void); #endif -// su_render_samples(SynthState* synthState, int maxSamples, float* buffer): -// Renders at most maxsamples to the buffer, using and modifying the -// synthesizer state in synthState. +// int su_render(SynthState* synthState, float* buffer, int samples): +// Renders 'samples' number of samples to the buffer, using and modifying +// the synthesizer state in synthState. +// +// Parameters: +// synthState pointer to current synthState. RandSeed should be > 0 e.g. 1 +// buffer audio sample buffer, L R L R ... +// samples maximum number of samples to be rendered. WARNING: buffer +// should have a length of 2 * samples as the audio is stereo. +// +// Returns error code: +// 0 everything ok +// (returns always 0 as no errors are implemented yet) +int CALLCONV su_render(SynthState* synthState, float* buffer, int samples); + +// int su_render_time(SynthState* synthState, 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: // synthState pointer to current synthState. RandSeed should be > 0 e.g. 1 @@ -68,39 +83,23 @@ extern void CALLCONV su_load_gmdls(void); // rendered; either set it to INT32_MAX to always render full // buffer, or something like SAMPLE_RATE * 60 / (BPM * 4) for // having 4 rows per beat. -// maxSamples maximum number of samples to be rendered. buffer should -// have a length of 2 * maxsamples as the audio is stereo. // 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. // -// Returns: -// -1 end of row was not reached & buffer full -// 0 end of row was reached & buffer full (there is space for zero -// samples in the buffer) -// n>0 end of row was reached & there is space for n samples in the buffer +// 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 // -// Beware of infinite loops: with a rowlen of 0; or without resetting rowtick -// between rows; or with a problematic synth patch e.g. if the speed is -// modulated to be become infinite, this function might return maxsamples i.e. -// not render any samples. If you try to call this with your buffer until the -// whole buffer is filled, you will be stuck in an infinite loop. -// -// So a reasonable track player would be something like: -// -// function render_buffer(maxsamples,buffer) { -// remaining = maxsamples -// for i = 0..MAX_TRIES // limit retries to prevent infinite loop -// remaining = su_render_samples(synthState, -// remaining, -// &buffer[(maxsamples-remaining)*2]) -// if remaining >= 0 // end of row reached -// song_row++ // advance row -// retrigger/release voices based on the new row -// if remaining <= 0 // buffer full -// return -// return // could not fill buffer despite MAX_TRIES, something is wrong -// // audio will come to sudden end -// } -extern int CALLCONV su_render_samples(SynthState* synthState, int maxSamples, float* buffer); +// Returns error code: +// 0 everything ok +// (no actual errors implemented yet) +int CALLCONV su_render_time(SynthState* synthState, float* buffer, int* samples, int* time); // Arithmetic opcode ids extern const int su_add_id; diff --git a/src/sointu.asm b/src/sointu.asm index d78c8a8..790ba9d 100644 --- a/src/sointu.asm +++ b/src/sointu.asm @@ -33,44 +33,53 @@ struc su_synth_state .polyphony resd 1 .numvoices resd 1 .randseed resd 1 - .globaltime resd 1 - .rowtick resd 1 - .rowlen resd 1 + .globaltime resd 1 endstruc SECT_TEXT(sursampl) -EXPORT MANGLE_FUNC(su_render_samples,12) +EXPORT MANGLE_FUNC(su_render_time,16) %if BITS == 32 ; stdcall pushad ; push registers - mov ecx, [esp + 4 + 32] ; ecx = &synthState - mov esi, [esp + 8 + 32] ; esi = bufsize - mov edx, [esp + 12 + 32] ; edx = &buffer + 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: rdx = bufsize, r8 = &buffer, rcx = &synthstate + %ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rcx = &synthstate, rdx = &buffer, r8 = &bufsize, r9 = &time push_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile - mov rsi, rdx ; rsi = bufsize - mov rdx, r8 ; rdx = &buffer - %else ; System V ABI: rsi = bufsize, rdx = &buffer, rdi = &synthstate + mov rsi, r8 ; rsi = &samples + mov rbx, r9 ; rbx = &time + %else ; System V ABI: rdi = &synthstate, 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 - push _SI ; push bufsize - push _DX ; push bufptr - push _CX ; this takes place of the voicetrack + push _SI ; push the pointer to samples + push _BX ; push the pointer to 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 mov eax, [_CX + su_synth_state.randseed] push _AX ; randseed mov eax, [_CX + su_synth_state.globaltime] - push _AX ; global tick time - mov eax, [_CX + su_synth_state.rowlen] - push _AX ; push the rowlength to stack so we can easily compare to it, normally this would be row - mov eax, [_CX + su_synth_state.rowtick] + push _AX ; global tick time + mov ebx, dword [_BX] ; zero extend dereferenced pointer + push _BX ; the nominal rowlength should be time_in + xor eax, eax ; rowtick starts at 0 su_render_samples_loop: - cmp eax, [_SP] ; compare current tick to rowlength - jge su_render_samples_row_advance - sub dword [_SP + PTRSIZE*5], 1 ; compare current tick to rowlength - jb su_render_samples_buffer_full + cmp eax, [_SP] ; if rowtick >= maxtime + jge su_render_samples_time_finish ; goto finish + mov ecx, [_SP + PTRSIZE*5] ; ecx = buffer length in samples + cmp [_SP + PTRSIZE*6], ecx ; if samples >= maxsamples + jge su_render_samples_time_finish ; goto finish + inc eax ; time++ + inc dword [_SP + PTRSIZE*6] ; samples++ mov _CX, [_SP + PTRSIZE*3] push _AX ; push rowtick mov eax, [_CX + su_synth_state.polyphony] @@ -97,24 +106,26 @@ su_render_samples_loop: 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 - inc eax jmp su_render_samples_loop -su_render_samples_row_advance: - xor eax, eax ; row has finished, so clear the rowtick for next round -su_render_samples_buffer_full: +su_render_samples_time_finish: pop _CX pop _BX pop _DX pop _CX mov [_CX + su_synth_state.randseed], edx - mov [_CX + su_synth_state.globaltime], ebx - mov [_CX + su_synth_state.rowtick], eax - pop _AX - pop _AX + mov [_CX + su_synth_state.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 + xor eax, eax ; TODO: set eax to possible error code, now just 0 %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 12 + ret 16 %else %ifidn __OUTPUT_FORMAT__,win64 pop_registers rdi, rsi, rbx, rbp ; win64 ABI: these registers are non-volatile @@ -123,3 +134,48 @@ su_render_samples_buffer_full: %endif ret %endif + +EXPORT MANGLE_FUNC(su_render,12) +%if BITS == 32 ; stdcall + mov eax, 0x7FFFFFFF ; don't care about time, just try to fill the buffer + push eax + mov eax, [esp + 8] ; eax = &synthState + mov ecx, [esp + 12] ; ecx = &buffer + mov edx, [esp + 16] ; edx = samples + push edx + lea edx, [esp + 4] + push edx + lea edx, [esp + 4] + push edx + push ecx + push eax +%else + %ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rdx = bufsize, r8 = &buffer, rcx = &synthstate + push r8 + mov r8, _SP + mov r9, 0x7FFFFFFF ; don't care about time, just try to fill the buffer + push r9 + mov r9, _SP ; still, we have to pass a pointer to time, so pointer to stack + %else ; System V ABI: rdi = &synthstate, rsi = &buffer, rdx = samples + push rdx + mov rdx, _SP + mov rcx, 0x7FFFFFFF ; don't care about time, just try to fill the buffer + push rcx + mov rcx, _SP ; still, we have to pass a pointer to time, so pointer to stack + %endif +%endif + call MANGLE_FUNC(su_render_time,16) +%if BITS == 32 ; stdcall + pop ecx + pop ecx + ret 12 +%else + %ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rdx = bufsize, r8 = &buffer, rcx = &synthstate + pop r9 + pop r8 + %else + pop rcx + pop rdx + %endif + ret +%endif diff --git a/src/sointu_footer.inc b/src/sointu_footer.inc index fff207c..aeb9d07 100644 --- a/src/sointu_footer.inc +++ b/src/sointu_footer.inc @@ -331,15 +331,15 @@ EXPORT MANGLE_FUNC(su_power,0) %endmacro ;------------------------------------------------------------------------------- -; su_render function: the entry point for the synth +; su_render_song function: the entry point for the synth ;------------------------------------------------------------------------------- -; Has the signature su_render(void *ptr), where ptr is a pointer to -; the output buffer +; 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 ;------------------------------------------------------------------------------- -SECT_TEXT(surender) +SECT_TEXT(surensng) -EXPORT MANGLE_FUNC(su_render,PTRSIZE) ; Stack: ptr +EXPORT MANGLE_FUNC(su_render_song,PTRSIZE) ; Stack: ptr render_prologue xor eax, eax %ifdef INCLUDE_MULTIVOICE_TRACKS diff --git a/tests/test_render_samples.c b/tests/test_render_samples.c index 1238153..039495e 100644 --- a/tests/test_render_samples.c +++ b/tests/test_render_samples.c @@ -19,7 +19,7 @@ #define SAMPLES_PER_ROW SAMPLE_RATE * 4 * 60 / (BPM * 16) const int su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS; -void CALLCONV su_render(float* buffer) { +void CALLCONV su_render_song(float* buffer) { SynthState* synthState; const unsigned char commands[] = { su_envelope_id, // MONO su_envelope_id, // MONO @@ -33,14 +33,13 @@ void CALLCONV su_render(float* buffer) { memset(synthState, 0, sizeof(SynthState)); memcpy(synthState->Commands, commands, sizeof(commands)); memcpy(synthState->Values, values, sizeof(values)); - synthState->RandSeed = 1; - synthState->SamplesPerRow = INT32_MAX; + synthState->RandSeed = 1; synthState->NumVoices = 1; synthState->Synth.Voices[0].Note = 64; - retval = su_render_samples(synthState, su_max_samples / 2, buffer); + retval = su_render(synthState, buffer, su_max_samples / 2); synthState->Synth.Voices[0].Release++; buffer = buffer + su_max_samples; - retval = su_render_samples(synthState, su_max_samples / 2, buffer); + retval = su_render(synthState, buffer, su_max_samples / 2); free(synthState); return; } diff --git a/tests/test_render_samples_api.c b/tests/test_render_samples_api.c index 6e3cad6..3755b14 100644 --- a/tests/test_render_samples_api.c +++ b/tests/test_render_samples_api.c @@ -19,8 +19,11 @@ int main(int argc, char* argv[]) { su_advance_id };// MONO const unsigned char values[] = { 64, 64, 64, 80, 128, // envelope 1 95, 64, 64, 80, 128, // envelope 2 - 128 }; - int remaining, remainingOut; + 128 }; + int errcode; + int time; + int samples; + int totalrendered; int retval; synthState = (SynthState*)malloc(sizeof(SynthState)); buffer = (float*)malloc(2 * sizeof(float) * su_max_samples); @@ -29,58 +32,89 @@ int main(int argc, char* argv[]) { memcpy(synthState->Values, values, sizeof(values)); synthState->RandSeed = 1; synthState->NumVoices = 1; - synthState->Synth.Voices[0].Note = 64; - remaining = su_max_samples; - // First check that when RowLen = 0, we render nothing and remaining does not change - synthState->SamplesPerRow = 0; - if (su_render_samples(synthState, remaining, buffer) != remaining) + synthState->Synth.Voices[0].Note = 64; + totalrendered = 0; + // First check that when we render using su_render_time with 0 time + // we get nothing done + samples = su_max_samples; + time = 0; + errcode = su_render_time(synthState, buffer, &samples, &time); + if (errcode != 0) { - printf("su_render_samples rendered samples despite number of samples per row being 0"); + printf("su_render_time returned error"); + goto fail; + } + if (samples > 0) + { + printf("su_render_time rendered samples, despite it should not"); + goto fail; + } + if (time > 0) + { + printf("su_render_time advanced time, despite it should not"); + goto fail; + } + // Then check that when we render using su_render_time with 0 samples, + // we get nothing done + samples = 0; + time = INT32_MAX; + errcode = su_render_time(synthState, buffer, &samples, &time); + if (errcode != 0) + { + printf("su_render_time returned error"); + goto fail; + } + if (samples > 0) + { + printf("su_render_time rendered samples, despite it should not"); + goto fail; + } + if (time > 0) + { + printf("su_render_time advanced time, despite it should not"); goto fail; } // Then check that each time we call render, only SAMPLES_PER_ROW // number of samples are rendered - synthState->SamplesPerRow = SAMPLES_PER_ROW; for (int i = 0; i < 16; i++) { // Simulate "small buffers" i.e. render a buffer with 1 sample // check that buffer full - remainingOut = su_render_samples(synthState, 1, &buffer[2 * (su_max_samples - remaining)]); - if (remainingOut != -1) + samples = 1; + time = INT32_MAX; + su_render_time(synthState, &buffer[totalrendered*2], &samples, &time); + totalrendered += samples; + if (samples != 1) { - printf("su_render_samples should have return -1, as it should have believed buffer is full"); + printf("su_render should have return 1, as it should have believed buffer is full"); goto fail; } - if (synthState->RowTick != 1) + if (time != 1) { - printf("su_render_samples RowTick should be at 1 after rendering 1 tick of a row"); + printf("su_render should have advanced the time also by one"); + goto fail; + } + samples = SAMPLES_PER_ROW - 1; + time = INT32_MAX; + su_render_time(synthState, &buffer[totalrendered * 2], &samples, &time); + totalrendered += samples; + if (samples != SAMPLES_PER_ROW - 1) + { + printf("su_render should have return SAMPLES_PER_ROW - 1, as it should have believed buffer is full"); goto fail; } - remaining--; // we rendered just one sample - remainingOut = su_render_samples(synthState, remaining, &buffer[2 * (su_max_samples - remaining)]); - if (remainingOut != remaining - SAMPLES_PER_ROW + 1) + if (time != SAMPLES_PER_ROW - 1) { - printf("su_render_samples did not render SAMPLES_PER_ROW, despite rowLen being SAMPLES_PER_ROW"); + printf("su_render should have advanced the time also by SAMPLES_PER_ROW - 1"); goto fail; } - if (synthState->RowTick != 0) - { - printf("The row should be have been reseted"); - goto fail; - } - remaining = remainingOut; if (i == 8) - synthState->Synth.Voices[0].Release++; + synthState->Synth.Voices[0].Release++; } - if (remaining != 0) { - printf("The buffer should be full and row finished"); - goto fail; - } - // Finally, now that there is no more buffer remaining, should return -1 - if (su_render_samples(synthState, remaining, &buffer[2 * (su_max_samples - remaining)]) != -1) + if (totalrendered != su_max_samples) { - printf("su_render_samples should have ran out of buffer and thus return -1"); + printf("su_render should have rendered a total of su_max_samples"); goto fail; - } + } retval = 0; finish: free(synthState); diff --git a/tests/test_renderer.c b/tests/test_renderer.c index c52edff..5ad5b10 100644 --- a/tests/test_renderer.c +++ b/tests/test_renderer.c @@ -24,7 +24,7 @@ #else // 64-bit #define CALLCONV // the asm will use honor honor correct x64 ABI on all 64-bit platforms #endif -extern void CALLCONV su_render(void *); +extern void CALLCONV su_render_song(void *); #ifdef INCLUDE_GMDLS extern void CALLCONV su_load_gmdls(void); @@ -67,7 +67,7 @@ int main(int argc, char* argv[]) { su_load_gmdls(); #endif - su_render(buf); + su_render_song(buf); snprintf(filename, sizeof filename, "%s%s%s", expected_output_folder, test_name, ".raw"); @@ -117,7 +117,10 @@ int main(int argc, char* argv[]) { printf("4klang rendered different wave than expected\n"); goto fail; } - max_diff = fmax(diff, max_diff); + + if (diff > max_diff) { + max_diff = diff; + } } if (max_diff > 1e-6) {