mirror of
https://github.com/vsariola/sointu.git
synced 2025-11-18 16:49:06 -05:00
drafting
This commit is contained in:
parent
7f03664870
commit
fa7901c7c6
20
audio.go
20
audio.go
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -65,6 +66,12 @@ type (
|
|||||||
|
|
||||||
// Close disposes the synth, freeing any resources. No other functions should be called after Close.
|
// Close disposes the synth, freeing any resources. No other functions should be called after Close.
|
||||||
Close()
|
Close()
|
||||||
|
|
||||||
|
// Returns the number of cores the synth is using
|
||||||
|
NumCores() int
|
||||||
|
|
||||||
|
// Populates the given array with the current CPU load of each core
|
||||||
|
CPULoad([]CPULoad)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synther compiles a given Patch into a Synth, throwing errors if the
|
// Synther compiles a given Patch into a Synth, throwing errors if the
|
||||||
@ -74,6 +81,8 @@ type (
|
|||||||
Synth(patch Patch, bpm int) (Synth, error)
|
Synth(patch Patch, bpm int) (Synth, error)
|
||||||
SupportsParallelism() bool
|
SupportsParallelism() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CPULoad float32
|
||||||
)
|
)
|
||||||
|
|
||||||
// Play plays the Song by first compiling the patch with the given Synther,
|
// Play plays the Song by first compiling the patch with the given Synther,
|
||||||
@ -209,6 +218,17 @@ func (buffer AudioBuffer) Raw(pcm16 bool) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *CPULoad) Update(duration time.Duration, frames int64) {
|
||||||
|
if frames <= 0 {
|
||||||
|
return // no frames rendered, so cannot compute CPU load
|
||||||
|
}
|
||||||
|
realtime := float64(duration) / 1e9
|
||||||
|
songtime := float64(frames) / 44100
|
||||||
|
newload := realtime / songtime
|
||||||
|
alpha := math.Exp(-songtime) // smoothing factor, time constant of 1 second
|
||||||
|
*p = CPULoad(float64(*p)*alpha + newload*(1-alpha))
|
||||||
|
}
|
||||||
|
|
||||||
func (data AudioBuffer) rawToBuffer(pcm16 bool, buf *bytes.Buffer) error {
|
func (data AudioBuffer) rawToBuffer(pcm16 bool, buf *bytes.Buffer) error {
|
||||||
var err error
|
var err error
|
||||||
if pcm16 {
|
if pcm16 {
|
||||||
|
|||||||
2
patch.go
2
patch.go
@ -21,7 +21,7 @@ type (
|
|||||||
NumVoices int
|
NumVoices int
|
||||||
Units []Unit
|
Units []Unit
|
||||||
Mute bool `yaml:",omitempty"` // Mute is only used in the tracker for soloing/muting instruments; the compiled player ignores this field
|
Mute bool `yaml:",omitempty"` // Mute is only used in the tracker for soloing/muting instruments; the compiled player ignores this field
|
||||||
CoreBitMask uint `yaml:",omitempty"` // CoreBitMask tells which cores this instrument can run on; 0 means all cores
|
CoreMaskM1 int `yaml:",omitempty"` // CoreMaskM1 is a bit mask of which cores are used, minus 1. Minus 1 is done so that the default value 0 means bit mask 0b0001 i.e. only core 1 is rendering the instrument.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package tracker
|
package tracker
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Bool struct {
|
Bool struct {
|
||||||
value BoolValue
|
value BoolValue
|
||||||
@ -76,7 +78,8 @@ func (m *Model) getCoresBit(bit int) bool {
|
|||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return m.d.Song.Patch[m.d.InstrIndex].CoreBitMask&(1<<bit) != 0
|
mask := m.d.Song.Patch[m.d.InstrIndex].CoreMaskM1 + 1
|
||||||
|
return mask&(1<<bit) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) setCoresBit(bit int, value bool) {
|
func (m *Model) setCoresBit(bit int, value bool) {
|
||||||
@ -84,10 +87,34 @@ func (m *Model) setCoresBit(bit int, value bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer (*Model)(m).change("CoreBitMask", PatchChange, MinorChange)()
|
defer (*Model)(m).change("CoreBitMask", PatchChange, MinorChange)()
|
||||||
|
mask := m.d.Song.Patch[m.d.InstrIndex].CoreMaskM1 + 1
|
||||||
if value {
|
if value {
|
||||||
m.d.Song.Patch[m.d.InstrIndex].CoreBitMask |= (1 << bit)
|
mask |= (1 << bit)
|
||||||
} else {
|
} else {
|
||||||
m.d.Song.Patch[m.d.InstrIndex].CoreBitMask &^= (1 << bit)
|
mask &^= (1 << bit)
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].CoreMaskM1 = max(mask-1, 0) // -1 would have all cores disabled, so make that 0 i.e. use core 1 only
|
||||||
|
m.warnAboutCrossCoreSends()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) warnAboutCrossCoreSends() {
|
||||||
|
for i, instr := range m.d.Song.Patch {
|
||||||
|
for _, unit := range instr.Units {
|
||||||
|
if unit.Type == "send" {
|
||||||
|
targetID, ok := unit.Parameters["target"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
it, _, err := m.d.Song.Patch.FindUnit(targetID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if instr.CoreMaskM1 != m.d.Song.Patch[it].CoreMaskM1 {
|
||||||
|
m.Alerts().AddNamed("CrossCoreSend", fmt.Sprintf("Instrument %d '%s' has a send to instrument %d '%s' but they are not on the same cores, which may cause issues", i+1, instr.Name, it+1, m.d.Song.Patch[it].Name), Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,14 +5,17 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gioui.org/gesture"
|
"gioui.org/gesture"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"github.com/vsariola/sointu/version"
|
"github.com/vsariola/sointu/version"
|
||||||
|
"github.com/vsariola/sointu/vm"
|
||||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -110,9 +113,24 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
}
|
}
|
||||||
oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "")
|
oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "")
|
||||||
|
|
||||||
cpuload := tr.Model.CPULoad()
|
var sb strings.Builder
|
||||||
cpuLabel := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, fmt.Sprintf("%.0f %%", cpuload*100))
|
var loadArr [vm.MAX_CORES]sointu.CPULoad
|
||||||
if cpuload >= 1 {
|
tr.Model.CPULoad(loadArr[:])
|
||||||
|
c := min(vm.MAX_CORES, tr.Model.NumCores())
|
||||||
|
high := false
|
||||||
|
for i := range c {
|
||||||
|
if i > 0 {
|
||||||
|
// unless this is the first item, add the separator before it.
|
||||||
|
fmt.Fprint(&sb, ", ")
|
||||||
|
}
|
||||||
|
cpuLoad := loadArr[i]
|
||||||
|
fmt.Fprintf(&sb, "%.0f %%", cpuLoad*100)
|
||||||
|
if cpuLoad >= 1 {
|
||||||
|
high = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cpuLabel := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, sb.String())
|
||||||
|
if high {
|
||||||
cpuLabel.Color = tr.Theme.SongPanel.ErrorColor
|
cpuLabel.Color = tr.Theme.SongPanel.ErrorColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -397,7 +397,8 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) CPULoad() float64 { return m.playerStatus.CPULoad }
|
func (m *Model) NumCores() int { return m.playerStatus.NumCores }
|
||||||
|
func (m *Model) CPULoad(buf []sointu.CPULoad) { copy(buf, m.playerStatus.CPULoad[:]) }
|
||||||
|
|
||||||
func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer }
|
func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer }
|
||||||
func (m *Model) Broker() *Broker { return m.broker }
|
func (m *Model) Broker() *Broker { return m.broker }
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/vm"
|
"github.com/vsariola/sointu/vm"
|
||||||
@ -42,7 +41,8 @@ type (
|
|||||||
PlayerStatus struct {
|
PlayerStatus struct {
|
||||||
SongPos sointu.SongPos // the current position in the score
|
SongPos sointu.SongPos // the current position in the score
|
||||||
VoiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
|
VoiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
|
||||||
CPULoad float64 // current CPU load of the player, used to adjust the render rate
|
NumCores int
|
||||||
|
CPULoad [vm.MAX_CORES]sointu.CPULoad // current CPU load of the player, used to adjust the render rate
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayerProcessContext is the context given to the player when processing
|
// PlayerProcessContext is the context given to the player when processing
|
||||||
@ -97,9 +97,6 @@ func NewPlayer(broker *Broker, synther sointu.Synther) *Player {
|
|||||||
// buffer. It is used to trigger and release notes during processing. The
|
// buffer. It is used to trigger and release notes during processing. The
|
||||||
// context is also used to get the current BPM from the host.
|
// context is also used to get the current BPM from the host.
|
||||||
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext) {
|
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext) {
|
||||||
startTime := time.Now()
|
|
||||||
startFrame := p.frame
|
|
||||||
|
|
||||||
p.processMessages(context)
|
p.processMessages(context)
|
||||||
p.events.adjustTimes(p.frameDeltas, p.frame, p.frame+int64(len(buffer)))
|
p.events.adjustTimes(p.frameDeltas, p.frame, p.frame+int64(len(buffer)))
|
||||||
|
|
||||||
@ -164,7 +161,10 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
|||||||
}
|
}
|
||||||
// when the buffer is full, return
|
// when the buffer is full, return
|
||||||
if len(buffer) == 0 {
|
if len(buffer) == 0 {
|
||||||
p.updateCPULoad(time.Since(startTime), p.frame-startFrame)
|
if p.synth != nil {
|
||||||
|
p.status.NumCores = p.synth.NumCores()
|
||||||
|
p.synth.CPULoad(p.status.CPULoad[:])
|
||||||
|
}
|
||||||
p.send(nil)
|
p.send(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -448,14 +448,3 @@ func (p *Player) processNoteEvent(ev NoteEvent) {
|
|||||||
p.synth.Trigger(oldestVoice, ev.Note)
|
p.synth.Trigger(oldestVoice, ev.Note)
|
||||||
TrySend(p.broker.ToModel, MsgToModel{TriggerChannel: instrIndex + 1})
|
TrySend(p.broker.ToModel, MsgToModel{TriggerChannel: instrIndex + 1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) updateCPULoad(duration time.Duration, frames int64) {
|
|
||||||
if frames <= 0 {
|
|
||||||
return // no frames rendered, so cannot compute CPU load
|
|
||||||
}
|
|
||||||
realtime := float64(duration) / 1e9
|
|
||||||
songtime := float64(frames) / 44100
|
|
||||||
newload := realtime / songtime
|
|
||||||
alpha := math.Exp(-songtime) // smoothing factor, time constant of 1 second
|
|
||||||
p.status.CPULoad = float64(p.status.CPULoad)*alpha + newload*(1-alpha)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/vm"
|
"github.com/vsariola/sointu/vm"
|
||||||
@ -16,7 +17,10 @@ import (
|
|||||||
type NativeSynther struct {
|
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) SupportsParallelism() bool { return false }
|
func (s NativeSynther) SupportsParallelism() bool { return false }
|
||||||
@ -46,7 +50,7 @@ func Synth(patch sointu.Patch, bpm int) (*NativeSynth, error) {
|
|||||||
s.Opcodes[0] = 0
|
s.Opcodes[0] = 0
|
||||||
s.NumVoices = 1
|
s.NumVoices = 1
|
||||||
s.Polyphony = 0
|
s.Polyphony = 0
|
||||||
return (*NativeSynth)(s), nil
|
return &NativeSynth{csynth: *s}, nil
|
||||||
}
|
}
|
||||||
for i, v := range comPatch.Opcodes {
|
for i, v := range comPatch.Opcodes {
|
||||||
s.Opcodes[i] = (C.uchar)(v)
|
s.Opcodes[i] = (C.uchar)(v)
|
||||||
@ -65,11 +69,19 @@ func Synth(patch sointu.Patch, bpm int) (*NativeSynth, error) {
|
|||||||
s.NumVoices = C.uint(comPatch.NumVoices)
|
s.NumVoices = C.uint(comPatch.NumVoices)
|
||||||
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
|
s.Polyphony = C.uint(comPatch.PolyphonyBitmask)
|
||||||
s.RandSeed = 1
|
s.RandSeed = 1
|
||||||
return (*NativeSynth)(s), nil
|
return &NativeSynth{csynth: *s}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NativeSynth) Close() {}
|
func (s *NativeSynth) Close() {}
|
||||||
|
|
||||||
|
func (s *NativeSynth) NumCores() int { return 1 }
|
||||||
|
func (s *NativeSynth) CPULoad(loads []sointu.CPULoad) {
|
||||||
|
if len(loads) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loads[0] = s.cpuLoad
|
||||||
|
}
|
||||||
|
|
||||||
// Render renders until the buffer is full or the modulated time is reached, whichever
|
// Render renders until the buffer is full or the modulated time is reached, whichever
|
||||||
// happens first.
|
// happens first.
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@ -92,12 +104,14 @@ func (s *NativeSynth) Close() {}
|
|||||||
// exit condition would fire when the time is already past maxtime.
|
// 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.
|
// 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) {
|
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
|
// 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 {
|
if len(buffer)%1 == 1 {
|
||||||
return -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))
|
samples := C.int(len(buffer))
|
||||||
|
startTime := time.Now()
|
||||||
|
defer func() { bridgesynth.cpuLoad.Update(time.Since(startTime), int64(samples)) }()
|
||||||
time := C.int(maxtime)
|
time := C.int(maxtime)
|
||||||
errcode := int(C.su_render(synth, (*C.float)(&buffer[0][0]), &samples, &time))
|
errcode := int(C.su_render(synth, (*C.float)(&buffer[0][0]), &samples, &time))
|
||||||
if errcode > 0 {
|
if errcode > 0 {
|
||||||
@ -108,7 +122,7 @@ func (bridgesynth *NativeSynth) Render(buffer sointu.AudioBuffer, maxtime int) (
|
|||||||
|
|
||||||
// Trigger is part of C.Synths' implementation of sointu.Synth interface
|
// Trigger is part of C.Synths' implementation of sointu.Synth interface
|
||||||
func (bridgesynth *NativeSynth) Trigger(voice int, note byte) {
|
func (bridgesynth *NativeSynth) Trigger(voice int, note byte) {
|
||||||
s := (*C.Synth)(bridgesynth)
|
s := &bridgesynth.csynth
|
||||||
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -119,7 +133,7 @@ func (bridgesynth *NativeSynth) Trigger(voice int, note byte) {
|
|||||||
|
|
||||||
// Release is part of C.Synths' implementation of sointu.Synth interface
|
// Release is part of C.Synths' implementation of sointu.Synth interface
|
||||||
func (bridgesynth *NativeSynth) Release(voice int) {
|
func (bridgesynth *NativeSynth) Release(voice int) {
|
||||||
s := (*C.Synth)(bridgesynth)
|
s := &bridgesynth.csynth
|
||||||
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -128,7 +142,7 @@ func (bridgesynth *NativeSynth) Release(voice int) {
|
|||||||
|
|
||||||
// Update
|
// Update
|
||||||
func (bridgesynth *NativeSynth) Update(patch sointu.Patch, bpm int) error {
|
func (bridgesynth *NativeSynth) Update(patch sointu.Patch, bpm int) error {
|
||||||
s := (*C.Synth)(bridgesynth)
|
s := &bridgesynth.csynth
|
||||||
if n := patch.NumDelayLines(); n > 128 {
|
if n := patch.NumDelayLines(); n > 128 {
|
||||||
return fmt.Errorf("native bridge has currently a hard limit of 128 delaylines; patch uses %v", n)
|
return fmt.Errorf("native bridge has currently a hard limit of 128 delaylines; patch uses %v", n)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
@ -27,6 +28,7 @@ type (
|
|||||||
stack []float32
|
stack []float32
|
||||||
state synthState
|
state synthState
|
||||||
delaylines []delayline
|
delaylines []delayline
|
||||||
|
cpuLoad sointu.CPULoad
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoSynther is a Synther implementation that can converts patches into
|
// GoSynther is a Synther implementation that can converts patches into
|
||||||
@ -118,6 +120,14 @@ func (s *GoSynth) Release(voiceIndex int) {
|
|||||||
|
|
||||||
func (s *GoSynth) Close() {}
|
func (s *GoSynth) Close() {}
|
||||||
|
|
||||||
|
func (s *GoSynth) NumCores() int { return 1 }
|
||||||
|
func (s *GoSynth) CPULoad(loads []sointu.CPULoad) {
|
||||||
|
if len(loads) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loads[0] = s.cpuLoad
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
|
func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
|
||||||
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
|
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -146,7 +156,10 @@ func (s *GoSynth) Update(patch sointu.Patch, bpm int) error {
|
|||||||
return nil
|
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() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
renderError = fmt.Errorf("render panicced: %v", err)
|
renderError = fmt.Errorf("render panicced: %v", err)
|
||||||
@ -156,7 +169,7 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
|||||||
stack := s.stack[:]
|
stack := s.stack[:]
|
||||||
stack = append(stack, []float32{0, 0, 0, 0}...)
|
stack = append(stack, []float32{0, 0, 0, 0}...)
|
||||||
synth := &s.state
|
synth := &s.state
|
||||||
for time < maxtime && len(buffer) > 0 {
|
for renderTime < maxtime && len(buffer) > 0 {
|
||||||
opcodesInstr := s.bytecode.Opcodes
|
opcodesInstr := s.bytecode.Opcodes
|
||||||
operandsInstr := s.bytecode.Operands
|
operandsInstr := s.bytecode.Operands
|
||||||
opcodes, operands := opcodesInstr, operandsInstr
|
opcodes, operands := opcodesInstr, operandsInstr
|
||||||
@ -185,7 +198,7 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
|||||||
}
|
}
|
||||||
tcount := transformCounts[opNoStereo-1]
|
tcount := transformCounts[opNoStereo-1]
|
||||||
if len(operands) < tcount {
|
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]
|
voice := &voices[0]
|
||||||
unit := &units[0]
|
unit := &units[0]
|
||||||
@ -292,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)
|
r := unit.state[0] + float32(math.Exp2(float64(stack[l-1]*2.206896551724138))-1)
|
||||||
w := int(r+1.5) - 1
|
w := int(r+1.5) - 1
|
||||||
unit.state[0] = r - float32(w)
|
unit.state[0] = r - float32(w)
|
||||||
time += w
|
renderTime += w
|
||||||
stack = stack[:l-1]
|
stack = stack[:l-1]
|
||||||
case opIn:
|
case opIn:
|
||||||
var channel byte
|
var channel byte
|
||||||
@ -584,26 +597,26 @@ func (s *GoSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, t
|
|||||||
case opSync:
|
case opSync:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return samples, time, errors.New("invalid / unimplemented opcode")
|
return samples, renderTime, errors.New("invalid / unimplemented opcode")
|
||||||
}
|
}
|
||||||
units = units[1:]
|
units = units[1:]
|
||||||
}
|
}
|
||||||
if len(stack) < 4 {
|
if len(stack) < 4 {
|
||||||
return samples, time, errors.New("stack underflow")
|
return samples, renderTime, errors.New("stack underflow")
|
||||||
}
|
}
|
||||||
if len(stack) > 4 {
|
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]
|
buffer[0][0], buffer[0][1] = synth.outputs[0], synth.outputs[1]
|
||||||
synth.outputs[0] = 0
|
synth.outputs[0] = 0
|
||||||
synth.outputs[1] = 0
|
synth.outputs[1] = 0
|
||||||
buffer = buffer[1:]
|
buffer = buffer[1:]
|
||||||
samples++
|
samples++
|
||||||
time++
|
renderTime++
|
||||||
s.state.globalTime++
|
s.state.globalTime++
|
||||||
}
|
}
|
||||||
s.stack = stack[:0]
|
s.stack = stack[:0]
|
||||||
return samples, time, nil
|
return samples, renderTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *synthState) rand() float32 {
|
func (s *synthState) rand() float32 {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
ParallelSynth struct {
|
ParallelSynth struct {
|
||||||
voiceMapping [][]int
|
voiceMapping voiceMapping
|
||||||
synths []sointu.Synth
|
synths []sointu.Synth
|
||||||
commands chan<- parallelSynthCommand // maxtime
|
commands chan<- parallelSynthCommand // maxtime
|
||||||
results <-chan parallelSynthResult // rendered buffer
|
results <-chan parallelSynthResult // rendered buffer
|
||||||
@ -24,6 +24,8 @@ type (
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
voiceMapping [MAX_CORES][MAX_VOICES]int
|
||||||
|
|
||||||
parallelSynthCommand struct {
|
parallelSynthCommand struct {
|
||||||
core int
|
core int
|
||||||
samples int
|
samples int
|
||||||
@ -38,6 +40,8 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MAX_CORES = 4
|
||||||
|
|
||||||
func MakeParallelSynther(synther sointu.Synther) ParallelSynther {
|
func MakeParallelSynther(synther sointu.Synther) ParallelSynther {
|
||||||
return ParallelSynther{synther: synther, name: "Parallel " + synther.Name()}
|
return ParallelSynther{synther: synther, name: "Parallel " + synther.Name()}
|
||||||
}
|
}
|
||||||
@ -67,16 +71,21 @@ func (s ParallelSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error
|
|||||||
|
|
||||||
func (s *ParallelSynth) Update(patch sointu.Patch, bpm int) error {
|
func (s *ParallelSynth) Update(patch sointu.Patch, bpm int) error {
|
||||||
patches, voiceMapping := splitPatchByCores(patch)
|
patches, voiceMapping := splitPatchByCores(patch)
|
||||||
|
if s.voiceMapping != voiceMapping {
|
||||||
s.voiceMapping = voiceMapping
|
s.voiceMapping = voiceMapping
|
||||||
|
s.closeSynths()
|
||||||
|
}
|
||||||
for i, p := range patches {
|
for i, p := range patches {
|
||||||
if len(s.synths) <= i {
|
if len(s.synths) <= i {
|
||||||
synth, err := s.synther.Synth(p, bpm)
|
synth, err := s.synther.Synth(p, bpm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.closeSynths()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.synths = append(s.synths, synth)
|
s.synths = append(s.synths, synth)
|
||||||
} else {
|
} else {
|
||||||
if err := s.synths[i].Update(p, bpm); err != nil {
|
if err := s.synths[i].Update(p, bpm); err != nil {
|
||||||
|
s.closeSynths()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,9 +113,14 @@ func (s *ParallelSynth) startProcesses() {
|
|||||||
|
|
||||||
func (s *ParallelSynth) Close() {
|
func (s *ParallelSynth) Close() {
|
||||||
close(s.commands)
|
close(s.commands)
|
||||||
|
s.closeSynths()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ParallelSynth) closeSynths() {
|
||||||
for _, synth := range s.synths {
|
for _, synth := range s.synths {
|
||||||
synth.Close()
|
synth.Close()
|
||||||
}
|
}
|
||||||
|
s.synths = s.synths[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ParallelSynth) Trigger(voiceIndex int, note byte) {
|
func (s *ParallelSynth) Trigger(voiceIndex int, note byte) {
|
||||||
@ -125,6 +139,23 @@ func (s *ParallelSynth) Release(voiceIndex int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ParallelSynth) NumCores() (coreCount int) {
|
||||||
|
for i := range s.synths {
|
||||||
|
coreCount += s.synths[i].NumCores()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ParallelSynth) CPULoad(loads []sointu.CPULoad) {
|
||||||
|
for _, synth := range s.synths {
|
||||||
|
synth.CPULoad(loads)
|
||||||
|
if len(loads) <= synth.NumCores() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loads = loads[synth.NumCores():]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ParallelSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, time int, renderError error) {
|
func (s *ParallelSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, time int, renderError error) {
|
||||||
count := len(s.synths)
|
count := len(s.synths)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
@ -134,6 +165,9 @@ func (s *ParallelSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples
|
|||||||
samples = math.MaxInt
|
samples = math.MaxInt
|
||||||
time = math.MaxInt
|
time = math.MaxInt
|
||||||
for i := 0; i < count; i++ {
|
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
|
result := <-s.results
|
||||||
if result.renderError != nil && renderError == nil {
|
if result.renderError != nil && renderError == nil {
|
||||||
renderError = result.renderError
|
renderError = result.renderError
|
||||||
@ -150,28 +184,34 @@ func (s *ParallelSynth) Render(buffer sointu.AudioBuffer, maxtime int) (samples
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitPatchByCores(patch sointu.Patch) ([]sointu.Patch, [][]int) {
|
func splitPatchByCores(patch sointu.Patch) ([]sointu.Patch, voiceMapping) {
|
||||||
maxCores := 1
|
cores := 1
|
||||||
for _, instr := range patch {
|
for _, instr := range patch {
|
||||||
maxCores = max(bits.Len(instr.CoreBitMask), maxCores)
|
cores = max(bits.Len((uint)(instr.CoreMaskM1+1)), cores)
|
||||||
}
|
}
|
||||||
ret := make([]sointu.Patch, maxCores)
|
cores = min(cores, MAX_CORES)
|
||||||
for core := 0; core < maxCores; core++ {
|
ret := make([]sointu.Patch, cores)
|
||||||
ret[core] = make(sointu.Patch, 0, len(patch))
|
for c := 0; c < cores; c++ {
|
||||||
|
ret[c] = make(sointu.Patch, 0, len(patch))
|
||||||
}
|
}
|
||||||
voicemapping := make([][]int, maxCores)
|
var voicemapping [MAX_CORES][MAX_VOICES]int
|
||||||
for core := range maxCores {
|
for c := 0; c < MAX_CORES; c++ {
|
||||||
voicemapping[core] = make([]int, patch.NumVoices())
|
for j := 0; j < MAX_VOICES; j++ {
|
||||||
for j := range voicemapping[core] {
|
voicemapping[c][j] = -1
|
||||||
voicemapping[core][j] = -1
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
for c := range cores {
|
||||||
coreVoice := 0
|
coreVoice := 0
|
||||||
curVoice := 0
|
curVoice := 0
|
||||||
for _, instr := range patch {
|
for _, instr := range patch {
|
||||||
if instr.CoreBitMask == 0 || (instr.CoreBitMask&(1<<core)) != 0 {
|
mask := instr.CoreMaskM1 + 1
|
||||||
ret[core] = append(ret[core], instr)
|
if mask&(1<<c) != 0 {
|
||||||
|
ret[c] = append(ret[c], instr)
|
||||||
for j := 0; j < instr.NumVoices; j++ {
|
for j := 0; j < instr.NumVoices; j++ {
|
||||||
voicemapping[core][curVoice+j] = coreVoice + j
|
if coreVoice+j >= MAX_VOICES {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
voicemapping[c][curVoice+j] = coreVoice + j
|
||||||
}
|
}
|
||||||
coreVoice += instr.NumVoices
|
coreVoice += instr.NumVoices
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user