#![allow(clippy::upper_case_acronyms)]
use std::{
fmt,
fs::File,
io::{BufRead, BufReader},
num::ParseIntError,
path::PathBuf,
str::FromStr,
};
use anyhow::{anyhow, Result};
use bytes::Bytes;
use clap::Parser;
use debug::DebugCommands;
use futures::{stream, StreamExt};
use handlers::run_server;
use minidsp::{
builder::{Builder, DeviceHandle},
device::DeviceKind,
tcp_server,
transport::net::{self, discovery},
Gain, MiniDSP, MiniDSPError, Source,
};
mod debug;
mod handlers;
use std::{io::Read, time::Duration};
#[cfg(feature = "hid")]
use minidsp::transport::hid;
#[derive(Clone, Parser, Debug)]
#[clap(version=env!("CARGO_PKG_VERSION"), author=env!("CARGO_PKG_AUTHORS"))]
struct Opts {
#[clap(short, long, parse(from_occurrences))]
verbose: i32,
#[clap(long = "output", short = 'o', default_value = "text")]
output_format: OutputFormat,
#[clap(long, env = "MINIDSP_LOG")]
log: Option<PathBuf>,
#[clap(long)]
all_local_devices: bool,
#[clap(name = "usb", env = "MINIDSP_USB", long)]
#[cfg(feature = "hid")]
hid_option: Option<hid::Device>,
#[clap(name = "tcp", env = "MINIDSP_TCP", long)]
tcp_option: Option<String>,
#[clap(name = "force-kind", long)]
force_kind: Option<DeviceKind>,
#[clap(long, env = "MINIDSP_URL")]
url: Option<String>,
#[clap(long, env = "MINIDSPD_URL")]
daemon_url: Option<String>,
#[clap(long, env = "MINIDSP_SOCK")]
#[cfg(target_family = "unix")]
daemon_sock: Option<String>,
#[clap(short = 'f')]
file: Option<PathBuf>,
#[clap(subcommand)]
subcmd: Option<SubCommand>,
}
impl Opts {
async fn apply_builder(&self, builder: &mut Builder) -> Result<(), MiniDSPError> {
#[allow(unused_mut)]
let mut bound = false;
#[cfg(target_family = "unix")]
if let Some(socket_path) = &self.daemon_sock {
builder.with_unix_socket(socket_path).await?;
bound = true;
}
if bound {
} else if let Some(tcp) = &self.tcp_option {
let tcp = if tcp.contains(':') {
tcp.to_string()
} else {
format!("{}:5333", tcp)
};
builder.with_tcp(tcp).unwrap();
} else if let Some(url) = &self.url {
builder
.with_url(url)
.map_err(|_| MiniDSPError::InvalidURL)?;
} else if let Some(url) = &self.daemon_url {
builder.with_http(url).await?;
} else if let Some(device) = self.hid_option.as_ref() {
if let Some(ref path) = device.path {
builder.with_usb_path(path);
} else if let Some((vid, pid)) = device.id {
builder.with_usb_product_id(vid, pid)?;
}
} else {
#[cfg(target_family = "unix")]
let _ = builder.with_unix_socket("/tmp/minidsp.sock").await;
let _ = builder.with_default_usb();
}
builder.with_logging(self.verbose as u8, self.log.clone());
if let Some(force_kind) = self.force_kind {
builder.force_device_kind(force_kind);
}
Ok(())
}
}
#[derive(Clone, Parser, Debug)]
enum SubCommand {
Probe {
#[clap(long, short)]
net: bool,
},
Status,
Gain {
#[clap(long, short)]
relative: bool,
value: Gain,
},
Mute {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
Source {
value: Source,
},
Config {
value: u8,
},
Dirac {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
Input {
input_index: usize,
#[clap(subcommand)]
cmd: InputCommand,
},
Output {
output_index: usize,
#[clap(subcommand)]
cmd: OutputCommand,
},
Server {
#[clap(default_value = "0.0.0.0:5333")]
bind_address: String,
#[clap(long)]
advertise: Option<String>,
#[clap(long)]
ip: Option<String>,
},
Debug {
#[clap(subcommand)]
cmd: DebugCommands,
},
}
#[derive(Clone, Parser, Debug)]
enum InputCommand {
Gain {
value: Gain,
},
Mute {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
Routing {
output_index: usize,
#[clap(subcommand)]
cmd: RoutingCommand,
},
PEQ {
index: PEQTarget,
#[clap(subcommand)]
cmd: FilterCommand,
},
}
#[derive(Clone, Parser, Debug)]
enum RoutingCommand {
Enable {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
Gain {
value: Gain,
},
}
#[derive(Clone, Parser, Debug)]
enum OutputCommand {
Gain {
value: Gain,
},
Mute {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
Delay {
delay: f32,
},
Invert {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
PEQ {
index: PEQTarget,
#[clap(subcommand)]
cmd: FilterCommand,
},
FIR {
#[clap(subcommand)]
cmd: FilterCommand,
},
Crossover {
group: usize,
index: PEQTarget,
#[clap(subcommand)]
cmd: FilterCommand,
},
Compressor {
#[clap(short='b', long, parse(try_from_str = on_or_off))]
bypass: Option<bool>,
#[clap(short = 't', long, allow_hyphen_values(true))]
threshold: Option<f32>,
#[clap(short = 'k', long)]
ratio: Option<f32>,
#[clap(short = 'a', long)]
attack: Option<f32>,
#[clap(short = 'r', long)]
release: Option<f32>,
},
}
#[derive(Debug, Clone, Copy)]
enum PEQTarget {
All,
One(usize),
}
impl FromStr for PEQTarget {
type Err = <usize as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.to_lowercase() == "all" {
Ok(PEQTarget::All)
} else {
Ok(PEQTarget::One(usize::from_str(s)?))
}
}
}
#[derive(Clone, Parser, Debug)]
enum FilterCommand {
Set {
coeff: Vec<f32>,
},
Bypass {
#[clap(parse(try_from_str = on_or_off))]
value: bool,
},
Clear,
Import {
filename: PathBuf,
format: Option<String>,
},
}
#[derive(Debug, Parser)]
pub struct ProductId {
pub vid: u16,
pub pid: Option<u16>,
}
#[derive(Debug, strum::EnumString, strum::Display, Clone, Copy, Eq, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum OutputFormat {
Text,
Json,
JsonLine,
}
impl OutputFormat {
pub fn format<T>(self, obj: &T) -> String
where
T: serde::Serialize + fmt::Display,
{
match self {
OutputFormat::Text => format!("{}", obj),
OutputFormat::Json => {
serde_json::to_string_pretty(obj).expect("couldn't serialize object as json")
}
OutputFormat::JsonLine => {
serde_json::to_string(obj).expect("couldn't serialize object as json")
}
}
}
}
impl FromStr for ProductId {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<_> = s.split(':').collect();
if parts.len() > 2 {
return Err("");
}
let vid = u16::from_str_radix(parts[0], 16).map_err(|_| "couldn't parse vendor id")?;
let mut pid: Option<u16> = None;
if parts.len() > 1 {
pid = Some(u16::from_str_radix(parts[1], 16).map_err(|_| "couldn't parse product id")?);
}
Ok(ProductId { vid, pid })
}
}
async fn run_probe(devices: Vec<DeviceHandle>, net: bool) -> Result<()> {
for dev in &devices {
println!(
"Found {} with serial {} at {} [hw_id: {}, dsp_version: {}]",
dev.device_spec.product_name,
dev.device_info.serial,
dev.url,
dev.device_info.hw_id,
dev.device_info.dsp_version
);
}
if net {
println!("Probing for network devices...");
let devices = net::discover_timeout(Duration::from_secs(8)).await?;
if devices.is_empty() {
println!("No network devices detected")
} else {
for device in &devices {
println!("Found: {}", device);
}
}
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::builder().format_timestamp_millis().init();
let opts: Opts = Opts::parse();
let mut builder = Builder::new();
opts.apply_builder(&mut builder).await?;
let mut devices: Vec<_> = builder
.probe()
.filter_map(|x| async move { x.ok() })
.collect()
.await;
devices.sort_by(|a, b| a.device_info.serial.cmp(&b.device_info.serial));
if let Some(SubCommand::Probe { net }) = opts.subcmd {
run_probe(devices, net).await?;
return Ok(());
}
if !opts.all_local_devices {
devices.truncate(1);
}
if let Some(SubCommand::Server { .. }) = opts.subcmd {
log::warn!("The `server` command is deprecated and will be removed in a future release. Use `minidspd` instead.");
let transport = devices
.first()
.ok_or_else(|| anyhow!("No devices found"))?
.transport
.try_clone()
.expect("device has disappeared");
run_server(opts.subcmd.unwrap(), Box::pin(transport)).await?;
return Ok(());
}
let devices: Vec<_> = devices
.into_iter()
.map(|dev| dev.to_minidsp().expect("device has disappeared"))
.collect();
if devices.is_empty() {
return Err(anyhow!("No devices found"));
}
match &opts.file {
Some(filename) => {
let file: Box<dyn Read> = {
if filename.to_string_lossy() == "-" {
Box::new(std::io::stdin())
} else {
Box::new(File::open(filename)?)
}
};
let reader = BufReader::new(file);
let cmds = reader.lines().filter_map(|s| {
let trimmed = s.ok()?.trim().to_string();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
Some(trimmed)
} else {
None
}
});
for cmd in cmds {
let words = shellwords::split(&cmd)?;
let prefix = &["minidsp".to_string()];
let words = prefix.iter().chain(words.iter());
let this_opts = Opts::try_parse_from(words);
let this_opts = match this_opts {
Ok(x) => x,
Err(e) => {
eprintln!("While executing: {}\n{}", cmd, e);
return Err(anyhow!("Command failure"));
}
};
let results = stream::iter(&devices)
.then(|dev| handlers::run_command(dev, this_opts.subcmd.as_ref(), &opts))
.collect::<Vec<_>>()
.await;
for result in results {
result?;
}
}
}
None => {
let results = stream::iter(&devices)
.then(|dev| handlers::run_command(dev, opts.subcmd.as_ref(), &opts))
.collect::<Vec<_>>()
.await;
for result in results {
result?;
}
}
}
Ok(())
}
fn on_or_off(s: &str) -> Result<bool, &'static str> {
match s {
"on" => Ok(true),
"true" => Ok(true),
"off" => Ok(false),
"false" => Ok(false),
_ => Err("expected `on`, `true`, `off`, `false`"),
}
}
fn parse_hex(s: &str) -> Result<Bytes, hex::FromHexError> {
Ok(Bytes::from(hex::decode(s.replace(" ", ""))?))
}
fn parse_hex_u16(src: &str) -> Result<u16, ParseIntError> {
u16::from_str_radix(src, 16)
}