コード例 #1
0
ファイル: track.py プロジェクト: meownoid/melodia
    def __init__(
        self,
        signature: Union[Signature, Tuple[int, int]] = Signature(4, 4),
        content: Optional[Iterable[Tuple[Note, Union[Signature, Tuple[int,
                                                                      int],
                                                     None]]]] = None,
    ):
        """
        Initializes Track object.

        :param signature: time signature of the track (default: (4, 4))
        :param content: optional content of the track as a list of pairs (note, position) (default: None)
        """
        self._signature: Signature
        if isinstance(signature, Signature):
            self._signature = signature
        elif isinstance(signature, Iterable):
            self._signature = Signature(*signature)
        else:
            raise TypeError(
                'signature must be a Signature object or a pair of integers')

        self._max_denominator = 1
        self._heap: List[Tuple[Signature, Note]] = []
        self._max_position: Signature = Signature(0, 1)

        self._sorted = True

        if content is not None:
            for note, position in content:
                self.add(note, where=position)
コード例 #2
0
ファイル: midi.py プロジェクト: meownoid/melodia
    def load(self, file: BinaryIO, channel: Optional[int] = None) -> Track:
        """
        Reads track from the MIDI file. MIDI file can contain up to 16 channels.
        If ``channel`` is specified, track from this channel is returned. If ``channel``
        is omitted, track composed of all channels is returned.

        :param file: binary file-like object
        :param channel: channel to read from (default: None)
        :return: parsed track
        """

        kind = None
        ticks_per_quarter = None
        tracks = None

        for chunk_kind, chunk_data in self._read_chunks(file):
            if chunk_kind == b'MThd':
                kind, n_tracks, division = self._parse_header(chunk_data)
                if kind != 0:
                    raise MIDIParsingError(f'Unsupported MIDI file: format is {kind}')
                if n_tracks != 1:
                    raise MIDIParsingError('Invalid MIDI file: multiple tracks in a single track file')
                if division > 0x7FFF:
                    raise MIDIParsingError('Unsupported MIDI file: unsupported division')
                ticks_per_quarter = division
            if chunk_kind == b'MTrk':
                if ticks_per_quarter is None:
                    raise MIDIParsingError('Invalid MIDI file: missing header chunk')
                tracks = self._parse_track(chunk_data, ticks_per_quarter)

        if kind is None:
            raise MIDIParsingError('Invalid MIDI file: missing header chunk')

        if tracks is None:
            raise MIDIParsingError('Invalid MIDI file: missing track chunk')

        if not tracks:
            return Track(signature=Signature(4, 4))

        if channel is None:
            max_signature = Signature(0, 1)

            for track in tracks.values():
                if track.signature > max_signature:
                    max_signature = track.signature

            common_track = Track(signature=max_signature)

            for track in tracks.values():
                for position, note in track:
                    common_track.add(note, position)

            return common_track

        if channel not in tracks:
            raise ValueError(f'No channel {channel} found')

        return tracks[channel]
コード例 #3
0
ファイル: track.py プロジェクト: meownoid/melodia
    def add(self,
            what: Union[Note, Iterable[Note]],
            where: Union[Signature, Tuple[int, int], None] = None) -> None:
        """
        Adds note or multiple notes to the track to the specified position.
        In the case of the multiple notes, all the notes will be placed in the same position.
        If position is not specified, note or notes will be placed at the end of the track
        (after the right-most ending of the note).

        :param what: one note or iterable of notes
        :param where: position where note or multiple notes will be placed (default: None)
        :return: None
        """
        position: Signature
        if where is None:
            position = self._max_position
        elif isinstance(where, Signature):
            position = where
        elif isinstance(where, Iterable):
            position = Signature(*where)
        else:
            raise TypeError(
                'where must be a Signature object or a pair of integers')

        if isinstance(what, Iterable):
            for x in what:
                self.add(what=x, where=position)
            return

        note: Note = what

        if position.denominator > self._max_denominator:
            self._max_denominator = position.denominator
            self._heap = [(s.to(self._max_denominator), n)
                          for s, n in self._heap]

        position_transformed = position.to(self._max_denominator)

        heapq.heappush(self._heap, (position_transformed, note))
        self._sorted = False

        end_position = position_transformed + note.duration
        if end_position > self._max_position:
            self._max_position = end_position
コード例 #4
0
ファイル: note.py プロジェクト: meownoid/melodia
    def __init__(self,
                 tone: Union[Tone, int, str],
                 duration: Union[Signature, Tuple[int, int]] = Signature(1, 4),
                 velocity: float = 0.75):
        """
        Initializes Note object. Tone can be a Tone object, an integer or a string.
        In the case of an integer, Tone constructor is called.
        In the case of a string, Tone.from_notation is called.
        Duration can be a Signature object or a pair of integers.
        In the case of a pair of integers Signature constructor is called.
        Velocity must be a float in the range [0.0, 1.0] where 0.0 is the minimum velocity and
        1.0 is the maximum velocity. By default velocity is 0.75 (as in many DAWs).

        :param tone: tone of the note, must be Tone object, integer or string
        :param duration: duration of the note, must be Signature or pair of integers (default: (1, 4))
        :param velocity: float number in the range [0.0, 1.0] (default: 0.75)
        """
        self._tone: Tone
        if isinstance(tone, Tone):
            self._tone = tone
        elif isinstance(tone, int):
            self._tone = Tone(tone)
        elif isinstance(tone, str):
            self._tone = Tone.from_notation(tone)
        else:
            raise TypeError(
                'tone must be a Tone object, an integer or a string')

        self._duration: Signature
        if isinstance(duration, Signature):
            self._duration = duration
        elif isinstance(duration, Iterable):
            self._duration = Signature(*duration)
        else:
            raise TypeError(
                'duration must be a Signature object or a pair of integers')

        if not 0.0 <= velocity <= 1.0:
            raise ValueError('velocity must be in range [0.0, 1.0]')

        self._velocity: float = velocity
コード例 #5
0
ファイル: midi.py プロジェクト: meownoid/melodia
    def _parse_track(
            self,
            data: _Reader,
            ticks_per_quarter: int
    ) -> Dict[int, Track]:
        previous_status = None

        def read_event(status: Optional[int] = None) -> _Event:
            nonlocal previous_status

            if status is None:
                status = MIDIReader._read_8(data)

            if status == 0xFF:
                previous_status = status
                # New meta event
                kind = MIDIReader._read_8(data)
                length = MIDIReader._read_var_len(data)
                if kind == 0x2F:
                    # End of track
                    if length != 0x00:
                        raise MIDIParsingError(f'Invalid MIDI file: end of track event has length {length} != 0')
                    return _EventEndOfTrack()
                elif kind == 0x20:
                    # Meta channel
                    if length != 0x01:
                        raise MIDIParsingError(f'Invalid MIDI file: meta channel event has length {length} != 3')
                    channel = MIDIReader._read_8(data)
                    return _EventGlobalChannel(channel=channel)
                elif kind == 0x58:
                    # Time signature
                    if length != 0x04:
                        raise MIDIParsingError(f'Invalid MIDI file: time signature event has length {length} != 3')
                    nn = MIDIReader._read_8(data)
                    dd = MIDIReader._read_8(data)
                    cc = MIDIReader._read_8(data)
                    bb = MIDIReader._read_8(data)
                    return _EventTimeSignature(nn=nn, dd=dd, cc=cc, bb=bb)
                else:
                    # Skip unsupported meta event body
                    data.read(length)
                    return _EventUnsupported()
            elif status >= 0x80:
                previous_status = status
                # New event
                kind = status >> 4
                channel = status & 0xF
                if kind == 8:
                    pitch = MIDIReader._read_8(data)
                    velocity = MIDIReader._read_8(data)
                    return _EventNoteOFF(channel=channel, pitch=pitch, velocity=velocity)
                elif kind == 9:
                    pitch = MIDIReader._read_8(data)
                    velocity = MIDIReader._read_8(data)

                    if velocity == 0:
                        return _EventNoteOFF(channel=channel, pitch=pitch, velocity=velocity)

                    return _EventNoteON(channel=channel, pitch=pitch, velocity=velocity)
                elif status == 0xF0:
                    # System exclusive
                    value = 0
                    while value != 0xF7:
                        value = data.read(1)
                    return _EventUnsupported()
                else:
                    length = _event_lengths[status]
                    if length == -1:
                        raise MIDIParsingError(f'Unknown status {status}')
                    else:
                        data.read(length)
                    return _EventUnsupported()
            else:
                # Running status
                data.shift(-1)
                return read_event(status=previous_status)

        channel_events: Dict[int, List[Tuple[int, _Event]]] = defaultdict(list)
        channel_signatures: Dict[int, Signature] = {}
        global_channel: int = -1

        last_time = 0

        while True:
            delta_time = MIDIReader._read_var_len(data)
            absolute_time = last_time + delta_time
            last_time = absolute_time
            event = read_event()

            if isinstance(event, (_EventNoteON, _EventNoteOFF)):
                channel_events[event.channel].append((absolute_time, event))
                continue

            if isinstance(event, _EventEndOfTrack):
                break

            if isinstance(event, _EventGlobalChannel):
                global_channel = event.channel
                continue

            if isinstance(event, _EventTimeSignature):
                channel_signatures[global_channel] = Signature(event.nn, 1 << event.dd)
                continue

        # In case there is only signature
        if not channel_events:
            channel_events[0].append((0, _EventUnsupported()))

        result: Dict[int, Track] = {}

        for channel, events in channel_events.items():
            if channel in channel_signatures:
                signature = channel_signatures[channel]
            else:
                signature = channel_signatures.get(-1, Signature(4, 4))

            track = Track(signature=signature)
            running_notes: Dict[int, Tuple[int, _EventNoteON]] = {}

            for absolute_time, event in events:
                if isinstance(event, _EventNoteON):
                    if event.pitch in running_notes:
                        continue

                    running_notes[event.pitch] = (absolute_time, event)
                    continue

                if isinstance(event, _EventNoteOFF):
                    if event.pitch not in running_notes:
                        continue

                    note_beginning, note_on = running_notes[event.pitch]
                    del running_notes[event.pitch]

                    tone: Tone = self._pitch_to_tone(event.pitch)
                    duration: Signature = self._time_to_signature(
                        absolute_time - note_beginning,
                        ticks_per_quarter=ticks_per_quarter
                    )
                    velocity: float = max(0.0, min(1.0, note_on.velocity / 127.0))
                    position = self._time_to_signature(
                        note_beginning,
                        ticks_per_quarter=ticks_per_quarter
                    )

                    track.add(Note(tone, duration, velocity), position)
                    continue

            result[channel] = track

        return result
コード例 #6
0
ファイル: midi.py プロジェクト: meownoid/melodia
    def _time_to_signature(time: int, ticks_per_quarter: int) -> Signature:
        assert ticks_per_quarter > 0, 'ticks_per_quarter must be positive'

        return Signature(time * 4096 // ticks_per_quarter, 4 * 4096).normalized()
コード例 #7
0
ファイル: midi.py プロジェクト: meownoid/melodia
    def _signature_to_pulses(self, signature: Signature) -> int:
        normalized = signature.normalized()

        return self._pulses_per_whole * normalized.nominator // normalized.denominator