use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::dsp::Band;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PresetBackup {
pub source: String,
pub dest: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum Request {
Status,
Enable,
Disable,
ListPresets,
SetPreset(String),
SavePreset {
name: String,
},
ClonePreset {
source: String,
dest: String,
},
DeletePresets {
names: Vec<String>,
},
RenamePreset {
from: String,
to: String,
},
ExportPreset {
name: String,
path: PathBuf,
},
ImportPreset {
path: PathBuf,
name: Option<String>,
},
SetBand {
freq: f32,
gain_db: f32,
q: f32,
},
RemoveBand {
freq: f32,
},
SetPreamp(f32),
SetAutoOffLowPower(bool),
SetAutoOffIdle(bool),
SaveSessionAs {
name: String,
},
SaveSessionOverwrite,
DiscardSession,
ResetPreset {
name: String,
},
ConfirmResetPreset {
name: String,
backups: Vec<PresetBackup>,
},
Reset,
ConfirmReset {
backups: Vec<PresetBackup>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum Response {
Ok,
Status(Status),
Tuning(Tuning),
Presets {
active: String,
names: Vec<String>,
},
ResetWouldOverwrite {
names: Vec<String>,
},
UnsavedSession(Tuning),
Error(String),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Tuning {
pub enabled: bool,
pub preset: String,
pub preamp_db: f32,
pub bands: Vec<Band>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Status {
pub enabled: bool,
pub active_preset: String,
pub preamp_db: f32,
pub band_count: usize,
pub limiter: bool,
pub output_device: Option<String>,
pub low_power: bool,
pub auto_off_low_power: bool,
pub auto_off_idle: bool,
pub idle_suspended: bool,
}
pub fn socket_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_default();
PathBuf::from(home).join("Library/Application Support/eqtune/eqtune.sock")
}
pub fn send(req: &Request) -> anyhow::Result<Response> {
let path = socket_path();
let mut stream = UnixStream::connect(&path).map_err(|e| {
anyhow::anyhow!(
"could not reach the eqtune daemon ({e}). Is it running? Try `eqtune install` then `eqtune on`."
)
})?;
let mut line = serde_json::to_string(req)?;
line.push('\n');
stream.write_all(line.as_bytes())?;
stream.flush()?;
let mut reader = BufReader::new(stream);
let mut resp = String::new();
reader.read_line(&mut resp)?;
Ok(serde_json::from_str(resp.trim_end())?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips() {
let reqs = [
Request::Status,
Request::Enable,
Request::Disable,
Request::Reset,
Request::ListPresets,
Request::SetPreset("flat".into()),
Request::SavePreset { name: "car".into() },
Request::ClonePreset {
source: "bright".into(),
dest: "desk".into(),
},
Request::DeletePresets {
names: vec!["desk".into(), "car".into()],
},
Request::RenamePreset {
from: "car".into(),
to: "car-v2".into(),
},
Request::ExportPreset {
name: "car-v2".into(),
path: PathBuf::from("/tmp/car-v2.toml"),
},
Request::ImportPreset {
path: PathBuf::from("/tmp/car-v2.toml"),
name: Some("shared-car".into()),
},
Request::SetBand {
freq: 1000.0,
gain_db: -10.0,
q: 1.0,
},
Request::RemoveBand { freq: 2000.0 },
Request::SetPreamp(7.0),
Request::SetAutoOffLowPower(false),
Request::SetAutoOffIdle(false),
Request::SaveSessionAs {
name: "daily".into(),
},
Request::SaveSessionOverwrite,
Request::DiscardSession,
Request::ResetPreset {
name: "bright".into(),
},
Request::ConfirmResetPreset {
name: "bright".into(),
backups: vec![PresetBackup {
source: "bright".into(),
dest: "my-bright".into(),
}],
},
Request::ConfirmReset {
backups: vec![PresetBackup {
source: "mellow".into(),
dest: "my-mellow".into(),
}],
},
];
for r in reqs {
let s = serde_json::to_string(&r).unwrap();
assert_eq!(serde_json::from_str::<Request>(&s).unwrap(), r);
}
}
#[test]
fn response_round_trips() {
let st = Status {
enabled: true,
active_preset: "default".into(),
preamp_db: 7.0,
band_count: 3,
limiter: true,
output_device: Some("MacBook Pro Speakers".into()),
low_power: false,
auto_off_low_power: true,
auto_off_idle: true,
idle_suspended: false,
};
let resps = [
Response::Ok,
Response::Status(st),
Response::Tuning(Tuning {
enabled: true,
preset: "bright".into(),
preamp_db: -8.0,
bands: vec![
crate::dsp::Band {
kind: crate::dsp::BandKind::Peaking,
freq: 1000.0,
gain_db: 4.5,
q: 1.41,
},
crate::dsp::Band {
kind: crate::dsp::BandKind::Peaking,
freq: 8000.0,
gain_db: 9.5,
q: 1.41,
},
],
}),
Response::Presets {
active: "default".into(),
names: vec!["default".into(), "flat".into()],
},
Response::ResetWouldOverwrite {
names: vec!["bright".into()],
},
Response::UnsavedSession(Tuning {
enabled: false,
preset: "bright".into(),
preamp_db: -8.0,
bands: vec![],
}),
Response::Error("nope".into()),
];
for r in resps {
let s = serde_json::to_string(&r).unwrap();
assert_eq!(serde_json::from_str::<Response>(&s).unwrap(), r);
}
}
}