diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/coremidi | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/coremidi')
29 files changed, 3197 insertions, 0 deletions
diff --git a/third_party/rust/coremidi/.cargo-checksum.json b/third_party/rust/coremidi/.cargo-checksum.json new file mode 100644 index 0000000000..07faa51931 --- /dev/null +++ b/third_party/rust/coremidi/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{".idea/.gitignore":"4073be0dd9e3ae38beba97eed085d5f023a5f2cdc101ec35f2877b185b1f1193",".travis.yml":"0d0c4ef138608fde4242675504f63116c8f49c8699f42542718f2f5cc1cebbb4","Cargo.toml":"8ed93b632b7ba2fc80d4bee845b19fd0a7b7d50f5cd60657fb4e8b8a05749aab","LICENSE":"c1c457e1b2794c9044f0e2d66a7010c223d0fb65b06e1dfbab6e21888ab09c16","README.md":"b77d39ce7ffe042c2cd3eee3e65c57323c217d1eb650ed19c56ccacd4e8854aa","ci/build-docs.sh":"644c0b2e90dc72092295152a00bf2ad6367017082f28d8fe36ee7be9821caa32","ci/publish.sh":"cc63e14aa05b503d378df854baadecd2d35bb445db06f9cdf7465fc1346fa5e3","ci/update-version.sh":"d91eedac8f507c2e4727887d2d15f27420676f46449fd0115d0ccae8e72fc95c","examples/endpoints.rs":"9327ce3dc03e49b8d2c1cf16161c9b03fc059aca9216b6d67c9ae4796fd43e40","examples/notifications.rs":"3b97c5348321dbbb172625c7b7219f1c60a47fa36d820c426d9cb7c6cc099ccf","examples/properties.rs":"1362ec194c43bcaaa1bcf7d3726eb4408eda85d290d0d706ac05ad2f71df4a33","examples/receive.rs":"28dab3891aacc2af44bffaf518eb39c69926d957a0f29f940ed44f7b16ec287a","examples/send.rs":"28e4d2558e8cf95f431c48e344101132cb499495084b59c0b6bd3107147701e4","examples/virtual-destination.rs":"f06e55093fe5117f2587d7a9799aa628c770eba2b0ca0f4804db1a72aa0bbcfc","examples/virtual-source.rs":"f0807ee9f13f27236bab8f86718d9e1645772a7d5a8095f59d0f4e5b7aeb72e9","rust-toolchain":"f6a0b9759d1af128dd09bb3f49812c052c89168e7b159e6d269036a2faba3260","src/callback.rs":"1200934d86f16ac4f678db1f2b9e2c8f621c00835de88fff36e4a317ce64aec1","src/client.rs":"05c6ecdd2fedc6f719ff93b7b6b98e911e920aa9094b342152fbcbe6d47cc64d","src/devices.rs":"33e7e85fda187a8ce0063f4db67486a96cde2794d07ecd1a73a97f5ec5ef9a13","src/endpoints/destinations.rs":"f165076d0193fc975e16e201c2d549462c87d2114095b3b4ca32b66418fb6cba","src/endpoints/mod.rs":"76dd40f64e5bda497ce84787d42035c59e3eb7cf1298307b0cb14e9eecd3e9d1","src/endpoints/sources.rs":"1fcadc52167a864dff1df09d584182e7666921af7ea72fa097f5b76ed031ddba","src/lib.rs":"ca9005ca22c90acf7379b588d268ffbbb0561760a9c10adde3356838dd994861","src/notifications.rs":"e2cec6e17c3b10e631f01c880d9637145742194cc4edbd24c7ad795f95f7b6d4","src/object.rs":"2ba5d6e17a7a99716dd9e435beffb47e169bb1e78b00713a29ea816399603a2f","src/packets.rs":"c18491c05fe2a61598b56a2ec86dbfc1490c9aea5b78534978aeb79372756e66","src/ports.rs":"25b91051635677479e2521bc3c7fbbfe29c9a2920aa19c34f95faa67b1fbb3fb","src/properties.rs":"c21b96a1265e04b59ca9cd3656d625188ce182e4afa36d849881c4521d0b8cb1"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/coremidi/.idea/.gitignore b/third_party/rust/coremidi/.idea/.gitignore new file mode 100644 index 0000000000..62c893550a --- /dev/null +++ b/third_party/rust/coremidi/.idea/.gitignore @@ -0,0 +1 @@ +.idea/
\ No newline at end of file diff --git a/third_party/rust/coremidi/.travis.yml b/third_party/rust/coremidi/.travis.yml new file mode 100644 index 0000000000..ede61a6cf3 --- /dev/null +++ b/third_party/rust/coremidi/.travis.yml @@ -0,0 +1,45 @@ +language: rust + +rust: + - stable + - beta + - nightly + +matrix: + allow_failures: + - rust: nightly + +os: + - osx + +cache: cargo + +before_script: + - ./ci/update-version.sh + +before_deploy: + - ./ci/build-docs.sh + +deploy: + - provider: pages + skip_cleanup: true + github_token: $GH_TOKEN + local_dir: target/doc + on: + tags: true + rust: stable + - provider: script + skip_cleanup: true + script: ./ci/publish.sh + on: + tags: true + rust: stable + +notifications: + email: + on_success: never + +env: + global: + - secure: "bJonMOXVCuSpIR+UnzLiitJAfhJFQn6JcTLacFzzEPa6hknrQo6SM96Mi91fvqu4S7OXevo9bIyUL+kQIfuU1VOggFxAroyF3Jw2UN+h0S9GgQnSOthnBcNi76QpZRDJWhOtoYPTq1Yp3UZjbh/OkVUfLNtfICJgAV3dM0cF4dpGPSA2d7eoXvx2wBaAa1pIuzPXvVlWPtQ9lcX8EhZgn3jp+ci2Cae5TQr9hGP7KdTAuuU2VqJYL9+JNnNpQasMiNgvqk7p2o/LIPcZIp8RbJSxkkBt+KAwMjuSEqfxv+2rL4oW7KkB7FwmY3GjJRmSXrElJZUKAJiLEUwlugr3gJ0vRJ5GsFg7qUthJ3Y/yf05iK8K6fQ5EuTqdFv8e2/CdJZBSIfhfHg6To/wu0oKPawGVZr/jAFU1MUVF8YpFHFpo4c/cdGUbwC9Qup2bQVLaxVbuaBhy3bcu7n8Aij/5W+V2lytyo5LbCDoyJwLOn7Xm4FupaAbNcdX5iT8K7WBSHk3N2ZIrSZyWrx96o450EdSWoabSEzVDx5mO2juU1Yt1iyEFI/kQwCWBVhsFx9HqN+FB0d+j06tq+OmlVqYqB24qfRFIFJGfMaYUtNM0eWFQ36aF/oKm/n3lGh+SF6q3hgKwdu8eXShkU2bS8EW0eOPIaPxQlTJFnQgjrkxGtE=" # GH_TOKEN + - secure: "Z4MoTWguJiRwyKV0crXje5nwezs4CzCzDgRjD+Dyia3y+XDDHJCii00V0TJhnWabc0W6Ym/+1iRdFCUqEewGY5/0tbkZI+pW4a6DsmMAfWKk5guXaS5QpvYCjGo4j+uXxSqLAaTbu6KxYb8YNjQ0lsGoR4yyvvB89ap+wmyjgBY/+TFZQ1g4RsAczXLvvpXgy0ELo7jR8aa1X3wS0kPwHkJ6X1/M0VydIO4/07bC2b95kJTrmnGgvPy2AtRi8jvTNJ/hRKw6WEC+qAinhAfVjvNE8GFRcvve0TWSxBX2yXBuTGmvasOYE3FT7o4A6AoiARh+pGKV8dgZGR7VwEkE7hmcykUF05NFHSb/7a4i+Ps7QJwWrb5QnX37i7eCVxo+u6GPpIJtWiN4Qftb6muhir7r0cfUTN+EyLEva5glI+MaH0+f5LDxLlfl+6tukoU96kNBdbCrvcM87DERU8pLnidL8NyyOEqALCPFCBeiSIqz1d+322MxHB5uMdQYDgxdCAsqGSBnQiuiWRzaffHt0DIL+q0UEyJp9n9lWcyV+Ie7L+IAqyxR4X9Yt9NzTOsCSNuKkEXDq31qApje2V7AVQiFVbcwiB3pxtLj6SnEAq8DEHAH1cETupEp+n+xUeyHGHaUWBUJm7v4wEC79dKOkpY14/dj0O+SXjSuZnBBDfU=" # CRATES_TOKEN diff --git a/third_party/rust/coremidi/Cargo.toml b/third_party/rust/coremidi/Cargo.toml new file mode 100644 index 0000000000..fba5458f81 --- /dev/null +++ b/third_party/rust/coremidi/Cargo.toml @@ -0,0 +1,34 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "coremidi" +version = "0.6.0" +authors = ["Christian Perez-Llamas"] +description = "CoreMIDI library for Rust" +homepage = "https://github.com/chris-zen/coremidi" +documentation = "https://chris-zen.github.io/coremidi/coremidi/" +readme = "README.md" +keywords = [ + "CoreMIDI", + "MIDI", + "OSX", + "macOS", + "music", +] +license = "MIT" +repository = "https://github.com/chris-zen/coremidi" + +[dependencies] +core-foundation = "0.9.1" +core-foundation-sys = "0.8.2" +coremidi-sys = "3.0.1" diff --git a/third_party/rust/coremidi/LICENSE b/third_party/rust/coremidi/LICENSE new file mode 100644 index 0000000000..c3895e808f --- /dev/null +++ b/third_party/rust/coremidi/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Christian Perez-Llamas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/rust/coremidi/README.md b/third_party/rust/coremidi/README.md new file mode 100644 index 0000000000..c00412781a --- /dev/null +++ b/third_party/rust/coremidi/README.md @@ -0,0 +1,109 @@ +# coremidi + +This is a [CoreMIDI](https://developer.apple.com/reference/coremidi) library for Rust built on top of the low-level bindings [coremidi-sys](https://github.com/jonas-k/coremidi-sys). +CoreMIDI is a macOS framework that provides APIs for communicating with MIDI (Musical Instrument Digital Interface) devices, including hardware keyboards and synthesizers. + +This library preserves the fundamental concepts behind the CoreMIDI framework, while being Rust idiomatic. This means that if you already know CoreMIDI, you will find very easy to start using it. + +The **documentation** for the master branch can be found here: https://chris-zen.github.io/coremidi/coremidi/ + +Please see the [examples](examples) for an idea on how it looks like, but if you are eager to see some code, this is how you would send some note: + +```rust +extern crate coremidi; +use coremidi::{Client, Destinations, PacketBuffer}; +use std::time::Duration; +use std::thread; +let client = Client::new("example-client").unwrap(); +let output_port = client.output_port("example-port").unwrap(); +let destination = Destinations::from_index(0).unwrap(); +let note_on = PacketBuffer::new(0, &[0x90, 0x40, 0x7f]); +let note_off = PacketBuffer::new(0, &[0x80, 0x40, 0x7f]); +output_port.send(&destination, ¬e_on).unwrap(); +thread::sleep(Duration::from_millis(1000)); +output_port.send(&destination, ¬e_off).unwrap(); +``` + +If you are looking for a portable MIDI library then you can look into: +- [midir](https://github.com/Boddlnagg/midir) (which is using this lib) +- [portmidi-rs](https://github.com/musitdev/portmidi-rs) + +For handling low level MIDI data you may look into: +- [rimd](https://github.com/RustAudio/rimd) +- [midi-rs](https://github.com/samdoshi/midi-rs) + +[![Build Status](https://travis-ci.org/chris-zen/coremidi.svg?branch=master)](https://travis-ci.org/chris-zen/coremidi) +[![Crates.io](https://img.shields.io/crates/v/coremidi.svg)](https://crates.io/crates/coremidi) +[![Crates.io](https://img.shields.io/crates/d/coremidi.svg)](https://crates.io/crates/coremidi) +[![Crates.io](https://img.shields.io/crates/dv/coremidi.svg)](https://crates.io/crates/coremidi) +[![GitHub tag](https://img.shields.io/github/tag/chris-zen/coremidi.svg)](https://travis-ci.org/chris-zen/coremidi) +[![Minimum rustc version](https://img.shields.io/badge/rustc-1.36+-blue.svg)](https://blog.rust-lang.org/2019/07/04/Rust-1.36.0.html) +[![Join the chat at https://gitter.im/coremidi/Lobby](https://badges.gitter.im/coremidi/Lobby.svg)](https://gitter.im/coremidi/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +# Installation + +The library is published into [crates.io](https://crates.io/crates/coremidi), so it can be used by adding the following lines into your `Cargo.toml` file (but remember to update the version number accordingly): + +```toml +[dependencies] +coremidi = "^0.6.0" +``` + +If you prefer to live in the edge ;-) you can use the master branch by including this instead: + +```toml +[dependencies] +coremidi = { git = "https://github.com/chris-zen/coremidi", branch="master" } +``` + +To play with the source code yourself you can clone the repo and build the code and documentation with the following commands: + +```sh +git clone https://github.com/chris-zen/coremidi.git +cd coremidi +cargo build +cargo test +cargo doc +open target/doc/coremidi/index.html +``` + +# Examples + +The examples can be run with: + +```sh +cargo run --example send +``` + +These are the provided examples: + +- [endpoints](examples/endpoints.rs): how to enumerate sources and destinations. +- [send](examples/send.rs): how to create an output port and send MIDI messages. +- [receive](examples/receive.rs): how to create an input port and receive MIDI messages. +- [virtual-source](examples/virtual-source.rs): how to create a virtual source and generate MIDI messages. +- [virtual-destination](examples/virtual-destination.rs): how to create a virtual destination and receive MIDI messages. +- [properties](examples/properties.rs): how to set and get properties on MIDI objects. +- [notifications](examples/notifications.rs): how to receive MIDI client notifications. + +# Roadmap + +- [x] Enumerate destinations +- [x] Create output ports +- [x] Create a PacketList from MIDI bytes +- [x] Send a PacketList into an output port +- [x] Create virtual sources +- [x] Support a virtual source receiving a PacketList +- [x] Flush output +- [x] Enumerate sources +- [x] Create input ports +- [x] Support callbacks from input messages +- [x] Connect and disconnect sources +- [x] Add support to build PacketList (PacketBuffer) +- [x] Create virtual destinations with callback +- [x] Stop and restart MIDI I/O +- [x] MIDI Objects and properties +- [x] Client notifications +- [ ] Support Sysex +- [ ] Support devices +- [ ] Support entities +- [ ] MIDIThru connections diff --git a/third_party/rust/coremidi/ci/build-docs.sh b/third_party/rust/coremidi/ci/build-docs.sh new file mode 100755 index 0000000000..1fc2214bfd --- /dev/null +++ b/third_party/rust/coremidi/ci/build-docs.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -ex + +cargo doc + +REPO_NAME=$(echo $TRAVIS_REPO_SLUG | cut -d '/' -f 2) +echo "<meta http-equiv=refresh content=0;url=$REPO_NAME/index.html>" > target/doc/index.html diff --git a/third_party/rust/coremidi/ci/publish.sh b/third_party/rust/coremidi/ci/publish.sh new file mode 100755 index 0000000000..cd4ee25d4b --- /dev/null +++ b/third_party/rust/coremidi/ci/publish.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -ex + +cargo login $CRATES_TOKEN + +# --allow-dirty is required because Cargo.toml version has been updated by an script +cargo publish --allow-dirty diff --git a/third_party/rust/coremidi/ci/update-version.sh b/third_party/rust/coremidi/ci/update-version.sh new file mode 100755 index 0000000000..2874dc8e0b --- /dev/null +++ b/third_party/rust/coremidi/ci/update-version.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -ex + +VERSION=${TRAVIS_TAG:-$(git describe --tags)} + +sed -i '' "s/version = \".*\"/version = \"$VERSION\"/g" Cargo.toml diff --git a/third_party/rust/coremidi/examples/endpoints.rs b/third_party/rust/coremidi/examples/endpoints.rs new file mode 100644 index 0000000000..1411d1d2aa --- /dev/null +++ b/third_party/rust/coremidi/examples/endpoints.rs @@ -0,0 +1,24 @@ +extern crate coremidi; + +fn main() { + println!("System destinations:"); + + for (i, destination) in coremidi::Destinations.into_iter().enumerate() { + let display_name = get_display_name(&destination); + println!("[{}] {}", i, display_name); + } + + println!(); + println!("System sources:"); + + for (i, source) in coremidi::Sources.into_iter().enumerate() { + let display_name = get_display_name(&source); + println!("[{}] {}", i, display_name); + } +} + +fn get_display_name(endpoint: &coremidi::Endpoint) -> String { + endpoint + .display_name() + .unwrap_or_else(|| "[Unknown Display Name]".to_string()) +} diff --git a/third_party/rust/coremidi/examples/notifications.rs b/third_party/rust/coremidi/examples/notifications.rs new file mode 100644 index 0000000000..ef58c43d26 --- /dev/null +++ b/third_party/rust/coremidi/examples/notifications.rs @@ -0,0 +1,31 @@ +extern crate core_foundation; +extern crate coremidi; + +use coremidi::{Client, Notification}; + +use core_foundation::runloop::{kCFRunLoopDefaultMode, CFRunLoopRunInMode}; + +fn main() { + println!("Logging MIDI Client Notifications"); + println!("Will Quit Automatically After 10 Seconds"); + println!(); + + let _client = Client::new_with_notifications("example-client", print_notification).unwrap(); + + // As the MIDIClientCreate docs say (https://developer.apple.com/documentation/coremidi/1495360-midiclientcreate), + // notifications will be delivered on the run loop that was current when + // Client was created. + // + // In order to actually receive the notifications, a run loop must be + // running. Since this sample app does not use an app framework like + // UIApplication or NSApplication, it does not have a run loop running yet. + // So we start one that lasts for 10 seconds with the following line. + // + // You may not have to do this in your app - see https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW24 + // for information about when run loops are running automatically. + unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10.0, 0) }; +} + +fn print_notification(notification: &Notification) { + println!("Received Notification: {:?} \r", notification); +} diff --git a/third_party/rust/coremidi/examples/properties.rs b/third_party/rust/coremidi/examples/properties.rs new file mode 100644 index 0000000000..b673a22d9b --- /dev/null +++ b/third_party/rust/coremidi/examples/properties.rs @@ -0,0 +1,25 @@ +extern crate coremidi; + +use coremidi::{Client, PacketList, Properties, PropertyGetter, PropertySetter}; + +fn main() { + let client = Client::new("Example Client").unwrap(); + + let callback = |packet_list: &PacketList| { + println!("{}", packet_list); + }; + + // Creates a virtual destination, then gets its properties + let destination = client + .virtual_destination("Example Destination", callback) + .unwrap(); + + println!("Created Virtual Destination..."); + + // How to get a property + let name: String = Properties::name().value_from(&destination).unwrap(); + println!("With Name: {}", name); + + // How to set a property + Properties::private().set_value(&destination, true).unwrap(); +} diff --git a/third_party/rust/coremidi/examples/receive.rs b/third_party/rust/coremidi/examples/receive.rs new file mode 100644 index 0000000000..93bb9c23f2 --- /dev/null +++ b/third_party/rust/coremidi/examples/receive.rs @@ -0,0 +1,71 @@ +extern crate coremidi; + +use std::env; + +fn main() { + let source_index = get_source_index(); + println!("Source index: {}", source_index); + + let source = coremidi::Source::from_index(source_index).unwrap(); + println!("Source display name: {}", source.display_name().unwrap()); + + let client = coremidi::Client::new("Example Client").unwrap(); + + let callback = |packet_list: &coremidi::PacketList| { + println!("{}", packet_list); + }; + + let input_port = client.input_port("Example Port", callback).unwrap(); + input_port.connect_source(&source).unwrap(); + + let mut input_line = String::new(); + println!("Press Enter to Finish"); + std::io::stdin() + .read_line(&mut input_line) + .expect("Failed to read line"); + + input_port.disconnect_source(&source).unwrap(); +} + +fn get_source_index() -> usize { + let mut args_iter = env::args(); + let tool_name = args_iter + .next() + .and_then(|path| { + path.split(std::path::MAIN_SEPARATOR) + .last() + .map(|v| v.to_string()) + }) + .unwrap_or_else(|| "receive".to_string()); + + match args_iter.next() { + Some(arg) => match arg.parse::<usize>() { + Ok(index) => { + if index >= coremidi::Sources::count() { + println!("Source index out of range: {}", index); + std::process::exit(-1); + } + index + } + Err(_) => { + println!("Wrong source index: {}", arg); + std::process::exit(-1); + } + }, + None => { + println!("Usage: {} <source-index>", tool_name); + println!(); + println!("Available Sources:"); + print_sources(); + std::process::exit(-1); + } + } +} + +fn print_sources() { + for (i, source) in coremidi::Sources.into_iter().enumerate() { + if let Some(display_name) = source.display_name() { + println!("[{}] {}", i, display_name) + } + } +} diff --git a/third_party/rust/coremidi/examples/send.rs b/third_party/rust/coremidi/examples/send.rs new file mode 100644 index 0000000000..804710c734 --- /dev/null +++ b/third_party/rust/coremidi/examples/send.rs @@ -0,0 +1,85 @@ +extern crate coremidi; + +use std::env; +use std::thread; +use std::time::Duration; + +fn main() { + let destination_index = get_destination_index(); + println!("Destination index: {}", destination_index); + + let destination = coremidi::Destination::from_index(destination_index).unwrap(); + println!( + "Destination display name: {}", + destination.display_name().unwrap() + ); + + let client = coremidi::Client::new("Example Client").unwrap(); + let output_port = client.output_port("Example Port").unwrap(); + + let note_on = create_note_on(0, 64, 127); + let note_off = create_note_off(0, 64, 127); + + for i in 0..10 { + println!("[{}] Sending note ...", i); + + output_port.send(&destination, ¬e_on).unwrap(); + + thread::sleep(Duration::from_millis(1000)); + + output_port.send(&destination, ¬e_off).unwrap(); + } +} + +fn get_destination_index() -> usize { + let mut args_iter = env::args(); + let tool_name = args_iter + .next() + .and_then(|path| { + path.split(std::path::MAIN_SEPARATOR) + .last() + .map(|v| v.to_string()) + }) + .unwrap_or_else(|| "send".to_string()); + + match args_iter.next() { + Some(arg) => match arg.parse::<usize>() { + Ok(index) => { + if index >= coremidi::Destinations::count() { + println!("Destination index out of range: {}", index); + std::process::exit(-1); + } + index + } + Err(_) => { + println!("Wrong destination index: {}", arg); + std::process::exit(-1); + } + }, + None => { + println!("Usage: {} <destination-index>", tool_name); + println!(); + println!("Available Destinations:"); + print_destinations(); + std::process::exit(-1); + } + } +} + +fn print_destinations() { + for (i, destination) in coremidi::Destinations.into_iter().enumerate() { + if let Some(display_name) = destination.display_name() { + println!("[{}] {}", i, display_name) + } + } +} + +fn create_note_on(channel: u8, note: u8, velocity: u8) -> coremidi::PacketBuffer { + let data = &[0x90 | (channel & 0x0f), note & 0x7f, velocity & 0x7f]; + coremidi::PacketBuffer::new(0, data) +} + +fn create_note_off(channel: u8, note: u8, velocity: u8) -> coremidi::PacketBuffer { + let data = &[0x80 | (channel & 0x0f), note & 0x7f, velocity & 0x7f]; + coremidi::PacketBuffer::new(0, data) +} diff --git a/third_party/rust/coremidi/examples/virtual-destination.rs b/third_party/rust/coremidi/examples/virtual-destination.rs new file mode 100644 index 0000000000..989dcebb46 --- /dev/null +++ b/third_party/rust/coremidi/examples/virtual-destination.rs @@ -0,0 +1,20 @@ +extern crate coremidi; + +fn main() { + let client = coremidi::Client::new("Example Client").unwrap(); + + let callback = |packet_list: &coremidi::PacketList| { + println!("{}", packet_list); + }; + + let _destination = client + .virtual_destination("Example Destination", callback) + .unwrap(); + + let mut input_line = String::new(); + println!("Created Virtual Destination \"Example Destination\""); + println!("Press Enter to Finish"); + std::io::stdin() + .read_line(&mut input_line) + .expect("Failed to read line"); +} diff --git a/third_party/rust/coremidi/examples/virtual-source.rs b/third_party/rust/coremidi/examples/virtual-source.rs new file mode 100644 index 0000000000..28867162ce --- /dev/null +++ b/third_party/rust/coremidi/examples/virtual-source.rs @@ -0,0 +1,32 @@ +extern crate coremidi; + +use std::thread; +use std::time::Duration; + +fn main() { + let client = coremidi::Client::new("Example Client").unwrap(); + let source = client.virtual_source("Example Source").unwrap(); + + let note_on = create_note_on(0, 64, 127); + let note_off = create_note_off(0, 64, 127); + + for i in 0..10 { + println!("[{}] Sending note...", i); + + source.received(¬e_on).unwrap(); + + thread::sleep(Duration::from_millis(1000)); + + source.received(¬e_off).unwrap(); + } +} + +fn create_note_on(channel: u8, note: u8, velocity: u8) -> coremidi::PacketBuffer { + let data = &[0x90 | (channel & 0x0f), note & 0x7f, velocity & 0x7f]; + coremidi::PacketBuffer::new(0, data) +} + +fn create_note_off(channel: u8, note: u8, velocity: u8) -> coremidi::PacketBuffer { + let data = &[0x80 | (channel & 0x0f), note & 0x7f, velocity & 0x7f]; + coremidi::PacketBuffer::new(0, data) +} diff --git a/third_party/rust/coremidi/rust-toolchain b/third_party/rust/coremidi/rust-toolchain new file mode 100644 index 0000000000..ba0a719118 --- /dev/null +++ b/third_party/rust/coremidi/rust-toolchain @@ -0,0 +1 @@ +1.51.0 diff --git a/third_party/rust/coremidi/src/callback.rs b/third_party/rust/coremidi/src/callback.rs new file mode 100644 index 0000000000..c15ca0da30 --- /dev/null +++ b/third_party/rust/coremidi/src/callback.rs @@ -0,0 +1,35 @@ +// A lifetime-managed wrapper for callback functions +#[derive(Debug, PartialEq)] +pub struct BoxedCallback<T>(*mut Box<dyn FnMut(&T)>); + +impl<T> BoxedCallback<T> { + pub fn new<F: FnMut(&T) + Send + 'static>(f: F) -> BoxedCallback<T> { + BoxedCallback(Box::into_raw(Box::new(Box::new(f)))) + } + + pub fn null() -> BoxedCallback<T> { + BoxedCallback(::std::ptr::null_mut()) + } + + pub fn raw_ptr(&mut self) -> *mut ::std::os::raw::c_void { + self.0 as *mut ::std::os::raw::c_void + } + + // must not be null + pub unsafe fn call_from_raw_ptr(raw_ptr: *mut ::std::os::raw::c_void, arg: &T) { + let callback = &mut *(raw_ptr as *mut Box<dyn FnMut(&T)>); + callback(arg); + } +} + +unsafe impl<T> Send for BoxedCallback<T> {} + +impl<T> Drop for BoxedCallback<T> { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { + let _ = Box::from_raw(self.0); + } + } + } +} diff --git a/third_party/rust/coremidi/src/client.rs b/third_party/rust/coremidi/src/client.rs new file mode 100644 index 0000000000..240240d569 --- /dev/null +++ b/third_party/rust/coremidi/src/client.rs @@ -0,0 +1,238 @@ +use core_foundation::{ + base::{OSStatus, TCFType}, + string::CFString, +}; + +use coremidi_sys::{ + MIDIClientCreate, MIDIClientDispose, MIDIDestinationCreate, MIDIInputPortCreate, + MIDINotification, MIDIOutputPortCreate, MIDIPacketList, MIDISourceCreate, +}; + +use std::{mem::MaybeUninit, ops::Deref, os::raw::c_void, panic::catch_unwind, ptr}; + +use crate::{ + callback::BoxedCallback, + endpoints::{destinations::VirtualDestination, sources::VirtualSource, Endpoint}, + notifications::Notification, + object::Object, + packets::PacketList, + ports::{InputPort, OutputPort, Port}, + result_from_status, +}; + +/// A [MIDI client](https://developer.apple.com/reference/coremidi/midiclientref). +/// +/// An object maintaining per-client state. +/// +/// A simple example to create a Client: +/// +/// ```rust,no_run +/// let client = coremidi::Client::new("example-client").unwrap(); +/// ``` +#[derive(Debug)] +pub struct Client { + // Order is important, object needs to be dropped first + object: Object, + callback: BoxedCallback<Notification>, +} + +impl Client { + /// Creates a new CoreMIDI client with support for notifications. + /// See [MIDIClientCreate](https://developer.apple.com/reference/coremidi/1495360-midiclientcreate). + /// + /// The notification callback will be called on the [run loop](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html) + /// that was current when this associated function is called. + /// + /// It follows that this particular run loop needs to be running in order to + /// actually receive notifications. The run loop can be started after the + /// client has been created if need be. + pub fn new_with_notifications<F>(name: &str, callback: F) -> Result<Client, OSStatus> + where + F: FnMut(&Notification) + Send + 'static, + { + let client_name = CFString::new(name); + let mut client_ref = MaybeUninit::uninit(); + let mut boxed_callback = BoxedCallback::new(callback); + let status = unsafe { + MIDIClientCreate( + client_name.as_concrete_TypeRef(), + Some(Self::notify_proc as extern "C" fn(_, _)), + boxed_callback.raw_ptr(), + client_ref.as_mut_ptr(), + ) + }; + result_from_status(status, || { + let client_ref = unsafe { client_ref.assume_init() }; + Client { + object: Object(client_ref), + callback: boxed_callback, + } + }) + } + + /// Creates a new CoreMIDI client. + /// See [MIDIClientCreate](https://developer.apple.com/reference/coremidi/1495360-midiclientcreate). + /// + pub fn new(name: &str) -> Result<Client, OSStatus> { + let client_name = CFString::new(name); + let mut client_ref = MaybeUninit::uninit(); + let status = unsafe { + MIDIClientCreate( + client_name.as_concrete_TypeRef(), + None, + ptr::null_mut(), + client_ref.as_mut_ptr(), + ) + }; + result_from_status(status, || { + let client_ref = unsafe { client_ref.assume_init() }; + Client { + object: Object(client_ref), + callback: BoxedCallback::null(), + } + }) + } + + /// Creates an output port through which the client may send outgoing MIDI messages to any MIDI destination. + /// See [MIDIOutputPortCreate](https://developer.apple.com/reference/coremidi/1495166-midioutputportcreate). + /// + pub fn output_port(&self, name: &str) -> Result<OutputPort, OSStatus> { + let port_name = CFString::new(name); + let mut port_ref = MaybeUninit::uninit(); + let status = unsafe { + MIDIOutputPortCreate( + self.object.0, + port_name.as_concrete_TypeRef(), + port_ref.as_mut_ptr(), + ) + }; + result_from_status(status, || { + let port_ref = unsafe { port_ref.assume_init() }; + OutputPort { + port: Port { + object: Object(port_ref), + }, + } + }) + } + + /// Creates an input port through which the client may receive incoming MIDI messages from any MIDI source. + /// See [MIDIInputPortCreate](https://developer.apple.com/reference/coremidi/1495225-midiinputportcreate). + /// + pub fn input_port<F>(&self, name: &str, callback: F) -> Result<InputPort, OSStatus> + where + F: FnMut(&PacketList) + Send + 'static, + { + let port_name = CFString::new(name); + let mut port_ref = MaybeUninit::uninit(); + let mut box_callback = BoxedCallback::new(callback); + let status = unsafe { + MIDIInputPortCreate( + self.object.0, + port_name.as_concrete_TypeRef(), + Some(Self::read_proc as extern "C" fn(_, _, _)), + box_callback.raw_ptr(), + port_ref.as_mut_ptr(), + ) + }; + result_from_status(status, || { + let port_ref = unsafe { port_ref.assume_init() }; + InputPort { + port: Port { + object: Object(port_ref), + }, + callback: box_callback, + } + }) + } + + /// Creates a virtual source in the client. + /// See [MIDISourceCreate](https://developer.apple.com/reference/coremidi/1495212-midisourcecreate). + /// + pub fn virtual_source(&self, name: &str) -> Result<VirtualSource, OSStatus> { + let virtual_source_name = CFString::new(name); + let mut virtual_source = MaybeUninit::uninit(); + let status = unsafe { + MIDISourceCreate( + self.object.0, + virtual_source_name.as_concrete_TypeRef(), + virtual_source.as_mut_ptr(), + ) + }; + result_from_status(status, || { + let virtual_source = unsafe { virtual_source.assume_init() }; + VirtualSource { + endpoint: Endpoint { + object: Object(virtual_source), + }, + } + }) + } + + /// Creates a virtual destination in the client. + /// See [MIDIDestinationCreate](https://developer.apple.com/reference/coremidi/1495347-mididestinationcreate). + /// + pub fn virtual_destination<F>( + &self, + name: &str, + callback: F, + ) -> Result<VirtualDestination, OSStatus> + where + F: FnMut(&PacketList) + Send + 'static, + { + let virtual_destination_name = CFString::new(name); + let mut virtual_destination = MaybeUninit::uninit(); + let mut boxed_callback = BoxedCallback::new(callback); + let status = unsafe { + MIDIDestinationCreate( + self.object.0, + virtual_destination_name.as_concrete_TypeRef(), + Some(Self::read_proc as extern "C" fn(_, _, _)), + boxed_callback.raw_ptr(), + virtual_destination.as_mut_ptr(), + ) + }; + result_from_status(status, || { + let virtual_destination = unsafe { virtual_destination.assume_init() }; + VirtualDestination { + endpoint: Endpoint { + object: Object(virtual_destination), + }, + callback: boxed_callback, + } + }) + } + + extern "C" fn notify_proc(notification_ptr: *const MIDINotification, ref_con: *mut c_void) { + let _ = catch_unwind(|| unsafe { + if let Ok(notification) = Notification::from(&*notification_ptr) { + BoxedCallback::call_from_raw_ptr(ref_con, ¬ification) + } + }); + } + + extern "C" fn read_proc( + pktlist: *const MIDIPacketList, + read_proc_ref_con: *mut c_void, + _src_conn_ref_con: *mut c_void, + ) { + let _ = catch_unwind(|| unsafe { + let packet_list = &*(pktlist as *const PacketList); + BoxedCallback::call_from_raw_ptr(read_proc_ref_con, packet_list); + }); + } +} + +impl Deref for Client { + type Target = Object; + + fn deref(&self) -> &Object { + &self.object + } +} + +impl Drop for Client { + fn drop(&mut self) { + unsafe { MIDIClientDispose(self.object.0) }; + } +} diff --git a/third_party/rust/coremidi/src/devices.rs b/third_party/rust/coremidi/src/devices.rs new file mode 100644 index 0000000000..d8f8ba6449 --- /dev/null +++ b/third_party/rust/coremidi/src/devices.rs @@ -0,0 +1,20 @@ +use std::ops::Deref; + +use crate::object::Object; + +/// A [MIDI object](https://developer.apple.com/reference/coremidi/midideviceref). +/// +/// A MIDI device or external device, containing entities. +/// +#[derive(Debug, PartialEq)] +pub struct Device { + pub(crate) object: Object, +} + +impl Deref for Device { + type Target = Object; + + fn deref(&self) -> &Object { + &self.object + } +} diff --git a/third_party/rust/coremidi/src/endpoints/destinations.rs b/third_party/rust/coremidi/src/endpoints/destinations.rs new file mode 100644 index 0000000000..2a28770340 --- /dev/null +++ b/third_party/rust/coremidi/src/endpoints/destinations.rs @@ -0,0 +1,138 @@ +use coremidi_sys::{ + ItemCount, MIDIEndpointDispose, MIDIGetDestination, MIDIGetNumberOfDestinations, +}; + +use std::ops::Deref; + +use crate::{callback::BoxedCallback, object::Object, packets::PacketList}; + +use super::Endpoint; + +/// A [MIDI source](https://developer.apple.com/reference/coremidi/midiendpointref) owned by an entity. +/// +/// A source can be created from an index like this: +/// +/// ```rust,no_run +/// let source = coremidi::Destination::from_index(0).unwrap(); +/// println!("The source at index 0 has display name '{}'", source.display_name().unwrap()); +/// ``` +/// +#[derive(Debug)] +pub struct Destination { + pub(crate) endpoint: Endpoint, +} + +impl Destination { + /// Create a destination endpoint from its index. + /// See [MIDIGetDestination](https://developer.apple.com/reference/coremidi/1495108-midigetdestination) + /// + pub fn from_index(index: usize) -> Option<Destination> { + let endpoint_ref = unsafe { MIDIGetDestination(index as ItemCount) }; + match endpoint_ref { + 0 => None, + _ => Some(Destination { + endpoint: Endpoint { + object: Object(endpoint_ref), + }, + }), + } + } +} + +impl Deref for Destination { + type Target = Endpoint; + + fn deref(&self) -> &Endpoint { + &self.endpoint + } +} + +/// Destination endpoints available in the system. +/// +/// The number of destinations available in the system can be retrieved with: +/// +/// ``` +/// let number_of_destinations = coremidi::Destinations::count(); +/// ``` +/// +/// The destinations in the system can be iterated as: +/// +/// ```rust,no_run +/// for destination in coremidi::Destinations { +/// println!("{}", destination.display_name().unwrap()); +/// } +/// ``` +/// +pub struct Destinations; + +impl Destinations { + /// Get the number of destinations available in the system for sending MIDI messages. + /// See [MIDIGetNumberOfDestinations](https://developer.apple.com/reference/coremidi/1495309-midigetnumberofdestinations). + /// + pub fn count() -> usize { + unsafe { MIDIGetNumberOfDestinations() as usize } + } +} + +impl IntoIterator for Destinations { + type Item = Destination; + type IntoIter = DestinationsIterator; + + fn into_iter(self) -> Self::IntoIter { + DestinationsIterator { + index: 0, + count: Self::count(), + } + } +} + +pub struct DestinationsIterator { + index: usize, + count: usize, +} + +impl Iterator for DestinationsIterator { + type Item = Destination; + + fn next(&mut self) -> Option<Destination> { + if self.index < self.count { + let destination = Destination::from_index(self.index); + self.index += 1; + destination + } else { + None + } + } +} + +/// A [MIDI virtual destination](https://developer.apple.com/reference/coremidi/1495347-mididestinationcreate) owned by a client. +/// +/// A virtual destination can be created like: +/// +/// ```rust,no_run +/// let client = coremidi::Client::new("example-client").unwrap(); +/// client.virtual_destination("example-destination", |packet_list| println!("{}", packet_list)).unwrap(); +/// ``` +/// +#[derive(Debug)] +pub struct VirtualDestination { + // Note: the order is important here, endpoint needs to be dropped first + pub(crate) endpoint: Endpoint, + pub(crate) callback: BoxedCallback<PacketList>, +} + +impl VirtualDestination {} + +impl Deref for VirtualDestination { + type Target = Endpoint; + + fn deref(&self) -> &Endpoint { + &self.endpoint + } +} + +impl Drop for VirtualDestination { + fn drop(&mut self) { + unsafe { MIDIEndpointDispose(self.endpoint.object.0) }; + } +} diff --git a/third_party/rust/coremidi/src/endpoints/mod.rs b/third_party/rust/coremidi/src/endpoints/mod.rs new file mode 100644 index 0000000000..1a321a446e --- /dev/null +++ b/third_party/rust/coremidi/src/endpoints/mod.rs @@ -0,0 +1,47 @@ +pub mod destinations; +pub mod sources; + +use std::ops::Deref; + +use core_foundation_sys::base::OSStatus; +use coremidi_sys::MIDIFlushOutput; + +use crate::object::Object; + +/// A MIDI source or source, owned by an entity. +/// See [MIDIEndpointRef](https://developer.apple.com/reference/coremidi/midiendpointref). +/// +/// You don't need to create an endpoint directly, instead you can create system sources and sources or virtual ones from a client. +/// +#[derive(Debug)] +pub struct Endpoint { + pub(crate) object: Object, +} + +impl Endpoint { + /// Unschedules previously-sent packets. + /// See [MIDIFlushOutput](https://developer.apple.com/reference/coremidi/1495312-midiflushoutput). + /// + pub fn flush(&self) -> Result<(), OSStatus> { + let status = unsafe { MIDIFlushOutput(self.object.0) }; + if status == 0 { + Ok(()) + } else { + Err(status) + } + } +} + +impl AsRef<Object> for Endpoint { + fn as_ref(&self) -> &Object { + &self.object + } +} + +impl Deref for Endpoint { + type Target = Object; + + fn deref(&self) -> &Object { + &self.object + } +} diff --git a/third_party/rust/coremidi/src/endpoints/sources.rs b/third_party/rust/coremidi/src/endpoints/sources.rs new file mode 100644 index 0000000000..dcef316ce6 --- /dev/null +++ b/third_party/rust/coremidi/src/endpoints/sources.rs @@ -0,0 +1,151 @@ +use core_foundation_sys::base::OSStatus; + +use coremidi_sys::{ + ItemCount, MIDIEndpointDispose, MIDIGetNumberOfSources, MIDIGetSource, MIDIReceived, +}; + +use std::ops::Deref; + +use crate::object::Object; +use crate::packets::PacketList; + +use super::Endpoint; + +/// A [MIDI source](https://developer.apple.com/reference/coremidi/midiendpointref) owned by an entity. +/// +/// A source can be created from an index like this: +/// +/// ```rust,no_run +/// let source = coremidi::Source::from_index(0).unwrap(); +/// println!("The source at index 0 has display name '{}'", source.display_name().unwrap()); +/// ``` +/// +#[derive(Debug)] +pub struct Source { + endpoint: Endpoint, +} + +impl Source { + /// Create a source endpoint from its index. + /// See [MIDIGetSource](https://developer.apple.com/reference/coremidi/1495168-midigetsource) + /// + pub fn from_index(index: usize) -> Option<Source> { + let endpoint_ref = unsafe { MIDIGetSource(index as ItemCount) }; + match endpoint_ref { + 0 => None, + _ => Some(Source { + endpoint: Endpoint { + object: Object(endpoint_ref), + }, + }), + } + } +} + +impl Deref for Source { + type Target = Endpoint; + + fn deref(&self) -> &Endpoint { + &self.endpoint + } +} + +/// Source endpoints available in the system. +/// +/// The number of sources available in the system can be retrieved with: +/// +/// ```rust,no_run +/// let number_of_sources = coremidi::Sources::count(); +/// ``` +/// +/// The sources in the system can be iterated as: +/// +/// ```rust,no_run +/// for source in coremidi::Sources { +/// println!("{}", source.display_name().unwrap()); +/// } +/// ``` +/// +pub struct Sources; + +impl Sources { + /// Get the number of sources available in the system for receiving MIDI messages. + /// See [MIDIGetNumberOfSources](https://developer.apple.com/reference/coremidi/1495116-midigetnumberofsources). + /// + pub fn count() -> usize { + unsafe { MIDIGetNumberOfSources() as usize } + } +} + +impl IntoIterator for Sources { + type Item = Source; + type IntoIter = SourcesIterator; + + fn into_iter(self) -> Self::IntoIter { + SourcesIterator { + index: 0, + count: Self::count(), + } + } +} + +pub struct SourcesIterator { + index: usize, + count: usize, +} + +impl Iterator for SourcesIterator { + type Item = Source; + + fn next(&mut self) -> Option<Source> { + if self.index < self.count { + let source = Source::from_index(self.index); + self.index += 1; + source + } else { + None + } + } +} + +/// A [MIDI virtual source](https://developer.apple.com/reference/coremidi/1495212-midisourcecreate) owned by a client. +/// +/// A virtual source can be created like: +/// +/// ```rust,no_run +/// let client = coremidi::Client::new("example-client").unwrap(); +/// let source = client.virtual_source("example-source").unwrap(); +/// ``` +/// +#[derive(Debug)] +pub struct VirtualSource { + pub(crate) endpoint: Endpoint, +} + +impl VirtualSource { + /// Distributes incoming MIDI from a source to the client input ports which are connected to that source. + /// See [MIDIReceived](https://developer.apple.com/reference/coremidi/1495276-midireceived) + /// + pub fn received(&self, packet_list: &PacketList) -> Result<(), OSStatus> { + let status = unsafe { MIDIReceived(self.endpoint.object.0, packet_list.as_ptr()) }; + if status == 0 { + Ok(()) + } else { + Err(status) + } + } +} + +impl Deref for VirtualSource { + type Target = Endpoint; + + fn deref(&self) -> &Endpoint { + &self.endpoint + } +} + +impl Drop for VirtualSource { + fn drop(&mut self) { + unsafe { MIDIEndpointDispose(self.endpoint.object.0) }; + } +} diff --git a/third_party/rust/coremidi/src/lib.rs b/third_party/rust/coremidi/src/lib.rs new file mode 100644 index 0000000000..67a71724d8 --- /dev/null +++ b/third_party/rust/coremidi/src/lib.rs @@ -0,0 +1,93 @@ +#![crate_name = "coremidi"] +#![crate_type = "lib"] +#![doc(html_root_url = "https://chris-zen.github.io/coremidi/")] + +/*! +This is a [CoreMIDI](https://developer.apple.com/reference/coremidi) library for Rust built on top of the low-level bindings [coremidi-sys](https://github.com/jonas-k/coremidi-sys). +CoreMIDI is a macOS framework that provides APIs for communicating with MIDI (Musical Instrument Digital Interface) devices, including hardware keyboards and synthesizers. + +This library preserves the fundamental concepts behind the CoreMIDI framework, while being Rust idiomatic. This means that if you already know CoreMIDI, you will find very easy to start using it. + +Please see the [examples](https://github.com/chris-zen/coremidi/tree/master/examples) for getting an idea of how it looks like, but if you are eager to see an example, this is how you would send some note: + +```rust,no_run +extern crate coremidi; +use std::time::Duration; +use std::thread; +let client = coremidi::Client::new("example-client").unwrap(); +let output_port = client.output_port("example-port").unwrap(); +let destination = coremidi::Destination::from_index(0).unwrap(); +let note_on = coremidi::PacketBuffer::new(0, &[0x90, 0x40, 0x7f]); +let note_off = coremidi::PacketBuffer::new(0, &[0x80, 0x40, 0x7f]); +output_port.send(&destination, ¬e_on).unwrap(); +thread::sleep(Duration::from_millis(1000)); +output_port.send(&destination, ¬e_off).unwrap(); +``` + +If you are looking for a portable MIDI library then you can look into: + +- [midir](https://github.com/Boddlnagg/midir) (which is using this lib) +- [portmidi-rs](https://github.com/musitdev/portmidi-rs) + +For handling low level MIDI data you may look into: + +- [midi-rs](https://github.com/samdoshi/midi-rs) +- [rimd](https://github.com/RustAudio/rimd) + +*/ + +mod callback; +mod client; +mod devices; +mod endpoints; +mod notifications; +mod object; +mod packets; +mod ports; +mod properties; + +use core_foundation_sys::base::OSStatus; + +use coremidi_sys::{MIDIFlushOutput, MIDIRestart}; + +pub use crate::client::Client; +pub use crate::devices::Device; +pub use crate::endpoints::destinations::{Destination, Destinations, VirtualDestination}; +pub use crate::endpoints::sources::{Source, Sources, VirtualSource}; +pub use crate::endpoints::Endpoint; +pub use crate::notifications::{AddedRemovedInfo, IoErrorInfo, Notification, PropertyChangedInfo}; +pub use crate::object::ObjectType; +pub use crate::packets::{Packet, PacketBuffer, PacketList, PacketListIterator}; +pub use crate::ports::{InputPort, OutputPort}; +pub use crate::properties::{ + BooleanProperty, IntegerProperty, Properties, PropertyGetter, PropertySetter, StringProperty, +}; + +/// Unschedules previously-sent packets for all the endpoints. +/// See [MIDIFlushOutput](https://developer.apple.com/reference/coremidi/1495312-midiflushoutput). +/// +pub fn flush() -> Result<(), OSStatus> { + let status = unsafe { MIDIFlushOutput(0) }; + unit_result_from_status(status) +} + +/// Stops and restarts MIDI I/O. +/// See [MIDIRestart](https://developer.apple.com/reference/coremidi/1495146-midirestart). +/// +pub fn restart() -> Result<(), OSStatus> { + let status = unsafe { MIDIRestart() }; + unit_result_from_status(status) +} + +/// Convert an OSStatus into a Result<T, OSStatus> given a mapping closure +fn result_from_status<T, F: FnOnce() -> T>(status: OSStatus, f: F) -> Result<T, OSStatus> { + match status { + 0 => Ok(f()), + _ => Err(status), + } +} + +/// Convert an OSSStatus into a Result<(), OSStatus> +fn unit_result_from_status(status: OSStatus) -> Result<(), OSStatus> { + result_from_status(status, || ()) +} diff --git a/third_party/rust/coremidi/src/notifications.rs b/third_party/rust/coremidi/src/notifications.rs new file mode 100644 index 0000000000..627375fe82 --- /dev/null +++ b/third_party/rust/coremidi/src/notifications.rs @@ -0,0 +1,344 @@ +#![allow(non_upper_case_globals)] +#![allow(clippy::unnecessary_cast)] + +use core_foundation::base::{OSStatus, TCFType}; +use core_foundation::string::{CFString, CFStringRef}; + +use coremidi_sys::{ + kMIDIMsgIOError, kMIDIMsgObjectAdded, kMIDIMsgObjectRemoved, kMIDIMsgPropertyChanged, + kMIDIMsgSerialPortOwnerChanged, kMIDIMsgSetupChanged, kMIDIMsgThruConnectionsChanged, + MIDIIOErrorNotification, MIDINotification, MIDIObjectAddRemoveNotification, + MIDIObjectPropertyChangeNotification, +}; + +use crate::devices::Device; +use crate::object::{Object, ObjectType}; + +#[derive(Debug, PartialEq)] +pub struct AddedRemovedInfo { + pub parent: Object, + pub parent_type: ObjectType, + pub child: Object, + pub child_type: ObjectType, +} + +#[derive(Debug, PartialEq)] +pub struct PropertyChangedInfo { + pub object: Object, + pub object_type: ObjectType, + pub property_name: String, +} + +#[derive(Debug, PartialEq)] +pub struct IoErrorInfo { + pub driver_device: Device, + pub error_code: OSStatus, +} + +/// A message describing a system state change. +/// See [MIDINotification](https://developer.apple.com/reference/coremidi/midinotification). +/// +#[derive(Debug, PartialEq)] +pub enum Notification { + SetupChanged, + ObjectAdded(AddedRemovedInfo), + ObjectRemoved(AddedRemovedInfo), + PropertyChanged(PropertyChangedInfo), + ThruConnectionsChanged, + SerialPortOwnerChanged, + IoError(IoErrorInfo), +} + +impl Notification { + pub fn from(notification: &MIDINotification) -> Result<Notification, OSStatus> { + match notification.messageID as ::std::os::raw::c_uint { + kMIDIMsgSetupChanged => Ok(Notification::SetupChanged), + kMIDIMsgObjectAdded | kMIDIMsgObjectRemoved => { + Self::from_object_added_removed(notification) + } + kMIDIMsgPropertyChanged => Self::from_property_changed(notification), + kMIDIMsgThruConnectionsChanged => Ok(Notification::ThruConnectionsChanged), + kMIDIMsgSerialPortOwnerChanged => Ok(Notification::SerialPortOwnerChanged), + kMIDIMsgIOError => Ok(Self::from_io_error(notification)), + unknown => Err(unknown as OSStatus), + } + } + + fn from_object_added_removed( + notification: &MIDINotification, + ) -> Result<Notification, OSStatus> { + let add_remove_notification = + unsafe { &*(notification as *const _ as *const MIDIObjectAddRemoveNotification) }; + let parent_type = ObjectType::from(add_remove_notification.parentType); + let child_type = ObjectType::from(add_remove_notification.childType); + match (parent_type, child_type) { + (Ok(parent_type), Ok(child_type)) => { + let add_remove_info = AddedRemovedInfo { + parent: Object(add_remove_notification.parent), + parent_type, + child: Object(add_remove_notification.child), + child_type, + }; + match notification.messageID as ::std::os::raw::c_uint { + kMIDIMsgObjectAdded => Ok(Notification::ObjectAdded(add_remove_info)), + kMIDIMsgObjectRemoved => Ok(Notification::ObjectRemoved(add_remove_info)), + _ => unreachable!(), + } + } + _ => Err(notification.messageID as OSStatus), + } + } + + fn from_property_changed(notification: &MIDINotification) -> Result<Notification, i32> { + let property_changed_notification = + unsafe { &*(notification as *const _ as *const MIDIObjectPropertyChangeNotification) }; + match ObjectType::from(property_changed_notification.objectType) { + Ok(object_type) => { + let property_name = { + let name_ref: CFStringRef = property_changed_notification.propertyName; + let name: CFString = unsafe { TCFType::wrap_under_get_rule(name_ref) }; + name.to_string() + }; + let property_changed_info = PropertyChangedInfo { + object: Object(property_changed_notification.object), + object_type, + property_name, + }; + Ok(Notification::PropertyChanged(property_changed_info)) + } + Err(_) => Err(notification.messageID as i32), + } + } + + fn from_io_error(notification: &MIDINotification) -> Notification { + let io_error_notification = + unsafe { &*(notification as *const _ as *const MIDIIOErrorNotification) }; + let io_error_info = IoErrorInfo { + driver_device: Device { + object: Object(io_error_notification.driverDevice), + }, + error_code: io_error_notification.errorCode, + }; + Notification::IoError(io_error_info) + } +} + +#[cfg(test)] +mod tests { + + use core_foundation::base::{OSStatus, TCFType}; + use core_foundation::string::CFString; + + use coremidi_sys::{ + kMIDIMsgIOError, kMIDIMsgObjectAdded, kMIDIMsgObjectRemoved, kMIDIMsgPropertyChanged, + kMIDIMsgSerialPortOwnerChanged, kMIDIMsgSetupChanged, kMIDIMsgThruConnectionsChanged, + kMIDIObjectType_Device, kMIDIObjectType_Other, MIDIIOErrorNotification, MIDINotification, + MIDINotificationMessageID, MIDIObjectAddRemoveNotification, + MIDIObjectPropertyChangeNotification, MIDIObjectRef, + }; + + use crate::devices::Device; + use crate::notifications::{AddedRemovedInfo, IoErrorInfo, Notification, PropertyChangedInfo}; + use crate::object::{Object, ObjectType}; + + #[test] + fn notification_from_error() { + let notification_raw = MIDINotification { + messageID: 0xffff as MIDINotificationMessageID, + messageSize: 8, + }; + let notification = Notification::from(¬ification_raw); + assert!(notification.is_err()); + assert_eq!(notification.err().unwrap(), 0xffff as i32); + } + + #[test] + fn notification_from_setup_changed() { + let notification_raw = MIDINotification { + messageID: kMIDIMsgSetupChanged as MIDINotificationMessageID, + messageSize: 8, + }; + let notification = Notification::from(¬ification_raw); + assert!(notification.is_ok()); + assert_eq!(notification.unwrap(), Notification::SetupChanged); + } + + #[test] + fn notification_from_object_added() { + let notification_raw = MIDIObjectAddRemoveNotification { + messageID: kMIDIMsgObjectAdded as MIDINotificationMessageID, + messageSize: 24, + parent: 1 as MIDIObjectRef, + parentType: kMIDIObjectType_Device, + child: 2 as MIDIObjectRef, + childType: kMIDIObjectType_Other, + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_ok()); + + let info = AddedRemovedInfo { + parent: Object(1), + parent_type: ObjectType::Device, + child: Object(2), + child_type: ObjectType::Other, + }; + + assert_eq!(notification.unwrap(), Notification::ObjectAdded(info)); + } + + #[test] + fn notification_from_object_removed() { + let notification_raw = MIDIObjectAddRemoveNotification { + messageID: kMIDIMsgObjectRemoved as MIDINotificationMessageID, + messageSize: 24, + parent: 1 as MIDIObjectRef, + parentType: kMIDIObjectType_Device, + child: 2 as MIDIObjectRef, + childType: kMIDIObjectType_Other, + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_ok()); + + let info = AddedRemovedInfo { + parent: Object(1), + parent_type: ObjectType::Device, + child: Object(2), + child_type: ObjectType::Other, + }; + + assert_eq!(notification.unwrap(), Notification::ObjectRemoved(info)); + } + + #[test] + fn notification_from_object_added_removed_err() { + let notification_raw = MIDIObjectAddRemoveNotification { + messageID: kMIDIMsgObjectAdded as MIDINotificationMessageID, + messageSize: 24, + parent: 1 as MIDIObjectRef, + parentType: kMIDIObjectType_Device, + child: 2 as MIDIObjectRef, + childType: 0xffff, + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_err()); + assert_eq!(notification.err().unwrap(), kMIDIMsgObjectAdded as i32); + + let notification_raw = MIDIObjectAddRemoveNotification { + messageID: kMIDIMsgObjectRemoved as MIDINotificationMessageID, + messageSize: 24, + parent: 1 as MIDIObjectRef, + parentType: 0xffff, + child: 2 as MIDIObjectRef, + childType: kMIDIObjectType_Device, + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_err()); + assert_eq!(notification.err().unwrap(), kMIDIMsgObjectRemoved as i32); + } + + #[test] + fn notification_from_property_changed() { + let name = CFString::new("name"); + let notification_raw = MIDIObjectPropertyChangeNotification { + messageID: kMIDIMsgPropertyChanged as MIDINotificationMessageID, + messageSize: 24, + object: 1 as MIDIObjectRef, + objectType: kMIDIObjectType_Device, + propertyName: name.as_concrete_TypeRef(), + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_ok()); + + let info = PropertyChangedInfo { + object: Object(1), + object_type: ObjectType::Device, + property_name: "name".to_string(), + }; + + assert_eq!(notification.unwrap(), Notification::PropertyChanged(info)); + } + + #[test] + fn notification_from_property_changed_error() { + let name = CFString::new("name"); + let notification_raw = MIDIObjectPropertyChangeNotification { + messageID: kMIDIMsgPropertyChanged as MIDINotificationMessageID, + messageSize: 24, + object: 1 as MIDIObjectRef, + objectType: 0xffff, + propertyName: name.as_concrete_TypeRef(), + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_err()); + assert_eq!(notification.err().unwrap(), kMIDIMsgPropertyChanged as i32); + } + + #[test] + fn notification_from_thru_connections_changed() { + let notification_raw = MIDINotification { + messageID: kMIDIMsgThruConnectionsChanged as MIDINotificationMessageID, + messageSize: 8, + }; + let notification = Notification::from(¬ification_raw); + assert!(notification.is_ok()); + assert_eq!(notification.unwrap(), Notification::ThruConnectionsChanged); + } + + #[test] + fn notification_from_serial_port_owner_changed() { + let notification_raw = MIDINotification { + messageID: kMIDIMsgSerialPortOwnerChanged as MIDINotificationMessageID, + messageSize: 8, + }; + let notification = Notification::from(¬ification_raw); + assert!(notification.is_ok()); + assert_eq!(notification.unwrap(), Notification::SerialPortOwnerChanged); + } + + #[test] + fn notification_from_io_error() { + let notification_raw = MIDIIOErrorNotification { + messageID: kMIDIMsgIOError as MIDINotificationMessageID, + messageSize: 16, + driverDevice: 1 as MIDIObjectRef, + errorCode: 123 as OSStatus, + }; + + let notification = Notification::from(unsafe { + &*(¬ification_raw as *const _ as *const MIDINotification) + }); + + assert!(notification.is_ok()); + + let info = IoErrorInfo { + driver_device: Device { object: Object(1) }, + error_code: 123 as OSStatus, + }; + + assert_eq!(notification.unwrap(), Notification::IoError(info)); + } +} diff --git a/third_party/rust/coremidi/src/object.rs b/third_party/rust/coremidi/src/object.rs new file mode 100644 index 0000000000..42b6cfe5a8 --- /dev/null +++ b/third_party/rust/coremidi/src/object.rs @@ -0,0 +1,179 @@ +#![allow(non_upper_case_globals)] + +use core_foundation_sys::base::OSStatus; + +use coremidi_sys::{ + kMIDIObjectType_Destination, kMIDIObjectType_Device, kMIDIObjectType_Entity, + kMIDIObjectType_ExternalDestination, kMIDIObjectType_ExternalDevice, + kMIDIObjectType_ExternalEntity, kMIDIObjectType_ExternalSource, kMIDIObjectType_Other, + kMIDIObjectType_Source, MIDIObjectRef, SInt32, +}; + +use std::fmt; + +use crate::properties::{ + BooleanProperty, IntegerProperty, Properties, PropertyGetter, PropertySetter, StringProperty, +}; + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub enum ObjectType { + Other, + Device, + Entity, + Source, + Destination, + ExternalDevice, + ExternalEntity, + ExternalSource, + ExternalDestination, +} + +impl ObjectType { + pub fn from(value: i32) -> Result<ObjectType, i32> { + match value { + kMIDIObjectType_Other => Ok(ObjectType::Other), + kMIDIObjectType_Device => Ok(ObjectType::Device), + kMIDIObjectType_Entity => Ok(ObjectType::Entity), + kMIDIObjectType_Source => Ok(ObjectType::Source), + kMIDIObjectType_Destination => Ok(ObjectType::Destination), + kMIDIObjectType_ExternalDevice => Ok(ObjectType::ExternalDevice), + kMIDIObjectType_ExternalEntity => Ok(ObjectType::ExternalEntity), + kMIDIObjectType_ExternalSource => Ok(ObjectType::ExternalSource), + kMIDIObjectType_ExternalDestination => Ok(ObjectType::ExternalDestination), + unknown => Err(unknown), + } + } +} + +/// A [MIDI Object](https://developer.apple.com/reference/coremidi/midiobjectref). +/// +/// The base class of many CoreMIDI objects. +/// +#[derive(PartialEq)] +pub struct Object(pub(crate) MIDIObjectRef); + +impl Object { + /// Get the name for the object. + /// + pub fn name(&self) -> Option<String> { + Properties::name().value_from(self).ok() + } + + /// Get the unique id for the object. + /// + pub fn unique_id(&self) -> Option<u32> { + Properties::unique_id() + .value_from(self) + .ok() + .map(|v: SInt32| v as u32) + } + + /// Get the display name for the object. + /// + pub fn display_name(&self) -> Option<String> { + Properties::display_name().value_from(self).ok() + } + + /// Sets an object's string-type property. + /// + pub fn set_property_string(&self, name: &str, value: &str) -> Result<(), OSStatus> { + StringProperty::new(name).set_value(self, value) + } + + /// Gets an object's string-type property. + /// + pub fn get_property_string(&self, name: &str) -> Result<String, OSStatus> { + StringProperty::new(name).value_from(self) + } + + /// Sets an object's integer-type property. + /// + pub fn set_property_integer(&self, name: &str, value: i32) -> Result<(), OSStatus> { + IntegerProperty::new(name).set_value(self, value) + } + + /// Gets an object's integer-type property. + /// + pub fn get_property_integer(&self, name: &str) -> Result<i32, OSStatus> { + IntegerProperty::new(name).value_from(self) + } + + /// Sets an object's boolean-type property. + /// + /// CoreMIDI treats booleans as integers (0/1) but this API uses native bool types + /// + pub fn set_property_boolean(&self, name: &str, value: bool) -> Result<(), OSStatus> { + BooleanProperty::new(name).set_value(self, value) + } + + /// Gets an object's boolean-type property. + /// + /// CoreMIDI treats booleans as integers (0/1) but this API uses native bool types + /// + pub fn get_property_boolean(&self, name: &str) -> Result<bool, OSStatus> { + BooleanProperty::new(name).value_from(self) + } +} + +impl fmt::Debug for Object { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Object({:x})", self.0 as usize) + } +} + +#[cfg(test)] +mod tests { + use crate::object::ObjectType; + + use coremidi_sys::{ + kMIDIObjectType_Destination, kMIDIObjectType_Device, kMIDIObjectType_Entity, + kMIDIObjectType_ExternalDestination, kMIDIObjectType_ExternalDevice, + kMIDIObjectType_ExternalEntity, kMIDIObjectType_ExternalSource, kMIDIObjectType_Other, + kMIDIObjectType_Source, + }; + + #[test] + fn objecttype_from() { + assert_eq!( + ObjectType::from(kMIDIObjectType_Other), + Ok(ObjectType::Other) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_Device), + Ok(ObjectType::Device) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_Entity), + Ok(ObjectType::Entity) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_Source), + Ok(ObjectType::Source) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_Destination), + Ok(ObjectType::Destination) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_ExternalDevice), + Ok(ObjectType::ExternalDevice) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_ExternalEntity), + Ok(ObjectType::ExternalEntity) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_ExternalSource), + Ok(ObjectType::ExternalSource) + ); + assert_eq!( + ObjectType::from(kMIDIObjectType_ExternalDestination), + Ok(ObjectType::ExternalDestination) + ); + } + + #[test] + fn objecttype_from_error() { + assert_eq!(ObjectType::from(0xffff_i32), Err(0xffff)); + } +} diff --git a/third_party/rust/coremidi/src/packets.rs b/third_party/rust/coremidi/src/packets.rs new file mode 100644 index 0000000000..4d83b83121 --- /dev/null +++ b/third_party/rust/coremidi/src/packets.rs @@ -0,0 +1,791 @@ +use coremidi_sys::MIDIPacketList; +use coremidi_sys::{MIDIPacket, MIDIPacketNext, MIDITimeStamp}; + +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::slice; + +pub type Timestamp = u64; + +const MAX_PACKET_DATA_LENGTH: usize = 0xffffusize; + +#[cfg(any(target_arch = "arm", target_arch = "aarch64"))] +pub mod alignment { + pub type Marker = [u32; 0]; // ensures 4-byte alignment (on ARM) + pub const NEEDS_ALIGNMENT: bool = true; +} + +#[cfg(not(any(target_arch = "arm", target_arch = "aarch64")))] +pub mod alignment { + pub type Marker = [u8; 0]; // unaligned + pub const NEEDS_ALIGNMENT: bool = false; +} + +/// A collection of simultaneous MIDI events. +/// See [MIDIPacket](https://developer.apple.com/reference/coremidi/midipacket). +/// +#[repr(C)] +pub struct Packet { + // NOTE: At runtime this type must only be used behind immutable references + // that point to valid instances of MIDIPacket (mutable references would allow mem::swap). + // This type must NOT implement `Copy`! + // On ARM, this must be 4-byte aligned. + inner: PacketInner, + _alignment_marker: alignment::Marker, +} + +#[repr(packed)] +struct PacketInner { + timestamp: MIDITimeStamp, + length: u16, + data: [u8; 0], // zero-length, because we cannot make this type bigger without knowing how much data there actually is +} + +impl Packet { + /// Get the packet timestamp. + /// + pub fn timestamp(&self) -> Timestamp { + self.inner.timestamp as Timestamp + } + + /// Get the packet data. This method just gives raw MIDI bytes. You would need another + /// library to decode them and work with higher level events. + /// + /// + /// The following example: + /// + /// ``` + /// let packet_list = &coremidi::PacketBuffer::new(0, &[0x90, 0x40, 0x7f]); + /// for packet in packet_list.iter() { + /// for byte in packet.data() { + /// print!(" {:x}", byte); + /// } + /// } + /// ``` + /// + /// will print: + /// + /// ```text + /// 90 40 7f + /// ``` + pub fn data(&self) -> &[u8] { + let data_ptr = self.inner.data.as_ptr(); + let data_len = self.inner.length as usize; + unsafe { slice::from_raw_parts(data_ptr, data_len) } + } +} + +impl fmt::Debug for Packet { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let result = write!( + f, + "Packet(ptr={:x}, ts={:016x}, data=[", + self as *const _ as usize, + self.timestamp() as u64 + ); + let result = self + .data() + .iter() + .enumerate() + .fold(result, |prev_result, (i, b)| match prev_result { + Err(err) => Err(err), + Ok(()) => { + let sep = if i > 0 { ", " } else { "" }; + write!(f, "{}{:02x}", sep, b) + } + }); + result.and_then(|_| write!(f, "])")) + } +} + +impl fmt::Display for Packet { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let result = write!(f, "{:016x}:", self.timestamp()); + self.data() + .iter() + .fold(result, |prev_result, b| match prev_result { + Err(err) => Err(err), + Ok(()) => write!(f, " {:02x}", b), + }) + } +} + +/// A [list of MIDI events](https://developer.apple.com/reference/coremidi/midipacketlist) being received from, or being sent to, one endpoint. +/// +#[repr(C)] +pub struct PacketList { + // NOTE: This type must only exist in the form of immutable references + // pointing to valid instances of MIDIPacketList. + // This type must NOT implement `Copy`! + inner: PacketListInner, + _do_not_construct: alignment::Marker, +} + +#[repr(packed)] +struct PacketListInner { + num_packets: u32, + data: [MIDIPacket; 0], +} + +impl PacketList { + /// For internal usage only. + /// Requires this instance to actually point to a valid MIDIPacketList + pub(crate) unsafe fn as_ptr(&self) -> *mut MIDIPacketList { + self as *const PacketList as *mut PacketList as *mut MIDIPacketList + } +} + +impl PacketList { + /// Check if the packet list is empty. + /// + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get the number of packets in the list. + /// + pub fn len(&self) -> usize { + self.inner.num_packets as usize + } + + /// Get an iterator for the packets in the list. + /// + pub fn iter(&self) -> PacketListIterator { + PacketListIterator { + count: self.len(), + packet_ptr: std::ptr::addr_of!(self.inner.data) as *const MIDIPacket, + _phantom: ::std::marker::PhantomData::default(), + } + } +} + +impl fmt::Debug for PacketList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let result = write!(f, "PacketList(ptr={:x}, packets=[", unsafe { + self.as_ptr() as usize + }); + self.iter() + .enumerate() + .fold(result, |prev_result, (i, packet)| match prev_result { + Err(err) => Err(err), + Ok(()) => { + let sep = if i != 0 { ", " } else { "" }; + write!(f, "{}{:?}", sep, packet) + } + }) + .and_then(|_| write!(f, "])")) + } +} + +impl fmt::Display for PacketList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let num_packets = self.inner.num_packets; + let result = write!(f, "PacketList(len={})", num_packets); + self.iter() + .fold(result, |prev_result, packet| match prev_result { + Err(err) => Err(err), + Ok(()) => write!(f, "\n {}", packet), + }) + } +} + +pub struct PacketListIterator<'a> { + count: usize, + packet_ptr: *const MIDIPacket, + _phantom: ::std::marker::PhantomData<&'a Packet>, +} + +impl<'a> Iterator for PacketListIterator<'a> { + type Item = &'a Packet; + + fn next(&mut self) -> Option<&'a Packet> { + if self.count > 0 { + let packet = unsafe { &*(self.packet_ptr as *const Packet) }; + self.count -= 1; + self.packet_ptr = unsafe { MIDIPacketNext(self.packet_ptr) }; + Some(packet) + } else { + None + } + } +} + +const PACKET_LIST_HEADER_SIZE: usize = 4; // MIDIPacketList::numPackets: UInt32 +const PACKET_HEADER_SIZE: usize = 8 + // MIDIPacket::timeStamp: MIDITimeStamp/UInt64 + 2; // MIDIPacket::length: UInt16 + +const INLINE_PACKET_BUFFER_SIZE: usize = 28; // must be divisible by 4 + +enum PacketBufferStorage { + /// Inline stores the data directy on the stack, if it is small enough. + /// NOTE: using u32 ensures correct alignment (required on ARM) + Inline([u32; INLINE_PACKET_BUFFER_SIZE / 4]), + /// External is used whenever the size of the data exceeds INLINE_PACKET_BUFFER_SIZE. + /// This means that the size of the contained vector is always greater than INLINE_PACKET_BUFFER_SIZE. + External(Vec<u32>), +} + +impl PacketBufferStorage { + #[inline] + pub fn with_capacity(capacity: usize) -> PacketBufferStorage { + if capacity <= INLINE_PACKET_BUFFER_SIZE { + PacketBufferStorage::Inline([0; INLINE_PACKET_BUFFER_SIZE / 4]) + } else { + let u32_len = ((capacity - 1) / 4) + 1; + let mut buffer = Vec::with_capacity(u32_len); + unsafe { + buffer.set_len(u32_len); + } + PacketBufferStorage::External(buffer) + } + } + + #[inline] + fn capacity(&self) -> usize { + match *self { + PacketBufferStorage::Inline(ref inline) => inline.len() * 4, + PacketBufferStorage::External(ref vec) => vec.len() * 4, + } + } + + #[inline] + fn get_slice(&self) -> &[u8] { + unsafe { + match *self { + PacketBufferStorage::Inline(ref inline) => { + slice::from_raw_parts(inline.as_ptr() as *const u8, inline.len() * 4) + } + PacketBufferStorage::External(ref vec) => { + slice::from_raw_parts(vec.as_ptr() as *const u8, vec.len() * 4) + } + } + } + } + + #[inline] + fn get_slice_mut(&mut self) -> &mut [u8] { + unsafe { + match *self { + PacketBufferStorage::Inline(ref mut inline) => { + slice::from_raw_parts_mut(inline.as_mut_ptr() as *mut u8, inline.len() * 4) + } + PacketBufferStorage::External(ref mut vec) => { + slice::from_raw_parts_mut(vec.as_mut_ptr() as *mut u8, vec.len() * 4) + } + } + } + } + + unsafe fn assign_packet(&mut self, packet_offset: usize, time: MIDITimeStamp, data: &[u8]) { + assert!(data.len() <= MAX_PACKET_DATA_LENGTH, "packet data too long"); // cannot store longer size in u16 + + if alignment::NEEDS_ALIGNMENT { + debug_assert!(packet_offset & 0b11 == 0); + } + + let slice = self.get_slice_mut(); + let ptr = slice[packet_offset..].as_mut_ptr() as *mut Packet; + (*ptr).inner.timestamp = time; + (*ptr).inner.length = data.len() as u16; + let packet_data_start = packet_offset + PACKET_HEADER_SIZE; + slice[packet_data_start..(packet_data_start + data.len())].copy_from_slice(data); + } + + /// Requires that there is a valid Packet at `offset`, which has enough space for `data` + unsafe fn extend_packet(&mut self, packet_offset: usize, data: &[u8]) { + let slice = self.get_slice_mut(); + let ptr = slice[packet_offset..].as_mut_ptr() as *mut Packet; + let packet_data_start = packet_offset + PACKET_HEADER_SIZE + (*ptr).inner.length as usize; + (*ptr).inner.length += data.len() as u16; + slice[packet_data_start..(packet_data_start + data.len())].copy_from_slice(data); + } + + /// Call this only with larger length values (won't make the buffer smaller) + unsafe fn ensure_capacity(&mut self, capacity: usize) { + if capacity < INLINE_PACKET_BUFFER_SIZE || capacity < self.get_slice().len() { + return; + } + + let vec_capacity = ((capacity - 1) / 4) + 1; + let vec: Option<Vec<u32>> = match *self { + PacketBufferStorage::Inline(ref inline) => { + let mut v = Vec::with_capacity(vec_capacity); + v.extend_from_slice(inline); + v.set_len(vec_capacity); + Some(v) + } + PacketBufferStorage::External(ref mut vec) => { + let current_len = vec.len(); + vec.reserve(vec_capacity - current_len); + vec.set_len(vec_capacity); + None + } + }; + + // to prevent borrowcheck errors, this must come after the `match` + if let Some(v) = vec { + *self = PacketBufferStorage::External(v); + } + } +} + +impl Deref for PacketBufferStorage { + type Target = PacketList; + + #[inline] + fn deref(&self) -> &PacketList { + unsafe { &*(self.get_slice().as_ptr() as *const PacketList) } + } +} + +impl DerefMut for PacketBufferStorage { + // NOTE: Mutable references `&mut PacketList` must not be exposed in the public API! + // The user could use mem::swap to modify the header without modifying the packets that follow. + #[inline] + fn deref_mut(&mut self) -> &mut PacketList { + unsafe { &mut *(self.get_slice_mut().as_mut_ptr() as *mut PacketList) } + } +} + +/// A mutable `PacketList` builder. +/// +/// A `PacketList` is an inmmutable reference to a [MIDIPacketList](https://developer.apple.com/reference/coremidi/midipacketlist) structure, +/// while a `PacketBuffer` is a mutable structure that allows to build a `PacketList` by adding packets. +/// It dereferences to a `PacketList`, so it can be used whenever a `PacketList` is needed. +/// +pub struct PacketBuffer { + storage: PacketBufferStorage, + last_packet_offset: usize, +} + +impl Deref for PacketBuffer { + type Target = PacketList; + + #[inline] + fn deref(&self) -> &PacketList { + self.storage.deref() + } +} + +impl PacketBuffer { + /// Create a `PacketBuffer` with a single packet containing the provided timestamp and data. + /// + /// According to the official documentation for CoreMIDI, the timestamp represents + /// the time at which the events are to be played, where zero means "now". + /// The timestamp applies to the first MIDI byte in the packet. + /// + /// Example on how to create a `PacketBuffer` with a single packet for a MIDI note on for C-5: + /// + /// ``` + /// let buffer = coremidi::PacketBuffer::new(0, &[0x90, 0x3c, 0x7f]); + /// assert_eq!(buffer.len(), 1) + /// ``` + pub fn new(time: MIDITimeStamp, data: &[u8]) -> PacketBuffer { + let capacity = data.len() + PACKET_LIST_HEADER_SIZE + PACKET_HEADER_SIZE; + let mut storage = PacketBufferStorage::with_capacity(capacity); + storage.deref_mut().inner.num_packets = 1; + let last_packet_offset = PACKET_LIST_HEADER_SIZE; + unsafe { + storage.assign_packet(last_packet_offset, time, data); + } + + PacketBuffer { + storage, + last_packet_offset, + } + } + + /// Create an empty `PacketBuffer` with no packets. + /// + /// Example on how to create an empty `PacketBuffer` + /// with a capacity for 128 bytes in total (including headers): + /// + /// ``` + /// let buffer = coremidi::PacketBuffer::with_capacity(128); + /// assert_eq!(buffer.len(), 0); + /// assert_eq!(buffer.capacity(), 128); + /// ``` + pub fn with_capacity(capacity: usize) -> PacketBuffer { + let capacity = std::cmp::max(capacity, INLINE_PACKET_BUFFER_SIZE); + let mut storage = PacketBufferStorage::with_capacity(capacity); + storage.deref_mut().inner.num_packets = 0; + + PacketBuffer { + storage, + last_packet_offset: PACKET_LIST_HEADER_SIZE, + } + } + + /// Get underlying buffer capacity in bytes + pub fn capacity(&self) -> usize { + self.storage.capacity() + } + + /// Add a new event containing the provided timestamp and data. + /// + /// According to the official documentation for CoreMIDI, the timestamp represents + /// the time at which the events are to be played, where zero means "now". + /// The timestamp applies to the first MIDI byte in the packet. + /// + /// An event must not have a timestamp that is smaller than that of a previous event + /// in the same `PacketList` + /// + /// Example: + /// + /// ``` + /// let mut chord = coremidi::PacketBuffer::new(0, &[0x90, 0x3c, 0x7f]); + /// chord.push_data(0, &[0x90, 0x40, 0x7f]); + /// assert_eq!(chord.len(), 1); + /// let repr = format!("{}", &chord as &coremidi::PacketList); + /// assert_eq!(repr, "PacketList(len=1)\n 0000000000000000: 90 3c 7f 90 40 7f"); + /// ``` + pub fn push_data(&mut self, time: MIDITimeStamp, data: &[u8]) -> &mut Self { + let (can_merge, previous_data_len) = self.can_merge_into_last_packet(time, data); + + if can_merge { + let new_packet_size = Self::packet_size(previous_data_len + data.len()); + unsafe { + self.storage + .ensure_capacity(self.last_packet_offset + new_packet_size); + self.storage.extend_packet(self.last_packet_offset, data); + } + } else { + let packet_size = Self::packet_size(data.len()); + let next_offset = self.next_packet_offset(); + unsafe { + self.storage.ensure_capacity(next_offset + packet_size); + self.storage.assign_packet(next_offset, time, data); + } + self.packet_list_mut().num_packets += 1; + self.last_packet_offset = next_offset; + } + + self + } + + /// Clears the buffer, removing all packets. + /// Note that this method has no effect on the allocated capacity of the buffer. + pub fn clear(&mut self) { + self.packet_list_mut().num_packets = 0; + self.last_packet_offset = PACKET_LIST_HEADER_SIZE; + } + + /// Checks whether the given tiemstamped data can be merged into the previous packet + fn can_merge_into_last_packet(&self, time: MIDITimeStamp, data: &[u8]) -> (bool, usize) { + if self.packet_list_is_empty() { + (false, 0) + } else { + let previous_packet = self.last_packet(); + let previous_packet_data = previous_packet.data(); + let previous_data_len = previous_packet_data.len(); + let can_merge = previous_packet.timestamp() == time + && Self::not_sysex(data) + && Self::has_status_byte(data) + && Self::not_sysex(previous_packet_data) + && Self::has_status_byte(previous_packet_data) + && previous_data_len + data.len() < MAX_PACKET_DATA_LENGTH; + + (can_merge, previous_data_len) + } + } + + #[inline] + fn last_packet(&self) -> &Packet { + assert!(self.packet_list().num_packets > 0); + let packets_slice = self.storage.get_slice(); + let packet_slot = &packets_slice[self.last_packet_offset..]; + unsafe { &*(packet_slot.as_ptr() as *const Packet) } + } + + #[inline] + fn next_packet_offset(&self) -> usize { + if self.packet_list_is_empty() { + self.last_packet_offset + } else { + let data_len = self.last_packet().inner.length as usize; + let next_offset = self.last_packet_offset + Self::packet_size(data_len); + if alignment::NEEDS_ALIGNMENT { + (next_offset + 3) & !(3usize) + } else { + next_offset + } + } + } + + #[inline] + fn not_sysex(data: &[u8]) -> bool { + data[0] != 0xF0 + } + + #[inline] + fn has_status_byte(data: &[u8]) -> bool { + data[0] & 0b10000000 != 0 + } + + #[inline] + fn packet_size(data_len: usize) -> usize { + PACKET_HEADER_SIZE + data_len + } + + #[inline] + fn packet_list(&self) -> &PacketListInner { + &self.storage.deref().inner + } + + #[inline] + fn packet_list_is_empty(&self) -> bool { + self.packet_list().num_packets == 0 + } + + #[inline] + fn packet_list_mut(&mut self) -> &mut PacketListInner { + &mut self.storage.deref_mut().inner + } +} + +#[cfg(test)] +mod tests { + use super::*; + use coremidi_sys::{MIDIPacketList, MIDITimeStamp}; + use std::mem; + + #[test] + pub fn packet_struct_layout() { + let expected_align = if super::alignment::NEEDS_ALIGNMENT { + 4 + } else { + 1 + }; + assert_eq!(expected_align, mem::align_of::<Packet>()); + assert_eq!(expected_align, mem::align_of::<PacketList>()); + + let dummy_packet: Packet = unsafe { mem::zeroed() }; + let ptr = &dummy_packet as *const _ as *const u8; + assert_eq!( + PACKET_HEADER_SIZE, + dummy_packet.inner.data.as_ptr() as usize - ptr as usize + ); + + let dummy_packet_list: PacketList = unsafe { mem::zeroed() }; + let ptr = &dummy_packet_list as *const _ as *const u8; + assert_eq!( + PACKET_LIST_HEADER_SIZE, + std::ptr::addr_of!(dummy_packet_list.inner.data) as usize - ptr as usize + ); + } + + #[test] + pub fn single_packet_alloc_inline() { + let packet_buf = PacketBuffer::new(42, &[0x90u8, 0x40, 0x7f]); + if let PacketBufferStorage::External(_) = packet_buf.storage { + panic!("A single 3-byte message must not be allocated externally") + } + } + + #[test] + fn packet_buffer_deref() { + let packet_buf = PacketBuffer::new(42, &[0x90u8, 0x40, 0x7f]); + let packet_list: &PacketList = &packet_buf; + assert_eq!( + unsafe { packet_list.as_ptr() as *const MIDIPacketList }, + packet_buf.storage.get_slice().as_ptr() as *const _ as *const MIDIPacketList + ); + } + + #[test] + fn packet_list_length() { + let mut packet_buf = PacketBuffer::new(42, &[0x90u8, 0x40, 0x7f]); + packet_buf.push_data(43, &[0x91u8, 0x40, 0x7f]); + packet_buf.push_data(44, &[0x80u8, 0x40, 0x7f]); + packet_buf.push_data(45, &[0x81u8, 0x40, 0x7f]); + assert_eq!(packet_buf.len(), 4); + } + + #[test] + fn packet_buffer_empty_with_capacity() { + let packet_buf = PacketBuffer::with_capacity(128); + assert_eq!(packet_buf.capacity(), 128); + assert_eq!(packet_buf.len(), 0); + } + + #[test] + fn packet_buffer_with_capacity_zero() { + let packet_buf = PacketBuffer::with_capacity(0); + assert_eq!(packet_buf.capacity(), INLINE_PACKET_BUFFER_SIZE); + assert_eq!(packet_buf.len(), 0); + } + + #[test] + fn packet_buffer_with_capacity() { + let mut packet_buf = PacketBuffer::with_capacity(128); + packet_buf.push_data(43, &[0x91u8, 0x40, 0x7f]); + packet_buf.push_data(44, &[0x80u8, 0x40, 0x7f]); + packet_buf.push_data(45, &[0x81u8, 0x40, 0x7f]); + assert_eq!(packet_buf.capacity(), 128); + assert_eq!(packet_buf.len(), 3); + } + + #[test] + fn packet_buffer_clear() { + let mut packet_buf = PacketBuffer::new(42, &[0x90u8, 0x40, 0x7f]); + packet_buf.push_data(43, &[0x91u8, 0x40, 0x7f]); + packet_buf.push_data(44, &[0x80u8, 0x40, 0x7f]); + packet_buf.push_data(45, &[0x81u8, 0x40, 0x7f]); + assert_eq!(packet_buf.len(), 4); + packet_buf.clear(); + assert_eq!(packet_buf.len(), 0); + } + + #[test] + fn compare_equal_timestamps() { + // these messages should be merged into a single packet + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (42, vec![0x90, 0x41, 0x7f]), + (42, vec![0x90, 0x42, 0x7f]), + ]) + } + } + + #[test] + fn compare_unequal_timestamps() { + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (43, vec![0x90, 0x40, 0x7f]), + (44, vec![0x90, 0x40, 0x7f]), + ]) + } + } + + #[test] + fn compare_sysex() { + // the sysex must not be merged with the surrounding packets + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (42, vec![0xF0, 0x01, 0x01, 0x01, 0x01, 0x01, 0xF7]), // sysex + (42, vec![0x90, 0x41, 0x7f]), + ]) + } + } + + #[test] + fn compare_sysex_split() { + // the sysex must not be merged with the surrounding packets + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (42, vec![0xF0, 0x01, 0x01, 0x01, 0x01]), // sysex part 1 + (42, vec![0x01, 0xF7]), // sysex part 2 + (42, vec![0x90, 0x41, 0x7f]), + ]) + } + } + + #[test] + fn compare_sysex_split2() { + // the sysex must not be merged with the surrounding packets + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (42, vec![0xF0, 0x01, 0x01, 0x01, 0x01]), // sysex part 1 + (42, vec![0x01, 0x01, 0x01]), // sysex part 2 + (42, vec![0x01, 0xF7]), // sysex part 3 + (42, vec![0x90, 0x41, 0x7f]), + ]) + } + } + + #[test] + fn compare_sysex_malformed() { + // the sysex must not be merged with the surrounding packets + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (42, vec![0xF0, 0x01, 0x01, 0x01, 0x01]), // sysex part 1 + (42, vec![0x01, 0x01, 0x01]), // sysex part 2 + //(42, vec![0x01, 0xF7]), // sysex part 3 (missing) + (42, vec![0x90, 0x41, 0x7f]), + ]) + } + } + + #[test] + fn compare_sysex_long() { + let mut sysex = vec![0xF0]; + sysex.resize(301, 0x01); + sysex.push(0xF7); + unsafe { + compare_packet_list(vec![ + (42, vec![0x90, 0x40, 0x7f]), + (43, vec![0x90, 0x41, 0x7f]), + (43, sysex), + ]) + } + } + + /// Compares the results of building a PacketList using our PacketBuffer API + /// and the native API (MIDIPacketListAdd, etc). + unsafe fn compare_packet_list(packets: Vec<(MIDITimeStamp, Vec<u8>)>) { + use coremidi_sys::{MIDIPacketListAdd, MIDIPacketListInit}; + + // allocate a buffer on the stack for building the list using native methods + const BUFFER_SIZE: usize = 65536; // maximum allowed size + let buffer: &mut [u8] = &mut [0; BUFFER_SIZE]; + let pkt_list_ptr = buffer.as_mut_ptr() as *mut MIDIPacketList; + + // build the list + let mut pkt_ptr = MIDIPacketListInit(pkt_list_ptr); + for pkt in &packets { + pkt_ptr = MIDIPacketListAdd( + pkt_list_ptr, + BUFFER_SIZE as u64, + pkt_ptr, + pkt.0, + pkt.1.len() as u64, + pkt.1.as_ptr(), + ); + assert!(!pkt_ptr.is_null()); + } + let list_native = &*(pkt_list_ptr as *const _ as *const PacketList); + + // build the PacketBuffer, containing the same packets + let mut packet_buf = PacketBuffer::new(packets[0].0, &packets[0].1); + for pkt in &packets[1..] { + packet_buf.push_data(pkt.0, &pkt.1); + } + + // print buffer contents for debugging purposes + let packet_buf_slice = packet_buf.storage.get_slice(); + println!( + "\nbuffer: {:?}", + packet_buf_slice + .iter() + .map(|b| format!("{:02X}", b)) + .collect::<Vec<String>>() + .join(" ") + ); + println!( + "\nnative: {:?}", + buffer[0..packet_buf_slice.len()] + .iter() + .map(|b| format!("{:02X}", b)) + .collect::<Vec<String>>() + .join(" ") + ); + + let list: &PacketList = &packet_buf; + + // check if the contents match + assert_eq!( + list_native.len(), + list.len(), + "PacketList lengths must match" + ); + for (n, p) in list_native.iter().zip(list.iter()) { + assert_eq!(n.data(), p.data()); + } + } +} diff --git a/third_party/rust/coremidi/src/ports.rs b/third_party/rust/coremidi/src/ports.rs new file mode 100644 index 0000000000..af55858033 --- /dev/null +++ b/third_party/rust/coremidi/src/ports.rs @@ -0,0 +1,130 @@ +use core_foundation::base::OSStatus; + +use coremidi_sys::{MIDIPortConnectSource, MIDIPortDisconnectSource, MIDIPortDispose, MIDISend}; + +use std::ops::Deref; +use std::ptr; + +use crate::callback::BoxedCallback; +use crate::endpoints::destinations::Destination; +use crate::endpoints::sources::Source; +use crate::object::Object; +use crate::packets::PacketList; + +/// A MIDI connection port owned by a client. +/// See [MIDIPortRef](https://developer.apple.com/reference/coremidi/midiportref). +/// +/// Ports can't be instantiated directly, but through a client. +/// +#[derive(Debug)] +pub struct Port { + pub(crate) object: Object, +} + +impl Deref for Port { + type Target = Object; + + fn deref(&self) -> &Object { + &self.object + } +} + +impl Drop for Port { + fn drop(&mut self) { + unsafe { MIDIPortDispose(self.object.0) }; + } +} + +/// An output [MIDI port](https://developer.apple.com/reference/coremidi/midiportref) owned by a client. +/// +/// A simple example to create an output port and send a MIDI event: +/// +/// ```rust,no_run +/// let client = coremidi::Client::new("example-client").unwrap(); +/// let output_port = client.output_port("example-port").unwrap(); +/// let destination = coremidi::Destination::from_index(0).unwrap(); +/// let packets = coremidi::PacketBuffer::new(0, &[0x90, 0x40, 0x7f]); +/// output_port.send(&destination, &packets).unwrap(); +/// ``` +#[derive(Debug)] +pub struct OutputPort { + pub(crate) port: Port, +} + +impl OutputPort { + /// Send a list of packets to a destination. + /// See [MIDISend](https://developer.apple.com/reference/coremidi/1495289-midisend). + /// + pub fn send( + &self, + destination: &Destination, + packet_list: &PacketList, + ) -> Result<(), OSStatus> { + let status = unsafe { + MIDISend( + self.port.object.0, + destination.endpoint.object.0, + packet_list.as_ptr(), + ) + }; + if status == 0 { + Ok(()) + } else { + Err(status) + } + } +} + +impl Deref for OutputPort { + type Target = Port; + + fn deref(&self) -> &Port { + &self.port + } +} + +/// An input [MIDI port](https://developer.apple.com/reference/coremidi/midiportref) owned by a client. +/// +/// A simple example to create an input port: +/// +/// ```rust,no_run +/// let client = coremidi::Client::new("example-client").unwrap(); +/// let input_port = client.input_port("example-port", |packet_list| println!("{}", packet_list)).unwrap(); +/// let source = coremidi::Source::from_index(0).unwrap(); +/// input_port.connect_source(&source); +/// ``` +#[derive(Debug)] +pub struct InputPort { + // Note: the order is important here, port needs to be dropped first + pub(crate) port: Port, + pub(crate) callback: BoxedCallback<PacketList>, +} + +impl InputPort { + pub fn connect_source(&self, source: &Source) -> Result<(), OSStatus> { + let status = + unsafe { MIDIPortConnectSource(self.object.0, source.object.0, ptr::null_mut()) }; + if status == 0 { + Ok(()) + } else { + Err(status) + } + } + + pub fn disconnect_source(&self, source: &Source) -> Result<(), OSStatus> { + let status = unsafe { MIDIPortDisconnectSource(self.object.0, source.object.0) }; + if status == 0 { + Ok(()) + } else { + Err(status) + } + } +} + +impl Deref for InputPort { + type Target = Port; + + fn deref(&self) -> &Port { + &self.port + } +} diff --git a/third_party/rust/coremidi/src/properties.rs b/third_party/rust/coremidi/src/properties.rs new file mode 100644 index 0000000000..a588378531 --- /dev/null +++ b/third_party/rust/coremidi/src/properties.rs @@ -0,0 +1,508 @@ +use std::mem::MaybeUninit; + +use core_foundation::{ + base::{CFGetRetainCount, CFIndex, CFTypeRef, OSStatus, TCFType}, + string::{CFString, CFStringRef}, +}; + +use coremidi_sys::*; + +use crate::{object::Object, result_from_status, unit_result_from_status}; + +pub trait PropertyGetter<T> { + fn value_from(&self, object: &Object) -> Result<T, OSStatus>; +} + +pub trait PropertySetter<T> { + fn set_value(&self, object: &Object, value: T) -> Result<(), OSStatus>; +} + +/// Because Property structs can be constructed from strings that have been +/// passed in from the user or are constants CFStringRefs from CoreMidi, we +/// need to abstract over how we store their keys. +enum PropertyKeyStorage { + Owned(CFString), + Constant(CFStringRef), +} + +impl PropertyKeyStorage { + /// Return a raw CFStringRef pointing to this property key + fn as_string_ref(&self) -> CFStringRef { + match self { + PropertyKeyStorage::Owned(owned) => owned.as_concrete_TypeRef(), + PropertyKeyStorage::Constant(constant) => *constant, + } + } + + /// For checking the retain count when debugging + #[allow(dead_code)] + fn retain_count(&self) -> CFIndex { + match self { + PropertyKeyStorage::Owned(owned) => owned.retain_count(), + PropertyKeyStorage::Constant(constant) => unsafe { + CFGetRetainCount(*constant as CFTypeRef) + }, + } + } +} + +/// A MIDI object property which value is an String +/// +pub struct StringProperty(PropertyKeyStorage); + +impl StringProperty { + pub fn new(name: &str) -> Self { + StringProperty(PropertyKeyStorage::Owned(CFString::new(name))) + } + + /// Note: Should only be used internally with predefined CoreMidi constants, + /// since it does not bump the retain count of the CFStringRef. + fn from_constant_string_ref(string_ref: CFStringRef) -> Self { + StringProperty(PropertyKeyStorage::Constant(string_ref)) + } +} + +impl<T> PropertyGetter<T> for StringProperty +where + T: From<String>, +{ + fn value_from(&self, object: &Object) -> Result<T, OSStatus> { + let property_key = self.0.as_string_ref(); + let mut string_ref = MaybeUninit::uninit(); + let status = + unsafe { MIDIObjectGetStringProperty(object.0, property_key, string_ref.as_mut_ptr()) }; + result_from_status(status, || { + let string_ref = unsafe { string_ref.assume_init() }; + if string_ref.is_null() { + return "".to_string().into(); + }; + let cf_string: CFString = unsafe { TCFType::wrap_under_create_rule(string_ref) }; + cf_string.to_string().into() + }) + } +} + +impl<'a, T> PropertySetter<T> for StringProperty +where + T: Into<String>, +{ + fn set_value(&self, object: &Object, value: T) -> Result<(), OSStatus> { + let property_key = self.0.as_string_ref(); + let value: String = value.into(); + let string = CFString::new(&value); + let string_ref = string.as_concrete_TypeRef(); + let status = unsafe { MIDIObjectSetStringProperty(object.0, property_key, string_ref) }; + unit_result_from_status(status) + } +} + +/// A MIDI object property which value is an Integer +/// +pub struct IntegerProperty(PropertyKeyStorage); + +impl IntegerProperty { + pub fn new(name: &str) -> Self { + IntegerProperty(PropertyKeyStorage::Owned(CFString::new(name))) + } + + /// Note: Should only be used internally with predefined CoreMidi constants, + /// since it does not bump the retain count of the CFStringRef. + fn from_constant_string_ref(string_ref: CFStringRef) -> Self { + IntegerProperty(PropertyKeyStorage::Constant(string_ref)) + } +} + +impl<T> PropertyGetter<T> for IntegerProperty +where + T: From<SInt32>, +{ + fn value_from(&self, object: &Object) -> Result<T, OSStatus> { + let property_key = self.0.as_string_ref(); + let mut value = MaybeUninit::uninit(); + let status = + unsafe { MIDIObjectGetIntegerProperty(object.0, property_key, value.as_mut_ptr()) }; + result_from_status(status, || { + let value = unsafe { value.assume_init() }; + value.into() + }) + } +} + +impl<T> PropertySetter<T> for IntegerProperty +where + T: Into<SInt32>, +{ + fn set_value(&self, object: &Object, value: T) -> Result<(), OSStatus> { + let property_key = self.0.as_string_ref(); + let status = unsafe { MIDIObjectSetIntegerProperty(object.0, property_key, value.into()) }; + unit_result_from_status(status) + } +} + +/// A MIDI object property which value is a Boolean +/// +pub struct BooleanProperty(IntegerProperty); + +impl BooleanProperty { + pub fn new(name: &str) -> Self { + BooleanProperty(IntegerProperty::new(name)) + } + + /// Note: Should only be used internally with predefined CoreMidi constants, + /// since it does not bump the retain count of the CFStringRef. + fn from_constant_string_ref(string_ref: CFStringRef) -> Self { + BooleanProperty(IntegerProperty::from_constant_string_ref(string_ref)) + } +} + +impl<T> PropertyGetter<T> for BooleanProperty +where + T: From<bool>, +{ + fn value_from(&self, object: &Object) -> Result<T, OSStatus> { + self.0 + .value_from(object) + .map(|value: SInt32| (value == 1).into()) + } +} + +impl<T> PropertySetter<T> for BooleanProperty +where + T: Into<bool>, +{ + fn set_value(&self, object: &Object, value: T) -> Result<(), OSStatus> { + let value: SInt32 = if value.into() { 1 } else { 0 }; + self.0.set_value(object, value) + } +} + +/// The set of properties that might be available for MIDI objects. +/// +pub struct Properties; + +impl Properties { + /// See [kMIDIPropertyName](https://developer.apple.com/reference/coremidi/kmidipropertyname) + pub fn name() -> StringProperty { + StringProperty::from_constant_string_ref(unsafe { kMIDIPropertyName }) + } + + /// See [kMIDIPropertyManufacturer](https://developer.apple.com/reference/coremidi/kmidipropertymanufacturer) + pub fn manufacturer() -> StringProperty { + StringProperty::from_constant_string_ref(unsafe { kMIDIPropertyManufacturer }) + } + + /// See [kMIDIPropertyModel](https://developer.apple.com/reference/coremidi/kmidipropertymodel) + pub fn model() -> StringProperty { + StringProperty::from_constant_string_ref(unsafe { kMIDIPropertyModel }) + } + + /// See [kMIDIPropertyUniqueID](https://developer.apple.com/reference/coremidi/kmidipropertyuniqueid) + pub fn unique_id() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyUniqueID }) + } + + /// See [kMIDIPropertyDeviceID](https://developer.apple.com/reference/coremidi/kmidipropertydeviceid) + pub fn device_id() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyDeviceID }) + } + + /// See [kMIDIPropertyReceiveChannels](https://developer.apple.com/reference/coremidi/kmidipropertyreceivechannels) + pub fn receive_channels() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceiveChannels }) + } + + /// See [kMIDIPropertyTransmitChannels](https://developer.apple.com/reference/coremidi/kmidipropertytransmitchannels) + pub fn transmit_channels() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitChannels }) + } + + /// See [kMIDIPropertyMaxSysExSpeed](https://developer.apple.com/reference/coremidi/kmidipropertymaxsysexspeed) + pub fn max_sysex_speed() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyMaxSysExSpeed }) + } + + /// See [kMIDIPropertyAdvanceScheduleTimeMuSec](https://developer.apple.com/reference/coremidi/kMIDIPropertyAdvanceScheduleTimeMuSec) + pub fn advance_schedule_time_musec() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyAdvanceScheduleTimeMuSec }) + } + + /// See [kMIDIPropertyIsEmbeddedEntity](https://developer.apple.com/reference/coremidi/kMIDIPropertyIsEmbeddedEntity) + pub fn is_embedded_entity() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyIsEmbeddedEntity }) + } + + /// See [kMIDIPropertyIsBroadcast](https://developer.apple.com/reference/coremidi/kMIDIPropertyIsBroadcast) + pub fn is_broadcast() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyIsBroadcast }) + } + + /// See [kMIDIPropertySingleRealtimeEntity](https://developer.apple.com/reference/coremidi/kMIDIPropertySingleRealtimeEntity) + pub fn single_realtime_entity() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertySingleRealtimeEntity }) + } + + /// See [kMIDIPropertyConnectionUniqueID](https://developer.apple.com/reference/coremidi/kMIDIPropertyConnectionUniqueID) + pub fn connection_unique_id() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyConnectionUniqueID }) + } + + /// See [kMIDIPropertyOffline](https://developer.apple.com/reference/coremidi/kMIDIPropertyOffline) + pub fn offline() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyOffline }) + } + + /// See [kMIDIPropertyPrivate](https://developer.apple.com/reference/coremidi/kMIDIPropertyPrivate) + pub fn private() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyPrivate }) + } + + /// See [kMIDIPropertyDriverOwner](https://developer.apple.com/reference/coremidi/kMIDIPropertyDriverOwner) + pub fn driver_owner() -> StringProperty { + StringProperty::from_constant_string_ref(unsafe { kMIDIPropertyDriverOwner }) + } + + // /// See [kMIDIPropertyNameConfiguration](https://developer.apple.com/reference/coremidi/kMIDIPropertyNameConfiguration) + // pub fn name_configuration() -> Property { unsafe { Property(kMIDIPropertyNameConfiguration) } } + + // /// See [kMIDIPropertyImage](https://developer.apple.com/reference/coremidi/kMIDIPropertyImage) + // pub fn image() -> Property { unsafe { Property(kMIDIPropertyImage) } } + + /// See [kMIDIPropertyDriverVersion](https://developer.apple.com/reference/coremidi/kMIDIPropertyDriverVersion) + pub fn driver_version() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyDriverVersion }) + } + + /// See [kMIDIPropertySupportsGeneralMIDI](https://developer.apple.com/reference/coremidi/kMIDIPropertySupportsGeneralMIDI) + pub fn supports_general_midi() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertySupportsGeneralMIDI }) + } + + /// See [kMIDIPropertySupportsMMC](https://developer.apple.com/reference/coremidi/kMIDIPropertySupportsMMC) + pub fn supports_mmc() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertySupportsMMC }) + } + + /// See [kMIDIPropertyCanRoute](https://developer.apple.com/reference/coremidi/kMIDIPropertyCanRoute) + pub fn can_route() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyCanRoute }) + } + + /// See [kMIDIPropertyReceivesClock](https://developer.apple.com/reference/coremidi/kMIDIPropertyReceivesClock) + pub fn receives_clock() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceivesClock }) + } + + /// See [kMIDIPropertyReceivesMTC](https://developer.apple.com/reference/coremidi/kMIDIPropertyReceivesMTC) + pub fn receives_mtc() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceivesMTC }) + } + + /// See [kMIDIPropertyReceivesNotes](https://developer.apple.com/reference/coremidi/kMIDIPropertyReceivesNotes) + pub fn receives_notes() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceivesNotes }) + } + + /// See [kMIDIPropertyReceivesProgramChanges](https://developer.apple.com/reference/coremidi/kMIDIPropertyReceivesProgramChanges) + pub fn receives_program_changes() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceivesProgramChanges }) + } + + /// See [kMIDIPropertyReceivesBankSelectMSB](https://developer.apple.com/reference/coremidi/kMIDIPropertyReceivesBankSelectMSB) + pub fn receives_bank_select_msb() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceivesBankSelectMSB }) + } + + /// See [kMIDIPropertyReceivesBankSelectLSB](https://developer.apple.com/reference/coremidi/kMIDIPropertyReceivesBankSelectLSB) + pub fn receives_bank_select_lsb() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyReceivesBankSelectLSB }) + } + + /// See [kMIDIPropertyTransmitsBankSelectMSB](https://developer.apple.com/reference/coremidi/kMIDIPropertyTransmitsBankSelectMSB) + pub fn transmits_bank_select_msb() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitsBankSelectMSB }) + } + + /// See [kMIDIPropertyTransmitsBankSelectLSB](https://developer.apple.com/reference/coremidi/kMIDIPropertyTransmitsBankSelectLSB) + pub fn transmits_bank_select_lsb() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitsBankSelectLSB }) + } + + /// See [kMIDIPropertyTransmitsClock](https://developer.apple.com/reference/coremidi/kMIDIPropertyTransmitsClock) + pub fn transmits_clock() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitsClock }) + } + + /// See [kMIDIPropertyTransmitsMTC](https://developer.apple.com/reference/coremidi/kMIDIPropertyTransmitsMTC) + pub fn transmits_mtc() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitsMTC }) + } + + /// See [kMIDIPropertyTransmitsNotes](https://developer.apple.com/reference/coremidi/kMIDIPropertyTransmitsNotes) + pub fn transmits_notes() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitsNotes }) + } + + /// See [kMIDIPropertyTransmitsProgramChanges](https://developer.apple.com/reference/coremidi/kMIDIPropertyTransmitsProgramChanges) + pub fn transmits_program_changes() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyTransmitsProgramChanges }) + } + + /// See [kMIDIPropertyPanDisruptsStereo](https://developer.apple.com/reference/coremidi/kMIDIPropertyPanDisruptsStereo) + pub fn pan_disrupts_stereo() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyPanDisruptsStereo }) + } + + /// See [kMIDIPropertyIsSampler](https://developer.apple.com/reference/coremidi/kMIDIPropertyIsSampler) + pub fn is_sampler() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyIsSampler }) + } + + /// See [kMIDIPropertyIsDrumMachine](https://developer.apple.com/reference/coremidi/kMIDIPropertyIsDrumMachine) + pub fn is_drum_machine() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyIsDrumMachine }) + } + + /// See [kMIDIPropertyIsMixer](https://developer.apple.com/reference/coremidi/kMIDIPropertyIsMixer) + pub fn is_mixer() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyIsMixer }) + } + + /// See [kMIDIPropertyIsEffectUnit](https://developer.apple.com/reference/coremidi/kMIDIPropertyIsEffectUnit) + pub fn is_effect_unit() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertyIsEffectUnit }) + } + + /// See [kMIDIPropertyMaxReceiveChannels](https://developer.apple.com/reference/coremidi/kMIDIPropertyMaxReceiveChannels) + pub fn max_receive_channels() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyMaxReceiveChannels }) + } + + /// See [kMIDIPropertyMaxTransmitChannels](https://developer.apple.com/reference/coremidi/kMIDIPropertyMaxTransmitChannels) + pub fn max_transmit_channels() -> IntegerProperty { + IntegerProperty::from_constant_string_ref(unsafe { kMIDIPropertyMaxTransmitChannels }) + } + + /// See [kMIDIPropertyDriverDeviceEditorApp](https://developer.apple.com/reference/coremidi/kMIDIPropertyDriverDeviceEditorApp) + pub fn driver_device_editor_app() -> StringProperty { + StringProperty::from_constant_string_ref(unsafe { kMIDIPropertyDriverDeviceEditorApp }) + } + + /// See [kMIDIPropertySupportsShowControl](https://developer.apple.com/reference/coremidi/kMIDIPropertySupportsShowControl) + pub fn supports_show_control() -> BooleanProperty { + BooleanProperty::from_constant_string_ref(unsafe { kMIDIPropertySupportsShowControl }) + } + + /// See [kMIDIPropertyDisplayName](https://developer.apple.com/reference/coremidi/kMIDIPropertyDisplayName) + pub fn display_name() -> StringProperty { + StringProperty::from_constant_string_ref(unsafe { kMIDIPropertyDisplayName }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{endpoints::destinations::VirtualDestination, Client}; + + const NAME_ORIG: &str = "A"; + + fn setup() -> (Client, VirtualDestination) { + let client = Client::new("Test Client").unwrap(); + let dest = client.virtual_destination(NAME_ORIG, |_| ()).unwrap(); + (client, dest) + } + + mod string { + use super::*; + + const NAME_MODIFIED: &str = "B"; + + // Test getting the original value of the "name" property + fn check_get_original(property: &StringProperty, dest: &VirtualDestination) { + let name: String = property.value_from(dest).unwrap(); + + assert_eq!(name, NAME_ORIG); + } + + // Test setting then getting the "name" property + fn check_roundtrip(property: &StringProperty, dest: &VirtualDestination) { + property.set_value(dest, NAME_MODIFIED).unwrap(); + let name: String = property.value_from(dest).unwrap(); + + assert_eq!(name, NAME_MODIFIED); + } + + #[test] + fn test_from_constant() { + let (_client, dest) = setup(); + let property = Properties::name(); + + check_get_original(&property, &dest); + check_roundtrip(&property, &dest); + } + + #[test] + fn test_new() { + let (_client, dest) = setup(); + // "name" is the value of the CoreMidi constant kMIDIPropertyName + let property = StringProperty::new("name"); + + check_get_original(&property, &dest); + check_roundtrip(&property, &dest); + } + } + + mod integer { + use super::*; + + const ADVANCED_SCHEDULE_TIME: i32 = 44; + + #[test] + fn test_not_set() { + let (_client, dest) = setup(); + // Is not set by default for Virtual Destinations + let property = Properties::advance_schedule_time_musec(); + + let value: Result<i32, _> = property.value_from(&dest); + + assert!(value.is_err()) + } + + #[test] + fn test_roundtrip() { + let (_client, dest) = setup(); + let property = Properties::advance_schedule_time_musec(); + + property.set_value(&dest, ADVANCED_SCHEDULE_TIME).unwrap(); + let num: i32 = property.value_from(&dest).unwrap(); + + assert_eq!(num, ADVANCED_SCHEDULE_TIME); + } + } + + mod boolean { + use super::*; + + #[test] + fn test_not_set() { + let (_client, dest) = setup(); + // Not set by default on Virtual Destinations + let property = Properties::transmits_program_changes(); + + let value: Result<bool, _> = property.value_from(&dest); + + assert!(value.is_err()) + } + + #[test] + fn test_roundtrip() { + let (_client, dest) = setup(); + let property = Properties::private(); + + property.set_value(&dest, true).unwrap(); + let value: bool = property.value_from(&dest).unwrap(); + + assert_eq!(value, true); + } + } +} |