Skip to content

Commit bf290cb

Browse files
committed
Fix ntp/osc timestamp/timetag conversion and TimedMessage time.
1 parent 9da6496 commit bf290cb

4 files changed

Lines changed: 74 additions & 53 deletions

File tree

pythonosc/osc_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def _parse_datagram(self) -> None:
5353
elif param == "m": # MIDI.
5454
val, index = osc_types.get_midi(self._dgram, index)
5555
elif param == "t": # osc time tag:
56-
val, index = osc_types.get_ttag(self._dgram, index)
56+
val, index = osc_types.get_timetag(self._dgram, index)
5757
elif param == "T": # True.
5858
val = True
5959
elif param == "F": # False.

pythonosc/osc_packet.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
It lets you access easily to OscMessage and OscBundle instances in the packet.
44
"""
55

6-
import calendar
76
import collections
87
import time
98

@@ -22,11 +21,11 @@
2221
field_names=('time', 'message'))
2322

2423

25-
def _timed_msg_of_bundle(bundle: osc_bundle.OscBundle, now: int) -> List[TimedMessage]:
24+
def _timed_msg_of_bundle(bundle: osc_bundle.OscBundle, now: float) -> List[TimedMessage]:
2625
"""Returns messages contained in nested bundles as a list of TimedMessage."""
2726
msgs = []
2827
for content in bundle:
29-
if type(content) == osc_message.OscMessage:
28+
if type(content) is osc_message.OscMessage:
3029
if (bundle.timestamp == osc_types.IMMEDIATELY or bundle.timestamp < now):
3130
msgs.append(TimedMessage(now, content))
3231
else:
@@ -56,7 +55,7 @@ def __init__(self, dgram: bytes) -> None:
5655
Raises:
5756
- ParseError if the datagram could not be parsed.
5857
"""
59-
now = calendar.timegm(time.gmtime())
58+
now = time.time()
6059
try:
6160
if osc_bundle.OscBundle.dgram_is_bundle(dgram):
6261
self._messages = sorted(

pythonosc/parsing/ntp.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
from typing import Union
88

9-
# conversion factor for fractional seconds (maximum value of fractional part)
10-
FRACTIONAL_CONVERSION = 2 ** 32
119

1210
# 63 zero bits followed by a one in the least signifigant bit is a special
1311
# case meaning "immediately."
14-
IMMEDIATELY = struct.pack('>q', 1)
12+
IMMEDIATELY = struct.pack('>Q', 1)
13+
14+
# timetag * (1 / 2 ** 32) == l32bits + (r32bits / 1 ** 32)
15+
_NTP_TIMESTAMP_TO_SECONDS = 1. / 2. ** 32.
16+
_SECONDS_TO_NTP_TIMESTAMP = 2. ** 32.
1517

1618
# From NTP lib.
1719
_SYSTEM_EPOCH = datetime.date(*time.gmtime(0)[0:3])
@@ -24,27 +26,33 @@ class NtpError(Exception):
2426
"""Base class for ntp module errors."""
2527

2628

27-
def ntp_to_system_time(date: Union[int, float]) -> Union[int, float]:
28-
"""Convert a NTP time to system time.
29-
30-
System time is reprensented by seconds since the epoch in UTC.
29+
def ntp_to_system_time(timestamp: bytes) -> float:
30+
"""Convert a NTP timestamp to system time in seconds.
3131
"""
32-
return date - _NTP_DELTA
32+
try:
33+
timestamp = struct.unpack('>Q', timestamp)
34+
except Exception as e:
35+
raise NtpError(e)
36+
return timestamp * _NTP_TIMESTAMP_TO_SECONDS - _NTP_DELTA
3337

34-
def system_time_to_ntp(date: Union[int, float]) -> bytes:
35-
"""Convert a system time to NTP time.
3638

37-
System time is reprensented by seconds since the epoch in UTC.
39+
def system_time_to_ntp(seconds: float) -> bytes:
40+
"""Convert a system time in seconds to NTP timestamp.
3841
"""
3942
try:
40-
num_secs = int(date)
43+
seconds = seconds + _NTP_DELTA
4144
except ValueError as e:
4245
raise NtpError(e)
43-
44-
num_secs_ntp = num_secs + _NTP_DELTA
45-
46-
sec_frac = float(date - num_secs)
47-
48-
picos = int(sec_frac * FRACTIONAL_CONVERSION)
49-
50-
return struct.pack('>I', int(num_secs_ntp)) + struct.pack('>I', picos)
46+
return struct.pack('>Q', int(seconds * _SECONDS_TO_NTP_TIMESTAMP))
47+
48+
49+
def ntp_time_to_system_epoch(seconds: float) -> float:
50+
"""Convert a NTP time in seconds to system time in seconds.
51+
"""
52+
return seconds - _NTP_DELTA
53+
54+
55+
def system_time_to_ntp_epoch(seconds: float) -> float:
56+
"""Convert a system time in seconds to NTP time in seconds.
57+
"""
58+
return seconds + _NTP_DELTA

pythonosc/parsing/osc_types.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Functions to get OSC types from datagrams and vice versa"""
22

3-
import decimal
43
import struct
54

65
from pythonosc.parsing import ntp
@@ -22,9 +21,10 @@ class BuildError(Exception):
2221

2322
# Datagram length in bytes for types that have a fixed size.
2423
_INT_DGRAM_LEN = 4
24+
_UINT64_DGRAM_LEN = 8
2525
_FLOAT_DGRAM_LEN = 4
2626
_DOUBLE_DGRAM_LEN = 8
27-
_DATE_DGRAM_LEN = _INT_DGRAM_LEN * 2
27+
_TIMETAG_DGRAM_LEN = 8
2828
# Strings and blob dgram length is always a multiple of 4 bytes.
2929
_STRING_DGRAM_PAD = 4
3030
_BLOB_DGRAM_PAD = 4
@@ -126,7 +126,31 @@ def get_int(dgram: bytes, start_index: int) -> Tuple[int, int]:
126126
raise ParseError('Could not parse datagram %s' % e)
127127

128128

129-
def get_ttag(dgram: bytes, start_index: int) -> Tuple[datetime, int]:
129+
def get_uint64(dgram: bytes, start_index: int) -> Tuple[int, int]:
130+
"""Get a 64-bit big-endian unsigned integer from the datagram.
131+
132+
Args:
133+
dgram: A datagram packet.
134+
start_index: An index where the integer starts in the datagram.
135+
136+
Returns:
137+
A tuple containing the integer and the new end index.
138+
139+
Raises:
140+
ParseError if the datagram could not be parsed.
141+
"""
142+
try:
143+
if len(dgram[start_index:]) < _UINT64_DGRAM_LEN:
144+
raise ParseError('Datagram is too short')
145+
return (
146+
struct.unpack('>Q',
147+
dgram[start_index:start_index + _UINT64_DGRAM_LEN])[0],
148+
start_index + _UINT64_DGRAM_LEN)
149+
except (struct.error, TypeError) as e:
150+
raise ParseError('Could not parse datagram %s' % e)
151+
152+
153+
def get_timetag(dgram: bytes, start_index: int) -> Tuple[datetime, int]:
130154
"""Get a 64-bit OSC time tag from the datagram.
131155
132156
Args:
@@ -140,29 +164,22 @@ def get_ttag(dgram: bytes, start_index: int) -> Tuple[datetime, int]:
140164
Raises:
141165
ParseError if the datagram could not be parsed.
142166
"""
143-
144-
_TTAG_DGRAM_LEN = 8
145-
146167
try:
147-
if len(dgram[start_index:]) < _TTAG_DGRAM_LEN:
168+
if len(dgram[start_index:]) < _TIMETAG_DGRAM_LEN:
148169
raise ParseError('Datagram is too short')
149170

150-
seconds, idx = get_int(dgram, start_index)
151-
second_decimals, _ = get_int(dgram, idx)
152-
153-
if seconds < 0:
154-
seconds += ntp.FRACTIONAL_CONVERSION
155-
156-
if second_decimals < 0:
157-
second_decimals += ntp.FRACTIONAL_CONVERSION
171+
timetag, _ = get_uint64(dgram, start_index)
172+
seconds_float = timetag * ntp._NTP_TIMESTAMP_TO_SECONDS
173+
seconds = int(seconds_float)
174+
fraction = seconds_float - seconds
158175

159176
hours, seconds = seconds // 3600, seconds % 3600
160177
minutes, seconds = seconds // 60, seconds % 60
161178

162-
utc = datetime.combine(ntp._NTP_EPOCH, datetime.min.time()) + timedelta(hours=hours, minutes=minutes,
163-
seconds=seconds)
179+
utc = (datetime.combine(ntp._NTP_EPOCH, datetime.min.time()) +
180+
timedelta(hours=hours, minutes=minutes, seconds=seconds))
164181

165-
return (utc, second_decimals), start_index + _TTAG_DGRAM_LEN
182+
return (utc, fraction), start_index + _TIMETAG_DGRAM_LEN
166183
except (struct.error, TypeError) as e:
167184
raise ParseError('Could not parse datagram %s' % e)
168185

@@ -284,7 +301,7 @@ def write_blob(val: bytes) -> bytes:
284301
return dgram
285302

286303

287-
def get_date(dgram: bytes, start_index: int) -> Tuple[Union[int, float], int]:
304+
def get_date(dgram: bytes, start_index: int) -> Tuple[float, int]:
288305
"""Get a 64-bit big-endian fixed-point time tag as a date from the datagram.
289306
290307
According to the specifications, a date is represented as is:
@@ -304,16 +321,13 @@ def get_date(dgram: bytes, start_index: int) -> Tuple[Union[int, float], int]:
304321
ParseError if the datagram could not be parsed.
305322
"""
306323
# Check for the special case first.
307-
if dgram[start_index:start_index + _DATE_DGRAM_LEN] == ntp.IMMEDIATELY:
308-
return IMMEDIATELY, start_index + _DATE_DGRAM_LEN
309-
if len(dgram[start_index:]) < _DATE_DGRAM_LEN:
324+
if dgram[start_index:start_index + _TIMETAG_DGRAM_LEN] == ntp.IMMEDIATELY:
325+
return IMMEDIATELY, start_index + _TIMETAG_DGRAM_LEN
326+
if len(dgram[start_index:]) < _TIMETAG_DGRAM_LEN:
310327
raise ParseError('Datagram is too short')
311-
num_secs, start_index = get_int(dgram, start_index)
312-
fraction, start_index = get_int(dgram, start_index)
313-
# Sum seconds and fraction of second:
314-
system_time = num_secs + (fraction / ntp.FRACTIONAL_CONVERSION)
315-
316-
return ntp.ntp_to_system_time(system_time), start_index
328+
timetag, start_index = get_uint64(dgram, start_index)
329+
seconds = timetag * ntp._NTP_TIMESTAMP_TO_SECONDS
330+
return ntp.ntp_time_to_system_epoch(seconds), start_index
317331

318332

319333
def write_date(system_time: Union[int, float]) -> bytes:

0 commit comments

Comments
 (0)