├── .gitignore ├── src ├── types.rs ├── wav.rs ├── stream.rs ├── list.rs ├── midi │ └── parse.rs ├── osc.rs ├── config.rs ├── main.rs └── midi.rs ├── assets └── logo_transparent.png ├── scripts ├── README.md ├── pre-build-linux.sh └── pre-build-win.ps1 ├── justfile ├── .cargo └── config.toml ├── CHANGELOG.md ├── Cross.toml ├── LICENCE ├── Cargo.toml ├── rustfmt.toml ├── flake.lock ├── flake.nix ├── .github └── workflows │ └── ci.yaml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | pub enum Action { 2 | Stop, 3 | Start, 4 | Err(String), 5 | } 6 | -------------------------------------------------------------------------------- /assets/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alisomay/smrec/HEAD/assets/logo_transparent.png -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Pre build scripts 2 | 3 | These are about the become obsolete. 4 | They are here for historical reasons. 5 | Once ` cpal`` publishes a new version, we can remove them. 6 | For linux users, we'll see how it works but I might make a PR to `cpal`` to automate installation of jack and alsa dependencies also if it makes sense. 7 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 2 | 3 | build-win: 4 | . .\pre-build-win.ps1 5 | cargo build --release 6 | install-win: 7 | . .\pre-build-win.ps1 8 | cargo build --release 9 | Copy-Item -Path {{ justfile_directory() }}\target\release\smrec.exe -Destination {{ env_var_or_default("USERPROFILE", "") }}\.cargo\bin\ 10 | 11 | prepare-linux: 12 | ./pre-build-linux.sh -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | rustflags = [ 4 | "-C", "link-arg=-Bstatic", 5 | ] 6 | 7 | [target.armv7-unknown-linux-gnueabihf] 8 | linker = "arm-linux-gnueabihf-gcc" 9 | rustflags = [ 10 | "-C", "link-arg=-Bstatic", 11 | ] 12 | 13 | # [target.i686-unknown-linux-gnu] 14 | # linker = "i686-linux-gnu-gcc" 15 | # rustflags = [ 16 | # "-C", "link-arg=-Bstatic", 17 | # ] 18 | 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format of this changelog is based on 4 | [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | smrec project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | 39 | 40 | ## [0.2.1] - 2020.11.20 41 | 42 | - Initial release 43 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | # Install build dependencies for the right architecture per target 2 | # [target.i686-unknown-linux-gnu] 3 | # runner = "qemu-user" 4 | # pre-build = [ 5 | # "export BUILDKIT_PROGRESS=plain", 6 | # "dpkg --add-architecture $CROSS_DEB_ARCH", 7 | # "apt-get update && apt-get --assume-yes install libasound2-dev:$CROSS_DEB_ARCH libjack-jackd2-dev:$CROSS_DEB_ARCH" 8 | # ] 9 | 10 | [target.aarch64-unknown-linux-gnu] 11 | runner = "qemu-user" 12 | pre-build = [ 13 | "export BUILDKIT_PROGRESS=plain", 14 | "dpkg --add-architecture $CROSS_DEB_ARCH", 15 | "apt-get update && apt-get --assume-yes install libasound2-dev:$CROSS_DEB_ARCH libjack-jackd2-dev:$CROSS_DEB_ARCH" 16 | ] 17 | 18 | [target.armv7-unknown-linux-gnueabihf] 19 | runner = "qemu-user" 20 | pre-build = [ 21 | "export BUILDKIT_PROGRESS=plain", 22 | "dpkg --add-architecture $CROSS_DEB_ARCH", 23 | "apt-get update && apt-get --assume-yes install libasound2-dev:$CROSS_DEB_ARCH libjack-jackd2-dev:$CROSS_DEB_ARCH" 24 | ] 25 | 26 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - Ali Somay (alisomay@runbox.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/wav.rs: -------------------------------------------------------------------------------- 1 | use cpal::{FromSample, Sample}; 2 | use std::{ 3 | fs::File, 4 | io::BufWriter, 5 | sync::{Arc, Mutex}, 6 | }; 7 | 8 | pub fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat { 9 | if format.is_float() { 10 | hound::SampleFormat::Float 11 | } else { 12 | hound::SampleFormat::Int 13 | } 14 | } 15 | 16 | #[allow(clippy::cast_possible_truncation)] 17 | pub fn spec_from_config(config: &cpal::SupportedStreamConfig) -> hound::WavSpec { 18 | hound::WavSpec { 19 | // Hardcoded because channels will be always mono. 20 | channels: 1, 21 | sample_rate: config.sample_rate().0 as _, 22 | // Truncation is safe because we're only using 8, 16, 24 and 32 bit samples. 23 | bits_per_sample: (config.sample_format().sample_size() * 8) as _, 24 | sample_format: sample_format(config.sample_format()), 25 | } 26 | } 27 | 28 | pub fn write_input_data( 29 | input: &[T], 30 | writer: &Arc>>>>, 31 | ) where 32 | T: Sample, 33 | U: Sample + hound::Sample + FromSample, 34 | { 35 | if let Ok(mut guard) = writer.try_lock() { 36 | if let Some(writer) = guard.as_mut() { 37 | for &sample in input { 38 | let sample: U = U::from_sample(sample); 39 | writer.write_sample(sample).ok(); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "smrec" 3 | version = "0.2.1" 4 | authors = ["alisomay "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Minimalist multi-track audio recorder which may be controlled via OSC or MIDI." 8 | readme = "README.md" 9 | homepage = "https://github.com/alisomay/smrec" 10 | repository = "https://github.com/alisomay/smrec" 11 | documentation = "https://docs.rs/smrec/0.1.4/smrec/#" 12 | keywords = ["audio", "record", "midi", "osc", "cli"] 13 | categories = ["multimedia"] 14 | exclude = [ 15 | "tests/*", 16 | "assets/favicon/*", 17 | "assets/logo_*" 18 | ] 19 | 20 | # When https://github.com/RustAudio/cpal/issues/794 is resolved this can continue to track the stable release. 21 | [target.'cfg(target_os = "windows")'.dependencies] 22 | cpal = { git = "https://github.com/RustAudio/cpal.git", features = ["asio"] } 23 | midir = { version = "0.9", features = ["winrt"] } 24 | 25 | [target.'cfg(target_os = "linux")'.dependencies] 26 | cpal = { git = "https://github.com/RustAudio/cpal.git", features = ["jack"] } 27 | midir = { version = "0.9", features = ["jack"] } 28 | 29 | # [target.'cfg(target_os = "windows")'.dependencies] 30 | # cpal = { version = "0.15", features = ["asio"] } 31 | # midir = { version = "0.9", features = ["winrt"] } 32 | 33 | # [target.'cfg(target_os = "linux")'.dependencies] 34 | # cpal = { version = "0.15", features = ["jack"] } 35 | # midir = { version = "0.9", features = ["jack"] } 36 | 37 | [dependencies] 38 | midir = "0.9" 39 | clap = { version = "4", features = ["derive", "env"] } 40 | serde = { version = "1.0", features = ["derive"] } 41 | chrono = { version = "0.4", features = ["serde"] } 42 | anyhow = "1.0" 43 | crossbeam = "0.8" 44 | rosc = "0.10" 45 | hound = "3.4" 46 | camino = "1" 47 | toml = "0.8" 48 | home = "0.5" 49 | ctrlc = "3.1" 50 | thiserror = "1.0" 51 | glob-match = "0.2" 52 | nom = "7" 53 | # cpal = "0.15" 54 | cpal = { git = "https://github.com/RustAudio/cpal.git" } 55 | -------------------------------------------------------------------------------- /scripts/pre-build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Determine the package manager 4 | if command -v apt-get >/dev/null 2>&1; then 5 | PACKAGE_MANAGER="apt-get" 6 | elif command -v dnf >/dev/null 2>&1; then 7 | PACKAGE_MANAGER="dnf" 8 | elif command -v yum >/dev/null 2>&1; then 9 | PACKAGE_MANAGER="yum" 10 | elif command -v zypper >/dev/null 2>&1; then 11 | PACKAGE_MANAGER="zypper" 12 | elif command -v pacman >/dev/null 2>&1; then 13 | PACKAGE_MANAGER="pacman" 14 | else 15 | echo "Unsupported package manager. Exiting." 16 | exit 1 17 | fi 18 | 19 | # Define the package names 20 | if [[ "$PACKAGE_MANAGER" == "apt-get" ]]; then 21 | ALSA_PACKAGE="libasound2-dev" 22 | JACK_PACKAGE="libjack-jackd2-dev" 23 | elif [[ "$PACKAGE_MANAGER" == "dnf" || "$PACKAGE_MANAGER" == "yum" ]]; then 24 | ALSA_PACKAGE="alsa-lib-devel" 25 | JACK_PACKAGE="jack-audio-connection-kit-devel" 26 | elif [[ "$PACKAGE_MANAGER" == "zypper" ]]; then 27 | ALSA_PACKAGE="alsa-devel" 28 | JACK_PACKAGE="jack-devel" 29 | elif [[ "$PACKAGE_MANAGER" == "pacman" ]]; then 30 | ALSA_PACKAGE="alsa-lib" 31 | JACK_PACKAGE="jack" 32 | fi 33 | 34 | # Update package lists for the latest version of the repository 35 | if [[ "$PACKAGE_MANAGER" == "pacman" ]]; then 36 | echo "Updating package databases..." 37 | sudo pacman -Syq --noconfirm || { 38 | echo "Updating package databases failed. Exiting." 39 | exit 1 40 | } 41 | else 42 | echo "Updating package lists..." 43 | sudo $PACKAGE_MANAGER update -yq || { 44 | echo "Updating package lists failed. Exiting." 45 | exit 1 46 | } 47 | fi 48 | 49 | # Install necessary packages for ALSA 50 | echo "Installing ALSA development files..." 51 | sudo $PACKAGE_MANAGER install -yq $ALSA_PACKAGE || { 52 | echo "Installing ALSA development files failed. Exiting." 53 | exit 1 54 | } 55 | 56 | # Install necessary packages for JACK 57 | echo "Installing JACK development files..." 58 | sudo $PACKAGE_MANAGER install -yq $JACK_PACKAGE || { 59 | echo "Installing JACK development files failed. Exiting." 60 | exit 1 61 | } 62 | 63 | echo "Build environment preparation complete." 64 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # max_width = 100 2 | # hard_tabs = false 3 | # tab_spaces = 4 4 | # newline_style = "Auto" 5 | # indent_style = "Block" 6 | # use_small_heuristics = "Default" 7 | # fn_call_width = 60 8 | # attr_fn_like_width = 70 9 | # struct_lit_width = 18 10 | # struct_variant_width = 35 11 | # array_width = 60 12 | # chain_width = 60 13 | # single_line_if_else_max_width = 50 14 | # wrap_comments = false 15 | # format_code_in_doc_comments = false 16 | # comment_width = 80 17 | # normalize_comments = false 18 | # normalize_doc_attributes = false 19 | # license_template_path = "" 20 | # format_strings = false 21 | # format_macro_matchers = false 22 | # format_macro_bodies = true 23 | # hex_literal_case = "Preserve" 24 | # empty_item_single_line = true 25 | # struct_lit_single_line = true 26 | # fn_single_line = false 27 | # where_single_line = false 28 | # imports_indent = "Block" 29 | # imports_layout = "Mixed" 30 | imports_granularity = "Crate" 31 | group_imports = "One" 32 | reorder_imports = true 33 | reorder_modules = true 34 | # reorder_impl_items = false 35 | # type_punctuation_density = "Wide" 36 | # space_before_colon = false 37 | # space_after_colon = true 38 | # spaces_around_ranges = false 39 | # binop_separator = "Front" 40 | # remove_nested_parens = true 41 | # combine_control_expr = true 42 | # overflow_delimited_expr = false 43 | # struct_field_align_threshold = 0 44 | # enum_discrim_align_threshold = 0 45 | # match_arm_blocks = true 46 | # match_arm_leading_pipes = "Never" 47 | # force_multiline_blocks = false 48 | # fn_args_layout = "Tall" 49 | # brace_style = "SameLineWhere" 50 | # control_brace_style = "AlwaysSameLine" 51 | # trailing_semicolon = true 52 | # trailing_comma = "Vertical" 53 | # match_block_trailing_comma = false 54 | # blank_lines_upper_bound = 1 55 | # blank_lines_lower_bound = 0 56 | edition = "2021" 57 | # version = "One" 58 | # inline_attribute_width = 0 59 | # format_generated_files = true 60 | # merge_derives = true 61 | # use_try_shorthand = false 62 | # use_field_init_shorthand = false 63 | # force_explicit_abi = true 64 | # condense_wildcard_suffixes = false 65 | # color = "Auto" 66 | # required_version = "1.4.38" 67 | # unstable_features = false 68 | # disable_all_formatting = false 69 | # skip_children = false 70 | # hide_parse_errors = false 71 | # error_on_line_overflow = false 72 | # error_on_unformatted = false 73 | # report_todo = "Never" 74 | # report_fixme = "Never" 75 | # ignore = [] 76 | # emit_mode = "Files" 77 | # make_backup = false 78 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "naersk": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs" 6 | }, 7 | "locked": { 8 | "lastModified": 1688534083, 9 | "narHash": "sha256-/bI5vsioXscQTsx+Hk9X5HfweeNZz/6kVKsbdqfwW7g=", 10 | "owner": "nix-community", 11 | "repo": "naersk", 12 | "rev": "abca1fb7a6cfdd355231fc220c3d0302dbb4369a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "nix-community", 17 | "ref": "master", 18 | "repo": "naersk", 19 | "type": "github" 20 | } 21 | }, 22 | "nixpkgs": { 23 | "locked": { 24 | "lastModified": 1700108881, 25 | "narHash": "sha256-+Lqybl8kj0+nD/IlAWPPG/RDTa47gff9nbei0u7BntE=", 26 | "owner": "NixOS", 27 | "repo": "nixpkgs", 28 | "rev": "7414e9ee0b3e9903c24d3379f577a417f0aae5f1", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "id": "nixpkgs", 33 | "type": "indirect" 34 | } 35 | }, 36 | "nixpkgs_2": { 37 | "locked": { 38 | "lastModified": 1699963925, 39 | "narHash": "sha256-LE7OV/SwkIBsCpAlIPiFhch/J+jBDGEZjNfdnzCnCrY=", 40 | "owner": "NixOS", 41 | "repo": "nixpkgs", 42 | "rev": "bf744fe90419885eefced41b3e5ae442d732712d", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "NixOS", 47 | "ref": "nixos-unstable", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "naersk": "naersk", 55 | "nixpkgs": "nixpkgs_2", 56 | "utils": "utils" 57 | } 58 | }, 59 | "systems": { 60 | "locked": { 61 | "lastModified": 1681028828, 62 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 63 | "owner": "nix-systems", 64 | "repo": "default", 65 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "nix-systems", 70 | "repo": "default", 71 | "type": "github" 72 | } 73 | }, 74 | "utils": { 75 | "inputs": { 76 | "systems": "systems" 77 | }, 78 | "locked": { 79 | "lastModified": 1689068808, 80 | "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", 81 | "owner": "numtide", 82 | "repo": "flake-utils", 83 | "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "numtide", 88 | "repo": "flake-utils", 89 | "type": "github" 90 | } 91 | } 92 | }, 93 | "root": "root", 94 | "version": 7 95 | } 96 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | # Define the inputs required for this flake. 3 | inputs = { 4 | # The version of nixpkgs we're using. We need a newer rustc that is available on the unstable banch. 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | # The flake-utils provide helpful utilities for managing flakes. 8 | utils.url = "github:numtide/flake-utils"; 9 | 10 | # naersk is used for building Rust projects with Nix. 11 | naersk.url = "github:nix-community/naersk/master"; 12 | }; 13 | 14 | # Define the outputs of the flake, which depend on the inputs and the current flake. 15 | outputs = { self, nixpkgs, utils, naersk }: 16 | # Use flake-utils to generate outputs for each system supported by default. 17 | utils.lib.eachDefaultSystem (system: 18 | let 19 | # Import the nixpkgs for the given system. 20 | pkgs = import nixpkgs { inherit system; }; 21 | 22 | # Instantiate naersk with the current package set. 23 | naerskLib = pkgs.callPackage naersk { }; 24 | 25 | # Name of the Rust project. 26 | name = "smrec"; 27 | 28 | # Build-time dependencies. 29 | nativeBuildInputs = with pkgs; [ 30 | cargo # Cargo, the Rust package manager. 31 | rustc # The Rust compiler. 32 | pkg-config # Helper tool used when compiling applications and libraries. 33 | ]; 34 | 35 | # Runtime dependencies. 36 | buildInputs = with pkgs; [ 37 | alsa-lib # ALSA library for audio. 38 | jack2 # JACK Audio Connection Kit. 39 | ]; 40 | in 41 | { 42 | # Define a development shell for this project. 43 | devShells.default = pkgs.mkShell 44 | { 45 | # Pass both build-time and runtime dependencies to the shell environment. 46 | inherit buildInputs nativeBuildInputs; 47 | }; 48 | 49 | # Define the Rust package. 50 | packages.default = pkgs.rustPlatform.buildRustPackage { 51 | pname = name; # Package name. 52 | version = "0.2.1"; # Version of the package. 53 | inherit buildInputs nativeBuildInputs; 54 | src = ./.; # Source directory for the Rust project. 55 | cargoLock = { 56 | lockFile = ./Cargo.lock; # Path to the Cargo.lock file. 57 | # Specific output hashes for dependencies, required for reproducibility. 58 | outputHashes = { 59 | "asio-sys-0.2.1" = "sha256-MPknKFVyxTDI7r4xC860RSOa9zmB/iQsCZeAlvE8cdk="; 60 | }; 61 | }; 62 | }; 63 | 64 | # Define the application produced by this project. 65 | apps.default = { 66 | type = "app"; # Type is an application. 67 | # The path to the executable that will run the app. 68 | program = "${self.packages.${system}.default}/bin/${name}"; 69 | }; 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | use crate::{wav::write_input_data, WriterHandles}; 2 | use anyhow::{bail, Result}; 3 | use cpal::{traits::DeviceTrait, FromSample, Sample}; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | pub fn build( 7 | device: &cpal::Device, 8 | config: cpal::SupportedStreamConfig, 9 | channels_to_record: &[usize], 10 | writers_in_stream: Arc>>, 11 | ) -> Result { 12 | let stream_error_callback = move |err| { 13 | eprintln!("An error occurred on the input stream: {err}"); 14 | }; 15 | 16 | match config.sample_format() { 17 | cpal::SampleFormat::I8 => Ok(device.build_input_stream( 18 | &config.into(), 19 | process::(channels_to_record.to_vec(), writers_in_stream), 20 | stream_error_callback, 21 | None, 22 | )?), 23 | cpal::SampleFormat::I16 => Ok(device.build_input_stream( 24 | &config.into(), 25 | process::(channels_to_record.to_vec(), writers_in_stream), 26 | stream_error_callback, 27 | None, 28 | )?), 29 | cpal::SampleFormat::I32 => Ok(device.build_input_stream( 30 | &config.into(), 31 | process::(channels_to_record.to_vec(), writers_in_stream), 32 | stream_error_callback, 33 | None, 34 | )?), 35 | cpal::SampleFormat::F32 => Ok(device.build_input_stream( 36 | &config.into(), 37 | process::(channels_to_record.to_vec(), writers_in_stream), 38 | stream_error_callback, 39 | None, 40 | )?), 41 | sample_format => bail!( 42 | "Sample format {:?} is not supported by this program.", 43 | sample_format 44 | ), 45 | } 46 | } 47 | 48 | #[allow(clippy::type_complexity)] 49 | fn process( 50 | channels_to_record: Vec, 51 | writers_in_stream: Arc>>, 52 | ) -> Box 53 | where 54 | T: Sample, 55 | U: Sample + hound::Sample + FromSample, 56 | { 57 | Box::new(move |data: &[T], _: &_| { 58 | // We really don't do much here. We just record the data to the files. 59 | // So avoiding continuous allocation is not a priority. 60 | // We have a lot of time to do processing in every call to this function, so we can afford to do some allocation. 61 | // Premature optimization is the root of all evil. :) 62 | let mut channel_buffer = Vec::>::with_capacity(channels_to_record.len()); 63 | 64 | for _ in 0..channels_to_record.len() { 65 | channel_buffer.push(Vec::with_capacity(data.len())); 66 | } 67 | 68 | // Channels to record has an ascending order, so does the interleaved data. 69 | 70 | // Process the frame 71 | for frame in data.chunks(channels_to_record.len()) { 72 | // We have one sample for each channel in this frame since we're recording mono. 73 | 74 | for (channel_idx, sample) in frame.iter().enumerate() { 75 | // Put that sample in the corresponding channel buffer. 76 | // De-interleave the data in other words. 77 | channel_buffer[channel_idx].push(*sample); 78 | } 79 | } 80 | 81 | if let Some(writers) = writers_in_stream.lock().unwrap().as_ref() { 82 | let writers_in_stream = writers.clone(); 83 | // Write the de-interleaved buffer to the files. 84 | for (channel_idx, channel_data) in channel_buffer.iter().enumerate() { 85 | write_input_data::(channel_data, &writers_in_stream[channel_idx]); 86 | } 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cpal::traits::{DeviceTrait, HostTrait}; 3 | use midir::{Ignore, MidiInput, MidiOutput}; 4 | 5 | pub fn enumerate_audio() -> Result<()> { 6 | println!("Audio Hosts and Devices"); 7 | println!("======================="); 8 | println!(" Supported hosts:\n {:?}", cpal::ALL_HOSTS); 9 | let available_hosts = cpal::available_hosts(); 10 | println!(" Available hosts:\n {available_hosts:?}"); 11 | 12 | for host_id in available_hosts { 13 | println!(); 14 | println!(" {} Default Devices:", host_id.name()); 15 | let host = cpal::host_from_id(host_id)?; 16 | 17 | host.default_input_device().map_or_else( 18 | || { 19 | println!(" Default Input Device:\n None"); 20 | }, 21 | |d| { 22 | println!(" Default Input Device:\n {}", d.name().unwrap()); 23 | }, 24 | ); 25 | host.default_output_device().map_or_else( 26 | || { 27 | println!(" Default Output Device:\n None"); 28 | }, 29 | |d| { 30 | println!(" Default Output Device:\n {}", d.name().unwrap()); 31 | }, 32 | ); 33 | 34 | let devices = host.devices()?; 35 | println!(); 36 | println!(" {} Available Devices:", host_id.name()); 37 | for (device_index, device) in devices.enumerate() { 38 | println!(" {}. \"{}\"", device_index + 1, device.name()?); 39 | 40 | // Input configs 41 | if let Ok(conf) = device.default_input_config() { 42 | // println!(" Default input stream config:\n {:?}", conf); 43 | // SupportedStreamConfig { channels: 16, sample_rate: SampleRate(44100), buffer_size: Range { min: 14, max: 4096 }, sample_format: F32 } 44 | println!(" Default input stream config:"); 45 | println!( 46 | " Channels: {}\n Sample Rate: {}\n Buffer Size {}\n Sample Format: {}", 47 | conf.channels(), 48 | conf.sample_rate().0, 49 | match conf.buffer_size() { 50 | cpal::SupportedBufferSize::Unknown => "unknown".to_string(), 51 | cpal::SupportedBufferSize::Range { min, max } => 52 | format!("{{ min: {min}, max: {max} }}"), 53 | }, 54 | conf.sample_format() 55 | ); 56 | } 57 | let input_configs = match device.supported_input_configs() { 58 | Ok(f) => f.collect(), 59 | Err(err) => { 60 | println!(" Error getting supported input configs: {err}"); 61 | Vec::new() 62 | } 63 | }; 64 | if !input_configs.is_empty() { 65 | // TODO: If necessary list all supported stream configs 66 | } 67 | 68 | // Output configs 69 | if let Ok(conf) = device.default_output_config() { 70 | println!(" Default output stream config:"); 71 | println!( 72 | " Channels: {}\n Sample Rate: {}\n Buffer Size {}\n Sample Format: {}", 73 | conf.channels(), 74 | conf.sample_rate().0, 75 | match conf.buffer_size() { 76 | cpal::SupportedBufferSize::Unknown => "unknown".to_string(), 77 | cpal::SupportedBufferSize::Range { min, max } => 78 | format!("{{ min: {min}, max: {max} }}"), 79 | }, 80 | conf.sample_format() 81 | ); 82 | } 83 | let output_configs = match device.supported_output_configs() { 84 | Ok(f) => f.collect(), 85 | Err(err) => { 86 | println!(" Error getting supported output configs: {err}"); 87 | Vec::new() 88 | } 89 | }; 90 | if !output_configs.is_empty() { 91 | // TODO: If necessary list all supported stream configs 92 | } 93 | } 94 | } 95 | 96 | Ok(()) 97 | } 98 | 99 | pub fn enumerate_midi() -> Result<()> { 100 | let mut midi_in = MidiInput::new("dummy input")?; 101 | midi_in.ignore(Ignore::None); 102 | let midi_out = MidiOutput::new("dummy output")?; 103 | 104 | println!("Midi Ports"); 105 | println!("=========="); 106 | 107 | println!(" Available input ports:"); 108 | for (i, p) in midi_in.ports().iter().enumerate() { 109 | println!(" {}: {}", i, midi_in.port_name(p)?); 110 | } 111 | 112 | println!(" Available output ports:"); 113 | for (i, p) in midi_out.ports().iter().enumerate() { 114 | println!(" {}: {}", i, midi_out.port_name(p)?); 115 | } 116 | 117 | Ok(()) 118 | } 119 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | permissions: write-all 3 | on: [push] 4 | env: 5 | CRATE_NAME: smrec 6 | GITHUB_TOKEN: ${{ github.token }} 7 | RUST_BACKTRACE: 1 8 | jobs: 9 | test: 10 | name: ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} 11 | runs-on: ${{ matrix.platform.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: 16 | # Linux Gnu 17 | - os_name: Linux-x86_64 18 | os: ubuntu-latest 19 | target: x86_64-unknown-linux-gnu 20 | bin: smrec 21 | name: smrec-Linux-x86_64-gnu.tar.gz 22 | # - os_name: Linux-i686 23 | # os: ubuntu-latest 24 | # target: i686-unknown-linux-gnu 25 | # bin: smrec 26 | # name: smrec-Linux-i686-gnu.tar.gz 27 | 28 | # Linux Gnu Arm 29 | - os_name: Linux-aarch64 30 | os: ubuntu-latest 31 | target: aarch64-unknown-linux-gnu 32 | bin: smrec 33 | name: smrec-Linux-aarch64-gnu.tar.gz 34 | - os_name: Linux-armv7 35 | os: ubuntu-latest 36 | target: armv7-unknown-linux-gnueabihf 37 | bin: smrec 38 | name: smrec-Linux-armv7-gnueabihf.tar.gz 39 | 40 | # Windows Arm 41 | - os_name: Windows-aarch64 42 | os: windows-latest 43 | target: aarch64-pc-windows-msvc 44 | bin: smrec.exe 45 | name: smrec-Windows-aarch64.zip 46 | skip_tests: true 47 | 48 | # Windows 49 | - os_name: Windows-i686 50 | os: windows-latest 51 | target: i686-pc-windows-msvc 52 | bin: smrec.exe 53 | name: smrec-Windows-i686.zip 54 | skip_tests: true 55 | - os_name: Windows-x86_64 56 | os: windows-latest 57 | target: x86_64-pc-windows-msvc 58 | bin: smrec.exe 59 | name: smrec-Windows-x86_64.zip 60 | 61 | # macOS 62 | - os_name: macOS-x86_64 63 | os: macOS-latest 64 | target: x86_64-apple-darwin 65 | bin: smrec 66 | name: smrec-Darwin-x86_64.tar.gz 67 | 68 | # macOS Arm 69 | - os_name: macOS-aarch64 70 | os: macOS-latest 71 | target: aarch64-apple-darwin 72 | bin: smrec 73 | name: smrec-Darwin-aarch64.tar.gz 74 | skip_tests: true 75 | toolchain: 76 | - stable 77 | steps: 78 | - uses: actions/checkout@v4 79 | - name: Cache cargo & target directories 80 | uses: Swatinem/rust-cache@v2 81 | with: 82 | key: "v2" 83 | 84 | - name: Configure Git 85 | run: | 86 | git config --global user.email "alisomay@runbox.com" 87 | git config --global user.name "Ali Somay" 88 | 89 | - name: Install alsa & jack libraries 90 | run: sudo apt-get update && sudo apt-get install -y libjack-jackd2-dev libasound2-dev 91 | if: matrix.platform.target == 'x86_64-unknown-linux-gnu' 92 | 93 | # --locked can be added later 94 | - name: Build binary 95 | uses: houseabsolute/actions-rust-cross@v0 96 | with: 97 | command: "build" 98 | target: ${{ matrix.platform.target }} 99 | toolchain: ${{ matrix.toolchain }} 100 | args: "--release" 101 | strip: true 102 | env: 103 | # Build environment variables 104 | GITHUB_ENV: ${{ github.workspace }}/.env 105 | 106 | # --locked can be added later 107 | - name: Run tests 108 | uses: houseabsolute/actions-rust-cross@v0 109 | with: 110 | command: "test" 111 | target: ${{ matrix.platform.target }} 112 | toolchain: ${{ matrix.toolchain }} 113 | args: "--release" 114 | if: ${{ !matrix.platform.skip_tests }} 115 | 116 | - name: Package as archive 117 | shell: bash 118 | run: | 119 | cd target/${{ matrix.platform.target }}/release 120 | if [[ "${{ matrix.platform.os }}" == "windows-latest" ]]; then 121 | 7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} 122 | else 123 | tar czvf ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} 124 | fi 125 | cd - 126 | if: | 127 | matrix.toolchain == 'stable' && 128 | ( startsWith( github.ref, 'refs/tags/v' ) || 129 | github.ref == 'refs/tags/test-release' ) 130 | 131 | - name: Publish release artifacts 132 | uses: actions/upload-artifact@v3 133 | with: 134 | name: smrec-${{ matrix.platform.os_name }} 135 | path: "smrec-*" 136 | if: matrix.toolchain == 'stable' && github.ref == 'refs/tags/test-release' 137 | 138 | - name: Generate SHA-256 139 | run: shasum -a 256 ${{ matrix.platform.name }} 140 | if: | 141 | matrix.toolchain == 'stable' && 142 | matrix.platform.os == 'macOS-latest' && 143 | ( startsWith( github.ref, 'refs/tags/v' ) || 144 | github.ref == 'refs/tags/test-release' ) 145 | 146 | - name: Publish GitHub release 147 | uses: softprops/action-gh-release@v1 148 | with: 149 | draft: true 150 | files: "smrec*" 151 | body_path: CHANGELOG.md 152 | if: matrix.toolchain == 'stable' && startsWith( github.ref, 'refs/tags/v' ) 153 | -------------------------------------------------------------------------------- /src/midi/parse.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | use crate::midi::MidiConfig; 4 | use anyhow::{anyhow, Result}; 5 | use nom::{ 6 | branch::alt, 7 | bytes::complete::take_until, 8 | character::complete::{char, digit1, multispace0}, 9 | combinator::{map, map_res}, 10 | multi::separated_list0, 11 | sequence::{delimited, preceded, tuple}, 12 | IResult, 13 | }; 14 | use std::collections::HashMap; 15 | 16 | /// Parses * or a u8 ranged number 17 | fn parse_u8_or_star(input: &str) -> IResult<&str, u8> { 18 | let star_parser = map(char('*'), |_| 255_u8); 19 | let num_parser = map_res(preceded(multispace0, digit1), str::parse::); 20 | 21 | // Try parsing as a number first, and if it fails, try parsing as the '*' character. 22 | alt((num_parser, star_parser))(input) 23 | } 24 | 25 | /// Parses a u8 ranged number 26 | fn parse_u8(input: &str) -> IResult<&str, u8> { 27 | map_res(preceded(multispace0, digit1), str::parse::)(input) 28 | } 29 | 30 | /// Parses the port name until the first [ 31 | fn parse_port_name(input: &str) -> IResult<&str, &str> { 32 | let (input, _) = multispace0(input)?; // Consume leading spaces 33 | let (input, name) = take_until("[")(input)?; 34 | let (name, _) = name.trim_end().split_at(name.trim_end().len()); // Trim trailing spaces in the port name 35 | Ok((input, name)) 36 | } 37 | 38 | /// Parses channel and its CC numbers a three element tuple (, u8, u8) 39 | fn parse_channel_and_ccs(input: &str) -> IResult<&str, (u8, u8, u8)> { 40 | delimited( 41 | preceded(multispace0, char('(')), 42 | tuple(( 43 | preceded(multispace0, parse_u8_or_star), 44 | preceded( 45 | multispace0, 46 | delimited( 47 | preceded(multispace0, char(',')), 48 | parse_u8, 49 | preceded(multispace0, char(',')), 50 | ), 51 | ), 52 | preceded(multispace0, parse_u8), 53 | )), 54 | preceded(multispace0, char(')')), 55 | )(input) 56 | } 57 | 58 | /// Parse a list of channels and CCs [(..), (..), (..)] 59 | fn parse_list(input: &str) -> IResult<&str, Vec<(u8, u8, u8)>> { 60 | delimited( 61 | preceded(multispace0, char('[')), 62 | separated_list0(preceded(multispace0, char(',')), parse_channel_and_ccs), 63 | preceded(multispace0, char(']')), 64 | )(input) 65 | } 66 | 67 | /// Parses an entire port configuration 68 | fn parse_port(input: &str) -> IResult<&str, (&str, Vec<(u8, u8, u8)>)> { 69 | // Consume leading spaces 70 | let (input, _) = multispace0(input)?; 71 | 72 | // Parse port name 73 | let (input, port_name) = parse_port_name(input)?; 74 | 75 | // Consume characters until the next opening bracket `[` 76 | let (input, _) = take_until("[")(input)?; 77 | 78 | // Parse the list of channels and CCs 79 | let (input, channels_and_ccs) = parse_list(input)?; 80 | 81 | Ok((input, (port_name, channels_and_ccs))) 82 | } 83 | 84 | /// Parses the complete MIDI input or output configuration 85 | fn parse_midi_config_raw(input: &str) -> IResult<&str, Vec<(&str, Vec<(u8, u8, u8)>)>> { 86 | delimited( 87 | preceded(multispace0, char('[')), 88 | separated_list0(preceded(multispace0, char(',')), parse_port), 89 | preceded(multispace0, char(']')), 90 | )(input) 91 | } 92 | 93 | /// Parses the [`MidiConfig`] from the provided configuration string. 94 | pub fn parse_midi_config(input: &str) -> Result { 95 | let mut map: HashMap> = HashMap::new(); 96 | let (_, port_configs) = 97 | parse_midi_config_raw(input).map_err(|_| anyhow!("Can not parse provided MIDI config."))?; 98 | for (name, channel_configs) in port_configs { 99 | map.insert(name.to_string(), channel_configs); 100 | } 101 | Ok(MidiConfig(map)) 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::*; 107 | 108 | #[test] 109 | fn test_parse_u8() { 110 | assert_eq!(parse_u8("23"), Ok(("", 23))); 111 | assert_eq!(parse_u8(" 23"), Ok(("", 23))); 112 | assert_eq!(parse_u8("0"), Ok(("", 0))); 113 | assert!(parse_u8("256").is_err()); 114 | } 115 | 116 | #[test] 117 | fn test_parse_u8_or_star() { 118 | assert_eq!(parse_u8_or_star("23"), Ok(("", 23))); 119 | assert_eq!(parse_u8_or_star(" 23"), Ok(("", 23))); 120 | assert_eq!(parse_u8_or_star("0"), Ok(("", 0))); 121 | assert_eq!(parse_u8_or_star("*"), Ok(("", 255))); 122 | assert!(parse_u8_or_star("256").is_err()); 123 | } 124 | 125 | #[test] 126 | fn test_parse_port_name() { 127 | assert_eq!(parse_port_name("some port["), Ok(("[", "some port"))); 128 | assert_eq!( 129 | parse_port_name(" spaced port ["), 130 | Ok(("[", "spaced port")) 131 | ); 132 | } 133 | 134 | #[test] 135 | fn test_parse_channel_and_ccs() { 136 | assert_eq!(parse_channel_and_ccs("(1,23,44)"), Ok(("", (1, 23, 44)))); 137 | assert_eq!( 138 | parse_channel_and_ccs("(1 , 23 , 44)"), 139 | Ok(("", (1, 23, 44))) 140 | ); 141 | assert_eq!(parse_channel_and_ccs(" ( 1 , 2 , 3 )"), Ok(("", (1, 2, 3)))); 142 | } 143 | 144 | #[test] 145 | fn test_parse_port() { 146 | let expected = ("", ("some port", vec![(1, 23, 44), (12, 5, 6), (9, 0, 1)])); 147 | assert_eq!( 148 | parse_port("some port[(1,23,44), (12, 5, 6), (9, 0,1)]"), 149 | Ok(expected) 150 | ); 151 | } 152 | 153 | #[test] 154 | fn test_parse_midi_config_raw() { 155 | let expected = Ok(( 156 | "", 157 | vec![ 158 | ("some port", vec![(1, 23, 44), (12, 5, 6), (9, 0, 1)]), 159 | ("another port", vec![(4, 55, 44)]), 160 | ("maybe another", vec![(2, 44, 33)]), 161 | ], 162 | )); 163 | 164 | assert_eq!( 165 | parse_midi_config_raw("[some port[(1,23,44), (12, 5, 6), (9, 0,1)], another port[(4,55, 44)],maybe another[(2,44,33)]]"), 166 | expected 167 | ); 168 | 169 | // With more spaces 170 | let expected = Ok(("", vec![("a very spaced port", vec![(1, 2, 3)])])); 171 | 172 | assert_eq!( 173 | parse_midi_config_raw("[ a very spaced port [ ( 1 , 2 , 3 ) ] ]"), 174 | expected 175 | ); 176 | } 177 | 178 | #[test] 179 | fn test_parse_list() { 180 | let expected = Ok(("", vec![(1, 23, 44), (12, 5, 6), (9, 0, 1)])); 181 | assert_eq!(parse_list("[(1,23,44), (12, 5, 6), (9, 0,1)]"), expected); 182 | } 183 | 184 | #[test] 185 | fn test_trailing_and_leading_spaces() { 186 | let input = "[ spaced port [ ( 1 , 2 , 3 ) , (4 ,5, 6) ] ]"; 187 | let result = parse_midi_config_raw(input); 188 | assert_eq!( 189 | result, 190 | Ok(("", vec![("spaced port", vec![(1, 2, 3), (4, 5, 6)])])) 191 | ); 192 | } 193 | 194 | #[test] 195 | fn test_special_chars_in_port_names() { 196 | let input = "[portname!@#[(1,2,3)]]"; 197 | let result = parse_midi_config_raw(input); 198 | assert_eq!(result, Ok(("", vec![("portname!@#", vec![(1, 2, 3)])]))); 199 | } 200 | 201 | #[test] 202 | fn test_star_in_tuple() { 203 | let input = "[port_name[(*,2,3), (4,5,6)]]"; 204 | let result = parse_midi_config_raw(input); 205 | assert_eq!( 206 | result, 207 | Ok(("", vec![("port_name", vec![(255, 2, 3), (4, 5, 6)])])) 208 | ); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/osc.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Action; 2 | use anyhow::Result; 3 | use rosc::{encoder::encode, OscMessage, OscPacket, OscType}; 4 | use std::{ 5 | net::{SocketAddr, UdpSocket}, 6 | str::FromStr, 7 | sync::Arc, 8 | }; 9 | 10 | pub struct Osc { 11 | sender_socket: Arc, 12 | receiver_socket: Arc, 13 | sender_channel: crossbeam::channel::Sender, 14 | receiver_channel: crossbeam::channel::Receiver, 15 | udp_thread: Option>, 16 | messaging_thread: Option>, 17 | } 18 | 19 | impl Osc { 20 | pub fn new( 21 | osc_config: &[String], 22 | sender_channel: crossbeam::channel::Sender, 23 | receiver_channel: crossbeam::channel::Receiver, 24 | ) -> Result { 25 | let recv_addr = if let Some(addr) = osc_config.get(0) { 26 | SocketAddr::from_str(addr)? 27 | } else { 28 | // Listen to all network and a random port by default. 29 | SocketAddr::from(([0, 0, 0, 0], 0)) 30 | }; 31 | 32 | let send_addr = if let Some(addr) = osc_config.get(1) { 33 | SocketAddr::from_str(addr)? 34 | } else { 35 | SocketAddr::from(([127, 0, 0, 1], 0)) 36 | }; 37 | 38 | let sender_socket = Arc::new( 39 | // We're binding to build the socket, we don't care about the address because we're not going to listen. 40 | UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], 0))) 41 | .unwrap_or_else(|_| panic!("Failed to bind socket to address {send_addr}")), 42 | ); 43 | 44 | // The address we're going to send to. 45 | sender_socket 46 | .connect(send_addr) 47 | .unwrap_or_else(|_| panic!("Failed to connect socket to address {send_addr}")); 48 | 49 | match send_addr.ip() { 50 | std::net::IpAddr::V4(addr) => { 51 | if addr.is_broadcast() { 52 | if let Err(err) = sender_socket.set_broadcast(true) { 53 | eprintln!("Error setting socket to broadcast: {err}"); 54 | } 55 | } 56 | } 57 | std::net::IpAddr::V6(_) => { 58 | panic!("IPv6 is not supported yet.") 59 | } 60 | } 61 | 62 | match send_addr.ip() { 63 | std::net::IpAddr::V4(addr) => { 64 | if addr.is_broadcast() { 65 | if let Err(err) = sender_socket.set_broadcast(true) { 66 | eprintln!("Error setting socket to broadcast: {err}"); 67 | } 68 | } 69 | } 70 | std::net::IpAddr::V6(_) => { 71 | panic!("IPv6 is not supported yet.") 72 | } 73 | } 74 | 75 | let receiver_socket = Arc::new( 76 | UdpSocket::bind(recv_addr) 77 | .unwrap_or_else(|_| panic!("Failed to bind socket to address {recv_addr}")), 78 | ); 79 | 80 | println!("Will be sending OSC messages to {send_addr}"); 81 | println!( 82 | "Listening for OSC messages on {}", 83 | receiver_socket.local_addr()? 84 | ); 85 | 86 | Ok(Self { 87 | sender_socket, 88 | receiver_socket, 89 | sender_channel, 90 | receiver_channel, 91 | udp_thread: None, 92 | messaging_thread: None, 93 | }) 94 | } 95 | 96 | pub fn listen(&mut self) { 97 | if self.messaging_thread.is_none() { 98 | let socket = self.sender_socket.clone(); 99 | let receiver_channel = self.receiver_channel.clone(); 100 | self.messaging_thread = Some(std::thread::spawn(move || loop { 101 | match receiver_channel.recv() { 102 | Ok(Action::Start) => { 103 | if let Err(err) = socket.send( 104 | &encode(&OscPacket::Message(OscMessage { 105 | addr: "/smrec/start".to_string(), 106 | args: Vec::new(), 107 | })) 108 | .expect("OSC packet should encode."), 109 | ) { 110 | eprintln!("Error sending OSC packet: {err}"); 111 | }; 112 | } 113 | Ok(Action::Stop) => { 114 | if let Err(err) = socket.send( 115 | &encode(&OscPacket::Message(OscMessage { 116 | addr: "/smrec/stop".to_string(), 117 | args: Vec::new(), 118 | })) 119 | .expect("OSC packet should encode."), 120 | ) { 121 | eprintln!("Error sending OSC packet: {err}"); 122 | }; 123 | } 124 | Ok(Action::Err(err)) => { 125 | if let Err(err) = socket.send( 126 | &encode(&OscPacket::Message(OscMessage { 127 | addr: "/smrec/error".to_string(), 128 | args: vec![OscType::String(err)], 129 | })) 130 | .expect("OSC packet should encode."), 131 | ) { 132 | eprintln!("Error sending OSC packet: {err}"); 133 | }; 134 | } 135 | Err(err) => { 136 | eprintln!("Error receiving from channel: {err}"); 137 | } 138 | } 139 | })); 140 | } 141 | 142 | if self.udp_thread.is_none() { 143 | let socket = self.receiver_socket.clone(); 144 | let sender_channel = self.sender_channel.clone(); 145 | self.udp_thread = Some(std::thread::spawn(move || { 146 | let mut buf = [0u8; rosc::decoder::MTU]; 147 | 148 | loop { 149 | match socket.recv_from(&mut buf) { 150 | Ok((size, _addr)) => match rosc::decoder::decode_udp(&buf[..size]) { 151 | Ok((_, osc_packet)) => { 152 | handle_packet(&osc_packet, &sender_channel); 153 | } 154 | Err(err) => { 155 | eprintln!("Error decoding UDP packet: {err}"); 156 | } 157 | }, 158 | Err(err) => { 159 | eprintln!("Error receiving from socket: {err}"); 160 | } 161 | } 162 | } 163 | })); 164 | } 165 | } 166 | } 167 | 168 | fn handle_packet(packet: &OscPacket, channel: &crossbeam::channel::Sender) { 169 | match packet { 170 | OscPacket::Message(message) => { 171 | handle_message(message, channel); 172 | } 173 | OscPacket::Bundle(bundle) => { 174 | bundle 175 | .content 176 | .iter() 177 | .for_each(|packet| handle_packet(packet, channel)); 178 | } 179 | } 180 | } 181 | 182 | fn handle_message(message: &OscMessage, channel: &crossbeam::channel::Sender) { 183 | match message.addr.as_str() { 184 | "/smrec/start" => { 185 | channel.send(Action::Start).unwrap(); 186 | } 187 | "/smrec/stop" => { 188 | channel.send(Action::Stop).unwrap(); 189 | } 190 | _ => { 191 | // Ignore 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /scripts/pre-build-win.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [switch]$ci 3 | ) 4 | 5 | Write-Output "=============================================================================" 6 | Write-Output "Pre build script for cpal asio feature." 7 | Write-Output "Make sure that you have sourced this script instead of directly executing it or if in CI environment, pass the --ci flag." 8 | Write-Output "=============================================================================" 9 | 10 | function Write-Env { 11 | <# 12 | .SYNOPSIS 13 | Sets an environment variable either in the current process or in the GITHUB_ENV file. 14 | 15 | .DESCRIPTION 16 | This function sets an environment variable. If the --ci switch is specified when 17 | running the script, it writes the environment variable to the GITHUB_ENV file 18 | so it is available to subsequent steps in a GitHub Actions workflow. Otherwise, 19 | it sets the environment variable in the current process. 20 | 21 | .PARAMETER name 22 | The name of the environment variable to set. 23 | 24 | .PARAMETER value 25 | The value to set the environment variable to. 26 | 27 | .EXAMPLE 28 | Write-Env "MY_VARIABLE" "Some Value" 29 | 30 | This example sets the MY_VARIABLE environment variable to "Some Value" in the current process. 31 | 32 | .EXAMPLE 33 | .\pre-build-win.ps1 --ci 34 | Write-Env "MY_VARIABLE" "Some Value" 35 | 36 | This example, when run within the script with the --ci switch, writes the MY_VARIABLE 37 | environment variable to the GITHUB_ENV file with a value of "Some Value". 38 | #> 39 | param ( 40 | [string]$name, 41 | [string]$value 42 | ) 43 | if ($ci) { 44 | Write-Output "$name=$value" | Out-File -FilePath $env:GITHUB_ENV -Append 45 | } 46 | else { 47 | Write-Output "Setting $name=$value" 48 | [System.Environment]::SetEnvironmentVariable($name, $value, "Process") 49 | } 50 | } 51 | 52 | function Invoke-VcVars { 53 | <# 54 | .SYNOPSIS 55 | This function sets the Visual Studio build environment for the current session. 56 | 57 | .DESCRIPTION 58 | The function first determines the system architecture. It then searches for the vcvarsall.bat file 59 | specific to the detected architecture and executes it to set the Visual Studio build environment 60 | for the current session. 61 | #> 62 | 63 | # Determine the system architecture 64 | Write-Output "Determining system architecture..." 65 | $arch = if ([Environment]::Is64BitOperatingSystem) { 66 | switch -Wildcard ((Get-CimInstance -ClassName Win32_Processor).Description) { 67 | "*ARM64*" { "arm64" } 68 | "*ARM*" { "arm" } 69 | default { "amd64" } 70 | } 71 | } 72 | else { 73 | "x86" 74 | } 75 | 76 | Write-Output "Architecture detected as $arch." 77 | 78 | 79 | # Define search paths based on architecture 80 | # Will be overridden if CI flag is set 81 | $paths = @('C:\Program Files (x86)\Microsoft Visual Studio\', 'C:\Program Files\Microsoft Visual Studio\') 82 | 83 | # Find Visual Studio version number (only when CI flag is set) 84 | # TODO: This can be more robust and improve.. but later.. 85 | if ($ci) { 86 | Write-Output "Searching for Visual Studio version number..." 87 | $vsVersion = Get-ChildItem 'C:\Program Files (x86)\Microsoft Visual Studio\' -Directory | 88 | Where-Object { $_.Name -match '\d{4}' } | 89 | Select-Object -ExpandProperty Name -First 1 90 | 91 | if (-not $vsVersion) { 92 | $vsVersion = Get-ChildItem 'C:\Program Files\Microsoft Visual Studio\' -Directory | 93 | Where-Object { $_.Name -match '\d{4}' } | 94 | Select-Object -ExpandProperty Name -First 1 95 | } 96 | 97 | if ($vsVersion) { 98 | Write-Output "Visual Studio version $vsVersion detected." 99 | $paths = 100 | @( 101 | "C:\Program Files (x86)\Microsoft Visual Studio\$vsVersion\Community\VC\Auxiliary\Build\", 102 | "C:\Program Files\Microsoft Visual Studio\$vsVersion\Community\VC\Auxiliary\Build\", 103 | "C:\Program Files (x86)\Microsoft Visual Studio\$vsVersion\Professional\VC\Auxiliary\Build\", 104 | "C:\Program Files\Microsoft Visual Studio\$vsVersion\Professional\VC\Auxiliary\Build\", 105 | "C:\Program Files (x86)\Microsoft Visual Studio\$vsVersion\Enterprise\VC\Auxiliary\Build\", 106 | "C:\Program Files\Microsoft Visual Studio\$vsVersion\Enterprise\VC\Auxiliary\Build\" 107 | ) 108 | 109 | } 110 | else { 111 | Write-Output "Visual Studio version not detected. Proceeding with original search paths." 112 | $paths = @('C:\Program Files (x86)\Microsoft Visual Studio\', 'C:\Program Files\Microsoft Visual Studio\') 113 | } 114 | } 115 | 116 | 117 | # Search for vcvarsall.bat and execute the first instance found with the appropriate architecture argument 118 | Write-Output "Searching for vcvarsall.bat..." 119 | foreach ($path in $paths) { 120 | $vcvarsPath = Get-ChildItem $path -Recurse -Filter vcvarsall.bat -ErrorAction SilentlyContinue | Select-Object -First 1 121 | if ($vcvarsPath) { 122 | Write-Output "Found vcvarsall.bat at $($vcvarsPath.FullName). Initializing environment..." 123 | $cmdOutput = cmd /c """$($vcvarsPath.FullName)"" $arch && set" 124 | foreach ($line in $cmdOutput) { 125 | if ($line -match "^(.*?)=(.*)$") { 126 | $varName = $matches[1] 127 | $varValue = $matches[2] 128 | Write-Env $varName $varValue 129 | } 130 | } 131 | return 132 | } 133 | } 134 | 135 | Write-Error "Error: Could not find vcvarsall.bat. Please install the latest version of Visual Studio." 136 | exit 1 137 | } 138 | 139 | # Main script begins here 140 | 141 | # Ensure execution policy allows for script execution 142 | if (-not (Get-ExecutionPolicy -Scope CurrentUser) -eq "Unrestricted") { 143 | Set-ExecutionPolicy -Scope CurrentUser Unrestricted 144 | } 145 | 146 | # Check if running on Windows 147 | if ($env:OS -match "Windows") { 148 | Write-Output "Detected Windows OS." 149 | 150 | # Directory to store the ASIO SDK 151 | $out_dir = [System.IO.Path]::GetTempPath() 152 | $asio_dir = Join-Path $out_dir "asio_sdk" 153 | 154 | if (-not (Test-Path $asio_dir)) { 155 | Write-Output "ASIO SDK not found. Downloading..." 156 | 157 | # Download the ASIO SDK 158 | $asio_zip_path = Join-Path $out_dir "asio_sdk.zip" 159 | Invoke-WebRequest -Uri "https://www.steinberg.net/asiosdk" -OutFile $asio_zip_path 160 | 161 | # Unzip the ASIO SDK 162 | Write-Output "Unzipping ASIO SDK..." 163 | Expand-Archive -Path $asio_zip_path -DestinationPath $out_dir -Force 164 | 165 | # Move the contents of the inner directory (like asiosdk_2.3.3_2019-06-14) to $asio_dir 166 | $innerDir = Get-ChildItem -Path $out_dir -Directory | Where-Object { $_.Name -match 'asio.*' } | Select-Object -First 1 167 | Move-Item -Path "$($innerDir.FullName)\*" -Destination $asio_dir -Force 168 | } 169 | else { 170 | Write-Output "ASIO SDK already exists. Skipping download." 171 | } 172 | 173 | # Set the CPAL_ASIO_DIR environment variable 174 | Write-Output "Setting CPAL_ASIO_DIR environment variable..." 175 | Write-Env "CPAL_ASIO_DIR" $asio_dir 176 | 177 | # Check if LIBCLANG_PATH is set 178 | if (-not $env:LIBCLANG_PATH) { 179 | Write-Error "Error: LIBCLANG_PATH is not set!" 180 | Write-Output "Please ensure LLVM is installed and set the LIBCLANG_PATH environment variable." 181 | exit 1 182 | } 183 | else { 184 | Write-Output "LIBCLANG_PATH is set to $env:LIBCLANG_PATH." 185 | } 186 | 187 | # Run the vcvars function 188 | Invoke-VcVars 189 | 190 | Write-Output "Environment is ready for build." 191 | } 192 | else { 193 | Write-Output "This setup script is intended for Windows only." 194 | } 195 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::{wav::spec_from_config, WriterHandles}; 2 | use anyhow::{anyhow, bail, Result}; 3 | use camino::Utf8PathBuf; 4 | use chrono::{Datelike, Timelike, Utc}; 5 | use cpal::{ 6 | traits::{DeviceTrait, HostTrait}, 7 | SupportedStreamConfig, 8 | }; 9 | use serde::{ 10 | de::{self, Deserializer, MapAccess, Visitor}, 11 | Deserialize, 12 | }; 13 | use std::{ 14 | collections::HashMap, 15 | fmt, 16 | str::FromStr, 17 | sync::{Arc, Mutex}, 18 | }; 19 | 20 | /// Chooses which channels to record. 21 | pub fn choose_channels_to_record( 22 | include: Option>, 23 | exclude: Option>, 24 | config: &cpal::SupportedStreamConfig, 25 | ) -> Result> { 26 | match (include, exclude) { 27 | // Includes only the channels in the list. 28 | (Some(include), None) => Ok(include.iter().map(|i| i - 1).collect()), 29 | // Includes all channels but excludes the ones in the list. 30 | (None, Some(exclude)) => { 31 | let mut channels = (0..config.channels() as usize).collect::>(); 32 | let exclude = exclude.iter().map(|i| i - 1).collect::>(); 33 | 34 | for channel in exclude { 35 | if let Some(pos) = channels.iter().position(|i| *i == channel) { 36 | channels.remove(pos); 37 | } else { 38 | bail!( 39 | "Channel {} is meant to be excluded but it does not exist.", 40 | channel + 1 41 | ); 42 | } 43 | } 44 | 45 | Ok(channels) 46 | } 47 | (Some(_), Some(_)) => bail!("Using --exclude and --include together is not allowed."), 48 | // Includes all channels by default. 49 | (None, None) => Ok((0..config.channels() as usize).collect()), 50 | } 51 | } 52 | 53 | /// Chooses the host to use. 54 | pub fn choose_host(host: Option) -> Result { 55 | #[cfg(target_os = "windows")] 56 | if host.as_ref().map_or(false, |host| host == "Asio") { 57 | return Ok(cpal::host_from_id(cpal::HostId::Asio).expect("Failed to initialise ASIO host.")); 58 | } 59 | 60 | if let Some(chosen_host_name) = host { 61 | let available_hosts = cpal::available_hosts(); 62 | let host_id = available_hosts 63 | .iter() 64 | .find(|host_id| host_id.name() == chosen_host_name); 65 | if let Some(host_id) = host_id { 66 | cpal::host_from_id(*host_id).map_err(|e| anyhow::anyhow!(e)) 67 | } else { 68 | bail!("Provided host {chosen_host_name} was not found.") 69 | } 70 | } else { 71 | // Use the default host when not provided. 72 | Ok(cpal::default_host()) 73 | } 74 | } 75 | 76 | /// Chooses the device to use. 77 | pub fn choose_device(host: &cpal::Host, device: Option) -> Result { 78 | if let Some(chosen_device_name) = device { 79 | let devices = host.devices()?; 80 | let device = devices 81 | .enumerate() 82 | .find(|(_device_index, device)| device.name().expect("Later") == chosen_device_name); 83 | if let Some((_, device)) = device { 84 | Ok(device) 85 | } else { 86 | bail!("Provided device {chosen_device_name} not found.") 87 | } 88 | } else { 89 | // Try to use the default device when not provided. 90 | host.default_input_device() 91 | .ok_or_else(|| anyhow::anyhow!("No default audio device found.")) 92 | } 93 | } 94 | 95 | #[derive(Deserialize, Clone, Debug)] 96 | pub struct SmrecConfig { 97 | #[serde(deserialize_with = "deserialize_usize_keys_greater_than_0")] 98 | channel_names: HashMap, 99 | #[serde(skip)] 100 | channels_to_record: Vec, 101 | #[serde(skip)] 102 | out_path: Option, 103 | #[serde(skip)] 104 | cpal_stream_config: Option, 105 | } 106 | 107 | impl SmrecConfig { 108 | pub fn new( 109 | config_path: Option, 110 | out_path: Option, 111 | channels_to_record: Vec, 112 | cpal_stream_config: SupportedStreamConfig, 113 | ) -> Result { 114 | let current_dir_config = Utf8PathBuf::from("./.smrec/config.toml"); 115 | 116 | let path = if let Some(path) = config_path { 117 | Utf8PathBuf::from_str(&path)? 118 | } else if current_dir_config.exists() { 119 | current_dir_config 120 | } else { 121 | Utf8PathBuf::from_path_buf( 122 | home::home_dir().ok_or_else(|| anyhow!("User home directory was not found."))?, 123 | ) 124 | .map_err(|buf| { 125 | anyhow!( 126 | "User home directory is not an Utf8 path. : {}", 127 | buf.display() 128 | ) 129 | })? 130 | .join(".smrec") 131 | .join("config.toml") 132 | }; 133 | 134 | if path.exists() { 135 | let config = std::fs::read_to_string(path)?; 136 | let mut config: Self = toml::from_str(&config)?; 137 | config.channels_to_record = channels_to_record; 138 | 139 | config.channels_to_record.iter().for_each(|channel| { 140 | if config.channel_names.contains_key(&(channel + 1)) { 141 | let name = config.channel_names.get(&(channel + 1)).unwrap(); 142 | if !std::path::Path::new(name) 143 | .extension() 144 | .map_or(false, |ext| ext.eq_ignore_ascii_case("wav")) 145 | { 146 | config 147 | .channel_names 148 | .insert(*channel + 1, format!("{name}.wav")); 149 | } 150 | } else { 151 | config 152 | .channel_names 153 | .insert(*channel + 1, format!("chn_{}.wav", channel + 1)); 154 | } 155 | }); 156 | config.cpal_stream_config = Some(cpal_stream_config); 157 | config.out_path = out_path; 158 | return Ok(config); 159 | } 160 | 161 | let mut channel_names = HashMap::new(); 162 | for channel in &channels_to_record { 163 | channel_names.insert(*channel + 1, format!("chn_{}.wav", channel + 1)); 164 | } 165 | Ok(Self { 166 | channel_names, 167 | channels_to_record, 168 | out_path, 169 | cpal_stream_config: Some(cpal_stream_config), 170 | }) 171 | } 172 | 173 | pub fn supported_cpal_stream_config(&self) -> SupportedStreamConfig { 174 | self.cpal_stream_config.clone().unwrap() 175 | } 176 | 177 | pub fn channels_to_record(&self) -> &[usize] { 178 | &self.channels_to_record 179 | } 180 | 181 | pub fn channel_count(&self) -> usize { 182 | self.channels_to_record.len() 183 | } 184 | 185 | pub fn get_channel_name_from_0_indexed_channel_num(&self, index: usize) -> Result { 186 | Ok(self 187 | .channel_names 188 | .get(&(index + 1)) 189 | .ok_or_else(|| anyhow!("Channel {} does not exist.", index + 1))? 190 | .to_string()) 191 | } 192 | 193 | pub fn writers(&self) -> Result { 194 | let now = Utc::now(); 195 | 196 | // Format the date for YYYYMMDD_HHMMSS 197 | let dirname_date = format!( 198 | "{:04}{:02}{:02}_{:02}{:02}{:02}", 199 | now.year(), 200 | now.month(), 201 | now.day(), 202 | now.hour(), 203 | now.minute(), 204 | now.second() 205 | ); 206 | 207 | // Stamp base directory with date. 208 | let base = if let Some(out) = &self.out_path { 209 | Utf8PathBuf::from_str(out)? 210 | } else { 211 | Utf8PathBuf::from(".") 212 | }; 213 | 214 | if !base.exists() { 215 | bail!("Output path which is provided {base} does not exist."); 216 | } 217 | 218 | let base = base.join(format!("rec_{dirname_date}")); 219 | 220 | // Create the base directory if it does not exist. 221 | if !base.exists() { 222 | std::fs::create_dir_all(&base)?; 223 | } 224 | 225 | // Make writers. 226 | let mut writers = Vec::new(); 227 | for channel_num in &self.channels_to_record { 228 | let name = self.get_channel_name_from_0_indexed_channel_num(*channel_num)?; 229 | let spec = spec_from_config(&self.supported_cpal_stream_config()); 230 | let writer = hound::WavWriter::create(base.join(&name), spec) 231 | .expect("Failed to create wav writer."); 232 | writers.push(Arc::new(Mutex::new(Some(writer)))); 233 | } 234 | 235 | Ok(Arc::new(writers)) 236 | } 237 | } 238 | 239 | fn deserialize_usize_keys_greater_than_0<'de, D>( 240 | deserializer: D, 241 | ) -> Result, D::Error> 242 | where 243 | D: Deserializer<'de>, 244 | { 245 | struct UsizeKeyVisitor; 246 | 247 | impl<'de> Visitor<'de> for UsizeKeyVisitor { 248 | type Value = HashMap; 249 | 250 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 251 | formatter.write_str("a map with string keys that represent usizes") 252 | } 253 | 254 | fn visit_map(self, mut access: M) -> Result 255 | where 256 | M: MapAccess<'de>, 257 | { 258 | let mut map = HashMap::with_capacity(access.size_hint().unwrap_or(0)); 259 | while let Some((key, value)) = access.next_entry::()? { 260 | let usize_key = key.parse::().map_err(de::Error::custom)?; 261 | if usize_key < 1 { 262 | return Err(de::Error::custom( 263 | "a usize key must be greater than or equal to 1", 264 | )); 265 | } 266 | map.insert(usize_key, value); 267 | } 268 | Ok(map) 269 | } 270 | } 271 | 272 | deserializer.deserialize_map(UsizeKeyVisitor) 273 | } 274 | 275 | #[cfg(test)] 276 | mod tests { 277 | use super::*; 278 | 279 | #[test] 280 | fn deserialize_external_config() { 281 | let config: &str = r#" 282 | [channel_names] 283 | 1 = "channel_1" 284 | 2 = "channel_2" 285 | 3 = "channel_3" 286 | 4 = "channel_4" 287 | 5 = "channel_5" 288 | 6 = "channel_6" 289 | 7 = "channel_7" 290 | 8 = "channel_8" 291 | "#; 292 | 293 | let config: SmrecConfig = toml::from_str(config).unwrap(); 294 | 295 | config.channel_names.iter().for_each(|(key, value)| { 296 | assert_eq!(key.to_string(), value.replace("channel_", "")); 297 | }); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # smrec 6 | 7 | Minimalist multi-track audio recorder which may be controlled via OSC or MIDI. 8 | 9 | I did this because I needed a simple multi-track audio recorder which I could control via OSC or MIDI. 10 | 11 | I didn't want and do not have the resources to use a DAW for this purpose in my setup and wanted mono wave files for each channel organized in a directory per recording by date and time 12 | 13 | I'm using this recorder in a setup where I use a [Behringer XR18](https://www.behringer.com/product.html?modelCode=P0BI8) as an audio interface and a [LattePanda 3 Delta](https://www.lattepanda.com/lattepanda-3-delta) as a SBC. 14 | 15 | Now let's record some sound! 🔔 16 | 17 | --- 18 | 19 | ## Installation 20 | 21 | ``` 22 | cargo install smrec 23 | ``` 24 | 25 | ### Installing for Windows and ASIO support 26 | 27 | `smrec` uses [`cpal`](https://github.com/RustAudio/cpal) as the underlying audio API. `cpal` supports WASAPI, DirectSound and ASIO on Windows. However, since `cargo` builds binaries from source in the target machine and it is [not very straight forward](https://github.com/RustAudio/cpal#asio-on-windows) to build `cpal` with ASIO support due to `asio-sys` build script, **there is a pre-build script provided** in this repository. 28 | 29 | To install `smrec` on Windows, please follow these steps in order: 30 | 31 | - Install the latest Visual Studio (if you don't have it already) 32 | - Install the latest LLVM (if you don't have it already) from [here](https://releases.llvm.org/download.html) 33 | - Open Command Prompt as administrator 34 | - Set LLVM path in the environment variables (system wide) 35 | ``` 36 | setx /M LIBCLANG_PATH "C:\Program Files\LLVM\bin" 37 | ``` 38 | - Open PowerShell as administrator 39 | - Check if the environment variable is set correctly 40 | ``` 41 | $env:LIBCLANG_PATH 42 | ``` 43 | - Source the pre-build script in the repository (assuming you are in the root of the repository) 44 | ``` 45 | . .\pre-build-win.ps1 46 | ``` 47 | this script will download the ASIO SDK, set Visual Studio environment variables and `CPAL_ASIO_DIR` variable for the current shell session. 48 | - Install as usual 49 | ``` 50 | cargo install smrec 51 | ``` 52 | 53 | If you know what you're doing feel free to skip these steps and consult the [`cpal` documentation](https://github.com/RustAudio/cpal#asio-on-windows). 54 | 55 | ### Pre-built binaries 56 | 57 | Pre-built binaries as an alternative are available for Windows [here](https://github.com/alisomay/smrec/releases) due to the complicated process of building `cpal` with ASIO support on Windows currently. 58 | 59 | ## Tutorial 60 | 61 | ### Simply as a command 62 | 63 | ``` 64 | smrec 65 | ``` 66 | 67 | Runs with the default configuration which is: 68 | 69 | - The default audio host 70 | - The default audio device 71 | - The default set of input channels 72 | - The default directory to record which is the current working directory 73 | - The recording goes on until `ctrl+c` is pressed and the program is interrupted. 74 | - Creates a directory named `rec_YYYYMMDD_HHMMSS` in the current working directory and records the audio in that directory. 75 | - The audio is recorded in `wav` format. 76 | - The audio is recorded in the default sample rate and buffer size and sample format of the audio device. 77 | - For every channel a separate file is created (mono) and the file name for each is `chn_XX.wav` where `XX` is the channel number. 78 | 79 | To record for a specific duration, use the `--duration` flag and specify the duration in seconds. 80 | The following command records for 10 seconds: 81 | 82 | ``` 83 | smrec --duration 10 84 | ``` 85 | 86 | By using the `--host` and `--device` flag , you can specify the audio host and device to use. The following command uses `MacBook Pro Microphone` as the audio device: 87 | 88 | ``` 89 | smrec --device "MacBook Pro Microphone" 90 | ``` 91 | 92 | #### Listing midi ports and audio hosts and devices 93 | 94 | ``` 95 | smrec list 96 | ``` 97 | 98 | #### Including and excluding channels from a recording 99 | 100 | By default, all channels of the audio device are recorded. You can specify which channels to include or exclude from the recording by using the `--include` and `--exclude` flags. These flags can not be used together. The following command records only the first two channels of a 4 channel audio device: 101 | 102 | ``` 103 | smrec --include 1,2 104 | ``` 105 | 106 | And this command records all channels except the first channel of a 4 channel audio device: 107 | 108 | ``` 109 | smrec --exclude 1 110 | ``` 111 | 112 | as seen in the examples, the channel numbers start from 1 and they can be specified as a comma separated list. 113 | 114 | #### Recording to a specific directory 115 | 116 | By default, the recording is done in the current working directory. You can specify a directory to record to by using the `--directory` flag. The following command records to the `~/Music` directory: 117 | 118 | ``` 119 | smrec --out ~/Music 120 | ``` 121 | 122 | #### Configuring with a configuration file 123 | 124 | `smrec` uses the cli arguments for configuration and they precede everything. However, you can configure some aspects (probably more to come) of `smrec` by using a configuration file so they replace the default configuration. The configuration file is a `toml` file and it is named `config.toml`. The configuration file is searched in the following order: 125 | 126 | - `.smrec/config.toml` in the current working directory. 127 | - `.smrec/config.toml` in the user home directory. 128 | - If none of the above is found, the default configuration is used. 129 | 130 | The configuration file can configure: 131 | 132 | - Channel names 133 | 134 | ```toml 135 | [channel_names] 136 | 1 = "Kick.wav" 137 | 2 = "Snare.wav" 138 | 3 = "Hi-Hat.wav" 139 | ``` 140 | 141 | - More to come.. 142 | 143 | ### OSC control 144 | 145 | `smrec` normally starts recording as soon as it is run. However it also has options for various control methods. 146 | 147 | Running, `smrec --osc` will not start recording immediately but instead it will wait for an OSC message to start recording. 148 | The default OSC port for receiving and sending is chosen randomly by the os and the default addresses for sending and receiving is `127.0.0.1` and `0.0.0.0`. 149 | After running the command above, the output might look like this: 150 | 151 | ``` 152 | Will be sending OSC messages to 127.0.0.1:61213 153 | Listening for OSC messages on 0.0.0.0:51014 154 | ``` 155 | 156 | Currently `smrec` does not support IPv6. 157 | 158 | In the default configuration: 159 | 160 | - Listens for OSC messages on a randomly chosen port on all addresses. 161 | - Sends OSC messages to localhost on a randomly chosen port. 162 | 163 | To configure OSC further arguments could be added to the flag: 164 | 165 | ``` 166 | smrec --osc ":;:" 167 | ``` 168 | 169 | or 170 | 171 | ``` 172 | smrec --osc ":" 173 | ``` 174 | 175 | the second form would keep the default send address and port. 176 | 177 | ``` 178 | smrec --osc "0.0.0.0:18000;255.255.255.255:18001" 179 | ``` 180 | 181 | will listen for OSC messages on all addresses on port `18000` and send OSC messages to all addresses on port `18001`. 182 | Yes, `smrec` can also broadcast OSC messages is the OS and the network allows it. 183 | 184 | #### OSC messages 185 | 186 | The messages which `smrec` listens for are: 187 | 188 | - `/smrec/start` - Starts the recording, sending a second start will stop the running recording and starts a new one creating a new directory in the specified root. 189 | - `/smrec/stop` - Stops the recording if there is a running one. 190 | 191 | The messages which `smrec` sends are: 192 | 193 | - `/smrec/start` - Sent when a new recording is started. 194 | - `/smrec/stop` - Sent when a running recording is stopped. 195 | - `/smrec/error `- Sent when some errors occur and the error message is transferred a string in the argument. 196 | 197 | ### MIDI control 198 | 199 | `smrec` can also be controlled via MIDI. It can even be controlled via OSC and MIDI simultaneously. 200 | Though `smrec` is a simplistic application to serve a single purpose the MIDI communication the options it provides for configuring MIDI is extensive. 201 | 202 | Running, `smrec --midi` will not start recording immediately but instead it will wait for a MIDI CC message to start recording. 203 | 204 | Here is the default configuration: 205 | 206 | - Finds all available MIDI ports and starts listening on them. 207 | - Listens for any channel in those ports. 208 | - Reacts to CC 16 to start the recording and CC 17 to stop the recording. 209 | - As in OSC, sending subsequent CC 16 messages will stop the running recording and start a new one creating a new directory in the specified root. 210 | - `smrec --midi` is synonymous with `smrec --midi "[*[(*,16,17)]]"` which will be explained below. 211 | 212 | #### Configuration 213 | 214 | The `--midi` flag accepts a string argument which is parsed as a configuration string which configures the input and output. 215 | These strings are separated by a semicolon (`;`) and the first part configures the input and the second part configures the output. 216 | Any part could be left out. 217 | 218 | Deconstruction: 219 | 220 | - `[..]` is a container for an input or output configuration. 221 | - `[port name[..], ..]` a comma separated list of port names which `smrec` will connect to. 222 | - `[port name[(..), ..], ..]` each port name should contain at least one channel/MIDI CC filter configuration. 223 | - `(, , )` this is the structure of a channel/MIDI CC filter configuration. 224 | - `(1,2,3)` here is an example, this will listen for CC 2 on channel 2 to start the recording and CC 3 on channel 2 to stop the recording. All other messages in that port is ignored. MIDI channels are 0 indexed! 225 | - `[my nice port[(1,2,3), ..], ..]` this is how we use that tuple. 226 | - `[my nice port[(1,2,3), (15, 127, 126), ..], ..]` as all the elements we can have multiples of those. 227 | - `[ my first port[(1,2,3), (15, 127, 126), (12,4,5)], my second port[(1,2,3)] ]` here is a valid configuration string. It will listen for CC 2 on channel 2 to start the recording and CC 3 on channel 2 to stop the recording on `my first port` and listen for CC 2 on channel 2 to start the recording and CC 3 on channel 2 to stop the recording on `my second port`. All other messages in those ports are ignored. 228 | 229 | Use of '\*' and glob patterns: 230 | 231 | - Port names in the configuration string are treated as [glob patterns](). 232 | - `*` matches any port. Which in the end means all ports. 233 | - All valid glob patterns could be used to match port names. 234 | - Deconstructing the default configuration string: `[*[(*,16,17)]]` now should make sense. 235 | - Listen on all ports, do not filter by channel reacting to all MIDI CC messages which are CC 16 to start the recording and CC 17 to stop the recording. 236 | 237 | `smrec` can also send midi messages on certain events. 238 | If the output port is configured with a configuration, the configured CC messages will be sent on the configured port and channels on start and stop events. 239 | 240 | #### Values 241 | 242 | MIDI CC values are considered momentary. 243 | 244 | Once a value `127` is received through a configured MIDI CC number the action is taken immediately. 245 | **This is why sending bursts of MIDI CC messages is not a good idea.** 246 | Every message would trigger a new recording if it is configured to start the recording. 247 | 248 | `smrec` sends MIDI CC messages with a value of `127` on start and `127` on stop to the configured MIDI CC numbers if output is configured. 249 | 250 | As a last example to get the hang of it, this configuration string will listen for CC 2 on channel 2 to start the recording and CC 3 on channel 2 to stop the recording on `my first port` and listen for CC 2 on channel 2 to start the recording and CC 3 on channel 2 to stop the recording on `my second port`. All other messages in those ports are ignored. On start and stop events, it will send CC 16 with a value of 127 on channel 2 on `my first port` and send CC 17 with a value of 127 on channel 2 on `my second port`. 251 | 252 | ``` 253 | [ my first port[(1,2,3), (15, 127, 126), (12,4,5)], my second port[(1,2,3)] ];[ my first port[(1,2,3), (15, 127, 126), (12,4,5)], my second port[(1,2,3)] ] 254 | ``` 255 | 256 | ## Next steps 257 | 258 | I'm going to make sure, 259 | 260 | - Installation gets smoother 261 | - Proper distribution packages are provided 262 | - Documentation is complete 263 | - Bugs are fixed 264 | - Better messages to the user 265 | 266 | But I don't plan to heavily maintain this project, I'll just make sure that it is usable enough and lives. 267 | 268 | ## Support 269 | 270 | - Desktop 271 | - macOS: 272 | - `x86_64` ✅ 273 | - `aarch64` ✅ 274 | - linux: 275 | - `x86_64` ✅ 276 | - `aarch64` ✅ 277 | - windows: 278 | - `x86_64` ✅ 279 | - `aarch64` ✅ 280 | 281 | ## Contributing 282 | 283 | - Be friendly and productive 284 | - Follow common practice open source contribution culture 285 | - Rust [code of conduct](https://www.rust-lang.org/policies/code-of-conduct) applies 286 | 287 | Thank you 🙏 288 | 289 | ## Last words 290 | 291 | It is something I needed to resolve a specific problem and I shared it publicly. 292 | I hope it resolves your problem too. 293 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Most of the lints we deny here have a good chance to be relevant for our project. 2 | #![deny(clippy::all)] 3 | // We warn for all lints on the planet. Just to filter them later for customization. 4 | // It is impossible to remember all the lints so a subtractive approach keeps us updated, in control and knowledgeable. 5 | #![warn(clippy::pedantic, clippy::nursery, clippy::cargo)] 6 | // Then in the end we allow ridiculous or too restrictive lints that are not relevant for our project. 7 | // This list is dynamic and will grow in time which will define our style. 8 | #![allow( 9 | clippy::multiple_crate_versions, 10 | clippy::blanket_clippy_restriction_lints, 11 | clippy::missing_docs_in_private_items, 12 | clippy::pub_use, 13 | clippy::std_instead_of_alloc, 14 | clippy::std_instead_of_core, 15 | clippy::implicit_return, 16 | clippy::missing_inline_in_public_items, 17 | clippy::similar_names, 18 | clippy::question_mark_used, 19 | clippy::expect_used, 20 | clippy::missing_errors_doc, 21 | clippy::pattern_type_mismatch, 22 | clippy::module_name_repetitions, 23 | clippy::empty_structs_with_brackets, 24 | clippy::as_conversions, 25 | clippy::self_named_module_files, 26 | clippy::cargo_common_metadata, 27 | clippy::exhaustive_structs, 28 | // It is a binary crate, panicing is usually fine. 29 | clippy::missing_panics_doc 30 | )] 31 | 32 | mod config; 33 | mod list; 34 | mod midi; 35 | mod osc; 36 | mod stream; 37 | mod types; 38 | mod wav; 39 | 40 | use crate::{ 41 | config::{choose_channels_to_record, SmrecConfig}, 42 | midi::Midi, 43 | }; 44 | use anyhow::{bail, Result}; 45 | use clap::{Parser, Subcommand}; 46 | use config::{choose_device, choose_host}; 47 | use cpal::traits::{DeviceTrait, StreamTrait}; 48 | use hound::WavWriter; 49 | use osc::Osc; 50 | use std::{ 51 | cell::RefCell, 52 | fs::File, 53 | io::BufWriter, 54 | rc::Rc, 55 | sync::{Arc, Mutex}, 56 | }; 57 | use types::Action; 58 | 59 | #[derive(Parser)] 60 | #[command( 61 | author, 62 | version, 63 | about = "Minimalist multi-track audio recorder which may be controlled via OSC or MIDI. 64 | You may visit for a detailed tutorial." 65 | )] 66 | struct Cli { 67 | /// Specify audio host. 68 | /// Example: smrec --host "Asio" 69 | #[clap(long)] 70 | host: Option, 71 | /// Specify audio device. 72 | /// Example: smrec --device "MacBook Pro Microphone" 73 | #[clap(long)] 74 | device: Option, 75 | /// Include specified channels in recording. 76 | /// Example: smrec --include 1,2 77 | #[clap(long, value_delimiter = ',', num_args = 1..)] 78 | include: Option>, 79 | /// Exclude specified channels from recording. 80 | /// Example: smrec --exclude 1 81 | #[clap(long, value_delimiter = ',', num_args = 1..)] 82 | exclude: Option>, 83 | /// Specify path to configuration file. 84 | /// Example: smrec --config "./config.toml" 85 | #[clap(long)] 86 | config: Option, 87 | /// Specify directory for recording output. 88 | /// Example: smrec --out ~/Music 89 | #[clap(long)] 90 | out: Option, 91 | /// Specify recording duration in seconds. 92 | /// Example: smrec --duration 10 93 | #[clap(long)] 94 | duration: Option, 95 | /// Configure OSC control. 96 | /// Example: smrec --osc "0.0.0.0:18000;255.255.255.255:18001" 97 | #[clap(long, value_delimiter = ';', num_args = 0..2, default_value = "EMPTY_HACK", hide_default_value = true)] 98 | osc: Vec, 99 | /// Configure MIDI control. 100 | /// Example: smrec --midi my first port[(1,2,3), (15, 127, 126), (12,4,5)], my second port[(1,2,3)] 101 | #[clap(long, value_delimiter = ';', num_args = 0..2, default_value = "EMPTY_HACK", hide_default_value = true)] 102 | midi: Vec, 103 | 104 | #[clap(subcommand)] 105 | command: Option, 106 | } 107 | 108 | #[derive(Subcommand)] 109 | enum Commands { 110 | /// Lists hosts, devices and configs. 111 | #[clap(about = "Lists hosts, devices and configs.")] 112 | List(List), 113 | } 114 | 115 | #[derive(Parser)] 116 | struct List { 117 | /// List MIDI configurations. 118 | /// Example: smrec list --midi 119 | #[clap(long)] 120 | midi: bool, 121 | /// List audio configurations. 122 | /// Example: smrec list --audio 123 | #[clap(long)] 124 | audio: bool, 125 | } 126 | 127 | pub type WriterHandle = Arc>>>>; 128 | pub type WriterHandles = Arc>; 129 | 130 | #[allow(clippy::too_many_lines)] 131 | fn main() -> Result<()> { 132 | let cli = Cli::parse(); 133 | 134 | let host = choose_host(cli.host)?; 135 | 136 | if let Some(command) = cli.command { 137 | match command { 138 | // Enumerate and exit. 139 | Commands::List(list) => { 140 | if list.midi { 141 | list::enumerate_midi()?; 142 | } 143 | if list.audio { 144 | list::enumerate_audio()?; 145 | } 146 | if !list.audio || !list.midi { 147 | list::enumerate_audio()?; 148 | println!(); 149 | list::enumerate_midi()?; 150 | } 151 | } 152 | }; 153 | return Ok(()); 154 | } 155 | 156 | let device = choose_device(&host, cli.device)?; 157 | let writers_container: Arc>> = Arc::new(Mutex::new(None)); 158 | let stream_container: Rc>> = Rc::new(RefCell::new(None)); 159 | 160 | if let Ok(config) = device.default_input_config() { 161 | let smrec_config = Arc::new(SmrecConfig::new( 162 | cli.config, 163 | cli.out, 164 | choose_channels_to_record(cli.include, cli.exclude, &config)?, 165 | config.clone(), 166 | )?); 167 | 168 | let (to_main_thread, from_listener_thread) = crossbeam::channel::unbounded::(); 169 | let (to_listener_thread, from_main_thread) = crossbeam::channel::unbounded::(); 170 | 171 | let cli_osc = if cli.osc == vec!["EMPTY_HACK"] { 172 | None 173 | } else if cli.osc.is_empty() { 174 | Some(vec![]) 175 | } else { 176 | Some(cli.osc) 177 | }; 178 | 179 | let cli_midi = if cli.midi == vec!["EMPTY_HACK"] { 180 | None 181 | } else if cli.midi.is_empty() { 182 | Some(vec![]) 183 | } else { 184 | Some(cli.midi) 185 | }; 186 | 187 | let osc = if let Some(osc_config) = cli_osc { 188 | if osc_config.len() > 2 { 189 | bail!("Too many arguments for --osc"); 190 | } 191 | let mut osc = Osc::new( 192 | &osc_config, 193 | to_main_thread.clone(), 194 | from_main_thread.clone(), 195 | )?; 196 | osc.listen(); 197 | Some(osc) 198 | } else { 199 | None 200 | }; 201 | 202 | let midi = if let Some(midi) = cli_midi { 203 | let mut midi = Midi::new(to_main_thread, from_main_thread, &midi)?; 204 | midi.listen()?; 205 | Some(midi) 206 | } else { 207 | None 208 | }; 209 | 210 | match (midi, osc) { 211 | (None, None) => { 212 | // Pass 213 | } 214 | _ => listen_and_block_main_thread( 215 | &from_listener_thread, 216 | &to_listener_thread, 217 | &device, 218 | &stream_container, 219 | &writers_container, 220 | &smrec_config, 221 | ), 222 | } 223 | 224 | // No listeners, just start recording, for ever or for a certain duration. 225 | 226 | new_recording( 227 | &device, 228 | &stream_container, 229 | &writers_container, 230 | &smrec_config, 231 | )?; 232 | 233 | cli.duration.map_or_else( 234 | || { 235 | std::thread::park(); 236 | }, 237 | |dur| { 238 | let secs = dur 239 | .parse::() 240 | .expect("--duration must be a positive integer."); 241 | std::thread::park_timeout(std::time::Duration::from_secs(secs)); 242 | }, 243 | ); 244 | 245 | stop_recording(&stream_container, &writers_container)?; 246 | println!("Recording complete!"); 247 | } else { 248 | bail!("No default input config found for device."); 249 | } 250 | 251 | Ok(()) 252 | } 253 | 254 | pub fn listen_and_block_main_thread( 255 | from_listener_thread: &crossbeam::channel::Receiver, 256 | to_listener_thread: &crossbeam::channel::Sender, 257 | device: &cpal::Device, 258 | stream_container: &Rc>>, 259 | writers_container: &Arc>>, 260 | smrec_config: &SmrecConfig, 261 | ) { 262 | loop { 263 | match from_listener_thread.recv() { 264 | Ok(Action::Start) => { 265 | if let Err(err) = 266 | new_recording(device, stream_container, writers_container, smrec_config) 267 | { 268 | println!("Error starting recording: {err}"); 269 | 270 | to_listener_thread 271 | .send(Action::Err(format!("Error starting recording: {err}"))) 272 | .expect("Internal thread error."); 273 | } else { 274 | to_listener_thread 275 | .send(Action::Start) 276 | .expect("Internal thread error."); 277 | } 278 | } 279 | Ok(Action::Stop) => { 280 | if let Err(err) = stop_recording(stream_container, writers_container) { 281 | println!("Error stopping recording: {err}"); 282 | to_listener_thread 283 | .send(Action::Err(format!("Error starting recording: {err}"))) 284 | .expect("Internal thread error."); 285 | } else { 286 | to_listener_thread 287 | .send(Action::Stop) 288 | .expect("Internal thread error."); 289 | } 290 | } 291 | // Should not be used here though, no user facing api anyway. 292 | Ok(Action::Err(err)) => { 293 | println!("Error: {err}"); 294 | } 295 | Err(_) => { 296 | println!("Error receiving from listener thread."); 297 | } 298 | } 299 | } 300 | } 301 | 302 | pub fn new_recording( 303 | device: &cpal::Device, 304 | stream_container: &Rc>>, 305 | writer_handles: &Arc>>, 306 | smrec_config: &SmrecConfig, 307 | ) -> Result<()> { 308 | // If there's an active stream, pause it and finalize the writers 309 | if let Some(stream) = stream_container.borrow_mut().as_mut() { 310 | stream.pause()?; 311 | finalize_writers_if_some(writer_handles).unwrap(); 312 | println!("Restarting new recording..."); 313 | } else { 314 | println!("Starting recording..."); 315 | } 316 | 317 | // Make new writers 318 | let writers = smrec_config.writers()?; 319 | // Replace the old ones. 320 | writer_handles.lock().unwrap().replace(writers); 321 | 322 | // Errors when ctrl+c handler is already set. We ignore this error since we have no intention of a reset. 323 | let writer_handles_in_ctrlc = Arc::clone(writer_handles); 324 | let _ = ctrlc::try_set_handler(move || { 325 | // TODO: Necessary to drop stream? 326 | 327 | // TODO: Maybe inform user in unsuccessful operation? 328 | finalize_writers_if_some(&writer_handles_in_ctrlc).unwrap(); 329 | 330 | // TODO: Better message, differentiate if the recording was stopped or interrupted. 331 | println!("\rRecording interrupted thus stopped."); 332 | std::process::exit(0); 333 | }); 334 | 335 | // Create and start a new stream 336 | let new_stream = stream::build( 337 | device, 338 | smrec_config.supported_cpal_stream_config(), 339 | smrec_config.channels_to_record(), 340 | Arc::clone(writer_handles), 341 | )?; 342 | 343 | new_stream.play()?; 344 | println!("Recording started."); 345 | stream_container.borrow_mut().replace(new_stream); 346 | 347 | Ok(()) 348 | } 349 | 350 | pub fn stop_recording( 351 | stream_container: &Rc>>, 352 | writer_handles: &Arc>>, 353 | ) -> Result<()> { 354 | println!("Stopping recording..."); 355 | 356 | if let Some(stream) = stream_container.borrow_mut().take() { 357 | stream.pause()?; 358 | finalize_writers_if_some(writer_handles)?; 359 | println!("Recording stopped."); 360 | return Ok(()); 361 | } 362 | println!("There is no running recording to stop."); 363 | 364 | Ok(()) 365 | } 366 | 367 | pub fn finalize_writers_if_some(writers: &Arc>>) -> Result<()> { 368 | let writers = writers.lock().unwrap().take(); 369 | if let Some(writers) = writers { 370 | for writer in writers.iter() { 371 | if let Some(writer) = writer.lock().unwrap().take() { 372 | writer.finalize().unwrap(); 373 | } 374 | } 375 | } 376 | Ok(()) 377 | } 378 | -------------------------------------------------------------------------------- /src/midi.rs: -------------------------------------------------------------------------------- 1 | mod parse; 2 | 3 | const CHANNEL_MASK: u8 = 0b0000_1111; 4 | const ANY_CHANNEL_INTERNAL: u8 = 0xFF; 5 | 6 | use crate::types::Action; 7 | use anyhow::{bail, Result}; 8 | use midir::{ 9 | MidiInput, MidiInputConnection, MidiInputPort, MidiOutput, MidiOutputConnection, MidiOutputPort, 10 | }; 11 | use std::{ 12 | collections::HashMap, 13 | ops::Deref, 14 | str::FromStr, 15 | sync::{Arc, Mutex}, 16 | }; 17 | 18 | enum MessageType { 19 | NoteOff, 20 | NoteOn, 21 | PolyphonicAfterTouch, 22 | ControlChange, 23 | ProgramChange, 24 | AfterTouch, 25 | PitchBendChange, 26 | Ignored, 27 | } 28 | 29 | const fn get_message_type(message: &[u8]) -> MessageType { 30 | match message[0] >> 4 { 31 | 0x8 => MessageType::NoteOff, 32 | 0x9 => MessageType::NoteOn, 33 | 0xA => MessageType::PolyphonicAfterTouch, 34 | 0xB => MessageType::ControlChange, 35 | 0xC => MessageType::ProgramChange, 36 | 0xD => MessageType::AfterTouch, 37 | 0xE => MessageType::PitchBendChange, 38 | _ => MessageType::Ignored, 39 | } 40 | } 41 | 42 | const fn get_channel(message: &[u8]) -> u8 { 43 | message[0] & CHANNEL_MASK 44 | } 45 | 46 | const fn make_cc_message(channel: u8, cc_num: u8, value: u8) -> [u8; 3] { 47 | [0xB0 + channel, cc_num, value] 48 | } 49 | 50 | /// `HashMap` of port name to vector of (`channel_num`, `cc_num`[start], `cc_num`[stop]) 51 | #[derive(Debug, Clone)] 52 | pub struct MidiConfig(HashMap>); 53 | 54 | impl Deref for MidiConfig { 55 | type Target = HashMap>; 56 | 57 | fn deref(&self) -> &Self::Target { 58 | &self.0 59 | } 60 | } 61 | 62 | impl FromStr for MidiConfig { 63 | type Err = anyhow::Error; 64 | 65 | fn from_str(s: &str) -> Result { 66 | parse::parse_midi_config(s) 67 | } 68 | } 69 | 70 | #[allow(clippy::type_complexity)] 71 | pub struct Midi { 72 | input: MidiInput, 73 | output: Option, 74 | input_config: MidiConfig, 75 | output_config: Option, 76 | sender_channel: crossbeam::channel::Sender, 77 | receiver_channel: crossbeam::channel::Receiver, 78 | input_connections: HashMap>>, 79 | output_thread: Option>, 80 | } 81 | 82 | impl Midi { 83 | fn find_input_ports(&self, pattern: &str) -> Result> { 84 | let mut found = Vec::new(); 85 | for port in self.input.ports() { 86 | let name = self.input.port_name(&port).unwrap(); 87 | if glob_match::glob_match(pattern, &name) { 88 | found.push((name, port)); 89 | } 90 | } 91 | if found.len() == 1 { 92 | println!("Started listening on MIDI input port: {:?}\n", found[0].0); 93 | } 94 | if found.len() > 1 { 95 | println!("Warning: Found more than one MIDI input port matching the pattern and listening on them.\nFound ports: {:?}", found.iter().map(|(name, _)| name).collect::>()); 96 | } 97 | if found.is_empty() { 98 | bail!("No MIDI input port found matching the pattern."); 99 | } 100 | Ok(found) 101 | } 102 | 103 | fn find_output_ports(&self, pattern: &str) -> Result> { 104 | if let Some(ref output) = self.output { 105 | let mut found = Vec::new(); 106 | for port in output.ports() { 107 | let name = output.port_name(&port).unwrap(); 108 | if glob_match::glob_match(pattern, &name) { 109 | found.push((name, port)); 110 | } 111 | } 112 | if found.len() == 1 { 113 | println!( 114 | "Notifications will be sent on MIDI output port: {:?}\n", 115 | found[0].0 116 | ); 117 | } 118 | if found.len() > 1 { 119 | println!("Warning: Found more than one MIDI output port matching the pattern and will send notifications to them.\nFound ports: {:?}", found.iter().map(|(name, _)| name).collect::>()); 120 | } 121 | if found.is_empty() { 122 | bail!("No MIDI output port found matching the pattern."); 123 | } 124 | Ok(found) 125 | } else { 126 | bail!("No midi output configured.") 127 | } 128 | } 129 | 130 | pub fn new( 131 | sender_channel: crossbeam::channel::Sender, 132 | receiver_channel: crossbeam::channel::Receiver, 133 | cli_config: &[String], 134 | ) -> Result { 135 | let input = MidiInput::new("smrec")?; 136 | 137 | let input_config = if let Some(input_config) = cli_config.get(0) { 138 | MidiConfig::from_str(input_config)? 139 | } else { 140 | // Listen all ports and all channels by default. 141 | MidiConfig::from_str("[*[(*,16,17)]]")? 142 | }; 143 | let output_config = if let Some(output_config) = cli_config.get(1) { 144 | Some(MidiConfig::from_str(output_config)?) 145 | } else { 146 | None 147 | }; 148 | 149 | Ok(Self { 150 | input, 151 | output: if output_config.is_some() { 152 | Some(MidiOutput::new("smrec")?) 153 | } else { 154 | None 155 | }, 156 | input_config, 157 | output_config, 158 | sender_channel, 159 | receiver_channel, 160 | input_connections: HashMap::new(), 161 | output_thread: None, 162 | }) 163 | } 164 | 165 | // These are going to be addressed in a later refactor. 166 | #[allow(clippy::type_complexity)] 167 | fn input_ports_from_configs(&self) -> Result)>> { 168 | self.input_config 169 | .iter() 170 | .filter_map(|(port_name, configs)| { 171 | let input_ports = self.find_input_ports(port_name).ok()?; 172 | Some( 173 | input_ports 174 | .into_iter() 175 | .map(move |(name, port)| (name, port, configs.clone())) 176 | .collect::>(), 177 | ) 178 | }) 179 | .flatten() 180 | .map(Ok) 181 | .collect::)>, anyhow::Error>>() 182 | } 183 | 184 | fn register_midi_input_hooks(&mut self) -> Result<()> { 185 | let input_ports = self.input_ports_from_configs()?; 186 | 187 | // Start listening for MIDI messages on all configured ports and channels. 188 | for (port_name, port, configs) in input_ports { 189 | let to_main_thread = self.sender_channel.clone(); 190 | 191 | let input = MidiInput::new("smrec")?; 192 | self.input_connections.insert( 193 | port_name.clone(), 194 | input 195 | .connect( 196 | &port, 197 | &port_name, 198 | move |_stamp, message, configs| { 199 | let channel = get_channel(message); 200 | let message_type = get_message_type(message); 201 | if matches!(message_type, MessageType::ControlChange) { 202 | if let (Some(cc_number), Some(value)) = 203 | (message.get(1), message.get(2)) 204 | { 205 | let active_config = configs 206 | .iter() 207 | .filter(|(chn, start_cc_num, stop_cc_num)| { 208 | chn == &channel 209 | && (cc_number == start_cc_num 210 | || cc_number == stop_cc_num) 211 | }) 212 | .collect::>(); 213 | 214 | let any_channel_receive_configs = configs 215 | .iter() 216 | .filter(|(chn, start_cc_num, stop_cc_num)| { 217 | *chn == ANY_CHANNEL_INTERNAL 218 | && (cc_number == start_cc_num 219 | || cc_number == stop_cc_num) 220 | }) 221 | .collect::>(); 222 | 223 | // There can be only one channel and one message type so either the active config is empty or has one element. 224 | if !active_config.is_empty() { 225 | let (chn, start_cc_num, stop_cc_num) = active_config[0]; 226 | 227 | if chn == &channel 228 | && cc_number == start_cc_num 229 | && *value == 127 230 | { 231 | to_main_thread.send(Action::Start).unwrap(); 232 | } 233 | 234 | if chn == &channel 235 | && cc_number == stop_cc_num 236 | && *value == 127 237 | { 238 | to_main_thread.send(Action::Stop).unwrap(); 239 | } 240 | } 241 | 242 | for (_, start_cc_num, stop_cc_num) in 243 | any_channel_receive_configs 244 | { 245 | if cc_number == start_cc_num && *value == 127 { 246 | to_main_thread.send(Action::Start).unwrap(); 247 | } 248 | 249 | if cc_number == stop_cc_num && *value == 127 { 250 | to_main_thread.send(Action::Stop).unwrap(); 251 | } 252 | } 253 | } else { 254 | println!("Invalid CC message: {message:?}"); 255 | } 256 | } 257 | }, 258 | configs, 259 | ) 260 | .expect("Could not bind to {port_name}"), 261 | ); 262 | } 263 | 264 | Ok(()) 265 | } 266 | 267 | // These are going to be addressed in a later refactor. 268 | #[allow(clippy::type_complexity)] 269 | fn output_connections_from_config( 270 | &self, 271 | ) -> Result>, Vec<(u8, u8, u8)>)>>> { 272 | if let Some(ref output_config) = self.output_config { 273 | let output_ports = output_config 274 | .iter() 275 | .filter_map(|(port_name, configs)| { 276 | let output_ports = self.find_output_ports(port_name).ok()?; 277 | Some( 278 | output_ports 279 | .into_iter() 280 | .map(move |(name, port)| (name, port, configs.clone())) 281 | .collect::>(), 282 | ) 283 | }) 284 | .flatten() 285 | .map(Ok) 286 | .collect::)>, anyhow::Error>>( 287 | )?; 288 | 289 | return output_ports 290 | .iter() 291 | .map(|(port_name, port, configs)| { 292 | let output = MidiOutput::new("smrec")?; 293 | Ok(Some(( 294 | port_name.clone(), 295 | Arc::new(Mutex::new( 296 | output 297 | .connect(port, port_name) 298 | .expect("Could not bind to {port_name}"), 299 | )), 300 | configs.clone(), 301 | ))) 302 | }) 303 | .collect::>, Vec<(u8, u8, u8)>)>>, 305 | _, 306 | >>(); 307 | } 308 | 309 | Ok(None) 310 | } 311 | 312 | fn spin_midi_output_thread_if_necessary(&mut self) -> Result<()> { 313 | let output_connections = self.output_connections_from_config()?; 314 | let receiver_channel = self.receiver_channel.clone(); 315 | 316 | if let Some(output_connections) = output_connections { 317 | self.output_thread = Some(std::thread::spawn(move || { 318 | loop { 319 | if let Ok(action) = receiver_channel.recv() { 320 | match action { 321 | Action::Start => { 322 | for (port_name, connection, configs) in &output_connections { 323 | for (channel, start_cc_num, _) in configs { 324 | // Send to all channels if channel is 255. 325 | if *channel == ANY_CHANNEL_INTERNAL { 326 | for chn in 0..15 { 327 | if let Err(err) = connection 328 | .lock() 329 | .unwrap() 330 | .send(&make_cc_message(chn, *start_cc_num, 127)) 331 | { 332 | println!( 333 | "Error sending CC message to {port_name}: {err} ", 334 | ); 335 | } 336 | } 337 | continue; 338 | } 339 | 340 | if let Err(err) = connection 341 | .lock() 342 | .unwrap() 343 | .send(&make_cc_message(*channel, *start_cc_num, 127)) 344 | { 345 | println!( 346 | "Error sending CC message to {port_name}: {err} ", 347 | ); 348 | } 349 | } 350 | } 351 | } 352 | Action::Stop => { 353 | for (port_name, connection, configs) in &output_connections { 354 | for (channel, _, stop_cc_num) in configs { 355 | // Send to all channels if channel is 255. 356 | if *channel == ANY_CHANNEL_INTERNAL { 357 | for chn in 0..15 { 358 | if let Err(err) = connection 359 | .lock() 360 | .unwrap() 361 | .send(&make_cc_message(chn, *stop_cc_num, 127)) 362 | { 363 | println!( 364 | "Error sending CC message to {port_name}: {err} ", 365 | ); 366 | } 367 | } 368 | continue; 369 | } 370 | 371 | if let Err(err) = connection 372 | .lock() 373 | .unwrap() 374 | .send(&make_cc_message(*channel, *stop_cc_num, 127)) 375 | { 376 | println!( 377 | "Error sending CC message to {port_name}: {err} ", 378 | ); 379 | } 380 | } 381 | } 382 | } 383 | Action::Err(_) => { 384 | // Ignore, we don't send midi messages when errors occur. 385 | } 386 | } 387 | } 388 | } 389 | })); 390 | } 391 | 392 | Ok(()) 393 | } 394 | 395 | pub fn listen(&mut self) -> Result<()> { 396 | self.register_midi_input_hooks()?; 397 | self.spin_midi_output_thread_if_necessary()?; 398 | 399 | Ok(()) 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "alsa" 16 | version = "0.7.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" 19 | dependencies = [ 20 | "alsa-sys", 21 | "bitflags 1.3.2", 22 | "libc", 23 | "nix 0.24.3", 24 | ] 25 | 26 | [[package]] 27 | name = "alsa-sys" 28 | version = "0.3.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 31 | dependencies = [ 32 | "libc", 33 | "pkg-config", 34 | ] 35 | 36 | [[package]] 37 | name = "android-tzdata" 38 | version = "0.1.1" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 41 | 42 | [[package]] 43 | name = "android_system_properties" 44 | version = "0.1.5" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 47 | dependencies = [ 48 | "libc", 49 | ] 50 | 51 | [[package]] 52 | name = "anstream" 53 | version = "0.6.4" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 56 | dependencies = [ 57 | "anstyle", 58 | "anstyle-parse", 59 | "anstyle-query", 60 | "anstyle-wincon", 61 | "colorchoice", 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle" 67 | version = "1.0.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 70 | 71 | [[package]] 72 | name = "anstyle-parse" 73 | version = "0.2.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 76 | dependencies = [ 77 | "utf8parse", 78 | ] 79 | 80 | [[package]] 81 | name = "anstyle-query" 82 | version = "1.0.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 85 | dependencies = [ 86 | "windows-sys", 87 | ] 88 | 89 | [[package]] 90 | name = "anstyle-wincon" 91 | version = "3.0.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 94 | dependencies = [ 95 | "anstyle", 96 | "windows-sys", 97 | ] 98 | 99 | [[package]] 100 | name = "anyhow" 101 | version = "1.0.75" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 104 | 105 | [[package]] 106 | name = "asio-sys" 107 | version = "0.2.1" 108 | source = "git+https://github.com/RustAudio/cpal.git#efa0c7af6b997cbdab3b95e5d792717503ef1667" 109 | dependencies = [ 110 | "bindgen 0.69.1", 111 | "cc", 112 | "num-derive 0.4.1", 113 | "num-traits", 114 | "once_cell", 115 | "parse_cfg", 116 | "walkdir", 117 | ] 118 | 119 | [[package]] 120 | name = "autocfg" 121 | version = "1.1.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 124 | 125 | [[package]] 126 | name = "bindgen" 127 | version = "0.68.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" 130 | dependencies = [ 131 | "bitflags 2.4.1", 132 | "cexpr", 133 | "clang-sys", 134 | "lazy_static", 135 | "lazycell", 136 | "peeking_take_while", 137 | "proc-macro2", 138 | "quote", 139 | "regex", 140 | "rustc-hash", 141 | "shlex", 142 | "syn 2.0.39", 143 | ] 144 | 145 | [[package]] 146 | name = "bindgen" 147 | version = "0.69.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" 150 | dependencies = [ 151 | "bitflags 2.4.1", 152 | "cexpr", 153 | "clang-sys", 154 | "lazy_static", 155 | "lazycell", 156 | "log", 157 | "peeking_take_while", 158 | "prettyplease", 159 | "proc-macro2", 160 | "quote", 161 | "regex", 162 | "rustc-hash", 163 | "shlex", 164 | "syn 2.0.39", 165 | "which", 166 | ] 167 | 168 | [[package]] 169 | name = "bitflags" 170 | version = "1.3.2" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 173 | 174 | [[package]] 175 | name = "bitflags" 176 | version = "2.4.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 179 | 180 | [[package]] 181 | name = "bumpalo" 182 | version = "3.14.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 185 | 186 | [[package]] 187 | name = "byteorder" 188 | version = "1.5.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 191 | 192 | [[package]] 193 | name = "bytes" 194 | version = "1.5.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 197 | 198 | [[package]] 199 | name = "camino" 200 | version = "1.1.6" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" 203 | 204 | [[package]] 205 | name = "cc" 206 | version = "1.0.83" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 209 | dependencies = [ 210 | "jobserver", 211 | "libc", 212 | ] 213 | 214 | [[package]] 215 | name = "cesu8" 216 | version = "1.1.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 219 | 220 | [[package]] 221 | name = "cexpr" 222 | version = "0.6.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 225 | dependencies = [ 226 | "nom", 227 | ] 228 | 229 | [[package]] 230 | name = "cfg-if" 231 | version = "1.0.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 234 | 235 | [[package]] 236 | name = "chrono" 237 | version = "0.4.31" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 240 | dependencies = [ 241 | "android-tzdata", 242 | "iana-time-zone", 243 | "js-sys", 244 | "num-traits", 245 | "serde", 246 | "wasm-bindgen", 247 | "windows-targets", 248 | ] 249 | 250 | [[package]] 251 | name = "clang-sys" 252 | version = "1.6.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" 255 | dependencies = [ 256 | "glob", 257 | "libc", 258 | "libloading", 259 | ] 260 | 261 | [[package]] 262 | name = "clap" 263 | version = "4.4.8" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" 266 | dependencies = [ 267 | "clap_builder", 268 | "clap_derive", 269 | ] 270 | 271 | [[package]] 272 | name = "clap_builder" 273 | version = "4.4.8" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" 276 | dependencies = [ 277 | "anstream", 278 | "anstyle", 279 | "clap_lex", 280 | "strsim", 281 | ] 282 | 283 | [[package]] 284 | name = "clap_derive" 285 | version = "4.4.7" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 288 | dependencies = [ 289 | "heck", 290 | "proc-macro2", 291 | "quote", 292 | "syn 2.0.39", 293 | ] 294 | 295 | [[package]] 296 | name = "clap_lex" 297 | version = "0.6.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 300 | 301 | [[package]] 302 | name = "colorchoice" 303 | version = "1.0.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 306 | 307 | [[package]] 308 | name = "combine" 309 | version = "4.6.6" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 312 | dependencies = [ 313 | "bytes", 314 | "memchr", 315 | ] 316 | 317 | [[package]] 318 | name = "core-foundation" 319 | version = "0.9.3" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 322 | dependencies = [ 323 | "core-foundation-sys", 324 | "libc", 325 | ] 326 | 327 | [[package]] 328 | name = "core-foundation-sys" 329 | version = "0.8.4" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 332 | 333 | [[package]] 334 | name = "coreaudio-rs" 335 | version = "0.11.3" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" 338 | dependencies = [ 339 | "bitflags 1.3.2", 340 | "core-foundation-sys", 341 | "coreaudio-sys", 342 | ] 343 | 344 | [[package]] 345 | name = "coreaudio-sys" 346 | version = "0.2.13" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d" 349 | dependencies = [ 350 | "bindgen 0.68.1", 351 | ] 352 | 353 | [[package]] 354 | name = "coremidi" 355 | version = "0.6.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "1a7847ca018a67204508b77cb9e6de670125075f7464fff5f673023378fa34f5" 358 | dependencies = [ 359 | "core-foundation", 360 | "core-foundation-sys", 361 | "coremidi-sys", 362 | ] 363 | 364 | [[package]] 365 | name = "coremidi-sys" 366 | version = "3.1.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "79a6deed0c97b2d40abbab77e4c97f81d71e162600423382c277dd640019116c" 369 | dependencies = [ 370 | "core-foundation-sys", 371 | ] 372 | 373 | [[package]] 374 | name = "cpal" 375 | version = "0.15.2" 376 | source = "git+https://github.com/RustAudio/cpal.git#efa0c7af6b997cbdab3b95e5d792717503ef1667" 377 | dependencies = [ 378 | "alsa", 379 | "asio-sys", 380 | "core-foundation-sys", 381 | "coreaudio-rs", 382 | "dasp_sample", 383 | "jack", 384 | "jni 0.19.0", 385 | "js-sys", 386 | "libc", 387 | "mach2", 388 | "ndk", 389 | "ndk-context", 390 | "num-traits", 391 | "oboe", 392 | "once_cell", 393 | "parking_lot", 394 | "wasm-bindgen", 395 | "wasm-bindgen-futures", 396 | "web-sys", 397 | "windows 0.48.0", 398 | ] 399 | 400 | [[package]] 401 | name = "crossbeam" 402 | version = "0.8.2" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" 405 | dependencies = [ 406 | "cfg-if", 407 | "crossbeam-channel", 408 | "crossbeam-deque", 409 | "crossbeam-epoch", 410 | "crossbeam-queue", 411 | "crossbeam-utils", 412 | ] 413 | 414 | [[package]] 415 | name = "crossbeam-channel" 416 | version = "0.5.8" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 419 | dependencies = [ 420 | "cfg-if", 421 | "crossbeam-utils", 422 | ] 423 | 424 | [[package]] 425 | name = "crossbeam-deque" 426 | version = "0.8.3" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 429 | dependencies = [ 430 | "cfg-if", 431 | "crossbeam-epoch", 432 | "crossbeam-utils", 433 | ] 434 | 435 | [[package]] 436 | name = "crossbeam-epoch" 437 | version = "0.9.15" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 440 | dependencies = [ 441 | "autocfg", 442 | "cfg-if", 443 | "crossbeam-utils", 444 | "memoffset", 445 | "scopeguard", 446 | ] 447 | 448 | [[package]] 449 | name = "crossbeam-queue" 450 | version = "0.3.8" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" 453 | dependencies = [ 454 | "cfg-if", 455 | "crossbeam-utils", 456 | ] 457 | 458 | [[package]] 459 | name = "crossbeam-utils" 460 | version = "0.8.16" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 463 | dependencies = [ 464 | "cfg-if", 465 | ] 466 | 467 | [[package]] 468 | name = "ctrlc" 469 | version = "3.4.1" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" 472 | dependencies = [ 473 | "nix 0.27.1", 474 | "windows-sys", 475 | ] 476 | 477 | [[package]] 478 | name = "dasp_sample" 479 | version = "0.11.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 482 | 483 | [[package]] 484 | name = "either" 485 | version = "1.9.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 488 | 489 | [[package]] 490 | name = "equivalent" 491 | version = "1.0.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 494 | 495 | [[package]] 496 | name = "errno" 497 | version = "0.3.6" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" 500 | dependencies = [ 501 | "libc", 502 | "windows-sys", 503 | ] 504 | 505 | [[package]] 506 | name = "glob" 507 | version = "0.3.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 510 | 511 | [[package]] 512 | name = "glob-match" 513 | version = "0.2.1" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" 516 | 517 | [[package]] 518 | name = "hashbrown" 519 | version = "0.14.2" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" 522 | 523 | [[package]] 524 | name = "heck" 525 | version = "0.4.1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 528 | 529 | [[package]] 530 | name = "home" 531 | version = "0.5.5" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 534 | dependencies = [ 535 | "windows-sys", 536 | ] 537 | 538 | [[package]] 539 | name = "hound" 540 | version = "3.5.1" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" 543 | 544 | [[package]] 545 | name = "iana-time-zone" 546 | version = "0.1.58" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 549 | dependencies = [ 550 | "android_system_properties", 551 | "core-foundation-sys", 552 | "iana-time-zone-haiku", 553 | "js-sys", 554 | "wasm-bindgen", 555 | "windows-core", 556 | ] 557 | 558 | [[package]] 559 | name = "iana-time-zone-haiku" 560 | version = "0.1.2" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 563 | dependencies = [ 564 | "cc", 565 | ] 566 | 567 | [[package]] 568 | name = "indexmap" 569 | version = "2.1.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 572 | dependencies = [ 573 | "equivalent", 574 | "hashbrown", 575 | ] 576 | 577 | [[package]] 578 | name = "jack" 579 | version = "0.11.4" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" 582 | dependencies = [ 583 | "bitflags 1.3.2", 584 | "jack-sys", 585 | "lazy_static", 586 | "libc", 587 | "log", 588 | ] 589 | 590 | [[package]] 591 | name = "jack-sys" 592 | version = "0.5.1" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" 595 | dependencies = [ 596 | "bitflags 1.3.2", 597 | "lazy_static", 598 | "libc", 599 | "libloading", 600 | "log", 601 | "pkg-config", 602 | ] 603 | 604 | [[package]] 605 | name = "jni" 606 | version = "0.19.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" 609 | dependencies = [ 610 | "cesu8", 611 | "combine", 612 | "jni-sys", 613 | "log", 614 | "thiserror", 615 | "walkdir", 616 | ] 617 | 618 | [[package]] 619 | name = "jni" 620 | version = "0.20.0" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" 623 | dependencies = [ 624 | "cesu8", 625 | "combine", 626 | "jni-sys", 627 | "log", 628 | "thiserror", 629 | "walkdir", 630 | ] 631 | 632 | [[package]] 633 | name = "jni-sys" 634 | version = "0.3.0" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 637 | 638 | [[package]] 639 | name = "jobserver" 640 | version = "0.1.27" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" 643 | dependencies = [ 644 | "libc", 645 | ] 646 | 647 | [[package]] 648 | name = "js-sys" 649 | version = "0.3.65" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 652 | dependencies = [ 653 | "wasm-bindgen", 654 | ] 655 | 656 | [[package]] 657 | name = "lazy_static" 658 | version = "1.4.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 661 | 662 | [[package]] 663 | name = "lazycell" 664 | version = "1.3.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 667 | 668 | [[package]] 669 | name = "libc" 670 | version = "0.2.150" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 673 | 674 | [[package]] 675 | name = "libloading" 676 | version = "0.7.4" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" 679 | dependencies = [ 680 | "cfg-if", 681 | "winapi", 682 | ] 683 | 684 | [[package]] 685 | name = "linux-raw-sys" 686 | version = "0.4.11" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" 689 | 690 | [[package]] 691 | name = "lock_api" 692 | version = "0.4.11" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 695 | dependencies = [ 696 | "autocfg", 697 | "scopeguard", 698 | ] 699 | 700 | [[package]] 701 | name = "log" 702 | version = "0.4.20" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 705 | 706 | [[package]] 707 | name = "mach2" 708 | version = "0.4.1" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" 711 | dependencies = [ 712 | "libc", 713 | ] 714 | 715 | [[package]] 716 | name = "memchr" 717 | version = "2.6.4" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 720 | 721 | [[package]] 722 | name = "memoffset" 723 | version = "0.9.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 726 | dependencies = [ 727 | "autocfg", 728 | ] 729 | 730 | [[package]] 731 | name = "midir" 732 | version = "0.9.1" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "a456444d83e7ead06ae6a5c0a215ed70282947ff3897fb45fcb052b757284731" 735 | dependencies = [ 736 | "alsa", 737 | "bitflags 1.3.2", 738 | "coremidi", 739 | "jack-sys", 740 | "js-sys", 741 | "libc", 742 | "wasm-bindgen", 743 | "web-sys", 744 | "windows 0.43.0", 745 | ] 746 | 747 | [[package]] 748 | name = "minimal-lexical" 749 | version = "0.2.1" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 752 | 753 | [[package]] 754 | name = "ndk" 755 | version = "0.7.0" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" 758 | dependencies = [ 759 | "bitflags 1.3.2", 760 | "jni-sys", 761 | "ndk-sys", 762 | "num_enum", 763 | "raw-window-handle", 764 | "thiserror", 765 | ] 766 | 767 | [[package]] 768 | name = "ndk-context" 769 | version = "0.1.1" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 772 | 773 | [[package]] 774 | name = "ndk-sys" 775 | version = "0.4.1+23.1.7779620" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" 778 | dependencies = [ 779 | "jni-sys", 780 | ] 781 | 782 | [[package]] 783 | name = "nix" 784 | version = "0.24.3" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 787 | dependencies = [ 788 | "bitflags 1.3.2", 789 | "cfg-if", 790 | "libc", 791 | ] 792 | 793 | [[package]] 794 | name = "nix" 795 | version = "0.27.1" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 798 | dependencies = [ 799 | "bitflags 2.4.1", 800 | "cfg-if", 801 | "libc", 802 | ] 803 | 804 | [[package]] 805 | name = "nom" 806 | version = "7.1.3" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 809 | dependencies = [ 810 | "memchr", 811 | "minimal-lexical", 812 | ] 813 | 814 | [[package]] 815 | name = "num-derive" 816 | version = "0.3.3" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 819 | dependencies = [ 820 | "proc-macro2", 821 | "quote", 822 | "syn 1.0.109", 823 | ] 824 | 825 | [[package]] 826 | name = "num-derive" 827 | version = "0.4.1" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" 830 | dependencies = [ 831 | "proc-macro2", 832 | "quote", 833 | "syn 2.0.39", 834 | ] 835 | 836 | [[package]] 837 | name = "num-traits" 838 | version = "0.2.17" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 841 | dependencies = [ 842 | "autocfg", 843 | ] 844 | 845 | [[package]] 846 | name = "num_enum" 847 | version = "0.5.11" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" 850 | dependencies = [ 851 | "num_enum_derive", 852 | ] 853 | 854 | [[package]] 855 | name = "num_enum_derive" 856 | version = "0.5.11" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" 859 | dependencies = [ 860 | "proc-macro-crate", 861 | "proc-macro2", 862 | "quote", 863 | "syn 1.0.109", 864 | ] 865 | 866 | [[package]] 867 | name = "oboe" 868 | version = "0.5.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" 871 | dependencies = [ 872 | "jni 0.20.0", 873 | "ndk", 874 | "ndk-context", 875 | "num-derive 0.3.3", 876 | "num-traits", 877 | "oboe-sys", 878 | ] 879 | 880 | [[package]] 881 | name = "oboe-sys" 882 | version = "0.5.0" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" 885 | dependencies = [ 886 | "cc", 887 | ] 888 | 889 | [[package]] 890 | name = "once_cell" 891 | version = "1.18.0" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 894 | 895 | [[package]] 896 | name = "parking_lot" 897 | version = "0.12.1" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 900 | dependencies = [ 901 | "lock_api", 902 | "parking_lot_core", 903 | ] 904 | 905 | [[package]] 906 | name = "parking_lot_core" 907 | version = "0.9.9" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 910 | dependencies = [ 911 | "cfg-if", 912 | "libc", 913 | "redox_syscall", 914 | "smallvec", 915 | "windows-targets", 916 | ] 917 | 918 | [[package]] 919 | name = "parse_cfg" 920 | version = "4.1.1" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49" 923 | dependencies = [ 924 | "nom", 925 | ] 926 | 927 | [[package]] 928 | name = "peeking_take_while" 929 | version = "0.1.2" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 932 | 933 | [[package]] 934 | name = "pkg-config" 935 | version = "0.3.27" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 938 | 939 | [[package]] 940 | name = "prettyplease" 941 | version = "0.2.15" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" 944 | dependencies = [ 945 | "proc-macro2", 946 | "syn 2.0.39", 947 | ] 948 | 949 | [[package]] 950 | name = "proc-macro-crate" 951 | version = "1.3.1" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 954 | dependencies = [ 955 | "once_cell", 956 | "toml_edit 0.19.15", 957 | ] 958 | 959 | [[package]] 960 | name = "proc-macro2" 961 | version = "1.0.69" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 964 | dependencies = [ 965 | "unicode-ident", 966 | ] 967 | 968 | [[package]] 969 | name = "quote" 970 | version = "1.0.33" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 973 | dependencies = [ 974 | "proc-macro2", 975 | ] 976 | 977 | [[package]] 978 | name = "raw-window-handle" 979 | version = "0.5.2" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" 982 | 983 | [[package]] 984 | name = "redox_syscall" 985 | version = "0.4.1" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 988 | dependencies = [ 989 | "bitflags 1.3.2", 990 | ] 991 | 992 | [[package]] 993 | name = "regex" 994 | version = "1.10.2" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 997 | dependencies = [ 998 | "aho-corasick", 999 | "memchr", 1000 | "regex-automata", 1001 | "regex-syntax", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "regex-automata" 1006 | version = "0.4.3" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 1009 | dependencies = [ 1010 | "aho-corasick", 1011 | "memchr", 1012 | "regex-syntax", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "regex-syntax" 1017 | version = "0.8.2" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1020 | 1021 | [[package]] 1022 | name = "rosc" 1023 | version = "0.10.1" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "b2e63d9e6b0d090be1485cf159b1e04c3973d2d3e1614963544ea2ff47a4a981" 1026 | dependencies = [ 1027 | "byteorder", 1028 | "nom", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "rustc-hash" 1033 | version = "1.1.0" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1036 | 1037 | [[package]] 1038 | name = "rustix" 1039 | version = "0.38.21" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 1042 | dependencies = [ 1043 | "bitflags 2.4.1", 1044 | "errno", 1045 | "libc", 1046 | "linux-raw-sys", 1047 | "windows-sys", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "same-file" 1052 | version = "1.0.6" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1055 | dependencies = [ 1056 | "winapi-util", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "scopeguard" 1061 | version = "1.2.0" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1064 | 1065 | [[package]] 1066 | name = "serde" 1067 | version = "1.0.192" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" 1070 | dependencies = [ 1071 | "serde_derive", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "serde_derive" 1076 | version = "1.0.192" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" 1079 | dependencies = [ 1080 | "proc-macro2", 1081 | "quote", 1082 | "syn 2.0.39", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "serde_spanned" 1087 | version = "0.6.4" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" 1090 | dependencies = [ 1091 | "serde", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "shlex" 1096 | version = "1.2.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" 1099 | 1100 | [[package]] 1101 | name = "smallvec" 1102 | version = "1.11.2" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 1105 | 1106 | [[package]] 1107 | name = "smrec" 1108 | version = "0.2.1" 1109 | dependencies = [ 1110 | "anyhow", 1111 | "camino", 1112 | "chrono", 1113 | "clap", 1114 | "cpal", 1115 | "crossbeam", 1116 | "ctrlc", 1117 | "glob-match", 1118 | "home", 1119 | "hound", 1120 | "midir", 1121 | "nom", 1122 | "rosc", 1123 | "serde", 1124 | "thiserror", 1125 | "toml", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "strsim" 1130 | version = "0.10.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1133 | 1134 | [[package]] 1135 | name = "syn" 1136 | version = "1.0.109" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1139 | dependencies = [ 1140 | "proc-macro2", 1141 | "quote", 1142 | "unicode-ident", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "syn" 1147 | version = "2.0.39" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 1150 | dependencies = [ 1151 | "proc-macro2", 1152 | "quote", 1153 | "unicode-ident", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "thiserror" 1158 | version = "1.0.50" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 1161 | dependencies = [ 1162 | "thiserror-impl", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "thiserror-impl" 1167 | version = "1.0.50" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 1170 | dependencies = [ 1171 | "proc-macro2", 1172 | "quote", 1173 | "syn 2.0.39", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "toml" 1178 | version = "0.8.8" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" 1181 | dependencies = [ 1182 | "serde", 1183 | "serde_spanned", 1184 | "toml_datetime", 1185 | "toml_edit 0.21.0", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "toml_datetime" 1190 | version = "0.6.5" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1193 | dependencies = [ 1194 | "serde", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "toml_edit" 1199 | version = "0.19.15" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 1202 | dependencies = [ 1203 | "indexmap", 1204 | "toml_datetime", 1205 | "winnow", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "toml_edit" 1210 | version = "0.21.0" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" 1213 | dependencies = [ 1214 | "indexmap", 1215 | "serde", 1216 | "serde_spanned", 1217 | "toml_datetime", 1218 | "winnow", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "unicode-ident" 1223 | version = "1.0.12" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1226 | 1227 | [[package]] 1228 | name = "utf8parse" 1229 | version = "0.2.1" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1232 | 1233 | [[package]] 1234 | name = "walkdir" 1235 | version = "2.4.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 1238 | dependencies = [ 1239 | "same-file", 1240 | "winapi-util", 1241 | ] 1242 | 1243 | [[package]] 1244 | name = "wasm-bindgen" 1245 | version = "0.2.88" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 1248 | dependencies = [ 1249 | "cfg-if", 1250 | "wasm-bindgen-macro", 1251 | ] 1252 | 1253 | [[package]] 1254 | name = "wasm-bindgen-backend" 1255 | version = "0.2.88" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 1258 | dependencies = [ 1259 | "bumpalo", 1260 | "log", 1261 | "once_cell", 1262 | "proc-macro2", 1263 | "quote", 1264 | "syn 2.0.39", 1265 | "wasm-bindgen-shared", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "wasm-bindgen-futures" 1270 | version = "0.4.38" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" 1273 | dependencies = [ 1274 | "cfg-if", 1275 | "js-sys", 1276 | "wasm-bindgen", 1277 | "web-sys", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "wasm-bindgen-macro" 1282 | version = "0.2.88" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 1285 | dependencies = [ 1286 | "quote", 1287 | "wasm-bindgen-macro-support", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "wasm-bindgen-macro-support" 1292 | version = "0.2.88" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 1295 | dependencies = [ 1296 | "proc-macro2", 1297 | "quote", 1298 | "syn 2.0.39", 1299 | "wasm-bindgen-backend", 1300 | "wasm-bindgen-shared", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "wasm-bindgen-shared" 1305 | version = "0.2.88" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 1308 | 1309 | [[package]] 1310 | name = "web-sys" 1311 | version = "0.3.65" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" 1314 | dependencies = [ 1315 | "js-sys", 1316 | "wasm-bindgen", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "which" 1321 | version = "4.4.2" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 1324 | dependencies = [ 1325 | "either", 1326 | "home", 1327 | "once_cell", 1328 | "rustix", 1329 | ] 1330 | 1331 | [[package]] 1332 | name = "winapi" 1333 | version = "0.3.9" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1336 | dependencies = [ 1337 | "winapi-i686-pc-windows-gnu", 1338 | "winapi-x86_64-pc-windows-gnu", 1339 | ] 1340 | 1341 | [[package]] 1342 | name = "winapi-i686-pc-windows-gnu" 1343 | version = "0.4.0" 1344 | source = "registry+https://github.com/rust-lang/crates.io-index" 1345 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1346 | 1347 | [[package]] 1348 | name = "winapi-util" 1349 | version = "0.1.6" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1352 | dependencies = [ 1353 | "winapi", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "winapi-x86_64-pc-windows-gnu" 1358 | version = "0.4.0" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1361 | 1362 | [[package]] 1363 | name = "windows" 1364 | version = "0.43.0" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" 1367 | dependencies = [ 1368 | "windows_aarch64_gnullvm 0.42.2", 1369 | "windows_aarch64_msvc 0.42.2", 1370 | "windows_i686_gnu 0.42.2", 1371 | "windows_i686_msvc 0.42.2", 1372 | "windows_x86_64_gnu 0.42.2", 1373 | "windows_x86_64_gnullvm 0.42.2", 1374 | "windows_x86_64_msvc 0.42.2", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "windows" 1379 | version = "0.48.0" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 1382 | dependencies = [ 1383 | "windows-targets", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "windows-core" 1388 | version = "0.51.1" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 1391 | dependencies = [ 1392 | "windows-targets", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "windows-sys" 1397 | version = "0.48.0" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1400 | dependencies = [ 1401 | "windows-targets", 1402 | ] 1403 | 1404 | [[package]] 1405 | name = "windows-targets" 1406 | version = "0.48.5" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1409 | dependencies = [ 1410 | "windows_aarch64_gnullvm 0.48.5", 1411 | "windows_aarch64_msvc 0.48.5", 1412 | "windows_i686_gnu 0.48.5", 1413 | "windows_i686_msvc 0.48.5", 1414 | "windows_x86_64_gnu 0.48.5", 1415 | "windows_x86_64_gnullvm 0.48.5", 1416 | "windows_x86_64_msvc 0.48.5", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "windows_aarch64_gnullvm" 1421 | version = "0.42.2" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1424 | 1425 | [[package]] 1426 | name = "windows_aarch64_gnullvm" 1427 | version = "0.48.5" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1430 | 1431 | [[package]] 1432 | name = "windows_aarch64_msvc" 1433 | version = "0.42.2" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1436 | 1437 | [[package]] 1438 | name = "windows_aarch64_msvc" 1439 | version = "0.48.5" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1442 | 1443 | [[package]] 1444 | name = "windows_i686_gnu" 1445 | version = "0.42.2" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1448 | 1449 | [[package]] 1450 | name = "windows_i686_gnu" 1451 | version = "0.48.5" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1454 | 1455 | [[package]] 1456 | name = "windows_i686_msvc" 1457 | version = "0.42.2" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1460 | 1461 | [[package]] 1462 | name = "windows_i686_msvc" 1463 | version = "0.48.5" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1466 | 1467 | [[package]] 1468 | name = "windows_x86_64_gnu" 1469 | version = "0.42.2" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1472 | 1473 | [[package]] 1474 | name = "windows_x86_64_gnu" 1475 | version = "0.48.5" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1478 | 1479 | [[package]] 1480 | name = "windows_x86_64_gnullvm" 1481 | version = "0.42.2" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1484 | 1485 | [[package]] 1486 | name = "windows_x86_64_gnullvm" 1487 | version = "0.48.5" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1490 | 1491 | [[package]] 1492 | name = "windows_x86_64_msvc" 1493 | version = "0.42.2" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1496 | 1497 | [[package]] 1498 | name = "windows_x86_64_msvc" 1499 | version = "0.48.5" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1502 | 1503 | [[package]] 1504 | name = "winnow" 1505 | version = "0.5.19" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" 1508 | dependencies = [ 1509 | "memchr", 1510 | ] 1511 | --------------------------------------------------------------------------------