Skip to content

Commit d3c1e19

Browse files
committed
fix(player): render low-mid-high waveforms and scroll long errors
1 parent e79aa28 commit d3c1e19

6 files changed

Lines changed: 525 additions & 236 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "audio-orbit"
3-
version = "0.9.5"
3+
version = "0.9.4"
44
edition = "2021"
55
build = "build.rs"
66

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Saved files use the stop-time based format `audio-orbit-records-yyyy-mm-dd-hh-mm
102102

103103
Audio Orbit includes an optional 100% free recognition path. Recognition is off by default. When it is enabled, internet radio can be identified instantly from stream metadata when the station provides `StreamTitle`. For audio fingerprint recognition, Audio Orbit can install and manage SongRec in the portable `.audio-orbit-dll` helper folder, or you can set a custom SongRec executable in **Settings > Recognition**.
104104

105-
Audio Orbit captures a short DSP-free sample from the current local track or live radio stream, writes a temporary WAV file, and asks SongRec to recognize it. No paid API key is required. When automatic SongRec management is enabled, Audio Orbit checks SongRec releases at most once per day on startup and also provides manual Check / Install / update buttons. The managed installer downloads a Windows `.zip` or `.exe` asset from GitHub releases into `.audio-orbit-dll`, extracts `songrec.exe`, `songrec-cli.exe`, or `audio-file-to-recognized-song.exe` when the release asset is a ZIP, validates the installed executable, and reports a normal error instead of silently stopping if the install worker fails. CLI-only release assets are normalized to `songrec-cli.exe` inside `.audio-orbit-dll` so Audio Orbit can find them consistently.
105+
Audio Orbit captures a short DSP-free sample from the current local track or live radio stream, writes a temporary WAV file, and asks SongRec to recognize it. No paid API key is required. When automatic SongRec management is enabled, Audio Orbit checks SongRec releases at most once per day on startup and also provides manual Check / Install / update buttons. The managed installer downloads a Windows portable `.zip` or `.exe` asset from GitHub releases into `.audio-orbit-dll`, prefers ZIP/CLI assets over GUI installer assets, extracts `songrec.exe`, `songrec-cli.exe`, or `audio-file-to-recognized-song.exe` when the release asset is a ZIP, validates the installed executable, and reports a normal scrollable error instead of silently stopping if the install worker fails. CLI-only release assets are normalized to `songrec-cli.exe` inside `.audio-orbit-dll` so Audio Orbit can find them consistently. Installer-like assets are rejected instead of being renamed into `.audio-orbit-dll` as if they were CLI executables.
106106

107107
SongRec is an unofficial Shazam-compatible recognizer, so this feature is treated as a free optional external backend rather than a required runtime dependency.
108108

@@ -182,7 +182,7 @@ Use the search button in the track list header to reveal search. Search filters
182182

183183
### Waveform and silence skip
184184

185-
Local track waveforms mark long quiet sections that silence skipping will bypass. Local tracks and internet radio both use RustFFT-based perceptual spectrum analysis, adaptive live normalization, spectral motion, and low/high-frequency balance, so the bars show bass-heavy and bright passages differently instead of collapsing into a flat loud/quiet wall. Internet radio uses a smoothed 15-second live visualizer window and clips older levels as new audio arrives.
185+
Local track waveforms mark long quiet sections that silence skipping will bypass. Local tracks and internet radio both use a RustFFT-based, DJ-player-style analyzer: the amplitude envelope is still based on waveform min/max and RMS, but every bar also stores low/mid/high spectral energy. The renderer draws those bands as a stacked colored bar, so bass-heavy, vocal/mid-heavy, and bright/treble-heavy sections differ visually instead of collapsing into a flat loud/quiet wall. Internet radio uses the same analyzer in a smoothed 15-second live visualizer window with adaptive normalization so live streams no longer drift into constant 100% bars.
186186

187187
### Manage Favorites
188188

@@ -246,6 +246,6 @@ Copyright (C) 2020–present [Zoltán Rózsa](https://github.com/rozsazoltan)
246246

247247
### Notes on waveform analysis and recognition
248248

249-
Audio Orbit renders local and live radio waveform bars through a RustFFT-based perceptual spectrum analysis path. The analyzer combines log-frequency band energy, spectral motion, loudness, adaptive normalization, and low/high-frequency balance. The renderer also splits each bar around the center line based on spectral brightness, so bass-heavy and treble-heavy passages are visibly different instead of only changing the total bar height. Live radio uses a smoothed 15-second AIMP-style visible window so the visualizer feels stable instead of flickery and avoids the previous constant-100% wall.
249+
Audio Orbit renders local and live radio waveform bars through a RustFFT-based low/mid/high spectral analysis path. The design follows the same general idea used by DJ-player RGB waveforms: keep a conventional min/max amplitude overview, but store separate bass, mid, and treble energy for every visual bucket. The renderer then draws a stacked spectral bar instead of using one brightness number, which makes frequency content visible rather than only loudness. Live radio uses the same analyzer with fast-attack/slow-release smoothing and adaptive floor/peak tracking so the visualizer feels stable instead of flickery and avoids the previous constant-100% wall.
250250

251251
Free recognition uses radio stream metadata first when available. If a real audio lookup is needed, Audio Orbit can install or update `songrec.exe` / `songrec-cli.exe` in the managed `.audio-orbit-dll` folder, or use a custom executable path from Settings > Recognition. Recognized titles are copied to the clipboard automatically.

src/audio_player.rs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,18 @@ impl<R> Seek for RadioStream<R> {
171171
struct RadioVisualizerBucket {
172172
at: Instant,
173173
peak: f32,
174-
brightness: f32,
174+
low: f32,
175+
mid: f32,
176+
high: f32,
175177
}
176178

177179
#[derive(Clone, Copy, Debug)]
178180
pub struct RadioVisualizerBar {
179181
pub age_seconds: f32,
180182
pub peak: f32,
181-
pub brightness: f32,
183+
pub low: f32,
184+
pub mid: f32,
185+
pub high: f32,
182186
}
183187

184188
#[derive(Clone, Debug, Default)]
@@ -325,7 +329,9 @@ impl<S: Source<Item = f32>> LiveRadioSource<S> {
325329
state.peaks.push_back(RadioVisualizerBucket {
326330
at: now,
327331
peak: bucket.level.clamp(0.0, 1.0),
328-
brightness: bucket.brightness.clamp(0.0, 1.0),
332+
low: bucket.low.clamp(0.0, 1.0),
333+
mid: bucket.mid.clamp(0.0, 1.0),
334+
high: bucket.high.clamp(0.0, 1.0),
329335
});
330336

331337
let history = Duration::from_secs(RADIO_VISUALIZER_HISTORY_SECONDS as u64);
@@ -637,7 +643,9 @@ impl AudioPlayer {
637643
}
638644

639645
let mut slot_peaks = vec![0.0_f32; requested_points];
640-
let mut slot_brightness = vec![0.5_f32; requested_points];
646+
let mut slot_low = vec![0.0_f32; requested_points];
647+
let mut slot_mid = vec![0.0_f32; requested_points];
648+
let mut slot_high = vec![0.0_f32; requested_points];
641649
for bucket in &state.peaks {
642650
let age_seconds = now.duration_since(bucket.at).as_secs_f32();
643651
if age_seconds > max_age {
@@ -650,31 +658,41 @@ impl AudioPlayer {
650658
let slot = requested_points - 1 - slot_from_right;
651659
if bucket.peak >= slot_peaks[slot] {
652660
slot_peaks[slot] = bucket.peak;
653-
slot_brightness[slot] = bucket.brightness.clamp(0.0, 1.0);
661+
slot_low[slot] = bucket.low.clamp(0.0, 1.0);
662+
slot_mid[slot] = bucket.mid.clamp(0.0, 1.0);
663+
slot_high[slot] = bucket.high.clamp(0.0, 1.0);
654664
}
655665
}
656666

657-
let mut previous = 0.0_f32;
658-
let mut previous_brightness = 0.5_f32;
667+
let mut previous_peak = 0.0_f32;
668+
let mut previous_low = 0.0_f32;
669+
let mut previous_mid = 0.0_f32;
670+
let mut previous_high = 0.0_f32;
659671
let bars = slot_peaks
660672
.into_iter()
661-
.zip(slot_brightness.into_iter())
673+
.zip(slot_low.into_iter())
674+
.zip(slot_mid.into_iter())
675+
.zip(slot_high.into_iter())
662676
.enumerate()
663-
.filter_map(|(slot, (peak, brightness))| {
664-
let shaped = if peak > previous {
665-
previous * 0.25 + peak * 0.75
677+
.filter_map(|(slot, (((peak, low), mid), high))| {
678+
let shaped = if peak > previous_peak {
679+
previous_peak * 0.22 + peak * 0.78
666680
} else {
667-
previous * 0.68 + peak * 0.32
681+
previous_peak * 0.70 + peak * 0.30
668682
};
669-
previous = shaped;
670-
previous_brightness = previous_brightness * 0.70 + brightness * 0.30;
683+
previous_peak = shaped;
684+
previous_low = previous_low * 0.58 + low * 0.42;
685+
previous_mid = previous_mid * 0.58 + mid * 0.42;
686+
previous_high = previous_high * 0.56 + high * 0.44;
671687
if shaped <= 0.003 {
672688
return None;
673689
}
674690
Some(RadioVisualizerBar {
675691
age_seconds: (requested_points - 1 - slot) as f32 * bucket_seconds,
676692
peak: shaped.clamp(0.0, 1.0),
677-
brightness: previous_brightness.clamp(0.0, 1.0),
693+
low: previous_low.clamp(0.0, 1.0),
694+
mid: previous_mid.clamp(0.0, 1.0),
695+
high: previous_high.clamp(0.0, 1.0),
678696
})
679697
})
680698
.collect();

0 commit comments

Comments
 (0)