mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
The -er suffix is more idiomatic for single method interfaces, and the interface is not doing much more than converting the patch to a synth. Names were updated throughout the project to reflect this change. In particular, the "Service" in SynthService was not telling anything helpful.
188 lines
6.5 KiB
Go
188 lines
6.5 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 NativeSynther struct {
|
|
}
|
|
|
|
type NativeSynth C.Synth
|
|
|
|
func (s NativeSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
|
synth, err := Synth(patch, bpm)
|
|
return synth, err
|
|
}
|
|
|
|
func Synth(patch sointu.Patch, bpm int) (*NativeSynth, 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 (*NativeSynth)(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 *NativeSynth) Render(buffer sointu.AudioBuffer, 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))
|
|
time := C.int(maxtime)
|
|
errcode := int(C.su_render(synth, (*C.float)(&buffer[0][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 *NativeSynth) 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 *NativeSynth) 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 *NativeSynth) 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, ", ")
|
|
}
|