From 9f50f3c77edcd417c479c6b01d14be0bcc790d14 Mon Sep 17 00:00:00 2001 From: steve Date: Sun, 1 Dec 2019 10:13:36 +0000 Subject: [PATCH] o audio - 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 --- debugger/commands.go | 5 + errors/categories.go | 4 +- errors/messages.go | 16 +-- gopher2600.go | 31 +++++- gui/sdldebug/sdldebug.go | 5 + gui/sdlplay/audio.go | 131 +++++++++++------------ gui/sdlplay/sdlplay.go | 7 +- hardware/tia/audio/audio.go | 124 ++++++++++++++++------ hardware/tia/audio/channels.go | 133 ++++++++++++++++++++++++ hardware/tia/audio/registers.go | 63 +++++++++++ hardware/tia/polycounter/polycounter.go | 2 +- hardware/tia/polycounter/tables.go | 69 ++++++++++-- hardware/tia/step.go | 10 +- regression/frame.go | 1 + regression/playback.go | 1 + screendigest/digest.go | 5 + television/protocol.go | 33 ++++-- television/television.go | 23 +++- wavwriter/wav.go | 81 +++++++++++++++ web2600/src/canvas.go | 8 ++ 20 files changed, 605 insertions(+), 147 deletions(-) create mode 100644 hardware/tia/audio/channels.go create mode 100644 hardware/tia/audio/registers.go create mode 100644 wavwriter/wav.go diff --git a/debugger/commands.go b/debugger/commands.go index 16601e9d..c3d53956 100644 --- a/debugger/commands.go +++ b/debugger/commands.go @@ -22,6 +22,7 @@ import ( // debugger keywords const ( + cmdAudio = "AUDIO" cmdBall = "BALL" cmdBreak = "BREAK" cmdCPU = "CPU" @@ -70,6 +71,7 @@ const ( const cmdHelp = "HELP" var commandTemplate = []string{ + cmdAudio, cmdBall, cmdBreak + " [%S %N|%N] {& %S %N|& %N}", 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) } + case cmdAudio: + dbg.printInstrument(dbg.vcs.TIA.Audio) + case cmdTV: option, present := tokens.Get() if present { diff --git a/errors/categories.go b/errors/categories.go index 80e40c25..555f2bc7 100644 --- a/errors/categories.go +++ b/errors/categories.go @@ -90,7 +90,6 @@ const ( UnwritableAddress UnpokeableAddress UnpeekableAddress - UnrecognisedAddress // cartridges CartridgeError @@ -107,6 +106,9 @@ const ( // screen digest ScreenDigest + // wavwriter + WavWriter + // gui UnsupportedGUIRequest SDL diff --git a/errors/messages.go b/errors/messages.go index d8834ca4..23ecc529 100644 --- a/errors/messages.go +++ b/errors/messages.go @@ -72,12 +72,11 @@ var messages = map[Errno]string{ InvalidOperationMidInstruction: "cpu error: invalid operation mid-instruction (%v)", // memory - MemoryError: "memory error: %v", - UnreadableAddress: "memory error: memory location is not readable (%#04x)", - UnwritableAddress: "memory error: memory location is not writable (%#04x)", - UnpokeableAddress: "memory error: cannot poke address (%#04x)", - UnpeekableAddress: "memory error: cannot peek address (%#04x)", - UnrecognisedAddress: "memory error: address unrecognised (%#04x)", + MemoryError: "memory error: %v", + UnreadableAddress: "memory error: memory location is not readable (%#04x)", + UnwritableAddress: "memory error: memory location is not writable (%#04x)", + UnpokeableAddress: "memory error: cannot poke address (%v)", + UnpeekableAddress: "memory error: cannot peek address (%v)", // cartridges CartridgeError: "cartridge error: %v", @@ -94,7 +93,10 @@ var messages = map[Errno]string{ // screen digest ScreenDigest: "television error: screendigest: %v", + // audio2wav + WavWriter: "wav writer: %v", + // gui UnsupportedGUIRequest: "gui error: unsupported request (%v)", - SDL: "gui error: SDL: %v", + SDL: "SDL: %v", } diff --git a/gopher2600.go b/gopher2600.go index 12745009..366f1c81 100644 --- a/gopher2600.go +++ b/gopher2600.go @@ -7,6 +7,7 @@ import ( "gopher2600/debugger/colorterm" "gopher2600/debugger/console" "gopher2600/disassembly" + "gopher2600/errors" "gopher2600/gui" "gopher2600/gui/sdldebug" "gopher2600/gui/sdlplay" @@ -17,6 +18,7 @@ import ( "gopher2600/recorder" "gopher2600/regression" "gopher2600/television" + "gopher2600/wavwriter" "io" "math/rand" "os" @@ -80,6 +82,7 @@ func play(md *modalflag.Modes) error { stable := md.AddBool("stable", true, "wait for stable frame before opening display") fpscap := md.AddBool("fpscap", true, "cap fps to specification") record := md.AddBool("record", false, "record user input to a file") + wav := md.AddString("wav", "", "record audio to wav file") p, err := md.Parse() if p != modalflag.ParseContinue { @@ -97,14 +100,28 @@ func play(md *modalflag.Modes) error { tv, err := television.NewTelevision(*tvType) 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)) 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) if err != nil { return err @@ -135,14 +152,19 @@ func debug(md *modalflag.Modes) error { tv, err := television.NewTelevision(*tvType) if err != nil { - return err + return errors.New(errors.DebuggerError, err) } + defer tv.End() scr, err := sdldebug.NewSdlDebug(tv, 2.0) 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) if err != nil { return err @@ -263,6 +285,7 @@ func perform(md *modalflag.Modes) error { if err != nil { return err } + defer tv.End() if *display { scr, err := sdlplay.NewSdlPlay(tv, float32(*scaling)) diff --git a/gui/sdldebug/sdldebug.go b/gui/sdldebug/sdldebug.go index 0583c543..08fd9da9 100644 --- a/gui/sdldebug/sdldebug.go +++ b/gui/sdldebug/sdldebug.go @@ -122,6 +122,11 @@ func (scr *SdlDebug) Reset() error { return scr.pxl.reset() } +// EndRendering implements television.Renderer interface +func (scr *SdlDebug) EndRendering() error { + return nil +} + // IsVisible implements gui.GUI interface func (scr SdlDebug) IsVisible() bool { flgs := scr.window.GetFlags() diff --git a/gui/sdlplay/audio.go b/gui/sdlplay/audio.go index 24675b7a..4fd2194c 100644 --- a/gui/sdlplay/audio.go +++ b/gui/sdlplay/audio.go @@ -2,103 +2,88 @@ package sdlplay import ( "gopher2600/hardware/tia/audio" - "gopher2600/paths" - "os" - "path/filepath" - "strconv" - "strings" - "github.com/veandco/go-sdl2/mix" "github.com/veandco/go-sdl2/sdl" ) -const samplePath = "samples" -const sampleDistro = "little-scale_atari_2600_sample_pack" -const samplePak = "Atari_2600_Cropped" +// the buffer length is important to get right. unfortunately, there's no +// special way (that I know of) that can tells us what the ideal value is. we +// 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 { - prevAud audio.Audio - - samples [16][32]*mix.Chunk + id sdl.AudioDeviceID + spec sdl.AudioSpec + buffer []uint8 + bufferCt int } func newSound(scr *SdlPlay) (*sound, error) { snd := &sound{} - // prerequisite: SDL_INIT_AUDIO must be included in the call to sdl.Init() - mix.OpenAudio(22050, sdl.AUDIO_S16SYS, 2, 640) + snd.buffer = make([]uint8, bufferLength) - path := paths.ResourcePath(samplePath, sampleDistro, samplePak) - - walkFn := func(p string, info os.FileInfo, err error) error { - t := p - t = strings.TrimPrefix(t, path) - t = strings.TrimPrefix(t, string(os.PathSeparator)) - 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 + spec := &sdl.AudioSpec{ + Freq: audio.SampleFreq, + Format: sdl.AUDIO_U8, + Channels: 1, + Silence: 127, + Samples: uint16(bufferLength), } - err := filepath.Walk(path, walkFn) + var err error + var actualSpec sdl.AudioSpec + + snd.id, err = sdl.OpenAudioDevice("", false, spec, &actualSpec, 0) if err != nil { return nil, err } + snd.spec = actualSpec + + // make sure audio device is unpaused on startup + sdl.PauseAudioDevice(snd.id, false) + return snd, nil } // SetAudio implements the television.AudioMixer interface -func (scr *SdlPlay) SetAudio(aud audio.Audio) error { - if aud.Volume0 != scr.snd.prevAud.Volume0 { - mix.Volume(0, int(aud.Volume0*8)) - } - if aud.Volume1 != scr.snd.prevAud.Volume1 { - mix.Volume(1, int(aud.Volume1*8)) - } +func (scr *SdlPlay) SetAudio(audioData uint8) error { + scr.snd.buffer[scr.snd.bufferCt] = audioData + scr.snd.spec.Silence - if aud.Control0 != scr.snd.prevAud.Control0 || aud.Freq0 != scr.snd.prevAud.Freq0 { - if aud.Control0 == 0 { - mix.HaltChannel(0) - } else { - scr.snd.samples[aud.Control0][31-aud.Freq0].Play(0, -1) - } + scr.snd.bufferCt++ + if scr.snd.bufferCt >= len(scr.snd.buffer) { + return scr.FlushAudio() } - 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 } + +// 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() +} diff --git a/gui/sdlplay/sdlplay.go b/gui/sdlplay/sdlplay.go index 3c24bd72..b2cf2da0 100644 --- a/gui/sdlplay/sdlplay.go +++ b/gui/sdlplay/sdlplay.go @@ -239,7 +239,7 @@ func (scr *SdlPlay) SetAltPixel(x, y int, red, green, blue byte, vblank bool) er 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 { return nil } @@ -253,6 +253,11 @@ func (scr *SdlPlay) Reset() error { return nil } +// EndRendering implements television.Renderer interface +func (scr *SdlPlay) EndRendering() error { + return nil +} + // IsVisible implements gui.GUI interface func (scr SdlPlay) IsVisible() bool { flgs := scr.window.GetFlags() diff --git a/hardware/tia/audio/audio.go b/hardware/tia/audio/audio.go index dfe9c741..078fe87b 100644 --- a/hardware/tia/audio/audio.go +++ b/hardware/tia/audio/audio.go @@ -1,43 +1,105 @@ 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 { - Control0 uint8 - Control1 uint8 - Freq0 uint8 - Freq1 uint8 - Volume0 uint8 - Volume1 uint8 + // clock114 is so called because of the observation that the 30Khz + // reference frequency described in the Stella Programmer's Guide is + // generated from the 3.58Mhz clock divided by 114, giving a sample + // frequency of 31403Hz or 31Khz - close enought to the 30Khz referency + // frequency we need. Ron Fries' talks about this in his original + // 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 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 -// interesting to the audio sub-system -// -// Returns true if memory.ChipData has not been serviced. -func (au *Audio) UpdateOutput(data memory.ChipData) bool { - switch data.Name { - case "AUDC0": - au.Control0 = data.Value & 0x0f - case "AUDC1": - au.Control1 = data.Value & 0x0f - case "AUDF0": - au.Freq0 = data.Value & 0x1f - case "AUDF1": - au.Freq1 = data.Value & 0x1f - case "AUDV0": - au.Volume0 = data.Value & 0x0f - case "AUDV1": - au.Volume1 = data.Value & 0x0f - default: - return true + // from TIASound.c: + // + // "Initialze the bit patterns for the polynomials. The 4bit and 5bit patterns + // are the identical ones used in the tia chip. Though the patterns could be + // packed with 8 bits per byte, using only a single bit per byte keeps the math + // simple, which is important for efficient processing." + au.poly4bit = [15]uint8{1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0} + 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} + + // from TIASound.c (referring to 9 bit polynomial table): + // + // "Rather than have a table with 511 entries, I use a random number + // generator." + for i := 0; i < len(au.poly9bit); i++ { + au.poly9bit[i] = uint8(rand.Int() & 0x01) } - 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 } diff --git a/hardware/tia/audio/channels.go b/hardware/tia/audio/channels.go new file mode 100644 index 00000000..e7ffd482 --- /dev/null +++ b/hardware/tia/audio/channels.go @@ -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 + } + } + } +} diff --git a/hardware/tia/audio/registers.go b/hardware/tia/audio/registers.go new file mode 100644 index 00000000..fd9e6eb1 --- /dev/null +++ b/hardware/tia/audio/registers.go @@ -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 + } +} diff --git a/hardware/tia/polycounter/polycounter.go b/hardware/tia/polycounter/polycounter.go index 150f8c19..f6871449 100644 --- a/hardware/tia/polycounter/polycounter.go +++ b/hardware/tia/polycounter/polycounter.go @@ -15,7 +15,7 @@ type Polycounter struct { func (pcnt Polycounter) String() string { // 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 diff --git a/hardware/tia/polycounter/tables.go b/hardware/tia/polycounter/tables.go index 8664ebc0..55d51666 100644 --- a/hardware/tia/polycounter/tables.go +++ b/hardware/tia/polycounter/tables.go @@ -4,37 +4,86 @@ import "fmt" type table []string -// Table is the polycounter sequence over the space of 6 bits -var Table table +// Poly6Bit is the polycounter sequence over the space of 6 bits +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 // match the current count with the correct polycounter pattern. this is // currently used only in the String()/ToString() functions for presentation // purposes and when specifying the reset pattern in the call to Reset() func init() { - Table = make([]string, 64) 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 - Table[i] = fmt.Sprintf("%06b", p) + Poly6Bit[i] = fmt.Sprintf("%06b", p) } // sanity check that the table has looped correctly - if Table[63] != "000000" { + if Poly6Bit[63] != "000000" { panic("error during 6 bit polycounter generation") } // force the final value to be the invalid polycounter value. this is only // 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 func (tab table) LookupPattern(pattern string) int { - for i := 0; i < len(Table); i++ { - if Table[i] == pattern { + for i := 0; i < len(Poly6Bit); i++ { + if Poly6Bit[i] == pattern { return i } } diff --git a/hardware/tia/step.go b/hardware/tia/step.go index c6982d5c..3121f992 100644 --- a/hardware/tia/step.go +++ b/hardware/tia/step.go @@ -206,15 +206,11 @@ func (tia *TIA) Step(serviceMemory bool) (bool, error) { serviceMemory = tia.Video.UpdateSpritePixels(memoryData) } 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 - // color burst. this needs improving. audio signal is actually updated - // 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 + // copy audio to television signal + tia.sig.AudioUpdate, tia.sig.AudioData = tia.Audio.Mix() // send signal to television if err := tia.tv.Signal(tia.sig); err != nil { diff --git a/regression/frame.go b/regression/frame.go index 9c11d760..324e2df6 100644 --- a/regression/frame.go +++ b/regression/frame.go @@ -130,6 +130,7 @@ func (reg *FrameRegression) regress(newRegression bool, output io.Writer, msg st if err != nil { return false, "", errors.New(errors.RegressionFrameError, err) } + defer tv.End() dig, err := screendigest.NewSHA1(tv) if err != nil { diff --git a/regression/playback.go b/regression/playback.go index b20377b9..9a707ced 100644 --- a/regression/playback.go +++ b/regression/playback.go @@ -96,6 +96,7 @@ func (reg *PlaybackRegression) regress(newRegression bool, output io.Writer, msg if err != nil { return false, "", errors.New(errors.RegressionFrameError, err) } + defer tv.End() _, err = screendigest.NewSHA1(tv) if err != nil { diff --git a/screendigest/digest.go b/screendigest/digest.go index 8bb6e438..a5d90ff6 100644 --- a/screendigest/digest.go +++ b/screendigest/digest.go @@ -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 { return nil } + +// EndRendering implements television.Renderer interface +func (dig *SHA1) EndRendering() error { + return nil +} diff --git a/television/protocol.go b/television/protocol.go index 52184ea4..5672460f 100644 --- a/television/protocol.go +++ b/television/protocol.go @@ -1,9 +1,5 @@ package television -import ( - "gopher2600/hardware/tia/audio" -) - // Television defines the operations that can be performed on the conceptual // television. Note that the television implementation itself does not present // 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 // the VCS is stable 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 @@ -100,11 +104,23 @@ type PixelRenderer interface { // SetPixel(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. 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 @@ -137,10 +153,9 @@ type SignalAttributes struct { // uses HSyncSimple instead of HSync HSyncSimple bool - // audio signal is just the content of the VCS audio registers. for now, - // sounds is generated/mixed by the television or gui implementation - Audio audio.Audio - UpdateAudio bool + // raw sound data + AudioData uint8 + AudioUpdate bool } // StateReq is used to identify which television attribute is being asked diff --git a/television/television.go b/television/television.go index 4dd3f145..87a1afce 100644 --- a/television/television.go +++ b/television/television.go @@ -237,10 +237,10 @@ func (tv *television) Signal(sig SignalAttributes) error { } } - // mix audio on UpdateAudio signal - if sig.UpdateAudio { + // mix audio + if sig.AudioUpdate { for f := range tv.mixers { - err := tv.mixers[f].SetAudio(sig.Audio) + err := tv.mixers[f].SetAudio(sig.AudioData) if err != nil { return err } @@ -339,3 +339,20 @@ func (tv television) GetSpec() *Specification { func (tv television) IsStable() bool { 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 +} diff --git a/wavwriter/wav.go b/wavwriter/wav.go new file mode 100644 index 00000000..ca65af01 --- /dev/null +++ b/wavwriter/wav.go @@ -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 +} diff --git a/web2600/src/canvas.go b/web2600/src/canvas.go index 3874000a..56409144 100644 --- a/web2600/src/canvas.go +++ b/web2600/src/canvas.go @@ -38,6 +38,8 @@ func NewCanvas(worker js.Value) *Canvas { if err != nil { return nil } + defer scr.Television.End() + scr.Television.AddPixelRenderer(scr) // 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 } +// Resize implements telvision.PixelRenderer func (scr *Canvas) Resize(topScanline, numScanlines int) error { scr.top = topScanline 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 { return nil } + +// EndRendering implements telvision.PixelRenderer +func (scr *Canvas) EndRendering() error { + return nil +}