dj sets + lyrics: a mix that names its own tracks, and words pulled off the separated voice

This commit is contained in:
KeelrLeek committed 2026-08-24 10:49:10 -05:00
1 parent 4efe85b6bf
commit 1dd8573808
41 files changed
+9288 -647

No files matched your search

+43 -3
View File
@@ -76,6 +76,8 @@ pub struct Settings {
pub stream_source_prefs: config::SourcePrefs,
pub download_source_prefs: config::SourcePrefs,
pub volume_normalization: bool,
/// Word-by-word lyric fill — experimental, default off.
pub word_timing: bool,
}
#[tauri::command]
@@ -114,6 +116,7 @@ pub async fn get_settings(
stream_source_prefs: cfg.stream_source_prefs,
download_source_prefs: cfg.download_source_prefs,
volume_normalization: config::volume_normalization(&cfg),
word_timing: config::word_timing(&cfg),
username: cfg.username,
})
}
@@ -141,6 +144,7 @@ pub async fn set_settings(
stream_source_prefs: Option<config::SourcePrefs>,
download_source_prefs: Option<config::SourcePrefs>,
volume_normalization: Option<bool>,
word_timing: Option<bool>,
) -> Result<(), String> {
let mut cfg = config::load(&app);
let library_folder_moved = download_dir
@@ -209,6 +213,9 @@ pub async fn set_settings(
// takes effect from the next track.
crate::loudness::set_enabled(&app, enabled);
}
if word_timing.is_some() {
cfg.word_timing = word_timing;
}
config::save(&app, &cfg);
// A new library folder is a new library. Re-base immediately rather than
@@ -1550,6 +1557,15 @@ pub async fn catalog_search(
Ok(state.metadata.catalog_search(&query).await)
}
#[tauri::command]
pub async fn catalog_track_by_isrc(
state: State<'_, LibraryState>,
isrc: String,
) -> Result<Option<metadata::CatalogTrack>, String> {
state.touch_catalog_activity();
Ok(state.metadata.catalog_track_by_isrc(&isrc).await)
}
#[tauri::command]
pub async fn catalog_artist(
app: tauri::AppHandle,
@@ -4051,6 +4067,7 @@ pub fn play_local(
app: tauri::AppHandle,
path: String,
duration: Option<f64>,
start_secs: Option<f64>,
) -> Result<(), String> {
if std::fs::metadata(&path).is_err() {
return Err("file not found".into());
@@ -4064,7 +4081,11 @@ pub fn play_local(
0,
None,
Box::new(move || {
app2.state::<PlayerState>().player.play_local(&path2, duration);
// `start_secs` = play a SLICE of the file from there (the
// setlist segment rows); plain tracks pass None.
app2.state::<PlayerState>()
.player
.play_local_from(&path2, duration, start_secs);
}),
);
Ok(())
@@ -4407,6 +4428,8 @@ pub async fn waveform(
players: State<'_, PlayerState>,
path: String,
buckets: usize,
start_secs: Option<f64>,
end_secs: Option<f64>,
) -> Result<Vec<f32>, String> {
const MAX_WAVEFORM_BUCKETS: usize = 4096;
if buckets > MAX_WAVEFORM_BUCKETS {
@@ -4417,7 +4440,20 @@ pub async fn waveform(
if buckets == 0 {
return Ok(Vec::new());
}
let key = format!("{path}#{buckets}");
// A range = a setlist SLICE's lane; each slice of one file caches
// separately under its own key.
let ranged = start_secs.is_some() || end_secs.is_some();
let key = if ranged {
format!(
"{path}#{buckets}@{:.3}-{}",
start_secs.unwrap_or(0.0),
end_secs
.map(|e| format!("{e:.3}"))
.unwrap_or_else(|| "end".into()),
)
} else {
format!("{path}#{buckets}")
};
let job = players.waveform_job(&key);
let result = async {
let job_guard = job.clone().lock_owned().await;
@@ -4426,7 +4462,11 @@ pub async fn waveform(
}
let p = std::path::PathBuf::from(&path);
let (job_guard, peaks) = tauri::async_runtime::spawn_blocking(move || {
let peaks = player::compute_waveform(&p, buckets);
let peaks = if ranged {
player::compute_waveform_range(&p, buckets, start_secs.unwrap_or(0.0), end_secs)
} else {
player::compute_waveform(&p, buckets)
};
(job_guard, peaks)
})
.await