Compare commits

..

No commits in common. "ae6ead454ebda6a85048f477af0703dce2b6a45f" and "ae76e39d2ee060fd1032ed5d09325e445d4ea5da" have entirely different histories.

4 changed files with 48 additions and 184 deletions

View file

@ -1,22 +0,0 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View file

@ -9,4 +9,3 @@ eframe = "0.29.1"
egui = "0.29.1"
egui_flex = "0.1.1"
jack = "0.13.0"
native-dialog = "0.7.0"

View file

@ -1,15 +1,9 @@
# FSLCMix (Simple Jack Mixer)
# FSLCMix (Simple Jack Equalizer)
Made for my Linux Audio presentation for the USU Free Software and Linux Club (FSLC). Supports an arbitrary number of channels (default 5), but can be specified on the args via the `-c`/`--channels` argument. The exercise is to check out the `jackless` branch and re-add the JACK functionality without peeking at the `main` branch.
![Screenshot](./screenshots/screenshot.png)
## Features
- RMS and Peak Tracking
- Per-track Mute, Solo and Gain
- Per-track hard limiting
To build:
```

View file

@ -6,7 +6,7 @@ use eframe::egui::*;
const PEAK_HOLD_TIME: usize = 4000;
const DECAY_FACTOR: f32 = 0.9999;
use std::{process::exit, sync::{Arc, Mutex}};
use std::sync::{Arc, Mutex};
fn main() -> eframe::Result {
let args = Args::parse();
@ -14,7 +14,6 @@ fn main() -> eframe::Result {
let app = MixApp {
mix : shared_mix.clone(),
};
if let Ok((client, _status)) = jack::Client::new("fslcmix", jack::ClientOptions::default()) {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([500.0, 350.0])
@ -22,7 +21,7 @@ fn main() -> eframe::Result {
.with_max_inner_size([5000.0, 350.0]),
..Default::default()
};
let (client, _status) = jack::Client::new("fslcmix", jack::ClientOptions::default()).unwrap();
let process_callback = register_jack_callback(&client, shared_mix);
// Create process and activate the client
let process = jack::contrib::ClosureProcessHandler::new(process_callback);
@ -41,27 +40,6 @@ fn main() -> eframe::Result {
eprintln!("JACK exited with error: {err}");
}
result
}
else {
eprintln!("Could not connect to JACK Audio Server.");
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([150.0, 80.0])
.with_min_inner_size([150.0, 80.0])
.with_max_inner_size([150.0, 80.0]),
..Default::default()
};
let result = eframe::run_native(
"Error - FSLCMix",
options,
Box::new(|cc| {
cc.egui_ctx.set_theme(egui::Theme::Dark);
Ok(Box::new(ErrorBox { text: "Could not connect to JACK Audio Server".to_owned(), }))
}),
);
result
}
}
fn register_jack_callback(client: &jack::Client, mixer: Arc<Mutex<FslcMix>>) -> impl FnMut(&jack::Client, &jack::ProcessScope) -> jack::Control {
@ -85,33 +63,6 @@ fn register_jack_callback(client: &jack::Client, mixer: Arc<Mutex<FslcMix>>) ->
process_callback
}
fn db_peak(val : f32) -> f32 {
20.0 * val.log10()
}
fn db_rms(val : f32) -> f32 {
10.0 * val.log10()
}
struct ErrorBox {
text: String,
}
impl eframe::App for ErrorBox {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical(|ui| {
ui.label(&self.text);
ui.horizontal(|ui| {
if ui.button("Okay").clicked() {
exit(1);
}
});
});
});
}
}
struct MixApp {
mix: Arc<Mutex<FslcMix>>,
}
@ -137,7 +88,6 @@ struct FslcMix {
master: MixChannel,
normalize: bool,
ui_size: egui::Vec2,
max_gain: f32,
}
impl FslcMix {
@ -156,7 +106,6 @@ impl FslcMix {
normalize: false,
ui_size: egui::Vec2::new(400.0, 330.0), // This size doesn't matter since it's
// overritten
max_gain: 1.25,
}
}
@ -215,27 +164,7 @@ impl FslcMix {
ui.vertical(|ui| {
ui.horizontal(|ui| {
// ui.label("Licensed under the GPLv3.");
ui.label("Max Gain:");
let slider = ui.add(egui::DragValue::new(&mut self.max_gain).range(1.1..=2.0));
if slider.changed() {
self.update_max_gain();
}
let btn = ui.button("Reset");
if btn.clicked() {
self.max_gain = 1.25;
self.update_max_gain();
}
ui.toggle_value(&mut self.normalize, "Normalize");
if ui.add(egui::Button::new("All Unmute").frame(false)).clicked() {
for channel in &mut self.channels {
channel.mute = false;
}
}
if ui.add(egui::Button::new("All Unsolo").frame(false)).clicked() {
for channel in &mut self.channels {
channel.solo = false;
}
}
ui.toggle_value(&mut self.normalize, "Normalize")
});
});
ui.horizontal(|ui| {
@ -254,15 +183,6 @@ impl FslcMix {
// frame.set_window_size(window_size);
// frame.request_repaint();
}
fn update_max_gain(&mut self) {
// update max gain for all channels
for channel in &mut self.channels {
channel.max_gain = self.max_gain;
}
self.master.max_gain = self.max_gain;
}
}
struct MixChannel {
@ -276,19 +196,16 @@ struct MixChannel {
limit: bool,
mute: bool,
solo: bool,
others_solo: bool,
show_rms: bool,
max_gain: f32,
}
impl MixChannel {
fn mix(&mut self, input : &[f32], output : &mut [f32], any_solo : bool) {
// if self.mute || (any_solo && !self.solo) {
// self.last = 0.0;
// return;
// }
self.others_solo = any_solo;
if self.mute || (any_solo && !self.solo) {
self.last = 0.0;
return;
}
// Sanity check
assert!(input.len() == output.len());
self.rms(input);
@ -304,10 +221,7 @@ impl MixChannel {
if out_sample > self.max {
self.max = out_sample;
}
// Only mix into the output if we're not muted or no other tracks have solo
if !(self.mute || (any_solo && !self.solo)) {
output[i] += out_sample;
}
self.last = out_sample;
self.update_smoothed(self.last);
}
@ -317,29 +231,20 @@ impl MixChannel {
ui.vertical(|ui| {
ui.vertical(|ui| {
let wrap_mode = TextWrapMode::Extend;
let pb = ui.add(egui::Button::new(
format!("Peak: {:+2.2} dB", db_peak(self.max)))
.frame(false)
.small()
.wrap_mode(wrap_mode));
let pb = ui.add(egui::Button::new(format!("Peak: {:.2} dB", self.max.log10())).wrap_mode(wrap_mode));
if pb.clicked() {
self.max = 0.0;
}
let rb = ui.add(egui::Button::new(
format!("RMS: {:+2.2} dB", db_rms(self.last_rms)))
.frame(false)
.small()
.wrap_mode(wrap_mode));
let rb = ui.add(egui::Button::new(format!("RMS: {:.2}", self.last_rms)).wrap_mode(wrap_mode));
if rb.clicked() {
self.last_rms = 0.0;
}
});
ui.horizontal(|ui| {
ui.spacing_mut().slider_width = 175.0;
ui.add(egui::Slider::new(&mut self.gain, 0.0..=self.max_gain)
ui.add(egui::Slider::new(&mut self.gain, 0.0..=1.2)
//.text("Gain")
.vertical()
.max_decimals(2));
.vertical());
// ui.add(egui::ProgressBar::new(self.last));
self.levels_bar(ui);
});
@ -356,7 +261,7 @@ impl MixChannel {
ui.toggle_value(&mut self.solo, "S");
ui.toggle_value(&mut self.limit, "Lim");
});
ui.add(egui::TextEdit::singleline(&mut self.channel_name).desired_width(85.0));
ui.add(egui::TextEdit::singleline(&mut self.channel_name).desired_width(75.0));
});
}
@ -367,39 +272,29 @@ impl MixChannel {
} else {
self.last_smoothed
};
let val_db = if self.show_rms {
db_rms(val)
} else {
db_peak(val)
};
let (rect, response) = ui.allocate_exact_size(vec2(10.0, 190.0), egui::Sense::hover());
let (rect, response) = ui.allocate_exact_size(vec2(15.0, 190.0), egui::Sense::hover());
let painter = ui.painter();
let filled_height = (rect.height() * val / self.max_gain).min(rect.height()); // Show a bit over max amplitude
let filled_height = (rect.height() * val / 1.2).min(rect.height()); // Show a bit over max amplitude
// let filled_rect = Rect::from_min_max(rect.min, rect.min + vec2(rect.width(), filled_height));
// let remaining_rect = Rect::from_min_max(filled_rect.max, rect.max);
let filled_rect = Rect::from_min_max(rect.max - vec2(rect.width(), filled_height), rect.max);
// let remaining_rect = Rect::from_min_max(rect.min, filled_rect.max);
// painter.rect_filled(remaining_rect, 0.0, Color32::from_rgb(200, 0, 0));
let color_saturation = if self.mute || (!self.solo && self.others_solo) { 50 } else { 200 };
let color = if val < 1.0 {
Color32::from_rgb(0, color_saturation, 0)
} else if val < self.max_gain {
Color32::from_rgb(color_saturation, color_saturation, 0)
Color32::from_rgb(0, 200, 0)
} else if val < 1.2 {
Color32::from_rgb(200, 200, 0)
} else {
Color32::from_rgb(color_saturation, 0, 0)
Color32::from_rgb(200, 0, 0)
};
painter.rect_filled(filled_rect, 0.0, color);
painter.rect_stroke(rect, 0.0, (1.0, Color32::DARK_GRAY));
// Draw scale numbers
let num_steps = (self.max_gain * 10.0) as u16;
let num_steps = 12;
let step_size = rect.height() / num_steps as f32;
for i in 0..=num_steps {
let y_pos = rect.top() + i as f32 * step_size;
let number = if self.show_rms {
db_rms((num_steps - i) as f32 / 10.0)
} else {
db_peak((num_steps - i) as f32 / 10.0)
};
let number = (num_steps - i) as f32 / 10.0;
// Invert the order if you want 0 at the bottom
let text_pos = Pos2::new(rect.right() + 5.0, y_pos);
painter.text(text_pos,
@ -409,7 +304,7 @@ impl MixChannel {
Color32::DARK_GRAY);
}
response.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(format!("{:.3} dB", val_db));
.on_hover_text(format!("{:.1} db", self.last.log10()));
}
fn declare_jack_port(&self, client : &jack::Client) -> jack::Port<jack::AudioIn> {
@ -445,9 +340,7 @@ impl Default for MixChannel {
limit: false,
mute: false,
solo: false,
others_solo: false,
show_rms: false,
max_gain: 1.25,
}
}
}