From 98d9cacd9960e24c3aecdcbb0952e5b85cb0628d Mon Sep 17 00:00:00 2001 From: Alexander Rodionov Date: Sat, 27 Jan 2024 17:19:27 +0300 Subject: [PATCH] YAML based notation (#156) * add rtmidi install option * replace Makefile with Taskfile * switch to YAML-based notation * add lint stage to ci * support merge_voices * fix notation tests * store voice_time info in .time attribute in last_message * refactor * fix last bar wrong notes https://gitlab.tandav.me/tandav/notes/-/issues/5930 * support continuing voices from previous bar https://gitlab.tandav.me/tandav/notes/-/issues/5896 * fix pre-commit * Fix notation player cli https://gitlab.tandav.me/tandav/notes/-/issues/5931 * add RootTranspose event https://gitlab.tandav.me/tandav/notes/-/issues/5929 * gitlab parallel using needs --- .gitlab-ci.yml | 14 +- .pre-commit-config.yaml | 5 +- Makefile | 30 -- Taskfile.yml | 22 + pyproject.toml | 6 + src/musiclib/midi/notation.py | 397 +++++++++++------- src/musiclib/midi/parse.py | 29 ++ tests/midi/data/notation/0/code.txt | 10 - tests/midi/data/notation/0/code.yml | 9 + .../data/notation/0/midi-merge-voices.json | 18 + tests/midi/data/notation/0/midi.json | 39 +- tests/midi/data/notation/1/code.txt | 10 - tests/midi/data/notation/1/code.yml | 14 + .../data/notation/1/midi-merge-voices.json | 34 ++ tests/midi/data/notation/1/midi.json | 42 +- tests/midi/data/notation/2/code.txt | 9 - tests/midi/data/notation/2/code.yml | 14 + .../data/notation/2/midi-merge-voices.json | 50 +++ tests/midi/data/notation/2/midi.json | 50 ++- tests/midi/data/notation/3/code.yml | 14 + .../data/notation/3/midi-merge-voices.json | 46 ++ tests/midi/data/notation/3/midi.json | 46 ++ tests/midi/data/notation/4/code.yml | 50 +++ .../data/notation/4/midi-merge-voices.json | 52 +++ tests/midi/data/notation/4/midi.json | 49 +++ tests/midi/data/notation/5/code.yml | 12 + .../data/notation/5/midi-merge-voices.json | 36 ++ tests/midi/data/notation/5/midi.json | 37 ++ tests/midi/data/notation/6/code.yml | 14 + .../data/notation/6/midi-merge-voices.json | 42 ++ tests/midi/data/notation/6/midi.json | 42 ++ tests/midi/data/notation/7/code.yml | 11 + .../data/notation/7/midi-merge-voices.json | 36 ++ tests/midi/data/notation/7/midi.json | 34 ++ tests/midi/data/notation/8/code.yml | 25 ++ .../data/notation/8/midi-merge-voices.json | 22 + tests/midi/data/notation/8/midi.json | 21 + tests/midi/notation_test.py | 84 ++-- 38 files changed, 1134 insertions(+), 341 deletions(-) delete mode 100644 Makefile create mode 100644 Taskfile.yml delete mode 100644 tests/midi/data/notation/0/code.txt create mode 100644 tests/midi/data/notation/0/code.yml create mode 100644 tests/midi/data/notation/0/midi-merge-voices.json delete mode 100644 tests/midi/data/notation/1/code.txt create mode 100644 tests/midi/data/notation/1/code.yml create mode 100644 tests/midi/data/notation/1/midi-merge-voices.json delete mode 100644 tests/midi/data/notation/2/code.txt create mode 100644 tests/midi/data/notation/2/code.yml create mode 100644 tests/midi/data/notation/2/midi-merge-voices.json create mode 100644 tests/midi/data/notation/3/code.yml create mode 100644 tests/midi/data/notation/3/midi-merge-voices.json create mode 100644 tests/midi/data/notation/3/midi.json create mode 100644 tests/midi/data/notation/4/code.yml create mode 100644 tests/midi/data/notation/4/midi-merge-voices.json create mode 100644 tests/midi/data/notation/4/midi.json create mode 100644 tests/midi/data/notation/5/code.yml create mode 100644 tests/midi/data/notation/5/midi-merge-voices.json create mode 100644 tests/midi/data/notation/5/midi.json create mode 100644 tests/midi/data/notation/6/code.yml create mode 100644 tests/midi/data/notation/6/midi-merge-voices.json create mode 100644 tests/midi/data/notation/6/midi.json create mode 100644 tests/midi/data/notation/7/code.yml create mode 100644 tests/midi/data/notation/7/midi-merge-voices.json create mode 100644 tests/midi/data/notation/7/midi.json create mode 100644 tests/midi/data/notation/8/code.yml create mode 100644 tests/midi/data/notation/8/midi-merge-voices.json create mode 100644 tests/midi/data/notation/8/midi.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 51efa0e5..72f10690 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,5 @@ stages: + # - build - test variables: @@ -14,13 +15,13 @@ cache: test: stage: test + needs: [] tags: - u61 - docker image: python:3.12 script: - pip install .[dev] - - pre-commit run --all-files - pytest --cov musiclib --cov-report term --cov-report xml --junitxml report.xml coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: @@ -31,3 +32,14 @@ test: coverage_format: cobertura path: coverage.xml junit: report.xml + +lint: + stage: test + needs: [] + tags: + - u61 + - docker + image: python:3.12 + script: + - pip install .[dev] + - pre-commit run --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 253500d6..2d7f2d29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,7 +64,10 @@ repos: rev: v1.8.0 hooks: - id: mypy - additional_dependencies: [svg.py, numpy] + additional_dependencies: + - svg.py + - numpy + - types-PyYAML # - repo: https://github.com/RobertCraigie/pyright-python # rev: v1.1.327 diff --git a/Makefile b/Makefile deleted file mode 100644 index bc7bd7f1..00000000 --- a/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -.PHONY: test -test: - pytest - -.PHONY: coverage -coverage: - python -m pytest --cov=musiclib --cov-report=html tests - open htmlcov/index.html - -.PHONY: profile -profile: - python -m cProfile -o logs/profile.txt -m musiclib.daw video - python -m gprof2dot -f pstats logs/profile.txt | dot -Tsvg -o logs/callgraph.svg - -.PHONY: bumpver -bumpver: - # usage: make bumpver PART=minor - bumpver update --no-fetch --$(PART) - - -.PHONY: bumpver-dev-start -bumpver-dev-start: - # usage: make bumpver-dev PART=minor - # don't forget to pass PART - bumpver update --no-fetch --tag dev --no-commit --no-tag-commit --$(PART) - -.PHONY: bumpver-dev-stop -bumpver-dev-stop: - # PART is not passed here - bumpver update --no-fetch --tag final --no-commit --no-tag-commit diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 00000000..86908064 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,22 @@ +version: '3' + +tasks: + test: + desc: Run tests + cmds: + - pytest + + bumpver: + desc: 'Bump version. Pass --. Usage example: task bumpver -- --minor' + cmds: + - bumpver update --no-fetch {{.CLI_ARGS}} + + bumpver-dev-start: + desc: 'Bump version to start new dev cycle. Pass --. Usage example: task bumpver-dev-start -- --minor' + cmds: + - bumpver update --no-fetch --tag dev --no-commit --no-tag-commit {{.CLI_ARGS}} + + bumpver-dev-stop: + desc: 'Bump version to stop dev cycle. -- is not passed. Usage example: task bumpver-dev-stop' + cmds: + - bumpver update --no-fetch --tag final --no-commit --no-tag-commit diff --git a/pyproject.toml b/pyproject.toml index 23741575..eb069cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,8 @@ dependencies = [ "mido>=1.3.0", "svg.py", "numpy", + "PyYAML", + "pydantic>=2.5", ] [project.optional-dependencies] @@ -25,6 +27,10 @@ dev = [ "pytest-cov", ] +rtmidi = [ + "python-rtmidi", +] + [project.urls] source = "https://github.com/tandav/musiclib" # docs = "https://tandav.github.io/musiclib" diff --git a/src/musiclib/midi/notation.py b/src/musiclib/midi/notation.py index 019f8221..cf50e24b 100644 --- a/src/musiclib/midi/notation.py +++ b/src/musiclib/midi/notation.py @@ -1,190 +1,263 @@ +from __future__ import annotations + import argparse -import bisect import collections -import json -import operator import pathlib -from typing import NamedTuple +from typing import Literal +from typing import TypeAlias import mido -from mido.midifiles.tracks import _to_reltime +import yaml +from pydantic import BaseModel +from pydantic import ConfigDict +from pydantic import field_validator import musiclib -from musiclib.midi.parse import Midi -from musiclib.midi.parse import MidiNote -from musiclib.midi.parse import abs_messages +from musiclib.midi.parse import merge_tracks from musiclib.midi.player import Player from musiclib.note import SpecificNote -class IntervalEvent(NamedTuple): - interval: int - on: int - off: int - - -class Event: - def __init__(self, code: str) -> None: - self.type, *_kw = code.splitlines() - self.kw = dict(kv.split(maxsplit=1) for kv in _kw) - - -class Header(Event): - def __init__(self, code: str) -> None: - super().__init__(code) - self.musiclib_version = self.kw['musiclib_version'] - if musiclib.__version__ != self.musiclib_version: - raise ValueError(f'musiclib must be exact version {self.musiclib_version} to parse notation') - self.root = SpecificNote.from_str(self.kw['root']) - self.ticks_per_beat = int(self.kw['ticks_per_beat']) - self.midi_channels = json.loads(self.kw['midi_channels']) - - -class RootChange(Event): - def __init__(self, code: str) -> None: - super().__init__(code) - self.root = SpecificNote.from_str(self.kw['root']) - - -class Voice: - def __init__(self, code: str) -> None: - self.channel, intervals_str = code.split(maxsplit=1) - self.interval_events = self.parse_interval_events(intervals_str) - - def parse_interval_events(self, intervals_str: str, ticks_per_beat: int = 96) -> list[IntervalEvent]: - interval: int | None = None - on = 0 - off = 0 - interval_events = [] - for interval_str in intervals_str.split(): - if interval_str == '..': - if interval is None: - on += ticks_per_beat - else: - off += ticks_per_beat - elif interval_str == '--': - if interval is None: - raise ValueError('Cannot have -- in the beginning of a voice') - off += ticks_per_beat - else: - if interval is not None: - interval_events.append(IntervalEvent(interval, on, off)) - on = off - off += ticks_per_beat - interval = int(interval_str, base=12) - if interval is None: - raise ValueError('Cannot have empty voice') - interval_events.append(IntervalEvent(interval, on, off)) - return interval_events +class ArbitraryTypes(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class EventData(BaseModel): + type: Literal['Bar', 'RootNote', 'RootTranspose'] + model_config = ConfigDict(extra='allow') + + +class RootNoteData(EventData): + root: str + + +class RootTransposeData(EventData): + semitones: int + + +class BarData(EventData): + voices: dict[str, list[str]] class Bar: - def __init__(self, code: str) -> None: - self.voices = [Voice(voice_code) for voice_code in code.splitlines()] - - def to_midi(self, root: SpecificNote, *, figured_bass: bool = True) -> dict[str, list[MidiNote]]: - if not isinstance(root, SpecificNote): - raise TypeError(f'root must be SpecificNote, got {root}') - channels = collections.defaultdict(list) - if not figured_bass: - for voice in self.voices: - for interval_event in voice.interval_events: - channels[voice.channel].append( - MidiNote( - note=root + interval_event.interval, - on=interval_event.on, - off=interval_event.off, - ), - ) - return dict(channels) - *voices, bass = self.voices - for bass_interval_event in bass.interval_events: - bass_note = root + bass_interval_event.interval - bass_on = bass_interval_event.on - bass_off = bass_interval_event.off - channels[bass.channel].append(MidiNote(on=bass_on, off=bass_off, note=bass_note)) - - for voice in voices: - above_bass_events = voice.interval_events[ - bisect.bisect_left(voice.interval_events, bass_interval_event.on, key=operator.attrgetter('on')): - bisect.bisect_left(voice.interval_events, bass_interval_event.off, key=operator.attrgetter('on')) - ] - - for interval_event in above_bass_events: - channels[voice.channel].append( - MidiNote( - note=bass_note + interval_event.interval, - on=interval_event.on, - off=interval_event.off, - ), - ) - return dict(channels) + def __init__(self, channel_voices: dict[str, list[list[int | str]]], n_beats: int) -> None: + self.channel_voices = channel_voices + self.n_beats = n_beats + self.channels_sorted = ['bass', *sorted(self.channel_voices.keys() - {'bass'})] # sorting is necessary because bass must be first + + @classmethod + def from_yaml_data(cls, data: BarData) -> Bar: + channel_voices: dict[str, list[list[int | str]]] = {} + voices_n_beats = set() + for channel, voices_str in data.voices.items(): + if channel == 'bass' and len(voices_str) != 1: + raise ValueError('bass must have only 1 voice') + channel_voices[channel] = [] + for voice_str in voices_str: + beats: list[int | str] = [] + for beats_str in voice_str.split(): + if beats_str in {'..', '--'}: + beats.append(beats_str) + else: + beats.append(int(beats_str, base=12)) + voices_n_beats.add(len(beats)) + channel_voices[channel].append(beats) + 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) + + def to_midi( # noqa: C901,PLR0912 pylint: disable=too-many-branches + self, + *, + root_note: int, + bass_note: int | None = None, + voice_last_message: collections.defaultdict[tuple[str, int], mido.Message] | None = None, + last_bar: bool = True, + ticks_per_beat: int = 96, + midi_channels: dict[str, int] | None = None, + ) -> dict[tuple[str, int], list[mido.Message]]: + messages = collections.defaultdict(list) + + if voice_last_message is None: + voice_last_message = collections.defaultdict(lambda: mido.Message(type='note_off')) + + midi_channels = midi_channels if midi_channels is not None else {} + + for beat_i in range(self.n_beats): + for channel in self.channels_sorted: + voices = self.channel_voices[channel] + midi_channel = midi_channels.get(channel, 0) + for voice_i, voice in enumerate(voices): + last_message = voice_last_message[channel, voice_i] + beat_note = voice[beat_i] + + if beat_note == '--' and last_message.type == 'note_off': + raise ValueError('Cannot have -- in the beginning of a voice or after note_off event') + + if beat_note == '..' and last_message.type == 'note_on': + messages[channel, voice_i].append(mido.Message(**last_message.dict() | {'type': 'note_off'})) + voice_last_message[channel, voice_i] = messages[channel, voice_i][-1].copy(time=0) + if channel == 'bass': + bass_note = None + if isinstance(beat_note, int): + if channel == 'bass': + bass_note = root_note + beat_note + note = bass_note + elif bass_note is None: + raise ValueError('bass_note must be set before other channels') + else: + note = bass_note + beat_note + + if last_message.type == 'note_off': + messages[channel, voice_i].append(mido.Message(type='note_on', note=note, channel=midi_channel, velocity=100, time=last_message.time)) + elif last_message.type == 'note_on': + # messages[channel, voice_i].append(mido.Message(type='note_off', note=last_message.note, velocity=last_message.velocity, time=last_message.time)) + 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 + 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})) + return messages + + +class RootNote(ArbitraryTypes): + root: SpecificNote + + @classmethod + def from_yaml_data(cls, data: RootNoteData) -> RootNote: + return cls(root=SpecificNote.from_str(data.root)) + + +class RootTranspose(ArbitraryTypes): + semitones: int + + @classmethod + def from_yaml_data(cls, data: RootTransposeData) -> RootTranspose: + return cls(semitones=data.semitones) + + +class NotationData(BaseModel): + model_config = ConfigDict(extra='forbid') + musiclib_version: str + events: list[EventData] + + @field_validator('events') + @classmethod + def last_event_must_be_bar(cls, v: list[EventData]) -> list[EventData]: + if v[-1].type != 'Bar': + raise ValueError('last event must be Bar') + return v + + +Event: TypeAlias = Bar | RootNote | RootTranspose class Notation: - def __init__(self, code: str) -> None: - self.parse(code) - self.ticks_per_beat = self.header.ticks_per_beat - self.midi_channels = self.header.midi_channels - - def parse(self, code: str) -> None: - self.events: list[Event | Bar] = [] - for event_code in code.strip().split('\n\n'): - if event_code.startswith('header'): - self.header = Header(event_code) - elif event_code.startswith('root_change'): - self.events.append(RootChange(event_code)) - else: - self.events.append(Bar(event_code)) - - def _to_midi(self) -> list[Midi]: - channels: list[list[MidiNote]] = [[] for _ in range(len(self.header.midi_channels))] - root = self.header.root - t = 0 - for event in self.events: - if isinstance(event, RootChange): - root = event.root + def __init__( + self, + musiclib_version: str, + events: list[Event], + ) -> None: + if musiclib.__version__ != musiclib_version: + raise ValueError(f'musiclib must be exact version {musiclib_version} to parse notation') + self.musiclib_version = musiclib_version + self.events = events + + @classmethod + def from_yaml_data(cls, yaml_data: str) -> Notation: + notation_data = NotationData.model_validate(yaml_data) + return cls(**notation_data.model_dump(exclude=['events']), events=cls.parse_events(notation_data.events)) + + @staticmethod + def parse_events(events: list[EventData]) -> list[Event]: + out = [] + for event in events: + cls_data = { + 'RootNote': RootNoteData, + 'RootTranspose': RootTransposeData, + 'Bar': BarData, + }[event.type] + event_data_model = cls_data.model_validate(event, from_attributes=True) + cls = { + 'RootNote': RootNote, + 'RootTranspose': RootTranspose, + 'Bar': Bar, + }[event_data_model.type] + out.append(cls.from_yaml_data(event_data_model)) # type: ignore[attr-defined] + return out + + def to_midi( + self, + *, + ticks_per_beat: int = 96, + merge_voices: bool = True, + midi_channels: dict[str, int] | None = None, + ) -> mido.MidiFile: + root_note = None + bass_note = None + + 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) + + for event_i, event in enumerate(self.events): + if isinstance(event, RootNote): + root_note = event.root.i + elif isinstance(event, RootTranspose): + if root_note is None: + raise ValueError('Root note was not set before this RootTranspose event') + root_note += event.semitones elif isinstance(event, Bar): - bar_midi = event.to_midi(root) - bar_off_channels = {channel: notes[-1].off for channel, notes in bar_midi.items()} - if len(set(bar_off_channels.values())) != 1: - raise ValueError(f'all channels in the bar must have equal length, got {bar_off_channels}') - bar_off = next(iter(bar_off_channels.values())) - for channel, notes in bar_midi.items(): - channel_id = self.header.midi_channels[channel] - channels[channel_id] += [ - MidiNote( - note=note.note, - on=t + note.on, - off=t + note.off, - channel=channel_id, - ) - for note in notes - ] - t += bar_off - else: - raise TypeError(f'unknown event type: {event}') - - return [Midi(notes=v, ticks_per_beat=self.ticks_per_beat) for v in channels] - - def to_midi(self) -> mido.MidiFile: - return mido.MidiFile( - type=1, - ticks_per_beat=self.ticks_per_beat, - tracks=[mido.MidiTrack(_to_reltime(abs_messages(midi))) for midi in self._to_midi()], - ) + if root_note is None: + raise ValueError('Root note was not set before this Bar event') + bar_messages = event.to_midi( + root_note=root_note, + bass_note=bass_note, + voice_last_message=voice_last_message, + last_bar=event_i == len(self.events) - 1, + ticks_per_beat=ticks_per_beat, + midi_channels=midi_channels, + ) + for (channel, voice_i), voice_messages in bar_messages.items(): + messages[channel, voice_i] += voice_messages + if merge_voices: + channel_tracks = collections.defaultdict(list) + for (channel, voice_i), voice_messages in messages.items(): # noqa: B007 + channel_tracks[channel].append(mido.MidiTrack(voice_messages)) + tracks_merged = [ + [mido.MetaMessage(type='track_name', name=channel), *merge_tracks(tracks, key=lambda msg: (msg.time, {'note_off': 0, 'note_on': 1}.get(msg.type, 3)))] + for channel, tracks in channel_tracks.items() + ] + return mido.MidiFile(tracks=tracks_merged, type=1, ticks_per_beat=ticks_per_beat) + tracks = [ + mido.MidiTrack([mido.MetaMessage(type='track_name', name=f'{channel}-{voice_i}'), *voice_messages]) + for (channel, voice_i), voice_messages in messages.items() + ] + return mido.MidiFile(tracks=tracks, type=1, ticks_per_beat=ticks_per_beat) def play_file() -> None: + class StoreDictKeyPair(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None) -> None: # type: ignore[no-untyped-def] # noqa: ANN001,ARG002 + my_dict = {} + for kv in values.split(','): + k, v = kv.split('=') + my_dict[k] = int(v) + setattr(namespace, self.dest, my_dict) parser = argparse.ArgumentParser() parser.add_argument('--bpm', type=float, default=120) - parser.add_argument('--midiport', type=str, default='IAC Driver Bus 1') + parser.add_argument('--midi-port', type=str, default='IAC Driver Bus 1', help='MIDI port to send midi messages') + parser.add_argument('--midi-channels', action=StoreDictKeyPair, help='mapping of instruments to MIDI channels in instrument0:channel0,instrument1:channel1 format') parser.add_argument('filepath', type=pathlib.Path) args = parser.parse_args() - code = args.filepath.read_text() - nt = Notation(code) - midi = nt.to_midi() - player = Player(args.midiport) + yaml_data = yaml.safe_load(args.filepath.read_text()) + nt = Notation.from_yaml_data(yaml_data) + midi = nt.to_midi(midi_channels=args.midi_channels) + player = Player(args.midi_port) player.play_midi(midi, beats_per_minute=args.bpm) diff --git a/src/musiclib/midi/parse.py b/src/musiclib/midi/parse.py index 67e9ac19..f600cc7d 100644 --- a/src/musiclib/midi/parse.py +++ b/src/musiclib/midi/parse.py @@ -5,7 +5,10 @@ from typing import Literal import mido +from mido.midifiles.tracks import MidiTrack +from mido.midifiles.tracks import _to_abstime from mido.midifiles.tracks import _to_reltime +from mido.midifiles.tracks import fix_end_of_track from musiclib.note import SpecificNote from musiclib.noteset import SpecificNoteSet @@ -228,3 +231,29 @@ def from_dict(midi: dict) -> mido.MidiFile: # type: ignore[type-arg] for track in midi['tracks'] ], ) + + +def merge_tracks(tracks, skip_checks=False, key=lambda msg: msg.time): # type: ignore[no-untyped-def] # noqa: ANN001,ANN201,FBT002 + """Returns a MidiTrack object with all messages from all tracks. + + The messages are returned in playback order with delta times + as if they were all in one track. + + Pass skip_checks=True to skip validation of messages before merging. + This should ONLY be used when the messages in tracks have already + been validated by mido.checks. + + # TODO: make MR to mido + """ + messages = [] + for track in tracks: + messages.extend(_to_abstime(track, skip_checks=skip_checks)) + + messages.sort(key=key) + + return MidiTrack( + fix_end_of_track( + _to_reltime(messages, skip_checks=skip_checks), + skip_checks=skip_checks, + ), + ) diff --git a/tests/midi/data/notation/0/code.txt b/tests/midi/data/notation/0/code.txt deleted file mode 100644 index 4a24ab6d..00000000 --- a/tests/midi/data/notation/0/code.txt +++ /dev/null @@ -1,10 +0,0 @@ -header -musiclib_version 2.3.0dev0 -root C3 -ticks_per_beat 96 -midi_channels {"bass": 0, "piano": 1, "flute": 2} - -flute 17 14 10 17 -flute 14 10 07 14 -piano 10 07 04 10 -bass 00 05 07 00 diff --git a/tests/midi/data/notation/0/code.yml b/tests/midi/data/notation/0/code.yml new file mode 100644 index 00000000..72350518 --- /dev/null +++ b/tests/midi/data/notation/0/code.yml @@ -0,0 +1,9 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: E3 + + - type: Bar + voices: + bass: + - 0 1 2 3 diff --git a/tests/midi/data/notation/0/midi-merge-voices.json b/tests/midi/data/notation/0/midi-merge-voices.json new file mode 100644 index 00000000..97b80b05 --- /dev/null +++ b/tests/midi/data/notation/0/midi-merge-voices.json @@ -0,0 +1,18 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/0/midi.json b/tests/midi/data/notation/0/midi.json index ae3148c6..8b782c14 100644 --- a/tests/midi/data/notation/0/midi.json +++ b/tests/midi/data/notation/0/midi.json @@ -3,42 +3,15 @@ "ticks_per_beat": 96, "tracks": [ [ - {"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": "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": 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": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 1, "note": 59, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 59, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 60, "velocity": 100, "is_meta": false} - ], - [ - {"type": "note_on", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 69, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 65, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 69, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 65, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 62, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 62, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 64, "velocity": 100, "is_meta": false} + {"type": "note_off", "time": 96, "channel": 0, "note": 55, "velocity": 100, "is_meta": false} ] ] } diff --git a/tests/midi/data/notation/1/code.txt b/tests/midi/data/notation/1/code.txt deleted file mode 100644 index 2975a7b0..00000000 --- a/tests/midi/data/notation/1/code.txt +++ /dev/null @@ -1,10 +0,0 @@ -header -musiclib_version 2.3.0dev0 -root C3 -ticks_per_beat 96 -midi_channels {"bass": 0, "piano": 1, "flute": 2} - -flute 17 14 10 20 -flute 14 10 07 17 -piano 10 -- -- 14 -bass 00 05 07 00 diff --git a/tests/midi/data/notation/1/code.yml b/tests/midi/data/notation/1/code.yml new file mode 100644 index 00000000..240fa8ac --- /dev/null +++ b/tests/midi/data/notation/1/code.yml @@ -0,0 +1,14 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + flute: + - 10 14 + - 3 4 + piano: + - 1 2 + bass: + - 0 0 diff --git a/tests/midi/data/notation/1/midi-merge-voices.json b/tests/midi/data/notation/1/midi-merge-voices.json new file mode 100644 index 00000000..6173afc8 --- /dev/null +++ b/tests/midi/data/notation/1/midi-merge-voices.json @@ -0,0 +1,34 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": "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": "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": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/1/midi.json b/tests/midi/data/notation/1/midi.json index 51b5f14a..c9627243 100644 --- a/tests/midi/data/notation/1/midi.json +++ b/tests/midi/data/notation/1/midi.json @@ -3,38 +3,32 @@ "ticks_per_beat": 96, "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": 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": 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": 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": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 288, "channel": 1, "note": 60, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 1, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 64, "velocity": 100, "is_meta": false} + {"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": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 69, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 65, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 69, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 65, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 62, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 62, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 72, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 72, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false} + {"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": "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} ] ] } diff --git a/tests/midi/data/notation/2/code.txt b/tests/midi/data/notation/2/code.txt deleted file mode 100644 index 183d6ad4..00000000 --- a/tests/midi/data/notation/2/code.txt +++ /dev/null @@ -1,9 +0,0 @@ -header -musiclib_version 2.3.0dev0 -root E3 -ticks_per_beat 96 -midi_channels {"bass": 0, "piano": 1, "flute": 2} - -flute 17 13 -piano 13 10 -bass 0 0 diff --git a/tests/midi/data/notation/2/code.yml b/tests/midi/data/notation/2/code.yml new file mode 100644 index 00000000..9aa355a2 --- /dev/null +++ b/tests/midi/data/notation/2/code.yml @@ -0,0 +1,14 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + flute: + - 17 14 10 17 + - 14 10 07 14 + piano: + - 10 07 04 10 + bass: + - 00 05 07 00 diff --git a/tests/midi/data/notation/2/midi-merge-voices.json b/tests/midi/data/notation/2/midi-merge-voices.json new file mode 100644 index 00000000..dfda8243 --- /dev/null +++ b/tests/midi/data/notation/2/midi-merge-voices.json @@ -0,0 +1,50 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 48, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false}, + {"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": 67, "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": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "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": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"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": 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": 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": 59, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 59, "velocity": 100, "is_meta": false}, + {"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": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/2/midi.json b/tests/midi/data/notation/2/midi.json index 832bc79c..83903bc9 100644 --- a/tests/midi/data/notation/2/midi.json +++ b/tests/midi/data/notation/2/midi.json @@ -3,22 +3,48 @@ "ticks_per_beat": 96, "tracks": [ [ - {"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": 52, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100, "is_meta": false} + {"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": 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": 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": 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": 1, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 1, "note": 64, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 1, "note": 64, "velocity": 100, "is_meta": false} + {"type": "track_name", "name": "flute-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false} ], [ - {"type": "note_on", "time": 0, "channel": 2, "note": 71, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 71, "velocity": 100, "is_meta": false}, - {"type": "note_on", "time": 0, "channel": 2, "note": 67, "velocity": 100, "is_meta": false}, - {"type": "note_off", "time": 96, "channel": 2, "note": 67, "velocity": 100, "is_meta": false} + {"type": "track_name", "name": "flute-1", "time": 0, "is_meta": true}, + {"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": 65, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 62, "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": "track_name", "name": "piano-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": 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": 59, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 59, "velocity": 100, "is_meta": false}, + {"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} ] ] } diff --git a/tests/midi/data/notation/3/code.yml b/tests/midi/data/notation/3/code.yml new file mode 100644 index 00000000..f7235b48 --- /dev/null +++ b/tests/midi/data/notation/3/code.yml @@ -0,0 +1,14 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + flute: + - 17 14 10 20 + - 14 10 07 17 + piano: + - 10 -- -- 14 + bass: + - 00 05 07 00 diff --git a/tests/midi/data/notation/3/midi-merge-voices.json b/tests/midi/data/notation/3/midi-merge-voices.json new file mode 100644 index 00000000..5f8ab7a4 --- /dev/null +++ b/tests/midi/data/notation/3/midi-merge-voices.json @@ -0,0 +1,46 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 48, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false}, + {"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": 67, "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": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"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": 60, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 288, "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": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/3/midi.json b/tests/midi/data/notation/3/midi.json new file mode 100644 index 00000000..0cb818a1 --- /dev/null +++ b/tests/midi/data/notation/3/midi.json @@ -0,0 +1,46 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 48, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "flute-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 72, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "flute-1", "time": 0, "is_meta": true}, + {"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": 65, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "piano-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 0, "channel": 0, "note": 60, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 288, "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} + ] + ] +} diff --git a/tests/midi/data/notation/4/code.yml b/tests/midi/data/notation/4/code.yml new file mode 100644 index 00000000..98c4ca89 --- /dev/null +++ b/tests/midi/data/notation/4/code.yml @@ -0,0 +1,50 @@ +musiclib_version: __tests_placeholder__ + +events: + - type: RootNote + root: E3 + + - type: Bar + voices: + flute: + - 17 13 + piano: + - 13 10 + bass: + - 0 0 + + - type: Bar + voices: + flute: + - -- -- + piano: + - -- -- + bass: + - 0 0 + + - type: Bar + voices: + flute: + - 1 -- + piano: + - 2 -- + bass: + - 0 -- + + - type: Bar + voices: + flute: + - -- -- + piano: + - -- -- + bass: + - -- -- + + - type: Bar + voices: + flute: + - 17 13 + piano: + - 13 10 + bass: + - 0 0 diff --git a/tests/midi/data/notation/4/midi-merge-voices.json b/tests/midi/data/notation/4/midi-merge-voices.json new file mode 100644 index 00000000..18c0bbc7 --- /dev/null +++ b/tests/midi/data/notation/4/midi-merge-voices.json @@ -0,0 +1,52 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 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": 52, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 384, "channel": 0, "note": 52, "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": 52, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100, "is_meta": false}, + {"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": 71, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 288, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 53, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 384, "channel": 0, "note": 53, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"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": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 288, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 54, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 384, "channel": 0, "note": 54, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "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": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/4/midi.json b/tests/midi/data/notation/4/midi.json new file mode 100644 index 00000000..c11ee6d3 --- /dev/null +++ b/tests/midi/data/notation/4/midi.json @@ -0,0 +1,49 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 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": 52, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 384, "channel": 0, "note": 52, "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": 52, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 52, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "flute-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 0, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 288, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 53, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 384, "channel": 0, "note": 53, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 71, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "piano-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 288, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 54, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 384, "channel": 0, "note": 54, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "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} + ] + ] +} diff --git a/tests/midi/data/notation/5/code.yml b/tests/midi/data/notation/5/code.yml new file mode 100644 index 00000000..6bfac20b --- /dev/null +++ b/tests/midi/data/notation/5/code.yml @@ -0,0 +1,12 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + flute: + - .. 14 17 20 + - 14 10 12 17 + bass: + - 00 00 00 00 diff --git a/tests/midi/data/notation/5/midi-merge-voices.json b/tests/midi/data/notation/5/midi-merge-voices.json new file mode 100644 index 00000000..851bcfdb --- /dev/null +++ b/tests/midi/data/notation/5/midi-merge-voices.json @@ -0,0 +1,36 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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, "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": "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": 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, "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": 60, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/5/midi.json b/tests/midi/data/notation/5/midi.json new file mode 100644 index 00000000..c35d9a18 --- /dev/null +++ b/tests/midi/data/notation/5/midi.json @@ -0,0 +1,37 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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, "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": "track_name", "name": "flute-1", "time": 0, "is_meta": true}, + {"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, "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": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "flute-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 96, "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": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 72, "velocity": 100, "is_meta": false} + ] + ] +} diff --git a/tests/midi/data/notation/6/code.yml b/tests/midi/data/notation/6/code.yml new file mode 100644 index 00000000..7bc6238d --- /dev/null +++ b/tests/midi/data/notation/6/code.yml @@ -0,0 +1,14 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + flute: + - .. 14 10 20 + - 14 10 07 17 + piano: + - .. .. .. 14 + bass: + - 00 05 07 00 diff --git a/tests/midi/data/notation/6/midi-merge-voices.json b/tests/midi/data/notation/6/midi-merge-voices.json new file mode 100644 index 00000000..1f140cd2 --- /dev/null +++ b/tests/midi/data/notation/6/midi-merge-voices.json @@ -0,0 +1,42 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 48, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false}, + {"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": 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": 65, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "end_of_track", "time": 0, "is_meta": true} + ], + [ + {"type": "track_name", "name": "piano", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 288, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/6/midi.json b/tests/midi/data/notation/6/midi.json new file mode 100644 index 00000000..155aa4fe --- /dev/null +++ b/tests/midi/data/notation/6/midi.json @@ -0,0 +1,42 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 48, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 48, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "flute-1", "time": 0, "is_meta": true}, + {"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": 65, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 65, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 62, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "flute-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 96, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 69, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 67, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 72, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 72, "velocity": 100, "is_meta": false} + ], + [ + {"type": "track_name", "name": "piano-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 288, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100, "is_meta": false} + ] + ] +} diff --git a/tests/midi/data/notation/7/code.yml b/tests/midi/data/notation/7/code.yml new file mode 100644 index 00000000..3ece79f2 --- /dev/null +++ b/tests/midi/data/notation/7/code.yml @@ -0,0 +1,11 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + flute: + - .. 14 .. 13 -- .. .. 14 + bass: + - 00 00 00 00 00 00 00 00 diff --git a/tests/midi/data/notation/7/midi-merge-voices.json b/tests/midi/data/notation/7/midi-merge-voices.json new file mode 100644 index 00000000..522400bc --- /dev/null +++ b/tests/midi/data/notation/7/midi-merge-voices.json @@ -0,0 +1,36 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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, "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, "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, "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": "end_of_track", "time": 0, "is_meta": true} + ], + [ + {"type": "track_name", "name": "flute", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 96, "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": 96, "channel": 0, "note": 63, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 192, "channel": 0, "note": 63, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 192, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/7/midi.json b/tests/midi/data/notation/7/midi.json new file mode 100644 index 00000000..a06fc1c2 --- /dev/null +++ b/tests/midi/data/notation/7/midi.json @@ -0,0 +1,34 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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, "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, "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, "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": "track_name", "name": "flute-0", "time": 0, "is_meta": true}, + {"type": "note_on", "time": 96, "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": 96, "channel": 0, "note": 63, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 192, "channel": 0, "note": 63, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 192, "channel": 0, "note": 64, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 64, "velocity": 100, "is_meta": false} + ] + ] +} diff --git a/tests/midi/data/notation/8/code.yml b/tests/midi/data/notation/8/code.yml new file mode 100644 index 00000000..721008f6 --- /dev/null +++ b/tests/midi/data/notation/8/code.yml @@ -0,0 +1,25 @@ +musiclib_version: __tests_placeholder__ +events: + - type: RootNote + root: C3 + + - type: Bar + voices: + bass: + - 00 00 + + - type: RootTranspose + semitones: 1 + + - type: Bar + voices: + bass: + - 00 00 + + - type: RootTranspose + semitones: -3 + + - type: Bar + voices: + bass: + - 00 00 diff --git a/tests/midi/data/notation/8/midi-merge-voices.json b/tests/midi/data/notation/8/midi-merge-voices.json new file mode 100644 index 00000000..ec7955c7 --- /dev/null +++ b/tests/midi/data/notation/8/midi-merge-voices.json @@ -0,0 +1,22 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 46, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 46, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 46, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 46, "velocity": 100, "is_meta": false}, + {"type": "end_of_track", "time": 0, "is_meta": true} + ] + ] +} diff --git a/tests/midi/data/notation/8/midi.json b/tests/midi/data/notation/8/midi.json new file mode 100644 index 00000000..03cd58d2 --- /dev/null +++ b/tests/midi/data/notation/8/midi.json @@ -0,0 +1,21 @@ +{ + "type": 1, + "ticks_per_beat": 96, + "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": 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": 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": 46, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 46, "velocity": 100, "is_meta": false}, + {"type": "note_on", "time": 0, "channel": 0, "note": 46, "velocity": 100, "is_meta": false}, + {"type": "note_off", "time": 96, "channel": 0, "note": 46, "velocity": 100, "is_meta": false} + ] + ] +} diff --git a/tests/midi/notation_test.py b/tests/midi/notation_test.py index 5c3e33f4..3409eaf2 100644 --- a/tests/midi/notation_test.py +++ b/tests/midi/notation_test.py @@ -1,61 +1,47 @@ import json from pathlib import Path +import musiclib import pytest +import yaml from musiclib.midi import notation from musiclib.midi import parse -from musiclib.midi.notation import IntervalEvent +notation_examples = (p for p in (Path(__file__).parent / 'data/notation').iterdir() if p.is_dir()) -@pytest.mark.parametrize( - ('code', 'channel'), [ - ('flute 1 2 3 4', 'flute'), - ('bass 9 8 -7 17 4 -12 0', 'bass'), - ], -) -def test_voice_channel(code, channel): - assert notation.Voice(code).channel == channel +@pytest.mark.parametrize('merge_voices', [False, True]) +@pytest.mark.parametrize('example_dir', notation_examples) +def test_to_midi(example_dir, merge_voices): + yaml_path = example_dir / 'code.yml' + with open(yaml_path) as f: + yaml_data = yaml.safe_load(f) + yaml_data['musiclib_version'] = musiclib.__version__ + notation_ = notation.Notation.from_yaml_data(yaml_data) + midifile = 'midi-merge-voices.json' if merge_voices else 'midi.json' + with open(example_dir / midifile) as f: + midi_dict = json.load(f) + midi = notation_.to_midi(merge_voices=merge_voices) + assert parse.to_dict(midi) == midi_dict -@pytest.mark.parametrize( - ('code', 'interval_events'), [ - ( - 'flute 1 2 3 4', - [ - IntervalEvent(interval=1, on=0, off=96), - IntervalEvent(interval=2, on=96, off=192), - IntervalEvent(interval=3, on=192, off=288), - IntervalEvent(interval=4, on=288, off=384), - ], - ), - ( - 'flute 15 -3 .. -10 26 17 28 -- 17 29 -15 27 -8 -5 25 23', - [ - IntervalEvent(interval=17, on=0, off=96), - IntervalEvent(interval=-3, on=96, off=288), - IntervalEvent(interval=-12, on=288, off=384), - IntervalEvent(interval=30, on=384, off=480), - IntervalEvent(interval=19, on=480, off=576), - IntervalEvent(interval=32, on=576, off=768), - IntervalEvent(interval=19, on=768, off=864), - IntervalEvent(interval=33, on=864, off=960), - IntervalEvent(interval=-17, on=960, off=1056), - IntervalEvent(interval=31, on=1056, off=1152), - IntervalEvent(interval=-8, on=1152, off=1248), - IntervalEvent(interval=-5, on=1248, off=1344), - IntervalEvent(interval=29, on=1344, off=1440), - IntervalEvent(interval=27, on=1440, off=1536), - ], - ), - ], -) -def test_voice(code, interval_events): - assert notation.Voice(code).interval_events == interval_events +def test_bass_is_set(): + yaml_data = yaml.safe_load(f''' + musiclib_version: {musiclib.__version__} + events: + - type: RootNote + root: C3 -@pytest.mark.parametrize('example_dir', (Path(__file__).parent / 'data/notation').iterdir()) -def test_to_midi(example_dir): - code = (example_dir / 'code.txt').read_text() - with open(example_dir / 'midi.json') as f: - midi_dict = json.load(f) - assert parse.to_dict(notation.Notation(code).to_midi()) == midi_dict + - type: Bar + voices: + flute: + - .. 14 10 20 + - 14 10 07 17 + piano: + - .. .. .. 14 + bass: + - 00 .. .. .. + ''') + notation_ = notation.Notation.from_yaml_data(yaml_data) + with pytest.raises(ValueError, match='bass_note must be set before other channels'): + notation_.to_midi()