diff --git a/src/core/bits.rs b/src/core/bits.rs new file mode 100644 index 0000000..8dfb1c7 --- /dev/null +++ b/src/core/bits.rs @@ -0,0 +1,133 @@ +/*! + +This module is for dealing with bit operations that were hard to figure out. + +!*/ + +#[inline] +pub(crate) fn decode_14_bit_number(bits: u16) -> u16 { + ((extract_high_bits(bits) as u16) << 7) | (extract_low_bits(bits) as u16) +} + +#[inline] +pub(crate) fn encode_14_bit_number(value: u16) -> u16 { + let hi_bits = value & 0b0011111110000000; + let lo_bits = value & 0b0000000001111111; + let lo_moved = lo_bits << 8; + let hi_moved = hi_bits >> 7; + lo_moved | hi_moved +} + +#[inline] +fn extract_low_bits(bits: u16) -> u8 { + ((bits >> 8) as u8) & 0b0000000001111111 +} + +#[inline] +fn extract_high_bits(bits: u16) -> u8 { + (bits & 0b0000000001111111) as u8 +} + +#[cfg(test)] +mod bit_tests { + use super::*; + + struct Number14Bit { + encoded: u16, + decoded: u16, + lo_bits: u8, + hi_bits: u8, + } + const NUMBER_14_BIT_08192: Number14Bit = Number14Bit { + encoded: 0b0000000001000000, + decoded: 0b0010000000000000, + lo_bits: 0b0000000, + hi_bits: 0b1000000, + }; + + const NUMBER_14_BIT_08292: Number14Bit = Number14Bit { + encoded: 0b0110010001000000, + decoded: 0b0010000001100100, + lo_bits: 0b1100100, + hi_bits: 0b1000000, + }; + const NUMBER_14_BIT_08092: Number14Bit = Number14Bit { + encoded: 0b0001110000111111, + decoded: 0b0001111110011100, + lo_bits: 0b0011100, + hi_bits: 0b0111111, + }; + const NUMBER_14_BIT_16383: Number14Bit = Number14Bit { + encoded: 0b0111111101111111, + decoded: 0b0011111111111111, + lo_bits: 0b1111111, + hi_bits: 0b1111111, + }; + const NUMBER_14_BIT_00001: Number14Bit = Number14Bit { + encoded: 0b0000000100000000, + decoded: 0b0000000000000001, + lo_bits: 0b0000001, + hi_bits: 0b0000000, + }; + + #[test] + fn test_14_bit_08192() { + let data = NUMBER_14_BIT_08192; + assert_eq!(extract_low_bits(data.encoded), data.lo_bits); + assert_eq!(extract_high_bits(data.encoded), data.hi_bits); + assert_eq!(decode_14_bit_number(data.encoded), data.decoded); + assert_eq!(encode_14_bit_number(data.decoded), data.encoded); + } + + #[test] + fn test_14_bit_08292() { + let data = NUMBER_14_BIT_08292; + assert_eq!(extract_low_bits(data.encoded), data.lo_bits); + assert_eq!(extract_high_bits(data.encoded), data.hi_bits); + assert_eq!(decode_14_bit_number(data.encoded), data.decoded); + assert_eq!(encode_14_bit_number(data.decoded), data.encoded); + } + + #[test] + fn test_14_bit_08092() { + let data = NUMBER_14_BIT_08092; + assert_eq!(extract_low_bits(data.encoded), data.lo_bits); + assert_eq!(extract_high_bits(data.encoded), data.hi_bits); + assert_eq!(decode_14_bit_number(data.encoded), data.decoded); + assert_eq!(encode_14_bit_number(data.decoded), data.encoded); + } + #[test] + fn test_14_bit_16383() { + let data = NUMBER_14_BIT_16383; + assert_eq!(extract_low_bits(data.encoded), data.lo_bits); + assert_eq!(extract_high_bits(data.encoded), data.hi_bits); + assert_eq!(decode_14_bit_number(data.encoded), data.decoded); + assert_eq!(encode_14_bit_number(data.decoded), data.encoded); + } + + #[test] + fn test_14_bit_00001() { + let data = NUMBER_14_BIT_00001; + assert_eq!(extract_low_bits(data.encoded), data.lo_bits); + assert_eq!(extract_high_bits(data.encoded), data.hi_bits); + assert_eq!(decode_14_bit_number(data.encoded), data.decoded); + assert_eq!(encode_14_bit_number(data.decoded), data.encoded); + } + + #[test] + fn test_14_all() { + for i in 0..=16383u16 { + let original = i; + let encoded = encode_14_bit_number(original); + let decoded = decode_14_bit_number(encoded); + assert_eq!(original, decoded); + if original != 0 { + assert_ne!( + encoded, decoded, + "encoded should not equal decoded, {} == {}", + encoded, decoded + ); + } + } + } +} diff --git a/src/core/message.rs b/src/core/message.rs index 6712411..de309fc 100644 --- a/src/core/message.rs +++ b/src/core/message.rs @@ -1,12 +1,12 @@ use crate::byte_iter::ByteIter; +use crate::core::bits::{decode_14_bit_number, encode_14_bit_number}; use crate::core::{ Channel, ControlValue, MonoModeChannels, NoteNumber, PitchBendValue, Program, StatusType, Velocity, }; use crate::error::{self, LibResult}; use crate::scribe::Scribe; -use log::trace; -use log::warn; +use log::{trace, warn}; use snafu::{OptionExt, ResultExt}; use std::convert::TryFrom; use std::io::{Read, Write}; @@ -116,10 +116,10 @@ impl PitchBendMessage { impl WriteBytes for PitchBendMessage { fn write(&self, w: &mut Scribe) -> LibResult<()> { write_status_byte(w, StatusType::PitchBend, self.channel)?; - let lsb = (self.pitch_bend.get() & 0x74) as u8; - let msb = ((self.pitch_bend.get() >> 7) & 0x74) as u8; - write_u8!(w, lsb)?; - write_u8!(w, msb)?; + let decoded = self.pitch_bend.get(); + let encoded = encode_14_bit_number(decoded); + write_u8!(w, ((encoded >> 8) as u8))?; + write_u8!(w, ((encoded & 0b0000000011111111) as u8))?; Ok(()) } } @@ -334,7 +334,12 @@ impl Message { noimpl!("channel pressure: https://github.com/webern/midi_file/issues/X") } StatusType::PitchBend => { - noimpl!("pitch bend: https://github.com/webern/midi_file/issues/10") + let value = iter.read_u16().unwrap(); + let decoded = decode_14_bit_number(value); + Ok(Message::PitchBend(PitchBendMessage { + channel, + pitch_bend: PitchBendValue::new(decoded), + })) } StatusType::System => noimpl!("system: https://github.com/webern/midi_file/issues/10"), } diff --git a/src/core/mod.rs b/src/core/mod.rs index 8ade474..f7399ee 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3,6 +3,7 @@ The `core` module is for types and concepts that are *not* strictly related to M These types and concepts could be used for realtime MIDI as well. !*/ +mod bits; mod clocks; mod duration_name; mod general_midi; diff --git a/src/lib.rs b/src/lib.rs index 94a8d14..8a2bb75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ #![deny(clippy::complexity)] #![deny(clippy::perf)] #![deny(clippy::style)] -#![deny(dead_code)] // TODO - maybe document all pub(crate) types // #![deny(missing_crate_level_docs)] // TODO - document all diff --git a/tests/data/pitch_bend.info b/tests/data/pitch_bend.info new file mode 100644 index 0000000..fc08260 --- /dev/null +++ b/tests/data/pitch_bend.info @@ -0,0 +1,5 @@ +Something I exported from Logic so that I know the exact pitch bend values. It has the following +pitch bend values shown by logic on each beat 0, 20, 40, 127 , 125, 101, 40, 20. Note that the +actual numbers are multiplied by 128 because Logic pretends pitch bend is 7-bits when it is actually +14-bits. +Creative commons, Public domain, etc. diff --git a/tests/data/pitch_bend.mid b/tests/data/pitch_bend.mid new file mode 100644 index 0000000..e7a02bc Binary files /dev/null and b/tests/data/pitch_bend.mid differ diff --git a/tests/data/pitch_bend_two_bytes.info b/tests/data/pitch_bend_two_bytes.info new file mode 100644 index 0000000..319136b --- /dev/null +++ b/tests/data/pitch_bend_two_bytes.info @@ -0,0 +1,3 @@ +Created with this amazing tool https://signal.vercel.app/edit +Pitch bend values are 8192, 8292, 8092, 16383, 0, 0, 1, 8192. +Creative commons, Public domain, etc. diff --git a/tests/data/pitch_bend_two_bytes.mid b/tests/data/pitch_bend_two_bytes.mid new file mode 100644 index 0000000..df95913 Binary files /dev/null and b/tests/data/pitch_bend_two_bytes.mid differ diff --git a/tests/integ.rs b/tests/integ.rs index 66ab24c..3316191 100644 --- a/tests/integ.rs +++ b/tests/integ.rs @@ -1,10 +1,12 @@ mod utils; +use crate::utils::{PITCH_BEND, PITCH_BEND_TWO_BYTES}; use midi_file::core::{Clocks, Control, DurationName, Message}; use midi_file::file::{Division, Event, Format, MetaEvent, QuarterNoteDivision}; use midi_file::MidiFile; use std::fs::File; use std::io::Read; +use tempfile::tempdir; use utils::{enable_logging, test_file, AVE_MARIS_STELLA}; #[test] @@ -176,3 +178,88 @@ fn ave_maris_stella_finale_export() { assert_eq!(original, written); } } + +#[test] +fn pitch_bend() { + enable_logging(); + let midi_file = MidiFile::load(test_file(PITCH_BEND)).unwrap(); + let track = midi_file.tracks().next().unwrap(); + + fn assert_pitch_bend(event: &Event, expected: u16) { + let message = match event { + Event::Midi(message) => message, + _ => panic!("wrong event type {:?}", event), + }; + let pitch_bend_message = match message { + Message::PitchBend(p) => p, + _ => panic!("wrong message type {:?}", message), + }; + assert_eq!(pitch_bend_message.pitch_bend().get(), expected); + } + + // The file was created with Logic Pro, which treats Pitch Bend values as a single 7-bit number, + // from 0-127 instead of using the full range. If we multiply by 128 then we get the actual, + // written 14-bit value instead of the value displayed in Logic's UI. + assert_pitch_bend(track.events().nth(8).unwrap().event(), 0); + assert_pitch_bend(track.events().nth(9).unwrap().event(), 20 * 128); + assert_pitch_bend(track.events().nth(10).unwrap().event(), 40 * 128); + assert_pitch_bend(track.events().nth(11).unwrap().event(), 127 * 128); + assert_pitch_bend(track.events().nth(12).unwrap().event(), 125 * 128); + assert_pitch_bend(track.events().nth(13).unwrap().event(), 101 * 128); + assert_pitch_bend(track.events().nth(14).unwrap().event(), 40 * 128); + assert_pitch_bend(track.events().nth(15).unwrap().event(), 20 * 128); + + let tempdir = tempdir().unwrap(); + let path = tempdir.path().join("file.mid"); + midi_file.save(&path).unwrap(); + let midi_file = MidiFile::load(&path).unwrap(); + let track = midi_file.tracks().next().unwrap(); + assert_pitch_bend(track.events().nth(8).unwrap().event(), 0); + assert_pitch_bend(track.events().nth(9).unwrap().event(), 20 * 128); + assert_pitch_bend(track.events().nth(10).unwrap().event(), 40 * 128); + assert_pitch_bend(track.events().nth(11).unwrap().event(), 127 * 128); + assert_pitch_bend(track.events().nth(12).unwrap().event(), 125 * 128); + assert_pitch_bend(track.events().nth(13).unwrap().event(), 101 * 128); + assert_pitch_bend(track.events().nth(14).unwrap().event(), 40 * 128); + assert_pitch_bend(track.events().nth(15).unwrap().event(), 20 * 128); +} + +#[test] +fn pitch_bend_two_byte() { + enable_logging(); + let midi_file = MidiFile::load(test_file(PITCH_BEND_TWO_BYTES)).unwrap(); + let track = midi_file.tracks().nth(1).unwrap(); + + fn assert_pitch_bend(event: &Event, expected: u16) { + let message = match event { + Event::Midi(message) => message, + _ => panic!("wrong event type {:?}", event), + }; + let pitch_bend_message = match message { + Message::PitchBend(p) => p, + _ => panic!("wrong message type {:?}", message), + }; + assert_eq!(pitch_bend_message.pitch_bend().get(), expected); + } + + assert_pitch_bend(track.events().nth(1).unwrap().event(), 8192); + assert_pitch_bend(track.events().nth(3).unwrap().event(), 8292); + assert_pitch_bend(track.events().nth(4).unwrap().event(), 8092); + assert_pitch_bend(track.events().nth(5).unwrap().event(), 16383); + assert_pitch_bend(track.events().nth(6).unwrap().event(), 0); + assert_pitch_bend(track.events().nth(7).unwrap().event(), 0); + assert_pitch_bend(track.events().nth(8).unwrap().event(), 1); + + let tempdir = tempdir().unwrap(); + let path = tempdir.path().join("file.mid"); + midi_file.save(&path).unwrap(); + let midi_file = MidiFile::load(&path).unwrap(); + let track = midi_file.tracks().nth(1).unwrap(); + assert_pitch_bend(track.events().nth(1).unwrap().event(), 8192); + assert_pitch_bend(track.events().nth(3).unwrap().event(), 8292); + assert_pitch_bend(track.events().nth(4).unwrap().event(), 8092); + assert_pitch_bend(track.events().nth(5).unwrap().event(), 16383); + assert_pitch_bend(track.events().nth(6).unwrap().event(), 0); + assert_pitch_bend(track.events().nth(7).unwrap().event(), 0); + assert_pitch_bend(track.events().nth(8).unwrap().event(), 1); +} diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs index d82e1f5..dfa8c59 100644 --- a/tests/roundtrip.rs +++ b/tests/roundtrip.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use tempfile::TempDir; use utils::{ enable_logging, test_file, ADESTE_FIDELES, ALS_DIE_ROEMER, AVE_MARIS_STELLA, BARITONE_SAX, - B_GUAJEO, LATER_FOLIA, LOGIC_PRO, PHOBOS_DORICO, TOBEFREE, + B_GUAJEO, LATER_FOLIA, LOGIC_PRO, PHOBOS_DORICO, PITCH_BEND, PITCH_BEND_TWO_BYTES, TOBEFREE, }; type RtResult = std::result::Result<(), RtErr>; @@ -241,6 +241,16 @@ fn phobos_dorico() { round_trip_test(PHOBOS_DORICO).unwrap(); } +#[test] +fn pitch_bend() { + round_trip_test(PITCH_BEND).unwrap(); +} + +#[test] +fn pitch_bend_two_bytes() { + round_trip_test(PITCH_BEND_TWO_BYTES).unwrap(); +} + #[test] fn tobeefree() { round_trip_test(TOBEFREE).unwrap(); diff --git a/tests/utils.rs b/tests/utils.rs index 1a0ea63..098c3ed 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -13,6 +13,8 @@ pub const B_GUAJEO: &str = "b_guajeo.mid"; pub const LATER_FOLIA: &str = "later_folia.mid"; pub const LOGIC_PRO: &str = "logic_pro.mid"; pub const PHOBOS_DORICO: &str = "phobos_dorico.mid"; +pub const PITCH_BEND: &str = "pitch_bend.mid"; +pub const PITCH_BEND_TWO_BYTES: &str = "pitch_bend_two_bytes.mid"; pub const TOBEFREE: &str = "tobefree.mid"; static LOGGER: Once = Once::new();