Skip to content

Commit

Permalink
Notation Updates (#157)
Browse files Browse the repository at this point in the history
* notation: add support for marker <from here> for faster composition

https://gitlab.tandav.me/tandav/notes/-/issues/5897

* support beat_multiplier

https://gitlab.tandav.me/tandav/notes/-/issues/5936

* pin dev dependencies

* remove explicit is_meta: false from non-meta messages

* fix last_bar

* fix edge case when bar only have note continue beats for bass
  • Loading branch information
tandav authored Jan 28, 2024
1 parent 98d9cac commit 66c6ab2
Show file tree
Hide file tree
Showing 33 changed files with 748 additions and 412 deletions.
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ dependencies = [

[project.optional-dependencies]
dev = [
"bumpver",
"hypothesis",
"pre-commit",
"pytest",
"pytest-asyncio",
"pytest-cov",
"bumpver==2023.1129",
"hypothesis==6.97.1",
"pre-commit==3.6.0",
"pytest==7.4.4",
"pytest-asyncio==0.23.3",
"pytest-cov==4.1.0", # https://github.com/pytest-dev/pytest/issues/11868#issuecomment-1913516238
]

rtmidi = [
Expand Down
52 changes: 44 additions & 8 deletions src/musiclib/midi/notation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ArbitraryTypes(BaseModel):


class EventData(BaseModel):
type: Literal['Bar', 'RootNote', 'RootTranspose']
type: Literal['Bar', 'RootNote', 'RootTranspose', 'SkipStart', 'SkipStop']
model_config = ConfigDict(extra='allow')


Expand All @@ -37,12 +37,19 @@ class RootTransposeData(EventData):

class BarData(EventData):
voices: dict[str, list[str]]
beat_multiplier: float = 1


class Bar:
def __init__(self, channel_voices: dict[str, list[list[int | str]]], n_beats: int) -> None:
def __init__(
self,
channel_voices: dict[str, list[list[int | str]]],
n_beats: int,
beat_multiplier: float,
) -> None:
self.channel_voices = channel_voices
self.n_beats = n_beats
self.beat_multiplier = beat_multiplier
self.channels_sorted = ['bass', *sorted(self.channel_voices.keys() - {'bass'})] # sorting is necessary because bass must be first

@classmethod
Expand All @@ -65,7 +72,7 @@ def from_yaml_data(cls, data: BarData) -> Bar:
if len(voices_n_beats) != 1:
raise ValueError('Voices must have same number of beats')
n_beats = next(iter(voices_n_beats))
return cls(channel_voices, n_beats)
return cls(channel_voices, n_beats, data.beat_multiplier)

def to_midi( # noqa: C901,PLR0912 pylint: disable=too-many-branches
self,
Expand Down Expand Up @@ -116,12 +123,12 @@ def to_midi( # noqa: C901,PLR0912 pylint: disable=too-many-branches
messages[channel, voice_i].append(mido.Message(**last_message.dict() | {'type': 'note_off'}))
messages[channel, voice_i].append(mido.Message(type='note_on', note=note, channel=midi_channel, velocity=100, time=0))
voice_last_message[channel, voice_i] = messages[channel, voice_i][-1].copy(time=0)
voice_last_message[channel, voice_i].time += ticks_per_beat
voice_last_message[channel, voice_i].time += int(ticks_per_beat * self.beat_multiplier)
if last_bar:
for (channel, voice_i), message in voice_last_message.items():
if message.type != 'note_on':
continue
messages[channel, voice_i].append(mido.Message(**message.dict() | {'type': 'note_off', 'time': ticks_per_beat}))
messages[channel, voice_i].append(mido.Message(**message.dict() | {'type': 'note_off', 'time': message.time}))
return messages


Expand All @@ -141,6 +148,14 @@ def from_yaml_data(cls, data: RootTransposeData) -> RootTranspose:
return cls(semitones=data.semitones)


class SkipStart:
pass


class SkipStop:
pass


class NotationData(BaseModel):
model_config = ConfigDict(extra='forbid')
musiclib_version: str
Expand All @@ -154,7 +169,13 @@ def last_event_must_be_bar(cls, v: list[EventData]) -> list[EventData]:
return v


Event: TypeAlias = Bar | RootNote | RootTranspose
Event: TypeAlias = (
Bar
| RootNote
| RootTranspose
| SkipStart
| SkipStop
)


class Notation:
Expand All @@ -175,8 +196,14 @@ def from_yaml_data(cls, yaml_data: str) -> Notation:

@staticmethod
def parse_events(events: list[EventData]) -> list[Event]:
out = []
out: list[Event] = []
for event in events:
if event.type == 'SkipStart':
out.append(SkipStart())
continue
if event.type == 'SkipStop':
out.append(SkipStop())
continue
cls_data = {
'RootNote': RootNoteData,
'RootTranspose': RootTransposeData,
Expand All @@ -191,7 +218,7 @@ def parse_events(events: list[EventData]) -> list[Event]:
out.append(cls.from_yaml_data(event_data_model)) # type: ignore[attr-defined]
return out

def to_midi(
def to_midi( # noqa: C901,PLR0912 pylint: disable=too-many-branches
self,
*,
ticks_per_beat: int = 96,
Expand All @@ -200,6 +227,7 @@ def to_midi(
) -> mido.MidiFile:
root_note = None
bass_note = None
skip = False

voice_last_message: collections.defaultdict[tuple[str, int], mido.Message] = collections.defaultdict(lambda: mido.Message(type='note_off'))
messages: collections.defaultdict[tuple[str, int], list[mido.Message]] = collections.defaultdict(list)
Expand All @@ -211,6 +239,10 @@ def to_midi(
if root_note is None:
raise ValueError('Root note was not set before this RootTranspose event')
root_note += event.semitones
elif isinstance(event, SkipStart):
skip = True
elif isinstance(event, SkipStop):
skip = False
elif isinstance(event, Bar):
if root_note is None:
raise ValueError('Root note was not set before this Bar event')
Expand All @@ -222,6 +254,10 @@ def to_midi(
ticks_per_beat=ticks_per_beat,
midi_channels=midi_channels,
)
if ('bass', 0) in bar_messages:
bass_note = bar_messages['bass', 0][-1].note
if skip:
continue
for (channel, voice_i), voice_messages in bar_messages.items():
messages[channel, voice_i] += voice_messages
if merge_voices:
Expand Down
4 changes: 2 additions & 2 deletions src/musiclib/midi/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def to_dict(midi: mido.MidiFile) -> dict: # type: ignore[type-arg]
return {
'type': midi.type,
'ticks_per_beat': midi.ticks_per_beat,
'tracks': [[message.dict() | {'is_meta': message.is_meta} for message in track] for track in midi.tracks],
'tracks': [[message.dict() | ({'is_meta': True} if message.is_meta else {}) for message in track] for track in midi.tracks],
}


Expand All @@ -225,7 +225,7 @@ def from_dict(midi: dict) -> mido.MidiFile: # type: ignore[type-arg]
ticks_per_beat=midi['ticks_per_beat'],
tracks=[
mido.MidiTrack(
(mido.MetaMessage if message.pop('is_meta') else mido.Message).from_dict(message)
(mido.MetaMessage if message.pop('is_meta', False) else mido.Message).from_dict(message)
for message in track
)
for track in midi['tracks']
Expand Down
16 changes: 8 additions & 8 deletions tests/midi/data/notation/0/midi-merge-voices.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"tracks": [
[
{"type": "track_name", "name": "bass", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 53, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 53, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 54, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 54, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 55, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 55, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 53, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 53, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 54, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 54, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 55, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 55, "velocity": 100},
{"type": "end_of_track", "time": 0, "is_meta": true}
]
]
Expand Down
16 changes: 8 additions & 8 deletions tests/midi/data/notation/0/midi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
"tracks": [
[
{"type": "track_name", "name": "bass-0", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 53, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 53, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 54, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 54, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 55, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 55, "velocity": 100, "is_meta": false}
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 53, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 53, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 54, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 54, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 55, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 55, "velocity": 100}
]
]
}
32 changes: 16 additions & 16 deletions tests/midi/data/notation/1/midi-merge-voices.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@
"tracks": [
[
{"type": "track_name", "name": "bass", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100},
{"type": "end_of_track", "time": 0, "is_meta": true}
],
[
{"type": "track_name", "name": "flute", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 60, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 51, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 60, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 0, "channel": 0, "note": 51, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 0, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 60, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 51, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 60, "velocity": 100},
{"type": "note_off", "time": 0, "channel": 0, "note": 51, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 64, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100},
{"type": "note_off", "time": 0, "channel": 0, "note": 52, "velocity": 100},
{"type": "end_of_track", "time": 0, "is_meta": true}
],
[
{"type": "track_name", "name": "piano", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 49, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 49, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 50, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 50, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 49, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 49, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 50, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 50, "velocity": 100},
{"type": "end_of_track", "time": 0, "is_meta": true}
]
]
Expand Down
32 changes: 16 additions & 16 deletions tests/midi/data/notation/1/midi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@
"tracks": [
[
{"type": "track_name", "name": "bass-0", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false}
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 48, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100}
],
[
{"type": "track_name", "name": "flute-0", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 60, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 60, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}
{"type": "note_on", "time": 0, "channel": 0, "note": 60, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 60, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 64, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100}
],
[
{"type": "track_name", "name": "flute-1", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 51, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 51, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100, "is_meta": false}
{"type": "note_on", "time": 0, "channel": 0, "note": 51, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 51, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 52, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100}
],
[
{"type": "track_name", "name": "piano-0", "time": 0, "is_meta": true},
{"type": "note_on", "time": 0, "channel": 0, "note": 49, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 49, "velocity": 100, "is_meta": false},
{"type": "note_on", "time": 0, "channel": 0, "note": 50, "velocity": 100, "is_meta": false},
{"type": "note_off", "time": 96, "channel": 0, "note": 50, "velocity": 100, "is_meta": false}
{"type": "note_on", "time": 0, "channel": 0, "note": 49, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 49, "velocity": 100},
{"type": "note_on", "time": 0, "channel": 0, "note": 50, "velocity": 100},
{"type": "note_off", "time": 96, "channel": 0, "note": 50, "velocity": 100}
]
]
}
22 changes: 22 additions & 0 deletions tests/midi/data/notation/10/code.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
musiclib_version: __tests_placeholder__
events:
- type: RootNote
root: C3

- type: Bar
beat_multiplier: 1
voices:
bass:
- 00 05 07 00

- type: Bar
beat_multiplier: 2
voices:
bass:
- 00 05

- type: Bar
beat_multiplier: 0.5
voices:
bass:
- 00 05 07 00 00 05 07 00
Loading

0 comments on commit 66c6ab2

Please sign in to comment.