improved tracker behaviour when rewinding

better visuals for tracker window

sketched in idea for a tracker "replay" feature that will allow a
selection of audio output to be replayed. it works in principal but is
not currently used
This commit is contained in:
JetSetIlly 2023-04-08 22:00:04 +01:00
parent 04c21937b9
commit aa4da3d979
9 changed files with 310 additions and 112 deletions

View file

@ -437,7 +437,7 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
// playmode)
// add audio tracker
dbg.Tracker = tracker.NewTracker(dbg)
dbg.Tracker = tracker.NewTracker(dbg, dbg.Rewind)
dbg.vcs.TIA.Audio.SetTracker(dbg.Tracker)
// add plug monitor

View file

@ -137,10 +137,13 @@ type imguiColors struct {
AudioOscLine imgui.Vec4
// audio tracker
AudioTrackerHeader imgui.Vec4
AudioTrackerRow imgui.Vec4
AudioTrackerRowAlt imgui.Vec4
AudioTrackerRowHover imgui.Vec4
AudioTrackerHeader imgui.Vec4
AudioTrackerRow imgui.Vec4
AudioTrackerRowAlt imgui.Vec4
AudioTrackerRowSelected imgui.Vec4
AudioTrackerRowSelectedAlt imgui.Vec4
AudioTrackerRowHover imgui.Vec4
AudioTrackerBorder imgui.Vec4
// piano keys
PianoKeysBackground imgui.Vec4
@ -330,10 +333,13 @@ func newColors() *imguiColors {
AudioOscLine: imgui.Vec4{0.10, 0.97, 0.29, 1.0},
// audio tracker
AudioTrackerHeader: imgui.Vec4{0.09, 0.09, 0.09, 1.0},
AudioTrackerRow: imgui.Vec4{0.10, 0.10, 0.10, 1.0},
AudioTrackerRowAlt: imgui.Vec4{0.12, 0.12, 0.12, 1.0},
AudioTrackerRowHover: imgui.Vec4{0.12, 0.12, 0.15, 1.0},
AudioTrackerHeader: imgui.Vec4{0.12, 0.25, 0.25, 1.0},
AudioTrackerRow: imgui.Vec4{0.10, 0.15, 0.15, 1.0},
AudioTrackerRowAlt: imgui.Vec4{0.12, 0.17, 0.17, 1.0},
AudioTrackerRowSelected: imgui.Vec4{0.15, 0.25, 0.25, 1.0},
AudioTrackerRowSelectedAlt: imgui.Vec4{0.17, 0.27, 0.27, 1.0},
AudioTrackerRowHover: imgui.Vec4{0.14, 0.20, 0.20, 1.0},
AudioTrackerBorder: imgui.Vec4{0.12, 0.25, 0.25, 1.0},
// piano keys
PianoKeysBackground: imgui.Vec4{0.10, 0.09, 0.05, 1.0},

View file

@ -0,0 +1,52 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package sdlimgui
type imguiSelection struct {
a int
b int
}
func (sel *imguiSelection) clear() {
sel.a = -1
sel.b = -1
}
func (sel *imguiSelection) dragStart(i int) {
sel.a = i
sel.b = i
}
func (sel *imguiSelection) drag(i int) {
sel.b = i
}
func (sel imguiSelection) isSingle() bool {
return sel.a == sel.b
}
func (sel imguiSelection) inRange(i int) bool {
if sel.b < sel.a {
return i >= sel.b && i <= sel.a
}
return i >= sel.a && i <= sel.b
}
func (sel imguiSelection) limits() (int, int) {
if sel.a < sel.b {
return sel.a, sel.b
}
return sel.b, sel.a
}

View file

@ -40,6 +40,8 @@ type winTracker struct {
whiteKeys imgui.PackedColor
whiteKeysGap imgui.PackedColor
pianoKeysHeight float32
selection imguiSelection
}
func newWinTracker(img *SdlImgui) (window, error) {
@ -49,6 +51,7 @@ func newWinTracker(img *SdlImgui) (window, error) {
whiteKeys: imgui.PackedColorFromVec4(imgui.Vec4{1.0, 1.0, 0.90, 1.0}),
whiteKeysGap: imgui.PackedColorFromVec4(imgui.Vec4{0.2, 0.2, 0.2, 1.0}),
}
win.selection.clear()
return win, nil
}
@ -99,58 +102,44 @@ func (win *winTracker) debuggerDraw() {
}
func (win *winTracker) draw() {
imgui.PushStyleColor(imgui.StyleColorHeaderHovered, win.img.cols.DisasmHover)
imgui.PushStyleColor(imgui.StyleColorHeaderActive, win.img.cols.DisasmHover)
defer imgui.PopStyleColorV(2)
imgui.PushStyleColor(imgui.StyleColorTableHeaderBg, win.img.cols.AudioTrackerHeader)
defer imgui.PopStyleColor()
tableFlags := imgui.TableFlagsNone
tableFlags |= imgui.TableFlagsSizingFixedFit
tableFlags |= imgui.TableFlagsBordersV
tableFlags |= imgui.TableFlagsBordersOuter
const tableColumns = 14
tableSetupColumns := func() {
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 0, 0)
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 15, 1)
imgui.TableSetupColumnV("AUDC0", imgui.TableColumnFlagsNone, 40, 2)
imgui.TableSetupColumnV("Description", imgui.TableColumnFlagsNone, 80, 2)
imgui.TableSetupColumnV("AUDF0", imgui.TableColumnFlagsNone, 40, 3)
imgui.TableSetupColumnV("Note", imgui.TableColumnFlagsNone, 30, 3)
imgui.TableSetupColumnV("AUDV0", imgui.TableColumnFlagsNone, 40, 4)
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 0, 5)
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 15, 6)
imgui.TableSetupColumnV("AUDC1", imgui.TableColumnFlagsNone, 40, 2)
imgui.TableSetupColumnV("Description", imgui.TableColumnFlagsNone, 80, 2)
imgui.TableSetupColumnV("AUDF1", imgui.TableColumnFlagsNone, 40, 8)
imgui.TableSetupColumnV("Note", imgui.TableColumnFlagsNone, 30, 3)
imgui.TableSetupColumnV("AUDV1", imgui.TableColumnFlagsNone, 40, 9)
}
// I can't get the header of the table to freeze in the scroller so I'm
// fudging the effect by having a separate table just for the header.
if !imgui.BeginTableV("trackerHeader", tableColumns, tableFlags, imgui.Vec2{}, 0) {
return
}
tableSetupColumns()
imgui.TableHeadersRow()
imgui.EndTable()
imgui.PushStyleColor(imgui.StyleColorTableBorderLight, win.img.cols.AudioTrackerBorder)
imgui.PushStyleColor(imgui.StyleColorTableBorderStrong, win.img.cols.AudioTrackerBorder)
imgui.PushStyleColor(imgui.StyleColorHeaderHovered, win.img.cols.AudioTrackerRowHover)
imgui.PushStyleColor(imgui.StyleColorHeaderActive, win.img.cols.AudioTrackerRowHover)
defer imgui.PopStyleColorV(5)
// new child that contains the main scrollable table
if imgui.BeginChildV("##trackerscroller", imgui.Vec2{X: 0, Y: imguiRemainingWinHeight() - win.pianoKeysHeight}, false, 0) {
numEntries := len(win.img.lz.Tracker.Entries)
if numEntries == 0 {
imgui.Spacing()
imgui.Text("No audio output/changes yet")
imgui.Text("Audio tracker history is empty")
} else {
if imgui.BeginTableV("tracker", tableColumns, tableFlags, imgui.Vec2{}, 0) {
tableSetupColumns()
// tracker table
const numColumns = 12
flgs := imgui.TableFlagsScrollY
flgs |= imgui.TableFlagsSizingStretchProp
flgs |= imgui.TableFlagsNoHostExtendX
if imgui.BeginTableV("tracker", numColumns, flgs, imgui.Vec2{}, 0) {
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 15, 0)
imgui.TableSetupColumnV("AUDC0", imgui.TableColumnFlagsNone, 40, 1)
imgui.TableSetupColumnV("Distortion", imgui.TableColumnFlagsNone, 80, 2)
imgui.TableSetupColumnV("AUDF0", imgui.TableColumnFlagsNone, 40, 3)
imgui.TableSetupColumnV("Note", imgui.TableColumnFlagsNone, 30, 4)
imgui.TableSetupColumnV("AUDV0", imgui.TableColumnFlagsNone, 40, 5)
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 15, 6)
imgui.TableSetupColumnV("AUDC1", imgui.TableColumnFlagsNone, 40, 7)
imgui.TableSetupColumnV("Distortion", imgui.TableColumnFlagsNone, 80, 8)
imgui.TableSetupColumnV("AUDF1", imgui.TableColumnFlagsNone, 40, 9)
imgui.TableSetupColumnV("Note", imgui.TableColumnFlagsNone, 30, 10)
imgui.TableSetupColumnV("AUDV1", imgui.TableColumnFlagsNone, 40, 11)
imgui.TableSetupScrollFreeze(0, 1)
imgui.TableHeadersRow()
// altenate row colors at change of frame number
var lastEntry tracker.Entry
var lastEntryChan0 tracker.Entry
var lastEntryChan1 tracker.Entry
var altRowCol bool
@ -162,22 +151,35 @@ func (win *winTracker) draw() {
entry := win.img.lz.Tracker.Entries[i]
imgui.TableNextRow()
// flip row color
if entry.Coords.Frame != lastEntry.Coords.Frame {
altRowCol = !altRowCol
}
altRowCol = !altRowCol
if altRowCol {
imgui.TableSetBgColor(imgui.TableBgTargetRowBg0, win.img.cols.AudioTrackerRowAlt)
imgui.TableSetBgColor(imgui.TableBgTargetRowBg1, win.img.cols.AudioTrackerRowAlt)
if win.selection.inRange(i) {
imgui.TableSetBgColor(imgui.TableBgTargetRowBg0, win.img.cols.AudioTrackerRowSelectedAlt)
imgui.TableSetBgColor(imgui.TableBgTargetRowBg1, win.img.cols.AudioTrackerRowSelectedAlt)
}
} else {
imgui.TableSetBgColor(imgui.TableBgTargetRowBg0, win.img.cols.AudioTrackerRow)
imgui.TableSetBgColor(imgui.TableBgTargetRowBg1, win.img.cols.AudioTrackerRow)
if win.selection.inRange(i) {
imgui.TableSetBgColor(imgui.TableBgTargetRowBg0, win.img.cols.AudioTrackerRowSelected)
imgui.TableSetBgColor(imgui.TableBgTargetRowBg1, win.img.cols.AudioTrackerRowSelected)
}
}
// add selectable for row. the text for the selectable
// depends on which channel the tracker entry represents
imgui.TableNextColumn()
imgui.SelectableV("", false, imgui.SelectableFlagsSpanAllColumns, imgui.Vec2{0, 0})
if entry.Channel == 1 || !entry.IsMusical() {
imgui.PushStyleColor(imgui.StyleColorText, win.img.cols.Transparent)
}
imgui.SelectableV(string(fonts.MusicNote), false, imgui.SelectableFlagsSpanAllColumns, imgui.Vec2{0, 0})
if entry.Channel == 1 || !entry.IsMusical() {
imgui.PopStyleColor()
}
imguiTooltip(func() {
imgui.Text(fmt.Sprintf("Frame: %d", entry.Coords.Frame))
imgui.Text(fmt.Sprintf("Scanline: %d", entry.Coords.Scanline))
@ -185,19 +187,33 @@ func (win *winTracker) draw() {
}, true)
// context menu on right mouse button
if imgui.IsItemHovered() && imgui.IsMouseDown(1) {
imgui.OpenPopup(trackerContextMenuID)
win.contextMenu = entry.Coords
if imgui.IsItemHovered() {
if imgui.IsMouseClicked(0) {
win.selection.dragStart(i)
}
if imgui.IsMouseDragging(0, 0.0) {
win.selection.drag(i)
}
if imgui.IsMouseDown(1) {
imgui.OpenPopup(trackerContextMenuID)
win.contextMenu = entry.Coords
}
}
if entry.Coords == win.contextMenu {
if imgui.BeginPopup(trackerContextMenuID) {
if imgui.Selectable("Rewind to") {
if imgui.Selectable("Clear note history") {
win.img.dbg.PushFunction(win.img.dbg.Tracker.Reset)
}
if imgui.Selectable(fmt.Sprintf("Rewind (to %s)", entry.Coords)) {
win.img.dbg.GotoCoords(entry.Coords)
}
imgui.EndPopup()
}
}
// if tracker entry is for channel one then skip the
// first half-dozen columns and add whatever the 'note
// icon' is
if entry.Channel == 1 {
imgui.TableNextColumn()
imgui.TableNextColumn()
@ -205,21 +221,9 @@ func (win *winTracker) draw() {
imgui.TableNextColumn()
imgui.TableNextColumn()
imgui.TableNextColumn()
imgui.TableNextColumn()
}
// convert musical note into something worth showing
musicalNote := string(entry.MusicalNote)
imgui.TableNextColumn()
switch entry.MusicalNote {
case tracker.Noise:
musicalNote = ""
case tracker.Low:
musicalNote = ""
case tracker.Silence:
musicalNote = ""
default:
imgui.Text(fmt.Sprintf("%c", fonts.MusicNote))
if entry.IsMusical() {
imgui.Text(string(fonts.MusicNote))
}
}
imgui.TableNextColumn()
@ -229,7 +233,20 @@ func (win *winTracker) draw() {
imgui.TableNextColumn()
imgui.Text(fmt.Sprintf("%05b", entry.Registers.Freq&0x1f))
imgui.TableNextColumn()
imgui.Text(musicalNote)
// convert musical note into something worth showing
musicalNote := string(entry.MusicalNote)
switch entry.MusicalNote {
case tracker.Noise:
musicalNote = ""
case tracker.Low:
musicalNote = ""
case tracker.Silence:
musicalNote = ""
default:
imgui.Text(musicalNote)
}
imgui.TableNextColumn()
// volum column
@ -253,21 +270,39 @@ func (win *winTracker) draw() {
}
imgui.Text(fmt.Sprintf("%02d %c", entry.Registers.Volume&0x4b, volumeArrow))
// record last entry for comparison purposes next iteration
lastEntry = entry
}
}
imgui.EndTable()
}
if win.img.dbg.State() == govern.Running {
imgui.SetScrollHereY(1.0)
}
if win.img.dbg.State() == govern.Running {
imgui.SetScrollHereY(1.0)
imgui.EndTable()
}
}
}
imgui.EndChild()
win.pianoKeysHeight = win.drawPianoKeys()
// don't allow grabbing or movement of window when piano keys are clicked
if imgui.BeginChildV("##pianokeys", imgui.Vec2{}, false, imgui.WindowFlagsNoMove) {
win.pianoKeysHeight = win.drawPianoKeys()
}
imgui.EndChild()
}
// *** replay button not currently used because the replay mechanism is currently unsatisfactory ***
func (win *winTracker) drawReplayButton() {
// disable replay button as appropriate
s, e := win.selection.limits()
if s == -1 || e == -1 || win.img.dbg.State() != govern.Paused {
imgui.PushItemFlag(imgui.ItemFlagsDisabled, true)
imgui.PushStyleVarFloat(imgui.StyleVarAlpha, disabledAlpha)
defer imgui.PopItemFlag()
defer imgui.PopStyleVar()
}
if imgui.Button("Replay") {
// *** this should be run asynchronously ***
win.img.dbg.Tracker.Replay(s, e, win.img.audio)
}
}

View file

@ -22,8 +22,8 @@ import (
// Tracker implementations display or otherwise record the state of the audio
// registers for each channel.
type Tracker interface {
// Tick is called every video cycle
Tick(channel int, reg Registers)
// AudioTick is called every video cycle
AudioTick(channel int, reg Registers)
}
// SampleFreq represents the number of samples generated per second. This is
@ -101,11 +101,11 @@ func (au *Audio) Step() bool {
if au.tracker != nil {
// it's impossible for both channels to have changed in a single video cycle
if au.channel0.registersChanged {
au.tracker.Tick(0, au.channel0.registers)
au.tracker.AudioTick(0, au.channel0.registers)
au.channel0.registersChanged = false
au.registersChanged = true
} else if au.channel1.registersChanged {
au.tracker.Tick(1, au.channel1.registers)
au.tracker.AudioTick(1, au.channel1.registers)
au.channel1.registersChanged = false
au.registersChanged = true
}

View file

@ -42,7 +42,7 @@ func LookupDistortion(reg audio.Registers) string {
// same as 4
return "Pure"
case 6:
return "Puzzy"
return "Buzzy/Pure"
case 7:
return "Reedy"
case 8:
@ -52,7 +52,7 @@ func LookupDistortion(reg audio.Registers) string {
return "Reedy"
case 10:
// same as 6
return "Puzzy"
return "Buzzy/Pure"
case 11:
// same as 0
return "-"

View file

@ -23,7 +23,7 @@ const NoPianoKey = 0
// NoteToPianoKey converts the musical note to the corresponding piano key.
//
// Handle sharps but not flats.
// Black notes are negative values of the white note it is associated with.
func NoteToPianoKey(note MusicalNote) PianoKey {
switch note {
case "A#0":

73
tracker/replay.go Normal file
View file

@ -0,0 +1,73 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package tracker
import (
"fmt"
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/rewind"
)
// create replay emulation if it has not been created already
func (tr *Tracker) createReplayEmulation(mixer television.AudioMixer) error {
if tr.replayEmulation != nil {
return nil
}
tv, err := television.NewTelevision("AUTO")
if err != nil {
return fmt.Errorf("tracker: create replay emulation: %w", err)
}
tv.AddAudioMixer(mixer)
tr.replayEmulation, err = hardware.NewVCS(tv, nil)
if err != nil {
return fmt.Errorf("tracker: create replay emulation: %w", err)
}
return nil
}
// Replay audio from start to end indexes
func (tr *Tracker) Replay(start int, end int, mixer television.AudioMixer) error {
// the replay will run even if the master emulation is running. this may
// cause audible issues with the hardware audio mixing
tr.createReplayEmulation(mixer)
startState := tr.rewind.GetState(tr.entries[start].Coords.Frame)
if startState == nil {
return fmt.Errorf("tracker: replay: can't find rewind state for frame %d", tr.entries[start].Coords.Frame)
}
rewind.Plumb(tr.replayEmulation, startState, true)
tr.replayEmulation.DetatchEmulationExtras()
err := tr.replayEmulation.Run(func() (govern.State, error) {
if tr.replayEmulation.TV.GetCoords().Frame > tr.entries[end].Coords.Frame {
return govern.Ending, nil
}
return govern.Running, nil
})
if err != nil {
return fmt.Errorf("tracker: replay: %w", err)
}
return nil
}

View file

@ -17,62 +17,84 @@ package tracker
import (
"github.com/jetsetilly/gopher2600/debugger/govern"
"github.com/jetsetilly/gopher2600/hardware"
"github.com/jetsetilly/gopher2600/hardware/television"
"github.com/jetsetilly/gopher2600/hardware/television/coords"
"github.com/jetsetilly/gopher2600/hardware/tia/audio"
"github.com/jetsetilly/gopher2600/rewind"
)
// Emulation defines as much of the emulation we require access to.
type Rewind interface {
GetState(frame int) *rewind.State
}
type Emulation interface {
State() govern.State
TV() *television.Television
}
// Entry represents a single change of audio for a channel
type Entry struct {
Coords coords.TelevisionCoords
// the (TV) time the change occurred
Coords coords.TelevisionCoords
// which channel the Registers field refers to
Channel int
Registers audio.Registers
// description of the change. the Registers field by comparison contains the
// numeric information of the audio change
Distortion string
MusicalNote MusicalNote
PianoKey PianoKey
}
// IsMusical returns true if entry represents a musical note
func (e Entry) IsMusical() bool {
return e.MusicalNote != Noise && e.MusicalNote != Silence && e.MusicalNote != Low
}
const maxTrackerEntries = 1024
// Tracker implements the audio.Tracker interface and keeps a history of the
// audio registers over time.
// audio registers over time
type Tracker struct {
emulation Emulation
rewind Rewind
// list of tracker entries. length is capped to maxTrackerEntries
entries []Entry
// previous register values so we can compare to see whether the registers
// have change and thus worth recording
prevRegister [2]audio.Registers
// the most recent information for each channel. the entries do no need to
// have happened at the same time. ie. lastEntry[0] might refer to an audio
// change on frame 10 and lastEntry[1] on frame 20
lastEntry [2]Entry
// emulation used for replaying tracker entries. it wil be created on demand
// on the first call to Replay()
replayEmulation *hardware.VCS
}
const maxTrackerEntries = 1024
// NewTracker is the preferred method of initialisation for the Tracker type.
func NewTracker(emulation Emulation) *Tracker {
// NewTracker is the preferred method of initialisation for the Tracker type
func NewTracker(emulation Emulation, rewind Rewind) *Tracker {
return &Tracker{
emulation: emulation,
rewind: rewind,
entries: make([]Entry, 0, maxTrackerEntries),
}
}
// Reset removes all entries from tracker list.
// Reset removes all entries from tracker list
func (tr *Tracker) Reset() {
tr.entries = tr.entries[:0]
}
// Tick implements the audio.Tracker interface
func (tr *Tracker) Tick(channel int, reg audio.Registers) {
if tr.emulation.State() == govern.Rewinding {
return
}
// AudioTick implements the audio.Tracker interface
func (tr *Tracker) AudioTick(channel int, reg audio.Registers) {
changed := !audio.CmpRegisters(reg, tr.prevRegister[channel])
tr.prevRegister[channel] = reg
@ -87,23 +109,33 @@ func (tr *Tracker) Tick(channel int, reg audio.Registers) {
MusicalNote: LookupMusicalNote(tv, reg),
}
e.PianoKey = NoteToPianoKey(e.MusicalNote)
tr.entries = append(tr.entries, e)
if len(tr.entries) > maxTrackerEntries {
tr.entries = tr.entries[1:]
if tr.emulation.State() != govern.Rewinding {
// find splice point in tracker
splice := len(tr.entries) - 1
for splice > 0 && !coords.GreaterThan(e.Coords, tr.entries[splice].Coords) {
splice--
}
tr.entries = tr.entries[:splice+1]
// add new entry and limit number of entries
tr.entries = append(tr.entries, e)
if len(tr.entries) > maxTrackerEntries {
tr.entries = tr.entries[1:]
}
}
// store recent entry
// store entry in lastEntry reference
tr.lastEntry[channel] = e
}
}
// Copy makes a copy of the Tracker entries.
// Copy makes a copy of the Tracker entries
func (tr *Tracker) Copy() []Entry {
return tr.entries
}
// GetLast entry for channel.
// GetLast entry for channel
func (tr *Tracker) GetLast(channel int) Entry {
return tr.lastEntry[channel]
}