Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
497 changes: 491 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

49 changes: 26 additions & 23 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
[workspace]
members = [
"prpr",
"prpr-avc",
"prpr-pbc",
"prpr-l10n",
"phira",
"phira-main",
"phira-monitor",
"phira",
"phira-main",
"phira-monitor",
"prpr",
"prpr-auto-offset",
"prpr-avc",
"prpr-l10n",
"prpr-pbc",
"tools/*",
]
resolver = "2"

Expand All @@ -32,13 +34,30 @@ image = { version = "0.25.10", default-features = false }
inputbox = "0.1.1"
lru = "0.16.3"
lyon = "1.0.19"

macroquad = { git = "https://github.com/Mivik/prpr-macroquad", rev = "b2eab29", default-features = false }
miniquad = { git = "https://github.com/Mivik/prpr-miniquad", rev = "0c525a3" }
nalgebra = "0.34.1"

objc2 = "0.6.4"
objc2-core-foundation = "0.3.2"
objc2-foundation = "0.3.2"
objc2-ui-kit = "0.3.2"
objc2-uniform-type-identifiers = "0.3.2"
once_cell = "1.21.4"
phira = { path = "phira", default-features = false }
phira-mp-client = { git = "https://github.com/TeamFlos/phira-mp", rev = "6967475" }
phira-mp-common = { git = "https://github.com/TeamFlos/phira-mp", rev = "6967475" }
pollster = "0.4.0"
prpr = { path = "prpr", default-features = false }
prpr-auto-offset = { path = "prpr-auto-offset" }
prpr-avc = { path = "prpr-avc" }
prpr-l10n = { path = "prpr-l10n" }
rand = "0.8.5"
regex = "1.12.3"
reqwest = { version = "0.13.2", default-features = false }
rfd = "0.17.2"
sasa = { git = "https://github.com/Mivik/sasa", rev = "e76229b", default-features = false }
serde = "1.0.228"
serde_json = "1.0.149"
serde_yaml = "0.9.34"
Expand All @@ -51,22 +70,6 @@ uuid = "1.22.0"
walkdir = "2.5.0"
zip = { version = "8.3.0", default-features = false }

objc2 = "0.6.4"
objc2-core-foundation = "0.3.2"
objc2-foundation = "0.3.2"
objc2-ui-kit = "0.3.2"
objc2-uniform-type-identifiers = "0.3.2"

macroquad = { git = "https://github.com/Mivik/prpr-macroquad", rev = "b2eab29", default-features = false }
miniquad = { git = "https://github.com/Mivik/prpr-miniquad", rev = "0c525a3" }
phira = { path = "phira", default-features = false }
phira-mp-client = { git = "https://github.com/TeamFlos/phira-mp", rev = "6967475" }
phira-mp-common = { git = "https://github.com/TeamFlos/phira-mp", rev = "6967475" }
prpr = { path = "prpr", default-features = false }
prpr-avc = { path = "prpr-avc" }
prpr-l10n = { path = "prpr-l10n" }
sasa = { git = "https://github.com/Mivik/sasa", rev = "e76229b", default-features = false }

[profile.release]
opt-level = 2
strip = true
Expand Down
8 changes: 4 additions & 4 deletions phira-monitor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ chrono = { workspace = true }
futures-util = { workspace = true }
log = "0.4.29"
macroquad = { workspace = true, default-features = false }

phira-mp-client = { workspace = true }
phira-mp-common = { workspace = true }
pretty_env_logger = "0.5.0"
prpr = { workspace = true }
reqwest = { workspace = true, default-features = false, features = [
"json",
"stream",
Expand All @@ -24,7 +28,3 @@ serde_json = { workspace = true }
serde_yaml = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

phira-mp-client = { workspace = true }
phira-mp-common = { workspace = true }
prpr = { workspace = true }
34 changes: 17 additions & 17 deletions phira/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }

[lints]
workspace = true

[lib]
crate-type = ["lib", "cdylib"]

Expand Down Expand Up @@ -46,8 +43,12 @@ logos = "0.16.1"
lru = { workspace = true }
lyon = { workspace = true }
macroquad = { workspace = true, default-features = false }

miniquad = { workspace = true }
nalgebra = { workspace = true }
once_cell = { workspace = true }
phira-mp-client = { workspace = true }
phira-mp-common = { workspace = true }
pollster = { workspace = true }
prpr = { workspace = true, features = ["log"], default-features = false }
prpr-l10n = { workspace = true }
Expand All @@ -63,6 +64,7 @@ reqwest = { workspace = true, default-features = false, features = [
"rustls",
"query",
] }
sanitize-filename = "0.6.0"
semver = { version = "1.0.27", features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
Expand All @@ -78,29 +80,24 @@ walkdir = { workspace = true }
zip = { workspace = true, features = ["chrono"] }
zstd = "0.13"

miniquad = { workspace = true }
phira-mp-client = { workspace = true }
phira-mp-common = { workspace = true }
sanitize-filename = "0.6.0"
[target.'cfg(not(any(target_os = "android", target_env = "ohos")))'.dependencies]
sasa = { workspace = true, default-features = true }

[target.'cfg(target_os = "android")'.dependencies]
jni = "0.22.4"
ndk-context = "0.1"
sasa = { workspace = true, default-features = false, features = ["oboe"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios", target_env = "ohos")))'.dependencies]
rfd = { workspace = true }

[target.'cfg(target_env = "ohos")'.dependencies]
sasa = { workspace = true, default-features = false, features = ["ohos"] }
napi-derive-ohos = { version = "1.1.6" }
napi-ohos = { version = "1.1.6", default-features = false, features = [
"napi8",
"async",
] }
sasa = { workspace = true, default-features = false, features = ["ohos"] }

[target.'cfg(not(any(target_os = "android", target_env = "ohos")))'.dependencies]
sasa = { workspace = true, default-features = true }

[target.'cfg(not(any(target_os = "android", target_os = "ios", target_env = "ohos")))'.dependencies]
rfd = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.22.4"
ndk-context = "0.1"
sasa = { workspace = true, default-features = false, features = ["oboe"] }

[target.'cfg(target_os = "ios")'.dependencies]
objc2 = { workspace = true }
Expand All @@ -113,3 +110,6 @@ dotenv-build = "0.1"
[dev-dependencies]
fluent = { workspace = true }
fluent-syntax = { workspace = true }

[lints]
workspace = true
3 changes: 3 additions & 0 deletions prpr-auto-offset/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Test charts and extracted files
test-charts/
test-charts-extracted/
10 changes: 10 additions & 0 deletions prpr-auto-offset/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "prpr-auto-offset"
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
rayon = "1"
realfft = "3"
rustfft = "6.4.1"
79 changes: 79 additions & 0 deletions prpr-auto-offset/src/audio/energy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::Signal;

/// Energy-difference novelty signal.
///
/// Computes the positive first-order difference of short-time RMS energy.
/// No thresholding — every frame gets a value.
pub struct EnergyDiff {
/// Native novelty samples at the energy hop rate.
native: Vec<f32>,
/// Time step between native samples, in seconds.
native_dt: f64,
/// Timestamp of the first native sample, in seconds.
native_t0: f64,
}

impl EnergyDiff {
pub fn new(pcm: &[f32], sample_rate: u32, frame_ms: f64, hop_ms: f64) -> Self {
let frame_samples = (frame_ms / 1000.0 * sample_rate as f64).round() as usize;
let hop_samples = (hop_ms / 1000.0 * sample_rate as f64).round() as usize;
let native_dt = hop_samples as f64 / sample_rate as f64;
let native_t0 = native_dt + frame_samples as f64 / sample_rate as f64 / 2.0;

let native = compute_energy_diff(pcm, frame_samples, hop_samples);
Self {
native,
native_dt,
native_t0,
}
}
}

impl Signal for EnergyDiff {
fn samples(&self, ts: &[f64]) -> Vec<f32> {
if ts.is_empty() {
return vec![];
}
ts.iter().map(|&t| interpolate(&self.native, self.native_dt, self.native_t0, t)).collect()
}
}

fn compute_energy_diff(pcm: &[f32], frame_samples: usize, hop_samples: usize) -> Vec<f32> {
if pcm.len() < frame_samples || frame_samples == 0 || hop_samples == 0 {
return vec![];
}

let energies: Vec<f32> = (0..)
.step_by(hop_samples)
.take_while(|&start| start + frame_samples <= pcm.len())
.map(|start| {
let sum_sq: f32 = pcm[start..start + frame_samples].iter().map(|&x| x * x).sum();
(sum_sq / frame_samples as f32).sqrt()
})
.collect();

if energies.len() < 2 {
return vec![];
}

energies.windows(2).map(|w| (w[1] - w[0]).max(0.0)).collect()
}

/// Linear interpolation at time `t` (seconds) in a signal sampled every `dt`.
fn interpolate(data: &[f32], dt: f64, t0: f64, t: f64) -> f32 {
if data.is_empty() {
return 0.0;
}
let idx = (t - t0) / dt;
if idx < 0.0 {
return data[0];
}
let i = idx as usize;
if i + 1 >= data.len() {
return data[data.len() - 1];
}
let frac = (idx - i as f64) as f32;
let a = data[i];
let b = data[i + 1];
a + (b - a) * frac
}
7 changes: 7 additions & 0 deletions prpr-auto-offset/src/audio/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod energy;
mod spectral;
mod superflux;

pub use energy::EnergyDiff;
pub use spectral::SpectralFlux;
pub use superflux::{compute_spectrogram, Filterbank, SuperFlux};
104 changes: 104 additions & 0 deletions prpr-auto-offset/src/audio/spectral.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use crate::Signal;
use rustfft::{num_complex::Complex32, FftPlanner};

/// Spectral-flux novelty signal computed via STFT.
///
/// For each STFT frame, computes the sum of positive magnitude-spectrum
/// differences from the previous frame. The result is a dense time series
/// with one value per STFT frame.
pub struct SpectralFlux {
/// Native novelty samples at the STFT hop rate.
native: Vec<f32>,
/// Time step between native samples, in seconds.
native_dt: f64,
/// Timestamp of the first native sample, in seconds.
native_t0: f64,
}

impl SpectralFlux {
pub fn new(pcm: &[f32], sample_rate: u32, fft_size: usize, hop_size: usize) -> Self {
assert!(fft_size.is_power_of_two());
let native_dt = hop_size as f64 / sample_rate as f64;
let native_t0 = fft_size as f64 / sample_rate as f64 / 2.0;
let native = compute_spectral_flux(pcm, fft_size, hop_size);
Self {
native,
native_dt,
native_t0,
}
}
}

impl Signal for SpectralFlux {
fn samples(&self, ts: &[f64]) -> Vec<f32> {
if ts.is_empty() {
return vec![];
}
ts.iter().map(|&t| interpolate(&self.native, self.native_dt, self.native_t0, t)).collect()
}
}

fn compute_spectral_flux(pcm: &[f32], n: usize, hop: usize) -> Vec<f32> {
if pcm.len() < n {
return vec![];
}

let n2 = (n - 1) as f32;
let window: Vec<f32> = (0..n).map(|i| 0.5 - 0.5 * (2.0 * std::f32::consts::PI * i as f32 / n2).cos()).collect();

let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(n);

let num_frames = (pcm.len() - n) / hop + 1;
let num_bins = n / 2 + 1;
let mut prev_mags = vec![0.0f32; num_bins];
let mut buffer = vec![Complex32::new(0.0, 0.0); n];
let mut novelty = Vec::with_capacity(num_frames);

for frame in 0..num_frames {
let start = frame * hop;
for (i, &w) in window.iter().enumerate() {
buffer[i] = Complex32::new(pcm[start + i] * w, 0.0);
}

fft.process(&mut buffer);

for i in 0..num_bins {
prev_mags[i] = core::mem::replace(&mut prev_mags[i], buffer[i].norm());
}

if frame == 0 {
novelty.push(0.0);
continue;
}

let flux: f32 = buffer[..num_bins]
.iter()
.enumerate()
.map(|(i, c)| (c.norm() - prev_mags[i]).max(0.0))
.sum();

novelty.push(flux);
}
Comment on lines +66 to +82

novelty
}

/// Linear interpolation at time `t` (seconds) in a signal sampled every `dt`.
fn interpolate(data: &[f32], dt: f64, t0: f64, t: f64) -> f32 {
if data.is_empty() {
return 0.0;
}
let idx = (t - t0) / dt;
if idx < 0.0 {
return data[0];
}
let i = idx as usize;
if i + 1 >= data.len() {
return data[data.len() - 1];
}
let frac = (idx - i as f64) as f32;
let a = data[i];
let b = data[i + 1];
a + (b - a) * frac
}
Loading
Loading