| 1 |
|
- |
//! Streaming WAV export: decode → channel-convert → resample → encode in a
|
| 2 |
|
- |
//! bounded window, so a multi-GB source never materializes as a 2-4x f32 buffer.
|
|
1 |
+ |
//! Streaming audio pipeline: decode → [per-frame edit stages] → channel-convert
|
|
2 |
+ |
//! → resample → encode, in a bounded window, so a multi-GB source never
|
|
3 |
+ |
//! materialises as a 2-4x f32 buffer.
|
| 3 |
4 |
|
//!
|
| 4 |
|
- |
//! Used only when the container reports an exact frame count (PCM WAV does),
|
| 5 |
|
- |
//! which lets the resampler output be sized — and clamped — exactly as the
|
| 6 |
|
- |
//! buffered path does, so the streamed result is bit-identical. Sources without
|
| 7 |
|
- |
//! a known frame count fall back to the buffered pipeline.
|
|
5 |
+ |
//! One driver ([`process_streaming`]) serves three callers — WAV export, AIFF
|
|
6 |
+ |
//! export, and the streamable edit ops (gain/fade) — by varying two things: the
|
|
7 |
+ |
//! [`StreamStage`]s applied per frame (none for export) and the [`StreamSink`]
|
|
8 |
+ |
//! that quantises/encodes the output. Sinks reuse the exact buffered quantisation
|
|
9 |
+ |
//! (`encode::write_sample`, `encode_aiff::write_aiff_sample`) and stages reuse the
|
|
10 |
+ |
//! exact buffered DSP (`edit::gain::apply_gain`, `FadeCurve::gain`), so every
|
|
11 |
+ |
//! streamed result is byte-identical to its buffered equivalent.
|
|
12 |
+ |
//!
|
|
13 |
+ |
//! Streaming needs the source's exact frame count (PCM WAV reports it; AIFF needs
|
|
14 |
+ |
//! it for the header up front, the resampler to size its output). Sources without
|
|
15 |
+ |
//! one (e.g. some compressed formats) return `Ok(false)` so the caller falls back
|
|
16 |
+ |
//! to the buffered pipeline.
|
| 8 |
17 |
|
|
| 9 |
|
- |
use std::io::{Seek, Write};
|
|
18 |
+ |
use std::fs::File;
|
|
19 |
+ |
use std::io::{BufWriter, Write};
|
| 10 |
20 |
|
use std::path::Path;
|
| 11 |
21 |
|
use std::sync::atomic::{AtomicBool, Ordering};
|
| 12 |
22 |
|
|
| 14 |
24 |
|
use rubato::Resampler;
|
| 15 |
25 |
|
|
| 16 |
26 |
|
use super::convert::{convert_channels, make_resampler, RESAMPLE_CHUNK};
|
| 17 |
|
- |
use super::decode::{decode_next_into, open_decoder};
|
|
27 |
+ |
use super::decode::{decode_next_into, open_decoder, OpenedDecoder};
|
| 18 |
28 |
|
use super::dither::SimpleRng;
|
| 19 |
|
- |
use super::encode::{sample_format_for, write_sample, DITHER_SEED};
|
|
29 |
+ |
use super::encode::{sample_format_for, wav_size_check, write_sample, DITHER_SEED};
|
|
30 |
+ |
use super::encode_aiff::{aiff_header_bytes, write_aiff_sample};
|
| 20 |
31 |
|
use super::ExportChannels;
|
|
32 |
+ |
use crate::edit::fade::FadeCurve;
|
|
33 |
+ |
use crate::edit::EditOperation;
|
| 21 |
34 |
|
use crate::error::{io_err, CoreError};
|
| 22 |
35 |
|
|
| 23 |
36 |
|
/// Resolve a concrete output channel count from the target spec + source.
|
| 29 |
42 |
|
}
|
| 30 |
43 |
|
}
|
| 31 |
44 |
|
|
| 32 |
|
- |
/// Interleave and write one per-channel resampler output block, clamping the
|
| 33 |
|
- |
/// total to `target_total` frames (matching the buffered path, which drops the
|
| 34 |
|
- |
/// extra frames the resampler emits while flushing its latency tail).
|
| 35 |
|
- |
fn write_block<W: Write + Seek>(
|
| 36 |
|
- |
writer: &mut WavWriter<W>,
|
| 37 |
|
- |
per_channel: &[Vec<f32>],
|
|
45 |
+ |
// ── FrameStream: chunked decoder ───────────────────────────────────────────
|
|
46 |
+ |
|
|
47 |
+ |
/// A pull-based reader over a decoded source: format parameters plus a
|
|
48 |
+ |
/// `next_block` that decodes one packet's worth of interleaved frames at a time.
|
|
49 |
+ |
pub(crate) struct FrameStream {
|
|
50 |
+ |
dec: OpenedDecoder,
|
|
51 |
+ |
}
|
|
52 |
+ |
|
|
53 |
+ |
impl FrameStream {
|
|
54 |
+ |
pub(crate) fn open(path: &Path) -> Result<Self, CoreError> {
|
|
55 |
+ |
Ok(Self {
|
|
56 |
+ |
dec: open_decoder(path)?,
|
|
57 |
+ |
})
|
|
58 |
+ |
}
|
|
59 |
+ |
|
|
60 |
+ |
pub(crate) fn sample_rate(&self) -> u32 {
|
|
61 |
+ |
self.dec.sample_rate
|
|
62 |
+ |
}
|
|
63 |
+ |
pub(crate) fn channels(&self) -> u16 {
|
|
64 |
+ |
self.dec.channels
|
|
65 |
+ |
}
|
|
66 |
+ |
pub(crate) fn n_frames(&self) -> Option<u64> {
|
|
67 |
+ |
self.dec.n_frames
|
|
68 |
+ |
}
|
|
69 |
+ |
|
|
70 |
+ |
/// Decode the next packet's frames into `out` (interleaved, appended).
|
|
71 |
+ |
/// Returns `Ok(false)` at end of stream.
|
|
72 |
+ |
pub(crate) fn next_block(&mut self, out: &mut Vec<f32>) -> Result<bool, CoreError> {
|
|
73 |
+ |
decode_next_into(
|
|
74 |
+ |
self.dec.format.as_mut(),
|
|
75 |
+ |
self.dec.decoder.as_mut(),
|
|
76 |
+ |
self.dec.track_id,
|
|
77 |
+ |
out,
|
|
78 |
+ |
)
|
|
79 |
+ |
}
|
|
80 |
+ |
}
|
|
81 |
+ |
|
|
82 |
+ |
// ── StreamStage: per-frame edit transforms (source layout) ─────────────────
|
|
83 |
+ |
|
|
84 |
+ |
/// A per-frame transform applied to interleaved source-layout audio before
|
|
85 |
+ |
/// channel-convert/resample. `frame_offset` is the absolute frame index of the
|
|
86 |
+ |
/// block's first frame, so position-dependent ops (fades) work across blocks.
|
|
87 |
+ |
pub(crate) trait StreamStage {
|
|
88 |
+ |
fn process(&mut self, block: &mut [f32], channels: u16, frame_offset: u64);
|
|
89 |
+ |
}
|
|
90 |
+ |
|
|
91 |
+ |
/// Gain in dB. Reuses `edit::gain::apply_gain` per block so streamed gain is
|
|
92 |
+ |
/// bit-identical to the buffered edit (same dB→linear scale, same clamp).
|
|
93 |
+ |
struct GainStage {
|
|
94 |
+ |
db: f64,
|
|
95 |
+ |
}
|
|
96 |
+ |
impl StreamStage for GainStage {
|
|
97 |
+ |
fn process(&mut self, block: &mut [f32], _channels: u16, _frame_offset: u64) {
|
|
98 |
+ |
crate::edit::gain::apply_gain(block, self.db);
|
|
99 |
+ |
}
|
|
100 |
+ |
}
|
|
101 |
+ |
|
|
102 |
+ |
/// Fade-in over the first `fade_frames` frames; identity afterwards. Matches
|
|
103 |
+ |
/// `edit::fade::apply_fade_in`'s `t = i / (fade_frames-1).max(1)`.
|
|
104 |
+ |
struct FadeInStage {
|
|
105 |
+ |
fade_frames: u64,
|
|
106 |
+ |
curve: FadeCurve,
|
|
107 |
+ |
}
|
|
108 |
+ |
impl StreamStage for FadeInStage {
|
|
109 |
+ |
fn process(&mut self, block: &mut [f32], channels: u16, frame_offset: u64) {
|
|
110 |
+ |
if self.fade_frames == 0 {
|
|
111 |
+ |
return;
|
|
112 |
+ |
}
|
|
113 |
+ |
let ch = channels as usize;
|
|
114 |
+ |
if ch == 0 {
|
|
115 |
+ |
return;
|
|
116 |
+ |
}
|
|
117 |
+ |
let denom = (self.fade_frames - 1).max(1) as f32;
|
|
118 |
+ |
let frames = block.len() / ch;
|
|
119 |
+ |
for fr in 0..frames {
|
|
120 |
+ |
let abs = frame_offset + fr as u64;
|
|
121 |
+ |
if abs >= self.fade_frames {
|
|
122 |
+ |
break; // rest of the file is identity
|
|
123 |
+ |
}
|
|
124 |
+ |
let g = self.curve.gain(abs as f32 / denom);
|
|
125 |
+ |
for c in 0..ch {
|
|
126 |
+ |
block[fr * ch + c] *= g;
|
|
127 |
+ |
}
|
|
128 |
+ |
}
|
|
129 |
+ |
}
|
|
130 |
+ |
}
|
|
131 |
+ |
|
|
132 |
+ |
/// Fade-out over the last `fade_frames` frames. Matches
|
|
133 |
+ |
/// `edit::fade::apply_fade_out`'s `t = 1 - i / (fade_frames-1).max(1)`.
|
|
134 |
+ |
struct FadeOutStage {
|
|
135 |
+ |
total_frames: u64,
|
|
136 |
+ |
fade_frames: u64,
|
|
137 |
+ |
curve: FadeCurve,
|
|
138 |
+ |
}
|
|
139 |
+ |
impl StreamStage for FadeOutStage {
|
|
140 |
+ |
fn process(&mut self, block: &mut [f32], channels: u16, frame_offset: u64) {
|
|
141 |
+ |
if self.fade_frames == 0 {
|
|
142 |
+ |
return;
|
|
143 |
+ |
}
|
|
144 |
+ |
let ch = channels as usize;
|
|
145 |
+ |
if ch == 0 {
|
|
146 |
+ |
return;
|
|
147 |
+ |
}
|
|
148 |
+ |
let start = self.total_frames - self.fade_frames;
|
|
149 |
+ |
let denom = (self.fade_frames - 1).max(1) as f32;
|
|
150 |
+ |
let frames = block.len() / ch;
|
|
151 |
+ |
for fr in 0..frames {
|
|
152 |
+ |
let abs = frame_offset + fr as u64;
|
|
153 |
+ |
if abs < start {
|
|
154 |
+ |
continue;
|
|
155 |
+ |
}
|
|
156 |
+ |
let i = abs - start;
|
|
157 |
+ |
let g = self.curve.gain(1.0 - (i as f32 / denom));
|
|
158 |
+ |
for c in 0..ch {
|
|
159 |
+ |
block[fr * ch + c] *= g;
|
|
160 |
+ |
}
|
|
161 |
+ |
}
|
|
162 |
+ |
}
|
|
163 |
+ |
}
|
|
164 |
+ |
|
|
165 |
+ |
// ── StreamSink: incremental encoders ───────────────────────────────────────
|
|
166 |
+ |
|
|
167 |
+ |
/// An incremental encoder fed one f32 sample at a time (interleaved order).
|
|
168 |
+ |
pub(crate) trait StreamSink {
|
|
169 |
+ |
fn write_sample(&mut self, sample: f32) -> Result<(), CoreError>;
|
|
170 |
+ |
fn finalize(self) -> Result<(), CoreError>;
|
|
171 |
+ |
}
|
|
172 |
+ |
|
|
173 |
+ |
/// Streaming WAV sink. Reuses `encode::write_sample` so output matches the
|
|
174 |
+ |
/// buffered `encode_wav` quantisation (and dither) at every bit depth.
|
|
175 |
+ |
struct WavSink {
|
|
176 |
+ |
writer: WavWriter<BufWriter<File>>,
|
| 38 |
177 |
|
bit_depth: u16,
|
| 39 |
|
- |
rng: &mut SimpleRng,
|
| 40 |
|
- |
target_total: usize,
|
| 41 |
|
- |
written: &mut usize,
|
| 42 |
|
- |
) -> Result<(), CoreError> {
|
| 43 |
|
- |
let frames = per_channel.first().map_or(0, |c| c.len());
|
| 44 |
|
- |
for f in 0..frames {
|
| 45 |
|
- |
if *written >= target_total {
|
| 46 |
|
- |
return Ok(());
|
|
178 |
+ |
rng: SimpleRng,
|
|
179 |
+ |
}
|
|
180 |
+ |
impl WavSink {
|
|
181 |
+ |
fn create(
|
|
182 |
+ |
dest: &Path,
|
|
183 |
+ |
channels: u16,
|
|
184 |
+ |
sample_rate: u32,
|
|
185 |
+ |
bit_depth: u16,
|
|
186 |
+ |
out_frames: usize,
|
|
187 |
+ |
) -> Result<Self, CoreError> {
|
|
188 |
+ |
wav_size_check((out_frames as u64).saturating_mul(channels as u64), bit_depth)?;
|
|
189 |
+ |
let spec = WavSpec {
|
|
190 |
+ |
channels,
|
|
191 |
+ |
sample_rate,
|
|
192 |
+ |
bits_per_sample: bit_depth,
|
|
193 |
+ |
sample_format: sample_format_for(bit_depth),
|
|
194 |
+ |
};
|
|
195 |
+ |
let writer =
|
|
196 |
+ |
WavWriter::create(dest, spec).map_err(|e| io_err(dest, std::io::Error::other(e)))?;
|
|
197 |
+ |
Ok(Self {
|
|
198 |
+ |
writer,
|
|
199 |
+ |
bit_depth,
|
|
200 |
+ |
rng: SimpleRng::new(DITHER_SEED),
|
|
201 |
+ |
})
|
|
202 |
+ |
}
|
|
203 |
+ |
}
|
|
204 |
+ |
impl StreamSink for WavSink {
|
|
205 |
+ |
fn write_sample(&mut self, sample: f32) -> Result<(), CoreError> {
|
|
206 |
+ |
write_sample(&mut self.writer, sample, self.bit_depth, &mut self.rng)
|
|
207 |
+ |
}
|
|
208 |
+ |
fn finalize(self) -> Result<(), CoreError> {
|
|
209 |
+ |
self.writer
|
|
210 |
+ |
.finalize()
|
|
211 |
+ |
.map_err(|e| CoreError::Export(format!("WAV finalize: {e}")))
|
|
212 |
+ |
}
|
|
213 |
+ |
}
|
|
214 |
+ |
|
|
215 |
+ |
/// Streaming AIFF sink. Writes the FORM/COMM/SSND header up front (frame count
|
|
216 |
+ |
/// known) and reuses `encode_aiff::write_aiff_sample` so output matches the
|
|
217 |
+ |
/// buffered `encode_aiff` byte-for-byte.
|
|
218 |
+ |
struct AiffSink {
|
|
219 |
+ |
writer: BufWriter<File>,
|
|
220 |
+ |
bit_depth: u16,
|
|
221 |
+ |
rng: SimpleRng,
|
|
222 |
+ |
needs_pad: bool,
|
|
223 |
+ |
}
|
|
224 |
+ |
impl AiffSink {
|
|
225 |
+ |
fn create(
|
|
226 |
+ |
dest: &Path,
|
|
227 |
+ |
channels: u16,
|
|
228 |
+ |
sample_rate: u32,
|
|
229 |
+ |
bit_depth: u16,
|
|
230 |
+ |
out_frames: usize,
|
|
231 |
+ |
) -> Result<Self, CoreError> {
|
|
232 |
+ |
if channels == 0 {
|
|
233 |
+ |
return Err(CoreError::Export("AIFF: 0 channels".to_string()));
|
| 47 |
234 |
|
}
|
| 48 |
|
- |
for channel in per_channel {
|
| 49 |
|
- |
write_sample(writer, channel[f], bit_depth, rng)?;
|
|
235 |
+ |
let (header, needs_pad) = aiff_header_bytes(channels, sample_rate, bit_depth, out_frames)?;
|
|
236 |
+ |
let file = File::create(dest).map_err(|e| io_err(dest, e))?;
|
|
237 |
+ |
let mut writer = BufWriter::new(file);
|
|
238 |
+ |
writer.write_all(&header).map_err(|e| io_err(dest, e))?;
|
|
239 |
+ |
Ok(Self {
|
|
240 |
+ |
writer,
|
|
241 |
+ |
bit_depth,
|
|
242 |
+ |
rng: SimpleRng::new(DITHER_SEED),
|
|
243 |
+ |
needs_pad,
|
|
244 |
+ |
})
|
|
245 |
+ |
}
|
|
246 |
+ |
}
|
|
247 |
+ |
impl StreamSink for AiffSink {
|
|
248 |
+ |
fn write_sample(&mut self, sample: f32) -> Result<(), CoreError> {
|
|
249 |
+ |
write_aiff_sample(&mut self.writer, sample, self.bit_depth, &mut self.rng)
|
|
250 |
+ |
}
|
|
251 |
+ |
fn finalize(mut self) -> Result<(), CoreError> {
|
|
252 |
+ |
if self.needs_pad {
|
|
253 |
+ |
self.writer
|
|
254 |
+ |
.write_all(&[0u8])
|
|
255 |
+ |
.map_err(|e| CoreError::Export(format!("AIFF pad: {e}")))?;
|
| 50 |
256 |
|
}
|
| 51 |
|
- |
*written += 1;
|
|
257 |
+ |
self.writer
|
|
258 |
+ |
.flush()
|
|
259 |
+ |
.map_err(|e| CoreError::Export(format!("AIFF flush: {e}")))
|
| 52 |
260 |
|
}
|
| 53 |
|
- |
Ok(())
|
| 54 |
261 |
|
}
|
| 55 |
262 |
|
|
| 56 |
|
- |
/// Attempt a streaming WAV export. Returns `Ok(true)` if it streamed the file,
|
| 57 |
|
- |
/// `Ok(false)` if the source has no known frame count and the caller should fall
|
| 58 |
|
- |
/// back to the buffered pipeline.
|
| 59 |
|
- |
pub(crate) fn try_stream_wav_export(
|
| 60 |
|
- |
source: &Path,
|
| 61 |
|
- |
dest: &Path,
|
|
263 |
+ |
// ── Driver ─────────────────────────────────────────────────────────────────
|
|
264 |
+ |
|
|
265 |
+ |
/// Run the streaming pipeline: decode → stages (source layout) → channel-convert
|
|
266 |
+ |
/// → resample → sink. Returns `Ok(false)` if the source has no known frame count
|
|
267 |
+ |
/// (the caller should fall back to the buffered pipeline). `make_sink` is given
|
|
268 |
+ |
/// the resolved output channel count, rate, and exact frame count.
|
|
269 |
+ |
fn process_streaming<S: StreamSink>(
|
|
270 |
+ |
mut stream: FrameStream,
|
|
271 |
+ |
stages: &mut [&mut dyn StreamStage],
|
| 62 |
272 |
|
target_channels: &ExportChannels,
|
| 63 |
273 |
|
target_rate: Option<u32>,
|
| 64 |
|
- |
bit_depth: u16,
|
| 65 |
274 |
|
cancel: &AtomicBool,
|
|
275 |
+ |
make_sink: impl FnOnce(u16, u32, usize) -> Result<S, CoreError>,
|
| 66 |
276 |
|
) -> Result<bool, CoreError> {
|
| 67 |
|
- |
if !matches!(bit_depth, 8 | 16 | 24 | 32) {
|
| 68 |
|
- |
return Err(CoreError::Export(format!(
|
| 69 |
|
- |
"unsupported bit depth: {bit_depth} (expected 8, 16, 24, or 32)"
|
| 70 |
|
- |
)));
|
| 71 |
|
- |
}
|
| 72 |
|
- |
|
| 73 |
|
- |
let mut dec = open_decoder(source)?;
|
| 74 |
|
- |
// Exact streaming needs the total frame count to size the resampler output.
|
| 75 |
|
- |
let Some(n_frames) = dec.n_frames else {
|
|
277 |
+ |
let Some(n_frames) = stream.n_frames() else {
|
| 76 |
278 |
|
return Ok(false);
|
| 77 |
279 |
|
};
|
| 78 |
280 |
|
let n_frames = n_frames as usize;
|
| 79 |
281 |
|
|
| 80 |
|
- |
let src_rate = dec.sample_rate;
|
| 81 |
|
- |
let src_channels = dec.channels;
|
|
282 |
+ |
let src_rate = stream.sample_rate();
|
|
283 |
+ |
let src_channels = stream.channels();
|
|
284 |
+ |
let src_ch = src_channels as usize;
|
| 82 |
285 |
|
let out_ch = out_channel_count(target_channels, src_channels) as usize;
|
| 83 |
286 |
|
let dst_rate = target_rate.unwrap_or(src_rate);
|
| 84 |
287 |
|
|
| 85 |
|
- |
// Reject a >4 GB output before writing a single byte (the streaming path
|
| 86 |
|
- |
// exists for long files, so this overflow is reachable here in a way the
|
| 87 |
|
- |
// buffered path rarely hits).
|
| 88 |
288 |
|
let out_frames = if dst_rate == src_rate || src_rate == 0 {
|
| 89 |
289 |
|
n_frames
|
| 90 |
290 |
|
} else {
|
| 91 |
291 |
|
((n_frames as f64) * (dst_rate as f64 / src_rate as f64)).round() as usize
|
| 92 |
292 |
|
};
|
| 93 |
|
- |
super::encode::wav_size_check((out_frames as u64).saturating_mul(out_ch as u64), bit_depth)?;
|
| 94 |
293 |
|
|
| 95 |
|
- |
let spec = WavSpec {
|
| 96 |
|
- |
channels: out_ch as u16,
|
| 97 |
|
- |
sample_rate: dst_rate,
|
| 98 |
|
- |
bits_per_sample: bit_depth,
|
| 99 |
|
- |
sample_format: sample_format_for(bit_depth),
|
| 100 |
|
- |
};
|
| 101 |
|
- |
let mut writer =
|
| 102 |
|
- |
WavWriter::create(dest, spec).map_err(|e| io_err(dest, std::io::Error::other(e)))?;
|
| 103 |
|
- |
let mut rng = SimpleRng::new(DITHER_SEED);
|
|
294 |
+ |
let mut sink = make_sink(out_ch as u16, dst_rate, out_frames)?;
|
|
295 |
+ |
let mut frame_offset: u64 = 0;
|
|
296 |
+ |
let mut packet: Vec<f32> = Vec::new();
|
|
297 |
+ |
|
|
298 |
+ |
// Apply edit stages to a freshly decoded packet (source layout), advancing
|
|
299 |
+ |
// the absolute frame offset.
|
|
300 |
+ |
let apply_stages =
|
|
301 |
+ |
|packet: &mut Vec<f32>, stages: &mut [&mut dyn StreamStage], frame_offset: &mut u64| {
|
|
302 |
+ |
let frames = packet.len().checked_div(src_ch).unwrap_or(0);
|
|
303 |
+ |
for stage in stages.iter_mut() {
|
|
304 |
+ |
stage.process(packet, src_channels, *frame_offset);
|
|
305 |
+ |
}
|
|
306 |
+ |
*frame_offset += frames as u64;
|
|
307 |
+ |
};
|
| 104 |
308 |
|
|
| 105 |
309 |
|
if dst_rate == src_rate {
|
| 106 |
|
- |
// No resampling: decode → channel-convert → write, one packet at a time.
|
| 107 |
|
- |
let mut packet: Vec<f32> = Vec::new();
|
|
310 |
+ |
// No resampling: decode → stages → channel-convert → write.
|
| 108 |
311 |
|
loop {
|
| 109 |
312 |
|
if cancel.load(Ordering::Acquire) {
|
| 110 |
313 |
|
return Err(CoreError::Cancelled);
|
| 111 |
314 |
|
}
|
| 112 |
315 |
|
packet.clear();
|
| 113 |
|
- |
if !decode_next_into(
|
| 114 |
|
- |
dec.format.as_mut(),
|
| 115 |
|
- |
dec.decoder.as_mut(),
|
| 116 |
|
- |
dec.track_id,
|
| 117 |
|
- |
&mut packet,
|
| 118 |
|
- |
)? {
|
|
316 |
+ |
if !stream.next_block(&mut packet)? {
|
| 119 |
317 |
|
break;
|
| 120 |
318 |
|
}
|
|
319 |
+ |
apply_stages(&mut packet, stages, &mut frame_offset);
|
| 121 |
320 |
|
let (converted, _) = convert_channels(&packet, src_channels, target_channels);
|
| 122 |
321 |
|
for &s in &converted {
|
| 123 |
|
- |
write_sample(&mut writer, s, bit_depth, &mut rng)?;
|
|
322 |
+ |
sink.write_sample(s)?;
|
| 124 |
323 |
|
}
|
| 125 |
324 |
|
}
|
| 126 |
325 |
|
} else {
|
| 129 |
328 |
|
"invalid resample params: channels={src_channels}, src_rate={src_rate}, dst_rate={dst_rate}"
|
| 130 |
329 |
|
)));
|
| 131 |
330 |
|
}
|
| 132 |
|
- |
let ratio = dst_rate as f64 / src_rate as f64;
|
| 133 |
|
- |
let target_total = ((n_frames as f64) * ratio).round() as usize;
|
|
331 |
+ |
let target_total = out_frames;
|
| 134 |
332 |
|
let mut resampler = make_resampler(src_rate, dst_rate, out_ch)?;
|
| 135 |
333 |
|
|
| 136 |
334 |
|
// Per-channel input accumulators (de-interleaved). Full RESAMPLE_CHUNK
|
| 137 |
335 |
|
// blocks are emitted as they fill, so the resampler sees the same
|
| 138 |
|
- |
// chunk-aligned input sequence as the buffered path regardless of how
|
| 139 |
|
- |
// the decoder packetizes the file.
|
|
336 |
+ |
// chunk-aligned input sequence as the buffered path.
|
| 140 |
337 |
|
let mut acc: Vec<Vec<f32>> = vec![Vec::new(); out_ch];
|
| 141 |
338 |
|
let mut written = 0usize;
|
| 142 |
|
- |
let mut packet: Vec<f32> = Vec::new();
|
| 143 |
339 |
|
|
| 144 |
340 |
|
loop {
|
| 145 |
341 |
|
if cancel.load(Ordering::Acquire) {
|
| 146 |
342 |
|
return Err(CoreError::Cancelled);
|
| 147 |
343 |
|
}
|
| 148 |
344 |
|
packet.clear();
|
| 149 |
|
- |
if !decode_next_into(
|
| 150 |
|
- |
dec.format.as_mut(),
|
| 151 |
|
- |
dec.decoder.as_mut(),
|
| 152 |
|
- |
dec.track_id,
|
| 153 |
|
- |
&mut packet,
|
| 154 |
|
- |
)? {
|
|
345 |
+ |
if !stream.next_block(&mut packet)? {
|
| 155 |
346 |
|
break;
|
| 156 |
347 |
|
}
|
|
348 |
+ |
apply_stages(&mut packet, stages, &mut frame_offset);
|
| 157 |
349 |
|
let (converted, _) = convert_channels(&packet, src_channels, target_channels);
|
| 158 |
350 |
|
let frames = converted.len() / out_ch;
|
| 159 |
351 |
|
for fr in 0..frames {
|
| 170 |
362 |
|
let out = resampler
|
| 171 |
363 |
|
.process(&input, None)
|
| 172 |
364 |
|
.map_err(|e| CoreError::Export(format!("resample: {e}")))?;
|
| 173 |
|
- |
write_block(&mut writer, &out, bit_depth, &mut rng, target_total, &mut written)?;
|
|
365 |
+ |
write_block(&mut sink, &out, target_total, &mut written)?;
|
| 174 |
366 |
|
}
|
| 175 |
367 |
|
}
|
| 176 |
368 |
|
|
| 177 |
|
- |
// Final short chunk (process_partial zero-pads internally), matching the
|
| 178 |
|
- |
// buffered path's handling of the last sub-chunk.
|
|
369 |
+ |
// Final short chunk (process_partial zero-pads internally).
|
| 179 |
370 |
|
if !acc[0].is_empty() {
|
| 180 |
371 |
|
let out = resampler
|
| 181 |
372 |
|
.process_partial(Some(acc.as_slice()), None)
|
| 182 |
373 |
|
.map_err(|e| CoreError::Export(format!("resample: {e}")))?;
|
| 183 |
|
- |
write_block(&mut writer, &out, bit_depth, &mut rng, target_total, &mut written)?;
|
|
374 |
+ |
write_block(&mut sink, &out, target_total, &mut written)?;
|
| 184 |
375 |
|
}
|
| 185 |
376 |
|
|
| 186 |
377 |
|
// Drain the resampler's latency tail until we have target_total frames.
|
| 189 |
380 |
|
.process_partial::<Vec<f32>>(None, None)
|
| 190 |
381 |
|
.map_err(|e| CoreError::Export(format!("resample flush: {e}")))?;
|
| 191 |
382 |
|
let produced = out.first().map_or(0, |c| c.len());
|
| 192 |
|
- |
write_block(&mut writer, &out, bit_depth, &mut rng, target_total, &mut written)?;
|
|
383 |
+ |
write_block(&mut sink, &out, target_total, &mut written)?;
|
| 193 |
384 |
|
if produced == 0 {
|
| 194 |
385 |
|
break;
|
| 195 |
386 |
|
}
|
| 196 |
387 |
|
}
|
| 197 |
388 |
|
}
|
| 198 |
389 |
|
|
| 199 |
|
- |
writer
|
| 200 |
|
- |
.finalize()
|
| 201 |
|
- |
.map_err(|e| io_err(dest, std::io::Error::other(e)))?;
|
|
390 |
+ |
sink.finalize()?;
|
| 202 |
391 |
|
Ok(true)
|
| 203 |
392 |
|
}
|
| 204 |
393 |
|
|
|
394 |
+ |
/// Interleave and write one per-channel resampler output block to the sink,
|
|
395 |
+ |
/// clamping the total to `target_total` frames (matching the buffered path, which
|
|
396 |
+ |
/// drops the extra frames the resampler emits while flushing its latency tail).
|
|
397 |
+ |
fn write_block<S: StreamSink>(
|
|
398 |
+ |
sink: &mut S,
|
|
399 |
+ |
per_channel: &[Vec<f32>],
|
|
400 |
+ |
target_total: usize,
|
|
401 |
+ |
written: &mut usize,
|
|
402 |
+ |
) -> Result<(), CoreError> {
|
|
403 |
+ |
let frames = per_channel.first().map_or(0, |c| c.len());
|
|
404 |
+ |
for f in 0..frames {
|
|
405 |
+ |
if *written >= target_total {
|
|
406 |
+ |
return Ok(());
|
|
407 |
+ |
}
|
|
408 |
+ |
for channel in per_channel {
|
|
409 |
+ |
sink.write_sample(channel[f])?;
|
|
410 |
+ |
}
|
|
411 |
+ |
*written += 1;
|
|
412 |
+ |
}
|
|
413 |
+ |
Ok(())
|
|
414 |
+ |
}
|
|
415 |
+ |
|
|
416 |
+ |
// ── Public entry points ─────────────────────────────────────────────────────
|
|
417 |
+ |
|
|
418 |
+ |
/// Attempt a streaming WAV export. `Ok(true)` if it streamed, `Ok(false)` if the
|
|
419 |
+ |
/// source has no known frame count (caller falls back to buffered).
|
|
420 |
+ |
pub(crate) fn try_stream_wav_export(
|
|
421 |
+ |
source: &Path,
|
|
422 |
+ |
dest: &Path,
|
|
423 |
+ |
target_channels: &ExportChannels,
|
|
424 |
+ |
target_rate: Option<u32>,
|
|
425 |
+ |
bit_depth: u16,
|
|
426 |
+ |
cancel: &AtomicBool,
|
|
427 |
+ |
) -> Result<bool, CoreError> {
|
|
428 |
+ |
if !matches!(bit_depth, 8 | 16 | 24 | 32) {
|
|
429 |
+ |
return Err(CoreError::Export(format!(
|
|
430 |
+ |
"unsupported bit depth: {bit_depth} (expected 8, 16, 24, or 32)"
|
|
431 |
+ |
)));
|
|
432 |
+ |
}
|
|
433 |
+ |
let stream = FrameStream::open(source)?;
|
|
434 |
+ |
process_streaming(
|