1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
use std::{path::Path, sync::Arc, thread::JoinHandle, time::Duration};
use cpal::{
Stream, StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use crossbeam::channel;
use crate::{
BUFFER_MS, CHANNEL_COUNT, SAMPLE_TAP_CAPACITY, SEEK_FADE_MS,
engine::command::SeekPosition,
error::{Result, VoxError},
};
mod command;
mod decoder;
mod resampler;
mod state;
mod tap;
pub(crate) use command::VoxCommand;
pub(crate) use decoder::VoxDecoder;
pub(crate) use resampler::VoxResampler;
pub(crate) use state::SharedState;
pub(crate) use tap::TapReader;
/// An audio playback engine capable of handling various formats, providing gapless playback, and a
/// tap for visualization purposes.
pub struct Vox {
state: Arc<SharedState>,
commands: channel::Sender<VoxCommand>,
sps: f64,
output_rate: u32,
output_channels: usize,
tap: TapReader,
_stream: Stream,
_decoder_thread: JoinHandle<()>,
}
impl Vox {
/// Create a new Vox object with device defaults
pub fn new() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| VoxError::Output("No output device recognized!".into()))?;
let config = device
.default_output_config()
.map_err(|e| VoxError::Output(e.to_string()))?;
let output_rate = config.sample_rate() as f32;
let output_channels = config.channels() as usize;
let stream_config: StreamConfig = config.into();
let buffer_size = (output_rate as usize * output_channels * BUFFER_MS) / 1000;
let (producer, mut consumer) = rtrb::RingBuffer::new(buffer_size);
let (mut tap_writer, tap_reader) = tap::new_tap(SAMPLE_TAP_CAPACITY);
let state = Arc::new(SharedState::default());
let state_clone = Arc::clone(&state);
let mut was_seeking = false;
let fade_total_samples = (output_rate as usize * output_channels * SEEK_FADE_MS) / 1000;
let mut fade_samples_remaining: usize = 0;
// Callback function
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _| {
let is_seeking = state_clone.is_seeking();
let is_inactive =
state_clone.is_paused() || !state_clone.is_active() || is_seeking;
// Output silence if paused, not active, or seeking
if is_inactive {
// Drain ring buffer in bulk
let available = consumer.slots();
if available > 0 {
if let Ok(chunk) = consumer.read_chunk(available) {
chunk.commit_all();
}
}
data.fill(0.0);
was_seeking = is_seeking;
return;
}
// Start fade-in if we just finished seeking
if was_seeking && !is_seeking {
fade_samples_remaining = fade_total_samples;
}
was_seeking = is_seeking;
// Batch read from ring buffer
let available = consumer.slots();
let to_read = available.min(data.len());
if to_read > 0 {
if let Ok(chunk) = consumer.read_chunk(to_read) {
let (first, second) = chunk.as_slices();
let mut i = 0;
for &s in first.iter().chain(second.iter()) {
let mut sample = s;
if fade_samples_remaining > 0 {
let progress = 1.0
- (fade_samples_remaining as f32
/ fade_total_samples as f32);
sample *= progress;
fade_samples_remaining -= 1;
}
data[i] = sample;
i += 1;
}
chunk.commit_all();
}
}
// Fill remainder with silence (underrun)
data[to_read..].fill(0.0);
if to_read > 0 {
state_clone.add_samples(to_read as u64);
}
tap_writer.push(data);
},
|_e| {},
None,
)
.expect("Failed to create stream");
let (tx, rx) = channel::bounded(CHANNEL_COUNT);
let decoder_thread = command::spawn(
rx,
producer,
Arc::clone(&state),
output_rate as u32,
output_channels,
);
stream.play().map_err(|e| VoxError::Output(e.to_string()))?;
Ok(Self {
state: state,
commands: tx,
sps: output_rate as f64 * output_channels as f64,
output_rate: output_rate as u32,
output_channels,
_stream: stream,
_decoder_thread: decoder_thread,
tap: tap_reader,
})
}
// ===========================
// PUBLIC FACING API
// =========================
/// Play an audio file, provided a filepath
pub fn play<P: AsRef<Path>>(&mut self, p: P) -> Result<()> {
let path = p.as_ref();
if !path.exists() {
let s = path.to_string_lossy().to_string();
return Err(VoxError::FileOpen(s));
}
self.state.start_seek();
self.state.reset_samples();
self.state.set_active(true);
self.commands
.send(VoxCommand::Play(path.to_string_lossy().to_string()))
.map_err(|_| VoxError::Output("Channel closed".into()))
}
/// Set the next track to be played for gapless playback, provided a filepath.
///
/// This is **NOT** a queueing function. Calling this function will overwrite the last
/// value that was passed to it. Once a transition takes place, the *next* track will
/// be set to None, and it will be safe to call this method without overwriting the
/// previously passed value.
///
/// This method uses non-blocking send - if the command channel is full, the command
/// is dropped. This is safe because QueueNext commands are coalesced by the worker,
/// so only the most recent one matters.
pub fn set_next<P: AsRef<Path>>(&mut self, p: P) -> Result<()> {
let path = p.as_ref();
if !path.exists() {
return Err(VoxError::FileOpen(path.to_string_lossy().to_string()));
}
// Use try_send to avoid blocking if channel is full.
// Dropped commands are OK - we coalesce QueueNext and only the last one matters.
let _ = self
.commands
.try_send(VoxCommand::QueueNext(path.to_string_lossy().to_string()));
Ok(())
}
pub fn seek_to(&mut self, pos: f64) -> Result<()> {
if !self.state.is_active() {
return Ok(());
}
self.state.start_seek();
self.commands
.send(VoxCommand::Seek(SeekPosition::Absolute(pos)))
.map_err(|e| VoxError::Seek(e.to_string()))?;
Ok(())
}
pub fn seek_relative(&mut self, increment: f64) -> Result<()> {
if !self.state.is_active() {
return Ok(());
}
self.state.start_seek();
self.commands
.send(VoxCommand::Seek(SeekPosition::Relative(increment)))
.map_err(|e| VoxError::Seek(e.to_string()))?;
Ok(())
}
/// *Toggle playback status*
///
/// If playing, pause the playback.
/// If paused, resume the playback.
pub fn toggle_playback(&self) {
self.state.toggle_playback();
}
pub fn is_paused(&self) -> bool {
self.state.is_paused()
}
pub fn is_active(&self) -> bool {
self.state.is_active()
}
/// Pause playback
pub fn pause(&self) {
self.state.set_paused(true)
}
/// Resume playback if paused
pub fn resume(&self) {
self.state.set_paused(false);
}
/// Stop all playback, current and upcoming
pub fn stop(&self) -> Result<()> {
self.commands
.send(VoxCommand::Stop)
.map_err(|_| VoxError::Output("Channel closed".into()))
}
/// Retrieve position of playback
pub fn position(&self) -> Duration {
Duration::from_secs_f64(self.state.get_samples() as f64 / self.sps)
}
/// Retrieve the playable duration of the current track.
/// This excludes encoder delay and padding samples for accurate progress tracking.
pub fn duration(&self) -> Duration {
Duration::from_secs_f64(self.state.get_duration_secs())
}
/// Retrieves the latest *amount* of requested samples
/// Returns Vec<f32>
pub fn get_latest_samples(&mut self, amount: usize) -> Vec<f32> {
self.tap.get_latest(amount)
}
/// Returns the output sample rate of the audio device in Hz
pub fn sample_rate(&self) -> u32 {
self.output_rate
}
/// Returns the number of interleaved channels in the audio output
pub fn channels(&self) -> usize {
self.output_channels
}
pub fn track_ended(&self) -> bool {
self.state.take_track_ended()
}
}
impl Drop for Vox {
fn drop(&mut self) {
let _ = self.commands.send(VoxCommand::Shutdown);
}
}