2025-08-13 00:36:32 +00:00
|
|
|
|
/*!
|
|
|
|
|
|
* AudioBox - 音声再生・合成Box
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ## 📝 概要
|
|
|
|
|
|
* Web Audio APIを使用してブラウザでの音声再生、
|
|
|
|
|
|
* 合成、エフェクト処理を統一的に管理するBox。
|
|
|
|
|
|
* ゲーム、音楽アプリ、オーディオビジュアライザー開発に最適。
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ## 🛠️ 利用可能メソッド
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ### 🔊 基本再生
|
|
|
|
|
|
* - `loadAudio(url)` - 音声ファイル読み込み
|
|
|
|
|
|
* - `play()` - 再生開始
|
|
|
|
|
|
* - `pause()` - 一時停止
|
|
|
|
|
|
* - `stop()` - 停止
|
|
|
|
|
|
* - `setVolume(volume)` - 音量設定 (0.0-1.0)
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ### 🎵 音声合成
|
|
|
|
|
|
* - `createTone(frequency, duration)` - 純音生成
|
|
|
|
|
|
* - `createNoise(type, duration)` - ノイズ生成
|
|
|
|
|
|
* - `createBeep()` - システム音
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ### 📊 解析・ビジュアライザー
|
|
|
|
|
|
* - `getFrequencyData()` - 周波数解析データ取得
|
|
|
|
|
|
* - `getWaveformData()` - 波形データ取得
|
|
|
|
|
|
* - `getVolume()` - 現在の音量レベル
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ### 🎛️ エフェクト
|
|
|
|
|
|
* - `addReverb(room)` - リバーブエフェクト
|
|
|
|
|
|
* - `addFilter(type, frequency)` - フィルター適用
|
|
|
|
|
|
* - `addDistortion(amount)` - ディストーション
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* ## 💡 使用例
|
|
|
|
|
|
* ```nyash
|
|
|
|
|
|
* local audio, visualizer
|
|
|
|
|
|
* audio = new AudioBox()
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* // 効果音再生
|
|
|
|
|
|
* audio.loadAudio("sounds/explosion.wav")
|
|
|
|
|
|
* audio.setVolume(0.7)
|
|
|
|
|
|
* audio.play()
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* // 音声合成
|
|
|
|
|
|
* audio.createTone(440, 1000) // A4音を1秒
|
|
|
|
|
|
* audio.createBeep() // システム音
|
2025-09-17 07:43:07 +09:00
|
|
|
|
*
|
2025-08-13 00:36:32 +00:00
|
|
|
|
* // オーディオビジュアライザー
|
|
|
|
|
|
* local freqData = audio.getFrequencyData()
|
|
|
|
|
|
* // freqDataを使用してcanvasに描画
|
|
|
|
|
|
* ```
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-09-17 07:43:07 +09:00
|
|
|
|
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
|
2025-08-13 00:36:32 +00:00
|
|
|
|
use std::any::Any;
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
use web_sys::{
|
2025-09-17 07:43:07 +09:00
|
|
|
|
AnalyserNode, AudioBuffer, AudioBufferSourceNode, AudioContext, AudioDestinationNode, GainNode,
|
|
|
|
|
|
OscillatorNode, PeriodicWave,
|
2025-08-13 00:36:32 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/// 音声管理Box
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct AudioBox {
|
|
|
|
|
|
base: BoxBase,
|
|
|
|
|
|
volume: f64,
|
|
|
|
|
|
is_playing: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AudioBox {
|
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
base: BoxBase::new(),
|
|
|
|
|
|
volume: 1.0,
|
|
|
|
|
|
is_playing: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 音量を設定 (0.0 - 1.0)
|
|
|
|
|
|
pub fn set_volume(&mut self, volume: f64) {
|
|
|
|
|
|
self.volume = volume.max(0.0).min(1.0);
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(gain) = &self.gain_node {
|
|
|
|
|
|
gain.gain().set_value(self.volume as f32);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 現在の音量を取得
|
|
|
|
|
|
pub fn get_volume(&self) -> f64 {
|
|
|
|
|
|
self.volume
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
/// 指定周波数の純音を生成
|
|
|
|
|
|
pub fn create_tone(&self, frequency: f64, duration_ms: f64) -> bool {
|
|
|
|
|
|
if let Some(context) = &self.context {
|
|
|
|
|
|
if let Ok(oscillator) = context.create_oscillator() {
|
|
|
|
|
|
if let Ok(gain) = context.create_gain() {
|
|
|
|
|
|
// 周波数設定
|
|
|
|
|
|
oscillator.frequency().set_value(frequency as f32);
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
// 音量設定
|
|
|
|
|
|
gain.gain().set_value(self.volume as f32);
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
// ノード接続
|
2025-09-17 07:43:07 +09:00
|
|
|
|
oscillator
|
|
|
|
|
|
.connect_with_audio_node(&gain)
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
gain.connect_with_audio_node(&context.destination())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
// 再生
|
|
|
|
|
|
let start_time = context.current_time();
|
|
|
|
|
|
let end_time = start_time + duration_ms / 1000.0;
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
oscillator.start_with_when(start_time).unwrap_or_default();
|
|
|
|
|
|
oscillator.stop_with_when(end_time).unwrap_or_default();
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
/// システムビープ音を生成
|
|
|
|
|
|
pub fn create_beep(&self) -> bool {
|
|
|
|
|
|
self.create_tone(800.0, 200.0) // 800Hz、200ms
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
/// ホワイトノイズを生成
|
|
|
|
|
|
pub fn create_noise(&self, duration_ms: f64) -> bool {
|
|
|
|
|
|
if let Some(context) = &self.context {
|
|
|
|
|
|
let sample_rate = context.sample_rate() as usize;
|
|
|
|
|
|
let length = ((duration_ms / 1000.0) * sample_rate as f64) as u32;
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
if let Ok(buffer) = context.create_buffer(1, length, sample_rate as f32) {
|
|
|
|
|
|
if let Ok(channel_data) = buffer.get_channel_data(0) {
|
|
|
|
|
|
// ホワイトノイズデータ生成
|
|
|
|
|
|
for i in 0..channel_data.length() {
|
|
|
|
|
|
let noise = (js_sys::Math::random() - 0.5) * 2.0; // -1.0 to 1.0
|
|
|
|
|
|
channel_data.set_index(i, noise as f32);
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
if let Ok(source) = context.create_buffer_source() {
|
|
|
|
|
|
source.set_buffer(Some(&buffer));
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
if let Ok(gain) = context.create_gain() {
|
|
|
|
|
|
gain.gain().set_value(self.volume as f32);
|
|
|
|
|
|
source.connect_with_audio_node(&gain).unwrap_or_default();
|
2025-09-17 07:43:07 +09:00
|
|
|
|
gain.connect_with_audio_node(&context.destination())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
source.start().unwrap_or_default();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
/// 周波数解析データを取得 (オーディオビジュアライザー用)
|
|
|
|
|
|
pub fn get_frequency_data(&self) -> Vec<u8> {
|
|
|
|
|
|
if let Some(analyser) = &self.analyser_node {
|
|
|
|
|
|
let buffer_length = analyser.frequency_bin_count() as usize;
|
|
|
|
|
|
let mut data_array = vec![0u8; buffer_length];
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
// 周波数データを取得
|
|
|
|
|
|
analyser.get_byte_frequency_data(&mut data_array);
|
|
|
|
|
|
return data_array;
|
|
|
|
|
|
}
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
/// 波形データを取得
|
|
|
|
|
|
pub fn get_waveform_data(&self) -> Vec<u8> {
|
|
|
|
|
|
if let Some(analyser) = &self.analyser_node {
|
|
|
|
|
|
let buffer_length = analyser.fft_size() as usize;
|
|
|
|
|
|
let mut data_array = vec![0u8; buffer_length];
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
// 時間領域データを取得
|
|
|
|
|
|
analyser.get_byte_time_domain_data(&mut data_array);
|
|
|
|
|
|
return data_array;
|
|
|
|
|
|
}
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 再生状態を確認
|
|
|
|
|
|
pub fn is_playing(&self) -> bool {
|
|
|
|
|
|
self.is_playing
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
/// Non-WASM環境用のダミー実装
|
|
|
|
|
|
pub fn create_tone(&self, frequency: f64, duration: f64) -> bool {
|
2025-09-17 07:43:07 +09:00
|
|
|
|
println!(
|
|
|
|
|
|
"AudioBox: Playing tone {}Hz for {}ms (simulated)",
|
|
|
|
|
|
frequency, duration
|
|
|
|
|
|
);
|
2025-08-13 00:36:32 +00:00
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub fn create_beep(&self) -> bool {
|
|
|
|
|
|
println!("AudioBox: Beep sound (simulated)");
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub fn create_noise(&self, duration: f64) -> bool {
|
|
|
|
|
|
println!("AudioBox: White noise for {}ms (simulated)", duration);
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub fn get_frequency_data(&self) -> Vec<u8> {
|
|
|
|
|
|
// シミュレーション用のダミーデータ
|
2025-09-17 07:43:07 +09:00
|
|
|
|
(0..64)
|
|
|
|
|
|
.map(|i| ((i as f64 * 4.0).sin() * 128.0 + 128.0) as u8)
|
|
|
|
|
|
.collect()
|
2025-08-13 00:36:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub fn get_waveform_data(&self) -> Vec<u8> {
|
|
|
|
|
|
// シミュレーション用のダミーデータ
|
2025-09-17 07:43:07 +09:00
|
|
|
|
(0..128)
|
|
|
|
|
|
.map(|i| ((i as f64 * 0.1).sin() * 64.0 + 128.0) as u8)
|
|
|
|
|
|
.collect()
|
2025-08-13 00:36:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// オーディオコンテキストの状態を確認
|
|
|
|
|
|
pub fn is_context_running(&self) -> bool {
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(context) = &self.context {
|
|
|
|
|
|
return context.state() == web_sys::AudioContextState::Running;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
true // Non-WASM環境では常にtrue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// オーディオコンテキストを再開 (ユーザー操作後に必要)
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
pub fn resume_context(&self) {
|
|
|
|
|
|
if let Some(context) = &self.context {
|
|
|
|
|
|
if context.state() == web_sys::AudioContextState::Suspended {
|
|
|
|
|
|
let _ = context.resume();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub fn resume_context(&self) {
|
|
|
|
|
|
println!("AudioBox: Resume context (simulated)");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl BoxCore for AudioBox {
|
|
|
|
|
|
fn box_id(&self) -> u64 {
|
|
|
|
|
|
self.base.id
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
fn parent_type_id(&self) -> Option<std::any::TypeId> {
|
|
|
|
|
|
self.base.parent_type_id
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
2025-09-17 07:43:07 +09:00
|
|
|
|
write!(
|
|
|
|
|
|
f,
|
|
|
|
|
|
"AudioBox(volume={:.2}, playing={})",
|
|
|
|
|
|
self.volume, self.is_playing
|
|
|
|
|
|
)
|
2025-08-13 00:36:32 +00:00
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
fn as_any(&self) -> &dyn Any {
|
|
|
|
|
|
self
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
|
|
|
|
self
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl NyashBox for AudioBox {
|
|
|
|
|
|
fn clone_box(&self) -> Box<dyn NyashBox> {
|
2025-08-15 14:29:47 +09:00
|
|
|
|
Box::new(self.clone())
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-15 04:29:41 +00:00
|
|
|
|
/// 仮実装: clone_boxと同じ(後で修正)
|
|
|
|
|
|
fn share_box(&self) -> Box<dyn NyashBox> {
|
|
|
|
|
|
self.clone_box()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
fn to_string_box(&self) -> StringBox {
|
2025-09-17 07:43:07 +09:00
|
|
|
|
StringBox::new(format!(
|
|
|
|
|
|
"AudioBox(volume={:.2}, playing={})",
|
|
|
|
|
|
self.volume, self.is_playing
|
|
|
|
|
|
))
|
2025-08-13 00:36:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn type_name(&self) -> &'static str {
|
|
|
|
|
|
"AudioBox"
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
|
2025-08-13 00:36:32 +00:00
|
|
|
|
fn equals(&self, other: &dyn NyashBox) -> BoolBox {
|
|
|
|
|
|
if let Some(other_audio) = other.as_any().downcast_ref::<AudioBox>() {
|
|
|
|
|
|
BoolBox::new(self.base.id == other_audio.base.id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
BoolBox::new(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for AudioBox {
|
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
|
self.fmt_box(f)
|
|
|
|
|
|
}
|
2025-09-17 07:43:07 +09:00
|
|
|
|
}
|