From a6dfec305a53c6a7618b98f43a6b4b89cfb76102 Mon Sep 17 00:00:00 2001 From: Minijackson Date: Sun, 7 Oct 2018 23:40:35 +0200 Subject: Add support for PulseEffects --- src/cli.rs | 113 ++++++++++++++++++++++++++-------- src/main.rs | 49 +++------------ src/pa_effects.rs | 125 ++++++++++++++++++++++++++++++++++++++ src/pa_eq.rs | 110 +++++++++++++++++++++++++++++++++ src/parsing/equalizer_apo.lalrpop | 8 +-- src/parsing/equalizer_apo.rs | 9 +-- src/utils.rs | 96 ++++++++--------------------- 7 files changed, 358 insertions(+), 152 deletions(-) create mode 100644 src/pa_effects.rs create mode 100644 src/pa_eq.rs (limited to 'src') diff --git a/src/cli.rs b/src/cli.rs index f24b222..4a499b5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,31 +21,16 @@ pub struct Cli { #[derive(StructOpt, Debug)] pub enum Command { - #[structopt(name = "load",)] - /// Load and switch to a given equalizer configuration - Load(LoadCli), - #[structopt(name = "reset")] - /// Switch to a neutral equalizer - Reset(ResetCli), -} - -#[derive(StructOpt, Debug)] -pub struct LoadCli { - #[structopt(default_value = "-")] - /// The file from which to load the equalizer configuration - /// - /// If "-" is given, read the configuration from the command-line. - pub file: String, - #[structopt( - short = "f", - raw( - possible_values = "&EqualizerConfFormat::variants()", - case_insensitive = "true" - ), - default_value = "EqualizerAPO" - )] - /// The file format of the equalizer configuration - pub format: EqualizerConfFormat, + #[structopt(name = "pa-eq")] + /// PulseAudio equalizer related commands + /// + /// Warning: the PulseAudio equalizer has been deprecated for a while, + /// and is known to sometimes cause crashes, latency or audible + /// artifacts + PaEq(pa_eq::Command), + #[structopt(name = "pa-effects")] + /// PulseEffects equalizer related commands + PaEffects(pa_effects::Command), } arg_enum! { @@ -55,5 +40,79 @@ arg_enum! { } } -#[derive(StructOpt, Debug)] -pub struct ResetCli {} +pub mod pa_eq { + use super::EqualizerConfFormat; + + #[derive(StructOpt, Debug)] + pub enum Command { + #[structopt(name = "load",)] + /// Load and switch to a given equalizer configuration + Load(LoadCli), + #[structopt(name = "reset")] + /// Switch to a neutral equalizer + Reset(ResetCli), + } + + #[derive(StructOpt, Debug)] + pub struct LoadCli { + #[structopt(default_value = "-")] + /// The file from which to load the equalizer configuration + /// + /// If "-" is given, read the configuration from the command-line. + pub file: String, + #[structopt( + short = "f", + raw( + possible_values = "&EqualizerConfFormat::variants()", + case_insensitive = "true" + ), + default_value = "EqualizerAPO" + )] + /// The file format of the equalizer configuration + pub format: EqualizerConfFormat, + } + + #[derive(StructOpt, Debug)] + pub struct ResetCli {} + +} + +pub mod pa_effects { + use super::EqualizerConfFormat; + + #[derive(StructOpt, Debug)] + pub enum Command { + #[structopt(name = "export-preset",)] + /// Export a PulseEffects preset + ExportPreset(ExportPresetCli), + } + + #[derive(StructOpt, Debug)] + pub struct ExportPresetCli { + #[structopt(default_value = "-")] + /// The file from which to load the equalizer configuration + /// + /// If "-" is given, read the configuration from the command-line. + pub file: String, + #[structopt( + short = "f", + raw( + possible_values = "&EqualizerConfFormat::variants()", + case_insensitive = "true" + ), + default_value = "EqualizerAPO" + )] + /// The file format of the equalizer configuration + pub format: EqualizerConfFormat, + #[structopt(short = "p")] + /// Use a given file as a base for PulseEffects preset instead of the + /// default one. + /// + /// If "-" is given, read the base preset from the command-line. + pub base_preset: Option, + #[structopt(short = "o")] + /// Write the preset to the given file, instead of the standard output + pub output: Option, + } + +} diff --git a/src/main.rs b/src/main.rs index 85d2443..a23034e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,21 +14,22 @@ extern crate structopt; extern crate lalrpop_util; +#[macro_use] +extern crate serde_json; + mod cli; mod dbus_api; mod parsing; mod utils; -use utils::*; -use dbus_api::sink::OrgPulseAudioExtEqualizing1Equalizer; +mod pa_eq; +mod pa_effects; + use cli::*; use failure::Error; use structopt::StructOpt; -use std::fs::File; -use std::io; - #[derive(Debug)] pub struct Filter { preamp: f64, @@ -74,41 +75,7 @@ fn start() -> Result<(), Error> { use Command::*; match args.cmd { - Load(args) => load(args), - Reset(args) => reset(args), + PaEq(args) => pa_eq::main(args), + PaEffects(args) => pa_effects::main(args), } } - -fn reset(args: ResetCli) -> Result<(), Error> { - let conn = connect()?; - let conn_sink = get_equalized_sink(&conn)?; - let filter_rate = conn_sink.get_filter_sample_rate()?; - let filter = Filter { - preamp: 1f64, - frequencies: vec![], - coefficients: vec![], - }.pad(filter_rate); - - send_filter(&conn_sink, filter)?; - - Ok(()) -} - -fn load(args: LoadCli) -> Result<(), Error> { - let conn = connect()?; - let conn_sink = get_equalized_sink(&conn)?; - - let filter = if args.file == "-" { - let stdin = io::stdin(); - let mut handle = stdin.lock(); - read_filter(&mut handle)? - } else { - let mut file = File::open(args.file)?; - read_filter(&mut file)? - }; - - let filter_rate = conn_sink.get_filter_sample_rate()?; - send_filter(&conn_sink, filter.pad(filter_rate))?; - - Ok(()) -} diff --git a/src/pa_effects.rs b/src/pa_effects.rs new file mode 100644 index 0000000..9ed0a1f --- /dev/null +++ b/src/pa_effects.rs @@ -0,0 +1,125 @@ +use cli::pa_effects::*; +use utils::*; +use Filter; + +use failure::Error; + +use serde_json; + +const DEFAULT_PRESET: &'static str = include_str!("../res/default-pa-effects-preset.json"); + +pub fn main(cmd: Command) -> Result<(), Error> { + use cli::pa_effects::Command::*; + + match cmd { + ExportPreset(args) => export_preset(args), + } +} + +fn export_preset(args: ExportPresetCli) -> Result<(), Error> { + debug!("Parsing base preset"); + let mut preset: serde_json::Value = match args.base_preset { + Some(file) => serde_json::from_str(&read_filearg_to_str(&file)?), + None => serde_json::from_str(&DEFAULT_PRESET), + }?; + + let filter = read_filter_from_arg(&args.file)?; + + preset["output"]["equalizer"] = filter_to_eq_preset(filter); + + println!("{}", preset); + Ok(()) +} + +fn filter_to_eq_preset(mut filter: Filter) -> serde_json::Value { + if filter.frequencies.len() > 30 { + info!("More than 30 frequencies specified, taking the approximative approach"); + filter = simplify_filter(filter); + } + + let mut equalizer: serde_json::Value = json!({ + "state": "true", + "num-bands": filter.frequencies.len(), + "input-gain": 0, + "output-gain": 0, + }); + + for (i, (frequency, coeff)) in filter + .frequencies + .into_iter() + .zip(filter.coefficients) + .enumerate() + { + equalizer[format!("band{}", i)] = json!({ + "gain": coeff, + "frequency": frequency, + "type": "peak", + }); + } + + equalizer +} + +fn simplify_filter(filter: Filter) -> Filter { + //let partition_size = (filter.frequencies.len() as f64 / 30f64).floor() as usize; + let mut partition_size = filter.frequencies.len() / 30; + let step_error = filter.frequencies.len() as f64 % 30f64; + if step_error != 0f64 { + info!("The approximation will be imperfect"); + partition_size += 1; + } + + //let mut cumulative_error = 0f64; + + let frequencies = filter.frequencies.chunks(partition_size).map(|vec| { + let sum: u32 = vec.iter().sum(); + sum / vec.len() as u32 + }).collect(); + + let coefficients = filter.coefficients.chunks(partition_size).map(|vec| { + let sum: f64 = vec.iter().sum(); + sum / vec.len() as f64 + }).collect(); + + Filter { + preamp: filter.preamp, + frequencies, coefficients + } +} + +/* +trait MultiPartitionnable { + type Item; + + fn multi_partition(self, size: usize, wanted_parts: usize) -> MultiPartition + where + Self: Iterator + Sized, + { + MultiPartition { + iter: self, + size, + wanted_parts, + cumulative_error: 0f64, + } + } +} + +impl MultiPartitionnable for Iterator { + type Item = I; +} + +struct MultiPartition { + iter: I, + size: usize, + wanted_parts: usize, + cumulative_error: f64, +} + +impl Iterator for MultiPartition { + type Item = Vec; + + fn next(&mut self) -> Option { + + } +} +*/ diff --git a/src/pa_eq.rs b/src/pa_eq.rs new file mode 100644 index 0000000..4086c31 --- /dev/null +++ b/src/pa_eq.rs @@ -0,0 +1,110 @@ +use Filter; +use cli::pa_eq::*; +use utils::*; + +use dbus_api::equalizing_manager::OrgPulseAudioExtEqualizing1Manager; +use dbus_api::server_lookup::OrgPulseAudioServerLookup1; +use dbus_api::sink::OrgPulseAudioExtEqualizing1Equalizer; + +use dbus::{BusType, ConnPath, Connection}; + +use failure::{Error, ResultExt}; + +#[derive(Fail, Debug)] +#[fail(display = "No equalized sink found")] +struct NoEqualizedSink; + +pub fn main(cmd: Command) -> Result<(), Error> { + use cli::pa_eq::Command::*; + + warn!("The PulseAudio equalizer has been deprecated for a while, and is known to sometimes cause crashes, latency or audible artifacts"); + + match cmd { + Load(args) => load(args), + Reset(args) => reset(args), + } +} + +pub fn reset(_args: ResetCli) -> Result<(), Error> { + let conn = connect()?; + let conn_sink = get_equalized_sink(&conn)?; + let filter_rate = conn_sink.get_filter_sample_rate()?; + let filter = Filter { + preamp: 1f64, + frequencies: vec![], + coefficients: vec![], + }.pad(filter_rate); + + send_filter(&conn_sink, filter)?; + + Ok(()) +} + +pub fn load(args: LoadCli) -> Result<(), Error> { + let conn = connect()?; + let conn_sink = get_equalized_sink(&conn)?; + + let filter = read_filter_from_arg(&args.file)?; + + let filter_rate = conn_sink.get_filter_sample_rate()?; + send_filter(&conn_sink, filter.pad(filter_rate))?; + + Ok(()) +} + +fn connect() -> Result { + Ok(connect_impl().context( + "Could not connect to PulseAudio's D-Bus socket. Have you loaded the 'module-dbus-protocol' module?" + )?) +} + +fn connect_impl() -> Result { + let pulse_sock_path = get_pulse_dbus_sock()?; + info!("PulseAudio's D-Bus socket path is: {}", pulse_sock_path); + + trace!("Connecting to PulseAudio's D-Bus socket"); + Ok(Connection::open_private(&pulse_sock_path)?) +} + +fn get_equalized_sink<'a>(conn: &'a Connection) -> Result, Error> { + Ok(get_equalized_sink_impl(conn).context( + "Could not find an equalized sink. Have you loaded the 'module-equalizer-sink' module?", + )?) +} + +fn get_equalized_sink_impl<'a>( + conn: &'a Connection, +) -> Result, Error> { + let conn_manager = conn.with_path("org.PulseAudio.Core1", "/org/pulseaudio/equalizing1", 2000); + + // TODO: make that a command-line option + trace!("Getting (one of) the equalized sink(s)"); + let mut sinks = conn_manager.get_equalized_sinks()?; + let sink_path = sinks.pop().ok_or(NoEqualizedSink {})?; + info!("Using equalized sink: {:?}", sink_path.as_cstr()); + + trace!("Connecting to equalized sink"); + Ok(conn.with_path("org.PulseAudio.Core1", sink_path, 2000)) +} + +fn send_filter(conn_sink: &ConnPath<&Connection>, filter: Filter) -> Result<(), Error> { + let channel = conn_sink.get_nchannels()?; + info!("Using channel: {}", channel); + trace!("Sending filter: {:?}", filter); + conn_sink.seed_filter( + channel, + filter.frequencies, + filter.coefficients.into_iter().map(decibel_to_ratio).collect(), + decibel_to_ratio(filter.preamp), + )?; + Ok(()) +} + +fn get_pulse_dbus_sock() -> Result { + trace!("Connecting to the D-Bus' session bus"); + let conn = Connection::get_private(BusType::Session)?; + let conn = conn.with_path("org.PulseAudio1", "/org/pulseaudio/server_lookup1", 2000); + + trace!("Checking PulseAudio's D-Bus socket path"); + Ok(conn.get_address()?) +} diff --git a/src/parsing/equalizer_apo.lalrpop b/src/parsing/equalizer_apo.lalrpop index 39dda67..752aee4 100644 --- a/src/parsing/equalizer_apo.lalrpop +++ b/src/parsing/equalizer_apo.lalrpop @@ -5,12 +5,8 @@ use std::str::FromStr; grammar; pub Main: Filter = { - => { - let coefficients: Vec<_> = eq.1.iter().map(|decibel| 10f64.powf(decibel / 10f64).sqrt()).collect(); - // TODO: add decibel_to_ratio conversion function - let preamp = 10f64.powf(preamp / 10f64); - Filter { preamp, frequencies: eq.0, coefficients } - } + => Filter { preamp, frequencies: eq.0, coefficients: eq.1 } + } Preamp: f64 = { diff --git a/src/parsing/equalizer_apo.rs b/src/parsing/equalizer_apo.rs index a24cf99..b0aa8f8 100644 --- a/src/parsing/equalizer_apo.rs +++ b/src/parsing/equalizer_apo.rs @@ -1,5 +1,5 @@ // auto-generated: "lalrpop 0.15.2" -// sha256: a735e8f9bd5cf5f3aac915d12b24752d366481c3952258e22871eef395f44 +// sha256: 3981cad2c0ee5c1d80c3b7278bb1bc9926ee7b273c2b68423accdd84e43c494 use ::Filter; use std::str::FromStr; #[allow(unused_extern_crates)] @@ -879,12 +879,7 @@ fn __action1< (_, eq, _): (usize, (Vec, Vec), usize), ) -> Filter { - { - let coefficients: Vec<_> = eq.1.iter().map(|decibel| 10f64.powf(decibel / 10f64).sqrt()).collect(); - // TODO: add decibel_to_ratio conversion function - let preamp = 10f64.powf(preamp / 10f64); - Filter { preamp, frequencies: eq.0, coefficients } - } + Filter { preamp, frequencies: eq.0, coefficients: eq.1 } } #[allow(unused_variables)] diff --git a/src/utils.rs b/src/utils.rs index de9ed3c..d75971a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,22 +1,15 @@ -use {EqualizerConfFormat, Filter}; +use Filter; +use cli::EqualizerConfFormat; use parsing::EqualizerApoParser; -use dbus_api::equalizing_manager::OrgPulseAudioExtEqualizing1Manager; -use dbus_api::server_lookup::OrgPulseAudioServerLookup1; -use dbus_api::sink::OrgPulseAudioExtEqualizing1Equalizer; - -use dbus::{BusType, ConnPath, Connection}; -use failure::{Error, ResultExt}; +use failure::Error; use lalrpop_util; use std::fmt; +use std::fs::File; use std::io; -#[derive(Fail, Debug)] -#[fail(display = "No equalized sink found")] -struct NoEqualizedSink; - #[derive(Fail, Debug)] #[fail( display = "Could not parse using the {} format: {}", @@ -28,66 +21,32 @@ struct ParseError { message: String, } -pub fn connect() -> Result { - Ok(connect_impl().context( - "Could not connect to PulseAudio's D-Bus socket. Have you loaded the 'module-dbus-protocol' module?" - )?) -} - -fn connect_impl() -> Result { - let pulse_sock_path = get_pulse_dbus_sock()?; - info!("PulseAudio's D-Bus socket path is: {}", pulse_sock_path); - - trace!("Connecting to PulseAudio's D-Bus socket"); - Ok(Connection::open_private(&pulse_sock_path)?) -} - -pub fn get_equalized_sink<'a>(conn: &'a Connection) -> Result, Error> { - Ok(get_equalized_sink_impl(conn).context( - "Could not find an equalized sink. Have you loaded the 'module-equalizer-sink' module?", - )?) -} - -fn get_equalized_sink_impl<'a>( - conn: &'a Connection, -) -> Result, Error> { - let conn_manager = conn.with_path("org.PulseAudio.Core1", "/org/pulseaudio/equalizing1", 2000); +pub fn read_filearg_to_str(file: &str) -> Result { + use std::io::Read; - // TODO: make that a command-line option - trace!("Getting (one of) the equalized sink(s)"); - let mut sinks = conn_manager.get_equalized_sinks()?; - let sink_path = sinks.pop().ok_or(NoEqualizedSink {})?; - info!("Using equalized sink: {:?}", sink_path.as_cstr()); - - trace!("Connecting to equalized sink"); - Ok(conn.with_path("org.PulseAudio.Core1", sink_path, 2000)) + let mut buffer = String::new(); + if file == "-" { + info!("Reading file from the command line"); + let stdin = io::stdin(); + let mut handle = stdin.lock(); + handle.read_to_string(&mut buffer)?; + } else { + let mut file = File::open(file)?; + file.read_to_string(&mut buffer)?; + } + Ok(buffer) } -pub fn send_filter(conn_sink: &ConnPath<&Connection>, filter: Filter) -> Result<(), Error> { - let channel = conn_sink.get_nchannels()?; - info!("Using channel: {}", channel); - trace!("Sending filter: {:?}", filter); - conn_sink.seed_filter( - channel, - filter.frequencies, - filter.coefficients, - filter.preamp, - )?; - Ok(()) +pub fn read_filter_from_arg(file: &str) -> Result { + debug!("Reading filter from '{}' in the EqualizerAPO format", file); + let content = read_filearg_to_str(file)?; + parse_filter(&content) } -pub fn read_filter(file: &mut T) -> Result -where - T: io::Read, -{ - let mut buffer = String::new(); - - info!("Reading filter in GraphicEQ format from the command line"); - file.read_to_string(&mut buffer)?; - +pub fn parse_filter(content: &str) -> Result { // TODO: lifetime issue when "throwing" parse error let filter = EqualizerApoParser::new() - .parse(&buffer) + .parse(&content) .map_err(|e| convert_parse_error(EqualizerConfFormat::EqualizerAPO, e))?; trace!("Parsed filter: {:?}", filter); @@ -109,13 +68,8 @@ where } } -fn get_pulse_dbus_sock() -> Result { - trace!("Connecting to the D-Bus' session bus"); - let conn = Connection::get_private(BusType::Session)?; - let conn = conn.with_path("org.PulseAudio1", "/org/pulseaudio/server_lookup1", 2000); - - trace!("Checking PulseAudio's D-Bus socket path"); - Ok(conn.get_address()?) +pub fn decibel_to_ratio(decibel: f64) -> f64 { + 10f64.powf(decibel / 10f64).sqrt() } /* -- cgit v1.2.3