ranim/cmd/preview/
audio.rs1use std::{sync::Arc, time::Duration, time::Instant};
10
11use rodio::{
12 Sink,
13 source::{SeekError, Source},
14};
15
16use ranim_core::{
17 SceneEvaluator,
18 audio::{MASTER_CHANNELS, MASTER_SAMPLE_RATE},
19};
20
21#[derive(Clone)]
24pub(crate) struct MixedAudio {
25 pcm: Arc<[f32]>,
26 total_secs: f64,
27}
28
29impl MixedAudio {
30 pub fn new(evaluator: &SceneEvaluator, total_secs: f64) -> Self {
32 Self {
33 pcm: evaluator.audio().clone(),
34 total_secs,
35 }
36 }
37
38 fn is_empty(&self) -> bool {
39 self.pcm.is_empty()
40 }
41
42 fn total_secs(&self) -> f64 {
43 self.total_secs
44 }
45}
46
47struct PcmSource {
49 pcm: Arc<[f32]>,
50 next: usize,
51}
52
53impl Iterator for PcmSource {
54 type Item = f32;
55
56 fn next(&mut self) -> Option<f32> {
57 let sample = self.pcm.get(self.next).copied();
58 if sample.is_some() {
59 self.next += 1;
60 }
61 sample
62 }
63}
64
65impl Source for PcmSource {
66 fn current_span_len(&self) -> Option<usize> {
67 Some(self.pcm.len() - self.next)
68 }
69
70 fn channels(&self) -> u16 {
71 MASTER_CHANNELS
72 }
73
74 fn sample_rate(&self) -> u32 {
75 MASTER_SAMPLE_RATE
76 }
77
78 fn total_duration(&self) -> Option<Duration> {
79 Some(Duration::from_secs_f64(
80 self.pcm.len() as f64 / (MASTER_SAMPLE_RATE as f64 * MASTER_CHANNELS as f64),
81 ))
82 }
83
84 fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
85 let frame = (pos.as_secs_f64() * MASTER_SAMPLE_RATE as f64) as usize;
86 self.next = (frame * MASTER_CHANNELS as usize).min(self.pcm.len());
87 Ok(())
88 }
89}
90
91enum PlayerState {
92 Playing { started_at: Instant, base_sec: f64 },
93 Paused { at_sec: f64 },
94}
95
96const SCRUB_GAIN: f32 = 0.6;
99const SCRUB_IDLE: Duration = Duration::from_millis(120);
100
101pub(crate) struct AudioPlayer {
103 mixed: MixedAudio,
104 sink: Sink,
105 _stream: rodio::OutputStream,
107 state: PlayerState,
108 speed: f64,
109 scrub_at: Option<Instant>,
111}
112
113impl AudioPlayer {
114 pub fn try_new(mixed: MixedAudio) -> Option<Self> {
118 if mixed.is_empty() {
119 return None;
120 }
121 let stream = rodio::OutputStreamBuilder::open_default_stream().ok()?;
122 let sink = Sink::connect_new(stream.mixer());
123 sink.pause();
124 Some(Self {
125 mixed,
126 sink,
127 _stream: stream,
128 state: PlayerState::Paused { at_sec: 0.0 },
129 speed: 1.0,
130 scrub_at: None,
131 })
132 }
133
134 pub fn pos_secs(&self) -> f64 {
136 match self.state {
137 PlayerState::Playing {
138 started_at,
139 base_sec,
140 } => (base_sec + started_at.elapsed().as_secs_f64() * self.speed)
141 .clamp(0.0, self.mixed.total_secs()),
142 PlayerState::Paused { at_sec } => at_sec,
143 }
144 }
145
146 pub fn play_from(&mut self, sec: f64, speed: f64) {
148 let sec = sec.clamp(0.0, self.mixed.total_secs());
149 self.speed = speed;
150 self.sink.set_volume(1.0);
151 self.sink.set_speed(speed as f32);
152 self.sink.clear();
153 self.sink.append(PcmSource {
154 pcm: self.mixed.pcm.clone(),
155 next: (sec * MASTER_SAMPLE_RATE as f64) as usize * MASTER_CHANNELS as usize,
156 });
157 self.sink.play();
158 self.scrub_at = None;
159 self.state = PlayerState::Playing {
160 started_at: Instant::now(),
161 base_sec: sec,
162 };
163 }
164
165 pub fn pause(&mut self) {
167 let pos = self.pos_secs();
168 self.scrub_at = None;
169 self.sink.set_volume(1.0);
170 self.sink.pause();
171 self.state = PlayerState::Paused { at_sec: pos };
172 }
173
174 pub fn seek_to(&mut self, sec: f64) {
176 let sec = sec.clamp(0.0, self.mixed.total_secs());
177 match self.state {
178 PlayerState::Playing { .. } => {
179 self.ensure_source_at(sec);
180 self.state = PlayerState::Playing {
181 started_at: Instant::now(),
182 base_sec: sec,
183 };
184 }
185 PlayerState::Paused { .. } => {
186 self.state = PlayerState::Paused { at_sec: sec };
187 }
188 }
189 }
190
191 fn ensure_source_at(&mut self, sec: f64) {
197 if self.sink.empty() {
198 self.sink.clear();
199 self.sink.append(PcmSource {
200 pcm: self.mixed.pcm.clone(),
201 next: (sec * MASTER_SAMPLE_RATE as f64) as usize * MASTER_CHANNELS as usize,
202 });
203 } else {
204 let _ = self.sink.try_seek(Duration::from_secs_f64(sec));
205 }
206 }
207
208 pub fn scrub(&mut self, sec: f64) {
213 let sec = sec.clamp(0.0, self.mixed.total_secs());
214 self.sink.set_volume(SCRUB_GAIN);
215 self.ensure_source_at(sec);
216 self.sink.play();
217 self.scrub_at = Some(Instant::now());
218 self.state = PlayerState::Playing {
219 started_at: Instant::now(),
220 base_sec: sec,
221 };
222 }
223
224 pub fn end_stale_scrub(&mut self) {
226 let Some(last_move) = self.scrub_at else {
227 return;
228 };
229 if last_move.elapsed() < SCRUB_IDLE {
230 return;
231 }
232 self.pause();
233 }
234
235 pub fn scrub_voice_active(&self) -> bool {
237 self.scrub_at.is_some()
238 }
239
240 pub fn set_speed(&mut self, speed: f64) {
242 let pos = self.pos_secs();
243 self.speed = speed;
244 self.sink.set_speed(speed as f32);
245 if matches!(self.state, PlayerState::Playing { .. }) {
246 self.state = PlayerState::Playing {
247 started_at: Instant::now(),
248 base_sec: pos,
249 };
250 }
251 }
252}