Skip to content

Commit

Permalink
implement pitch bend messages
Browse files Browse the repository at this point in the history
  • Loading branch information
webern committed Oct 5, 2023
1 parent f537434 commit c2f93cc
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 9 deletions.
139 changes: 139 additions & 0 deletions src/core/bits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Two 7-bit numbers

/*
Max 14-bit value
16383: 0b0011111111111111
Logic Value, Translated Value, Logic Num, Logic Val
0, 64?, 0, 64
*/

#[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
}

#[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
}

#[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
);
}
}
}
}
39 changes: 32 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,12 @@ 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);
println!("decoded flags: {:#018b}, value: {}", decoded, decoded);
println!("encoded flags: {:#018b}, value: {}", encoded, encoded);
write_u8!(w, ((encoded >> 8) as u8))?;
write_u8!(w, ((encoded & 0b0000000011111111) as u8))?;
Ok(())
}
}
Expand Down Expand Up @@ -334,7 +336,30 @@ 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 v = iter.read_u16().unwrap();
// let flags = 0b0000000000101100u16;
println!("original flags: {:#018b}, value: {}", v, v);
let decoded = decode_14_bit_number(v);
println!("decoded flags: {:#018b}, value: {}", decoded, decoded);

// let lsb = iter.read_or_die().context(io!())?;
// let msb = iter.read_or_die().context(io!())?;
// println!("pitch bend read msb lsb {} {}", msb, lsb);
// let high_bits = (msb as u16) << 7;
// let low_bits = (lsb as u16) & 0b0000_0000_0111_111;
// println!(
// "pitch bend read high_bits low_bits {} {}",
// high_bits, low_bits
// );
// let value = high_bits | low_bits;
// println!("______________________PITCH BEND READ: {}", value);

// let value = println!("value {}", value);
Ok(Message::PitchBend(PitchBendMessage {
channel,
// pitch_bend: PitchBendValue::new(value),
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 @@ -10,6 +10,7 @@ mod message;
mod numbers;
mod status_type;
pub(crate) mod vlq;
mod bits;

pub use clocks::Clocks;
pub use duration_name::DurationName;
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#![deny(clippy::complexity)]
#![deny(clippy::perf)]
#![deny(clippy::style)]
#![deny(dead_code)]
// #![deny(dead_code)]
// TODO - maybe document all pub(crate) types
// #![deny(missing_crate_level_docs)]
// TODO - document all
Expand Down
3 changes: 3 additions & 0 deletions tests/data/pitch_bend.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Something I exported from Logic so that I know the exact pitch bend values. It has the following
pitch bend values on each beat 0, 20, 40, 127 , 125, 101, 40, 20.
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, 16384, 0, 1, 8192.
Creative commons, Public domain, etc.
Binary file added tests/data/pitch_bend_two_bytes.mid
Binary file not shown.
91 changes: 91 additions & 0 deletions tests/integ.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
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 std::path::PathBuf;
use tempfile::tempdir;
use utils::{enable_logging, test_file, AVE_MARIS_STELLA};

#[test]
Expand Down Expand Up @@ -176,3 +179,91 @@ 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().skip(8).next().unwrap().event(), 0 * 128);
assert_pitch_bend(track.events().skip(9).next().unwrap().event(), 20 * 128);
assert_pitch_bend(track.events().skip(10).next().unwrap().event(), 40 * 128);
assert_pitch_bend(track.events().skip(11).next().unwrap().event(), 127 * 128);
assert_pitch_bend(track.events().skip(12).next().unwrap().event(), 125 * 128);
assert_pitch_bend(track.events().skip(13).next().unwrap().event(), 101 * 128);
assert_pitch_bend(track.events().skip(14).next().unwrap().event(), 40 * 128);
assert_pitch_bend(track.events().skip(15).next().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().skip(8).next().unwrap().event(), 0 * 128);
assert_pitch_bend(track.events().skip(9).next().unwrap().event(), 20 * 128);
assert_pitch_bend(track.events().skip(10).next().unwrap().event(), 40 * 128);
assert_pitch_bend(track.events().skip(11).next().unwrap().event(), 127 * 128);
assert_pitch_bend(track.events().skip(12).next().unwrap().event(), 125 * 128);
assert_pitch_bend(track.events().skip(13).next().unwrap().event(), 101 * 128);
assert_pitch_bend(track.events().skip(14).next().unwrap().event(), 40 * 128);
assert_pitch_bend(track.events().skip(15).next().unwrap().event(), 20 * 128);
}

#[test]
fn pitch_bend_two_byte() {
enable_logging();
println!("READING ORIGINAL ------------------------------------------------------------------");
let midi_file = MidiFile::load(test_file(PITCH_BEND_TWO_BYTES)).unwrap();
println!("{:?}", midi_file);
let track = midi_file.tracks().skip(1).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);
}

assert_pitch_bend(track.events().skip(1).next().unwrap().event(), 8192);
assert_pitch_bend(track.events().skip(3).next().unwrap().event(), 8292);
assert_pitch_bend(track.events().skip(4).next().unwrap().event(), 8092);
assert_pitch_bend(track.events().skip(5).next().unwrap().event(), 16383);
assert_pitch_bend(track.events().skip(6).next().unwrap().event(), 0);
assert_pitch_bend(track.events().skip(7).next().unwrap().event(), 0);
assert_pitch_bend(track.events().skip(8).next().unwrap().event(), 1);
let tempdir = tempdir().unwrap();
let path = tempdir.path().join("file.mid");
println!("WRITING ---------------------------------------------------------------------------");
midi_file.save(&path).unwrap();
println!("RELOADING -------------------------------------------------------------------------");
let midi_file = MidiFile::load(&path).unwrap();
let track = midi_file.tracks().skip(1).next().unwrap();
assert_pitch_bend(track.events().skip(1).next().unwrap().event(), 8192);
assert_pitch_bend(track.events().skip(3).next().unwrap().event(), 8292);
assert_pitch_bend(track.events().skip(4).next().unwrap().event(), 8092);
assert_pitch_bend(track.events().skip(5).next().unwrap().event(), 16383);
assert_pitch_bend(track.events().skip(6).next().unwrap().event(), 0);
assert_pitch_bend(track.events().skip(7).next().unwrap().event(), 0);
assert_pitch_bend(track.events().skip(8).next().unwrap().event(), 1);
}
13 changes: 12 additions & 1 deletion tests/roundtrip.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
mod utils;

use crate::utils::PITCH_BEND_TWO_BYTES;
use midi_file::MidiFile;
use std::fmt::{Debug, Display, Formatter};
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, TOBEFREE,
};

type RtResult = std::result::Result<(), RtErr>;
Expand Down Expand Up @@ -241,6 +242,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 c2f93cc

Please sign in to comment.