refactor(tracker): Player sends PlayerStatus to the Model

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-06-19 11:37:11 +03:00
parent c77d541dc6
commit 4f2c73d0db
4 changed files with 42 additions and 44 deletions

View File

@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
) )
type ( type (
@ -59,11 +58,9 @@ type (
// avoid allocations. All the infrequently passed messages can be boxed & // avoid allocations. All the infrequently passed messages can be boxed &
// cast to any; casting pointer types to any is cheap (does not allocate). // cast to any; casting pointer types to any is cheap (does not allocate).
MsgToModel struct { MsgToModel struct {
HasPanicPosLevels bool HasPanicPlayerStatus bool
Panic bool Panic bool
SongPosition sointu.SongPos PlayerStatus PlayerStatus
VoiceLevels [vm.MAX_VOICES]float32
CPULoad float64
HasDetectorResult bool HasDetectorResult bool
DetectorResult DetectorResult DetectorResult DetectorResult

View File

@ -174,7 +174,7 @@ func (v *Instruments) Item(i int) (name string, maxLevel float32, mute bool, ok
end = vm.MAX_VOICES end = vm.MAX_VOICES
} }
if start < end { if start < end {
for _, level := range v.voiceLevels[start:end] { for _, level := range v.playerStatus.VoiceLevels[start:end] {
if maxLevel < level { if maxLevel < level {
maxLevel = level maxLevel = level
} }

View File

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
"github.com/vsariola/sointu/vm"
) )
// Model implements the mutable state for the tracker program GUI. // Model implements the mutable state for the tracker program GUI.
@ -58,7 +57,6 @@ type (
panic bool panic bool
recording bool recording bool
playing bool playing bool
playPosition sointu.SongPos
loop Loop loop Loop
follow bool follow bool
quitted bool quitted bool
@ -68,8 +66,7 @@ type (
// reordering or deleting instrument can delete track) // reordering or deleting instrument can delete track)
linkInstrTrack bool linkInstrTrack bool
voiceLevels [vm.MAX_VOICES]float32 playerStatus PlayerStatus
cpuLoad float64
signalAnalyzer *ScopeModel signalAnalyzer *ScopeModel
detectorResult DetectorResult detectorResult DetectorResult
@ -159,9 +156,9 @@ const (
const maxUndo = 64 const maxUndo = 64
func (m *Model) PlayPosition() sointu.SongPos { return m.playPosition } func (m *Model) PlayPosition() sointu.SongPos { return m.playerStatus.SongPos }
func (m *Model) Loop() Loop { return m.loop } func (m *Model) Loop() Loop { return m.loop }
func (m *Model) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playPosition) } func (m *Model) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playerStatus.SongPos) }
func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave } func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave }
func (m *Model) Dialog() Dialog { return m.dialog } func (m *Model) Dialog() Dialog { return m.dialog }
func (m *Model) Quitted() bool { return m.quitted } func (m *Model) Quitted() bool { return m.quitted }
@ -332,19 +329,17 @@ func (m *Model) UnmarshalRecovery(bytes []byte) {
} }
func (m *Model) ProcessMsg(msg MsgToModel) { func (m *Model) ProcessMsg(msg MsgToModel) {
if msg.HasPanicPosLevels { if msg.HasPanicPlayerStatus {
m.playPosition = msg.SongPosition m.playerStatus = msg.PlayerStatus
m.voiceLevels = msg.VoiceLevels
if m.playing && m.follow { if m.playing && m.follow {
m.d.Cursor.SongPos = msg.SongPosition m.d.Cursor.SongPos = msg.PlayerStatus.SongPos
m.d.Cursor2.SongPos = msg.SongPosition m.d.Cursor2.SongPos = msg.PlayerStatus.SongPos
TrySend(m.broker.ToGUI, any(MsgToGUI{ TrySend(m.broker.ToGUI, any(MsgToGUI{
Kind: GUIMessageCenterOnRow, Kind: GUIMessageCenterOnRow,
Param: m.PlaySongRow(), Param: m.PlaySongRow(),
})) }))
} }
m.panic = msg.Panic m.panic = msg.Panic
m.cpuLoad = msg.CPULoad
} }
if msg.HasDetectorResult { if msg.HasDetectorResult {
m.detectorResult = msg.DetectorResult m.detectorResult = msg.DetectorResult
@ -379,7 +374,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
} }
} }
func (m *Model) CPULoad() float64 { return m.cpuLoad } func (m *Model) CPULoad() float64 { return 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 }

View File

@ -18,14 +18,12 @@ type (
// model via the playerMessages channel. The model sendTargets messages to the // model via the playerMessages channel. The model sendTargets messages to the
// player via the modelMessages channel. // player via the modelMessages channel.
Player struct { Player struct {
synth sointu.Synth // the synth used to render audio synth sointu.Synth // the synth used to render audio
song sointu.Song // the song being played song sointu.Song // the song being played
playing bool // is the player playing the score or not playing bool // is the player playing the score or not
rowtime int // how many samples have been played in the current row rowtime int // how many samples have been played in the current row
songPos sointu.SongPos // the current position in the score voices [vm.MAX_VOICES]voice
voiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice loop Loop
voices [vm.MAX_VOICES]voice
loop Loop
recording Recording // the recorded MIDI events and BPM recording Recording // the recorded MIDI events and BPM
@ -33,12 +31,20 @@ type (
frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source] frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source]
events NoteEventList events NoteEventList
cpuload float64 // current CPU load of the player, used to adjust the render rate status PlayerStatus // the part of the Player state that is communicated to the model to visualize what Player is doing
synther sointu.Synther // the synther used to create new synths synther sointu.Synther // the synther used to create new synths
broker *Broker // the broker used to communicate with different parts of the tracker broker *Broker // the broker used to communicate with different parts of the tracker
} }
// PlayerStatus is the part of the player state that is communicated to the
// model, for different visualizations of what is happening in the player.
PlayerStatus struct {
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
CPULoad float64 // 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
// audio. Currently it is only used to get BPM from the VSTI host. // audio. Currently it is only used to get BPM from the VSTI host.
PlayerProcessContext interface { PlayerProcessContext interface {
@ -144,9 +150,9 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
alpha := float32(math.Exp(-float64(rendered) / 15000)) alpha := float32(math.Exp(-float64(rendered) / 15000))
for i, state := range p.voices { for i, state := range p.voices {
if state.sustain { if state.sustain {
p.voiceLevels[i] = (p.voiceLevels[i]-0.5)*alpha + 0.5 p.status.VoiceLevels[i] = (p.status.VoiceLevels[i]-0.5)*alpha + 0.5
} else { } else {
p.voiceLevels[i] *= alpha p.status.VoiceLevels[i] *= alpha
} }
} }
// when the buffer is full, return // when the buffer is full, return
@ -166,14 +172,14 @@ 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
} }
origPos := p.songPos origPos := p.status.SongPos
p.songPos.PatternRow++ // advance row (this is why we subtracted one in Play()) p.status.SongPos.PatternRow++ // advance row (this is why we subtracted one in Play())
if p.loop.Length > 0 && p.songPos.PatternRow >= p.song.Score.RowsPerPattern && p.songPos.OrderRow == p.loop.Start+p.loop.Length-1 { if p.loop.Length > 0 && p.status.SongPos.PatternRow >= p.song.Score.RowsPerPattern && p.status.SongPos.OrderRow == p.loop.Start+p.loop.Length-1 {
p.songPos.PatternRow = 0 p.status.SongPos.PatternRow = 0
p.songPos.OrderRow = p.loop.Start p.status.SongPos.OrderRow = p.loop.Start
} }
p.songPos = p.song.Score.Clamp(p.songPos) p.status.SongPos = p.song.Score.Clamp(p.status.SongPos)
if p.songPos == origPos { if p.status.SongPos == origPos {
p.send(IsPlayingMsg{bool: false}) p.send(IsPlayingMsg{bool: false})
p.playing = false p.playing = false
for i := range p.song.Score.Tracks { for i := range p.song.Score.Tracks {
@ -182,7 +188,7 @@ func (p *Player) advanceRow() {
return return
} }
for i, t := range p.song.Score.Tracks { for i, t := range p.song.Score.Tracks {
n := t.Note(p.songPos) n := t.Note(p.status.SongPos)
switch { switch {
case n == 0: case n == 0:
p.processNoteEvent(NoteEvent{Channel: i, IsTrack: true, Source: p, On: false}) p.processNoteEvent(NoteEvent{Channel: i, IsTrack: true, Source: p, On: false})
@ -233,8 +239,8 @@ loop:
p.compileOrUpdateSynth() p.compileOrUpdateSynth()
case StartPlayMsg: case StartPlayMsg:
p.playing = true p.playing = true
p.songPos = m.SongPos p.status.SongPos = m.SongPos
p.songPos.PatternRow-- p.status.SongPos.PatternRow--
p.rowtime = math.MaxInt p.rowtime = math.MaxInt
for i, t := range p.song.Score.Tracks { for i, t := range p.song.Score.Tracks {
if !t.Effect { if !t.Effect {
@ -352,7 +358,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 // 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{}) { func (p *Player) send(message interface{}) {
TrySend(p.broker.ToModel, MsgToModel{HasPanicPosLevels: true, Panic: p.synth == nil, SongPosition: p.songPos, VoiceLevels: p.voiceLevels, CPULoad: p.cpuload, Data: message}) TrySend(p.broker.ToModel, MsgToModel{HasPanicPlayerStatus: true, Panic: p.synth == nil, PlayerStatus: p.status, Data: message})
} }
func (p *Player) processNoteEvent(ev NoteEvent) { func (p *Player) processNoteEvent(ev NoteEvent) {
@ -408,7 +414,7 @@ func (p *Player) processNoteEvent(ev NoteEvent) {
return return
} }
p.voices[oldestVoice] = voice{triggerEvent: ev, sustain: true, samplesSinceEvent: 0} p.voices[oldestVoice] = voice{triggerEvent: ev, sustain: true, samplesSinceEvent: 0}
p.voiceLevels[oldestVoice] = 1.0 p.status.VoiceLevels[oldestVoice] = 1.0
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})
} }
@ -421,5 +427,5 @@ func (p *Player) updateCPULoad(duration time.Duration, frames int64) {
songtime := float64(frames) / 44100 songtime := float64(frames) / 44100
newload := realtime / songtime newload := realtime / songtime
alpha := math.Exp(-songtime) // smoothing factor, time constant of 1 second alpha := math.Exp(-songtime) // smoothing factor, time constant of 1 second
p.cpuload = float64(p.cpuload)*alpha + newload*(1-alpha) p.status.CPULoad = float64(p.status.CPULoad)*alpha + newload*(1-alpha)
} }