opencode_voice/audio/
wav.rs1use anyhow::{Context, Result};
4use hound::{WavSpec, WavWriter};
5use std::path::{Path, PathBuf};
6use uuid::Uuid;
7
8use crate::audio::AudioConfig;
9
10pub fn write_wav(samples: &[i16], config: &AudioConfig, path: &Path) -> Result<()> {
14 let spec = WavSpec {
15 channels: config.channels,
16 sample_rate: config.sample_rate,
17 bits_per_sample: config.bit_depth,
18 sample_format: hound::SampleFormat::Int,
19 };
20
21 let mut writer = WavWriter::create(path, spec)
22 .with_context(|| format!("Failed to create WAV file at {}", path.display()))?;
23
24 for &sample in samples {
25 writer
26 .write_sample(sample)
27 .context("Failed to write audio sample")?;
28 }
29
30 writer.finalize().context("Failed to finalize WAV file")?;
31
32 Ok(())
33}
34
35pub fn create_temp_wav_path() -> PathBuf {
39 let filename = format!("opencode-voice-{}.wav", Uuid::new_v4());
40 std::env::temp_dir().join(filename)
41}
42
43pub struct TempWav {
49 path: PathBuf,
50}
51
52impl TempWav {
53 pub fn new() -> Self {
55 TempWav {
56 path: create_temp_wav_path(),
57 }
58 }
59
60 pub fn write(&self, samples: &[i16], config: &AudioConfig) -> Result<()> {
62 write_wav(samples, config, &self.path)
63 }
64
65 pub fn into_path(self) -> PathBuf {
69 let path = self.path.clone();
70 std::mem::forget(self); path
72 }
73}
74
75impl Drop for TempWav {
76 fn drop(&mut self) {
77 let _ = std::fs::remove_file(&self.path);
79 }
80}
81
82impl Default for TempWav {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::audio::default_audio_config;
92
93 #[test]
94 fn test_create_temp_wav_path_has_uuid() {
95 let path = create_temp_wav_path();
96 let filename = path.file_name().unwrap().to_str().unwrap();
97 assert!(filename.starts_with("opencode-voice-"));
98 assert!(filename.ends_with(".wav"));
99 }
100
101 #[test]
102 fn test_create_temp_wav_path_in_temp_dir() {
103 let path = create_temp_wav_path();
104 assert!(path.starts_with(std::env::temp_dir()));
105 }
106
107 #[test]
108 fn test_write_and_delete() {
109 let config = default_audio_config();
110 let samples: Vec<i16> = vec![0i16; 1000]; let wav = TempWav::new();
112 let path = wav.path.to_path_buf();
113
114 assert!(!path.exists(), "File should not exist before write");
116
117 wav.write(&samples, &config).expect("write should succeed");
119 assert!(path.exists(), "File should exist after write");
120
121 drop(wav);
123 assert!(!path.exists(), "File should be deleted after drop");
124 }
125
126 #[test]
127 fn test_drop_no_panic_when_file_missing() {
128 let wav = TempWav::new();
130 drop(wav); }
133
134 #[test]
135 fn test_write_wav_creates_valid_file() {
136 let config = default_audio_config();
137 let samples: Vec<i16> = (0..160).map(|i| i as i16 * 100).collect(); let path = create_temp_wav_path();
139
140 write_wav(&samples, &config, &path).expect("write_wav should succeed");
141 assert!(path.exists());
142
143 let reader = hound::WavReader::open(&path).expect("should be readable");
145 let spec = reader.spec();
146 assert_eq!(spec.channels, 1);
147 assert_eq!(spec.sample_rate, 16000);
148 assert_eq!(spec.bits_per_sample, 16);
149
150 let _ = std::fs::remove_file(&path);
152 }
153}