Browse Source

Add conversions between SystemTime and OscTime

This changes `OscTime` from a type alias into a struct. This allows us
to add implementations to `OscTime`, most notably conversions to and
from `std::time::SystemTime`. This has obvious utility such as
generating an OSC timestamp from the current time or some time relative
to it.

This is unfortunately a breaking change for users of this crate.
Conversions to and from `(u8, u8)` are provided to minimize the impact
of this breakage for users.

Addresses issue #13
pull/14/head
Wesley Merkel 3 weeks ago
parent
commit
0d06ccf24e
5 changed files with 166 additions and 19 deletions
  1. +8
    -5
      src/decoder.rs
  2. +5
    -5
      src/encoder.rs
  3. +150
    -6
      src/types.rs
  4. +2
    -2
      tests/decoder_test.rs
  5. +1
    -1
      tests/encoder_test.rs

+ 8
- 5
src/decoder.rs View File

@ -1,7 +1,7 @@
use crate::encoder;
use crate::errors::OscError;
use crate::types::{
OscArray, OscBundle, OscColor, OscMessage, OscMidiMessage, OscPacket, OscType, Result,
OscArray, OscBundle, OscColor, OscMessage, OscMidiMessage, OscPacket, OscTime, OscType, Result,
};
use std::io::{BufRead, Read};
@ -212,15 +212,18 @@ fn read_blob(cursor: &mut io::Cursor<&[u8]>) -> Result {
Ok(OscType::Blob(byte_buf))
}
fn read_time_tag(cursor: &mut io::Cursor<&[u8]>) -> Result<(u32, u32)> {
let date = cursor
fn read_time_tag(cursor: &mut io::Cursor<&[u8]>) -> Result<OscTime> {
let seconds = cursor
.read_u32::<BigEndian>()
.map_err(OscError::ReadError)?;
let frac = cursor
let fractional = cursor
.read_u32::<BigEndian>()
.map_err(OscError::ReadError)?;
Ok((date, frac))
Ok(OscTime {
seconds,
fractional,
})
}
fn read_midi_message(cursor: &mut io::Cursor<&[u8]>) -> Result<OscType> {

+ 5
- 5
src/encoder.rs View File

@ -1,5 +1,5 @@
use crate::errors::OscError;
use crate::types::{OscBundle, OscMessage, OscPacket, OscType, Result};
use crate::types::{OscBundle, OscMessage, OscPacket, OscTime, OscType, Result};
use byteorder::{BigEndian, ByteOrder};
@ -125,7 +125,7 @@ fn encode_arg(arg: &OscType) -> Result<(Option>, String)> {
}
Ok((Some(bytes), "b".into()))
}
OscType::Time((ref x, ref y)) => Ok((Some(encode_time_tag(*x, *y)), "t".into())),
OscType::Time(time) => Ok((Some(encode_time_tag(time)), "t".into())),
OscType::Midi(ref x) => Ok((Some(vec![x.port, x.status, x.data1, x.data2]), "m".into())),
OscType::Color(ref x) => Ok((Some(vec![x.red, x.green, x.blue, x.alpha]), "r".into())),
OscType::Bool(ref x) => {
@ -192,10 +192,10 @@ pub fn pad(pos: u64) -> u64 {
}
}
fn encode_time_tag(sec: u32, frac: u32) -> Vec<u8> {
fn encode_time_tag(time: OscTime) -> Vec<u8> {
let mut bytes = vec![0u8; 8];
BigEndian::write_u32(&mut bytes[..4], sec);
BigEndian::write_u32(&mut bytes[4..], frac);
BigEndian::write_u32(&mut bytes[..4], time.seconds);
BigEndian::write_u32(&mut bytes[4..], time.fractional);
bytes
}

+ 150
- 6
src/types.rs View File

@ -1,9 +1,80 @@
use crate::errors;
use std::{iter::FromIterator, result};
use std::{
iter::FromIterator,
result,
time::{Duration, SystemTime, UNIX_EPOCH},
};
/// A time tag in OSC message consists of two 32-bit integers where the first one denotes the number of seconds since 1900-01-01 and the second the fractions of a second.
/// For details on its semantics see http://opensoundcontrol.org/node/3/#timetags
pub type OscTime = (u32, u32);
///
/// # Examples
///
/// ```
/// use rosc::OscTime;
/// use std::time::SystemTime;
///
/// assert_eq!(OscTime::from(SystemTime::UNIX_EPOCH), OscTime::from((2_208_988_800, 0)));
/// ```
///
/// # Conversions between [`std::time::SystemTime`]
///
/// The [`From`]/[`Into`] traits are implemented to allow conversions between
/// [`SystemTime`](std::time::SystemTime) and `OscTime` in both directions. **These conversions are
/// lossy**, but are tested to have a deviation within 5 nanoseconds when converted back and forth
/// in either direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OscTime {
pub seconds: u32,
pub fractional: u32,
}
impl OscTime {
const UNIX_OFFSET: u64 = 2_208_988_800; // From RFC 5905
const TWO_POW_32: f64 = 4294967296.0; // Number of bits in a `u32`
const NANOS_PER_SECOND: f64 = 1.0e9;
fn epoch() -> SystemTime {
UNIX_EPOCH - Duration::from_secs(OscTime::UNIX_OFFSET)
}
}
impl From<SystemTime> for OscTime {
fn from(time: SystemTime) -> OscTime {
let duration_since_epoch = time.duration_since(OscTime::epoch()).unwrap();
let seconds = duration_since_epoch.as_secs() as u32;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = ((nanos * OscTime::TWO_POW_32) / OscTime::NANOS_PER_SECOND).round() as u32;
OscTime {
seconds,
fractional,
}
}
}
impl From<OscTime> for SystemTime {
fn from(time: OscTime) -> SystemTime {
let nanos = (time.fractional as f64) * OscTime::NANOS_PER_SECOND / OscTime::TWO_POW_32;
let duration_since_osc_epoch = Duration::new(time.seconds as u64, nanos as u32);
OscTime::epoch() + duration_since_osc_epoch
}
}
impl From<(u32, u32)> for OscTime {
fn from(time: (u32, u32)) -> OscTime {
let (seconds, fractional) = time;
OscTime {
seconds,
fractional,
}
}
}
impl From<OscTime> for (u32, u32) {
fn from(time: OscTime) -> (u32, u32) {
(time.seconds, time.fractional)
}
}
/// see OSC Type Tag String: [OSC Spec. 1.0](http://opensoundcontrol.org/spec-1_0)
/// padding: zero bytes (n*4)
@ -60,14 +131,18 @@ value_impl! {
}
impl From<(u32, u32)> for OscType {
fn from(time: (u32, u32)) -> Self {
OscType::Time((time.0, time.1))
OscType::Time(time.into())
}
}
impl From<SystemTime> for OscType {
fn from(time: SystemTime) -> Self {
OscType::Time(time.into())
}
}
impl OscType {
#[allow(dead_code)]
pub fn time(self) -> Option<(u32, u32)> {
pub fn time(self) -> Option<OscTime> {
match self {
OscType::Time((sec, frac)) => Some((sec, frac)),
OscType::Time(time) => Some(time),
_ => None,
}
}
@ -157,3 +232,72 @@ impl<'a> From<&'a str> for OscMessage {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOLERANCE_NANOS: u64 = 5;
#[test]
fn conversions_are_reversible() {
let times = vec![
UNIX_EPOCH,
UNIX_EPOCH - Duration::from_nanos(50),
OscTime::epoch(),
];
for time in times {
for i in 0..1000 {
let time = time + Duration::from_nanos(1) * i;
assert_eq_system_times(time, SystemTime::from(OscTime::from(time)));
}
}
for seconds in 0..10 {
for fractional in 0..1000 {
let osc_time = OscTime {
seconds,
fractional,
};
assert_eq_osc_times(osc_time, OscTime::from(SystemTime::from(osc_time)));
}
}
}
fn assert_eq_system_times(a: SystemTime, b: SystemTime) {
let difference = if a < b {
b.duration_since(a).unwrap()
} else {
a.duration_since(b).unwrap()
};
let tolerance = Duration::from_nanos(TOLERANCE_NANOS);
if difference > tolerance {
panic!(
"the fractional seconds components of {:?} and {:?} vary more than the required tolerance of {:?}",
a, b, tolerance,
);
}
}
fn assert_eq_osc_times(a: OscTime, b: OscTime) {
// I did not want to implement subtraction with carrying in order to implement this in the
// same way as the alternative for system times. Intsead we are compare each part of the
// OSC times separately.
let tolerance_fractional_seconds = ((TOLERANCE_NANOS as f64 * OscTime::TWO_POW_32)
/ OscTime::NANOS_PER_SECOND)
.round() as i64;
assert_eq!(
a.seconds, b.seconds,
"the seconds components of {:?} and {:?} are different",
a, b
);
if (a.fractional as i64 - b.fractional as i64).abs() > tolerance_fractional_seconds {
panic!(
"the fractional seconds components of {:?} and {:?} vary more than the required tolerance of {} fractional seconds",
a, b, tolerance_fractional_seconds,
);
}
}
}

+ 2
- 2
tests/decoder_test.rs View File

@ -4,7 +4,7 @@ extern crate rosc;
use byteorder::{BigEndian, ByteOrder};
use std::mem;
use rosc::{decoder, encoder, OscBundle, OscPacket, OscType};
use rosc::{decoder, encoder, OscBundle, OscPacket, OscTime, OscType};
#[test]
fn test_decode_no_args() {
@ -27,7 +27,7 @@ fn test_decode_no_args() {
#[test]
fn test_decode_empty_bundle() {
let timetag = (4, 2);
let timetag = OscTime::from((4, 2));
let content = vec![];
let packet = encoder::encode(&OscPacket::Bundle(OscBundle { timetag, content })).unwrap();
let osc_packet: Result<rosc::OscPacket, rosc::OscError> = decoder::decode(&packet);

+ 1
- 1
tests/encoder_test.rs View File

@ -29,7 +29,7 @@ fn test_encode_message_wo_args() {
#[test]
fn test_encode_empty_bundle() {
let bundle_packet = OscPacket::Bundle(OscBundle {
timetag: (4, 2),
timetag: (4, 2).into(),
content: vec![],
});

Loading…
Cancel
Save