mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-12 17:14:43 -04: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
@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- Multithreaded synths: the user can split the patch up to four threads.
|
||||||
|
Selecting the thread can be done on the instrument properties pane.
|
||||||
|
Multithreading works only on the multithreaded synths, selectable from the CPU
|
||||||
|
panel. Currently the multithreaded rendering has not yet been implemented in
|
||||||
|
the compiled player and the thread information is disregarded while compiling
|
||||||
|
the song. ([#199][i199])
|
||||||
- Preset explorer, whichs allows 1) searching the presets by name; 2) filtering
|
- Preset explorer, whichs allows 1) searching the presets by name; 2) filtering
|
||||||
them by category (directory); 3) filtering them by being builtin vs. user;
|
them by category (directory); 3) filtering them by being builtin vs. user;
|
||||||
4) filtering them if they need gm.dls (for Linux/Mac users, who don't have
|
4) filtering them if they need gm.dls (for Linux/Mac users, who don't have
|
||||||
@ -382,6 +388,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i186]: https://github.com/vsariola/sointu/issues/186
|
[i186]: https://github.com/vsariola/sointu/issues/186
|
||||||
[i192]: https://github.com/vsariola/sointu/issues/192
|
[i192]: https://github.com/vsariola/sointu/issues/192
|
||||||
[i196]: https://github.com/vsariola/sointu/issues/196
|
[i196]: https://github.com/vsariola/sointu/issues/196
|
||||||
|
[i199]: https://github.com/vsariola/sointu/issues/199
|
||||||
[i200]: https://github.com/vsariola/sointu/issues/200
|
[i200]: https://github.com/vsariola/sointu/issues/200
|
||||||
[i210]: https://github.com/vsariola/sointu/issues/210
|
[i210]: https://github.com/vsariola/sointu/issues/210
|
||||||
[i211]: https://github.com/vsariola/sointu/issues/211
|
[i211]: https://github.com/vsariola/sointu/issues/211
|
||||||
|
|||||||
23
audio.go
23
audio.go
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -62,6 +63,13 @@ type (
|
|||||||
// Release releases the currently playing note for a given voice. Called
|
// Release releases the currently playing note for a given voice. Called
|
||||||
// between synth.Renders.
|
// between synth.Renders.
|
||||||
Release(voice int)
|
Release(voice int)
|
||||||
|
|
||||||
|
// Close disposes the synth, freeing any resources. No other functions should be called after Close.
|
||||||
|
Close()
|
||||||
|
|
||||||
|
// Populates the given array with the current CPU load of each thread,
|
||||||
|
// returning the number of threads / elements populated
|
||||||
|
CPULoad([]CPULoad) int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synther compiles a given Patch into a Synth, throwing errors if the
|
// Synther compiles a given Patch into a Synth, throwing errors if the
|
||||||
@ -69,7 +77,10 @@ type (
|
|||||||
Synther interface {
|
Synther interface {
|
||||||
Name() string // Name of the synther, e.g. "Go" or "Native"
|
Name() string // Name of the synther, e.g. "Go" or "Native"
|
||||||
Synth(patch Patch, bpm int) (Synth, error)
|
Synth(patch Patch, bpm int) (Synth, error)
|
||||||
|
SupportsMultithreading() 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,
|
||||||
@ -83,6 +94,7 @@ func Play(synther Synther, song Song, progress func(float32)) (AudioBuffer, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("sointu.Play failed: %v", err)
|
return nil, fmt.Errorf("sointu.Play failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
curVoices := make([]int, len(song.Score.Tracks))
|
curVoices := make([]int, len(song.Score.Tracks))
|
||||||
for i := range curVoices {
|
for i := range curVoices {
|
||||||
curVoices[i] = song.Score.FirstVoiceForTrack(i)
|
curVoices[i] = song.Score.FirstVoiceForTrack(i)
|
||||||
@ -204,6 +216,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 {
|
||||||
|
|||||||
@ -7,4 +7,5 @@ import (
|
|||||||
|
|
||||||
var Synthers = []sointu.Synther{
|
var Synthers = []sointu.Synther{
|
||||||
vm.GoSynther{},
|
vm.GoSynther{},
|
||||||
|
vm.MakeMultithreadSynther(vm.GoSynther{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import "github.com/vsariola/sointu/vm/compiler/bridge"
|
import (
|
||||||
|
"github.com/vsariola/sointu/vm"
|
||||||
|
"github.com/vsariola/sointu/vm/compiler/bridge"
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Synthers = append(Synthers, bridge.NativeSynther{})
|
Synthers = append(Synthers, bridge.NativeSynther{})
|
||||||
|
Synthers = append(Synthers, vm.MakeMultithreadSynther(bridge.NativeSynther{}))
|
||||||
}
|
}
|
||||||
|
|||||||
35
patch.go
35
patch.go
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"math/bits"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -19,8 +20,12 @@ type (
|
|||||||
Name string `yaml:",omitempty"`
|
Name string `yaml:",omitempty"`
|
||||||
Comment string `yaml:",omitempty"`
|
Comment string `yaml:",omitempty"`
|
||||||
NumVoices int
|
NumVoices int
|
||||||
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
|
||||||
|
// ThreadMaskM1 is a bit mask of which threads are used, minus 1. Minus
|
||||||
|
// 1 is done so that the default value 0 means bit mask 0b0001 i.e. only
|
||||||
|
// thread 1 is rendering the instrument.
|
||||||
|
ThreadMaskM1 int `yaml:",omitempty"`
|
||||||
|
Units []Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
// Unit is e.g. a filter, oscillator, envelope and its parameters
|
||||||
@ -347,13 +352,14 @@ func init() {
|
|||||||
|
|
||||||
// Copy makes a deep copy of a unit.
|
// Copy makes a deep copy of a unit.
|
||||||
func (u *Unit) Copy() Unit {
|
func (u *Unit) Copy() Unit {
|
||||||
parameters := make(map[string]int)
|
ret := *u
|
||||||
|
ret.Parameters = make(map[string]int, len(u.Parameters))
|
||||||
for k, v := range u.Parameters {
|
for k, v := range u.Parameters {
|
||||||
parameters[k] = v
|
ret.Parameters[k] = v
|
||||||
}
|
}
|
||||||
varArgs := make([]int, len(u.VarArgs))
|
ret.VarArgs = make([]int, len(u.VarArgs))
|
||||||
copy(varArgs, u.VarArgs)
|
copy(ret.VarArgs, u.VarArgs)
|
||||||
return Unit{Type: u.Type, Parameters: parameters, VarArgs: varArgs, ID: u.ID, Disabled: u.Disabled, Comment: u.Comment}
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
var stackUseSource = [2]StackUse{
|
var stackUseSource = [2]StackUse{
|
||||||
@ -473,11 +479,12 @@ func (u *Unit) StackNeed() int {
|
|||||||
|
|
||||||
// Copy makes a deep copy of an Instrument
|
// Copy makes a deep copy of an Instrument
|
||||||
func (instr *Instrument) Copy() Instrument {
|
func (instr *Instrument) Copy() Instrument {
|
||||||
units := make([]Unit, len(instr.Units))
|
ret := *instr
|
||||||
|
ret.Units = make([]Unit, len(instr.Units))
|
||||||
for i, u := range instr.Units {
|
for i, u := range instr.Units {
|
||||||
units[i] = u.Copy()
|
ret.Units[i] = u.Copy()
|
||||||
}
|
}
|
||||||
return Instrument{Name: instr.Name, Comment: instr.Comment, NumVoices: instr.NumVoices, Units: units, Mute: instr.Mute}
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement the counter interface
|
// Implement the counter interface
|
||||||
@ -536,6 +543,16 @@ func (p Patch) NumSyncs() int {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Patch) NumThreads() int {
|
||||||
|
numThreads := 1
|
||||||
|
for _, instr := range p {
|
||||||
|
if l := bits.Len((uint)(instr.ThreadMaskM1 + 1)); l > numThreads {
|
||||||
|
numThreads = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numThreads
|
||||||
|
}
|
||||||
|
|
||||||
// FirstVoiceForInstrument returns the index of the first voice of given
|
// FirstVoiceForInstrument returns the index of the first voice of given
|
||||||
// instrument. For example, if the Patch has three instruments (0, 1 and 2),
|
// instrument. For example, if the Patch has three instruments (0, 1 and 2),
|
||||||
// with 1, 3, 2 voices, respectively, then FirstVoiceForInstrument(0) returns 0,
|
// with 1, 3, 2 voices, respectively, then FirstVoiceForInstrument(0) returns 0,
|
||||||
|
|||||||
5
song.go
5
song.go
@ -293,7 +293,10 @@ func (l Score) LengthInRows() int {
|
|||||||
|
|
||||||
// Copy makes a deep copy of a Score.
|
// Copy makes a deep copy of a Score.
|
||||||
func (s *Song) Copy() Song {
|
func (s *Song) Copy() Song {
|
||||||
return Song{BPM: s.BPM, RowsPerBeat: s.RowsPerBeat, Score: s.Score.Copy(), Patch: s.Patch.Copy()}
|
ret := *s
|
||||||
|
ret.Score = s.Score.Copy()
|
||||||
|
ret.Patch = s.Patch.Copy()
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming 44100 Hz playback speed, return the number of samples of each row of
|
// Assuming 44100 Hz playback speed, return the number of samples of each row of
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
package tracker
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Bool struct {
|
Bool struct {
|
||||||
value BoolValue
|
value BoolValue
|
||||||
@ -29,6 +33,10 @@ type (
|
|||||||
InstrEditor Model
|
InstrEditor Model
|
||||||
InstrPresets Model
|
InstrPresets Model
|
||||||
InstrComment Model
|
InstrComment Model
|
||||||
|
Thread1 Model
|
||||||
|
Thread2 Model
|
||||||
|
Thread3 Model
|
||||||
|
Thread4 Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeBool(valueEnabler interface {
|
func MakeBool(valueEnabler interface {
|
||||||
@ -66,6 +74,78 @@ func (v Bool) Enabled() bool {
|
|||||||
return v.enabler.Enabled()
|
return v.enabler.Enabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread methods
|
||||||
|
|
||||||
|
func (m *Model) getThreadsBit(bit int) bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
|
||||||
|
return mask&(1<<bit) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) setThreadsBit(bit int, value bool) {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("ThreadBitMask", PatchChange, MinorChange)()
|
||||||
|
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
|
||||||
|
if value {
|
||||||
|
mask |= (1 << bit)
|
||||||
|
} else {
|
||||||
|
mask &^= (1 << bit)
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 = max(mask-1, 0) // -1 would have all threads disabled, so make that 0 i.e. use at least thread 1
|
||||||
|
m.warnAboutCrossThreadSends()
|
||||||
|
m.warnNoMultithreadSupport()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) warnAboutCrossThreadSends() {
|
||||||
|
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.ThreadMaskM1 != m.d.Song.Patch[it].ThreadMaskM1 {
|
||||||
|
m.Alerts().AddNamed("CrossThreadSend", fmt.Sprintf("Instrument %d '%s' has a send to instrument %d '%s' but they are not on the same threads, which may cause issues", i+1, instr.Name, it+1, m.d.Song.Patch[it].Name), Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) warnNoMultithreadSupport() {
|
||||||
|
for _, instr := range m.d.Song.Patch {
|
||||||
|
if instr.ThreadMaskM1 > 0 && !m.synthers[m.syntherIndex].SupportsMultithreading() {
|
||||||
|
m.Alerts().AddNamed("NoMultithreadSupport", "The current synth does not support multithreading and the patch was configured to use more than one thread", Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Thread1() Bool { return MakeEnabledBool((*Thread1)(m)) }
|
||||||
|
func (m *Thread1) Value() bool { return (*Model)(m).getThreadsBit(0) }
|
||||||
|
func (m *Thread1) SetValue(val bool) { (*Model)(m).setThreadsBit(0, val) }
|
||||||
|
|
||||||
|
func (m *Model) Thread2() Bool { return MakeEnabledBool((*Thread2)(m)) }
|
||||||
|
func (m *Thread2) Value() bool { return (*Model)(m).getThreadsBit(1) }
|
||||||
|
func (m *Thread2) SetValue(val bool) { (*Model)(m).setThreadsBit(1, val) }
|
||||||
|
|
||||||
|
func (m *Model) Thread3() Bool { return MakeEnabledBool((*Thread3)(m)) }
|
||||||
|
func (m *Thread3) Value() bool { return (*Model)(m).getThreadsBit(2) }
|
||||||
|
func (m *Thread3) SetValue(val bool) { (*Model)(m).setThreadsBit(2, val) }
|
||||||
|
|
||||||
|
func (m *Model) Thread4() Bool { return MakeEnabledBool((*Thread4)(m)) }
|
||||||
|
func (m *Thread4) Value() bool { return (*Model)(m).getThreadsBit(3) }
|
||||||
|
func (m *Thread4) SetValue(val bool) { (*Model)(m).setThreadsBit(3, val) }
|
||||||
|
|
||||||
// Panic methods
|
// Panic methods
|
||||||
|
|
||||||
func (m *Model) Panic() Bool { return MakeEnabledBool((*Panic)(m)) }
|
func (m *Model) Panic() Bool { return MakeEnabledBool((*Panic)(m)) }
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type (
|
|||||||
list *layout.List
|
list *layout.List
|
||||||
soloBtn *Clickable
|
soloBtn *Clickable
|
||||||
muteBtn *Clickable
|
muteBtn *Clickable
|
||||||
|
threadBtns [4]*Clickable
|
||||||
soloHint string
|
soloHint string
|
||||||
unsoloHint string
|
unsoloHint string
|
||||||
muteHint string
|
muteHint string
|
||||||
@ -38,6 +39,7 @@ func NewInstrumentProperties() *InstrumentProperties {
|
|||||||
muteBtn: new(Clickable),
|
muteBtn: new(Clickable),
|
||||||
voices: NewNumericUpDownState(),
|
voices: NewNumericUpDownState(),
|
||||||
splitInstrumentBtn: new(Clickable),
|
splitInstrumentBtn: new(Clickable),
|
||||||
|
threadBtns: [4]*Clickable{new(Clickable), new(Clickable), new(Clickable), new(Clickable)},
|
||||||
}
|
}
|
||||||
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
||||||
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
||||||
@ -66,7 +68,21 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ip.list.Layout(gtx, 9, func(gtx C, index int) D {
|
thread1btn := ToggleIconBtn(tr.Thread1(), tr.Theme, ip.threadBtns[0], icons.ImageCropSquare, icons.ImageFilter1, "Do not render instrument on thread 1", "Render instrument on thread 1")
|
||||||
|
thread2btn := ToggleIconBtn(tr.Thread2(), tr.Theme, ip.threadBtns[1], icons.ImageCropSquare, icons.ImageFilter2, "Do not render instrument on thread 2", "Render instrument on thread 2")
|
||||||
|
thread3btn := ToggleIconBtn(tr.Thread3(), tr.Theme, ip.threadBtns[2], icons.ImageCropSquare, icons.ImageFilter3, "Do not render instrument on thread 3", "Render instrument on thread 3")
|
||||||
|
thread4btn := ToggleIconBtn(tr.Thread4(), tr.Theme, ip.threadBtns[3], icons.ImageCropSquare, icons.ImageFilter4, "Do not render instrument on thread 4", "Render instrument on thread 4")
|
||||||
|
|
||||||
|
threadbtnline := func(gtx C) D {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
layout.Rigid(thread1btn.Layout),
|
||||||
|
layout.Rigid(thread2btn.Layout),
|
||||||
|
layout.Rigid(thread3btn.Layout),
|
||||||
|
layout.Rigid(thread4btn.Layout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.list.Layout(gtx, 11, func(gtx C, index int) D {
|
||||||
switch index {
|
switch index {
|
||||||
case 0:
|
case 0:
|
||||||
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
||||||
@ -81,6 +97,8 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
soloBtn := ToggleIconBtn(tr.Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
soloBtn := ToggleIconBtn(tr.Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
||||||
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
||||||
case 8:
|
case 8:
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "Thread", threadbtnline)
|
||||||
|
case 10:
|
||||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
||||||
return ip.commentEditor.Layout(gtx, tr.InstrumentComment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
return ip.commentEditor.Layout(gtx, tr.InstrumentComment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
||||||
})
|
})
|
||||||
@ -94,7 +112,7 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
|
|
||||||
func layoutInstrumentPropertyLine(gtx C, text string, content layout.Widget) D {
|
func layoutInstrumentPropertyLine(gtx C, text string, content layout.Widget) D {
|
||||||
tr := TrackerFromContext(gtx)
|
tr := TrackerFromContext(gtx)
|
||||||
gtx.Constraints.Max.X = min(gtx.Dp(unit.Dp(200)), gtx.Constraints.Max.X)
|
gtx.Constraints.Max.X = min(gtx.Dp(300), gtx.Constraints.Max.X)
|
||||||
label := Label(tr.Theme, &tr.Theme.InstrumentEditor.Properties.Label, text)
|
label := Label(tr.Theme, &tr.Theme.InstrumentEditor.Properties.Label, text)
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(layout.Spacer{Width: 6, Height: 36}.Layout),
|
layout.Rigid(layout.Spacer{Width: 6, Height: 36}.Layout),
|
||||||
|
|||||||
@ -4,15 +4,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"slices"
|
||||||
"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,10 +114,37 @@ 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()
|
cpuSmallLabel := func(gtx C) D {
|
||||||
cpuLabel := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, fmt.Sprintf("%.0f %%", cpuload*100))
|
var a [vm.MAX_THREADS]sointu.CPULoad
|
||||||
if cpuload >= 1 {
|
c := tr.Model.CPULoad(a[:])
|
||||||
cpuLabel.Color = tr.Theme.SongPanel.ErrorColor
|
load := slices.Max(a[:c])
|
||||||
|
cpuLabel := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, fmt.Sprintf("%d%%", int(load*100+0.5)))
|
||||||
|
if load >= 1 {
|
||||||
|
cpuLabel.Color = tr.Theme.SongPanel.ErrorColor
|
||||||
|
}
|
||||||
|
return cpuLabel.Layout(gtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuEnlargedWidget := func(gtx C) D {
|
||||||
|
var sb strings.Builder
|
||||||
|
var a [vm.MAX_THREADS]sointu.CPULoad
|
||||||
|
c := tr.Model.CPULoad(a[:])
|
||||||
|
high := false
|
||||||
|
for i := range c {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Fprint(&sb, ", ")
|
||||||
|
}
|
||||||
|
cpuLoad := a[i]
|
||||||
|
fmt.Fprintf(&sb, "%d%%", int(cpuLoad*100+0.5))
|
||||||
|
if cpuLoad >= 1 {
|
||||||
|
high = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cpuLabel := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, sb.String())
|
||||||
|
if high {
|
||||||
|
cpuLabel.Color = tr.Theme.SongPanel.ErrorColor
|
||||||
|
}
|
||||||
|
return cpuLabel.Layout(gtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
synthBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.SynthBtn, tr.Model.SyntherName(), "")
|
synthBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.SynthBtn, tr.Model.SyntherName(), "")
|
||||||
@ -150,10 +181,10 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return t.CPUExpander.Layout(gtx, tr.Theme, "CPU", cpuLabel.Layout,
|
return t.CPUExpander.Layout(gtx, tr.Theme, "CPU", cpuSmallLabel,
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Load", cpuLabel.Layout) }),
|
layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Load", cpuEnlargedWidget) }),
|
||||||
layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Synth", synthBtn.Layout) }),
|
layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Synth", synthBtn.Layout) }),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -397,7 +397,9 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) CPULoad() float64 { return m.playerStatus.CPULoad }
|
func (m *Model) CPULoad(buf []sointu.CPULoad) int {
|
||||||
|
return copy(buf, m.playerStatus.CPULoad[:m.playerStatus.NumThreads])
|
||||||
|
}
|
||||||
|
|
||||||
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
|
NumThreads int
|
||||||
|
CPULoad [vm.MAX_THREADS]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)))
|
||||||
|
|
||||||
@ -127,12 +124,12 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
|||||||
if p.synth != nil {
|
if p.synth != nil {
|
||||||
rendered, timeAdvanced, err = p.synth.Render(buffer[:framesUntilEvent], timeUntilRowAdvance)
|
rendered, timeAdvanced, err = p.synth.Render(buffer[:framesUntilEvent], timeUntilRowAdvance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
p.send(Alert{Message: fmt.Sprintf("synth.Render: %s", err.Error()), Priority: Error, Name: "PlayerCrash", Duration: defaultAlertDuration})
|
p.send(Alert{Message: fmt.Sprintf("synth.Render: %s", err.Error()), Priority: Error, Name: "PlayerCrash", Duration: defaultAlertDuration})
|
||||||
}
|
}
|
||||||
// for performance, we don't check for NaN of every sample, because typically NaNs propagate
|
// for performance, we don't check for NaN of every sample, because typically NaNs propagate
|
||||||
if rendered > 0 && (isNaN(buffer[0][0]) || isNaN(buffer[0][1]) || isInf(buffer[0][0]) || isInf(buffer[0][1])) {
|
if rendered > 0 && (isNaN(buffer[0][0]) || isNaN(buffer[0][1]) || isInf(buffer[0][0]) || isInf(buffer[0][1])) {
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
p.send(Alert{Message: "Inf or NaN detected in synth output", Priority: Error, Name: "PlayerCrash", Duration: defaultAlertDuration})
|
p.send(Alert{Message: "Inf or NaN detected in synth output", Priority: Error, Name: "PlayerCrash", Duration: defaultAlertDuration})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -164,17 +161,26 @@ 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.NumThreads = p.synth.CPULoad(p.status.CPULoad[:])
|
||||||
|
}
|
||||||
p.send(nil)
|
p.send(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we were not able to fill the buffer with NUM_RENDER_TRIES attempts, destroy synth and throw an error
|
// we were not able to fill the buffer with NUM_RENDER_TRIES attempts, destroy synth and throw an error
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
p.events = p.events[:0] // clear events, so we don't try to process them again
|
p.events = p.events[:0] // clear events, so we don't try to process them again
|
||||||
p.SendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
|
p.SendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) destroySynth() {
|
||||||
|
if p.synth != nil {
|
||||||
|
p.synth.Close()
|
||||||
|
p.synth = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Player) advanceRow() {
|
func (p *Player) advanceRow() {
|
||||||
if p.song.Score.Length == 0 || p.song.Score.RowsPerPattern == 0 {
|
if p.song.Score.Length == 0 || p.song.Score.RowsPerPattern == 0 {
|
||||||
return
|
return
|
||||||
@ -227,7 +233,7 @@ loop:
|
|||||||
switch m := msg.(type) {
|
switch m := msg.(type) {
|
||||||
case PanicMsg:
|
case PanicMsg:
|
||||||
if m.bool {
|
if m.bool {
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
} else {
|
} else {
|
||||||
p.compileOrUpdateSynth()
|
p.compileOrUpdateSynth()
|
||||||
}
|
}
|
||||||
@ -283,7 +289,7 @@ loop:
|
|||||||
}
|
}
|
||||||
case sointu.Synther:
|
case sointu.Synther:
|
||||||
p.synther = m
|
p.synther = m
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
p.compileOrUpdateSynth()
|
p.compileOrUpdateSynth()
|
||||||
default:
|
default:
|
||||||
// ignore unknown messages
|
// ignore unknown messages
|
||||||
@ -355,7 +361,7 @@ func (p *Player) compileOrUpdateSynth() {
|
|||||||
if p.synth != nil {
|
if p.synth != nil {
|
||||||
err := p.synth.Update(p.song.Patch, p.song.BPM)
|
err := p.synth.Update(p.song.Patch, p.song.BPM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
p.SendAlert("PlayerCrash", fmt.Sprintf("synth.Update: %v", err), Error)
|
p.SendAlert("PlayerCrash", fmt.Sprintf("synth.Update: %v", err), Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -363,7 +369,7 @@ func (p *Player) compileOrUpdateSynth() {
|
|||||||
var err error
|
var err error
|
||||||
p.synth, err = p.synther.Synth(p.song.Patch, p.song.BPM)
|
p.synth, err = p.synther.Synth(p.song.Patch, p.song.BPM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.synth = nil
|
p.destroySynth()
|
||||||
p.SendAlert("PlayerCrash", fmt.Sprintf("synther.Synth: %v", err), Error)
|
p.SendAlert("PlayerCrash", fmt.Sprintf("synther.Synth: %v", err), Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -441,14 +447,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,9 +17,13 @@ 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) SupportsMultithreading() bool { return false }
|
||||||
|
|
||||||
func (s NativeSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
func (s NativeSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
||||||
synth, err := Synth(patch, bpm)
|
synth, err := Synth(patch, bpm)
|
||||||
@ -45,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)
|
||||||
@ -64,7 +69,17 @@ 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) 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
|
// 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.
|
// 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 {
|
||||||
@ -105,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
|
||||||
}
|
}
|
||||||
@ -116,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
|
||||||
}
|
}
|
||||||
@ -125,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,6 +86,7 @@ func TestRenderSamples(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
synth.Trigger(0, 64)
|
synth.Trigger(0, 64)
|
||||||
buffer := make(sointu.AudioBuffer, su_max_samples)
|
buffer := make(sointu.AudioBuffer, su_max_samples)
|
||||||
err = buffer[:len(buffer)/2].Fill(synth)
|
err = buffer[:len(buffer)/2].Fill(synth)
|
||||||
@ -162,6 +163,7 @@ func TestStackUnderflow(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
buffer := make(sointu.AudioBuffer, 1)
|
buffer := make(sointu.AudioBuffer, 1)
|
||||||
err = buffer.Fill(synth)
|
err = buffer.Fill(synth)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -178,6 +180,7 @@ func TestStackBalancing(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
buffer := make(sointu.AudioBuffer, 1)
|
buffer := make(sointu.AudioBuffer, 1)
|
||||||
err = buffer.Fill(synth)
|
err = buffer.Fill(synth)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -211,6 +214,7 @@ func TestStackOverflow(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
buffer := make(sointu.AudioBuffer, 1)
|
buffer := make(sointu.AudioBuffer, 1)
|
||||||
err = buffer.Fill(synth)
|
err = buffer.Fill(synth)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -228,6 +232,7 @@ func TestDivideByZero(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
buffer := make(sointu.AudioBuffer, 1)
|
buffer := make(sointu.AudioBuffer, 1)
|
||||||
err = buffer.Fill(synth)
|
err = buffer.Fill(synth)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@ -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
|
||||||
@ -93,7 +95,8 @@ success:
|
|||||||
f.Read(su_sample_table[:])
|
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) {
|
func (s GoSynther) Synth(patch sointu.Patch, bpm int) (sointu.Synth, error) {
|
||||||
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
|
bytecode, err := NewBytecode(patch, AllFeatures{}, bpm)
|
||||||
@ -115,6 +118,16 @@ func (s *GoSynth) Release(voiceIndex int) {
|
|||||||
s.state.voices[voiceIndex].sustain = false
|
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 {
|
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 {
|
||||||
@ -143,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)
|
||||||
@ -153,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
|
||||||
@ -182,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]
|
||||||
@ -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)
|
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
|
||||||
@ -581,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 {
|
||||||
|
|||||||
@ -197,6 +197,7 @@ func TestStackUnderflow(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
buffer := make(sointu.AudioBuffer, 1)
|
buffer := make(sointu.AudioBuffer, 1)
|
||||||
err = buffer.Fill(synth)
|
err = buffer.Fill(synth)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -213,6 +214,7 @@ func TestStackBalancing(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("bridge compile error: %v", err)
|
t.Fatalf("bridge compile error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer synth.Close()
|
||||||
buffer := make(sointu.AudioBuffer, 1)
|
buffer := make(sointu.AudioBuffer, 1)
|
||||||
err = buffer.Fill(synth)
|
err = buffer.Fill(synth)
|
||||||
if err == nil {
|
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