#[cfg(target_os = "windows")]
mod backend {
use std::collections::HashMap;
use std::sync::Arc;
use windows::{
core::PCWSTR,
Win32::{
Media::Audio::{
AUDIO_STREAM_CATEGORY, WAVEFORMATEX,
XAudio2::{
IXAudio2, IXAudio2MasteringVoice, IXAudio2SourceVoice,
IXAudio2VoiceCallback, XAudio2CreateWithVersionInfo,
XAUDIO2_BUFFER, XAUDIO2_COMMIT_NOW,
XAUDIO2_DEFAULT_CHANNELS, XAUDIO2_DEFAULT_FREQ_RATIO,
XAUDIO2_DEFAULT_PROCESSOR, XAUDIO2_DEFAULT_SAMPLERATE,
XAUDIO2_END_OF_STREAM, XAUDIO2_LOOP_INFINITE,
},
},
System::Com::{CoInitializeEx, COINIT_APARTMENTTHREADED},
},
};
const NTDDI_WIN10: u32 = 0x0A00_0000;
struct SoundEntry {
samples: Arc<Vec<i16>>,
channels: u16,
sample_rate: u32,
}
struct ActiveVoice {
voice: IXAudio2SourceVoice,
_data: Arc<Vec<i16>>, }
impl Drop for ActiveVoice {
fn drop(&mut self) {
unsafe {
let _ = self.voice.Stop(0, XAUDIO2_COMMIT_NOW);
self.voice.DestroyVoice();
}
}
}
pub struct AudioState {
voices: HashMap<u32, ActiveVoice>,
sounds: HashMap<u32, SoundEntry>,
next_id: u32,
_master: IXAudio2MasteringVoice,
engine: IXAudio2,
}
impl AudioState {
pub fn new() -> Option<Self> {
unsafe {
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
let mut engine: Option<IXAudio2> = None;
XAudio2CreateWithVersionInfo(
&mut engine,
0,
XAUDIO2_DEFAULT_PROCESSOR,
NTDDI_WIN10,
)
.ok()?;
let engine = engine?;
let mut master: Option<IXAudio2MasteringVoice> = None;
engine
.CreateMasteringVoice(
&mut master,
XAUDIO2_DEFAULT_CHANNELS,
XAUDIO2_DEFAULT_SAMPLERATE,
0,
PCWSTR(std::ptr::null()), None, AUDIO_STREAM_CATEGORY(6), )
.ok()?;
Some(Self {
voices: HashMap::new(),
sounds: HashMap::new(),
next_id: 1,
_master: master?,
engine,
})
}
}
pub fn load(&mut self, path: &str) -> u32 {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => { crate::log_warn!("サウンドファイルを読み込めません: '{path}'"); return 0; }
};
let lower = path.to_ascii_lowercase();
let result = if lower.ends_with(".ogg") {
decode_ogg(&bytes)
} else {
decode_wav(&bytes)
};
let (samples, channels, sample_rate) = match result {
Some(v) => v,
None => {
crate::log_warn!(
"サウンドのデコードに失敗しました: '{path}' (WAV PCM 8/16-bit または OGG Vorbis のみ対応)"
);
return 0;
}
};
let id = self.next_id;
self.next_id += 1;
self.sounds.insert(id, SoundEntry { samples: Arc::new(samples), channels, sample_rate });
id
}
pub fn play(&mut self, handle: u32, looping: bool) {
let Some(entry) = self.sounds.get(&handle) else { return };
self.voices.remove(&handle);
let wfx = WAVEFORMATEX {
wFormatTag: 1, nChannels: entry.channels,
nSamplesPerSec: entry.sample_rate,
nAvgBytesPerSec: entry.sample_rate * entry.channels as u32 * 2,
nBlockAlign: entry.channels * 2,
wBitsPerSample: 16,
cbSize: 0,
};
let samples = Arc::clone(&entry.samples);
let voice = unsafe {
let mut v: Option<IXAudio2SourceVoice> = None;
if self
.engine
.CreateSourceVoice(
&mut v,
&wfx,
0,
XAUDIO2_DEFAULT_FREQ_RATIO,
None::<&IXAudio2VoiceCallback>, None, None, )
.is_err()
{
return;
}
match v { Some(v) => v, None => return }
};
let buf = XAUDIO2_BUFFER {
Flags: XAUDIO2_END_OF_STREAM,
AudioBytes: (samples.len() * 2) as u32,
pAudioData: samples.as_ptr() as *const u8,
PlayBegin: 0,
PlayLength: 0,
LoopBegin: 0,
LoopLength: 0,
LoopCount: if looping { XAUDIO2_LOOP_INFINITE } else { 0 },
pContext: std::ptr::null_mut(),
};
unsafe {
if voice.SubmitSourceBuffer(&buf, None).is_err() { return; }
if voice.Start(0, XAUDIO2_COMMIT_NOW).is_err() { return; }
}
self.voices.insert(handle, ActiveVoice { voice, _data: samples });
}
pub fn stop(&mut self, handle: u32) {
self.voices.remove(&handle);
}
pub fn set_volume(&mut self, handle: u32, volume: f32) {
if let Some(av) = self.voices.get(&handle) {
unsafe { let _ = av.voice.SetVolume(volume, XAUDIO2_COMMIT_NOW); }
}
}
}
fn decode_wav(bytes: &[u8]) -> Option<(Vec<i16>, u16, u32)> {
if bytes.len() < 12 { return None; }
if &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" { return None; }
let mut pos = 12usize;
let mut channels: Option<u16> = None;
let mut sample_rate: Option<u32> = None;
let mut bits_per_sample: Option<u16> = None;
let mut audio_format: Option<u16> = None;
let mut samples: Option<Vec<i16>> = None;
while pos + 8 <= bytes.len() {
let tag = &bytes[pos..pos + 4];
let size = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().ok()?) as usize;
pos += 8;
if pos + size > bytes.len() { break; }
match tag {
b"fmt " if size >= 16 => {
audio_format = Some(u16::from_le_bytes(bytes[pos..pos + 2].try_into().ok()?));
channels = Some(u16::from_le_bytes(bytes[pos + 2..pos + 4].try_into().ok()?));
sample_rate = Some(u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().ok()?));
bits_per_sample = Some(u16::from_le_bytes(bytes[pos + 14..pos + 16].try_into().ok()?));
}
b"data" => {
let raw = &bytes[pos..pos + size];
samples = match (audio_format, bits_per_sample) {
(Some(1), Some(16)) => Some(
raw.chunks_exact(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect(),
),
(Some(1), Some(8)) => Some(
raw.iter().map(|&b| ((b as i16) - 128) << 8).collect(),
),
_ => return None,
};
}
_ => {}
}
pos += size + (size & 1); }
Some((samples?, channels?, sample_rate?))
}
fn decode_ogg(bytes: &[u8]) -> Option<(Vec<i16>, u16, u32)> {
use lewton::inside_ogg::OggStreamReader;
let cursor = std::io::Cursor::new(bytes);
let mut reader = OggStreamReader::new(cursor).ok()?;
let channels = reader.ident_hdr.audio_channels as u16;
let sample_rate = reader.ident_hdr.audio_sample_rate;
let mut samples = Vec::<i16>::new();
loop {
match reader.read_dec_packet_itl() {
Ok(Some(pck)) => samples.extend_from_slice(&pck),
Ok(None) => break,
Err(_) => return None,
}
}
Some((samples, channels, sample_rate))
}
}
#[cfg(not(target_os = "windows"))]
mod backend {
pub struct AudioState;
impl AudioState {
pub fn new() -> Option<Self> { None }
pub fn load(&mut self, _: &str) -> u32 { 0 }
pub fn play(&mut self, _: u32, _: bool) {}
pub fn stop(&mut self, _: u32) {}
pub fn set_volume(&mut self, _: u32, _: f32) {}
}
}
thread_local! {
static AUDIO: std::cell::RefCell<Option<backend::AudioState>> =
std::cell::RefCell::new(backend::AudioState::new());
}
pub fn load_sound(path: &str) -> u32 {
AUDIO.with(|a| {
let mut a = a.borrow_mut();
let Some(state) = a.as_mut() else { return 0 };
state.load(path)
})
}
pub fn play_sound(handle: u32, looping: bool) {
AUDIO.with(|a| {
if let Some(state) = a.borrow_mut().as_mut() {
state.play(handle, looping);
}
});
}
pub fn stop_sound(handle: u32) {
AUDIO.with(|a| {
if let Some(state) = a.borrow_mut().as_mut() {
state.stop(handle);
}
});
}
pub fn set_volume(handle: u32, volume: f32) {
AUDIO.with(|a| {
if let Some(state) = a.borrow_mut().as_mut() {
state.set_volume(handle, volume);
}
});
}
pub fn free_all_sounds() {
AUDIO.with(|a| { *a.borrow_mut() = None; });
}