mirror of
https://github.com/vsariola/sointu.git
synced 2025-11-12 12:52:53 -05:00
feat: add multithreaded rendering to the tracker side
The compiled player does not support multithreading, but with this, users can already start composing songs with slightly less powerful machines, even when targeting high-end machines. Related to #199
This commit is contained in:
parent
c583156d1b
commit
9b9dc3548f
@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
@ -16,9 +17,13 @@ import (
|
||||
type NativeSynther struct {
|
||||
}
|
||||
|
||||
type NativeSynth C.Synth
|
||||
type NativeSynth struct {
|
||||
csynth C.Synth
|
||||
cpuLoad sointu.CPULoad
|
||||
}
|
||||
|
||||
func (s NativeSynther) Name() string { return "Native" }
|
||||
func (s NativeSynther) Name() string { return "Native" }
|
||||
func (s NativeSynther) SupportsMultithreading() bool { return false }
|
||||
|
||||
func (s NativeSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
||||
synth, err := Synth(patch, bpm)
|
||||
@ -45,7 +50,7 @@ func Synth(patch sointu.Patch, bpm int) (*NativeSynth, error) {
|
||||
s.Opcodes[0] = 0
|
||||
s.NumVoices = 1
|
||||
s.Polyphony = 0
|
||||
return (*NativeSynth)(s), nil
|
||||
return &NativeSynth{csynth: *s}, nil
|
||||
}
|
||||
for i, v := range comPatch.Opcodes {
|
||||
s.Opcodes[i] = (C.uchar)(v)
|
||||
@ -64,7 +69,17 @@ func Synth(patch sointu.Patch, bpm int) (*NativeSynth, error) {
|
||||
s.NumVoices = C.uint(comPatch.NumVoices)
|
||||
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
|
||||
s.RandSeed = 1
|
||||
return (*NativeSynth)(s), nil
|
||||
return &NativeSynth{csynth: *s}, nil
|
||||
}
|
||||
|
||||
func (s *NativeSynth) Close() {}
|
||||
|
||||
func (s *NativeSynth) CPULoad(loads []sointu.CPULoad) int {
|
||||
if len(loads) < 1 {
|
||||
return 0
|
||||
}
|
||||
loads[0] = s.cpuLoad
|
||||
return 1
|
||||
}
|
||||
|
||||
// Render renders until the buffer is full or the modulated time is reached, whichever
|
||||
@ -89,12 +104,14 @@ func Synth(patch sointu.Patch, bpm int) (*NativeSynth, error) {
|
||||
// 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)
|
||||
synth := &bridgesynth.csynth
|
||||
// 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))
|
||||
startTime := time.Now()
|
||||
defer func() { bridgesynth.cpuLoad.Update(time.Since(startTime), int64(samples)) }()
|
||||
time := C.int(maxtime)
|
||||
errcode := int(C.su_render(synth, (*C.float)(&buffer[0][0]), &samples, &time))
|
||||
if errcode > 0 {
|
||||
@ -105,7 +122,7 @@ func (bridgesynth *NativeSynth) Render(buffer sointu.AudioBuffer, maxtime int) (
|
||||
|
||||
// Trigger is part of C.Synths' implementation of sointu.Synth interface
|
||||
func (bridgesynth *NativeSynth) Trigger(voice int, note byte) {
|
||||
s := (*C.Synth)(bridgesynth)
|
||||
s := &bridgesynth.csynth
|
||||
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
||||
return
|
||||
}
|
||||
@ -116,7 +133,7 @@ func (bridgesynth *NativeSynth) Trigger(voice int, note byte) {
|
||||
|
||||
// Release is part of C.Synths' implementation of sointu.Synth interface
|
||||
func (bridgesynth *NativeSynth) Release(voice int) {
|
||||
s := (*C.Synth)(bridgesynth)
|
||||
s := &bridgesynth.csynth
|
||||
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
||||
return
|
||||
}
|
||||
@ -125,7 +142,7 @@ func (bridgesynth *NativeSynth) Release(voice int) {
|
||||
|
||||
// Update
|
||||
func (bridgesynth *NativeSynth) Update(patch sointu.Patch, bpm int) error {
|
||||
s := (*C.Synth)(bridgesynth)
|
||||
s := &bridgesynth.csynth
|
||||
if n := patch.NumDelayLines(); n > 128 {
|
||||
return fmt.Errorf("native bridge has currently a hard limit of 128 delaylines; patch uses %v", n)
|
||||
}
|
||||
|
||||
@ -86,6 +86,7 @@ func TestRenderSamples(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
synth.Trigger(0, 64)
|
||||
buffer := make(sointu.AudioBuffer, su_max_samples)
|
||||
err = buffer[:len(buffer)/2].Fill(synth)
|
||||
@ -162,6 +163,7 @@ func TestStackUnderflow(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
buffer := make(sointu.AudioBuffer, 1)
|
||||
err = buffer.Fill(synth)
|
||||
if err == nil {
|
||||
@ -178,6 +180,7 @@ func TestStackBalancing(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
buffer := make(sointu.AudioBuffer, 1)
|
||||
err = buffer.Fill(synth)
|
||||
if err == nil {
|
||||
@ -211,6 +214,7 @@ func TestStackOverflow(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
buffer := make(sointu.AudioBuffer, 1)
|
||||
err = buffer.Fill(synth)
|
||||
if err == nil {
|
||||
@ -228,6 +232,7 @@ func TestDivideByZero(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
buffer := make(sointu.AudioBuffer, 1)
|
||||
err = buffer.Fill(synth)
|
||||
if err == nil {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
@ -27,6 +28,7 @@ type (
|
||||
stack []float32
|
||||
state synthState
|
||||
delaylines []delayline
|
||||
cpuLoad sointu.CPULoad
|
||||
}
|
||||
|
||||
// GoSynther is a Synther implementation that can converts patches into
|
||||
@ -93,7 +95,8 @@ success:
|
||||
f.Read(su_sample_table[:])
|
||||
}
|
||||
|
||||
func (s GoSynther) Name() string { return "Go" }
|
||||
func (s GoSynther) Name() string { return "Go" }
|
||||
func (s GoSynther) SupportsMultithreading() bool { return false }
|
||||
|
||||
func (s GoSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
||||
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
|
||||
@ -115,6 +118,16 @@ func (s *GoSynth) Release(voiceIndex int) {
|
||||
s.state.voices[voiceIndex].sustain = false
|
||||
}
|
||||
|
||||
func (s *GoSynth) Close() {}
|
||||
|
||||
func (s *GoSynth) CPULoad(loads []sointu.CPULoad) int {
|
||||
if len(loads) < 1 {
|
||||
return 0
|
||||
}
|
||||
loads[0] = s.cpuLoad
|
||||
return 1
|
||||
}
|
||||
|
||||
func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
|
||||
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
|
||||
if err != nil {
|
||||
@ -143,7 +156,10 @@ func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, time int, renderError error) {
|
||||
func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, renderTime int, renderError error) {
|
||||
startTime := time.Now()
|
||||
defer func() { s.cpuLoad.Update(time.Since(startTime), int64(samples)) }()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
renderError = fmt.Errorf("render panicced: %v", err)
|
||||
@ -153,7 +169,7 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
||||
stack := s.stack[:]
|
||||
stack = append(stack, []float32{0, 0, 0, 0}...)
|
||||
synth := &s.state
|
||||
for time < maxtime && len(buffer) > 0 {
|
||||
for renderTime < maxtime && len(buffer) > 0 {
|
||||
opcodesInstr := s.bytecode.Opcodes
|
||||
operandsInstr := s.bytecode.Operands
|
||||
opcodes, operands := opcodesInstr, operandsInstr
|
||||
@ -182,7 +198,7 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
||||
}
|
||||
tcount := transformCounts[opNoStereo-1]
|
||||
if len(operands) < tcount {
|
||||
return samples, time, errors.New("operand stream ended prematurely")
|
||||
return samples, renderTime, errors.New("operand stream ended prematurely")
|
||||
}
|
||||
voice := &voices[0]
|
||||
unit := &units[0]
|
||||
@ -289,7 +305,7 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
||||
r := unit.state[0] + float32(math.Exp2(float64(stack[l-1]*2.206896551724138))-1)
|
||||
w := int(r+1.5) - 1
|
||||
unit.state[0] = r - float32(w)
|
||||
time += w
|
||||
renderTime += w
|
||||
stack = stack[:l-1]
|
||||
case opIn:
|
||||
var channel byte
|
||||
@ -581,26 +597,26 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
||||
case opSync:
|
||||
break
|
||||
default:
|
||||
return samples, time, errors.New("invalid / unimplemented opcode")
|
||||
return samples, renderTime, errors.New("invalid / unimplemented opcode")
|
||||
}
|
||||
units = units[1:]
|
||||
}
|
||||
if len(stack) < 4 {
|
||||
return samples, time, errors.New("stack underflow")
|
||||
return samples, renderTime, errors.New("stack underflow")
|
||||
}
|
||||
if len(stack) > 4 {
|
||||
return samples, time, errors.New("stack not empty")
|
||||
return samples, renderTime, errors.New("stack not empty")
|
||||
}
|
||||
buffer[0][0], buffer[0][1] = synth.outputs[0], synth.outputs[1]
|
||||
synth.outputs[0] = 0
|
||||
synth.outputs[1] = 0
|
||||
buffer = buffer[1:]
|
||||
samples++
|
||||
time++
|
||||
renderTime++
|
||||
s.state.globalTime++
|
||||
}
|
||||
s.stack = stack[:0]
|
||||
return samples, time, nil
|
||||
return samples, renderTime, nil
|
||||
}
|
||||
|
||||
func (s *synthState) rand() float32 {
|
||||
|
||||
@ -197,6 +197,7 @@ func TestStackUnderflow(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
buffer := make(sointu.AudioBuffer, 1)
|
||||
err = buffer.Fill(synth)
|
||||
if err == nil {
|
||||
@ -213,6 +214,7 @@ func TestStackBalancing(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("bridge compile error: %v", err)
|
||||
}
|
||||
defer synth.Close()
|
||||
buffer := make(sointu.AudioBuffer, 1)
|
||||
err = buffer.Fill(synth)
|
||||
if err == nil {
|
||||
|
||||
217
vm/multithread_synth.go
Normal file
217
vm/multithread_synth.go
Normal file
@ -0,0 +1,217 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/bits"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type (
|
||||
MultithreadSynth struct {
|
||||
voiceMapping voiceMapping
|
||||
synths []sointu.Synth
|
||||
commands chan<- multithreadSynthCommand // maxtime
|
||||
results <-chan multithreadSynthResult // rendered buffer
|
||||
pool sync.Pool
|
||||
synther sointu.Synther
|
||||
}
|
||||
|
||||
MultithreadSynther struct {
|
||||
synther sointu.Synther
|
||||
name string
|
||||
}
|
||||
|
||||
voiceMapping [MAX_THREADS][MAX_VOICES]int
|
||||
|
||||
multithreadSynthCommand struct {
|
||||
thread int
|
||||
samples int
|
||||
time int
|
||||
}
|
||||
|
||||
multithreadSynthResult struct {
|
||||
buffer *sointu.AudioBuffer
|
||||
samples int
|
||||
time int
|
||||
renderError error
|
||||
}
|
||||
)
|
||||
|
||||
const MAX_THREADS = 4
|
||||
|
||||
func MakeMultithreadSynther(synther sointu.Synther) MultithreadSynther {
|
||||
return MultithreadSynther{synther: synther, name: "Multithread " + synther.Name()}
|
||||
}
|
||||
|
||||
func (s MultithreadSynther) Name() string { return s.name }
|
||||
func (s MultithreadSynther) SupportsMultithreading() bool { return true }
|
||||
|
||||
func (s MultithreadSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
||||
patches, voiceMapping := splitPatchByCores(patch)
|
||||
synths := make([]sointu.Synth, 0, len(patches))
|
||||
for _, p := range patches {
|
||||
synth, err := s.synther.Synth(p, bpm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
synths = append(synths, synth)
|
||||
}
|
||||
ret := &MultithreadSynth{
|
||||
synths: synths,
|
||||
voiceMapping: voiceMapping,
|
||||
pool: sync.Pool{New: func() any { ret := make(sointu.AudioBuffer, 0, 8096); return &ret }},
|
||||
}
|
||||
ret.startProcesses()
|
||||
ret.synther = s.synther
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) Update(patch sointu.Patch, bpm int) error {
|
||||
patches, voiceMapping := splitPatchByCores(patch)
|
||||
if s.voiceMapping != voiceMapping {
|
||||
s.voiceMapping = voiceMapping
|
||||
s.closeSynths()
|
||||
}
|
||||
for i, p := range patches {
|
||||
if len(s.synths) <= i {
|
||||
synth, err := s.synther.Synth(p, bpm)
|
||||
if err != nil {
|
||||
s.closeSynths()
|
||||
return err
|
||||
}
|
||||
s.synths = append(s.synths, synth)
|
||||
} else {
|
||||
if err := s.synths[i].Update(p, bpm); err != nil {
|
||||
s.closeSynths()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) startProcesses() {
|
||||
maxProcs := runtime.GOMAXPROCS(0)
|
||||
cmdChan := make(chan multithreadSynthCommand, maxProcs)
|
||||
s.commands = cmdChan
|
||||
resultsChan := make(chan multithreadSynthResult, maxProcs)
|
||||
s.results = resultsChan
|
||||
for i := 0; i < maxProcs; i++ {
|
||||
go func(commandCh <-chan multithreadSynthCommand, resultCh chan<- multithreadSynthResult) {
|
||||
for cmd := range commandCh {
|
||||
buffer := s.pool.Get().(*sointu.AudioBuffer)
|
||||
*buffer = append(*buffer, make(sointu.AudioBuffer, cmd.samples)...)
|
||||
samples, time, renderError := s.synths[cmd.thread].Render(*buffer, cmd.time)
|
||||
resultCh <- multithreadSynthResult{buffer: buffer, samples: samples, time: time, renderError: renderError}
|
||||
}
|
||||
}(cmdChan, resultsChan)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) Close() {
|
||||
close(s.commands)
|
||||
s.closeSynths()
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) closeSynths() {
|
||||
for _, synth := range s.synths {
|
||||
synth.Close()
|
||||
}
|
||||
s.synths = s.synths[:0]
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) Trigger(voiceIndex int, note byte) {
|
||||
for i, synth := range s.synths {
|
||||
if ind := s.voiceMapping[i][voiceIndex]; ind >= 0 {
|
||||
synth.Trigger(ind, note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) Release(voiceIndex int) {
|
||||
for i, synth := range s.synths {
|
||||
if ind := s.voiceMapping[i][voiceIndex]; ind >= 0 {
|
||||
synth.Release(ind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) CPULoad(loads []sointu.CPULoad) (elems int) {
|
||||
for _, synth := range s.synths {
|
||||
n := synth.CPULoad(loads)
|
||||
elems += n
|
||||
loads = loads[n:]
|
||||
if len(loads) <= 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *MultithreadSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, time int, renderError error) {
|
||||
count := len(s.synths)
|
||||
for i := 0; i < count; i++ {
|
||||
s.commands <- multithreadSynthCommand{thread: i, samples: len(buffer), time: maxtime}
|
||||
}
|
||||
clear(buffer)
|
||||
samples = math.MaxInt
|
||||
time = math.MaxInt
|
||||
for i := 0; i < count; i++ {
|
||||
// We mix the results as they come, but the order doesn't matter. This
|
||||
// leads to slight indeterminism in the results, because the order of
|
||||
// floating point additions can change the least significant bits.
|
||||
result := <-s.results
|
||||
if result.renderError != nil && renderError == nil {
|
||||
renderError = result.renderError
|
||||
}
|
||||
samples = min(samples, result.samples)
|
||||
time = min(time, result.time)
|
||||
for j := 0; j < samples; j++ {
|
||||
buffer[j][0] += (*result.buffer)[j][0]
|
||||
buffer[j][1] += (*result.buffer)[j][1]
|
||||
}
|
||||
*result.buffer = (*result.buffer)[:0]
|
||||
s.pool.Put(result.buffer)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func splitPatchByCores(patch sointu.Patch) ([]sointu.Patch, voiceMapping) {
|
||||
cores := 1
|
||||
for _, instr := range patch {
|
||||
cores = max(bits.Len((uint)(instr.ThreadMaskM1+1)), cores)
|
||||
}
|
||||
cores = min(cores, MAX_THREADS)
|
||||
ret := make([]sointu.Patch, cores)
|
||||
for c := 0; c < cores; c++ {
|
||||
ret[c] = make(sointu.Patch, 0, len(patch))
|
||||
}
|
||||
var voicemapping [MAX_THREADS][MAX_VOICES]int
|
||||
for c := 0; c < MAX_THREADS; c++ {
|
||||
for j := 0; j < MAX_VOICES; j++ {
|
||||
voicemapping[c][j] = -1
|
||||
}
|
||||
}
|
||||
for c := range cores {
|
||||
coreVoice := 0
|
||||
curVoice := 0
|
||||
for _, instr := range patch {
|
||||
mask := instr.ThreadMaskM1 + 1
|
||||
if mask&(1<<c) != 0 {
|
||||
ret[c] = append(ret[c], instr)
|
||||
for j := 0; j < instr.NumVoices; j++ {
|
||||
if coreVoice+j >= MAX_VOICES {
|
||||
break
|
||||
}
|
||||
voicemapping[c][curVoice+j] = coreVoice + j
|
||||
}
|
||||
coreVoice += instr.NumVoices
|
||||
}
|
||||
curVoice += instr.NumVoices
|
||||
}
|
||||
}
|
||||
return ret, voicemapping
|
||||
}
|
||||
Reference in New Issue
Block a user