Skip to content

Commit

Permalink
Merge pull request #20 from webern/bendy
Browse files Browse the repository at this point in the history
implement pitch bend messages
  • Loading branch information
webern authored Oct 5, 2023
2 parents f537434 + 766f181 commit 77ba05a
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 9 deletions.
133 changes: 133 additions & 0 deletions src/core/bits.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
}
}
}
19 changes: 12 additions & 7 deletions src/core/message.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -116,10 +116,10 @@ impl PitchBendMessage {
impl WriteBytes for PitchBendMessage {
fn write<W: Write>(&self, w: &mut Scribe<W>) -> 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(())
}
}
Expand Down Expand Up @@ -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"),
}
Expand Down
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/data/pitch_bend.info
Original file line number Diff line number Diff line change
@@ -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.
Binary file added tests/data/pitch_bend.mid
Binary file not shown.
3 changes: 3 additions & 0 deletions tests/data/pitch_bend_two_bytes.info
Original file line number Diff line number Diff line change
@@ -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.
Binary file added tests/data/pitch_bend_two_bytes.mid
Binary file not shown.
87 changes: 87 additions & 0 deletions tests/integ.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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);
}
12 changes: 11 additions & 1 deletion tests/roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions tests/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 77ba05a

Please sign in to comment.