summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMinijackson <minijackson@riseup.net>2018-10-07 23:40:35 +0200
committerMinijackson <minijackson@riseup.net>2018-10-07 23:40:35 +0200
commita6dfec305a53c6a7618b98f43a6b4b89cfb76102 (patch)
treef1ee180a53c47e31eaeb19bc3d749965023b6448 /src
parent600df820b205a0ab81294e795ae691158553ffb9 (diff)
downloadset_eq-a6dfec305a53c6a7618b98f43a6b4b89cfb76102.tar.gz
set_eq-a6dfec305a53c6a7618b98f43a6b4b89cfb76102.zip
Add support for PulseEffects
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs113
-rw-r--r--src/main.rs49
-rw-r--r--src/pa_effects.rs125
-rw-r--r--src/pa_eq.rs110
-rw-r--r--src/parsing/equalizer_apo.lalrpop8
-rw-r--r--src/parsing/equalizer_apo.rs9
-rw-r--r--src/utils.rs96
7 files changed, 358 insertions, 152 deletions
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 {
21 21
22#[derive(StructOpt, Debug)] 22#[derive(StructOpt, Debug)]
23pub enum Command { 23pub enum Command {
24 #[structopt(name = "load",)] 24 #[structopt(name = "pa-eq")]
25 /// Load and switch to a given equalizer configuration 25 /// PulseAudio equalizer related commands
26 Load(LoadCli), 26 ///
27 #[structopt(name = "reset")] 27 /// Warning: the PulseAudio equalizer has been deprecated for a while,
28 /// Switch to a neutral equalizer 28 /// and is known to sometimes cause crashes, latency or audible
29 Reset(ResetCli), 29 /// artifacts
30} 30 PaEq(pa_eq::Command),
31 31 #[structopt(name = "pa-effects")]
32#[derive(StructOpt, Debug)] 32 /// PulseEffects equalizer related commands
33pub struct LoadCli { 33 PaEffects(pa_effects::Command),
34 #[structopt(default_value = "-")]
35 /// The file from which to load the equalizer configuration
36 ///
37 /// If "-" is given, read the configuration from the command-line.
38 pub file: String,
39 #[structopt(
40 short = "f",
41 raw(
42 possible_values = "&EqualizerConfFormat::variants()",
43 case_insensitive = "true"
44 ),
45 default_value = "EqualizerAPO"
46 )]
47 /// The file format of the equalizer configuration
48 pub format: EqualizerConfFormat,
49} 34}
50 35
51arg_enum! { 36arg_enum! {
@@ -55,5 +40,79 @@ arg_enum! {
55 } 40 }
56} 41}
57 42
58#[derive(StructOpt, Debug)] 43pub mod pa_eq {
59pub struct ResetCli {} 44 use super::EqualizerConfFormat;
45
46 #[derive(StructOpt, Debug)]
47 pub enum Command {
48 #[structopt(name = "load",)]
49 /// Load and switch to a given equalizer configuration
50 Load(LoadCli),
51 #[structopt(name = "reset")]
52 /// Switch to a neutral equalizer
53 Reset(ResetCli),
54 }
55
56 #[derive(StructOpt, Debug)]
57 pub struct LoadCli {
58 #[structopt(default_value = "-")]
59 /// The file from which to load the equalizer configuration
60 ///
61 /// If "-" is given, read the configuration from the command-line.
62 pub file: String,
63 #[structopt(
64 short = "f",
65 raw(
66 possible_values = "&EqualizerConfFormat::variants()",
67 case_insensitive = "true"
68 ),
69 default_value = "EqualizerAPO"
70 )]
71 /// The file format of the equalizer configuration
72 pub format: EqualizerConfFormat,
73 }
74
75 #[derive(StructOpt, Debug)]
76 pub struct ResetCli {}
77
78}
79
80pub mod pa_effects {
81 use super::EqualizerConfFormat;
82
83 #[derive(StructOpt, Debug)]
84 pub enum Command {
85 #[structopt(name = "export-preset",)]
86 /// Export a PulseEffects preset
87 ExportPreset(ExportPresetCli),
88 }
89
90 #[derive(StructOpt, Debug)]
91 pub struct ExportPresetCli {
92 #[structopt(default_value = "-")]
93 /// The file from which to load the equalizer configuration
94 ///
95 /// If "-" is given, read the configuration from the command-line.
96 pub file: String,
97 #[structopt(
98 short = "f",
99 raw(
100 possible_values = "&EqualizerConfFormat::variants()",
101 case_insensitive = "true"
102 ),
103 default_value = "EqualizerAPO"
104 )]
105 /// The file format of the equalizer configuration
106 pub format: EqualizerConfFormat,
107 #[structopt(short = "p")]
108 /// Use a given file as a base for PulseEffects preset instead of the
109 /// default one.
110 ///
111 /// If "-" is given, read the base preset from the command-line.
112 pub base_preset: Option<String>,
113 #[structopt(short = "o")]
114 /// Write the preset to the given file, instead of the standard output
115 pub output: Option<String>,
116 }
117
118}
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;
14 14
15extern crate lalrpop_util; 15extern crate lalrpop_util;
16 16
17#[macro_use]
18extern crate serde_json;
19
17mod cli; 20mod cli;
18mod dbus_api; 21mod dbus_api;
19mod parsing; 22mod parsing;
20mod utils; 23mod utils;
21 24
22use utils::*; 25mod pa_eq;
23use dbus_api::sink::OrgPulseAudioExtEqualizing1Equalizer; 26mod pa_effects;
27
24use cli::*; 28use cli::*;
25 29
26use failure::Error; 30use failure::Error;
27use structopt::StructOpt; 31use structopt::StructOpt;
28 32
29use std::fs::File;
30use std::io;
31
32#[derive(Debug)] 33#[derive(Debug)]
33pub struct Filter { 34pub struct Filter {
34 preamp: f64, 35 preamp: f64,
@@ -74,41 +75,7 @@ fn start() -> Result<(), Error> {
74 use Command::*; 75 use Command::*;
75 76
76 match args.cmd { 77 match args.cmd {
77 Load(args) => load(args), 78 PaEq(args) => pa_eq::main(args),
78 Reset(args) => reset(args), 79 PaEffects(args) => pa_effects::main(args),
79 } 80 }
80} 81}
81
82fn reset(args: ResetCli) -> Result<(), Error> {
83 let conn = connect()?;
84 let conn_sink = get_equalized_sink(&conn)?;
85 let filter_rate = conn_sink.get_filter_sample_rate()?;
86 let filter = Filter {
87 preamp: 1f64,
88 frequencies: vec![],
89 coefficients: vec![],
90 }.pad(filter_rate);
91
92 send_filter(&conn_sink, filter)?;
93
94 Ok(())
95}
96
97fn load(args: LoadCli) -> Result<(), Error> {
98 let conn = connect()?;
99 let conn_sink = get_equalized_sink(&conn)?;
100
101 let filter = if args.file == "-" {
102 let stdin = io::stdin();
103 let mut handle = stdin.lock();
104 read_filter(&mut handle)?
105 } else {
106 let mut file = File::open(args.file)?;
107 read_filter(&mut file)?
108 };
109
110 let filter_rate = conn_sink.get_filter_sample_rate()?;
111 send_filter(&conn_sink, filter.pad(filter_rate))?;
112
113 Ok(())
114}
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 @@
1use cli::pa_effects::*;
2use utils::*;
3use Filter;
4
5use failure::Error;
6
7use serde_json;
8
9const DEFAULT_PRESET: &'static str = include_str!("../res/default-pa-effects-preset.json");
10
11pub fn main(cmd: Command) -> Result<(), Error> {
12 use cli::pa_effects::Command::*;
13
14 match cmd {
15 ExportPreset(args) => export_preset(args),
16 }
17}
18
19fn export_preset(args: ExportPresetCli) -> Result<(), Error> {
20 debug!("Parsing base preset");
21 let mut preset: serde_json::Value = match args.base_preset {
22 Some(file) => serde_json::from_str(&read_filearg_to_str(&file)?),
23 None => serde_json::from_str(&DEFAULT_PRESET),
24 }?;
25
26 let filter = read_filter_from_arg(&args.file)?;
27
28 preset["output"]["equalizer"] = filter_to_eq_preset(filter);
29
30 println!("{}", preset);
31 Ok(())
32}
33
34fn filter_to_eq_preset(mut filter: Filter) -> serde_json::Value {
35 if filter.frequencies.len() > 30 {
36 info!("More than 30 frequencies specified, taking the approximative approach");
37 filter = simplify_filter(filter);
38 }
39
40 let mut equalizer: serde_json::Value = json!({
41 "state": "true",
42 "num-bands": filter.frequencies.len(),
43 "input-gain": 0,
44 "output-gain": 0,
45 });
46
47 for (i, (frequency, coeff)) in filter
48 .frequencies
49 .into_iter()
50 .zip(filter.coefficients)
51 .enumerate()
52 {
53 equalizer[format!("band{}", i)] = json!({
54 "gain": coeff,
55 "frequency": frequency,
56 "type": "peak",
57 });
58 }
59
60 equalizer
61}
62
63fn simplify_filter(filter: Filter) -> Filter {
64 //let partition_size = (filter.frequencies.len() as f64 / 30f64).floor() as usize;
65 let mut partition_size = filter.frequencies.len() / 30;
66 let step_error = filter.frequencies.len() as f64 % 30f64;
67 if step_error != 0f64 {
68 info!("The approximation will be imperfect");
69 partition_size += 1;
70 }
71
72 //let mut cumulative_error = 0f64;
73
74 let frequencies = filter.frequencies.chunks(partition_size).map(|vec| {
75 let sum: u32 = vec.iter().sum();
76 sum / vec.len() as u32
77 }).collect();
78
79 let coefficients = filter.coefficients.chunks(partition_size).map(|vec| {
80 let sum: f64 = vec.iter().sum();
81 sum / vec.len() as f64
82 }).collect();
83
84 Filter {
85 preamp: filter.preamp,
86 frequencies, coefficients
87 }
88}
89
90/*
91trait MultiPartitionnable {
92 type Item;
93
94 fn multi_partition(self, size: usize, wanted_parts: usize) -> MultiPartition<Self>
95 where
96 Self: Iterator + Sized,
97 {
98 MultiPartition {
99 iter: self,
100 size,
101 wanted_parts,
102 cumulative_error: 0f64,
103 }
104 }
105}
106
107impl<I> MultiPartitionnable for Iterator<Item = I> {
108 type Item = I;
109}
110
111struct MultiPartition<I: Iterator> {
112 iter: I,
113 size: usize,
114 wanted_parts: usize,
115 cumulative_error: f64,
116}
117
118impl<I: Iterator> Iterator for MultiPartition<I> {
119 type Item = Vec<I::Item>;
120
121 fn next(&mut self) -> Option<Self::Item> {
122
123 }
124}
125*/
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 @@
1use Filter;
2use cli::pa_eq::*;
3use utils::*;
4
5use dbus_api::equalizing_manager::OrgPulseAudioExtEqualizing1Manager;
6use dbus_api::server_lookup::OrgPulseAudioServerLookup1;
7use dbus_api::sink::OrgPulseAudioExtEqualizing1Equalizer;
8
9use dbus::{BusType, ConnPath, Connection};
10
11use failure::{Error, ResultExt};
12
13#[derive(Fail, Debug)]
14#[fail(display = "No equalized sink found")]
15struct NoEqualizedSink;
16
17pub fn main(cmd: Command) -> Result<(), Error> {
18 use cli::pa_eq::Command::*;
19
20 warn!("The PulseAudio equalizer has been deprecated for a while, and is known to sometimes cause crashes, latency or audible artifacts");
21
22 match cmd {
23 Load(args) => load(args),
24 Reset(args) => reset(args),
25 }
26}
27
28pub fn reset(_args: ResetCli) -> Result<(), Error> {
29 let conn = connect()?;
30 let conn_sink = get_equalized_sink(&conn)?;
31 let filter_rate = conn_sink.get_filter_sample_rate()?;
32 let filter = Filter {
33 preamp: 1f64,
34 frequencies: vec![],
35 coefficients: vec![],
36 }.pad(filter_rate);
37
38 send_filter(&conn_sink, filter)?;
39
40 Ok(())
41}
42
43pub fn load(args: LoadCli) -> Result<(), Error> {
44 let conn = connect()?;
45 let conn_sink = get_equalized_sink(&conn)?;
46
47 let filter = read_filter_from_arg(&args.file)?;
48
49 let filter_rate = conn_sink.get_filter_sample_rate()?;
50 send_filter(&conn_sink, filter.pad(filter_rate))?;
51
52 Ok(())
53}
54
55fn connect() -> Result<Connection, Error> {
56 Ok(connect_impl().context(
57 "Could not connect to PulseAudio's D-Bus socket. Have you loaded the 'module-dbus-protocol' module?"
58 )?)
59}
60
61fn connect_impl() -> Result<Connection, Error> {
62 let pulse_sock_path = get_pulse_dbus_sock()?;
63 info!("PulseAudio's D-Bus socket path is: {}", pulse_sock_path);
64
65 trace!("Connecting to PulseAudio's D-Bus socket");
66 Ok(Connection::open_private(&pulse_sock_path)?)
67}
68
69fn get_equalized_sink<'a>(conn: &'a Connection) -> Result<ConnPath<'a, &'a Connection>, Error> {
70 Ok(get_equalized_sink_impl(conn).context(
71 "Could not find an equalized sink. Have you loaded the 'module-equalizer-sink' module?",
72 )?)
73}
74
75fn get_equalized_sink_impl<'a>(
76 conn: &'a Connection,
77) -> Result<ConnPath<'a, &'a Connection>, Error> {
78 let conn_manager = conn.with_path("org.PulseAudio.Core1", "/org/pulseaudio/equalizing1", 2000);
79
80 // TODO: make that a command-line option
81 trace!("Getting (one of) the equalized sink(s)");
82 let mut sinks = conn_manager.get_equalized_sinks()?;
83 let sink_path = sinks.pop().ok_or(NoEqualizedSink {})?;
84 info!("Using equalized sink: {:?}", sink_path.as_cstr());
85
86 trace!("Connecting to equalized sink");
87 Ok(conn.with_path("org.PulseAudio.Core1", sink_path, 2000))
88}
89
90fn send_filter(conn_sink: &ConnPath<&Connection>, filter: Filter) -> Result<(), Error> {
91 let channel = conn_sink.get_nchannels()?;
92 info!("Using channel: {}", channel);
93 trace!("Sending filter: {:?}", filter);
94 conn_sink.seed_filter(
95 channel,
96 filter.frequencies,
97 filter.coefficients.into_iter().map(decibel_to_ratio).collect(),
98 decibel_to_ratio(filter.preamp),
99 )?;
100 Ok(())
101}
102
103fn get_pulse_dbus_sock() -> Result<String, Error> {
104 trace!("Connecting to the D-Bus' session bus");
105 let conn = Connection::get_private(BusType::Session)?;
106 let conn = conn.with_path("org.PulseAudio1", "/org/pulseaudio/server_lookup1", 2000);
107
108 trace!("Checking PulseAudio's D-Bus socket path");
109 Ok(conn.get_address()?)
110}
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;
5grammar; 5grammar;
6 6
7pub Main: Filter = { 7pub Main: Filter = {
8 <preamp: Preamp> <eq: Eq> => { 8 <preamp: Preamp> <eq: Eq> => Filter { preamp, frequencies: eq.0, coefficients: eq.1 }
9 let coefficients: Vec<_> = eq.1.iter().map(|decibel| 10f64.powf(decibel / 10f64).sqrt()).collect(); 9
10 // TODO: add decibel_to_ratio conversion function
11 let preamp = 10f64.powf(preamp / 10f64);
12 Filter { preamp, frequencies: eq.0, coefficients }
13 }
14} 10}
15 11
16Preamp: f64 = { 12Preamp: 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 @@
1// auto-generated: "lalrpop 0.15.2" 1// auto-generated: "lalrpop 0.15.2"
2// sha256: a735e8f9bd5cf5f3aac915d12b24752d366481c3952258e22871eef395f44 2// sha256: 3981cad2c0ee5c1d80c3b7278bb1bc9926ee7b273c2b68423accdd84e43c494
3use ::Filter; 3use ::Filter;
4use std::str::FromStr; 4use std::str::FromStr;
5#[allow(unused_extern_crates)] 5#[allow(unused_extern_crates)]
@@ -879,12 +879,7 @@ fn __action1<
879 (_, eq, _): (usize, (Vec<u32>, Vec<f64>), usize), 879 (_, eq, _): (usize, (Vec<u32>, Vec<f64>), usize),
880) -> Filter 880) -> Filter
881{ 881{
882 { 882 Filter { preamp, frequencies: eq.0, coefficients: eq.1 }
883 let coefficients: Vec<_> = eq.1.iter().map(|decibel| 10f64.powf(decibel / 10f64).sqrt()).collect();
884 // TODO: add decibel_to_ratio conversion function
885 let preamp = 10f64.powf(preamp / 10f64);
886 Filter { preamp, frequencies: eq.0, coefficients }
887 }
888} 883}
889 884
890#[allow(unused_variables)] 885#[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,23 +1,16 @@
1use {EqualizerConfFormat, Filter}; 1use Filter;
2 2
3use cli::EqualizerConfFormat;
3use parsing::EqualizerApoParser; 4use parsing::EqualizerApoParser;
4 5
5use dbus_api::equalizing_manager::OrgPulseAudioExtEqualizing1Manager; 6use failure::Error;
6use dbus_api::server_lookup::OrgPulseAudioServerLookup1;
7use dbus_api::sink::OrgPulseAudioExtEqualizing1Equalizer;
8
9use dbus::{BusType, ConnPath, Connection};
10use failure::{Error, ResultExt};
11use lalrpop_util; 7use lalrpop_util;
12 8
13use std::fmt; 9use std::fmt;
10use std::fs::File;
14use std::io; 11use std::io;
15 12
16#[derive(Fail, Debug)] 13#[derive(Fail, Debug)]
17#[fail(display = "No equalized sink found")]
18struct NoEqualizedSink;
19
20#[derive(Fail, Debug)]
21#[fail( 14#[fail(
22 display = "Could not parse using the {} format: {}", 15 display = "Could not parse using the {} format: {}",
23 format, 16 format,
@@ -28,66 +21,32 @@ struct ParseError {
28 message: String, 21 message: String,
29} 22}
30 23
31pub fn connect() -> Result<Connection, Error> { 24pub fn read_filearg_to_str(file: &str) -> Result<String, Error> {
32 Ok(connect_impl().context( 25 use std::io::Read;
33 "Could not connect to PulseAudio's D-Bus socket. Have you loaded the 'module-dbus-protocol' module?"
34 )?)
35}
36
37fn connect_impl() -> Result<Connection, Error> {
38 let pulse_sock_path = get_pulse_dbus_sock()?;
39 info!("PulseAudio's D-Bus socket path is: {}", pulse_sock_path);
40
41 trace!("Connecting to PulseAudio's D-Bus socket");
42 Ok(Connection::open_private(&pulse_sock_path)?)
43}
44
45pub fn get_equalized_sink<'a>(conn: &'a Connection) -> Result<ConnPath<'a, &'a Connection>, Error> {
46 Ok(get_equalized_sink_impl(conn).context(
47 "Could not find an equalized sink. Have you loaded the 'module-equalizer-sink' module?",
48 )?)
49}
50
51fn get_equalized_sink_impl<'a>(
52 conn: &'a Connection,
53) -> Result<ConnPath<'a, &'a Connection>, Error> {
54 let conn_manager = conn.with_path("org.PulseAudio.Core1", "/org/pulseaudio/equalizing1", 2000);
55 26
56 // TODO: make that a command-line option 27 let mut buffer = String::new();
57 trace!("Getting (one of) the equalized sink(s)"); 28 if file == "-" {
58 let mut sinks = conn_manager.get_equalized_sinks()?; 29 info!("Reading file from the command line");
59 let sink_path = sinks.pop().ok_or(NoEqualizedSink {})?; 30 let stdin = io::stdin();
60 info!("Using equalized sink: {:?}", sink_path.as_cstr()); 31 let mut handle = stdin.lock();
61 32 handle.read_to_string(&mut buffer)?;
62 trace!("Connecting to equalized sink"); 33 } else {
63 Ok(conn.with_path("org.PulseAudio.Core1", sink_path, 2000)) 34 let mut file = File::open(file)?;
35 file.read_to_string(&mut buffer)?;
36 }
37 Ok(buffer)
64} 38}
65 39
66pub fn send_filter(conn_sink: &ConnPath<&Connection>, filter: Filter) -> Result<(), Error> { 40pub fn read_filter_from_arg(file: &str) -> Result<Filter, Error> {
67 let channel = conn_sink.get_nchannels()?; 41 debug!("Reading filter from '{}' in the EqualizerAPO format", file);
68 info!("Using channel: {}", channel); 42 let content = read_filearg_to_str(file)?;
69 trace!("Sending filter: {:?}", filter); 43 parse_filter(&content)
70 conn_sink.seed_filter(
71 channel,
72 filter.frequencies,
73 filter.coefficients,
74 filter.preamp,
75 )?;
76 Ok(())
77} 44}
78 45
79pub fn read_filter<T>(file: &mut T) -> Result<Filter, Error> 46pub fn parse_filter(content: &str) -> Result<Filter, Error> {
80where
81 T: io::Read,
82{
83 let mut buffer = String::new();
84
85 info!("Reading filter in GraphicEQ format from the command line");
86 file.read_to_string(&mut buffer)?;
87
88 // TODO: lifetime issue when "throwing" parse error 47 // TODO: lifetime issue when "throwing" parse error
89 let filter = EqualizerApoParser::new() 48 let filter = EqualizerApoParser::new()
90 .parse(&buffer) 49 .parse(&content)
91 .map_err(|e| convert_parse_error(EqualizerConfFormat::EqualizerAPO, e))?; 50 .map_err(|e| convert_parse_error(EqualizerConfFormat::EqualizerAPO, e))?;
92 trace!("Parsed filter: {:?}", filter); 51 trace!("Parsed filter: {:?}", filter);
93 52
@@ -109,13 +68,8 @@ where
109 } 68 }
110} 69}
111 70
112fn get_pulse_dbus_sock() -> Result<String, Error> { 71pub fn decibel_to_ratio(decibel: f64) -> f64 {
113 trace!("Connecting to the D-Bus' session bus"); 72 10f64.powf(decibel / 10f64).sqrt()
114 let conn = Connection::get_private(BusType::Session)?;
115 let conn = conn.with_path("org.PulseAudio1", "/org/pulseaudio/server_lookup1", 2000);
116
117 trace!("Checking PulseAudio's D-Bus socket path");
118 Ok(conn.get_address()?)
119} 73}
120 74
121/* 75/*