diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f9de892..472ad25 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -89,25 +89,27 @@ jobs: run: sudo apt-get update && sudo apt-get install -y libjack-jackd2-dev libasound2-dev if: matrix.platform.target == 'x86_64-unknown-linux-gnu' + # --locked can be added later - name: Build binary uses: houseabsolute/actions-rust-cross@v0 with: command: "build" target: ${{ matrix.platform.target }} toolchain: ${{ matrix.toolchain }} - args: "--locked --release" + args: "--release" strip: true env: # Build environment variables GITHUB_ENV: ${{ github.workspace }}/.env + # --locked can be added later - name: Run tests uses: houseabsolute/actions-rust-cross@v0 with: command: "test" target: ${{ matrix.platform.target }} toolchain: ${{ matrix.toolchain }} - args: "--locked --release" + args: "--release" if: ${{ !matrix.platform.skip_tests }} - name: Package as archive diff --git a/Cargo.lock b/Cargo.lock index ce6ed57..b513f46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1105,7 +1105,7 @@ checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smrec" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "camino", diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..db4ec23 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,77 @@ +# max_width = 100 +# hard_tabs = false +# tab_spaces = 4 +# newline_style = "Auto" +# indent_style = "Block" +# use_small_heuristics = "Default" +# fn_call_width = 60 +# attr_fn_like_width = 70 +# struct_lit_width = 18 +# struct_variant_width = 35 +# array_width = 60 +# chain_width = 60 +# single_line_if_else_max_width = 50 +# wrap_comments = false +# format_code_in_doc_comments = false +# comment_width = 80 +# normalize_comments = false +# normalize_doc_attributes = false +# license_template_path = "" +# format_strings = false +# format_macro_matchers = false +# format_macro_bodies = true +# hex_literal_case = "Preserve" +# empty_item_single_line = true +# struct_lit_single_line = true +# fn_single_line = false +# where_single_line = false +# imports_indent = "Block" +# imports_layout = "Mixed" +imports_granularity = "Crate" +group_imports = "One" +reorder_imports = true +reorder_modules = true +# reorder_impl_items = false +# type_punctuation_density = "Wide" +# space_before_colon = false +# space_after_colon = true +# spaces_around_ranges = false +# binop_separator = "Front" +# remove_nested_parens = true +# combine_control_expr = true +# overflow_delimited_expr = false +# struct_field_align_threshold = 0 +# enum_discrim_align_threshold = 0 +# match_arm_blocks = true +# match_arm_leading_pipes = "Never" +# force_multiline_blocks = false +# fn_args_layout = "Tall" +# brace_style = "SameLineWhere" +# control_brace_style = "AlwaysSameLine" +# trailing_semicolon = true +# trailing_comma = "Vertical" +# match_block_trailing_comma = false +# blank_lines_upper_bound = 1 +# blank_lines_lower_bound = 0 +edition = "2021" +# version = "One" +# inline_attribute_width = 0 +# format_generated_files = true +# merge_derives = true +# use_try_shorthand = false +# use_field_init_shorthand = false +# force_explicit_abi = true +# condense_wildcard_suffixes = false +# color = "Auto" +# required_version = "1.4.38" +# unstable_features = false +# disable_all_formatting = false +# skip_children = false +# hide_parse_errors = false +# error_on_line_overflow = false +# error_on_unformatted = false +# report_todo = "Never" +# report_fixme = "Never" +# ignore = [] +# emit_mode = "Files" +# make_backup = false diff --git a/src/config.rs b/src/config.rs index 7fcf609..8157a2a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,21 @@ -use crate::wav::spec_from_config; -use crate::WriterHandles; +use crate::{wav::spec_from_config, WriterHandles}; use anyhow::{anyhow, bail, Result}; use camino::Utf8PathBuf; use chrono::{Datelike, Timelike, Utc}; -use cpal::traits::{DeviceTrait, HostTrait}; -use cpal::SupportedStreamConfig; -use serde::de::{self, Deserializer, MapAccess, Visitor}; -use serde::Deserialize; -use std::collections::HashMap; -use std::fmt; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use cpal::{ + traits::{DeviceTrait, HostTrait}, + SupportedStreamConfig, +}; +use serde::{ + de::{self, Deserializer, MapAccess, Visitor}, + Deserialize, +}; +use std::{ + collections::HashMap, + fmt, + str::FromStr, + sync::{Arc, Mutex}, +}; /// Chooses which channels to record. pub fn choose_channels_to_record( @@ -132,17 +137,20 @@ impl SmrecConfig { config.channels_to_record = channels_to_record; config.channels_to_record.iter().for_each(|channel| { - if !config.channel_names.contains_key(&(channel + 1)) { - config - .channel_names - .insert(*channel + 1, format!("chn_{}.wav", channel + 1)); - } else { + if config.channel_names.contains_key(&(channel + 1)) { let name = config.channel_names.get(&(channel + 1)).unwrap(); - if !name.ends_with(".wav") { + if !std::path::Path::new(name) + .extension() + .map_or(false, |ext| ext.eq_ignore_ascii_case("wav")) + { config .channel_names - .insert(*channel + 1, format!("{}.wav", name)); + .insert(*channel + 1, format!("{name}.wav")); } + } else { + config + .channel_names + .insert(*channel + 1, format!("chn_{}.wav", channel + 1)); } }); config.cpal_stream_config = Some(cpal_stream_config); diff --git a/src/main.rs b/src/main.rs index c869ac1..9938795 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,34 @@ +// Most of the lints we deny here have a good chance to be relevant for our project. +#![deny(clippy::all)] +// We warn for all lints on the planet. Just to filter them later for customization. +// It is impossible to remember all the lints so a subtractive approach keeps us updated, in control and knowledgeable. +#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)] +// Then in the end we allow ridiculous or too restrictive lints that are not relevant for our project. +// This list is dynamic and will grow in time which will define our style. +#![allow( + clippy::multiple_crate_versions, + clippy::blanket_clippy_restriction_lints, + clippy::missing_docs_in_private_items, + clippy::pub_use, + clippy::std_instead_of_alloc, + clippy::std_instead_of_core, + clippy::implicit_return, + clippy::missing_inline_in_public_items, + clippy::similar_names, + clippy::question_mark_used, + clippy::expect_used, + clippy::missing_errors_doc, + clippy::pattern_type_mismatch, + clippy::module_name_repetitions, + clippy::empty_structs_with_brackets, + clippy::as_conversions, + clippy::self_named_module_files, + clippy::cargo_common_metadata, + clippy::exhaustive_structs, + // It is a binary crate, panicing is usually fine. + clippy::missing_panics_doc +)] + mod config; mod list; mod midi; @@ -6,22 +37,25 @@ mod stream; mod types; mod wav; +use crate::{ + config::{choose_channels_to_record, SmrecConfig}, + midi::Midi, +}; use anyhow::{bail, Result}; use clap::{Parser, Subcommand}; use config::{choose_device, choose_host}; use cpal::traits::{DeviceTrait, StreamTrait}; use hound::WavWriter; use osc::Osc; -use std::cell::RefCell; -use std::fs::File; -use std::io::BufWriter; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::{ + cell::RefCell, + fs::File, + io::BufWriter, + rc::Rc, + sync::{Arc, Mutex}, +}; use types::Action; -use crate::config::{choose_channels_to_record, SmrecConfig}; -use crate::midi::Midi; - #[derive(Parser)] #[command( author, @@ -93,6 +127,7 @@ struct List { pub type WriterHandle = Arc>>>>; pub type WriterHandles = Arc>; +#[allow(clippy::too_many_lines)] fn main() -> Result<()> { let cli = Cli::parse(); diff --git a/src/midi.rs b/src/midi.rs index 0e3fc78..ccbbc6a 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -8,10 +8,12 @@ use anyhow::{bail, Result}; use midir::{ MidiInput, MidiInputConnection, MidiInputPort, MidiOutput, MidiOutputConnection, MidiOutputPort, }; -use std::collections::HashMap; -use std::ops::Deref; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + ops::Deref, + str::FromStr, + sync::{Arc, Mutex}, +}; enum MessageType { NoteOff, @@ -45,7 +47,7 @@ const fn make_cc_message(channel: u8, cc_num: u8, value: u8) -> [u8; 3] { [0xB0 + channel, cc_num, value] } -/// HashMap of port name to vector of (channel_num, cc_num[start], cc_num[stop]) +/// `HashMap` of port name to vector of (`channel_num`, `cc_num`[start], `cc_num`[stop]) #[derive(Debug, Clone)] pub struct MidiConfig(HashMap>); @@ -160,9 +162,10 @@ impl Midi { }) } - pub fn listen(&mut self) -> Result<()> { - let input_ports = self - .input_config + // These are going to be addressed in a later refactor. + #[allow(clippy::type_complexity)] + fn input_ports_from_configs(&self) -> Result)>> { + self.input_config .iter() .filter_map(|(port_name, configs)| { let input_ports = self.find_input_ports(port_name).ok()?; @@ -175,7 +178,11 @@ impl Midi { }) .flatten() .map(Ok) - .collect::)>, anyhow::Error>>()?; + .collect::)>, anyhow::Error>>() + } + + fn register_midi_input_hooks(&mut self) -> Result<()> { + let input_ports = self.input_ports_from_configs()?; // Start listening for MIDI messages on all configured ports and channels. for (port_name, port, configs) in input_ports { @@ -254,46 +261,59 @@ impl Midi { ); } + Ok(()) + } + + // These are going to be addressed in a later refactor. + #[allow(clippy::type_complexity)] + fn output_connections_from_config( + &self, + ) -> Result>, Vec<(u8, u8, u8)>)>>> { if let Some(ref output_config) = self.output_config { - let output_connections = { - let output_ports = output_config - .iter() - .filter_map(|(port_name, configs)| { - let output_ports = self.find_output_ports(port_name).ok()?; - Some( - output_ports - .into_iter() - .map(move |(name, port)| (name, port, configs.clone())) - .collect::>(), - ) - }) - .flatten() - .map(Ok) - .collect::)>, anyhow::Error>>()?; - - output_ports - .iter() - .map(|(port_name, port, configs)| { - let output = MidiOutput::new("smrec")?; - Ok::< - ( - std::string::String, - Arc>, - std::vec::Vec<(u8, u8, u8)>, - ), - anyhow::Error, - >(( - port_name.clone(), - Arc::new(Mutex::new(output.connect(port, port_name).expect("Could not bind to {port_name}"))), - configs.clone(), - )) - }) - .collect::>, Vec<(u8, u8, u8)>)>, _>>( - )? - }; - - let receiver_channel = self.receiver_channel.clone(); + let output_ports = output_config + .iter() + .filter_map(|(port_name, configs)| { + let output_ports = self.find_output_ports(port_name).ok()?; + Some( + output_ports + .into_iter() + .map(move |(name, port)| (name, port, configs.clone())) + .collect::>(), + ) + }) + .flatten() + .map(Ok) + .collect::)>, anyhow::Error>>( + )?; + + return output_ports + .iter() + .map(|(port_name, port, configs)| { + let output = MidiOutput::new("smrec")?; + Ok(Some(( + port_name.clone(), + Arc::new(Mutex::new( + output + .connect(port, port_name) + .expect("Could not bind to {port_name}"), + )), + configs.clone(), + ))) + }) + .collect::>, Vec<(u8, u8, u8)>)>>, + _, + >>(); + } + Ok(None) + } + + fn spin_midi_output_thread_if_necessary(&mut self) -> Result<()> { + let output_connections = self.output_connections_from_config()?; + let receiver_channel = self.receiver_channel.clone(); + + if let Some(output_connections) = output_connections { self.output_thread = Some(std::thread::spawn(move || { loop { if let Ok(action) = receiver_channel.recv() { @@ -371,4 +391,11 @@ impl Midi { Ok(()) } + + pub fn listen(&mut self) -> Result<()> { + self.register_midi_input_hooks()?; + self.spin_midi_output_thread_if_necessary()?; + + Ok(()) + } } diff --git a/src/midi/parse.rs b/src/midi/parse.rs index ec7b79a..f1ca460 100644 --- a/src/midi/parse.rs +++ b/src/midi/parse.rs @@ -1,9 +1,7 @@ #![allow(clippy::type_complexity)] -use anyhow::anyhow; -use anyhow::Result; -use std::collections::HashMap; - +use crate::midi::MidiConfig; +use anyhow::{anyhow, Result}; use nom::{ branch::alt, bytes::complete::take_until, @@ -13,8 +11,7 @@ use nom::{ sequence::{delimited, preceded, tuple}, IResult, }; - -use crate::midi::MidiConfig; +use std::collections::HashMap; /// Parses * or a u8 ranged number fn parse_u8_or_star(input: &str) -> IResult<&str, u8> { diff --git a/src/osc.rs b/src/osc.rs index 2fcf942..9f762dd 100644 --- a/src/osc.rs +++ b/src/osc.rs @@ -1,10 +1,11 @@ use crate::types::Action; use anyhow::Result; -use rosc::encoder::encode; -use rosc::{OscMessage, OscPacket, OscType}; -use std::net::{SocketAddr, UdpSocket}; -use std::str::FromStr; -use std::sync::Arc; +use rosc::{encoder::encode, OscMessage, OscPacket, OscType}; +use std::{ + net::{SocketAddr, UdpSocket}, + str::FromStr, + sync::Arc, +}; pub struct Osc { sender_socket: Arc, diff --git a/src/stream.rs b/src/stream.rs index 7c0e185..a82a658 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,12 +1,8 @@ +use crate::{wav::write_input_data, WriterHandles}; use anyhow::{bail, Result}; -use cpal::traits::DeviceTrait; -use cpal::{FromSample, Sample}; - +use cpal::{traits::DeviceTrait, FromSample, Sample}; use std::sync::{Arc, Mutex}; -use crate::wav::write_input_data; -use crate::WriterHandles; - pub fn build( device: &cpal::Device, config: cpal::SupportedStreamConfig, diff --git a/src/wav.rs b/src/wav.rs index c9f0773..0ef9d34 100644 --- a/src/wav.rs +++ b/src/wav.rs @@ -1,7 +1,9 @@ use cpal::{FromSample, Sample}; -use std::fs::File; -use std::io::BufWriter; -use std::sync::{Arc, Mutex}; +use std::{ + fs::File, + io::BufWriter, + sync::{Arc, Mutex}, +}; pub fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat { if format.is_float() { @@ -32,7 +34,7 @@ pub fn write_input_data( { if let Ok(mut guard) = writer.try_lock() { if let Some(writer) = guard.as_mut() { - for &sample in input.iter() { + for &sample in input { let sample: U = U::from_sample(sample); writer.write_sample(sample).ok(); }