sointu/vm/compiler/bridge/bridge.go
5684185+vsariola@users.noreply.github.com 1ac2ad3c75 fix(vm/compiler): invert the logic of the release flag in the voices (closes #102)
This makes all envelopes released by default, instead of attacking. Add also test to demonstrate the buggy behaviour.
2023-09-23 15:56:46 +03:00

188 lines
6.4 KiB
Go

package bridge
// #cgo CFLAGS: -I"${SRCDIR}/../../../build/"
// #cgo LDFLAGS: "${SRCDIR}/../../../build/libsointu.a"
// #include <sointu.h>
import "C"
import (
"errors"
"fmt"
"strings"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
)
type BridgeService struct {
}
type BridgeSynth C.Synth
func (s BridgeService) Compile(patch sointu.Patch, bpm int) (sointu.Synth, error) {
synth, err := Synth(patch, bpm)
return synth, err
}
func Synth(patch sointu.Patch, bpm int) (*BridgeSynth, error) {
s := new(C.Synth)
if n := patch.NumDelayLines(); n > 64 {
return nil, fmt.Errorf("native bridge has currently a hard limit of 64 delaylines; patch uses %v", n)
}
comPatch, err := vm.Encode(patch, vm.AllFeatures{}, bpm)
if err != nil {
return nil, fmt.Errorf("error compiling patch: %v", err)
}
if len(comPatch.Commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
return nil, errors.New("bridge supports at most 2048 commands; the compiled patch has more")
}
if len(comPatch.Values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return nil, errors.New("bridge supports at most 16384 values; the compiled patch has more")
}
for i, v := range comPatch.Commands {
s.Commands[i] = (C.uchar)(v)
}
for i, v := range comPatch.Values {
s.Values[i] = (C.uchar)(v)
}
for i, v := range comPatch.DelayTimes {
s.DelayTimes[i] = (C.ushort)(v)
}
for i, v := range comPatch.SampleOffsets {
s.SampleOffsets[i].Start = (C.uint)(v.Start)
s.SampleOffsets[i].LoopStart = (C.ushort)(v.LoopStart)
s.SampleOffsets[i].LoopLength = (C.ushort)(v.LoopLength)
}
s.NumVoices = C.uint(comPatch.NumVoices)
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
s.RandSeed = 1
return (*BridgeSynth)(s), nil
}
// 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.
//
// 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
//
// 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 (bridgesynth *BridgeSynth) Render(buffer []float32, maxtime int) (int, int, error) {
synth := (*C.Synth)(bridgesynth)
// 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, 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), int(time), &RenderError{errcode: errcode}
}
return int(samples), int(time), nil
}
// Trigger is part of C.Synths' implementation of sointu.Synth interface
func (bridgesynth *BridgeSynth) Trigger(voice int, note byte) {
s := (*C.Synth)(bridgesynth)
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
return
}
s.SynthWrk.Voices[voice] = C.Voice{}
s.SynthWrk.Voices[voice].Note = C.int(note)
s.SynthWrk.Voices[voice].Sustain = 1
}
// Release is part of C.Synths' implementation of sointu.Synth interface
func (bridgesynth *BridgeSynth) Release(voice int) {
s := (*C.Synth)(bridgesynth)
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
return
}
s.SynthWrk.Voices[voice].Sustain = 0
}
// Update
func (bridgesynth *BridgeSynth) Update(patch sointu.Patch, bpm int) error {
s := (*C.Synth)(bridgesynth)
if n := patch.NumDelayLines(); n > 64 {
return fmt.Errorf("native bridge has currently a hard limit of 64 delaylines; patch uses %v", n)
}
comPatch, err := vm.Encode(patch, vm.AllFeatures{}, bpm)
if err != nil {
return fmt.Errorf("error compiling patch: %v", err)
}
if len(comPatch.Commands) > 2048 { // TODO: 2048 could probably be pulled automatically from cgo
return errors.New("bridge supports at most 2048 commands; the compiled patch has more")
}
if len(comPatch.Values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return errors.New("bridge supports at most 16384 values; the compiled patch has more")
}
needsRefresh := false
for i, v := range comPatch.Commands {
if cmdChar := (C.uchar)(v); s.Commands[i] != cmdChar {
s.Commands[i] = cmdChar
needsRefresh = true // if any of the commands change, we retrigger all units
}
}
for i, v := range comPatch.Values {
s.Values[i] = (C.uchar)(v)
}
for i, v := range comPatch.DelayTimes {
s.DelayTimes[i] = (C.ushort)(v)
}
for i, v := range comPatch.SampleOffsets {
s.SampleOffsets[i].Start = (C.uint)(v.Start)
s.SampleOffsets[i].LoopStart = (C.ushort)(v.LoopStart)
s.SampleOffsets[i].LoopLength = (C.ushort)(v.LoopLength)
}
s.NumVoices = C.uint(comPatch.NumVoices)
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
if needsRefresh {
for i := range s.SynthWrk.Voices {
// if any of the commands change, we retrigger all units
// note that we don't change the notes or release states, just the units
for j := range s.SynthWrk.Voices[i].Units {
s.SynthWrk.Voices[i].Units[j] = C.Unit{}
}
}
}
return nil
}
// Render error stores the exact errorcode, which is actually just the x87 FPU flags,
// with only the critical failure flags masked. Useful if you are interested exactly
// what went wrong with the patch.
type RenderError struct {
errcode int
}
func (e *RenderError) Error() string {
var reasons []string
if e.errcode&0x40 != 0 {
reasons = append(reasons, "FPU stack over/underflow")
}
if e.errcode&0x04 != 0 {
reasons = append(reasons, "FPU divide by zero")
}
if e.errcode&0x01 != 0 {
reasons = append(reasons, "FPU invalid operation")
}
if e.errcode&0x3800 != 0 {
reasons = append(reasons, "FPU stack push/pops are not balanced")
}
return "RenderError: " + strings.Join(reasons, ", ")
}