Bump all deps; migrate symphonia 0.5 -> 0.6 decode path; track Cargo.lock
pipewire 0.9->0.10, symphonia 0.5->0.6, rosc 0.10->0.11, dirs 5->7, biquad 0.5->0.6, ratatui->0.30.2. symphonia 0.6: get_probe().probe() returns the FormatReader; default_track(TrackType::Audio); CodecParameters::Audio; make_audio_decoder; next_packet()->Option; GenericAudioBufferRef + copy_to_slice_interleaved; tags via rev.media.tags/StandardTag; Packet.track_id is a field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLUwWE2KmFPzhKaf67tWbx
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
39095f3a4a
commit
caa66b88bb
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
*.lock
|
|
||||||
target
|
target
|
||||||
|
*.lock
|
||||||
|
!Cargo.lock
|
||||||
|
|||||||
Generated
+2707
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -10,15 +10,15 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
pipewire = "0.9.2"
|
pipewire = "0.10"
|
||||||
symphonia = { version = "0.5", features = ["all"] }
|
symphonia = { version = "0.6", features = ["all"] }
|
||||||
rosc = "0.10"
|
rosc = "0.11"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
dirs = "5"
|
dirs = "7"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
@@ -26,7 +26,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
|
|
||||||
# In-process DSP
|
# In-process DSP
|
||||||
ssstretch = "0.1" # Signalsmith Stretch — independent pitch + tempo
|
ssstretch = "0.1" # Signalsmith Stretch — independent pitch + tempo
|
||||||
biquad = "0.5" # IIR biquad filters for cutoff and band split
|
biquad = "0.6" # IIR biquad filters for cutoff and band split
|
||||||
atomic_float = "1" # AtomicF32 for lock-free param state
|
atomic_float = "1" # AtomicF32 for lock-free param state
|
||||||
timestretch = "0.4" # detect_bpm / BeatGrid for waveform overlay
|
timestretch = "0.4" # detect_bpm / BeatGrid for waveform overlay
|
||||||
|
|
||||||
|
|||||||
+245
-86
@@ -11,24 +11,23 @@
|
|||||||
/// with the app
|
/// with the app
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex, atomic::Ordering::Relaxed},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use crossbeam_channel::{Receiver, Sender, bounded};
|
use crossbeam_channel::{Receiver, Sender, bounded};
|
||||||
use symphonia::core::{
|
use symphonia::core::{
|
||||||
audio::{AudioBufferRef, Signal},
|
audio::GenericAudioBufferRef,
|
||||||
codecs::DecoderOptions,
|
codecs::{CodecParameters, audio::AudioDecoderOptions},
|
||||||
formats::FormatOptions,
|
formats::{FormatOptions, FormatReader, TrackType, probe::Hint},
|
||||||
io::MediaSourceStream,
|
io::MediaSourceStream,
|
||||||
meta::{MetadataOptions, StandardTagKey},
|
meta::{MetadataOptions, StandardTag},
|
||||||
probe::Hint,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
deck::DeckId,
|
deck::{DeckId, next_bar_sample},
|
||||||
dsp::{DeckDsp, DeckParams},
|
dsp::{DeckDsp, DeckParams, TEMPO_MAX, TEMPO_MIN},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Commands ────────────────────────────────────────────────────────────────
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
||||||
@@ -48,6 +47,18 @@ pub enum AudioCmd {
|
|||||||
AppendPcm { deck: DeckId, load_gen: u64, frames: Vec<f32> },
|
AppendPcm { deck: DeckId, load_gen: u64, frames: Vec<f32> },
|
||||||
/// Signal that decoding is complete for this generation.
|
/// Signal that decoding is complete for this generation.
|
||||||
MarkDecodingDone { deck: DeckId, load_gen: u64 },
|
MarkDecodingDone { deck: DeckId, load_gen: u64 },
|
||||||
|
/// Forwarded by the app after receiving `AudioStatus::BeatGrid`. Stores
|
||||||
|
/// the grid on the engine-side `DeckState` so SyncPlay can do bar-math.
|
||||||
|
SetBeatGrid { deck: DeckId, beats: Vec<u64>, bpm: f32 },
|
||||||
|
/// Beat-aligned auto-mix start: the slave deck waits until the master's
|
||||||
|
/// next bar boundary, then begins on its own first detected downbeat
|
||||||
|
/// with tempo matched to master's effective BPM.
|
||||||
|
SyncPlay {
|
||||||
|
deck: DeckId,
|
||||||
|
master: DeckId,
|
||||||
|
bars_to_wait: u32,
|
||||||
|
bass_kill_bars: u32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Status updates ──────────────────────────────────────────────────────────
|
// ─── Status updates ──────────────────────────────────────────────────────────
|
||||||
@@ -74,9 +85,17 @@ pub enum AudioStatus {
|
|||||||
/// Sample-frame offset of the first peak in this chunk.
|
/// Sample-frame offset of the first peak in this chunk.
|
||||||
frame_offset: u64,
|
frame_offset: u64,
|
||||||
},
|
},
|
||||||
/// BPM estimate from `timestretch::detect_bpm` over the first ~60 s of
|
/// Beat grid + BPM estimate from `timestretch::detect_beat_grid` over the
|
||||||
/// decoded audio. Emitted once per Load, after `Ready`.
|
/// first ~60 s of decoded audio. Emitted once per Load, after `Ready`.
|
||||||
Bpm { deck: DeckId, bpm: f32 },
|
/// `beats` contains sample-frame positions; the first entry is treated as
|
||||||
|
/// the downbeat of bar 1 (the detector does not label downbeats).
|
||||||
|
BeatGrid { deck: DeckId, beats: Vec<u64>, bpm: f32 },
|
||||||
|
/// A sync-play has been armed: the slave deck is queued and will start
|
||||||
|
/// at the master's next bar boundary. UI uses this to flip the deck
|
||||||
|
/// into `PlayState::WaitingForSync`.
|
||||||
|
SyncQueued { deck: DeckId, master: DeckId },
|
||||||
|
/// The wait fired — slave is now playing in phase with master.
|
||||||
|
SyncFired { deck: DeckId },
|
||||||
/// Decoded PCM chunk — routed back through the app into AudioCmd::AppendPcm.
|
/// Decoded PCM chunk — routed back through the app into AudioCmd::AppendPcm.
|
||||||
PcmChunk {
|
PcmChunk {
|
||||||
deck: DeckId,
|
deck: DeckId,
|
||||||
@@ -99,6 +118,16 @@ pub enum AudioStatus {
|
|||||||
|
|
||||||
// ─── Per-deck internal state ──────────────────────────────────────────────────
|
// ─── Per-deck internal state ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Pending sync-play state. The slave callback fires when master's published
|
||||||
|
/// `position` atomic crosses `master_target_frame`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct SyncTarget {
|
||||||
|
pub master_idx: usize,
|
||||||
|
pub master_target_frame: u64,
|
||||||
|
pub slave_start_frame: u64,
|
||||||
|
pub bass_kill_out_frames: u64,
|
||||||
|
}
|
||||||
|
|
||||||
struct DeckState {
|
struct DeckState {
|
||||||
id: DeckId,
|
id: DeckId,
|
||||||
playing: bool,
|
playing: bool,
|
||||||
@@ -114,6 +143,14 @@ struct DeckState {
|
|||||||
/// handles tempo). Advanced by the number of frames consumed each block.
|
/// handles tempo). Advanced by the number of frames consumed each block.
|
||||||
read_pos: usize,
|
read_pos: usize,
|
||||||
cue_pos: usize,
|
cue_pos: usize,
|
||||||
|
/// Detected BPM, or 0.0 if unknown.
|
||||||
|
bpm: f32,
|
||||||
|
/// Sample-frame positions of detected beats (engine-side copy of the
|
||||||
|
/// app-visible `Deck::beat_grid`). Empty until decode finishes.
|
||||||
|
beat_grid: Vec<u64>,
|
||||||
|
/// Armed by `AudioCmd::SyncPlay`; consumed by the realtime callback when
|
||||||
|
/// master's position crosses the target.
|
||||||
|
sync_pending: Option<SyncTarget>,
|
||||||
dsp: DeckDsp,
|
dsp: DeckDsp,
|
||||||
params: Arc<DeckParams>,
|
params: Arc<DeckParams>,
|
||||||
last_vu_update: std::time::Instant,
|
last_vu_update: std::time::Instant,
|
||||||
@@ -131,6 +168,9 @@ impl DeckState {
|
|||||||
sample_rate,
|
sample_rate,
|
||||||
read_pos: 0,
|
read_pos: 0,
|
||||||
cue_pos: 0,
|
cue_pos: 0,
|
||||||
|
bpm: 0.0,
|
||||||
|
beat_grid: Vec::new(),
|
||||||
|
sync_pending: None,
|
||||||
dsp: DeckDsp::new(sample_rate),
|
dsp: DeckDsp::new(sample_rate),
|
||||||
params,
|
params,
|
||||||
last_vu_update: std::time::Instant::now(),
|
last_vu_update: std::time::Instant::now(),
|
||||||
@@ -199,6 +239,10 @@ fn engine_thread(
|
|||||||
let deck_id = DeckId::from_index(i).unwrap();
|
let deck_id = DeckId::from_index(i).unwrap();
|
||||||
let ds_cb = Arc::clone(ds);
|
let ds_cb = Arc::clone(ds);
|
||||||
let status_tx_cb = status_tx.clone();
|
let status_tx_cb = status_tx.clone();
|
||||||
|
// Per-callback view of every deck's `Arc<DeckParams>` — needed so the
|
||||||
|
// slave's callback can lock-free peek the master's `position` atomic
|
||||||
|
// to decide when its sync wait should fire.
|
||||||
|
let params_all = params.clone();
|
||||||
|
|
||||||
let props = pipewire::properties::properties! {
|
let props = pipewire::properties::properties! {
|
||||||
*pipewire::keys::MEDIA_TYPE => "Audio",
|
*pipewire::keys::MEDIA_TYPE => "Audio",
|
||||||
@@ -234,6 +278,24 @@ fn engine_thread(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Sync-play wait: fire if master crossed the target ───
|
||||||
|
if let Some(t) = ds.sync_pending.clone() {
|
||||||
|
let master_pos = params_all[t.master_idx].position.load(Relaxed);
|
||||||
|
if master_pos >= t.master_target_frame {
|
||||||
|
ds.read_pos = (t.slave_start_frame as usize)
|
||||||
|
.min(ds.total_frames());
|
||||||
|
ds.playing = true;
|
||||||
|
ds.pending_play = false;
|
||||||
|
ds.sync_pending = None;
|
||||||
|
ds.params.bass_kill_remaining_out_frames
|
||||||
|
.store(t.bass_kill_out_frames, Relaxed);
|
||||||
|
ds.params.bass_kill_initial_out_frames
|
||||||
|
.store(t.bass_kill_out_frames, Relaxed);
|
||||||
|
let _ = status_tx_cb.try_send(
|
||||||
|
AudioStatus::SyncFired { deck: ds.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ds.playing && !ds.pcm.is_empty() {
|
if ds.playing && !ds.pcm.is_empty() {
|
||||||
let total = ds.total_frames();
|
let total = ds.total_frames();
|
||||||
if ds.read_pos >= total {
|
if ds.read_pos >= total {
|
||||||
@@ -275,6 +337,15 @@ fn engine_thread(
|
|||||||
*chunk.size_mut() = (n_frames * 8) as u32;
|
*chunk.size_mut() = (n_frames * 8) as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish source position for cross-deck sync visibility,
|
||||||
|
// and tick the bass-kill timer down in wall-clock frames.
|
||||||
|
ds.params.position.store(ds.read_pos as u64, Relaxed);
|
||||||
|
if ds.playing {
|
||||||
|
let bk = ds.params.bass_kill_remaining_out_frames.load(Relaxed);
|
||||||
|
ds.params.bass_kill_remaining_out_frames
|
||||||
|
.store(bk.saturating_sub(n_frames as u64), Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = status_tx_cb.try_send(AudioStatus::Position {
|
let _ = status_tx_cb.try_send(AudioStatus::Position {
|
||||||
deck: ds.id,
|
deck: ds.id,
|
||||||
position: ds.read_pos as u64,
|
position: ds.read_pos as u64,
|
||||||
@@ -299,11 +370,12 @@ fn engine_thread(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Command processing thread
|
// Command processing thread
|
||||||
let ds_cmd = deck_states.clone();
|
let ds_cmd = deck_states.clone();
|
||||||
let st_cmd = status_tx.clone();
|
let st_cmd = status_tx.clone();
|
||||||
|
let params_cmd = params.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
for cmd in cmd_rx.iter() {
|
for cmd in cmd_rx.iter() {
|
||||||
handle_cmd(cmd, &ds_cmd, &st_cmd);
|
handle_cmd(cmd, &ds_cmd, ¶ms_cmd, &st_cmd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -313,7 +385,12 @@ fn engine_thread(
|
|||||||
|
|
||||||
// ─── Command handler ──────────────────────────────────────────────────────────
|
// ─── Command handler ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn handle_cmd(cmd: AudioCmd, states: &[Arc<Mutex<DeckState>>], status_tx: &Sender<AudioStatus>) {
|
fn handle_cmd(
|
||||||
|
cmd: AudioCmd,
|
||||||
|
states: &[Arc<Mutex<DeckState>>],
|
||||||
|
params: &[Arc<DeckParams>],
|
||||||
|
status_tx: &Sender<AudioStatus>,
|
||||||
|
) {
|
||||||
match cmd {
|
match cmd {
|
||||||
AudioCmd::Load { deck, path } => {
|
AudioCmd::Load { deck, path } => {
|
||||||
let load_gen = {
|
let load_gen = {
|
||||||
@@ -382,6 +459,93 @@ fn handle_cmd(cmd: AudioCmd, states: &[Arc<Mutex<DeckState>>], status_tx: &Sende
|
|||||||
ds.decoding_done = true;
|
ds.decoding_done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AudioCmd::SetBeatGrid { deck, beats, bpm } => {
|
||||||
|
let mut ds = states[deck as usize].lock().unwrap();
|
||||||
|
ds.bpm = bpm;
|
||||||
|
ds.beat_grid = beats;
|
||||||
|
}
|
||||||
|
AudioCmd::SyncPlay { deck, master, bars_to_wait, bass_kill_bars } => {
|
||||||
|
if deck == master {
|
||||||
|
tracing::warn!("SyncPlay refused: deck and master are both {}", deck.label());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let slave_idx = deck as usize;
|
||||||
|
let master_idx = master as usize;
|
||||||
|
|
||||||
|
// Snapshot master state — published position is fresh enough
|
||||||
|
// (last realtime block, < ~25 ms ago).
|
||||||
|
let master_pos = params[master_idx].position.load(Relaxed);
|
||||||
|
let master_tempo = params[master_idx].tempo.load(Relaxed);
|
||||||
|
let (master_bpm, target_frame) = {
|
||||||
|
let m = states[master_idx].lock().unwrap();
|
||||||
|
let t = next_bar_sample(&m.beat_grid, master_pos, bars_to_wait);
|
||||||
|
(m.bpm, t)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(target_frame) = target_frame else {
|
||||||
|
tracing::warn!(
|
||||||
|
"SyncPlay deck {}: master {} has no beat grid (or no more bars). Falling back to immediate Play.",
|
||||||
|
deck.label(), master.label()
|
||||||
|
);
|
||||||
|
let mut ds = states[slave_idx].lock().unwrap();
|
||||||
|
if !ds.pcm.is_empty() { ds.playing = true; } else { ds.pending_play = true; }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if master_bpm <= 0.0 {
|
||||||
|
tracing::warn!("SyncPlay deck {}: master has no BPM; immediate Play.", deck.label());
|
||||||
|
let mut ds = states[slave_idx].lock().unwrap();
|
||||||
|
if !ds.pcm.is_empty() { ds.playing = true; } else { ds.pending_play = true; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slave-side beat info.
|
||||||
|
let (slave_bpm, slave_first_beat) = {
|
||||||
|
let s = states[slave_idx].lock().unwrap();
|
||||||
|
(s.bpm, s.beat_grid.first().copied())
|
||||||
|
};
|
||||||
|
let Some(slave_first_beat) = slave_first_beat else {
|
||||||
|
tracing::warn!(
|
||||||
|
"SyncPlay deck {}: slave has no beat grid yet; immediate Play.",
|
||||||
|
deck.label()
|
||||||
|
);
|
||||||
|
let mut ds = states[slave_idx].lock().unwrap();
|
||||||
|
if !ds.pcm.is_empty() { ds.playing = true; } else { ds.pending_play = true; }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if slave_bpm <= 0.0 {
|
||||||
|
let mut ds = states[slave_idx].lock().unwrap();
|
||||||
|
if !ds.pcm.is_empty() { ds.playing = true; } else { ds.pending_play = true; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beat-match.
|
||||||
|
let master_eff_bpm = master_bpm * master_tempo;
|
||||||
|
let slave_tempo = (master_eff_bpm / slave_bpm).clamp(TEMPO_MIN, TEMPO_MAX);
|
||||||
|
params[slave_idx].tempo.store(slave_tempo, Relaxed);
|
||||||
|
|
||||||
|
let bass_kill_out_frames = if bass_kill_bars > 0 {
|
||||||
|
((bass_kill_bars as f32 * 4.0 * 60.0 / master_eff_bpm)
|
||||||
|
* STREAM_SR as f32) as u64
|
||||||
|
} else { 0 };
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut ds = states[slave_idx].lock().unwrap();
|
||||||
|
ds.sync_pending = Some(SyncTarget {
|
||||||
|
master_idx,
|
||||||
|
master_target_frame: target_frame,
|
||||||
|
slave_start_frame: slave_first_beat,
|
||||||
|
bass_kill_out_frames,
|
||||||
|
});
|
||||||
|
ds.playing = false;
|
||||||
|
ds.pending_play = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"SyncPlay armed: deck {} → master {} at frame {} (tempo {:.3}, bass-kill {} frames)",
|
||||||
|
deck.label(), master.label(), target_frame, slave_tempo, bass_kill_out_frames
|
||||||
|
);
|
||||||
|
let _ = status_tx.send(AudioStatus::SyncQueued { deck, master });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,26 +576,26 @@ fn decode_file_streaming(
|
|||||||
hint.with_extension(ext);
|
hint.with_extension(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
let probed = symphonia::default::get_probe().format(
|
let mut format = symphonia::default::get_probe().probe(
|
||||||
&hint,
|
&hint,
|
||||||
mss,
|
mss,
|
||||||
&FormatOptions::default(),
|
FormatOptions::default(),
|
||||||
&MetadataOptions::default(),
|
MetadataOptions::default(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut format = probed.format;
|
|
||||||
|
|
||||||
let track = format
|
let track = format
|
||||||
.tracks()
|
.default_track(TrackType::Audio)
|
||||||
.iter()
|
|
||||||
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
|
|
||||||
.ok_or_else(|| anyhow!("No audio track found"))?
|
.ok_or_else(|| anyhow!("No audio track found"))?
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let sr = track.codec_params.sample_rate.unwrap_or(44100);
|
let audio_params = match &track.codec_params {
|
||||||
let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(2);
|
Some(CodecParameters::Audio(p)) => p.clone(),
|
||||||
|
_ => return Err(anyhow!("track has no audio codec parameters")),
|
||||||
|
};
|
||||||
|
|
||||||
let est_duration = track.codec_params.n_frames.unwrap_or(0);
|
let sr = audio_params.sample_rate.unwrap_or(44100);
|
||||||
|
|
||||||
|
let est_duration = track.num_frames.unwrap_or(0);
|
||||||
|
|
||||||
let (artist, album) = extract_tags(&mut format);
|
let (artist, album) = extract_tags(&mut format);
|
||||||
|
|
||||||
@@ -445,7 +609,7 @@ fn decode_file_streaming(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut decoder = symphonia::default::get_codecs()
|
let mut decoder = symphonia::default::get_codecs()
|
||||||
.make(&track.codec_params, &DecoderOptions::default())?;
|
.make_audio_decoder(&audio_params, &AudioDecoderOptions::default())?;
|
||||||
|
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
|
|
||||||
@@ -459,19 +623,19 @@ fn decode_file_streaming(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let packet = match format.next_packet() {
|
let packet = match format.next_packet() {
|
||||||
Ok(p) => p,
|
Ok(Some(p)) => p,
|
||||||
|
Ok(None) => break, // end of stream
|
||||||
Err(symphonia::core::errors::Error::IoError(_)) => break,
|
Err(symphonia::core::errors::Error::IoError(_)) => break,
|
||||||
Err(symphonia::core::errors::Error::ResetRequired) => break,
|
Err(symphonia::core::errors::Error::ResetRequired) => break,
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
if packet.track_id() != track_id { continue; }
|
if packet.track_id != track_id { continue; }
|
||||||
|
|
||||||
let decoded = decoder.decode(&packet)?;
|
let decoded = decoder.decode(&packet)?;
|
||||||
push_samples_and_peaks(
|
push_samples_and_peaks(
|
||||||
&mut pcm_batch,
|
&mut pcm_batch,
|
||||||
&mut total_frames,
|
&mut total_frames,
|
||||||
decoded,
|
decoded,
|
||||||
channels,
|
|
||||||
&mut wf,
|
&mut wf,
|
||||||
&mut mono_for_bpm,
|
&mut mono_for_bpm,
|
||||||
bpm_cap,
|
bpm_cap,
|
||||||
@@ -510,9 +674,17 @@ fn decode_file_streaming(
|
|||||||
// ms for a 60 s clip; the Ready status above means the track is already
|
// ms for a 60 s clip; the Ready status above means the track is already
|
||||||
// playable before this fires.
|
// playable before this fires.
|
||||||
if !mono_for_bpm.is_empty() {
|
if !mono_for_bpm.is_empty() {
|
||||||
let bpm = timestretch::detect_bpm(&mono_for_bpm, sr);
|
let grid = timestretch::detect_beat_grid(&mono_for_bpm, sr);
|
||||||
if bpm.is_finite() && bpm > 0.0 {
|
if grid.bpm.is_finite() && grid.bpm > 0.0 && !grid.beats.is_empty() {
|
||||||
let _ = status_tx.send(AudioStatus::Bpm { deck, bpm: bpm as f32 });
|
// `beats` from timestretch are `usize` sample indices into the
|
||||||
|
// 60 s analysis window — same sample-frame coordinate space the
|
||||||
|
// engine uses for `read_pos`.
|
||||||
|
let beats: Vec<u64> = grid.beats.iter().map(|&b| b as u64).collect();
|
||||||
|
let _ = status_tx.send(AudioStatus::BeatGrid {
|
||||||
|
deck,
|
||||||
|
beats,
|
||||||
|
bpm: grid.bpm as f32,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,17 +779,17 @@ impl WaveformAccum {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_tags(format: &mut Box<dyn symphonia::core::formats::FormatReader>) -> (Option<String>, Option<String>) {
|
fn extract_tags(format: &mut Box<dyn FormatReader>) -> (Option<String>, Option<String>) {
|
||||||
let mut artist = None;
|
let mut artist = None;
|
||||||
let mut album = None;
|
let mut album = None;
|
||||||
|
|
||||||
if let Some(meta) = format.metadata().current() {
|
if let Some(rev) = format.metadata().current() {
|
||||||
for tag in meta.tags() {
|
for tag in &rev.media.tags {
|
||||||
match tag.std_key {
|
match &tag.std {
|
||||||
Some(StandardTagKey::Artist) => artist = Some(tag.value.to_string()),
|
Some(StandardTag::Artist(v)) => artist = Some(v.to_string()),
|
||||||
Some(StandardTagKey::AlbumArtist) if artist.is_none()
|
Some(StandardTag::AlbumArtist(v)) if artist.is_none()
|
||||||
=> artist = Some(tag.value.to_string()),
|
=> artist = Some(v.to_string()),
|
||||||
Some(StandardTagKey::Album) => album = Some(tag.value.to_string()),
|
Some(StandardTag::Album(v)) => album = Some(v.to_string()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,8 +802,7 @@ fn extract_tags(format: &mut Box<dyn symphonia::core::formats::FormatReader>) ->
|
|||||||
fn push_samples_and_peaks(
|
fn push_samples_and_peaks(
|
||||||
pcm_batch: &mut Vec<f32>,
|
pcm_batch: &mut Vec<f32>,
|
||||||
total_frames: &mut u64,
|
total_frames: &mut u64,
|
||||||
buf: AudioBufferRef,
|
buf: GenericAudioBufferRef,
|
||||||
channels: usize,
|
|
||||||
wf: &mut WaveformAccum,
|
wf: &mut WaveformAccum,
|
||||||
mono_for_bpm: &mut Vec<f32>,
|
mono_for_bpm: &mut Vec<f32>,
|
||||||
bpm_cap: usize,
|
bpm_cap: usize,
|
||||||
@@ -640,53 +811,41 @@ fn push_samples_and_peaks(
|
|||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
status_tx: &Sender<AudioStatus>,
|
status_tx: &Sender<AudioStatus>,
|
||||||
) {
|
) {
|
||||||
macro_rules! push {
|
// symphonia 0.6 converts any sample format to interleaved f32 for us.
|
||||||
($buf:expr, $scale:expr) => {{
|
let ch = buf.num_planes().max(1);
|
||||||
let frames = $buf.frames();
|
let n = buf.samples_interleaved();
|
||||||
for f in 0..frames {
|
let mut inter = vec![0f32; n];
|
||||||
let l = *$buf.chan(0).get(f).unwrap_or(&Default::default());
|
buf.copy_to_slice_interleaved::<f32, _>(inter.as_mut_slice());
|
||||||
let r = if channels >= 2 {
|
|
||||||
*$buf.chan(1).get(f).unwrap_or(&Default::default())
|
|
||||||
} else { l };
|
|
||||||
|
|
||||||
let lf = l as f32 * $scale;
|
let frames = n / ch;
|
||||||
let rf = r as f32 * $scale;
|
for f in 0..frames {
|
||||||
pcm_batch.push(lf);
|
let lf = inter[f * ch];
|
||||||
pcm_batch.push(rf);
|
let rf = if ch >= 2 { inter[f * ch + 1] } else { lf };
|
||||||
*total_frames += 1;
|
pcm_batch.push(lf);
|
||||||
|
pcm_batch.push(rf);
|
||||||
|
*total_frames += 1;
|
||||||
|
|
||||||
let mono = (lf + rf) * 0.5;
|
let mono = (lf + rf) * 0.5;
|
||||||
wf.push_sample(mono);
|
wf.push_sample(mono);
|
||||||
|
|
||||||
if mono_for_bpm.len() < bpm_cap {
|
if mono_for_bpm.len() < bpm_cap {
|
||||||
mono_for_bpm.push(mono);
|
mono_for_bpm.push(mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
if wf.bands_batch.len() >= 64 {
|
if wf.bands_batch.len() >= 64 {
|
||||||
let _ = status_tx.send(AudioStatus::WaveformChunk {
|
let _ = status_tx.send(AudioStatus::WaveformChunk {
|
||||||
deck,
|
deck,
|
||||||
bands: std::mem::take(&mut wf.bands_batch),
|
bands: std::mem::take(&mut wf.bands_batch),
|
||||||
frame_offset: wf.bucket_start_frame,
|
frame_offset: wf.bucket_start_frame,
|
||||||
});
|
});
|
||||||
let _ = status_tx.send(AudioStatus::PcmChunk {
|
let _ = status_tx.send(AudioStatus::PcmChunk {
|
||||||
deck,
|
deck,
|
||||||
load_gen,
|
load_gen,
|
||||||
frames: std::mem::take(pcm_batch),
|
frames: std::mem::take(pcm_batch),
|
||||||
sample_rate,
|
sample_rate,
|
||||||
});
|
});
|
||||||
wf.bucket_start_frame = wf.frame_offset;
|
wf.bucket_start_frame = wf.frame_offset;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
match buf {
|
|
||||||
AudioBufferRef::F32(b) => push!(b, 1.0_f32),
|
|
||||||
AudioBufferRef::F64(b) => push!(b, 1.0_f32),
|
|
||||||
AudioBufferRef::S16(b) => push!(b, 1.0 / 32768.0_f32),
|
|
||||||
AudioBufferRef::S32(b) => push!(b, 1.0 / 2_147_483_648.0_f32),
|
|
||||||
AudioBufferRef::U8(b) => push!(b, 1.0 / 128.0_f32),
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user