added tracker package and audio tracking window

This commit is contained in:
JetSetIlly 2021-10-27 15:18:29 +01:00
parent 66abac33ae
commit b46dc6db58
19 changed files with 734 additions and 63 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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},

View file

@ -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()

View 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)
}

View file

@ -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+"}}},

View file

@ -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()
}

View file

@ -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
View 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)
}
})
}
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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,

View file

@ -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
View 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
View 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
View 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
}