Skip to main content

strat9_kernel/arch/x86_64/
speaker.rs

1//! PC Speaker driver : beep codes + music playback.
2//!
3//! Hardware: PIT channel 2 (port 0x42) generates a square wave,
4//! port 0x61 bits 0-1 gate the speaker on/off.
5//!
6//! Two modes:
7//! 1. **Blocking** (boot): `beep()` busy-waits : used before scheduler.
8//! 2. **Music** (runtime): note queue drained by PIT timer interrupt.
9
10use super::io::{inb, outb};
11use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering};
12
13const PIT_FREQ: u32 = 1_193_182;
14const PIT_CHANNEL2_CMD: u8 = 0xB2; // channel 2, lobyte/hibyte, mode 3 (square wave), binary
15const SPEAKER_GATE_BIT: u8 = 1 << 0;
16const SPEAKER_DATA_BIT: u8 = 1 << 1;
17
18static SPEAKER_ACTIVE: AtomicBool = AtomicBool::new(false);
19static MUSIC_PLAYING: AtomicBool = AtomicBool::new(false);
20
21/// Current note frequency (Hz). Updated by the music player on timer tick.
22static NOTE_FREQ: AtomicU32 = AtomicU32::new(0);
23
24/// Remaining ticks for the current note.
25static NOTE_TICKS_LEFT: AtomicU32 = AtomicU32::new(0);
26
27/// Index into the current note sequence.
28static NOTE_INDEX: AtomicU32 = AtomicU32::new(0);
29
30/// Total number of notes in the current sequence.
31static NOTE_TOTAL: AtomicU32 = AtomicU32::new(0);
32
33/// Pointer to the note sequence array (freq in Hz).
34static mut NOTE_SEQ_PTR: *const u32 = core::ptr::null();
35
36/// Pointer to the duration array (in milliseconds).
37static mut NOTE_DUR_PTR: *const u32 = core::ptr::null();
38
39/// Tempo multiplier (100 = normal, 200 = double speed, 50 = half speed).
40static TEMPO: AtomicU16 = AtomicU16::new(100);
41
42// =========================================================================
43// Low-level speaker control
44// =========================================================================
45
46/// Set the PIT channel 2 frequency. `hz` = 0 disables the tone.
47#[inline]
48unsafe fn pit_set_freq(hz: u32) {
49    if hz == 0 || hz > 100_000 {
50        outb(0x43, PIT_CHANNEL2_CMD);
51        outb(0x42, 0);
52        outb(0x42, 0);
53        return;
54    }
55    let count = PIT_FREQ / hz;
56    outb(0x43, PIT_CHANNEL2_CMD);
57    outb(0x42, (count & 0xFF) as u8);
58    outb(0x42, ((count >> 8) & 0xFF) as u8);
59}
60
61/// Enable or disable the speaker output.
62#[inline]
63unsafe fn speaker_enable(on: bool) {
64    let val = inb(0x61);
65    if on {
66        outb(0x61, val | SPEAKER_GATE_BIT | SPEAKER_DATA_BIT);
67    } else {
68        outb(0x61, val & !(SPEAKER_GATE_BIT | SPEAKER_DATA_BIT));
69    }
70}
71
72// =========================================================================
73// Blocking API (pre-scheduler, boot-time diagnostics)
74// =========================================================================
75
76/// Play a tone at `freq` Hz for `ms` milliseconds. Blocks via PIT busy-wait.
77pub fn beep(freq: u32, ms: u32) {
78    if freq == 0 || ms == 0 {
79        return;
80    }
81    unsafe {
82        pit_set_freq(freq);
83        speaker_enable(true);
84    }
85    pit_busy_wait(ms);
86    unsafe {
87        speaker_enable(false);
88    }
89}
90
91/// Silence the speaker immediately.
92pub fn beep_off() {
93    unsafe {
94        speaker_enable(false);
95    }
96    SPEAKER_ACTIVE.store(false, Ordering::Relaxed);
97}
98
99/// Busy-wait for `ms` milliseconds using PIT channel 2 (mode 0, one-shot).
100/// Works before the APIC timer / scheduler are running.
101fn pit_busy_wait(ms: u32) {
102    if ms == 0 {
103        return;
104    }
105    // PIT one-shot: count = microseconds * PIT_FREQ / 1_000_000
106    // For large waits, split into chunks of ~50ms (PIT 16-bit max count ~54ms)
107    let chunk_ms = core::cmp::min(ms, 50);
108    let chunks = (ms + chunk_ms - 1) / chunk_ms;
109
110    for _ in 0..chunks {
111        let wait_ms = core::cmp::min(chunk_ms, ms);
112        let count = (PIT_FREQ * wait_ms) / 1000;
113        let count = count.min(0xFFFF) as u16;
114
115        unsafe {
116            // Gate LOW → disable counting
117            let val = inb(0x61);
118            outb(0x61, val & !SPEAKER_GATE_BIT);
119
120            // Program PIT channel 2: mode 0 (one-shot)
121            outb(0x43, 0xB0);
122            outb(0x42, (count & 0xFF) as u8);
123            outb(0x42, ((count >> 8) & 0xFF) as u8);
124
125            // Gate HIGH → start counting
126            outb(0x61, val | SPEAKER_GATE_BIT);
127
128            // Poll bit 5 of port 0x61 (T2 output) until it goes HIGH
129            loop {
130                if inb(0x61) & (1 << 5) != 0 {
131                    break;
132                }
133                core::hint::spin_loop();
134            }
135        }
136    }
137}
138
139// =========================================================================
140// Boot diagnostic beep codes
141// =========================================================================
142
143/// Short beep : milestone reached.
144pub fn beep_ok() {
145    beep(880, 80); // A5, 80ms
146}
147
148/// Long beep : critical failure.
149pub fn beep_fail() {
150    beep(220, 300); // A3, 300ms
151}
152
153/// Double beep : warning.
154pub fn beep_warn() {
155    beep(660, 60); // E5, 60ms
156    pit_busy_wait(60);
157    beep(660, 60);
158}
159
160// =========================================================================
161// Boot crescendo : each phase plays a higher pitch
162// =========================================================================
163
164/// Play a crescendo beep for boot phase N.
165/// Each successive phase uses a higher frequency so you can identify
166/// the last completed phase by pitch alone.
167pub fn beep_phase(phase: u8) {
168    const PHASE_FREQS: [u32; 24] = [
169        // Boot fundamentals (1-6)
170        262, // 1: Kernel entry (C4)
171        294, // 2: Memory manager (D4)
172        330, // 3: Paging (E4)
173        349, // 4: APIC + SMP (F4)
174        392, // 5: Scheduler (G4)
175        440, // 6: Hardware drivers (A4)
176        // hardware::init() sub-drivers (7-14)
177        494, // 7: EC (B4)
178        523, // 8: Thermal (C5)
179        587, // 9: NIC (D5)
180        659, // 10: Storage (E5)
181        698, // 11: Timer (F5)
182        784, // 12: USB (G5)
183        880, // 13: VirtIO GPU (A5)
184        988, // 14: Framebuffer (B5)
185        // Individual drivers (15+)
186        1047, // 15: Hardware milestone (C6)
187        1175, // 16: Timers detailed (D6)
188        1319, // 17: USB detailed (E6)
189        1480, // 18: VirtIO block (F6)
190        1661, // 19: AHCI (G6)
191        1865, // 20: ATA (A6)
192        2093, // 21: NVMe (C7)
193        2349, // 22: VirtIO net (D7)
194        2637, // 23: VirtIO RNG (E7)
195        2794, // 24: VirtIO Console (F7)
196    ];
197    let idx = (phase as usize).min(PHASE_FREQS.len() - 1);
198    beep(PHASE_FREQS[idx], 60);
199}
200
201/// Panic melody : descending notes.
202pub fn beep_panic() {
203    let notes: [(u32, u32); 6] = [
204        (880, 150),
205        (740, 150),
206        (622, 150),
207        (523, 150),
208        (440, 150),
209        (330, 400),
210    ];
211    for (freq, dur) in notes {
212        beep(freq, dur);
213        pit_busy_wait(20);
214    }
215}
216
217/// Startup jingle : ascending notes (happy boot).
218pub fn beep_startup() {
219    let notes: [(u32, u32); 5] = [
220        (523, 80),   // C5
221        (659, 80),   // E5
222        (784, 80),   // G5
223        (1047, 80),  // C6
224        (1319, 150), // E6
225    ];
226    for (freq, dur) in notes {
227        beep(freq, dur);
228        pit_busy_wait(30);
229    }
230}
231
232// =========================================================================
233// Non-blocking music player (runtime, uses PIT tick)
234// =========================================================================
235
236/// A musical note: frequency in Hz and duration code.
237/// Duration codes map to note lengths at a given tempo.
238///
239/// | Code | Note value | Example at 120 BPM |
240/// |------|-----------|-------------------|
241/// | 1    | Whole     | 2000ms            |
242/// | 2    | Half      | 1000ms            |
243/// | 4    | Quarter   | 500ms             |
244/// | 8    | Eighth    | 250ms             |
245/// | 16   | Sixteenth | 125ms             |
246pub struct Note {
247    pub freq: u32,
248    pub dur: u32, // duration code (1, 2, 4, 8, 16)
249}
250
251/// Special frequency values.
252pub const REST: u32 = 0; // silence
253pub const END: u32 = 0xFFFF; // end of sequence
254
255/// Start playing a note sequence. Non-blocking : drained by `speaker_tick()`.
256///
257/// # Safety
258/// `notes` and `durs` must remain valid until playback completes.
259pub unsafe fn music_start(notes: &[u32], durs: &[u32], tempo_bpm: u32) {
260    if notes.is_empty() || durs.is_empty() {
261        return;
262    }
263    // Tempo: quarter note duration in ms = 60000 / bpm
264    // Multiply by 100 for precision: 6_000_000 / bpm
265    let quarter_ms = 6_000_000u32 / tempo_bpm;
266    TEMPO.store((quarter_ms / 100) as u16, Ordering::Relaxed);
267
268    NOTE_SEQ_PTR = notes.as_ptr();
269    NOTE_DUR_PTR = durs.as_ptr();
270    NOTE_TOTAL.store(notes.len() as u32, Ordering::Relaxed);
271    NOTE_INDEX.store(0, Ordering::Relaxed);
272    MUSIC_PLAYING.store(true, Ordering::SeqCst);
273
274    // Start first note immediately
275    speaker_tick();
276}
277
278/// Stop music playback.
279pub fn music_stop() {
280    MUSIC_PLAYING.store(false, Ordering::SeqCst);
281    beep_off();
282}
283
284/// Returns whether music is currently playing.
285pub fn music_playing() -> bool {
286    MUSIC_PLAYING.load(Ordering::SeqCst)
287}
288
289/// Call this from the PIT timer interrupt (~10ms tick) to advance music playback.
290///
291/// The caller (timer ISR) must hold no locks that `speaker_tick` could contend.
292pub fn speaker_tick() {
293    if !MUSIC_PLAYING.load(Ordering::SeqCst) {
294        return;
295    }
296
297    let ticks_left = NOTE_TICKS_LEFT.load(Ordering::Relaxed);
298    if ticks_left > 1 {
299        NOTE_TICKS_LEFT.store(ticks_left - 1, Ordering::Relaxed);
300        return;
301    }
302
303    // Advance to next note
304    let idx = NOTE_INDEX.load(Ordering::Relaxed);
305    let total = NOTE_TOTAL.load(Ordering::Relaxed);
306
307    if idx >= total || unsafe { *NOTE_SEQ_PTR.add(idx as usize) } == END {
308        music_stop();
309        return;
310    }
311
312    let freq = unsafe { *NOTE_SEQ_PTR.add(idx as usize) };
313    let dur_code = unsafe { *NOTE_DUR_PTR.add(idx as usize) };
314
315    NOTE_INDEX.store(idx + 1, Ordering::Relaxed);
316
317    // Calculate duration in ticks (10ms per tick)
318    // base_ms = (quarter_ms * 4) / dur_code
319    let tempo = TEMPO.load(Ordering::Relaxed) as u32;
320    let quarter_ms = tempo * 100;
321    let base_ms = (quarter_ms * 4) / dur_code.max(1);
322    let ticks = (base_ms / 10).max(1);
323
324    NOTE_TICKS_LEFT.store(ticks, Ordering::Relaxed);
325    NOTE_FREQ.store(freq, Ordering::Relaxed);
326
327    unsafe {
328        if freq == REST || freq == 0 {
329            speaker_enable(false);
330        } else {
331            pit_set_freq(freq);
332            speaker_enable(true);
333        }
334    }
335}
336
337// =========================================================================
338// Built-in melodies
339// =========================================================================
340
341/// The Imperial March (Star Wars) : for panic screens.
342/// Notes: (freq Hz, duration code)
343pub const IMPERIAL_MARCH_NOTES: [u32; 16] = [
344    392, 392, 392, 311, 466, 392, 311, 466, 392, 0, 587, 587, 587, 622, 466, 0,
345];
346pub const IMPERIAL_MARCH_DURS: [u32; 16] = [4, 4, 4, 8, 2, 4, 8, 2, 4, 4, 4, 4, 4, 4, 2, 4];
347
348/// Startup jingle (ascending arpeggio).
349pub const STARTUP_NOTES: [u32; 7] = [523, 0, 659, 0, 784, 0, 1047];
350pub const STARTUP_DURS: [u32; 7] = [8, 16, 8, 16, 8, 16, 4];
351
352/// Error / failure tone.
353pub const ERROR_NOTES: [u32; 4] = [440, 415, 370, 330];
354pub const ERROR_DURS: [u32; 4] = [4, 4, 4, 2];