use anyhow::{Context, Result};
use hound::{WavSpec, WavWriter};
use std::path::{Path, PathBuf};
use uuid::Uuid;
use crate::audio::AudioConfig;
pub fn write_wav(samples: &[i16], config: &AudioConfig, path: &Path) -> Result<()> {
let spec = WavSpec {
channels: config.channels,
sample_rate: config.sample_rate,
bits_per_sample: config.bit_depth,
sample_format: hound::SampleFormat::Int,
};
let mut writer = WavWriter::create(path, spec)
.with_context(|| format!("Failed to create WAV file at {}", path.display()))?;
for &sample in samples {
writer
.write_sample(sample)
.context("Failed to write audio sample")?;
}
writer.finalize().context("Failed to finalize WAV file")?;
Ok(())
}
pub fn create_temp_wav_path() -> PathBuf {
let filename = format!("opencode-voice-{}.wav", Uuid::new_v4());
std::env::temp_dir().join(filename)
}
pub struct TempWav {
path: PathBuf,
}
impl TempWav {
pub fn new() -> Self {
TempWav {
path: create_temp_wav_path(),
}
}
pub fn write(&self, samples: &[i16], config: &AudioConfig) -> Result<()> {
write_wav(samples, config, &self.path)
}
pub fn into_path(self) -> PathBuf {
let path = self.path.clone();
std::mem::forget(self); path
}
}
impl Drop for TempWav {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
impl Default for TempWav {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audio::default_audio_config;
#[test]
fn test_create_temp_wav_path_has_uuid() {
let path = create_temp_wav_path();
let filename = path.file_name().unwrap().to_str().unwrap();
assert!(filename.starts_with("opencode-voice-"));
assert!(filename.ends_with(".wav"));
}
#[test]
fn test_create_temp_wav_path_in_temp_dir() {
let path = create_temp_wav_path();
assert!(path.starts_with(std::env::temp_dir()));
}
#[test]
fn test_write_and_delete() {
let config = default_audio_config();
let samples: Vec<i16> = vec![0i16; 1000]; let wav = TempWav::new();
let path = wav.path.to_path_buf();
assert!(!path.exists(), "File should not exist before write");
wav.write(&samples, &config).expect("write should succeed");
assert!(path.exists(), "File should exist after write");
drop(wav);
assert!(!path.exists(), "File should be deleted after drop");
}
#[test]
fn test_drop_no_panic_when_file_missing() {
let wav = TempWav::new();
drop(wav); }
#[test]
fn test_write_wav_creates_valid_file() {
let config = default_audio_config();
let samples: Vec<i16> = (0..160).map(|i| i as i16 * 100).collect(); let path = create_temp_wav_path();
write_wav(&samples, &config, &path).expect("write_wav should succeed");
assert!(path.exists());
let reader = hound::WavReader::open(&path).expect("should be readable");
let spec = reader.spec();
assert_eq!(spec.channels, 1);
assert_eq!(spec.sample_rate, 16000);
assert_eq!(spec.bits_per_sample, 16);
let _ = std::fs::remove_file(&path);
}
}