Library changes outside the platform's own dependencies (those came with the platform commits above). libs/asset and libs/flow move to the Stage repository with the apps that use them. Covers work ebf442a94..2b1a41df4 for libs: - wm: the Android super-app compiles tiles on the phone against the host engine - cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust - cef: a page's audio can be captured instead of played - flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow - svg: a stroke that bends tighter than its half width collapses its inner edge to the corner - the accent a name is written with stops deciding whether search finds it - ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs - The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code sear - workspace: Scope's settings carry the code map's tab width and the directories each prepared project hides from its map - cef, video, platform: what Stage's browser needs from the shared layers - platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows - ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds - stitch: the crate says what the rest of the repository says about its license - ci: a run is one target dir and one cargo batch per target, and the wall draws - apps: what the CI box found - Remove Flow, asset and VJ applications moved into Stage - tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests - libs: no warnings in the workspace check on any row - tests: the last three failures of the CI box's night - gif: the crate's doc examples compile - cef: a hosted browser survives its host's exit - tests: the three binaries over ten seconds in release come under it - git: the memory ledger can take what it is asked for - script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix - fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts - platform, widgets, wm: a touch that is taken away is cancelled, never released - platform, wm: hosted apps ask the WM for what they cannot do themselves - wm on Android: apps build on the phone - task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D - flowgraph: ordered ports take many wires, sockets can override their icon, and the canvas embeds as a viewport - cef: captured frames and audio carry callback clocks and a navigation epoch - git: timings read a portable clock - ai: the hub's rig-fixture tests find the asset library without the asset client - ai llm: a Metal main buffer is filled in 32 MB chunks - diskmap: a scan lists each macOS directory in one getattrlistbulk call and classifies files without allocating or locking - platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap, - builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one - sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository - git: worktree status matches git on real repositories and can be cancelled - cuda: builds link only the CUDA toolkit they are given, never a system install - audio_route: one API on macOS, Windows and Linux, monitoring by default and processing only when asked, plus shaders that discard a value compile to - platform, widgets, ai: a restyle recompiles nothing it already has, shows only complete frames, and a window can own its caption - audio_route example: print the samples that reached the processor each second - widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them - ai hub: a local chat with no model says so once, plainly - ai: Claude Desktop drives any app with the F10 panel - wm: children open no console windows on Windows - ai: Connect hands the .mcpb to Claude Desktop itself - civil-time: one local wall clock for every app, and it knows the zone on Windows - ai-speech: makepad-ai-sfx (and with it ai-h3) is built only for IndexTTS - script + wasm_bridge: no build script that reruns on every file - build: the AI stack, csg, Scope and Source Library join the one workspace - ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature - widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features - apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature - livepipe: takes makepad-widgets without its default families, now that the AI stack is in the one workspace - ai chat: a build without local AI starts on the first provider that answers here, not on "No model" - thiserror: RUSTC_BOOTSTRAP no longer turns on the unstable generic member access API - builder: makepad-builder.exe declares itself like a well-formed Windows program and stops a stalled build through a job object instead of taskkill - builder + ai-cuda: the CUDA kernel progress shows in the Builder's Compile row, not scribbled over its screen - repo: no third-party comic archive, generated models or sample asset in the tree - builder: the Windows Builder ships as source, runs in its own console, and apps compile with Rust's GNU toolchain or Microsoft's - win_resource: PNG decode and encode on makepad-fast-inflate instead of zune-png - builder: one folder layout everywhere, makepad-builder.zip with only the files a Windows build reads, a Rust download over parallel connections, and Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
203 lines
7.7 KiB
Rust
203 lines
7.7 KiB
Rust
//! List audio clients, or play one application's audio through this process.
|
|
//!
|
|
//! `route list` prints clients and output devices.
|
|
//! `route play <bundle-id> [--seconds N] [--gain G] [--eq] [--hear]
|
|
//! [--monitor] [--switch S] [--skip S]` taps that application, silences its
|
|
//! direct output while the tap is read (`--hear` leaves it playing), plays
|
|
//! it here, and prints the tapped and played levels once a second and their
|
|
//! average over the run after `--skip` seconds. `--monitor` starts
|
|
//! monitoring (the application plays by itself, nothing plays here);
|
|
//! `--switch` flips between monitoring and processing after S seconds.
|
|
//! The host bundle needs NSAudioCaptureUsageDescription; the first play
|
|
//! asks for system audio recording permission.
|
|
|
|
use makepad_audio_route::{Equalizer, FrameInfo, Gain, Mute, Passthrough, Processor, Route, RouteConfig, Source};
|
|
use std::env;
|
|
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
fn main() {
|
|
let args: Vec<String> = env::args().skip(1).collect();
|
|
if args.is_empty() || args[0] == "list" {
|
|
list();
|
|
return;
|
|
}
|
|
if args[0] == "play" {
|
|
if let Err(error) = play(&args[1..]) {
|
|
eprintln!("{error}");
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
eprintln!("usage: route list");
|
|
eprintln!(" route play <bundle-id> [--seconds N] [--gain G] [--eq] [--hear] [--monitor] [--switch S] [--skip S]");
|
|
std::process::exit(2);
|
|
}
|
|
|
|
fn list() {
|
|
match makepad_audio_route::processes() {
|
|
Ok(processes) => {
|
|
println!("clients");
|
|
for process in processes {
|
|
let state = if process.output_running { "playing" } else { "idle" };
|
|
println!(" {state:7} pid {:6} {:<28} {}", process.pid, process.bundle_id, process.name);
|
|
}
|
|
}
|
|
Err(error) => eprintln!("clients: {error}"),
|
|
}
|
|
match makepad_audio_route::outputs() {
|
|
Ok(devices) => {
|
|
println!("outputs");
|
|
for device in devices {
|
|
println!(" {:5.0} Hz {:2} ch {} {}", device.sample_rate, device.channels, device.uid, device.name);
|
|
}
|
|
}
|
|
Err(error) => eprintln!("outputs: {error}"),
|
|
}
|
|
}
|
|
|
|
fn play(args: &[String]) -> Result<(), makepad_audio_route::Error> {
|
|
let bundle = args.first().filter(|arg| !arg.starts_with("--")).cloned().ok_or(makepad_audio_route::Error::NotFound)?;
|
|
let mut seconds = 10u64;
|
|
let mut gain = 1.0f32;
|
|
let mut eq = false;
|
|
let mut mute = Mute::ReplaceWhileRouted;
|
|
let mut processing = true;
|
|
let mut switch_at = 0u64;
|
|
let mut skip = 0u64;
|
|
let mut index = 1;
|
|
while index < args.len() {
|
|
match args[index].as_str() {
|
|
"--seconds" => {
|
|
seconds = args.get(index + 1).and_then(|value| value.parse().ok()).unwrap_or(seconds);
|
|
index += 2;
|
|
}
|
|
"--gain" => {
|
|
gain = args.get(index + 1).and_then(|value| value.parse().ok()).unwrap_or(gain);
|
|
index += 2;
|
|
}
|
|
"--eq" => {
|
|
eq = true;
|
|
index += 1;
|
|
}
|
|
"--hear" => {
|
|
mute = Mute::HearOriginal;
|
|
index += 1;
|
|
}
|
|
"--monitor" => {
|
|
processing = false;
|
|
index += 1;
|
|
}
|
|
"--switch" => {
|
|
switch_at = args.get(index + 1).and_then(|value| value.parse().ok()).unwrap_or(0);
|
|
index += 2;
|
|
}
|
|
"--skip" => {
|
|
skip = args.get(index + 1).and_then(|value| value.parse().ok()).unwrap_or(0);
|
|
index += 2;
|
|
}
|
|
_ => index += 1,
|
|
}
|
|
}
|
|
println!("routing {bundle} for {seconds}s at gain {gain}, {mute:?}, {}", if processing { "processing" } else { "monitoring" });
|
|
let levels = Arc::new(Levels::default());
|
|
let inner: Box<dyn Processor> = if eq {
|
|
let (equalizer, knobs) = Equalizer::new();
|
|
knobs.set_low_db(4.0);
|
|
knobs.set_high_db(3.0);
|
|
Box::new(equalizer)
|
|
} else if (gain - 1.0).abs() > 0.001 {
|
|
Box::new(Gain::new(gain).0)
|
|
} else {
|
|
Box::new(Passthrough)
|
|
};
|
|
let route = open(&bundle, mute, processing, Box::new(Meter { inner, levels: Arc::clone(&levels) }))?;
|
|
println!("open: {:.0} Hz, {} ch", route.sample_rate(), route.channels());
|
|
let (mut in_peak, mut out_peak) = (0f32, 0f32);
|
|
let (mut energy, mut samples) = (0f64, 0f64);
|
|
for second in 1..=seconds {
|
|
thread::sleep(Duration::from_secs(1));
|
|
let (rms, peak, played, count) = levels.take();
|
|
in_peak = in_peak.max(peak);
|
|
out_peak = out_peak.max(played);
|
|
if second > skip {
|
|
energy += (rms as f64).powi(2) * count as f64;
|
|
samples += count as f64;
|
|
}
|
|
// The sample count shows whether frames arrive at all while the
|
|
// application is silent (silent buffers) or not (no callbacks).
|
|
println!("{second:3}s tapped rms {:7.2} dBFS peak {:7.2} dBFS played peak {:7.2} dBFS {count} samples", db(rms), db(peak), db(played));
|
|
if second == switch_at {
|
|
let _ = route.set_processing(!route.processing());
|
|
println!(" switched to {}", if route.processing() { "processing" } else { "monitoring" });
|
|
}
|
|
if second % 2 == 0 || second == switch_at + 1 {
|
|
println!(" {}", route.describe());
|
|
}
|
|
}
|
|
drop(route);
|
|
println!("peak tapped {in_peak:0.4}, played {out_peak:0.4}; tapped rms over the run after {skip}s: {:.3} dBFS", db((energy / samples.max(1.0)).sqrt() as f32));
|
|
Ok(())
|
|
}
|
|
|
|
fn db(linear: f32) -> f32 {
|
|
20.0 * linear.max(1e-10).log10()
|
|
}
|
|
|
|
/// What the tap delivered and what was played, since the last take.
|
|
#[derive(Default)]
|
|
struct Levels {
|
|
/// The sum of squares, in units of 2^-40.
|
|
sumsq: AtomicU64,
|
|
count: AtomicU64,
|
|
peak_in: AtomicU32,
|
|
peak_out: AtomicU32,
|
|
}
|
|
|
|
impl Levels {
|
|
/// RMS and peak of the tap, the played peak, and the samples metered;
|
|
/// cleared.
|
|
fn take(&self) -> (f32, f32, f32, u64) {
|
|
let sum = self.sumsq.swap(0, Ordering::Relaxed) as f64 / (1u64 << 40) as f64;
|
|
let count = self.count.swap(0, Ordering::Relaxed);
|
|
let peak_in = f32::from_bits(self.peak_in.swap(0, Ordering::Relaxed));
|
|
let peak_out = f32::from_bits(self.peak_out.swap(0, Ordering::Relaxed));
|
|
((sum / count.max(1) as f64).sqrt() as f32, peak_in, peak_out, count)
|
|
}
|
|
}
|
|
|
|
struct Meter {
|
|
inner: Box<dyn Processor>,
|
|
levels: Arc<Levels>,
|
|
}
|
|
|
|
impl Processor for Meter {
|
|
fn process(&mut self, frames: &mut [f32], info: &FrameInfo) {
|
|
let (mut sum, mut peak) = (0f64, 0f32);
|
|
for sample in frames.iter() {
|
|
sum += (*sample as f64) * (*sample as f64);
|
|
peak = peak.max(sample.abs());
|
|
}
|
|
self.levels.sumsq.fetch_add((sum * (1u64 << 40) as f64) as u64, Ordering::Relaxed);
|
|
self.levels.count.fetch_add(frames.len() as u64, Ordering::Relaxed);
|
|
self.levels.peak_in.fetch_max(peak.to_bits(), Ordering::Relaxed);
|
|
self.inner.process(frames, info);
|
|
let played = frames.iter().fold(0f32, |peak, sample| peak.max(sample.abs()));
|
|
self.levels.peak_out.fetch_max(played.to_bits(), Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
fn open(bundle: &str, mute: Mute, processing: bool, processor: Box<dyn makepad_audio_route::Processor>) -> Result<Route, makepad_audio_route::Error> {
|
|
Route::open(
|
|
RouteConfig {
|
|
sources: vec![Source::BundleId(bundle.to_string())],
|
|
mute,
|
|
output: None,
|
|
processing,
|
|
state_dir: Some(std::env::temp_dir()),
|
|
},
|
|
processor,
|
|
)
|
|
}
|