diff --git a/.gitignore b/.gitignore index cbbbb0a..e80d76c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,9 @@ build/ old/ # VS Code -.vscode/ \ No newline at end of file +.vscode/ + +# project specific +# this is autogenerated from bridge.go.in +bridge/bridge.go + diff --git a/BindConfig.txt b/BindConfig.txt new file mode 100644 index 0000000..894a3bd --- /dev/null +++ b/BindConfig.txt @@ -0,0 +1,20 @@ +# Copyright 2012 Douglas Linder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required (VERSION 2.8) +SET(HERE "${CMAKE_ARGV3}") +SET(INCLUDE_PATH "${CMAKE_ARGV4}") +SET(LIBRARY_PATH "${CMAKE_ARGV5}") +message("${LIBRARY_PATH} ${INCLUDE_PATH}") +configure_file(${HERE}/bridge/bridge.go.in ${HERE}/bridge/bridge.go) \ No newline at end of file diff --git a/bridge/bridge.go.in b/bridge/bridge.go.in new file mode 100644 index 0000000..bf79edf --- /dev/null +++ b/bridge/bridge.go.in @@ -0,0 +1,55 @@ +package bridge + +import "fmt" +import "unsafe" + +// #cgo CFLAGS: -I${INCLUDE_PATH} +// #cgo LDFLAGS: ${LIBRARY_PATH} +// #include +import "C" + +type SynthState = C.SynthState + +func (s *SynthState) Render(buffer []float32) int { + fmt.Printf("Calling Render...\n") + var ret = C.su_render_samples(s, C.int(len(buffer))/2, (*C.float)(&buffer[0])) + fmt.Printf("Returning from Render...\n") + return int(ret) +} + +func (s *SynthState) SetCommands(c [2048]byte) { + pk := *((*[2048]C.uchar)(unsafe.Pointer(&c))) + s.Commands = pk +} + +func (s *SynthState) SetValues(c [16384]byte) { + pk := *((*[16384]C.uchar)(unsafe.Pointer(&c))) + s.Values = pk +} + +func (s *SynthState) Trigger(voice int,note int) { + fmt.Printf("Calling Trigger...\n") + s.Synth.Voices[voice] = C.Voice{} + s.Synth.Voices[voice].Note = C.int(note) + fmt.Printf("Returning from Trigger...\n") +} + +func (s *SynthState) Release(voice int) { + fmt.Printf("Calling Release...\n") + s.Synth.Voices[voice].Release = 1 + 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 + return s +} diff --git a/bridge/bridge_test.go b/bridge/bridge_test.go new file mode 100644 index 0000000..13b30a9 --- /dev/null +++ b/bridge/bridge_test.go @@ -0,0 +1,77 @@ +package bridge_test + +import ( + "bytes" + "encoding/binary" + "github.com/vsariola/sointu/bridge" + "io/ioutil" + "math" + "path" + "runtime" + "testing" +) + +const BPM = 100 +const SAMPLE_RATE = 44100 +const TOTAL_ROWS = 16 +const SAMPLES_PER_ROW = SAMPLE_RATE * 4 * 60 / (BPM * 16) + +const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS + +// const bufsize = su_max_samples * 2 + +func TestBridge(t *testing.T) { + commands := [2048]byte{ + 2, 2, 11, 0, // envelope mono, envelope mono, out stereo, advance + // TODO: pull these somehow from the C-side + } + values := [16384]byte{64, 64, 64, 80, 128, // envelope 1 + 95, 64, 64, 80, 128, // envelope 2 + 128} + s := bridge.NewSynthState() + // memcpy(synthState->Commands, commands, sizeof(commands)); + s.SetCommands(commands) + // memcpy(synthState->Values, values, sizeof(values)); + s.SetValues(values) + // 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; + s.Synth.Voices[0].Note = 64 + // 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)) + } + // 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)) + } + buffer = append(buffer, sbuffer...) + _, filename, _, _ := runtime.Caller(0) + expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", "test_render_samples.raw")) + if err != nil { + t.Fatalf("cannot read expected: %v", err) + } + var createdbuf bytes.Buffer + err = binary.Write(&createdbuf, binary.LittleEndian, buffer) + if err != nil { + t.Fatalf("error converting buffer: %v", err) + } + createdb := createdbuf.Bytes() + if len(createdb) != len(expectedb) { + t.Fatalf("buffer length mismatch, got %v, expected %v", len(createdb), len(expectedb)) + } + for i, v := range createdb { + if expectedb[i] != v { + t.Errorf("byte mismatch @ %v, got %v, expected %v", i, v, expectedb[i]) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bb4f14c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/vsariola/sointu + +go 1.15 diff --git a/include/sointu.h b/include/sointu.h new file mode 100644 index 0000000..1ba22e1 --- /dev/null +++ b/include/sointu.h @@ -0,0 +1,119 @@ +#ifndef _SOINTU_H +#define _SOINTU_H + +#pragma pack(push,4) // 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 Synth { + unsigned char Curvoices[32]; + float Left; + float Right; + float Aux[6]; + struct Voice Voices[32]; +} Synth; + +typedef struct DelayWorkspace { + float Buffer[65536]; + float Dcin; + float Dcout; + float Filtstate; +} DelayWorkspace; + +typedef struct SynthState { + struct Synth Synth; + 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 char Commands[32 * 64]; + unsigned char Values[32 * 64 * 8]; + unsigned int Polyphony; + unsigned int NumVoices; + unsigned int RandSeed; + unsigned int Globaltime; + unsigned int RowTick; + unsigned int RowLen; +} SynthState; +#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 + +#ifdef INCLUDE_GMDLS +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. +// +// NOTE: The buffer should have a length of 2 * maxsamples, as the audio +// is stereo. +// +// 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. +// +// 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); + +// Arithmetic opcode ids +extern const int su_add_id; +extern const int su_addp_id; +extern const int su_pop_id; +extern const int su_loadnote_id; +extern const int su_mul_id; +extern const int su_mulp_id; +extern const int su_push_id; +extern const int su_xch_id; + +// Effect opcode ids +extern const int su_distort_id; +extern const int su_hold_id; +extern const int su_crush_id; +extern const int su_gain_id; +extern const int su_invgain_id; +extern const int su_filter_id; +extern const int su_clip_id; +extern const int su_pan_id; +extern const int su_delay_id; +extern const int su_compres_id; + +// Flowcontrol opcode ids +extern const int su_advance_id; +extern const int su_speed_id; + +// Sink opcode ids +extern const int su_out_id; +extern const int su_outaux_id; +extern const int su_aux_id; +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; +extern const int su_in_id; + +#endif // _SOINTU_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e69de29..c1145db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -0,0 +1,18 @@ +set(LIB sointu) + +set(SOURCES sointu.asm) + +# Headers +include_directories(${PROJECT_SOURCE_DIR}/include) + +# Library target +add_library(${LIB} ${SOURCES}) +set_target_properties(${LIB} PROPERTIES LINKER_LANGUAGE C) +target_link_libraries(${LIB}) +target_compile_definitions(${LIB} PUBLIC SU_USE_INTROSPECTION) + +# Generate cgo wrapper +add_custom_command(TARGET ${LIB} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -P ${PROJECT_SOURCE_DIR}/BindConfig.txt ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/include $ +) diff --git a/src/introspection_footer.inc b/src/introspection_footer.inc index a7a17ff..9167c0f 100644 --- a/src/introspection_footer.inc +++ b/src/introspection_footer.inc @@ -3,14 +3,91 @@ ; Various compile time definitions exported SECT_DATA(introscn) + %ifdef SU_USE_16BIT_OUTPUT - EXPORT MANGLE_DATA(su_use_16bit_output) dd 1 + EXPORT MANGLE_DATA(su_use_16bit_output) + dd 1 %else - EXPORT MANGLE_DATA(su_use_16bit_output) dd 0 + EXPORT MANGLE_DATA(su_use_16bit_output) + dd 0 %endif -%ifdef MAX_SAMPLES - EXPORT MANGLE_DATA(su_max_samples) dd MAX_SAMPLES +%ifndef SU_DISABLE_PLAYER + %ifdef MAX_SAMPLES + EXPORT MANGLE_DATA(su_max_samples) + dd MAX_SAMPLES + %endif %endif +; 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 + +; 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 + %endif ; SU_USE_INTROSPECTION diff --git a/src/sointu.asm b/src/sointu.asm new file mode 100644 index 0000000..f96a18c --- /dev/null +++ b/src/sointu.asm @@ -0,0 +1,121 @@ +; source file for compiling sointu as a library +%define SU_DISABLE_PLAYER + +%include "sointu_header.inc" + +; TODO: make sure compile everything in + +USE_ENVELOPE +USE_OSCILLAT +USE_MULP +USE_PAN +USE_OUT + +%define INCLUDE_TRISAW +%define INCLUDE_SINE +%define INCLUDE_PULSE +%define INCLUDE_GATE +%define INCLUDE_STEREO_OSCILLAT +%define INCLUDE_STEREO_ENVELOPE +%define INCLUDE_STEREO_OUT +%define INCLUDE_POLYPHONY +%define INCLUDE_MULTIVOICE_TRACKS + +%include "sointu_footer.inc" + +section .text + +struc su_synth_state + .synth resb su_synth.size + .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 + .globaltime resd 1 + .rowtick resd 1 + .rowlen resd 1 +endstruc + +SECT_TEXT(sursampl) + +EXPORT MANGLE_FUNC(su_render_samples,12) +%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 +%else + %ifidn __OUTPUT_FORMAT__,win64 ; win64 ABI: rdx = bufsize, r8 = &buffer, rcx = &synthstate + 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 + push_registers rbx, rbp ; System V ABI: these registers are non-volatile + mov rcx, rdi ; rcx = &Synthstate + %endif +%endif + 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] +su_render_samples_loop: + mov _CX, [_SP + PTRSIZE*3] + push _AX ; push rowtick + mov eax, [_CX + su_synth_state.polyphony] + push _AX ;polyphony + mov eax, [_CX + su_synth_state.numvoices] + push _AX ;numvoices + lea _DX, [_CX+ su_synth_state.synth] + lea COM, [_CX+ su_synth_state.commands] + lea VAL, [_CX+ su_synth_state.values] + lea WRK, [_DX + su_synth.voices] + lea _CX, [_CX+ su_synth_state.delaywrks - su_delayline_wrk.filtstate] + call MANGLE_FUNC(su_run_vm,0) + pop _AX + pop _AX + mov _DI, [_SP + PTRSIZE*5] ; edi containts buffer ptr + mov _CX, [_SP + PTRSIZE*4] + lea _SI, [_CX + su_synth_state.synth + su_synth.left] + movsd ; copy left channel to output buffer + movsd ; copy right channel to output buffer + mov [_SP + PTRSIZE*5], _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 + 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: + 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 ; todo: return correct value based on this +%if BITS == 32 ; stdcall + popad + ret 12 +%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 + ret +%endif diff --git a/src/sointu_footer.inc b/src/sointu_footer.inc index 0a4d45a..e0be171 100644 --- a/src/sointu_footer.inc +++ b/src/sointu_footer.inc @@ -274,6 +274,8 @@ EXPORT MANGLE_FUNC(su_power,0) fstp st1 ; 2^x ret +%ifndef SU_DISABLE_PLAYER + ;------------------------------------------------------------------------------- ; output_sound macro: used by the render function to write sound to buffer ;------------------------------------------------------------------------------- @@ -482,6 +484,8 @@ su_update_voices_skipadd: %endif ;INCLUDE_MULTIVOICE_TRACKS +%endif ; SU_DISABLE_PLAYER + ;------------------------------------------------------------------------------- ; Include the rest of the code ;------------------------------------------------------------------------------- diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a7fcc0b..a469fc4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,11 +1,11 @@ function(regression_test testname) if(${ARGC} LESS 4) - set(source ${testname}) + set(source ${testname}.asm) else() set(source ${ARGV3}) endif() - add_executable(${testname} ${source}.asm test_renderer.c) + add_executable(${testname} ${source} test_renderer.c) # the tests include the entire ASM but we still want to rebuild when they change file(GLOB SOINTU ${PROJECT_SOURCE_DIR}/src/*.inc @@ -143,9 +143,12 @@ regression_test(test_delay_drymod "ENVELOPE;FOP_MULP;PANNING;VCO_SINE;SEND") regression_test(test_delay_flanger "ENVELOPE;FOP_MULP;PANNING;VCO_SINE;SEND") regression_test(test_envelope_mod "VCO_SINE;ENVELOPE;SEND") -regression_test(test_envelope_16bit ENVELOPE "" test_envelope) +regression_test(test_envelope_16bit ENVELOPE "" test_envelope.asm) target_compile_definitions(test_envelope_16bit PUBLIC SU_USE_16BIT_OUTPUT) regression_test(test_polyphony "ENVELOPE;VCO_SINE") 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 diff --git a/tests/expected_output/test_render_samples.raw b/tests/expected_output/test_render_samples.raw new file mode 100644 index 0000000..59c0e67 Binary files /dev/null and b/tests/expected_output/test_render_samples.raw differ diff --git a/tests/test_render_samples.c b/tests/test_render_samples.c new file mode 100644 index 0000000..6499e64 --- /dev/null +++ b/tests/test_render_samples.c @@ -0,0 +1,45 @@ +#include +#include +#include "../include/sointu.h" + +#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 + +#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; + +void CALLCONV su_render(float* buffer) { + SynthState* synthState; + 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 retval; + synthState = malloc(sizeof(SynthState)); + memset(synthState, 0, sizeof(SynthState)); + memcpy(synthState->Commands, commands, sizeof(commands)); + memcpy(synthState->Values, values, sizeof(values)); + synthState->RandSeed = 1; + synthState->RowLen = INT32_MAX; + synthState->NumVoices = 1; + synthState->Synth.Voices[0].Note = 64; + retval = su_render_samples(synthState, su_max_samples / 2, buffer); + synthState->Synth.Voices[0].Release++; + buffer = buffer + su_max_samples; + retval = su_render_samples(synthState, su_max_samples / 2, buffer); + free(synthState); + return; +}