mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-25 18:00:37 -04:00
This makes all envelopes released by default, instead of attacking. Add also test to demonstrate the buggy behaviour.
188 lines
6.4 KiB
Go
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, ", ")
|
|
}
|