diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ab5ab..322f5c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- Show CPU load percentage in the song panel ([#192][i192]) - Theme can be user configured, in theme.yml. This theme.yml should be placed in the usual sointu config directory (i.e. `os.UserConfigDir()/sointu/theme.yml`). See @@ -332,3 +333,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [i170]: https://github.com/vsariola/sointu/issues/170 [i176]: https://github.com/vsariola/sointu/issues/176 [i186]: https://github.com/vsariola/sointu/issues/186 +[i192]: https://github.com/vsariola/sointu/issues/192 diff --git a/tracker/broker.go b/tracker/broker.go index 0b1f7df..03f4f4f 100644 --- a/tracker/broker.go +++ b/tracker/broker.go @@ -63,6 +63,7 @@ type ( Panic bool SongPosition sointu.SongPos VoiceLevels [vm.MAX_VOICES]float32 + CPULoad float64 HasDetectorResult bool DetectorResult DetectorResult diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index c72d8e9..901a2fc 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -129,6 +129,14 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Cursor step", NumUpDown(tr.Theme, t.Step, "Cursor step").Layout) }), + layout.Rigid(func(gtx C) D { + cpuload := tr.Model.CPULoad() + label := Label(tr.Theme, &tr.Theme.SongPanel.RowValue, fmt.Sprintf("%.0f %%", cpuload*100)) + if cpuload >= 1 { + label.Color = tr.Theme.SongPanel.ErrorColor + } + return layoutSongOptionRow(gtx, tr.Theme, "CPU load", label.Layout) + }), ) }) }), diff --git a/tracker/model.go b/tracker/model.go index d70efd3..d865c1f 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -69,6 +69,7 @@ type ( linkInstrTrack bool voiceLevels [vm.MAX_VOICES]float32 + cpuLoad float64 signalAnalyzer *ScopeModel detectorResult DetectorResult @@ -343,6 +344,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) { })) } m.panic = msg.Panic + m.cpuLoad = msg.CPULoad } if msg.HasDetectorResult { m.detectorResult = msg.DetectorResult @@ -377,6 +379,8 @@ func (m *Model) ProcessMsg(msg MsgToModel) { } } +func (m *Model) CPULoad() float64 { return m.cpuLoad } + func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer } func (m *Model) Broker() *Broker { return m.broker } diff --git a/tracker/player.go b/tracker/player.go index 8b1c27e..3c1642d 100644 --- a/tracker/player.go +++ b/tracker/player.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "slices" + "time" "github.com/vsariola/sointu" "github.com/vsariola/sointu/vm" @@ -32,6 +33,8 @@ type ( frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source] events NoteEventList + cpuload float64 // current CPU load of the player, used to adjust the render rate + synther sointu.Synther // the synther used to create new synths broker *Broker // the broker used to communicate with different parts of the tracker } @@ -86,6 +89,9 @@ func NewPlayer(broker *Broker, synther sointu.Synther) *Player { // buffer. It is used to trigger and release notes during processing. The // context is also used to get the current BPM from the host. func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext) { + startTime := time.Now() + startFrame := p.frame + p.processMessages(context) p.events.adjustTimes(p.frameDeltas, p.frame, p.frame+int64(len(buffer))) @@ -145,6 +151,7 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext } // when the buffer is full, return if len(buffer) == 0 { + p.updateCPULoad(time.Since(startTime), p.frame-startFrame) p.send(nil) return } @@ -345,7 +352,7 @@ func (p *Player) compileOrUpdateSynth() { // all sendTargets from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock func (p *Player) send(message interface{}) { - TrySend(p.broker.ToModel, MsgToModel{HasPanicPosLevels: true, Panic: p.synth == nil, SongPosition: p.songPos, VoiceLevels: p.voiceLevels, Data: message}) + TrySend(p.broker.ToModel, MsgToModel{HasPanicPosLevels: true, Panic: p.synth == nil, SongPosition: p.songPos, VoiceLevels: p.voiceLevels, CPULoad: p.cpuload, Data: message}) } func (p *Player) processNoteEvent(ev NoteEvent) { @@ -405,3 +412,14 @@ func (p *Player) processNoteEvent(ev NoteEvent) { p.synth.Trigger(oldestVoice, ev.Note) 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.cpuload = float64(p.cpuload)*alpha + newload*(1-alpha) +}