mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2024-05-20 13:48:02 -04:00
added tracker package and audio tracking window
This commit is contained in:
parent
66abac33ae
commit
b46dc6db58
|
@ -45,6 +45,7 @@ import (
|
|||
"github.com/jetsetilly/gopher2600/reflection"
|
||||
"github.com/jetsetilly/gopher2600/rewind"
|
||||
"github.com/jetsetilly/gopher2600/setup"
|
||||
"github.com/jetsetilly/gopher2600/tracker"
|
||||
"github.com/jetsetilly/gopher2600/userinput"
|
||||
)
|
||||
|
||||
|
@ -119,6 +120,9 @@ type Debugger struct {
|
|||
Rewind *rewind.Rewind
|
||||
deepPoking chan bool
|
||||
|
||||
// audio tracker stores audio state over time
|
||||
Tracker *tracker.Tracker
|
||||
|
||||
// catchupQuantum differs from the quantum field in that it only applies in
|
||||
// the catchupLoop (part of the rewind system). it is set just before the
|
||||
// rewind process is started.
|
||||
|
@ -246,6 +250,10 @@ func NewDebugger(tv *television.Television, scr gui.GUI, term terminal.Terminal,
|
|||
dbg.vcs.TV.AddFrameTrigger(dbg.Rewind)
|
||||
dbg.vcs.TV.AddFrameTrigger(dbg.ref)
|
||||
|
||||
// add audio tracker
|
||||
dbg.Tracker = tracker.NewTracker(dbg)
|
||||
dbg.vcs.TIA.Audio.SetTracker(dbg.Tracker)
|
||||
|
||||
// halting coordination
|
||||
dbg.halting, err = newHaltCoordination(dbg)
|
||||
if err != nil {
|
||||
|
@ -293,6 +301,11 @@ func (dbg *Debugger) VCS() emulation.VCS {
|
|||
return dbg.vcs
|
||||
}
|
||||
|
||||
// TV implements the emulation.Emulation interface.
|
||||
func (dbg *Debugger) TV() emulation.TV {
|
||||
return dbg.vcs.TV
|
||||
}
|
||||
|
||||
// Debugger implements the emulation.Emulation interface.
|
||||
func (dbg *Debugger) Debugger() emulation.Debugger {
|
||||
return dbg
|
||||
|
|
|
@ -32,6 +32,14 @@ const (
|
|||
Ending
|
||||
)
|
||||
|
||||
// TV is a minimal abstraction of the TV hardware. Exists mainly to avoid a
|
||||
// circular import to the hardware package.
|
||||
//
|
||||
// The only likely implementation of this interface is the
|
||||
// television.Television type.
|
||||
type TV interface {
|
||||
}
|
||||
|
||||
// VCS is a minimal abstraction of the VCS hardware. Exists mainly to avoid a
|
||||
// circular import to the hardware package.
|
||||
//
|
||||
|
@ -50,6 +58,7 @@ type Debugger interface {
|
|||
// Emulation defines the public functions required for a GUI implementation
|
||||
// (and possibly other things) to interface with the underlying emulator.
|
||||
type Emulation interface {
|
||||
TV() TV
|
||||
VCS() VCS
|
||||
Debugger() Debugger
|
||||
UserInput() chan userinput.Event
|
||||
|
|
|
@ -70,6 +70,9 @@ const (
|
|||
EmulationRewindForward = '\uf04e'
|
||||
EmulationRewindAtStart = '\uf049'
|
||||
EmulationRewindAtEnd = '\uf050'
|
||||
MusicNote = '\uf001'
|
||||
VolumeUp = '\uf062'
|
||||
VolumeDown = '\uf063'
|
||||
)
|
||||
|
||||
// The first and last unicode points used in the application. We use this to
|
||||
|
|
|
@ -97,6 +97,12 @@ type imguiColors struct {
|
|||
AudioOscBg imgui.Vec4
|
||||
AudioOscLine imgui.Vec4
|
||||
|
||||
// audio tracker
|
||||
AudioTrackerHeader imgui.Vec4
|
||||
AudioTrackerRow imgui.Vec4
|
||||
AudioTrackerRowAlt imgui.Vec4
|
||||
AudioTrackerRowHover imgui.Vec4
|
||||
|
||||
// timeline plot
|
||||
TimelineMarkers imgui.Vec4
|
||||
TimelineScanlines imgui.Vec4
|
||||
|
@ -228,6 +234,12 @@ func newColors() *imguiColors {
|
|||
AudioOscBg: imgui.Vec4{0.21, 0.29, 0.23, 1.0},
|
||||
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},
|
||||
|
||||
// timeline
|
||||
TimelineMarkers: imgui.Vec4{1.00, 1.00, 1.00, 1.0},
|
||||
TimelineScanlines: imgui.Vec4{0.79, 0.04, 0.04, 1.0},
|
||||
|
|
|
@ -53,6 +53,7 @@ type LazyValues struct {
|
|||
Collisions *LazyCollisions
|
||||
ChipRegisters *LazyChipRegisters
|
||||
Log *LazyLog
|
||||
Tracker *LazyTracker
|
||||
SaveKey *LazySaveKey
|
||||
Rewind *LazyRewind
|
||||
|
||||
|
@ -88,6 +89,7 @@ func NewLazyValues() *LazyValues {
|
|||
val.Collisions = newLazyCollisions(val)
|
||||
val.ChipRegisters = newLazyChipRegisters(val)
|
||||
val.Log = newLazyLog(val)
|
||||
val.Tracker = newLazyTracker(val)
|
||||
val.SaveKey = newLazySaveKey(val)
|
||||
val.Breakpoints = newLazyBreakpoints(val)
|
||||
val.Rewind = newLazyRewind(val)
|
||||
|
@ -143,6 +145,7 @@ func (val *LazyValues) Refresh() {
|
|||
val.Collisions.update()
|
||||
val.ChipRegisters.update()
|
||||
val.Log.update()
|
||||
val.Tracker.update()
|
||||
val.SaveKey.update()
|
||||
val.Rewind.update()
|
||||
val.Breakpoints.update()
|
||||
|
@ -173,6 +176,7 @@ func (val *LazyValues) Refresh() {
|
|||
val.Collisions.push()
|
||||
val.ChipRegisters.push()
|
||||
val.Log.push()
|
||||
val.Tracker.push()
|
||||
val.SaveKey.push()
|
||||
val.Rewind.push()
|
||||
val.Breakpoints.push()
|
||||
|
|
42
gui/sdlimgui/lazyvalues/tracker.go
Normal file
42
gui/sdlimgui/lazyvalues/tracker.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// 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 lazyvalues
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/tracker"
|
||||
)
|
||||
|
||||
// LazyTracker lazily accesses logging entries.
|
||||
type LazyTracker struct {
|
||||
val *LazyValues
|
||||
|
||||
entries atomic.Value // []tracker.Entry
|
||||
Entries []tracker.Entry
|
||||
}
|
||||
|
||||
func newLazyTracker(val *LazyValues) *LazyTracker {
|
||||
return &LazyTracker{val: val}
|
||||
}
|
||||
|
||||
func (lz *LazyTracker) push() {
|
||||
lz.entries.Store(lz.val.dbg.Tracker.Copy())
|
||||
}
|
||||
|
||||
func (lz *LazyTracker) update() {
|
||||
lz.Entries = lz.entries.Load().([]tracker.Entry)
|
||||
}
|
|
@ -106,6 +106,9 @@ var windowDefs = [...]windowDef{
|
|||
{create: newWinTIA, menu: menuEntry{group: menuVCS}, open: true},
|
||||
{create: newWinTimer, menu: menuEntry{group: menuVCS}, open: true},
|
||||
|
||||
// windows that appear in the "tools" menu
|
||||
{create: newWinTracker, menu: menuEntry{group: menuTools}, open: false},
|
||||
|
||||
// windows that appear in cartridge specific menu
|
||||
{create: newWinDPCregisters, menu: menuEntry{group: menuCart, restrictBus: menuRestrictRegister, restrictMapper: []string{"DPC"}}},
|
||||
{create: newWinDPCplusRegisters, menu: menuEntry{group: menuCart, restrictBus: menuRestrictRegister, restrictMapper: []string{"DPC+"}}},
|
||||
|
|
|
@ -30,6 +30,7 @@ type menuGroup int
|
|||
const (
|
||||
menuDebugger menuGroup = iota
|
||||
menuVCS
|
||||
menuTools
|
||||
menuCart
|
||||
menuCoProc
|
||||
menuPlusROM
|
||||
|
@ -88,7 +89,14 @@ func (wm *manager) drawMenu() {
|
|||
for _, m := range wm.menu[menuVCS] {
|
||||
wm.drawMenuEntry(m)
|
||||
}
|
||||
imgui.EndMenu()
|
||||
}
|
||||
|
||||
// tools menu
|
||||
if imgui.BeginMenu("Tools") {
|
||||
for _, m := range wm.menu[menuTools] {
|
||||
wm.drawMenuEntry(m)
|
||||
}
|
||||
imgui.EndMenu()
|
||||
}
|
||||
|
||||
|
|
|
@ -370,27 +370,14 @@ func (win *winDbgScr) drawCoordsLine() {
|
|||
imgui.Text("")
|
||||
imgui.SameLineV(0, 15)
|
||||
|
||||
imgui.Text("Frame:")
|
||||
imgui.SameLine()
|
||||
imgui.Text(fmt.Sprintf("%-4d", win.img.lz.TV.Coords.Frame))
|
||||
// TV coordinates
|
||||
imgui.Text(win.img.lz.TV.Coords.String())
|
||||
|
||||
imgui.SameLineV(0, 15)
|
||||
imgui.Text("Scanline:")
|
||||
imgui.SameLine()
|
||||
if win.img.lz.TV.Coords.Scanline > 999 {
|
||||
} else {
|
||||
imgui.Text(fmt.Sprintf("%-3d", win.img.lz.TV.Coords.Scanline))
|
||||
}
|
||||
imgui.SameLineV(0, 15)
|
||||
imgui.Text("Clock:")
|
||||
imgui.SameLine()
|
||||
imgui.Text(fmt.Sprintf("%-3d", win.img.lz.TV.Coords.Clock))
|
||||
|
||||
// include tv signal information
|
||||
// tv signal information
|
||||
imgui.SameLineV(0, 20)
|
||||
imgui.Text(win.img.lz.TV.LastSignal.String())
|
||||
|
||||
// unsynced
|
||||
// unsynced indicator
|
||||
if !win.scr.crit.frameInfo.VSynced {
|
||||
imgui.SameLineV(0, 20)
|
||||
imgui.Text("UNSYNCED")
|
||||
|
|
244
gui/sdlimgui/win_tracker.go
Normal file
244
gui/sdlimgui/win_tracker.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v4"
|
||||
"github.com/jetsetilly/gopher2600/emulation"
|
||||
"github.com/jetsetilly/gopher2600/gui/fonts"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
"github.com/jetsetilly/gopher2600/tracker"
|
||||
)
|
||||
|
||||
const winTrackerID = "Audio Tracker"
|
||||
|
||||
type winTracker struct {
|
||||
img *SdlImgui
|
||||
open bool
|
||||
|
||||
footerHeight float32
|
||||
contextMenu coords.TelevisionCoords
|
||||
}
|
||||
|
||||
func newWinTracker(img *SdlImgui) (window, error) {
|
||||
win := &winTracker{
|
||||
img: img,
|
||||
}
|
||||
return win, nil
|
||||
}
|
||||
|
||||
func (win *winTracker) init() {
|
||||
// nominal value to stop scrollbar appearing for a frame (it takes a
|
||||
// frame before we set the correct footerHeight value
|
||||
win.footerHeight = imgui.FrameHeight() + imgui.CurrentStyle().FramePadding().Y
|
||||
}
|
||||
|
||||
func (win *winTracker) id() string {
|
||||
return winTrackerID
|
||||
}
|
||||
|
||||
func (win *winTracker) isOpen() bool {
|
||||
return win.open
|
||||
}
|
||||
|
||||
func (win *winTracker) setOpen(open bool) {
|
||||
win.open = open
|
||||
}
|
||||
|
||||
const trackerContextMenuID = "trackerContextMenu"
|
||||
|
||||
func (win *winTracker) draw() {
|
||||
if !win.open {
|
||||
return
|
||||
}
|
||||
|
||||
imgui.SetNextWindowPosV(imgui.Vec2{574, 97}, imgui.ConditionFirstUseEver, imgui.Vec2{0, 0})
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{496, 614}, imgui.ConditionFirstUseEver)
|
||||
|
||||
imgui.BeginV(win.id(), &win.open, 0)
|
||||
defer imgui.End()
|
||||
|
||||
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()
|
||||
|
||||
numEntries := len(win.img.lz.Tracker.Entries)
|
||||
if numEntries == 0 {
|
||||
imgui.Text("No audio output/changes yet")
|
||||
} else {
|
||||
tableFlags := imgui.TableFlagsNone
|
||||
tableFlags |= imgui.TableFlagsSizingFixedFit
|
||||
tableFlags |= imgui.TableFlagsBordersV
|
||||
tableFlags |= imgui.TableFlagsBordersOuter
|
||||
|
||||
// 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", 10, tableFlags, imgui.Vec2{}, 0) {
|
||||
return
|
||||
}
|
||||
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 0, 0)
|
||||
imgui.TableSetupColumnV("0", imgui.TableColumnFlagsNone, 15, 1)
|
||||
imgui.TableSetupColumnV("Control", imgui.TableColumnFlagsNone, 100, 2)
|
||||
imgui.TableSetupColumnV("Freq", imgui.TableColumnFlagsNone, 40, 3)
|
||||
imgui.TableSetupColumnV("Vol", imgui.TableColumnFlagsNone, 30, 4)
|
||||
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 0, 5)
|
||||
imgui.TableSetupColumnV("1", imgui.TableColumnFlagsNone, 15, 6)
|
||||
imgui.TableSetupColumnV("Control", imgui.TableColumnFlagsNone, 100, 7)
|
||||
imgui.TableSetupColumnV("Freq", imgui.TableColumnFlagsNone, 40, 8)
|
||||
imgui.TableSetupColumnV("Vol", imgui.TableColumnFlagsNone, 30, 9)
|
||||
imgui.TableHeadersRow()
|
||||
imgui.EndTable()
|
||||
|
||||
// new child that contains the main scrollable table
|
||||
imgui.BeginChildV("##trackerscroller", imgui.Vec2{X: 0, Y: imguiRemainingWinHeight() - win.footerHeight}, false, 0)
|
||||
|
||||
if !imgui.BeginTableV("tracker", 10, tableFlags, imgui.Vec2{}, 0) {
|
||||
return
|
||||
}
|
||||
|
||||
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 0, 0)
|
||||
imgui.TableSetupColumnV("0", imgui.TableColumnFlagsNone, 15, 1)
|
||||
imgui.TableSetupColumnV("Control", imgui.TableColumnFlagsNone, 100, 2)
|
||||
imgui.TableSetupColumnV("Freq", imgui.TableColumnFlagsNone, 40, 3)
|
||||
imgui.TableSetupColumnV("Vol", imgui.TableColumnFlagsNone, 30, 4)
|
||||
imgui.TableSetupColumnV("", imgui.TableColumnFlagsNone, 0, 5)
|
||||
imgui.TableSetupColumnV("1", imgui.TableColumnFlagsNone, 15, 6)
|
||||
imgui.TableSetupColumnV("Control", imgui.TableColumnFlagsNone, 100, 7)
|
||||
imgui.TableSetupColumnV("Freq", imgui.TableColumnFlagsNone, 40, 8)
|
||||
imgui.TableSetupColumnV("Vol", imgui.TableColumnFlagsNone, 30, 9)
|
||||
|
||||
// altenate row colors at change of frame number
|
||||
var lastEntry tracker.Entry
|
||||
var lastEntryChan0 tracker.Entry
|
||||
var lastEntryChan1 tracker.Entry
|
||||
var altRowCol bool
|
||||
|
||||
var clipper imgui.ListClipper
|
||||
clipper.Begin(numEntries)
|
||||
for clipper.Step() {
|
||||
for i := clipper.DisplayStart; i < clipper.DisplayEnd; i++ {
|
||||
entry := win.img.lz.Tracker.Entries[i]
|
||||
|
||||
imgui.TableNextRow()
|
||||
|
||||
// flip row color
|
||||
if entry.Coords.Frame != lastEntry.Coords.Frame {
|
||||
altRowCol = !altRowCol
|
||||
}
|
||||
|
||||
if altRowCol {
|
||||
imgui.TableSetBgColor(imgui.TableBgTargetRowBg0, win.img.cols.AudioTrackerRowAlt)
|
||||
imgui.TableSetBgColor(imgui.TableBgTargetRowBg1, win.img.cols.AudioTrackerRowAlt)
|
||||
} else {
|
||||
imgui.TableSetBgColor(imgui.TableBgTargetRowBg0, win.img.cols.AudioTrackerRow)
|
||||
imgui.TableSetBgColor(imgui.TableBgTargetRowBg1, win.img.cols.AudioTrackerRow)
|
||||
}
|
||||
|
||||
imgui.TableNextColumn()
|
||||
imgui.SelectableV("", false, imgui.SelectableFlagsSpanAllColumns, imgui.Vec2{0, 0})
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.BeginTooltip()
|
||||
imgui.Text(fmt.Sprintf("Frame: %d", entry.Coords.Frame))
|
||||
imgui.Text(fmt.Sprintf("Scanline: %d", entry.Coords.Scanline))
|
||||
imgui.Text(fmt.Sprintf("Clock: %d", entry.Coords.Clock))
|
||||
imgui.EndTooltip()
|
||||
}
|
||||
// context menu on right mouse button
|
||||
if imgui.IsItemHovered() && imgui.IsMouseDown(1) {
|
||||
imgui.OpenPopup(trackerContextMenuID)
|
||||
win.contextMenu = entry.Coords
|
||||
}
|
||||
if entry.Coords == win.contextMenu {
|
||||
if imgui.BeginPopup(trackerContextMenuID) {
|
||||
if imgui.Selectable("Rewind to") {
|
||||
win.img.dbg.PushGoto(entry.Coords)
|
||||
}
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
if entry.Channel == 1 {
|
||||
imgui.TableNextColumn()
|
||||
imgui.TableNextColumn()
|
||||
imgui.TableNextColumn()
|
||||
imgui.TableNextColumn()
|
||||
imgui.TableNextColumn()
|
||||
}
|
||||
|
||||
imgui.TableNextColumn()
|
||||
if entry.MusicalNote != tracker.NoMusicalNote {
|
||||
imgui.Text(fmt.Sprintf("%c", fonts.MusicNote))
|
||||
}
|
||||
imgui.TableNextColumn()
|
||||
imgui.Text(fmt.Sprintf("(%01x) %s", entry.Registers.Control&0x0f, entry.Distortion))
|
||||
imgui.TableNextColumn()
|
||||
imgui.Text(fmt.Sprintf("%05b", entry.Registers.Freq))
|
||||
imgui.TableNextColumn()
|
||||
|
||||
// volum column
|
||||
var volumeArrow rune
|
||||
|
||||
// compare with previous entry for the channel
|
||||
if entry.Channel == 0 {
|
||||
if entry.Registers.Volume > lastEntryChan0.Registers.Volume {
|
||||
volumeArrow = fonts.VolumeUp
|
||||
} else if entry.Registers.Volume < lastEntryChan0.Registers.Volume {
|
||||
volumeArrow = fonts.VolumeDown
|
||||
}
|
||||
lastEntryChan0 = entry
|
||||
} else {
|
||||
if entry.Registers.Volume > lastEntryChan1.Registers.Volume {
|
||||
volumeArrow = fonts.VolumeUp
|
||||
} else if entry.Registers.Volume < lastEntryChan1.Registers.Volume {
|
||||
volumeArrow = fonts.VolumeDown
|
||||
}
|
||||
lastEntryChan1 = entry
|
||||
}
|
||||
|
||||
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.emulation.State() == emulation.Running {
|
||||
imgui.SetScrollHereY(1.0)
|
||||
}
|
||||
|
||||
imgui.EndChild()
|
||||
|
||||
win.footerHeight = imguiMeasureHeight(func() {
|
||||
imgui.Spacing()
|
||||
|
||||
imgui.AlignTextToFramePadding()
|
||||
imgui.Text(fmt.Sprintf("Last change: %s", lastEntry.Coords))
|
||||
|
||||
imgui.SameLineV(0, 15)
|
||||
if imgui.Button("Rewind to") {
|
||||
win.img.dbg.PushGoto(lastEntry.Coords)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@
|
|||
// recording/playback and general information.
|
||||
package coords
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TelevisionCoords represents the state of the TV at any moment in time. It
|
||||
// can be used when all three values need to be stored or passed around.
|
||||
//
|
||||
|
@ -30,6 +32,11 @@ type TelevisionCoords struct {
|
|||
Clock int
|
||||
}
|
||||
|
||||
func (c TelevisionCoords) String() string {
|
||||
return fmt.Sprintf("Frame: %-4d Scanline: %-3d Clock: %-3d",
|
||||
c.Frame, c.Scanline, c.Clock)
|
||||
}
|
||||
|
||||
// Equal compares two instances of TelevisionCoords and return true if
|
||||
// both are equal.
|
||||
func Equal(A, B TelevisionCoords) bool {
|
||||
|
|
|
@ -19,6 +19,13 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// 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, changed bool)
|
||||
}
|
||||
|
||||
// SampleFreq represents the number of samples generated per second. This is
|
||||
// the 30Khz reference frequency desribed in the Stella Programmer's Guide.
|
||||
const SampleFreq = 31403
|
||||
|
@ -51,6 +58,8 @@ type Audio struct {
|
|||
// the volume output for each channel
|
||||
Vol0 uint8
|
||||
Vol1 uint8
|
||||
|
||||
tracker Tracker
|
||||
}
|
||||
|
||||
// NewAudio is the preferred method of initialisation for the Audio sub-system.
|
||||
|
@ -58,6 +67,20 @@ func NewAudio() *Audio {
|
|||
return &Audio{}
|
||||
}
|
||||
|
||||
func (au *Audio) Reset() {
|
||||
au.clock114 = 0
|
||||
au.clock342 = 0
|
||||
au.channel0 = channel{}
|
||||
au.channel1 = channel{}
|
||||
au.Vol0 = 0
|
||||
au.Vol1 = 0
|
||||
}
|
||||
|
||||
// SetTracker adds a Tracker implementation to the Audio sub-system.
|
||||
func (au *Audio) SetTracker(tracker Tracker) {
|
||||
au.tracker = tracker
|
||||
}
|
||||
|
||||
// Snapshot creates a copy of the TIA Audio sub-system in its current state.
|
||||
func (au *Audio) Snapshot() *Audio {
|
||||
n := *au
|
||||
|
@ -76,6 +99,11 @@ func (au *Audio) String() string {
|
|||
// Step the audio on one TIA clock. The step will be filtered to produce a
|
||||
// 30Khz clock.
|
||||
func (au *Audio) Step() bool {
|
||||
if au.tracker != nil {
|
||||
au.tracker.Tick(0, au.channel0.registers, au.channel0.registersChanged)
|
||||
au.tracker.Tick(1, au.channel1.registers, au.channel1.registersChanged)
|
||||
}
|
||||
|
||||
// the reference frequency for all sound produced by the TIA is 30Khz. this
|
||||
// is the 3.58Mhz clock, which the TIA operates at, divided by 114 (see
|
||||
// declaration). Mix() is called every video cycle and we return
|
||||
|
@ -103,5 +131,8 @@ func (au *Audio) Step() bool {
|
|||
au.Vol0 = au.channel0.actualVol
|
||||
au.Vol1 = au.channel1.actualVol
|
||||
|
||||
au.channel0.registersChanged = false
|
||||
au.channel1.registersChanged = false
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -15,24 +15,9 @@
|
|||
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type channel struct {
|
||||
// each channel has three registers that control its output. from the
|
||||
// "Stella Programmer's Guide":
|
||||
//
|
||||
// "Each audio circuit has three registers that control a noise-tone
|
||||
// generator (what kind of sound), a frequency selection (high or low pitch
|
||||
// of the sound), and a volume control."
|
||||
//
|
||||
// not all the bits are used in each register. the comments below indicate
|
||||
// how many of the least-significant bits are used.
|
||||
regControl uint8 // 4 bit
|
||||
regFreq uint8 // 5 bit
|
||||
regVolume uint8 // 4 bit
|
||||
registers Registers
|
||||
registersChanged bool
|
||||
|
||||
// which bit of each polynomial counter to use next
|
||||
poly4ct int
|
||||
|
@ -64,9 +49,7 @@ type channel struct {
|
|||
}
|
||||
|
||||
func (ch *channel) String() string {
|
||||
s := strings.Builder{}
|
||||
s.WriteString(fmt.Sprintf("%04b @ %05b ^ %04b", ch.regControl, ch.regFreq, ch.regVolume))
|
||||
return s.String()
|
||||
return ch.registers.String()
|
||||
}
|
||||
|
||||
// tick should be called at a frequency of 30Khz. when the 10Khz clock is
|
||||
|
@ -99,14 +82,14 @@ func (ch *channel) tick(tenKhz bool) {
|
|||
}
|
||||
|
||||
// check for clock tick
|
||||
if (ch.regControl&0x02 == 0x0) ||
|
||||
((ch.regControl&0x01 == 0x0) && div31[ch.poly5ct] != 0) ||
|
||||
((ch.regControl&0x01 == 0x1) && poly5bit[ch.poly5ct] != 0) ||
|
||||
((ch.regControl&0x0f == 0xf) && poly5bit[ch.poly5ct] != prevBit5) {
|
||||
if ch.regControl&0x04 == 0x04 {
|
||||
if (ch.registers.Control&0x02 == 0x0) ||
|
||||
((ch.registers.Control&0x01 == 0x0) && div31[ch.poly5ct] != 0) ||
|
||||
((ch.registers.Control&0x01 == 0x1) && poly5bit[ch.poly5ct] != 0) ||
|
||||
((ch.registers.Control&0x0f == 0xf) && poly5bit[ch.poly5ct] != prevBit5) {
|
||||
if ch.registers.Control&0x04 == 0x04 {
|
||||
// use pure clock
|
||||
|
||||
if ch.regControl&0x0f == 0x0f {
|
||||
if ch.registers.Control&0x0f == 0x0f {
|
||||
// use poly5/div3
|
||||
if poly5bit[ch.poly5ct] != prevBit5 {
|
||||
ch.div3ct++
|
||||
|
@ -117,7 +100,7 @@ func (ch *channel) tick(tenKhz bool) {
|
|||
if ch.actualVol != 0 {
|
||||
ch.actualVol = 0
|
||||
} else {
|
||||
ch.actualVol = ch.regVolume
|
||||
ch.actualVol = ch.registers.Volume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,13 +109,13 @@ func (ch *channel) tick(tenKhz bool) {
|
|||
if ch.actualVol != 0 {
|
||||
ch.actualVol = 0
|
||||
} else {
|
||||
ch.actualVol = ch.regVolume
|
||||
ch.actualVol = ch.registers.Volume
|
||||
}
|
||||
}
|
||||
} else if ch.regControl&0x08 == 0x08 {
|
||||
} else if ch.registers.Control&0x08 == 0x08 {
|
||||
// use poly poly5/poly9
|
||||
|
||||
if ch.regControl == 0x08 {
|
||||
if ch.registers.Control == 0x08 {
|
||||
// use poly9
|
||||
ch.poly9ct++
|
||||
if ch.poly9ct >= len(poly9bit) {
|
||||
|
@ -141,22 +124,22 @@ func (ch *channel) tick(tenKhz bool) {
|
|||
|
||||
// toggle volume
|
||||
if poly9bit[ch.poly9ct] != 0 {
|
||||
ch.actualVol = ch.regVolume
|
||||
ch.actualVol = ch.registers.Volume
|
||||
} else {
|
||||
ch.actualVol = 0
|
||||
}
|
||||
} else if ch.regControl&0x02 != 0 {
|
||||
if ch.actualVol != 0 || ch.regControl&0x01 == 0x01 {
|
||||
} else if ch.registers.Control&0x02 != 0 {
|
||||
if ch.actualVol != 0 || ch.registers.Control&0x01 == 0x01 {
|
||||
ch.actualVol = 0
|
||||
} else {
|
||||
ch.actualVol = ch.regVolume
|
||||
ch.actualVol = ch.registers.Volume
|
||||
}
|
||||
} else {
|
||||
// use poly5. we've already bumped poly5 counter forward
|
||||
|
||||
// toggle volume
|
||||
if poly5bit[ch.poly5ct] == 1 {
|
||||
ch.actualVol = ch.regVolume
|
||||
ch.actualVol = ch.registers.Volume
|
||||
} else {
|
||||
ch.actualVol = 0
|
||||
}
|
||||
|
@ -169,7 +152,7 @@ func (ch *channel) tick(tenKhz bool) {
|
|||
}
|
||||
|
||||
if poly4bit[ch.poly4ct] == 1 {
|
||||
ch.actualVol = ch.regVolume
|
||||
ch.actualVol = ch.registers.Volume
|
||||
} else {
|
||||
ch.actualVol = 0
|
||||
}
|
||||
|
|
|
@ -16,9 +16,37 @@
|
|||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jetsetilly/gopher2600/hardware/memory/bus"
|
||||
)
|
||||
|
||||
// each channel has three registers that control its output. from the
|
||||
// "Stella Programmer's Guide":
|
||||
//
|
||||
// "Each audio circuit has three registers that control a noise-tone
|
||||
// generator (what kind of sound), a frequency selection (high or low pitch
|
||||
// of the sound), and a volume control."
|
||||
//
|
||||
// not all the bits are used in each register. the comments below indicate
|
||||
// how many of the least-significant bits are used.
|
||||
type Registers struct {
|
||||
Control uint8 // 4 bit
|
||||
Freq uint8 // 5 bit
|
||||
Volume uint8 // 4 bit
|
||||
}
|
||||
|
||||
func (reg Registers) String() string {
|
||||
return fmt.Sprintf("%04b @ %05b ^ %04b", reg.Control, reg.Freq, reg.Volume)
|
||||
}
|
||||
|
||||
// CmpRegisters returns true if the two registers contain the same values
|
||||
func CmpRegisters(a Registers, b Registers) bool {
|
||||
return a.Control&0x4b == b.Control&0x4b &&
|
||||
a.Freq&0x5b == b.Freq&0x5b &&
|
||||
a.Volume&0x4b == b.Volume&0x4b
|
||||
}
|
||||
|
||||
// ReadMemRegisters checks the TIA memory for changes to registers that are
|
||||
// interesting to the audio sub-system
|
||||
//
|
||||
|
@ -26,22 +54,22 @@ import (
|
|||
func (au *Audio) ReadMemRegisters(data bus.ChipData) bool {
|
||||
switch data.Name {
|
||||
case "AUDC0":
|
||||
au.channel0.regControl = data.Value & 0x0f
|
||||
au.channel0.registers.Control = data.Value & 0x0f
|
||||
au.channel0.reactAUDCx()
|
||||
case "AUDC1":
|
||||
au.channel1.regControl = data.Value & 0x0f
|
||||
au.channel1.registers.Control = data.Value & 0x0f
|
||||
au.channel1.reactAUDCx()
|
||||
case "AUDF0":
|
||||
au.channel0.regFreq = data.Value & 0x1f
|
||||
au.channel0.registers.Freq = data.Value & 0x1f
|
||||
au.channel0.reactAUDCx()
|
||||
case "AUDF1":
|
||||
au.channel1.regFreq = data.Value & 0x1f
|
||||
au.channel1.registers.Freq = data.Value & 0x1f
|
||||
au.channel1.reactAUDCx()
|
||||
case "AUDV0":
|
||||
au.channel0.regVolume = data.Value & 0x0f
|
||||
au.channel0.registers.Volume = data.Value & 0x0f
|
||||
au.channel0.reactAUDCx()
|
||||
case "AUDV1":
|
||||
au.channel1.regVolume = data.Value & 0x0f
|
||||
au.channel1.registers.Volume = data.Value & 0x0f
|
||||
au.channel1.reactAUDCx()
|
||||
default:
|
||||
return true
|
||||
|
@ -52,17 +80,19 @@ func (au *Audio) ReadMemRegisters(data bus.ChipData) bool {
|
|||
|
||||
// changing the value of an AUDx registers causes some side effect.
|
||||
func (ch *channel) reactAUDCx() {
|
||||
ch.registersChanged = true
|
||||
|
||||
freq := uint8(0)
|
||||
|
||||
if ch.regControl == 0x00 || ch.regControl == 0x0b {
|
||||
ch.actualVol = ch.regVolume
|
||||
if ch.registers.Control == 0x00 || ch.registers.Control == 0x0b {
|
||||
ch.actualVol = ch.registers.Volume
|
||||
} else {
|
||||
freq = ch.regFreq
|
||||
freq = ch.registers.Freq
|
||||
|
||||
// from TIASound.c: when bits D2 and D3 are set, the input source is
|
||||
// switched to the 1.19MHz clock, so the '30KHz' source clock is
|
||||
// reduced to approximately 10KHz."
|
||||
ch.useTenKhz = ch.regControl&0b1100 == 0b1100
|
||||
ch.useTenKhz = ch.registers.Control&0b1100 == 0b1100
|
||||
}
|
||||
|
||||
if ch.freq != freq {
|
||||
|
|
|
@ -144,10 +144,17 @@ func (vcs *VCS) Reset() error {
|
|||
}
|
||||
|
||||
// easiest way of resetting the TIA is to just create new one
|
||||
//
|
||||
// 27/10/21 - we do want to save the audio though in order to keep any
|
||||
// attached trackers
|
||||
//
|
||||
// TODO: proper Reset() function for the TIA
|
||||
audio := vcs.TIA.Audio
|
||||
vcs.TIA, err = tia.NewTIA(vcs.TV, vcs.Mem.TIA, vcs.RIOT.Ports, vcs.CPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vcs.TIA.Audio = audio
|
||||
|
||||
// other areas of the VCS are simply reset because the emulation may have
|
||||
// altered the part of the state that we do *not* want to reset. notably,
|
||||
|
|
|
@ -74,6 +74,11 @@ func (pl *playmode) VCS() emulation.VCS {
|
|||
return pl.vcs
|
||||
}
|
||||
|
||||
// TV implements the emulation.Emulation interface.
|
||||
func (pl *playmode) TV() emulation.TV {
|
||||
return pl.vcs.TV
|
||||
}
|
||||
|
||||
// Debugger implements the emulation.Emulation interface.
|
||||
func (pl *playmode) Debugger() emulation.Debugger {
|
||||
return nil
|
||||
|
|
171
tracker/conversions.go
Normal file
171
tracker/conversions.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
// 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 (
|
||||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
"github.com/jetsetilly/gopher2600/hardware/tia/audio"
|
||||
)
|
||||
|
||||
// LookupDistortion converts the control register value into a text
|
||||
// description.
|
||||
//
|
||||
// Descriptions taken from Random Terrain's "The Atari 2600 Music and Sound
|
||||
// Page"
|
||||
//
|
||||
// https://www.randomterrain.com/atari-2600-memories-music-and-sound.html
|
||||
func LookupDistortion(reg audio.Registers) string {
|
||||
switch reg.Control {
|
||||
case 0:
|
||||
return "-"
|
||||
case 1:
|
||||
return "Buzzy"
|
||||
case 2:
|
||||
return "Rumble"
|
||||
case 3:
|
||||
return "Flangy"
|
||||
case 4:
|
||||
return "Pure"
|
||||
case 5:
|
||||
// same as 4
|
||||
return "Pure"
|
||||
case 6:
|
||||
return "Puzzy"
|
||||
case 7:
|
||||
return "Reedy"
|
||||
case 8:
|
||||
return "White Noise"
|
||||
case 9:
|
||||
// same as 7
|
||||
return "Reedy"
|
||||
case 10:
|
||||
// same as 6
|
||||
return "Puzzy"
|
||||
case 11:
|
||||
// same as 0
|
||||
return "-"
|
||||
case 12:
|
||||
return "Pure (low)"
|
||||
case 13:
|
||||
// same as 12
|
||||
return "Pure (low)"
|
||||
case 14:
|
||||
return "Electronic"
|
||||
case 15:
|
||||
return "Electronic"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// MusicalNote defines the musical note (C#, D, D#, etc.) of an TIA audio
|
||||
// channel register group.
|
||||
type MusicalNote string
|
||||
|
||||
// Values that the MusicalNote can be. For now we only have the two
|
||||
// possibilities.
|
||||
const (
|
||||
NoMusicalNote = MusicalNote("-")
|
||||
IsMusicalNote = MusicalNote("M")
|
||||
)
|
||||
|
||||
// LookupMusicalNote converts the current register values for a channel into a
|
||||
// musical note.
|
||||
//
|
||||
// Descriptions taken from Random Terrain's "The Atari 2600 Music and Sound
|
||||
// Page"
|
||||
//
|
||||
// https://www.randomterrain.com/atari-2600-memories-music-and-sound.html
|
||||
func LookupMusicalNote(tv *television.Television, reg audio.Registers) MusicalNote {
|
||||
switch tv.GetFrameInfo().Spec.ID {
|
||||
case "NTSC":
|
||||
switch reg.Control {
|
||||
case 1:
|
||||
return IsMusicalNote
|
||||
|
||||
case 2:
|
||||
fallthrough
|
||||
case 3:
|
||||
return IsMusicalNote
|
||||
|
||||
case 4:
|
||||
fallthrough
|
||||
case 5:
|
||||
return IsMusicalNote
|
||||
|
||||
case 6:
|
||||
fallthrough
|
||||
case 7:
|
||||
fallthrough
|
||||
case 9:
|
||||
fallthrough
|
||||
case 10:
|
||||
return IsMusicalNote
|
||||
|
||||
case 8:
|
||||
return IsMusicalNote
|
||||
|
||||
case 12:
|
||||
fallthrough
|
||||
case 13:
|
||||
return IsMusicalNote
|
||||
|
||||
case 14:
|
||||
fallthrough
|
||||
case 15:
|
||||
}
|
||||
case "PAL":
|
||||
switch reg.Control {
|
||||
case 1:
|
||||
return IsMusicalNote
|
||||
|
||||
case 2:
|
||||
fallthrough
|
||||
case 3:
|
||||
return IsMusicalNote
|
||||
|
||||
case 4:
|
||||
fallthrough
|
||||
case 5:
|
||||
return IsMusicalNote
|
||||
|
||||
case 6:
|
||||
fallthrough
|
||||
case 7:
|
||||
fallthrough
|
||||
case 9:
|
||||
fallthrough
|
||||
case 10:
|
||||
return IsMusicalNote
|
||||
|
||||
case 8:
|
||||
return IsMusicalNote
|
||||
|
||||
case 12:
|
||||
fallthrough
|
||||
case 13:
|
||||
return IsMusicalNote
|
||||
|
||||
case 14:
|
||||
fallthrough
|
||||
case 15:
|
||||
return IsMusicalNote
|
||||
}
|
||||
}
|
||||
|
||||
// control of 0 and 11
|
||||
return NoMusicalNote
|
||||
}
|
21
tracker/doc.go
Normal file
21
tracker/doc.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
// 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/>.
|
||||
|
||||
// Packate tracker implements the audio.Tracker interface and can be used to
|
||||
// record changes in the state of the TIA audio.
|
||||
//
|
||||
// Also contains functions that can convert register values into meaningful
|
||||
// descriptions.
|
||||
package tracker
|
91
tracker/tracker.go
Normal file
91
tracker/tracker.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
// 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 (
|
||||
"github.com/jetsetilly/gopher2600/emulation"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television"
|
||||
"github.com/jetsetilly/gopher2600/hardware/television/coords"
|
||||
"github.com/jetsetilly/gopher2600/hardware/tia/audio"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Coords coords.TelevisionCoords
|
||||
Channel int
|
||||
Registers audio.Registers
|
||||
|
||||
Distortion string
|
||||
MusicalNote MusicalNote
|
||||
}
|
||||
|
||||
// Tracker implements the audio.Tracker interface and keeps a history of the
|
||||
// audio registers over time.
|
||||
type Tracker struct {
|
||||
emulation emulation.Emulation
|
||||
|
||||
entries []Entry
|
||||
|
||||
// previous register values so we can compare to see whether the registers
|
||||
// have change and thus worth recording
|
||||
prevRegister0 audio.Registers
|
||||
prevRegister1 audio.Registers
|
||||
}
|
||||
|
||||
// NewTracker is the preferred method of initialisation for the Tracker type.
|
||||
func NewTracker(emulation emulation.Emulation) *Tracker {
|
||||
return &Tracker{
|
||||
emulation: emulation,
|
||||
entries: make([]Entry, 0, 1024),
|
||||
}
|
||||
}
|
||||
|
||||
// Tick implements the audio.Tracker interface
|
||||
func (tr *Tracker) Tick(channel int, reg audio.Registers, changed bool) {
|
||||
if tr.emulation.State() == emulation.Rewinding {
|
||||
return
|
||||
}
|
||||
|
||||
if changed {
|
||||
switch channel {
|
||||
case 0:
|
||||
changed = !audio.CmpRegisters(reg, tr.prevRegister0)
|
||||
tr.prevRegister0 = reg
|
||||
case 1:
|
||||
changed = !audio.CmpRegisters(reg, tr.prevRegister1)
|
||||
tr.prevRegister1 = reg
|
||||
}
|
||||
|
||||
tv := tr.emulation.TV().(*television.Television)
|
||||
|
||||
if changed {
|
||||
tr.entries = append(tr.entries, Entry{
|
||||
Coords: tv.GetCoords(),
|
||||
Channel: channel,
|
||||
Registers: reg,
|
||||
Distortion: LookupDistortion(reg),
|
||||
MusicalNote: LookupMusicalNote(tv, reg),
|
||||
})
|
||||
if len(tr.entries) > 1024 {
|
||||
tr.entries = tr.entries[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy makes a copy of the Tracker entries.
|
||||
func (tr *Tracker) Copy() []Entry {
|
||||
return tr.entries
|
||||
}
|
Loading…
Reference in a new issue