3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

キー入力とサウンド、意外とムズい Part4【完結】

Last updated at Posted at 2025-12-09

Part1 | Part2 | Part3 | Part4【完結】


はじめに

Part3でSDL2を使ったGUI描画ができるようになりました。

でもまだ足りないものがある...

  • キー入力 → ゲームを操作できない
  • サウンド → ピコピコ音が出ない

今回はこの2つを実装します。意外とムズいんですよ、これが。


目次

  1. CHIP-8のキー入力
  2. FX0A命令(キー入力待ち)
  3. SDL2でのキー処理
  4. サウンドタイマー
  5. 矩形波を生成する
  6. 実装と動作確認

CHIP-8のキー入力

16キーのレイアウト

CHIP-8は16個のキー(0-9, A-F)を使います:

CHIP-8のキー:
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ C │
├───┼───┼───┼───┤
│ 4 │ 5 │ 6 │ D │
├───┼───┼───┼───┤
│ 7 │ 8 │ 9 │ E │
├───┼───┼───┼───┤
│ A │ 0 │ B │ F │
└───┴───┴───┴───┘

これをキーボードにマッピングします:

キーボード:
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │
├───┼───┼───┼───┤
│ Q │ W │ E │ R │
├───┼───┼───┼───┤
│ A │ S │ D │ F │
├───┼───┼───┼───┤
│ Z │ X │ C │ V │
└───┴───┴───┴───┘

キー関連命令

オペコード 説明
EX9E VXのキーが押されていたらスキップ
EXA1 VXのキーが押されていなかったらスキップ
FX0A キーが押されるまで待機

FX0A命令(キー入力待ち)

この命令が一番厄介でした...

仕様

FX0A - LD Vx, K
キーが押されるまでエミュレータを停止し、
押されたキーの値をVxに格納する

問題: 「エミュレータを停止」ってどうやるの?

実装方法

フラグを使って状態管理します:

struct Chip8 {
    // ...
    waiting_for_key: bool,    // キー入力待ち状態
    key_register: usize,      // キーを格納するレジスタ
}

FX0A命令が来たらフラグを立てる:

(0xF, _, 0x0, 0xA) => {
    self.waiting_for_key = true;
    self.key_register = x;
    println!("Waiting for key input...");
}

cycle関数で状態チェック:

fn cycle(&mut self) {
    // キー入力待ち中は何もしない
    if self.waiting_for_key {
        return;
    }
    
    let opcode = self.fetch();
    self.execute(opcode);
}

キーが押されたら解除:

fn key_press(&mut self, key: u8) {
    if key < KEY_COUNT as u8 {
        self.keypad[key as usize] = true;
        
        // キー入力待ち中だったら解除
        if self.waiting_for_key {
            self.v[self.key_register] = key;
            self.waiting_for_key = false;
            println!("Key {:X} pressed!", key);
        }
    }
}

図解:キー入力待ちの流れ

┌─────────────┐
│  FX0A実行   │
└─────┬───────┘
      ↓
┌─────────────┐
│ waiting = true │
│ register = X │
└─────┬───────┘
      ↓
┌─────────────┐         ┌─────────────┐
│ cycle()は   │────────→│ キー押下?  │
│ 何もしない  │←────────│   (NO)     │
└─────────────┘         └─────────────┘
                              │
                              ↓ (YES)
                        ┌─────────────┐
                        │ V[X] = key  │
                        │ waiting=false│
                        └─────────────┘

SDL2でのキー処理

キーマッピング関数

fn keycode_to_chip8(keycode: Keycode) -> Option<u8> {
    match keycode {
        Keycode::Num1 => Some(0x1),
        Keycode::Num2 => Some(0x2),
        Keycode::Num3 => Some(0x3),
        Keycode::Num4 => Some(0xC),
        Keycode::Q => Some(0x4),
        Keycode::W => Some(0x5),
        Keycode::E => Some(0x6),
        Keycode::R => Some(0xD),
        Keycode::A => Some(0x7),
        Keycode::S => Some(0x8),
        Keycode::D => Some(0x9),
        Keycode::F => Some(0xE),
        Keycode::Z => Some(0xA),
        Keycode::X => Some(0x0),
        Keycode::C => Some(0xB),
        Keycode::V => Some(0xF),
        _ => None,
    }
}

イベント処理

for event in event_pump.poll_iter() {
    match event {
        Event::KeyDown { keycode: Some(keycode), .. } => {
            if let Some(key) = keycode_to_chip8(keycode) {
                chip8.key_press(key);
            }
        }
        Event::KeyUp { keycode: Some(keycode), .. } => {
            if let Some(key) = keycode_to_chip8(keycode) {
                chip8.key_release(key);
            }
        }
        // ...
    }
}

ポイント:KeyDownとKeyUpの両方を処理

CHIP-8のEX9E/EXA1命令は「今この瞬間キーが押されているか」をチェックします。

なので:

  • KeyDownkeypad[key] = true
  • KeyUpkeypad[key] = false

両方処理しないとキーが押しっぱなしになります。


サウンドタイマー

CHIP-8のサウンド仕様

CHIP-8のサウンドはめちゃくちゃシンプル:

  • sound_timer > 0 のときビープ音が鳴る
  • sound_timer は60Hzでデクリメント
  • 音の周波数は規定されていない(一般的には440Hz)
fn tick_timers(&mut self) -> bool {
    let should_beep = self.sound_timer > 0;
    
    if self.delay_timer > 0 {
        self.delay_timer -= 1;
    }
    if self.sound_timer > 0 {
        self.sound_timer -= 1;
    }
    
    should_beep
}

矩形波を生成する

SDL2のオーディオ

SDL2でオーディオを使うには AudioCallback トレイトを実装します:

use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};

struct SquareWave {
    phase_inc: f32,
    phase: f32,
    volume: f32,
}

impl AudioCallback for SquareWave {
    type Channel = f32;

    fn callback(&mut self, out: &mut [f32]) {
        for sample in out.iter_mut() {
            // 矩形波を生成
            *sample = if self.phase <= 0.5 {
                self.volume
            } else {
                -self.volume
            };
            self.phase = (self.phase + self.phase_inc) % 1.0;
        }
    }
}

矩形波とは

  ┌──┐  ┌──┐  ┌──┐
  │  │  │  │  │  │
──┘  └──┘  └──┘  └──

High (+volume) → Low (-volume) を繰り返す

レトロゲームのピコピコ音は、この矩形波で作られています。

オーディオデバイスの作成

fn create_audio_device(sdl_context: &sdl2::Sdl) -> Result<AudioDevice<SquareWave>, String> {
    let audio_subsystem = sdl_context.audio()?;

    let desired_spec = AudioSpecDesired {
        freq: Some(44100),      // サンプリングレート
        channels: Some(1),       // モノラル
        samples: Some(256),      // バッファサイズ
    };

    audio_subsystem.open_playback(None, &desired_spec, |spec| {
        SquareWave {
            phase_inc: 440.0 / spec.freq as f32,  // 440Hz(ラの音)
            phase: 0.0,
            volume: 0.25,
        }
    })
}

再生制御

// メインループ内
if should_beep && !sound_playing {
    audio_device.resume();  // 再生開始
    sound_playing = true;
} else if !should_beep && sound_playing {
    audio_device.pause();   // 一時停止
    sound_playing = false;
}

実装と動作確認

キー入力デモプログラム

キーを押すと対応する文字が表示されるデモ:

fn create_key_demo_program() -> Vec<u8> {
    let mut program = Vec::new();
    
    // CLS
    program.extend_from_slice(&[0x00, 0xE0]);
    
    // LD V0, K (キー入力待ち)
    program.extend_from_slice(&[0xF0, 0x0A]);
    
    // CLS
    program.extend_from_slice(&[0x00, 0xE0]);
    
    // LD F, V0 (フォントアドレス)
    program.extend_from_slice(&[0xF0, 0x29]);
    
    // LD V1, 28 (X座標)
    program.extend_from_slice(&[0x61, 0x1C]);
    
    // LD V2, 13 (Y座標)
    program.extend_from_slice(&[0x62, 0x0D]);
    
    // DRW V1, V2, 5
    program.extend_from_slice(&[0xD1, 0x25]);
    
    // LD V3, 30 (サウンドタイマー = 0.5秒)
    program.extend_from_slice(&[0x63, 0x1E]);
    
    // LD ST, V3 (サウンド開始)
    program.extend_from_slice(&[0xF3, 0x18]);
    
    // JP 0x202 (ループ)
    program.extend_from_slice(&[0x12, 0x02]);
    
    program
}

実行結果

cargo run --bin chip8-sound

出力:

🎮 CHIP-8 Emulator with Sound
================================
Controls:
  1234    →  123C
  QWER    →  456D
  ASDF    →  789E
  ZXCV    →  A0BF
  ESC     →  Quit
  SPACE   →  Reset
  P       →  Pause/Resume
================================

No ROM specified, running key input demo...

ROM loaded: 22 bytes

ウィンドウが開き、キーを押すと:

  1. 画面中央に対応する文字(0-F)が表示される
  2. 「ピー」とビープ音が0.5秒鳴る
  3. 次のキー入力待ちになる

意外とちゃんとゲームっぽくなってきた!


つまづいたポイント

1. キーが離されたときの処理

最初KeyUpを処理してなくて、キーが押しっぱなしになってました。

// ❌ NG: KeyDownだけ処理
Event::KeyDown { .. } => {
    chip8.key_press(key);
}

// ⭕ OK: KeyUpも処理
Event::KeyDown { .. } => { chip8.key_press(key); }
Event::KeyUp { .. } => { chip8.key_release(key); }

2. サウンドが止まらない

sound_timer > 0 のチェックをタイマー更新前にやってしまい、永遠にビープ音が...

// ❌ NG: デクリメント後にチェック
self.sound_timer -= 1;
let should_beep = self.sound_timer > 0;

// ⭕ OK: デクリメント前にチェック
let should_beep = self.sound_timer > 0;
self.sound_timer -= 1;

3. ウィンドウタイトルで状態表示

キー入力待ち中は何も反応しないので、ユーザーが困惑する...

解決策:ウィンドウタイトルに状態を表示!

if chip8.is_waiting_for_key() {
    canvas.window_mut().set_title("CHIP-8 - Waiting for key input...").ok();
} else if paused {
    canvas.window_mut().set_title("CHIP-8 - PAUSED").ok();
} else {
    canvas.window_mut().set_title("CHIP-8 Emulator").ok();
}

まとめ

今回実装したもの ✅

  • 16キー入力 のサポート
  • FX0A(キー入力待ち) の完全実装
  • SDL2オーディオ による矩形波生成
  • サウンドタイマー との連携
  • ポーズ機能 (Pキー)
  • ウィンドウタイトル での状態表示

学んだこと

  1. 状態管理 が重要

    • キー入力待ちはフラグで管理
    • cycle()を止めるだけでOK
  2. AudioCallback の仕組み

    • バッファにサンプルを書き込む
    • resume/pauseで再生制御
  3. ユーザーフィードバック の大切さ

    • 何も反応しないと不安になる
    • ウィンドウタイトルで状態を伝える

参考資料


この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

ここまで読んでくれてありがとうございました!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?