Skip to main content

orinium_browser/platform/audio/
mod.rs

1//! 音声データの管理と再生を行う
2
3use crate::engine::bridge::audio::{AudioError, AudioSink, AudioSinkFactory};
4use crate::platform::io as platform_io;
5use anyhow::{Context, Result};
6use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
7use cpal::{SampleFormat, StreamConfig};
8use std::io::Cursor;
9use std::sync::{Arc, Mutex, MutexGuard};
10use std::time::Duration;
11use symphonia::core::audio::sample::Sample;
12use symphonia::core::codecs::audio::AudioDecoderOptions;
13use symphonia::core::errors::Error;
14use symphonia::core::formats::FormatOptions;
15use symphonia::core::formats::TrackType;
16use symphonia::core::formats::probe::Hint;
17use symphonia::core::io::MediaSourceStream;
18use symphonia::core::meta::MetadataOptions;
19use symphonia::default::{get_codecs, get_probe};
20
21/// Largest audio payload accepted for decoding, guarding against memory
22/// exhaustion from an oversized media source.
23const MAX_AUDIO_INPUT_BYTES: usize = 64 * 1024 * 1024;
24/// Largest total number of decoded interleaved `f32` samples. A small
25/// compressed file can decode to billions of samples (a decompression bomb);
26/// this caps the decoded buffer at roughly 200 MiB.
27const MAX_DECODED_SAMPLES: usize = 50_000_000;
28
29/// 音声の管理を行う構造体
30pub struct SoundManager {
31    /// f32のインタリーブドサンプルバッファ
32    samples: Arc<Mutex<Vec<f32>>>,
33    /// 現在の再生位置(フレーム単位)
34    play_pos: Arc<Mutex<usize>>,
35    /// ソースのチャンネル数
36    src_channels: usize,
37    /// ソースのサンプルレート
38    // TODO: Hzを考慮したコンバーターを実装する
39    src_sample_rate: u32,
40    /// cpalのストリーム
41    stream: Option<cpal::Stream>,
42}
43
44impl SoundManager {
45    /// 初期化
46    pub fn init() -> Result<Arc<Mutex<Self>>> {
47        Ok(Arc::new(Mutex::new(Self::new())))
48    }
49
50    /// .Device を開かずに、遊休状態のマネージャを構築する。
51    ///
52    /// 実際の出力ストリームは最初の再生時に [`Self::ensure_stream`] で
53    /// 確保される。
54    pub fn new() -> Self {
55        SoundManager {
56            samples: Arc::new(Mutex::new(Vec::new())),
57            play_pos: Arc::new(Mutex::new(0)),
58            src_channels: 0,
59            src_sample_rate: 0,
60            stream: None,
61        }
62    }
63
64    /// cpalストリームを確保する
65    fn ensure_stream(&mut self) -> Result<()> {
66        if self.stream.is_some() {
67            return Ok(());
68        }
69
70        let host = cpal::default_host();
71        let device = host
72            .default_output_device()
73            .context("No default output device available")?;
74        let supported_cfg = device
75            .default_output_config()
76            .context("Failed to get default output config")?;
77        let config: StreamConfig = supported_cfg.into();
78        let sample_format = supported_cfg.sample_format();
79        let output_channels = config.channels as usize;
80
81        let samples = Arc::clone(&self.samples);
82        let play_pos = Arc::clone(&self.play_pos);
83        let src_channels = self.src_channels;
84
85        let err_fn = |err| log::error!("cpal stream error: {}", err);
86
87        let latency = Some(Duration::from_millis(100));
88
89        let stream = match sample_format {
90            SampleFormat::F32 => device.build_output_stream(
91                config,
92                move |data: &mut [f32], _| {
93                    write_output_f32(data, src_channels, output_channels, &samples, &play_pos)
94                },
95                err_fn,
96                latency,
97            )?,
98            SampleFormat::I16 => device.build_output_stream(
99                config,
100                move |data: &mut [i16], _| {
101                    write_output_i16(data, src_channels, output_channels, &samples, &play_pos)
102                },
103                err_fn,
104                latency,
105            )?,
106            SampleFormat::U16 => device.build_output_stream(
107                config,
108                move |data: &mut [u16], _| {
109                    write_output_u16(data, src_channels, output_channels, &samples, &play_pos)
110                },
111                err_fn,
112                latency,
113            )?,
114            _ => {
115                return Err(anyhow::anyhow!(
116                    "Unsupported sample format from output device"
117                ));
118            }
119        };
120
121        stream.play()?;
122        self.stream = Some(stream);
123        Ok(())
124    }
125
126    /// Decodes audio bytes and resets playback to the beginning.
127    pub fn load_from_bytes(&mut self, data: &[u8]) -> Result<()> {
128        let (samples, channels, sample_rate) = decode(data)?;
129        // replace buffer
130        {
131            let mut buf = self.samples.lock().unwrap_or_else(|e| e.into_inner());
132            *buf = samples;
133        }
134        // reset position
135        {
136            let mut pos = self.play_pos.lock().unwrap_or_else(|e| e.into_inner());
137            *pos = 0;
138        }
139        self.src_channels = channels;
140        self.src_sample_rate = sample_rate;
141
142        Ok(())
143    }
144
145    /// バイト列から音声を再生する
146    pub fn play_from_bytes(&mut self, data: &[u8]) -> Result<()> {
147        self.load_from_bytes(data)?;
148
149        self.ensure_stream()?;
150
151        Ok(())
152    }
153
154    /// ローカルファイルから音声を再生する
155    pub fn play_from_file(&mut self, path: &str) -> Result<()> {
156        let data = platform_io::load_local_file(path)
157            .with_context(|| format!("Failed to read local file: {}", path))?;
158        self.play_from_bytes(&data)
159    }
160
161    /// URIから音声を再生する(resourceまたはfileスキームに対応)
162    ///
163    /// 通常の再生には `play_from_bytes` を使用してください。
164    /// これはテスト用メソッドです
165    pub fn play_from_local_uri(&mut self, uri: &str) -> Result<()> {
166        if uri.starts_with("resource:") {
167            let rel = uri
168                .trim_start_matches("resource:///")
169                .trim_start_matches("resource://")
170                .trim_start_matches("resource:/")
171                .trim_start_matches("resource:");
172            let rel = rel.trim_start_matches('/');
173            let data = platform_io::load_resource(rel)
174                .with_context(|| format!("Failed to load resource: {}", rel))?;
175            return self.play_from_bytes(&data);
176        }
177        if uri.starts_with("file://") {
178            let p = uri.trim_start_matches("file://");
179            let data = platform_io::load_local_file(p)
180                .with_context(|| format!("Failed to read local file from URI: {}", p))?;
181            return self.play_from_bytes(&data);
182        }
183        let data = platform_io::load_local_file(uri)
184            .with_context(|| format!("Failed to read local file: {}", uri))?;
185        self.play_from_bytes(&data)
186    }
187
188    /// Pauses playback while preserving the current position.
189    pub fn pause(&mut self) -> Result<()> {
190        if let Some(stream) = &self.stream {
191            stream.pause()?;
192        }
193        Ok(())
194    }
195
196    /// Resumes playback from the current position.
197    pub fn resume(&mut self) -> Result<()> {
198        if self.src_channels == 0 {
199            anyhow::bail!("Cannot resume before audio is loaded");
200        }
201        self.ensure_stream()?;
202        if let Some(stream) = &self.stream {
203            stream.play()?;
204        }
205        Ok(())
206    }
207
208    /// Returns the current playback position in seconds.
209    pub fn current_seconds(&self) -> f32 {
210        if self.src_sample_rate == 0 {
211            return 0.0;
212        }
213        let frame = *self.play_pos.lock().unwrap_or_else(|e| e.into_inner());
214        frame as f32 / self.src_sample_rate as f32
215    }
216
217    /// Returns the decoded audio duration in seconds.
218    pub fn duration_seconds(&self) -> f32 {
219        if self.src_channels == 0 || self.src_sample_rate == 0 {
220            return 0.0;
221        }
222        let samples = self.samples.lock().unwrap_or_else(|e| e.into_inner());
223        let frames = samples.len() / self.src_channels;
224        frames as f32 / self.src_sample_rate as f32
225    }
226
227    /// Returns whether the decoded audio reached its end.
228    pub fn is_finished(&self) -> bool {
229        let frame = *self.play_pos.lock().unwrap_or_else(|e| e.into_inner());
230        let samples = self.samples.lock().unwrap_or_else(|e| e.into_inner());
231        self.src_channels > 0 && frame >= samples.len() / self.src_channels
232    }
233}
234
235impl Default for SoundManager {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241/* ============================
242 * Engine bridge
243 * ============================ */
244
245/// [`SoundManager`] を engine の [`AudioSink`] インターフェースへ適合させる。
246///
247/// engine 側の `<audio>` ウィジェットはこの型を知る必要がなく、
248/// `cpal` の出力デバイスも engine には露出しなくなる。
249pub struct PlatformAudioSink {
250    manager: Mutex<SoundManager>,
251}
252
253impl PlatformAudioSink {
254    /// 遊休状態の sink を作る。出力ストリームは最初の再生時に確保される。
255    pub fn new() -> Self {
256        Self {
257            manager: Mutex::new(SoundManager::new()),
258        }
259    }
260
261    /// ポイズンされたロックを握り潰して取り出す。
262    fn lock(&self) -> MutexGuard<'_, SoundManager> {
263        self.manager.lock().unwrap_or_else(|e| e.into_inner())
264    }
265}
266
267impl Default for PlatformAudioSink {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273impl AudioSink for PlatformAudioSink {
274    fn preload(&self, data: &[u8]) -> Result<(), AudioError> {
275        self.lock()
276            .load_from_bytes(data)
277            .map_err(|e| AudioError::Decode(format!("{e:#}")))
278    }
279
280    fn play(&self, source: &str, data: Option<&[u8]>) -> Result<(), AudioError> {
281        let mut manager = self.lock();
282        let result = match data {
283            Some(bytes) => manager.play_from_bytes(bytes),
284            // 取得済みバイトが無い場合は、platform 側で URI を解決する。
285            None => manager.play_from_local_uri(source),
286        };
287        result.map_err(|e| AudioError::Device(format!("{e:#}")))
288    }
289
290    fn pause(&self) -> Result<(), AudioError> {
291        self.lock()
292            .pause()
293            .map_err(|e| AudioError::Device(format!("{e:#}")))
294    }
295
296    fn resume(&self) -> Result<(), AudioError> {
297        self.lock()
298            .resume()
299            .map_err(|e| AudioError::Device(format!("{e:#}")))
300    }
301
302    fn current_seconds(&self) -> f32 {
303        self.lock().current_seconds()
304    }
305
306    fn duration_seconds(&self) -> f32 {
307        self.lock().duration_seconds()
308    }
309
310    fn is_finished(&self) -> bool {
311        self.lock().is_finished()
312    }
313}
314
315/// engine へ注入する [`AudioSinkFactory`]。
316///
317/// `<audio>` 要素ごとに独立した sink(= 独立した再生位置と cpal
318/// ストリーム)を作る。
319#[derive(Debug, Clone, Copy, Default)]
320pub struct PlatformAudioSinkFactory;
321
322impl AudioSinkFactory for PlatformAudioSinkFactory {
323    fn create(&self) -> Arc<dyn AudioSink> {
324        Arc::new(PlatformAudioSink::new())
325    }
326}
327
328/// engine へ注入する既定の [`AudioSinkFactory`] を返す。
329pub fn default_audio_sink_factory() -> Arc<dyn AudioSinkFactory> {
330    Arc::new(PlatformAudioSinkFactory)
331}
332
333/// 音声をデコードする
334fn decode(data: &[u8]) -> Result<(Vec<f32>, usize, u32)> {
335    if data.len() > MAX_AUDIO_INPUT_BYTES {
336        anyhow::bail!(
337            "audio payload of {} bytes exceeds the {MAX_AUDIO_INPUT_BYTES}-byte limit",
338            data.len()
339        );
340    }
341    let cursor = Cursor::new(data.to_vec());
342    let mss = MediaSourceStream::new(Box::new(cursor), Default::default());
343
344    let hint = Hint::new();
345    let fmt_opts: FormatOptions = Default::default();
346    let meta_opts: MetadataOptions = Default::default();
347    let dec_opts: AudioDecoderOptions = Default::default();
348
349    let mut format = get_probe()
350        .probe(&hint, mss, fmt_opts, meta_opts)
351        .context("Failed to probe media format")?;
352
353    let track = format
354        .default_track(TrackType::Audio)
355        .ok_or_else(|| anyhow::anyhow!("No default audio track found"))?;
356
357    let track_id = track.id;
358
359    let audio_params = track
360        .codec_params
361        .as_ref()
362        .and_then(|p| p.audio())
363        .ok_or_else(|| anyhow::anyhow!("Default track has no audio codec parameters"))?;
364
365    let mut decoder = get_codecs()
366        .make_audio_decoder(audio_params, &dec_opts)
367        .context("Failed to create audio decoder")?;
368
369    let mut samples = Vec::<f32>::new();
370    let mut channels = audio_params.channels.as_ref().map_or(1, |c| c.count());
371
372    let mut sample_rate = audio_params.sample_rate.unwrap_or(44100);
373
374    loop {
375        let packet = match format.next_packet() {
376            Ok(Some(packet)) => packet,
377            Ok(None) => break,
378            Err(err) => return Err(err).context("Failed to read media packet"),
379        };
380
381        if packet.track_id != track_id {
382            continue;
383        }
384
385        match decoder.decode(&packet) {
386            Ok(audio_buf) => {
387                channels = audio_buf.spec().channels().count();
388                sample_rate = audio_buf.spec().rate();
389
390                let decoded_len = audio_buf.samples_interleaved();
391                if samples.len().saturating_add(decoded_len) > MAX_DECODED_SAMPLES {
392                    anyhow::bail!("decoded audio exceeds the {MAX_DECODED_SAMPLES}-sample limit");
393                }
394                let mut decoded = vec![f32::MID; decoded_len];
395                audio_buf.copy_to_slice_interleaved(&mut decoded);
396                samples.extend_from_slice(&decoded);
397            }
398            Err(Error::DecodeError(_)) => {
399                continue;
400            }
401            Err(err) => {
402                return Err(err).context("Failed to decode audio packet");
403            }
404        }
405    }
406
407    Ok((samples, channels, sample_rate))
408}
409
410/// 出力バッファに音声データを書き込む(f32)
411fn write_output_f32(
412    output: &mut [f32],
413    src_channels: usize,
414    out_channels: usize,
415    samples: &Arc<Mutex<Vec<f32>>>,
416    pos: &Arc<Mutex<usize>>,
417) {
418    let mut p = pos.lock().unwrap();
419    let buf = samples.lock().unwrap();
420    let total_frames = buf.len().checked_div(src_channels).unwrap_or(0);
421
422    if out_channels == 0 {
423        return;
424    }
425    let frames_to_write = output.len() / out_channels;
426
427    for frame in 0..frames_to_write {
428        if total_frames == 0 || *p >= total_frames {
429            // zero out remaining
430            for ch in 0..out_channels {
431                output[frame * out_channels + ch] = 0.0;
432            }
433            continue;
434        }
435        for ch in 0..out_channels {
436            let src_index = (*p * src_channels) + (ch % src_channels);
437            if src_index < buf.len() {
438                output[frame * out_channels + ch] = buf[src_index];
439            } else {
440                output[frame * out_channels + ch] = 0.0;
441            }
442        }
443        *p += 1;
444    }
445}
446
447/// 出力バッファに音声データを書き込む(i16)
448fn write_output_i16(
449    output: &mut [i16],
450    src_channels: usize,
451    out_channels: usize,
452    samples: &Arc<Mutex<Vec<f32>>>,
453    pos: &Arc<Mutex<usize>>,
454) {
455    let mut p = pos.lock().unwrap();
456    let buf = samples.lock().unwrap();
457    let total_frames = buf.len().checked_div(src_channels).unwrap_or(0);
458
459    if out_channels == 0 {
460        return;
461    }
462    let frames_to_write = output.len() / out_channels;
463
464    for frame in 0..frames_to_write {
465        if total_frames == 0 || *p >= total_frames {
466            for ch in 0..out_channels {
467                output[frame * out_channels + ch] = 0;
468            }
469            continue;
470        }
471        for ch in 0..out_channels {
472            let src_index = (*p * src_channels) + (ch % src_channels);
473            if src_index < buf.len() {
474                let v = buf[src_index].clamp(-1.0, 1.0);
475                output[frame * out_channels + ch] = (v * i16::MAX as f32) as i16;
476            } else {
477                output[frame * out_channels + ch] = 0;
478            }
479        }
480        *p += 1;
481    }
482}
483
484/// 出力バッファに音声データを書き込む(u16)
485fn write_output_u16(
486    output: &mut [u16],
487    src_channels: usize,
488    out_channels: usize,
489    samples: &Arc<Mutex<Vec<f32>>>,
490    pos: &Arc<Mutex<usize>>,
491) {
492    let mut p = pos.lock().unwrap();
493    let buf = samples.lock().unwrap();
494    let total_frames = buf.len().checked_div(src_channels).unwrap_or(0);
495
496    if out_channels == 0 {
497        return;
498    }
499    let frames_to_write = output.len() / out_channels;
500
501    for frame in 0..frames_to_write {
502        if total_frames == 0 || *p >= total_frames {
503            for ch in 0..out_channels {
504                output[frame * out_channels + ch] = 0;
505            }
506            continue;
507        }
508        for ch in 0..out_channels {
509            let src_index = (*p * src_channels) + (ch % src_channels);
510            if src_index < buf.len() {
511                let v = buf[src_index].clamp(-1.0, 1.0);
512                output[frame * out_channels + ch] = ((v * 0.5 + 0.5) * u16::MAX as f32) as u16;
513            } else {
514                output[frame * out_channels + ch] = 0;
515            }
516        }
517        *p += 1;
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    /// Builds a minimal mono 16-bit PCM WAV with `samples` frames.
526    fn wav(samples: &[i16]) -> Vec<u8> {
527        let data_len = (samples.len() * 2) as u32;
528        let mut bytes = Vec::new();
529        bytes.extend_from_slice(b"RIFF");
530        bytes.extend_from_slice(&(36 + data_len).to_le_bytes());
531        bytes.extend_from_slice(b"WAVE");
532        bytes.extend_from_slice(b"fmt ");
533        bytes.extend_from_slice(&16u32.to_le_bytes());
534        bytes.extend_from_slice(&1u16.to_le_bytes()); // PCM
535        bytes.extend_from_slice(&1u16.to_le_bytes()); // channels
536        bytes.extend_from_slice(&8000u32.to_le_bytes()); // sample rate
537        bytes.extend_from_slice(&16000u32.to_le_bytes()); // byte rate
538        bytes.extend_from_slice(&2u16.to_le_bytes()); // block align
539        bytes.extend_from_slice(&16u16.to_le_bytes()); // bits per sample
540        bytes.extend_from_slice(b"data");
541        bytes.extend_from_slice(&data_len.to_le_bytes());
542        for sample in samples {
543            bytes.extend_from_slice(&sample.to_le_bytes());
544        }
545        bytes
546    }
547
548    #[test]
549    fn decodes_a_small_wav() {
550        let (samples, channels, sample_rate) =
551            decode(&wav(&[0, 1000, -1000, 2000])).expect("decodes wav");
552        assert!(!samples.is_empty());
553        assert_eq!(channels, 1);
554        assert_eq!(sample_rate, 8000);
555    }
556
557    #[test]
558    fn rejects_oversized_input_before_decoding() {
559        let oversized = vec![0_u8; MAX_AUDIO_INPUT_BYTES + 1];
560        let error = decode(&oversized).expect_err("oversized input must be rejected");
561        assert!(error.to_string().contains("exceeds"));
562    }
563}