Skip to main content

max / audiofiles

4.7 KB · 127 lines History Blame Raw
1 //! Export sample-info helpers: assemble a `RhaiSampleInfo` for device-profile
2 //! conform and probe bit depth from a file header. Split out of direct.rs.
3
4 #[cfg(feature = "device-profiles")]
5 use audiofiles_core::export::ExportItem;
6
7 /// Build a RhaiSampleInfo from an ExportItem and database lookups.
8 #[cfg(feature = "device-profiles")]
9 pub(super) fn build_sample_info(
10 db: &audiofiles_core::db::Database,
11 store: &audiofiles_core::store::SampleStore,
12 item: &ExportItem,
13 ) -> audiofiles_rhai::types::RhaiSampleInfo {
14 // Query audio_analysis for sample_rate and channels. Read the integers as
15 // i64 so a valid-but-out-of-range stored value isn't reported as a query
16 // error and lumped in with the legitimate "no analysis row yet" case; range
17 // problems are surfaced explicitly below instead. Only QueryReturnedNoRows
18 // is a silent (0,0) fallback, a genuine read error or a corrupt value
19 // (negative / oversized rate or channels) is logged so a device profile
20 // can't silently drop the sample on mystery zeros.
21 let (sample_rate, channels, duration) = db
22 .conn()
23 .query_row(
24 "SELECT sample_rate, channels, duration FROM audio_analysis WHERE hash = ?1",
25 [&item.hash],
26 |row| {
27 Ok((
28 row.get::<_, i64>(0)?,
29 row.get::<_, i64>(1)?,
30 row.get::<_, f64>(2)?,
31 ))
32 },
33 )
34 .map_or_else(
35 |e| {
36 if !matches!(e, rusqlite::Error::QueryReturnedNoRows) {
37 tracing::warn!(
38 "Failed to query audio_analysis for {}: {e}",
39 &item.hash[..8]
40 );
41 }
42 (0, 0, item.duration.unwrap_or(0.0))
43 },
44 |(sr, ch, dur)| {
45 let sample_rate = u32::try_from(sr).unwrap_or_else(|_| {
46 tracing::warn!("Corrupt sample_rate {sr} for {}", &item.hash[..8]);
47 0
48 });
49 let channels = u16::try_from(ch).unwrap_or_else(|_| {
50 tracing::warn!("Corrupt channels {ch} for {}", &item.hash[..8]);
51 0
52 });
53 (sample_rate, channels, dur)
54 },
55 );
56
57 // Query samples for file_size
58 let file_size = db
59 .conn()
60 .query_row(
61 "SELECT file_size FROM samples WHERE hash = ?1 AND deleted_at IS NULL",
62 [&item.hash],
63 // file_size is non-negative in practice; a corrupt negative surfaces
64 // as an error rather than wrapping silently to a nonsense u64.
65 |row| {
66 let v = row.get::<_, i64>(0)?;
67 u64::try_from(v).map_err(|_| rusqlite::Error::IntegralValueOutOfRange(0, v))
68 },
69 )
70 .unwrap_or_else(|_| {
71 // Fallback: read from store
72 store
73 .sample_path(&item.hash, &item.ext)
74 .ok()
75 .and_then(|p| std::fs::metadata(p).ok())
76 .map_or(0, |m| m.len())
77 });
78
79 // bit_depth is filled by the caller in a parallel pass off the DB lock, see
80 // resolve_device_profile. Probing it here would open a file per item while
81 // holding the DB lock on the GUI thread.
82 audiofiles_rhai::types::RhaiSampleInfo {
83 hash: item.hash.to_string(),
84 name: item.name.clone(),
85 extension: item.ext.clone(),
86 sample_rate,
87 bit_depth: 0,
88 channels,
89 duration,
90 file_size,
91 }
92 }
93
94 /// Probe bit depth from a WAV/AIFF file header without full decode.
95 #[cfg(feature = "device-profiles")]
96 pub(super) fn probe_bit_depth(path: &std::path::Path) -> Option<u16> {
97 // Try hound first (WAV)
98 if let Ok(reader) = hound::WavReader::open(path) {
99 return Some(reader.spec().bits_per_sample);
100 }
101 // Try symphonia for AIFF/other formats
102 let file = std::fs::File::open(path).ok()?;
103 let mss = symphonia::core::io::MediaSourceStream::new(
104 Box::new(file),
105 symphonia::core::io::MediaSourceStreamOptions::default(),
106 );
107 let mut hint = symphonia::core::formats::probe::Hint::new();
108 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
109 hint.with_extension(ext);
110 }
111 let format = symphonia::default::get_probe()
112 .probe(
113 &hint,
114 mss,
115 symphonia::core::formats::FormatOptions::default(),
116 symphonia::core::meta::MetadataOptions::default(),
117 )
118 .ok()?;
119 let track = format.default_track(symphonia::core::formats::TrackType::Audio)?;
120 track
121 .codec_params
122 .as_ref()?
123 .audio()?
124 .bits_per_sample
125 .map(|b| b as u16)
126 }
127