mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
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
This commit is contained in:
parent
261a379ae0
commit
9f50f3c77e
20 changed files with 605 additions and 147 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
133
hardware/tia/audio/channels.go
Normal file
133
hardware/tia/audio/channels.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
hardware/tia/audio/registers.go
Normal file
63
hardware/tia/audio/registers.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
81
wavwriter/wav.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue