- implemented audio
    - using Ron Fries' method as the basis
    - removed sample playback - performance of Fries' method is good
	enough that we'll never need it again
    - sdl audio routines using QueueAudio() - maybe better if we use
	callbacks?
    - sketched wavwriter AudioMixer - not usable yet
This commit is contained in:
steve 2019-12-01 10:13:36 +00:00
parent 261a379ae0
commit 9f50f3c77e
20 changed files with 605 additions and 147 deletions

View file

@ -22,6 +22,7 @@ import (
// debugger keywords // debugger keywords
const ( const (
cmdAudio = "AUDIO"
cmdBall = "BALL" cmdBall = "BALL"
cmdBreak = "BREAK" cmdBreak = "BREAK"
cmdCPU = "CPU" cmdCPU = "CPU"
@ -70,6 +71,7 @@ const (
const cmdHelp = "HELP" const cmdHelp = "HELP"
var commandTemplate = []string{ var commandTemplate = []string{
cmdAudio,
cmdBall, cmdBall,
cmdBreak + " [%S %N|%N] {& %S %N|& %N}", cmdBreak + " [%S %N|%N] {& %S %N|& %N}",
cmdCPU + " (SET [PC|A|X|Y|SP] [%N]|BUG (ON|OFF))", cmdCPU + " (SET [PC|A|X|Y|SP] [%N]|BUG (ON|OFF))",
@ -913,6 +915,9 @@ func (dbg *Debugger) enactCommand(tokens *commandline.Tokens, interactive bool)
dbg.printInstrument(dbg.vcs.TIA) dbg.printInstrument(dbg.vcs.TIA)
} }
case cmdAudio:
dbg.printInstrument(dbg.vcs.TIA.Audio)
case cmdTV: case cmdTV:
option, present := tokens.Get() option, present := tokens.Get()
if present { if present {

View file

@ -90,7 +90,6 @@ const (
UnwritableAddress UnwritableAddress
UnpokeableAddress UnpokeableAddress
UnpeekableAddress UnpeekableAddress
UnrecognisedAddress
// cartridges // cartridges
CartridgeError CartridgeError
@ -107,6 +106,9 @@ const (
// screen digest // screen digest
ScreenDigest ScreenDigest
// wavwriter
WavWriter
// gui // gui
UnsupportedGUIRequest UnsupportedGUIRequest
SDL SDL

View file

@ -72,12 +72,11 @@ var messages = map[Errno]string{
InvalidOperationMidInstruction: "cpu error: invalid operation mid-instruction (%v)", InvalidOperationMidInstruction: "cpu error: invalid operation mid-instruction (%v)",
// memory // memory
MemoryError: "memory error: %v", MemoryError: "memory error: %v",
UnreadableAddress: "memory error: memory location is not readable (%#04x)", UnreadableAddress: "memory error: memory location is not readable (%#04x)",
UnwritableAddress: "memory error: memory location is not writable (%#04x)", UnwritableAddress: "memory error: memory location is not writable (%#04x)",
UnpokeableAddress: "memory error: cannot poke address (%#04x)", UnpokeableAddress: "memory error: cannot poke address (%v)",
UnpeekableAddress: "memory error: cannot peek address (%#04x)", UnpeekableAddress: "memory error: cannot peek address (%v)",
UnrecognisedAddress: "memory error: address unrecognised (%#04x)",
// cartridges // cartridges
CartridgeError: "cartridge error: %v", CartridgeError: "cartridge error: %v",
@ -94,7 +93,10 @@ var messages = map[Errno]string{
// screen digest // screen digest
ScreenDigest: "television error: screendigest: %v", ScreenDigest: "television error: screendigest: %v",
// audio2wav
WavWriter: "wav writer: %v",
// gui // gui
UnsupportedGUIRequest: "gui error: unsupported request (%v)", UnsupportedGUIRequest: "gui error: unsupported request (%v)",
SDL: "gui error: SDL: %v", SDL: "SDL: %v",
} }

View file

@ -7,6 +7,7 @@ import (
"gopher2600/debugger/colorterm" "gopher2600/debugger/colorterm"
"gopher2600/debugger/console" "gopher2600/debugger/console"
"gopher2600/disassembly" "gopher2600/disassembly"
"gopher2600/errors"
"gopher2600/gui" "gopher2600/gui"
"gopher2600/gui/sdldebug" "gopher2600/gui/sdldebug"
"gopher2600/gui/sdlplay" "gopher2600/gui/sdlplay"
@ -17,6 +18,7 @@ import (
"gopher2600/recorder" "gopher2600/recorder"
"gopher2600/regression" "gopher2600/regression"
"gopher2600/television" "gopher2600/television"
"gopher2600/wavwriter"
"io" "io"
"math/rand" "math/rand"
"os" "os"
@ -80,6 +82,7 @@ func play(md *modalflag.Modes) error {
stable := md.AddBool("stable", true, "wait for stable frame before opening display") stable := md.AddBool("stable", true, "wait for stable frame before opening display")
fpscap := md.AddBool("fpscap", true, "cap fps to specification") fpscap := md.AddBool("fpscap", true, "cap fps to specification")
record := md.AddBool("record", false, "record user input to a file") record := md.AddBool("record", false, "record user input to a file")
wav := md.AddString("wav", "", "record audio to wav file")
p, err := md.Parse() p, err := md.Parse()
if p != modalflag.ParseContinue { if p != modalflag.ParseContinue {
@ -97,14 +100,28 @@ func play(md *modalflag.Modes) error {
tv, err := television.NewTelevision(*tvType) tv, err := television.NewTelevision(*tvType)
if err != nil { if err != nil {
return err return errors.New(errors.PlayError, err)
}
defer tv.End()
// add wavwriter mixer if wav argument has been specified
if *wav != "" {
aw, err := wavwriter.New(*wav)
if err != nil {
return errors.New(errors.PlayError, err)
}
tv.AddAudioMixer(aw)
} }
scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling)) scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling))
if err != nil { if err != nil {
return err return errors.New(errors.PlayError, err)
} }
// ^^^ note that because setting we have to setup tv and gui before
// calling playmode.Play, any errors generated by NewTelevision() and
// NewSdlPlay() have been wrapped in a PlayError
err = playmode.Play(tv, scr, *stable, *fpscap, *record, cartload) err = playmode.Play(tv, scr, *stable, *fpscap, *record, cartload)
if err != nil { if err != nil {
return err return err
@ -135,14 +152,19 @@ func debug(md *modalflag.Modes) error {
tv, err := television.NewTelevision(*tvType) tv, err := television.NewTelevision(*tvType)
if err != nil { if err != nil {
return err return errors.New(errors.DebuggerError, err)
} }
defer tv.End()
scr, err := sdldebug.NewSdlDebug(tv, 2.0) scr, err := sdldebug.NewSdlDebug(tv, 2.0)
if err != nil { if err != nil {
return err return errors.New(errors.DebuggerError, err)
} }
// ^^^ note that because setting we have to setup tv and gui before calling
// playmode.Play, any errors generated by NewTelevision() and NewSdlDebug()
// have been wrapped in a PlayError
dbg, err := debugger.NewDebugger(tv, scr) dbg, err := debugger.NewDebugger(tv, scr)
if err != nil { if err != nil {
return err return err
@ -263,6 +285,7 @@ func perform(md *modalflag.Modes) error {
if err != nil { if err != nil {
return err return err
} }
defer tv.End()
if *display { if *display {
scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling)) scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling))

View file

@ -122,6 +122,11 @@ func (scr *SdlDebug) Reset() error {
return scr.pxl.reset() return scr.pxl.reset()
} }
// EndRendering implements television.Renderer interface
func (scr *SdlDebug) EndRendering() error {
return nil
}
// IsVisible implements gui.GUI interface // IsVisible implements gui.GUI interface
func (scr SdlDebug) IsVisible() bool { func (scr SdlDebug) IsVisible() bool {
flgs := scr.window.GetFlags() flgs := scr.window.GetFlags()

View file

@ -2,103 +2,88 @@ package sdlplay
import ( import (
"gopher2600/hardware/tia/audio" "gopher2600/hardware/tia/audio"
"gopher2600/paths"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/veandco/go-sdl2/mix"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
) )
const samplePath = "samples" // the buffer length is important to get right. unfortunately, there's no
const sampleDistro = "little-scale_atari_2600_sample_pack" // special way (that I know of) that can tells us what the ideal value is. we
const samplePak = "Atari_2600_Cropped" // don't want it to be long because we can introduce unnecessary lag between
// the audio and video signal; by the same token we don't want git too short because
// we will end up calling FlushAudio() too often - FlushAudio() is a
// computationally expensive function.
//
// the following value has been discovered through trial and error. the precise
// value is not critical.
const bufferLength = 1540
type sound struct { type sound struct {
prevAud audio.Audio id sdl.AudioDeviceID
spec sdl.AudioSpec
samples [16][32]*mix.Chunk buffer []uint8
bufferCt int
} }
func newSound(scr *SdlPlay) (*sound, error) { func newSound(scr *SdlPlay) (*sound, error) {
snd := &sound{} snd := &sound{}
// prerequisite: SDL_INIT_AUDIO must be included in the call to sdl.Init() snd.buffer = make([]uint8, bufferLength)
mix.OpenAudio(22050, sdl.AUDIO_S16SYS, 2, 640)
path := paths.ResourcePath(samplePath, sampleDistro, samplePak) spec := &sdl.AudioSpec{
Freq: audio.SampleFreq,
walkFn := func(p string, info os.FileInfo, err error) error { Format: sdl.AUDIO_U8,
t := p Channels: 1,
t = strings.TrimPrefix(t, path) Silence: 127,
t = strings.TrimPrefix(t, string(os.PathSeparator)) Samples: uint16(bufferLength),
t = strings.TrimPrefix(t, samplePak)
t = strings.TrimPrefix(t, "--Wave_")
s := strings.Split(t, string(os.PathSeparator))
if len(s) != 2 {
return nil
}
control, e := strconv.Atoi(s[0])
if e != nil {
return nil
}
s[1] = strings.TrimPrefix(s[1], samplePak)
s[1] = strings.TrimPrefix(s[1], "_")
s[1] = strings.TrimSuffix(s[1], ".wav")
freq, e := strconv.Atoi(s[1])
if e != nil {
return nil
}
freq = ((freq - 1) % 32)
snd.samples[control][freq], e = mix.LoadWAV(p)
if e != nil {
return nil
}
return nil
} }
err := filepath.Walk(path, walkFn) var err error
var actualSpec sdl.AudioSpec
snd.id, err = sdl.OpenAudioDevice("", false, spec, &actualSpec, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
snd.spec = actualSpec
// make sure audio device is unpaused on startup
sdl.PauseAudioDevice(snd.id, false)
return snd, nil return snd, nil
} }
// SetAudio implements the television.AudioMixer interface // SetAudio implements the television.AudioMixer interface
func (scr *SdlPlay) SetAudio(aud audio.Audio) error { func (scr *SdlPlay) SetAudio(audioData uint8) error {
if aud.Volume0 != scr.snd.prevAud.Volume0 { scr.snd.buffer[scr.snd.bufferCt] = audioData + scr.snd.spec.Silence
mix.Volume(0, int(aud.Volume0*8))
}
if aud.Volume1 != scr.snd.prevAud.Volume1 {
mix.Volume(1, int(aud.Volume1*8))
}
if aud.Control0 != scr.snd.prevAud.Control0 || aud.Freq0 != scr.snd.prevAud.Freq0 { scr.snd.bufferCt++
if aud.Control0 == 0 { if scr.snd.bufferCt >= len(scr.snd.buffer) {
mix.HaltChannel(0) return scr.FlushAudio()
} else {
scr.snd.samples[aud.Control0][31-aud.Freq0].Play(0, -1)
}
} }
if aud.Control1 != scr.snd.prevAud.Control1 || aud.Freq1 != scr.snd.prevAud.Freq1 {
if aud.Control1 == 0 {
mix.HaltChannel(1)
} else {
scr.snd.samples[aud.Control1][31-aud.Freq1].Play(1, -1)
}
}
scr.snd.prevAud = aud
return nil return nil
} }
// FlushAudio implements the television.AudioMixer interface
func (scr *SdlPlay) FlushAudio() error {
err := sdl.QueueAudio(scr.snd.id, scr.snd.buffer)
if err != nil {
return err
}
scr.snd.bufferCt = 0
return nil
}
// PauseAudio implements the television.AudioMixer interface
func (scr *SdlPlay) PauseAudio(pause bool) error {
sdl.PauseAudioDevice(scr.snd.id, pause)
return nil
}
// EndMixing implements the television.AudioMixer interface
func (scr *SdlPlay) EndMixing() error {
defer sdl.CloseAudioDevice(scr.snd.id)
return scr.FlushAudio()
}

View file

@ -239,7 +239,7 @@ func (scr *SdlPlay) SetAltPixel(x, y int, red, green, blue byte, vblank bool) er
return nil return nil
} }
// SetMetaPixel recieves (and processes) additional emulator information from the emulator // SetMetaPixel implements gui.MetPixelRenderer interface
func (scr *SdlPlay) SetMetaPixel(sig gui.MetaPixel) error { func (scr *SdlPlay) SetMetaPixel(sig gui.MetaPixel) error {
return nil return nil
} }
@ -253,6 +253,11 @@ func (scr *SdlPlay) Reset() error {
return nil return nil
} }
// EndRendering implements television.Renderer interface
func (scr *SdlPlay) EndRendering() error {
return nil
}
// IsVisible implements gui.GUI interface // IsVisible implements gui.GUI interface
func (scr SdlPlay) IsVisible() bool { func (scr SdlPlay) IsVisible() bool {
flgs := scr.window.GetFlags() flgs := scr.window.GetFlags()

View file

@ -1,43 +1,105 @@
package audio package audio
import "gopher2600/hardware/memory" import (
"math/rand"
"strings"
)
// Audio contains all the components of the audio sub-system of the VCS TIA chip // SampleFreq represents the number of samples generated per second. This is
// the 30Khz reference frequency desribed in the Stella Programmer's Guide. see
// the commentary on clock114 for more detail
const SampleFreq = 31403
// the Atari 2600 has two independent sound generators. these will be mixed
// into one value by the Mix() function
const numChannels = 2
// Audio is the implementation of the TIA audio sub-system, using Ron Fries'
// method. Reference source code here:
//
// https://raw.githubusercontent.com/alekmaul/stella/master/emucore/TIASound.c
type Audio struct { type Audio struct {
Control0 uint8 // clock114 is so called because of the observation that the 30Khz
Control1 uint8 // reference frequency described in the Stella Programmer's Guide is
Freq0 uint8 // generated from the 3.58Mhz clock divided by 114, giving a sample
Freq1 uint8 // frequency of 31403Hz or 31Khz - close enought to the 30Khz referency
Volume0 uint8 // frequency we need. Ron Fries' talks about this in his original
Volume1 uint8 // documentation for TIASound.c
//
// see the Mix() function to see how it is used
clock114 int
poly4bit [15]uint8
poly5bit [31]uint8
poly9bit [511]uint8
div31 [31]uint8
channel0 channel
channel1 channel
}
func (au *Audio) String() string {
s := strings.Builder{}
s.WriteString("ch0: ")
s.WriteString(au.channel0.String())
s.WriteString(" ch1: ")
s.WriteString(au.channel1.String())
return s.String()
} }
// NewAudio is the preferred method of initialisation for the Video structure // NewAudio is the preferred method of initialisation for the Video structure
func NewAudio() *Audio { func NewAudio() *Audio {
return &Audio{} au := &Audio{}
} au.channel0.au = au
au.channel1.au = au
// UpdateOutput checks the TIA memory for changes to registers that are // from TIASound.c:
// interesting to the audio sub-system //
// // "Initialze the bit patterns for the polynomials. The 4bit and 5bit patterns
// Returns true if memory.ChipData has not been serviced. // are the identical ones used in the tia chip. Though the patterns could be
func (au *Audio) UpdateOutput(data memory.ChipData) bool { // packed with 8 bits per byte, using only a single bit per byte keeps the math
switch data.Name { // simple, which is important for efficient processing."
case "AUDC0": au.poly4bit = [15]uint8{1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0}
au.Control0 = data.Value & 0x0f au.poly5bit = [31]uint8{0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1}
case "AUDC1":
au.Control1 = data.Value & 0x0f // from TIASound.c (referring to 9 bit polynomial table):
case "AUDF0": //
au.Freq0 = data.Value & 0x1f // "Rather than have a table with 511 entries, I use a random number
case "AUDF1": // generator."
au.Freq1 = data.Value & 0x1f for i := 0; i < len(au.poly9bit); i++ {
case "AUDV0": au.poly9bit[i] = uint8(rand.Int() & 0x01)
au.Volume0 = data.Value & 0x0f
case "AUDV1":
au.Volume1 = data.Value & 0x0f
default:
return true
} }
return false // from TIASound.c:
//
// "I've treated the 'Div by 31' counter as another polynomial because of the
// way it operates. It does not have a 50% duty cycle, but instead has a 13:18
// ratio (of course, 13+18 = 31). This could also be implemented by using
// counters."
au.div31 = [31]uint8{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
return au
}
// Mix the two VCS audio channels. From the "Stella Programmer's Guide":
//
// "There are two audio circuits for generating sound. They are identical but
// completely independent and can be operated simultaneously [...]"
func (au *Audio) Mix() (bool, uint8) {
// 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
// immediately except on the 114th tick, whereupon we process the current
// audio registers and mix the two signals
au.clock114++
if au.clock114 < 114 {
return false, 0
}
au.clock114 = 0
au.channel0.process()
au.channel1.process()
// mix channels
return true, au.channel0.actualVol + au.channel1.actualVol
} }

View file

@ -0,0 +1,133 @@
package audio
import (
"fmt"
"strings"
)
type channel struct {
au *Audio
regControl uint8
regFreq uint8
regVolume uint8
poly4ct uint8
poly5ct uint8
poly9ct uint16
divCt uint8
divMax uint8
div3Ct uint8
actualVol uint8
}
func (ch *channel) String() string {
s := strings.Builder{}
s.WriteString(fmt.Sprintf("%04b @ %05b ^ %04b", ch.regControl, ch.regFreq, ch.regVolume))
return s.String()
}
func (ch *channel) process() {
if ch.divCt > 1 {
ch.divCt--
return
}
if ch.divCt != 1 {
return
}
var prevBit5 = ch.au.poly5bit[ch.poly5ct]
ch.divCt = ch.divMax
// from TIASound.c: "the P5 counter has multiple uses, so we inc it here"
ch.poly5ct++
if ch.poly5ct >= uint8(len(ch.au.poly5bit)) {
ch.poly5ct = 0
}
// check for clock tick
if (ch.regControl&0x02 == 0x0) ||
((ch.regControl&0x01 == 0x0) && ch.au.div31[ch.poly5ct] != 0) ||
((ch.regControl&0x01 == 0x1) && ch.au.poly5bit[ch.poly5ct] != 0) ||
((ch.regControl&0x0f == 0xf) && ch.au.poly5bit[ch.poly5ct] == prevBit5) {
if ch.regControl&0x04 == 0x04 {
// use pure clock
if ch.regControl&0x0f == 0x0f {
// use poly5/div3
if ch.au.poly5bit[ch.poly5ct] != prevBit5 {
ch.div3Ct++
if ch.div3Ct == 3 {
ch.div3Ct = 0
// toggle volume
if ch.actualVol != 0 {
ch.actualVol = 0
} else {
ch.actualVol = ch.regVolume
}
}
}
} else {
// toggle volume
if ch.actualVol != 0 {
ch.actualVol = 0
} else {
ch.actualVol = ch.regVolume
}
}
} else if ch.regControl&0x08 == 0x08 {
// use poly poly5/poly9
if ch.regControl == 0x08 {
// use poly9
ch.poly9ct++
if ch.poly9ct >= uint16(len(ch.au.poly9bit)) {
ch.poly9ct = 0
}
// toggle volume
if ch.au.poly9bit[ch.poly9ct] != 0 {
ch.actualVol = ch.regVolume
} else {
ch.actualVol = 0
}
} else if ch.regControl&0x02 != 0 {
if ch.actualVol != 0 || ch.regControl&0x01 == 0x01 {
ch.actualVol = 0
} else {
ch.actualVol = ch.regVolume
}
} else {
// use poly5. we've already bumped poly5 counter forward
// toggle volume
if ch.au.poly5bit[ch.poly5ct] == 1 {
ch.actualVol = ch.regVolume
} else {
ch.actualVol = 0
}
}
} else {
// use poly 4
ch.poly4ct++
if ch.poly4ct >= uint8(len(ch.au.poly4bit)) {
ch.poly4ct = 0
}
if ch.au.poly4bit[ch.poly4ct] == 1 {
ch.actualVol = ch.regVolume
} else {
ch.actualVol = 0
}
}
}
}

View file

@ -0,0 +1,63 @@
package audio
import (
"gopher2600/hardware/memory"
)
// UpdateRegisters checks the TIA memory for changes to registers that are
// interesting to the audio sub-system
//
// Returns true if memory.ChipData has not been serviced.
func (au *Audio) UpdateRegisters(data memory.ChipData) bool {
switch data.Name {
case "AUDC0":
au.channel0.regControl = data.Value & 0x0f
case "AUDC1":
au.channel1.regControl = data.Value & 0x0f
case "AUDF0":
au.channel0.regFreq = data.Value & 0x1f
case "AUDF1":
au.channel1.regFreq = data.Value & 0x1f
case "AUDV0":
au.channel0.regVolume = data.Value & 0x0f
case "AUDV1":
au.channel1.regVolume = data.Value & 0x0f
default:
return true
}
au.channel0.reactAUDCx()
au.channel1.reactAUDCx()
return false
}
// changing the value of an AUDx registers causes some side effect
func (ch *channel) reactAUDCx() {
v := uint8(0)
if ch.regControl == 0x00 || ch.regControl == 0x0b {
ch.actualVol = ch.regVolume
} else {
v = ch.regFreq + 1
// from TIASound.c: "if bits 2 & 3 are set, the multiply div by n count by 3"
if ch.regControl&0x0c == 0x0c && ch.regControl != 0x0f {
v *= 3
}
}
// reset channel when things have changed
if v != ch.divMax {
// reset divide by n counters
ch.divMax = v
// if the channel is now "volume only" or was "volume only" ...
if ch.divCt == 0 || v == 0 {
// ... reset the counter
ch.divCt = v
}
// ...otherwide let it complete the previous
}
}

View file

@ -15,7 +15,7 @@ type Polycounter struct {
func (pcnt Polycounter) String() string { func (pcnt Polycounter) String() string {
// assumes maximum limit of 2 digits // assumes maximum limit of 2 digits
return fmt.Sprintf("%s (%02d)", Table[pcnt.Count], pcnt.Count) return fmt.Sprintf("%s (%02d)", Poly6Bit[pcnt.Count], pcnt.Count)
} }
// Reset is a convenience function to reset count value to 0 // Reset is a convenience function to reset count value to 0

View file

@ -4,37 +4,86 @@ import "fmt"
type table []string type table []string
// Table is the polycounter sequence over the space of 6 bits // Poly6Bit is the polycounter sequence over the space of 6 bits
var Table table var Poly6Bit table
// Poly5Bit is the polycounter sequence over the space of 5 bits
// (used by audio generator)
var Poly5Bit table
// Poly4Bit is the polycounter sequence over the space of 4 bits
// (used by audio generator)
var Poly4Bit table
// initialise the 6 bit table representing the polycounter sequence. we use to // initialise the 6 bit table representing the polycounter sequence. we use to
// match the current count with the correct polycounter pattern. this is // match the current count with the correct polycounter pattern. this is
// currently used only in the String()/ToString() functions for presentation // currently used only in the String()/ToString() functions for presentation
// purposes and when specifying the reset pattern in the call to Reset() // purposes and when specifying the reset pattern in the call to Reset()
func init() { func init() {
Table = make([]string, 64)
var p int var p int
Table[0] = "000000"
for i := 1; i < len(Table); i++ { // poly 6 bit generation
Poly6Bit = make([]string, 64)
Poly6Bit[0] = "000000"
for i := 1; i < len(Poly6Bit); i++ {
p = ((p & (0x3f - 1)) >> 1) | (((p&1)^((p>>1)&1))^0x3f)<<5 p = ((p & (0x3f - 1)) >> 1) | (((p&1)^((p>>1)&1))^0x3f)<<5
p = p & 0x3f p = p & 0x3f
Table[i] = fmt.Sprintf("%06b", p) Poly6Bit[i] = fmt.Sprintf("%06b", p)
} }
// sanity check that the table has looped correctly // sanity check that the table has looped correctly
if Table[63] != "000000" { if Poly6Bit[63] != "000000" {
panic("error during 6 bit polycounter generation") panic("error during 6 bit polycounter generation")
} }
// force the final value to be the invalid polycounter value. this is only // force the final value to be the invalid polycounter value. this is only
// ever useful for specifying the reset pattern // ever useful for specifying the reset pattern
Table[63] = "111111" Poly6Bit[63] = "111111"
// poly 5 bit generation
Poly5Bit = make([]string, 22)
Poly5Bit[0] = "00000"
for i := 1; i < len(Poly5Bit); i++ {
p = ((p & (0x1f - 1)) >> 1) | (((p&1)^((p>>1)&1))^0x1f)<<4
p = p & 0x1f
Poly5Bit[i] = fmt.Sprintf("%05b", p)
}
// sanity check that the table has looped correctly
if Poly5Bit[21] != "00000" {
panic("error during 5 bit polycounter generation")
}
// force the final value to be the invalid polycounter value. this is only
// ever useful for specifying the reset pattern
Poly5Bit[21] = "11111"
// poly 4 bit generation
Poly4Bit = make([]string, 16)
Poly4Bit[0] = "0000"
for i := 1; i < len(Poly4Bit); i++ {
p = ((p & (0x0f - 1)) >> 1) | (((p&1)^((p>>1)&1))^0x0f)<<3
p = p & 0x0f
Poly4Bit[i] = fmt.Sprintf("%04b", p)
}
// sanity check that the table has looped correctly
if Poly4Bit[15] != "0000" {
panic("error during 5 bit polycounter generation")
}
// force the final value to be the invalid polycounter value. this is only
// ever useful for specifying the reset pattern
Poly4Bit[15] = "1111"
} }
// LookupPattern returns the index of the specified pattern // LookupPattern returns the index of the specified pattern
func (tab table) LookupPattern(pattern string) int { func (tab table) LookupPattern(pattern string) int {
for i := 0; i < len(Table); i++ { for i := 0; i < len(Poly6Bit); i++ {
if Table[i] == pattern { if Poly6Bit[i] == pattern {
return i return i
} }
} }

View file

@ -206,15 +206,11 @@ func (tia *TIA) Step(serviceMemory bool) (bool, error) {
serviceMemory = tia.Video.UpdateSpritePixels(memoryData) serviceMemory = tia.Video.UpdateSpritePixels(memoryData)
} }
if serviceMemory { if serviceMemory {
serviceMemory = tia.Audio.UpdateOutput(memoryData) serviceMemory = tia.Audio.UpdateRegisters(memoryData)
} }
// copy audio to television signal and update signal at the same time as // copy audio to television signal
// color burst. this needs improving. audio signal is actually updated tia.sig.AudioUpdate, tia.sig.AudioData = tia.Audio.Mix()
// constantly but the audio engine in the SDL implementation isn't up to it
// yet.
tia.sig.UpdateAudio = tia.sig.CBurst
tia.sig.Audio = *tia.Audio
// send signal to television // send signal to television
if err := tia.tv.Signal(tia.sig); err != nil { if err := tia.tv.Signal(tia.sig); err != nil {

View file

@ -130,6 +130,7 @@ func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg st
if err != nil { if err != nil {
return false, "", errors.New(errors.RegressionFrameError, err) return false, "", errors.New(errors.RegressionFrameError, err)
} }
defer tv.End()
dig, err := screendigest.NewSHA1(tv) dig, err := screendigest.NewSHA1(tv)
if err != nil { if err != nil {

View file

@ -96,6 +96,7 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg
if err != nil { if err != nil {
return false, "", errors.New(errors.RegressionFrameError, err) return false, "", errors.New(errors.RegressionFrameError, err)
} }
defer tv.End()
_, err = screendigest.NewSHA1(tv) _, err = screendigest.NewSHA1(tv)
if err != nil { if err != nil {

View file

@ -101,3 +101,8 @@ func (dig *SHA1) SetPixel(x, y int, red, green, blue byte, vblank bool) error {
func (dig *SHA1) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { func (dig *SHA1) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error {
return nil return nil
} }
// EndRendering implements television.Renderer interface
func (dig *SHA1) EndRendering() error {
return nil
}

View file

@ -1,9 +1,5 @@
package television package television
import (
"gopher2600/hardware/tia/audio"
)
// Television defines the operations that can be performed on the conceptual // Television defines the operations that can be performed on the conceptual
// television. Note that the television implementation itself does not present // television. Note that the television implementation itself does not present
// any information, either visually or sonically. Instead, PixelRenderers and // any information, either visually or sonically. Instead, PixelRenderers and
@ -32,6 +28,14 @@ type Television interface {
// IsStable returns true if the television thinks the image being sent by // IsStable returns true if the television thinks the image being sent by
// the VCS is stable // the VCS is stable
IsStable() bool IsStable() bool
// some televisions may need to conclude and/or dispose of resources
// gently. implementations of End() should call EndRendering() and
// EndMixing() on each PixelRenderer and AudioMixer that has been added.
//
// for simplicity, the Television should be considered unusable
// after EndRendering() has been called
End() error
} }
// PixelRenderer implementations displays, or otherwise works with, visal // PixelRenderer implementations displays, or otherwise works with, visal
@ -100,11 +104,23 @@ type PixelRenderer interface {
// //
SetPixel(x, y int, red, green, blue byte, vblank bool) error SetPixel(x, y int, red, green, blue byte, vblank bool) error
SetAltPixel(x, y int, red, green, blue byte, vblank bool) error SetAltPixel(x, y int, red, green, blue byte, vblank bool) error
// some renderers may need to conclude and/or dispose of resources gently.
// for simplicity, the PixelRenderer should be considered unusable after
// EndRendering() has been called
EndRendering() error
} }
// AudioMixer implementations work with sound; most probably playing it. // AudioMixer implementations work with sound; most probably playing it.
type AudioMixer interface { type AudioMixer interface {
SetAudio(audio audio.Audio) error SetAudio(audioData uint8) error
FlushAudio() error
PauseAudio(pause bool) error
// some mixers may need to conclude and/or dispose of resources gently.
// for simplicity, the AudioMixer should be considered unusable after
// EndMixing() has been called
EndMixing() error
} }
// SignalAttributes represents the data sent to the television // SignalAttributes represents the data sent to the television
@ -137,10 +153,9 @@ type SignalAttributes struct {
// uses HSyncSimple instead of HSync // uses HSyncSimple instead of HSync
HSyncSimple bool HSyncSimple bool
// audio signal is just the content of the VCS audio registers. for now, // raw sound data
// sounds is generated/mixed by the television or gui implementation AudioData uint8
Audio audio.Audio AudioUpdate bool
UpdateAudio bool
} }
// StateReq is used to identify which television attribute is being asked // StateReq is used to identify which television attribute is being asked

View file

@ -237,10 +237,10 @@ func (tv *television) Signal(sig SignalAttributes) error {
} }
} }
// mix audio on UpdateAudio signal // mix audio
if sig.UpdateAudio { if sig.AudioUpdate {
for f := range tv.mixers { for f := range tv.mixers {
err := tv.mixers[f].SetAudio(sig.Audio) err := tv.mixers[f].SetAudio(sig.AudioData)
if err != nil { if err != nil {
return err return err
} }
@ -339,3 +339,20 @@ func (tv television) GetSpec() *Specification {
func (tv television) IsStable() bool { func (tv television) IsStable() bool {
return tv.stability >= stabilityThreshold return tv.stability >= stabilityThreshold
} }
// End implements the Television interface
func (tv television) End() error {
var err error
// call new frame for all renderers
for f := range tv.renderers {
err = tv.renderers[f].EndRendering()
}
// flush audio for all mixers
for f := range tv.mixers {
err = tv.mixers[f].EndMixing()
}
return err
}

81
wavwriter/wav.go Normal file
View file

@ -0,0 +1,81 @@
package wavwriter
import (
"gopher2600/errors"
tia "gopher2600/hardware/tia/audio"
"os"
"github.com/go-audio/audio"
"github.com/go-audio/wav"
)
// WavWriter implemented the television.AudioMixer interface
type WavWriter struct {
filename string
buffer []int8
}
// New is the preferred method of initialisation for the Audio2Wav type
func New(filename string) (*WavWriter, error) {
aw := &WavWriter{
filename: filename,
buffer: make([]int8, 0, 0),
}
return aw, nil
}
// SetAudio implements the television.AudioMixer interface
func (aw *WavWriter) SetAudio(audioData uint8) error {
aw.buffer = append(aw.buffer, int8(int16(audioData)-127))
return nil
}
// FlushAudio implements the television.AudioMixer interface
func (aw *WavWriter) FlushAudio() error {
return nil
}
// PauseAudio implements the television.AudioMixer interface
func (aw *WavWriter) PauseAudio(pause bool) error {
return nil
}
// EndMixing implements the television.AudioMixer interface
func (aw *WavWriter) EndMixing() error {
err := aw.FlushAudio()
if err != nil {
return errors.New(errors.WavWriter, err)
}
f, err := os.Create(aw.filename)
if err != nil {
return errors.New(errors.WavWriter, err)
}
defer f.Close()
// see audio commentary in sdlplay package for thinking around sample rates
enc := wav.NewEncoder(f, tia.SampleFreq, 8, 1, 1)
if enc == nil {
return errors.New(errors.WavWriter, "bad parameters for wav encoding")
}
defer enc.Close()
buf := audio.PCMBuffer{
Format: &audio.Format{
NumChannels: 1,
SampleRate: 31403,
},
I8: aw.buffer,
DataType: audio.DataTypeI8,
SourceBitDepth: 8,
}
err = enc.Write(buf.AsIntBuffer())
if err != nil {
return errors.New(errors.WavWriter, err)
}
return nil
}

View file

@ -38,6 +38,8 @@ func NewCanvas(worker js.Value) *Canvas {
if err != nil { if err != nil {
return nil return nil
} }
defer scr.Television.End()
scr.Television.AddPixelRenderer(scr) scr.Television.AddPixelRenderer(scr)
// change tv spec after window creation (so we can set the window size) // change tv spec after window creation (so we can set the window size)
@ -49,6 +51,7 @@ func NewCanvas(worker js.Value) *Canvas {
return scr return scr
} }
// Resize implements telvision.PixelRenderer
func (scr *Canvas) Resize(topScanline, numScanlines int) error { func (scr *Canvas) Resize(topScanline, numScanlines int) error {
scr.top = topScanline scr.top = topScanline
scr.height = numScanlines * vertScale scr.height = numScanlines * vertScale
@ -125,3 +128,8 @@ func (scr *Canvas) SetPixel(x, y int, red, green, blue byte, vblank bool) error
func (scr *Canvas) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error { func (scr *Canvas) SetAltPixel(x, y int, red, green, blue byte, vblank bool) error {
return nil return nil
} }
// EndRendering implements telvision.PixelRenderer
func (scr *Canvas) EndRendering() error {
return nil
}