Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve conversions between ticks and seconds #531

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: CC0-1.0

[flake8]
ignore = F401, C901, F901
# W503 and W504 are incompatible. Prefer the modern approach.
ignore = F401, C901, F901, W503
exclude = .git,__pycache__,docs/conf.py,old,build,dist
max-complexity = 10
4 changes: 4 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,12 @@ Standard MIDI Files

.. autofunction:: tick2second

.. autofunction:: ticks2seconds

.. autofunction:: second2tick

.. autofunction:: seconds2ticks

.. autofunction:: bpm2tempo

.. autofunction:: tempo2bpm
Expand Down
12 changes: 7 additions & 5 deletions docs/files/midi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,13 @@ in number of beats per minute (BPM) or microseconds per quarter note, see
`MIDI Tempo vs. BPM`_ above) and ticks per per quarter note have to be decided
upon.

You can use :py:func:`tick2second` and :py:func:`second2tick` to convert to
You can use :py:func:`ticks2seconds` and :py:func:`seconds2ticks` to convert to
and from seconds and ticks. Note that integer rounding of the result might be
necessary because MIDI files require ticks to be integers.

If you have a lot of rounding errors you should increase the time resolution
with more ticks per quarter note, by setting MidiFile.ticks_per_beat to a
large number. Typical values range from 96 to 480 but some use even more ticks
per quarter note.
.. note::

If you have a lot of rounding errors you should increase the time
resolution with more ticks per quarter note, by setting
``MidiFile.ticks_per_beat`` to a large number. Typical values range from
``96`` to ``480`` but some use even more ticks per quarter note.
4 changes: 3 additions & 1 deletion mido/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@
MIN_SONGPOS, MAX_SONGPOS)
from .midifiles import (MidiFile, MidiTrack, merge_tracks,
MetaMessage, UnknownMetaMessage,
bpm2tempo, tempo2bpm, tick2second, second2tick,
bpm2tempo, tempo2bpm,
tick2second, second2tick, # Deprecated
ticks2seconds, seconds2ticks,
KeySignatureError)
from .parser import Parser, parse, parse_all
from .syx import read_syx_file, write_syx_file
Expand Down
4 changes: 3 additions & 1 deletion mido/midifiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# SPDX-License-Identifier: MIT

from .meta import MetaMessage, UnknownMetaMessage, KeySignatureError
from .units import tick2second, second2tick, bpm2tempo, tempo2bpm
from .units import (ticks2seconds, seconds2ticks,
tick2second, second2tick, # Deprecated
bpm2tempo, tempo2bpm)
from .tracks import MidiTrack, merge_tracks
from .midifiles import MidiFile
7 changes: 3 additions & 4 deletions mido/midifiles/midifiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from .meta import (MetaMessage, build_meta_message, meta_charset,
encode_variable_int)
from .tracks import MidiTrack, merge_tracks, fix_end_of_track
from .units import tick2second
from .units import ticks2seconds
from ..messages import Message, SPEC_BY_STATUS

# The default tempo is 120 BPM.
Expand Down Expand Up @@ -387,10 +387,9 @@ def __iter__(self):

tempo = DEFAULT_TEMPO
for msg in self.merged_track:
# Convert message time from absolute time
# in ticks to relative time in seconds.
# Convert message time from MIDI ticks to seconds.
if msg.time > 0:
delta = tick2second(msg.time, self.ticks_per_beat, tempo)
delta = ticks2seconds(msg.time, self.ticks_per_beat, tempo)
else:
delta = 0

Expand Down
39 changes: 31 additions & 8 deletions mido/midifiles/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,49 @@
#
# SPDX-License-Identifier: MIT

from fractions import Fraction


def tick2second(tick, ticks_per_beat, tempo):
"""Convert absolute time in ticks to seconds.
"""Converts time in MIDI ticks to seconds.

.. deprecated:: 2.0.0

Please use :func:`ticks2seconds` instead
"""
return ticks2seconds(tick, ticks_per_beat, tempo)


Returns absolute time in seconds for a chosen MIDI file time resolution
def ticks2seconds(ticks_time, resolution, tempo):
"""Converts time in MIDI ticks to seconds.

Returns fractional time in seconds for a chosen MIDI file time resolution
(ticks/pulses per quarter note, also called PPQN) and tempo (microseconds
per quarter note).
"""
scale = tempo * 1e-6 / ticks_per_beat
return tick * scale
scale = Fraction(tempo, resolution) * Fraction(1e-6)
return ticks_time * scale


def second2tick(second, ticks_per_beat, tempo):
"""Convert absolute time in seconds to ticks.
"""Converts time in seconds to MIDI ticks.

.. deprecated:: 2.0.0

Please use :func:`seconds2ticks` instead
"""
return seconds2ticks(second, ticks_per_beat, tempo)


def seconds2ticks(seconds_time, resolution, tempo):
"""Converts time in seconds to MIDI ticks.

Returns absolute time in ticks for a chosen MIDI file time resolution
Returns time in ticks for a chosen MIDI file time resolution
(ticks/pulses per quarter note, also called PPQN) and tempo (microseconds
per quarter note). Normal rounding applies.
"""
scale = tempo * 1e-6 / ticks_per_beat
return int(round(second / scale))
scale = Fraction(tempo, resolution) * Fraction(1e-6)
return round(seconds_time / scale, ndigits=None)


def bpm2tempo(bpm, time_signature=(4, 4)):
Expand Down
33 changes: 27 additions & 6 deletions tests/midifiles/test_units.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen <ombdalen@gmail.com>
#
# SPDX-License-Identifier: MIT
from fractions import Fraction

from mido.midifiles.units import tempo2bpm, bpm2tempo, tick2second, second2tick
from mido.midifiles.units import (tempo2bpm, bpm2tempo, tick2second,
second2tick, ticks2seconds, seconds2ticks)


def test_tempo2bpm():
Expand Down Expand Up @@ -54,17 +56,36 @@ def test_bpm2tempo():


# TODO: these tests could be improved with better test values such as
# edge cases.
def test_tick2second():
# edge cases.
def test_tick2second(): # Deprecated in favor of ticks2seconds()
# default tempo (500000 ms per quarter note)
assert tick2second(1, ticks_per_beat=100, tempo=500000) == 0.005
assert tick2second(2, ticks_per_beat=100, tempo=100000) == 0.002
assert (float(tick2second(1, ticks_per_beat=100, tempo=500000))
== 0.005)
assert (float(tick2second(2, ticks_per_beat=100, tempo=100000))
== 0.002)


def test_second2tick():
def test_ticks2seconds():
# default tempo (500000 ms per quarter note)
assert (ticks2seconds(1, resolution=100, tempo=500000)
== Fraction(2951479051793528125, 590295810358705651712))
assert (ticks2seconds(2, resolution=100, tempo=100000)
== Fraction(590295810358705625, 295147905179352825856))


def test_second2tick(): # Deprecated in favor of seconds2ticks()
# default tempo (500000 ms per quarter note)
assert second2tick(0.001, ticks_per_beat=100, tempo=500000) == 0
assert second2tick(0.004, ticks_per_beat=100, tempo=500000) == 1
assert second2tick(0.005, ticks_per_beat=100, tempo=500000) == 1
assert second2tick(0.0015, ticks_per_beat=100, tempo=100000) == 2
assert second2tick(0.0025, ticks_per_beat=100, tempo=100000) == 2


def test_seconds2ticks():
# default tempo (500000 ms per quarter note)
assert seconds2ticks(0.001, resolution=100, tempo=500000) == 0
assert seconds2ticks(0.004, resolution=100, tempo=500000) == 1
assert seconds2ticks(0.005, resolution=100, tempo=500000) == 1
assert seconds2ticks(0.0015, resolution=100, tempo=100000) == 2
assert seconds2ticks(0.0025, resolution=100, tempo=100000) == 2