From aa133b46065765fea9e1fa6ce6b80a10ddced60e Mon Sep 17 00:00:00 2001 From: Veikko Sariola Date: Sat, 24 Oct 2020 16:15:15 +0300 Subject: [PATCH] Change bridge.go so that there is just SetPatch(...) function, instead of having to SetCommands, SetValues etc. Now the patch definition in bridge_test.go and test_envelope.asm appear quite similar, so it's clear how they are related. --- bridge/bridge.go | 202 +++++++++++++++++++++++++----------------- bridge/bridge_test.go | 26 +++--- 2 files changed, 135 insertions(+), 93 deletions(-) diff --git a/bridge/bridge.go b/bridge/bridge.go index 423cf99..f1cecc2 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -1,58 +1,74 @@ package bridge -import "fmt" -import "unsafe" -import "math" +import ( + "errors" + "math" +) // #cgo CFLAGS: -I"${SRCDIR}/../include" // #cgo LDFLAGS: "${SRCDIR}/../build/src/libsointu.a" // #include import "C" -import "errors" -type SynthState = C.SynthState +// SynthState contains the entire state of sointu sound engine +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 type Opcode byte -var ( // cannot be const as the rhs are not known at compile-time - Add = Opcode(C.su_add_id) - Addp = Opcode(C.su_addp_id) - Pop = Opcode(C.su_pop_id) - Loadnote = Opcode(C.su_loadnote_id) - Mul = Opcode(C.su_mul_id) - Mulp = Opcode(C.su_mulp_id) - Push = Opcode(C.su_push_id) - Xch = Opcode(C.su_xch_id) - Distort = Opcode(C.su_distort_id) - Hold = Opcode(C.su_hold_id) - Crush = Opcode(C.su_crush_id) - Gain = Opcode(C.su_gain_id) - Invgain = Opcode(C.su_invgain_id) - Filter = Opcode(C.su_filter_id) - Clip = Opcode(C.su_clip_id) - Pan = Opcode(C.su_pan_id) - Delay = Opcode(C.su_delay_id) - Compres = Opcode(C.su_compres_id) - Advance = Opcode(C.su_advance_id) - Speed = Opcode(C.su_speed_id) - Out = Opcode(C.su_out_id) - Outaux = Opcode(C.su_outaux_id) - Aux = Opcode(C.su_aux_id) - Send = Opcode(C.su_send_id) - Envelope = Opcode(C.su_envelope_id) - Noise = Opcode(C.su_noise_id) - Oscillat = Opcode(C.su_oscillat_id) - Loadval = Opcode(C.su_loadval_id) - Receive = Opcode(C.su_receive_id) - In = Opcode(C.su_in_id) -) - -func (o Opcode) Stereo() Opcode { - return Opcode(byte(o) | 1) // set lowest bit +// Unit includes command (filter, oscillator, envelope etc.) and its parameters +type Unit struct { + Command Opcode + Params []byte } +// Instrument includes a list of units consisting of the instrument, and the number of polyphonic voices for this instrument +type Instrument struct { + NumVoices int + Units []Unit +} + +var ( // cannot be const as the rhs are not known at compile-time + Add = Opcode(C.su_add_id) + Addp = Opcode(C.su_addp_id) + Pop = Opcode(C.su_pop_id) + Loadnote = Opcode(C.su_loadnote_id) + Mul = Opcode(C.su_mul_id) + Mulp = Opcode(C.su_mulp_id) + Push = Opcode(C.su_push_id) + Xch = Opcode(C.su_xch_id) + Distort = Opcode(C.su_distort_id) + Hold = Opcode(C.su_hold_id) + Crush = Opcode(C.su_crush_id) + Gain = Opcode(C.su_gain_id) + Invgain = Opcode(C.su_invgain_id) + Filter = Opcode(C.su_filter_id) + Clip = Opcode(C.su_clip_id) + Pan = Opcode(C.su_pan_id) + Delay = Opcode(C.su_delay_id) + Compres = Opcode(C.su_compres_id) + Advance = Opcode(C.su_advance_id) + Speed = Opcode(C.su_speed_id) + Out = Opcode(C.su_out_id) + Outaux = Opcode(C.su_outaux_id) + Aux = Opcode(C.su_aux_id) + Send = Opcode(C.su_send_id) + Envelope = Opcode(C.su_envelope_id) + Noise = Opcode(C.su_noise_id) + Oscillat = Opcode(C.su_oscillat_id) + Loadval = Opcode(C.su_loadval_id) + Receive = Opcode(C.su_receive_id) + In = Opcode(C.su_in_id) +) + +// Stereo returns the stereo version of any (mono or stereo) opcode +func (o Opcode) Stereo() Opcode { + return Opcode(byte(o) | 1) // set lowest bit +} + +// Mono returns the mono version of any (mono or stereo) opcode func (o Opcode) Mono() Opcode { - return Opcode(byte(o) & 0xFE) // clear lowest bit + return Opcode(byte(o) & 0xFE) // clear lowest bit } // Render tries to fill the buffer with samples rendered by Sointu. @@ -66,53 +82,79 @@ func (o Opcode) Mono() Opcode { // callback called every time a row advances. Won't get called if you have // not set SamplesPerRow explicitly. // Returns the number samples rendered, len(buffer)/2 in the typical case where buffer was filled -func (s *SynthState) Render(buffer []float32,maxRows int,callback func()) (int, error) { - if len(buffer) % 1 == 1 { - return -1, errors.New("Render writes stereo signals, so buffer should have even length") - } - maxSamples := len(buffer) / 2 - remaining := maxSamples - for i := 0; i < maxRows; i++ { - remaining = int(C.su_render_samples(s,C.int(remaining),(*C.float)(&buffer[2*(maxSamples-remaining)]))) - if (remaining >= 0) { // values >= 0 mean that row end was reached - callback() - } - if (remaining <= 0) { // values <= 0 mean that buffer is full, ready to return - break; - } - } - return maxSamples - remaining, nil +func (s *SynthState) Render(buffer []float32, maxRows int, callback func()) (int, error) { + if len(buffer)%1 == 1 { + return -1, errors.New("Render writes stereo signals, so buffer should have even length") + } + maxSamples := len(buffer) / 2 + remaining := maxSamples + for i := 0; i < maxRows; i++ { + remaining = int(C.su_render_samples((*C.SynthState)(s), C.int(remaining), (*C.float)(&buffer[2*(maxSamples-remaining)]))) + if remaining >= 0 { // values >= 0 mean that row end was reached + callback() + } + if remaining <= 0 { // values <= 0 mean that buffer is full, ready to return + break + } + } + return maxSamples - remaining, nil } -func (s *SynthState) SetCommands(c [2048]Opcode) { - pk := *((*[2048]C.uchar)(unsafe.Pointer(&c))) - s.Commands = pk +func (s *SynthState) SetPatch(patch []Instrument) error { + totalVoices := 0 + commands := make([]Opcode, 0) + values := make([]byte, 0) + for _, instr := range patch { + if len(instr.Units) > 63 { + return errors.New("An instrument can have a maximum of 63 units") + } + if instr.NumVoices < 1 { + return errors.New("Each instrument must have at least 1 voice") + } + for _, unit := range instr.Units { + commands = append(commands, unit.Command) + values = append(values, unit.Params...) + } + commands = append(commands, Advance) + totalVoices += instr.NumVoices + } + if totalVoices > 32 { + return errors.New("Sointu does not support more than 32 concurrent voices") + } + 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") + } + 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") + } + cs := (*C.SynthState)(s) + for i := range commands { + cs.Commands[i] = (C.uchar)(commands[i]) + } + for i := range values { + cs.Values[i] = (C.uchar)(values[i]) + } + cs.NumVoices = C.uint(totalVoices) + return nil } -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) Trigger(voice int, note int) { + cs := (*C.SynthState)(s) + cs.Synth.Voices[voice] = C.Voice{} + cs.Synth.Voices[voice].Note = C.int(note) } func (s *SynthState) Release(voice int) { - fmt.Printf("Calling Release...\n") - s.Synth.Voices[voice].Release = 1 - fmt.Printf("Returning from Release...\n") + cs := (*C.SynthState)(s) + cs.Synth.Voices[voice].Release = 1 } 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 + 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 8126e27..ff2b605 100644 --- a/bridge/bridge_test.go +++ b/bridge/bridge_test.go @@ -3,11 +3,12 @@ package bridge_test import ( "bytes" "encoding/binary" - "github.com/vsariola/sointu/bridge" "io/ioutil" "path" "runtime" "testing" + + "github.com/vsariola/sointu/bridge" ) const BPM = 100 @@ -20,20 +21,19 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS // const bufsize = su_max_samples * 2 func TestBridge(t *testing.T) { - commands := [2048]bridge.Opcode{ - bridge.Envelope, bridge.Envelope, bridge.Out.Stereo(), bridge.Advance} - values := [16384]byte{64, 64, 64, 80, 128, // envelope 1 - 95, 64, 64, 80, 128, // envelope 2 - 128} s := bridge.NewSynthState() - s.SetCommands(commands) - s.SetValues(values) - s.NumVoices = 1 - s.Synth.Voices[0].Note = 64 - s.SamplesPerRow = SAMPLES_PER_ROW * 8 // this song is two blocks of 8 rows, release during second + s.SetPatch([]bridge.Instrument{ + bridge.Instrument{1, []bridge.Unit{ + bridge.Unit{bridge.Envelope, []byte{64, 64, 64, 80, 128}}, + bridge.Unit{bridge.Envelope, []byte{95, 64, 64, 80, 128}}, + bridge.Unit{bridge.Out.Stereo(), []byte{128}}, + }}, + }) + s.Trigger(0, 64) + s.SamplesPerRow = SAMPLES_PER_ROW * 8 // this song is two blocks of 8 rows, release before second block start buffer := make([]float32, 2*su_max_samples) - n,err := s.Render(buffer,2,func() { - s.Synth.Voices[0].Release = 1 + n, err := s.Render(buffer, 2, func() { + s.Release(0) }) if n < su_max_samples { t.Fatalf("could not fill the whole buffer, %v samples rendered, %v expected", n, su_max_samples)