From a9399f2d17f61f578d9c02c50f9a2f0f65139487 Mon Sep 17 00:00:00 2001 From: Jairinho Date: Tue, 1 Mar 2022 21:01:20 -0300 Subject: [PATCH] sound system refactored --- chip8/beep.wav | Bin 0 -> 26876 bytes chip8/chip8.go | 39 +++++++++++-------------- chip8/chip8_test.go | 4 ++- chip8/sound.wav | Bin 102615 -> 0 bytes main.go | 68 +++++++++++++++++++++++++------------------- 5 files changed, 59 insertions(+), 52 deletions(-) create mode 100644 chip8/beep.wav delete mode 100644 chip8/sound.wav diff --git a/chip8/beep.wav b/chip8/beep.wav new file mode 100644 index 0000000000000000000000000000000000000000..8343a63e7a68e3e5ebceb5659c5ab99c2cc263d2 GIT binary patch literal 26876 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 literal 0 HcmV?d00001 diff --git a/chip8/chip8.go b/chip8/chip8.go index fe0c6bc..afec0a8 100644 --- a/chip8/chip8.go +++ b/chip8/chip8.go @@ -4,10 +4,11 @@ import ( _ "embed" "encoding/binary" "image" + "math" ) -//go:embed sound.wav -var Sound []byte +//go:embed beep.wav +var Beep []byte const ( MemorySize = 4096 // 4KB of memory @@ -19,13 +20,14 @@ const ( ) const ( - Width = 64 - Height = 32 - FPS = 60 + Width = 64 + Height = 32 + FPS = 60 + SampleRate = 44100 ) type KeyPressed func(key uint8) bool -type PlaySound func(sound []byte) func() +type SoundPlayer func(sound []byte) (func(), func()) type ROM struct { Name string @@ -43,14 +45,14 @@ type Emulator struct { ROM ROM // game rom Display *image.RGBA // display buffer KeyPressed KeyPressed // input function - PlaySound PlaySound - StopSound func() + PlaySound func() // play sound effect + StopSound func() // stop sound effect } -func NewEmulator(keyPressed KeyPressed, playSound PlaySound) *Emulator { +func NewEmulator(keyPressed KeyPressed, soundPlayer SoundPlayer) *Emulator { emulator := new(Emulator) emulator.KeyPressed = keyPressed - emulator.PlaySound = playSound + emulator.PlaySound, emulator.StopSound = soundPlayer(Beep) emulator.Stack = NewStack() emulator.Display = image.NewRGBA(image.Rect(0, 0, Width, Height)) emulator.Reset() @@ -97,20 +99,13 @@ func (emulator *Emulator) LoadFont() { } func (emulator *Emulator) UpdateTimers() { - if emulator.DT > 0 { - emulator.DT -= 1 - } - if emulator.ST > 0 { - emulator.ST -= 1 - if emulator.StopSound == nil { - emulator.StopSound = emulator.PlaySound(Sound) - } + if emulator.ST != 0 { + emulator.PlaySound() } else { - if emulator.StopSound != nil { - emulator.StopSound() - emulator.StopSound = nil - } + emulator.StopSound() } + emulator.ST = uint8(math.Max(0, float64(emulator.ST)-1)) + emulator.DT = uint8(math.Max(0, float64(emulator.DT)-1)) } func (emulator *Emulator) Cycle() { diff --git a/chip8/chip8_test.go b/chip8/chip8_test.go index 9c7921c..2972ca2 100644 --- a/chip8/chip8_test.go +++ b/chip8/chip8_test.go @@ -8,7 +8,9 @@ import ( ) func TestEmulator_Reset(t *testing.T) { - emulator := chip8.NewEmulator(nil, nil) + soundPlayer := func(sound []byte) (func(), func()) { return nil, nil } + + emulator := chip8.NewEmulator(nil, soundPlayer) emulator.V[0x03] = 0xFF emulator.V[0x0F] = 0xBB diff --git a/chip8/sound.wav b/chip8/sound.wav deleted file mode 100644 index 657aa259f427c5a38d61397d7739e5ee19cec683..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102615 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 IuF{YH0Sfe`9smFU diff --git a/main.go b/main.go index 68951eb..1004071 100644 --- a/main.go +++ b/main.go @@ -50,71 +50,81 @@ var KeyMapping = []ebiten.Key{ ebiten.KeyV, } -type UI struct { - Emulator *chip8.Emulator - State State - AudioContext *audio.Context +type GUI struct { + State State + Emulator *chip8.Emulator } -func (ui *UI) Run() { +func (gui *GUI) Run() { for { - ui.Emulator.Cycle() + gui.Emulator.Cycle() time.Sleep(time.Millisecond * 2) } } -func (ui *UI) Update() error { +func (gui *GUI) Update() error { if ebiten.IsKeyPressed(ebiten.KeyEscape) { - ui.Emulator.Reset() + gui.Emulator.Reset() } - - switch ui.State { + switch gui.State { case LoadingState: - go ui.Run() - ui.State = RunningState + go gui.Run() + gui.State = RunningState case RunningState: - ui.Emulator.UpdateTimers() + gui.Emulator.UpdateTimers() } return nil } -func (ui *UI) Draw(screen *ebiten.Image) { - frame := ebiten.NewImageFromImage(ui.Emulator.Display) - +func (gui *GUI) Draw(screen *ebiten.Image) { + frame := ebiten.NewImageFromImage(gui.Emulator.Display) operation := new(ebiten.DrawImageOptions) operation.GeoM.Scale(ScreenScale, ScreenScale) - screen.DrawImage(frame, operation) } -func (ui *UI) Layout(int, int) (int, int) { +func (gui *GUI) Layout(int, int) (int, int) { return Width, Height } -func (ui *UI) KeyPressed(key uint8) bool { +func KeyPressed(key uint8) bool { return ebiten.IsKeyPressed(KeyMapping[key]) } -func (ui *UI) PlaySound(sound []byte) func() { - player := ui.AudioContext.NewPlayerFromBytes(sound) +func SoundPlayer(sound []byte) (func(), func()) { + player := audio.NewContext(chip8.SampleRate).NewPlayerFromBytes(sound) player.SetVolume(0.3) - player.Play() - return func() { _ = player.Close() } + return PlaySound(player), StopSound(player) +} + +func PlaySound(player *audio.Player) func() { + return func() { + if player.IsPlaying() { + return + } + player.Play() + } +} + +func StopSound(player *audio.Player) func() { + return func() { + player.Pause() + player.Seek(0) + } } func main() { rom := LoadROM() - ui := UI{} - ui.Emulator = chip8.NewEmulator(ui.KeyPressed, ui.PlaySound) - ui.State = LoadingState - ui.AudioContext = audio.NewContext(44100) - ui.Emulator.LoadROM(rom) + gui := GUI{} + gui.State = LoadingState + gui.Emulator = chip8.NewEmulator(KeyPressed, SoundPlayer) + gui.Emulator.LoadROM(rom) ebiten.SetWindowSize(Width, Height) ebiten.SetWindowTitle("CHIP-8 : " + rom.Name) - if err := ebiten.RunGame(&ui); err != nil { + if err := ebiten.RunGame(&gui); err != nil { log.Fatal(err) } }