diff --git a/bridge/bridge.go b/bridge/bridge.go index 3ffb595..fc4391c 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -2,6 +2,7 @@ package bridge import "fmt" import "unsafe" +import "math" // #cgo CFLAGS: -I"${SRCDIR}/../include" // #cgo LDFLAGS: "${SRCDIR}/../build/src/libsointu.a" @@ -83,16 +84,12 @@ func (s *SynthState) Release(voice int) { fmt.Printf("Returning from Release...\n") } -func (s *SynthState) RowEnd() bool { - return s.RowTick == s.RowLen -} - -func (s *SynthState) ResetRow() bool { - return s.RowTick == 0 -} - 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/bridge/bridge_test.go b/bridge/bridge_test.go index 9f6ef47..e85eb95 100644 --- a/bridge/bridge_test.go +++ b/bridge/bridge_test.go @@ -5,7 +5,6 @@ import ( "encoding/binary" "github.com/vsariola/sointu/bridge" "io/ioutil" - "math" "path" "runtime" "testing" @@ -34,7 +33,6 @@ func TestBridge(t *testing.T) { // synthState->RandSeed = 1; // initialized in NewSynthState // synthState->RowLen = INT32_MAX; - s.RowLen = math.MaxInt32 // (why?) // synthState->NumVoices = 1; s.NumVoices = 1 // synthState->Synth.Voices[0].Note = 64; @@ -42,15 +40,15 @@ func TestBridge(t *testing.T) { // retval = su_render_samples(buffer, su_max_samples / 2, synthState); buffer := make([]float32, su_max_samples) remaining := s.Render(buffer) - if remaining != 0 { - t.Fatalf("could not render full buffer, %v bytes remaining, expected %v", remaining, len(buffer)) + if remaining > 0 { + t.Fatalf("could not render full buffer, %v bytes remaining, expected <= 0", remaining) } // synthState->Synth.Voices[0].Release++; s.Synth.Voices[0].Release++ sbuffer := make([]float32, su_max_samples) remaining = s.Render(sbuffer) - if remaining != 0 { - t.Fatalf("could not render second full buffer, %v bytes remaining, expected %v", remaining, len(buffer)) + if remaining > 0 { + t.Fatalf("could not render second full buffer, %v bytes remaining, expected <= 0", remaining) } buffer = append(buffer, sbuffer...) _, filename, _, _ := runtime.Caller(0) diff --git a/include/sointu.h b/include/sointu.h index 1ba22e1..22ecc3a 100644 --- a/include/sointu.h +++ b/include/sointu.h @@ -38,9 +38,9 @@ typedef struct SynthState { unsigned int Polyphony; unsigned int NumVoices; unsigned int RandSeed; - unsigned int Globaltime; + unsigned int GlobalTick; unsigned int RowTick; - unsigned int RowLen; + unsigned int SamplesPerRow; // nominal value, actual rows could be more or less due to speed modulation } SynthState; #pragma pack(pop) @@ -58,22 +58,49 @@ typedef struct SynthState { extern void CALLCONV su_load_gmdls(void); #endif -// Returns the number of samples remaining in the buffer i.e. 0 if the buffer was -// filled completely. +// su_render_samples(SynthState* synthState, int maxSamples, float* buffer): +// Renders at most maxsamples to the buffer, using and modifying the +// synthesizer state in synthState. // -// NOTE: The buffer should have a length of 2 * maxsamples, as the audio -// is stereo. +// Parameters: +// synthState pointer to current synthState. RandSeed should be > 0 e.g. 1 +// Also synthState->SamplesPerRow cannot be 0 or nothing will be +// 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 ... // -// You should always check if rowtick >= rowlen after calling this. If so, most -// likely you didn't get full buffer filled but the end of row was hit before -// filling the buffer. In that case, trigger/release new voices, set rowtick to 0. +// 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 // // 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. -extern int CALLCONV su_render_samples(SynthState* synthState, int maxsamples, float* buffer); +// 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); // Arithmetic opcode ids extern const int su_add_id; @@ -110,7 +137,6 @@ extern const int su_send_id; // Source opcode ids extern const int su_envelope_id; extern const int su_noise_id; -extern const int su_aux_id; extern const int su_oscillat_id; extern const int su_loadval_id; extern const int su_receive_id; diff --git a/src/sointu.asm b/src/sointu.asm index f96a18c..9c0ea46 100644 --- a/src/sointu.asm +++ b/src/sointu.asm @@ -66,7 +66,11 @@ EXPORT MANGLE_FUNC(su_render_samples,12) 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] -su_render_samples_loop: +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 mov _CX, [_SP + PTRSIZE*3] push _AX ; push rowtick mov eax, [_CX + su_synth_state.polyphony] @@ -93,12 +97,11 @@ 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 - dec dword [_SP + PTRSIZE*5] - jz su_render_samples_finish - cmp eax, [_SP] ; compare current tick to rowlength - jl su_render_samples_loop -su_render_samples_finish: + 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: pop _CX pop _BX pop _DX @@ -107,7 +110,7 @@ su_render_samples_finish: mov [_CX + su_synth_state.globaltime], ebx mov [_CX + su_synth_state.rowtick], eax pop _AX - pop _AX ; todo: return correct value based on this + pop _AX %if BITS == 32 ; stdcall popad ret 12 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a469fc4..0f6975e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -151,4 +151,8 @@ regression_test(test_chords "ENVELOPE;VCO_SINE") regression_test(test_speed "ENVELOPE;VCO_SINE") regression_test(test_render_samples ENVELOPE "" test_render_samples.c) -target_link_libraries(test_render_samples sointu) \ No newline at end of file +target_link_libraries(test_render_samples sointu) + +add_executable(test_render_samples_api test_render_samples_api.c) +target_link_libraries(test_render_samples_api sointu) +add_test(test_render_samples_api test_render_samples_api) diff --git a/tests/test_render_samples.c b/tests/test_render_samples.c index fd59d0d..1238153 100644 --- a/tests/test_render_samples.c +++ b/tests/test_render_samples.c @@ -34,7 +34,7 @@ void CALLCONV su_render(float* buffer) { memcpy(synthState->Commands, commands, sizeof(commands)); memcpy(synthState->Values, values, sizeof(values)); synthState->RandSeed = 1; - synthState->RowLen = INT32_MAX; + synthState->SamplesPerRow = INT32_MAX; synthState->NumVoices = 1; synthState->Synth.Voices[0].Note = 64; retval = su_render_samples(synthState, su_max_samples / 2, buffer); diff --git a/tests/test_render_samples_api.c b/tests/test_render_samples_api.c new file mode 100644 index 0000000..6e3cad6 --- /dev/null +++ b/tests/test_render_samples_api.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include "../include/sointu.h" + +#define BPM 100 +#define SAMPLE_RATE 44100 +#define TOTAL_ROWS 16 +#define SAMPLES_PER_ROW SAMPLE_RATE * 4 * 60 / (BPM * 16) +const int su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS; + +int main(int argc, char* argv[]) { + SynthState* synthState; + float* buffer; + const unsigned char commands[] = { su_envelope_id, // MONO + su_envelope_id, // MONO + su_out_id + 1, // STEREO + 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; + int retval; + synthState = (SynthState*)malloc(sizeof(SynthState)); + buffer = (float*)malloc(2 * sizeof(float) * su_max_samples); + memset(synthState, 0, sizeof(SynthState)); + memcpy(synthState->Commands, commands, sizeof(commands)); + 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) + { + printf("su_render_samples rendered samples despite number of samples per row being 0"); + 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) + { + printf("su_render_samples should have return -1, as it should have believed buffer is full"); + goto fail; + } + if (synthState->RowTick != 1) + { + printf("su_render_samples RowTick should be at 1 after rendering 1 tick of a row"); + 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) + { + printf("su_render_samples did not render SAMPLES_PER_ROW, despite rowLen being SAMPLES_PER_ROW"); + 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++; + } + 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) + { + printf("su_render_samples should have ran out of buffer and thus return -1"); + goto fail; + } + retval = 0; +finish: + free(synthState); + free(buffer); + return retval; +fail: + retval = 1; + goto finish; +}