feat!: implement vsti, along with various refactorings and api changes for it

The RPC and sync library mechanisms were removed for now; they never really worked and contained several obvious bugs. Need to consider if syncs are useful at all during the compose time, or just used during intro.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2023-05-09 11:24:49 +03:00
parent 70080c2b9d
commit cd700ed954
34 changed files with 1210 additions and 750 deletions

View File

@ -59,32 +59,36 @@ func Synth(patch sointu.Patch) (*C.Synth, error) {
// Render renders until the buffer is full or the modulated time is reached, whichever
// happens first.
// Parameters:
// 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 depends on the modulation and if
// buffer is full before maxtime is reached.
//
// 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 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
// error potential error
//
// samples number of samples rendered in the buffer
// time how much the time advanced
// error potential error
//
// In practice, if nsamples = len(buffer)/2, then time <= maxtime. If maxtime was reached
// first, then nsamples <= len(buffer)/2 and time >= maxtime. Note that it could happen that
// time > maxtime, as it is modulated and the time could advance by 2 or more, so the loop
// exit condition would fire when the time is already past maxtime.
// Under no conditions, nsamples >= len(buffer)/2 i.e. guaranteed to never overwrite the buffer.
func (synth *C.Synth) Render(buffer []float32, syncBuffer []float32, maxtime int) (int, int, int, error) {
func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) {
// TODO: syncBuffer is not getting passed to cgo; do we want to even try to support the syncing with the native bridge
if len(buffer)%1 == 1 {
return -1, -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length")
return -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length")
}
samples := C.int(len(buffer) / 2)
time := C.int(maxtime)
errcode := int(C.su_render(synth, (*C.float)(&buffer[0]), &samples, &time))
if errcode > 0 {
return int(samples), 0, int(time), &RenderError{errcode: errcode}
return int(samples), int(time), &RenderError{errcode: errcode}
}
return int(samples), 0, int(time), nil
return int(samples), int(time), nil
}
// Trigger is part of C.Synths' implementation of sointu.Synth interface

View File

@ -40,7 +40,7 @@ func TestOscillatSine(t *testing.T) {
}}}
tracks := []sointu.Track{{NumVoices: 1, Order: []int{0}, Patterns: []sointu.Pattern{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}}}
song := sointu.Song{BPM: 100, RowsPerBeat: 4, Score: sointu.Score{RowsPerPattern: 16, Length: 1, Tracks: tracks}, Patch: patch}
buffer, _, err := sointu.Play(bridge.BridgeService{}, song, false)
buffer, err := sointu.Play(bridge.BridgeService{}, song, false)
if err != nil {
t.Fatalf("Render failed: %v", err)
}
@ -95,7 +95,7 @@ func TestAllRegressionTests(t *testing.T) {
if err != nil {
t.Fatalf("could not parse the .yml file: %v", err)
}
buffer, _, err := sointu.Play(bridge.BridgeService{}, song, false)
buffer, err := sointu.Play(bridge.BridgeService{}, song, false)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)

View File

@ -115,7 +115,7 @@ func (s *Interpreter) Update(patch sointu.Patch) error {
return nil
}
func (s *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (samples int, syncs int, time int, renderError error) {
func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time int, renderError error) {
defer func() {
if err := recover(); err != nil {
renderError = fmt.Errorf("render panicced: %v", err)
@ -133,10 +133,6 @@ func (s *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (
voicesRemaining := s.bytePatch.NumVoices
voices := s.synth.voices[:]
units := voices[0].units[:]
if byte(s.synth.globalTime) == 0 { // every 256 samples
syncBuf[0], syncBuf = float32(time), syncBuf[1:]
syncs++
}
for voicesRemaining > 0 {
op := commands[0]
commands = commands[1:]
@ -156,7 +152,7 @@ func (s *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (
}
tcount := transformCounts[opNoStereo-1]
if len(values) < tcount {
return samples, syncs, time, errors.New("value stream ended prematurely")
return samples, time, errors.New("value stream ended prematurely")
}
voice := &voices[0]
unit := &units[0]
@ -527,19 +523,17 @@ func (s *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (
stack = append(stack, gain)
}
case opSync:
if byte(s.synth.globalTime) == 0 { // every 256 samples
syncBuf[0], syncBuf = float32(stack[l-1]), syncBuf[1:]
}
break
default:
return samples, syncs, time, errors.New("invalid / unimplemented opcode")
return samples, time, errors.New("invalid / unimplemented opcode")
}
units = units[1:]
}
if len(stack) < 4 {
return samples, syncs, time, errors.New("stack underflow")
return samples, time, errors.New("stack underflow")
}
if len(stack) > 4 {
return samples, syncs, time, errors.New("stack not empty")
return samples, time, errors.New("stack not empty")
}
buffer[0] = synth.outputs[0]
buffer[1] = synth.outputs[1]
@ -551,7 +545,7 @@ func (s *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (
s.synth.globalTime++
}
s.stack = stack[:0]
return samples, syncs, time, nil
return samples, time, nil
}
func (s *synth) rand() float32 {

View File

@ -41,7 +41,7 @@ func TestAllRegressionTests(t *testing.T) {
if err != nil {
t.Fatalf("could not parse the .yml file: %v", err)
}
buffer, syncBuffer, err := sointu.Play(vm.SynthService{}, song, false)
buffer, err := sointu.Play(vm.SynthService{}, song, false)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
@ -68,9 +68,6 @@ func TestAllRegressionTests(t *testing.T) {
}
}
compareToRawFloat32(t, buffer, testname+".raw")
if strings.Contains(testname, "sync") {
compareToRawFloat32(t, syncBuffer, testname+"_syncbuf.raw")
}
})
}
}