Implement a bridge to call Sointu from Go language.

The main interface is render_samples function, which renders several samples in one call,
to limit the number of calls from Go to C. This is compiled into a library, which is then
linked and called from bridge.go.
This commit is contained in:
Veikko Sariola 2020-10-21 20:07:45 +03:00 committed by Veikko Sariola
parent af14cd310b
commit 7aac3917b7
13 changed files with 555 additions and 8 deletions

7
.gitignore vendored
View File

@ -18,4 +18,9 @@ build/
old/
# VS Code
.vscode/
.vscode/
# project specific
# this is autogenerated from bridge.go.in
bridge/bridge.go

20
BindConfig.txt Normal file
View File

@ -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)

55
bridge/bridge.go.in Normal file
View File

@ -0,0 +1,55 @@
package bridge
import "fmt"
import "unsafe"
// #cgo CFLAGS: -I${INCLUDE_PATH}
// #cgo LDFLAGS: ${LIBRARY_PATH}
// #include <sointu.h>
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
}

77
bridge/bridge_test.go Normal file
View File

@ -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])
}
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/vsariola/sointu
go 1.15

119
include/sointu.h Normal file
View File

@ -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

View File

@ -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 $<TARGET_FILE:sointu>
)

View File

@ -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

121
src/sointu.asm Normal file
View File

@ -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

View File

@ -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
;-------------------------------------------------------------------------------

View File

@ -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)

Binary file not shown.

View File

@ -0,0 +1,45 @@
#include <stdint.h>
#include <stdlib.h>
#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;
}