Source code for ndspy.soundSequence

# Copyright 2019 RoadrunnerWMC
#
# This file is part of ndspy.
#
# ndspy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ndspy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ndspy.  If not, see <https://www.gnu.org/licenses/>.
"""
Support for SSEQs and their sequence events.
"""

import enum
import struct

from . import _common


[docs]class SSEQ: """ A SSEQ sound sequence file. """ # When saving SDAT, two otherwise identical SSEQs will share data # only if their dataMergeOptimizationIDs are the same. # You can pretty safely ignore this. dataMergeOptimizationID = 0 def __init__(self, file=None, unk02=0, bankID=0, volume=127, channelPressure=64, polyphonicPressure=50, playerID=0): # ^ # Default values here are the most common ones in the NSMB SDAT. self.unk02 = unk02 self.bankID = bankID self.volume = volume self.channelPressure = channelPressure self.polyphonicPressure = polyphonicPressure self.playerID = playerID if file is not None: if not file.startswith(b'SSEQ'): raise ValueError("Wrong magic (should be b'SSEQ', instead " f'found {file[:4]})') version, totalFileLen = struct.unpack_from('<HI', file, 6) if version != 0x100: raise ValueError(f'Unsupported SSEQ version: {version}') self._events = [] dataOffs, = struct.unpack_from('<I', file, 0x18) self._eventsData = file[dataOffs:totalFileLen] self._parsed = False else: self._events = [] self._parsed = True
[docs] @classmethod def fromEvents(cls, events, unk02=0, bankID=0, volume=127, channelPressure=64, polyphonicPressure=50, playerID=0): """ Create a new SSEQ object from a list of sequence events. """ obj = cls(None, unk02, bankID, volume, channelPressure, polyphonicPressure, playerID) obj.events = events return obj
[docs] @classmethod def fromFile(cls, filePath, *args, **kwargs): """ Load an SSEQ from a filesystem file. """ with open(filePath, 'rb') as f: return cls(f.read(), *args, **kwargs)
@property def events(self): if not self.parsed: raise ValueError('You must parse the SSEQ with .parse() before you' ' can access .events!') return self._events @events.setter def events(self, value): self._events = value @property def eventsData(self): if self.parsed: raise ValueError('You cannot use .eventsData after you have parsed' ' the SSEQ!') return self._eventsData @eventsData.setter def eventsData(self, value): self._eventsData = value @property def parsed(self): return self._parsed @parsed.setter def parsed(self, value): raise RuntimeError('SSEQ.parsed is read-only!')
[docs] def parse(self): """ Attempt to process .eventsData to create .events. If successful, this switches the SSEQ from the unparsed to the parsed state. """ if self.parsed: return self._events, _ = readSequenceEvents(self._eventsData, []) self._parsed = True
[docs] def save(self): """ Generate file data representing this SSEQ, and then return that data, .unk02, .bankID, .volume, .channelPressure, .polyphonicPressure, and .playerID as a 7-tuple. This matches the parameters of the default class constructor. """ if self.parsed: seqEv, _ = saveSequenceEvents(self.events, []) else: seqEv = self._eventsData file = bytearray(0x1C + len(seqEv)) _common.NDS_STD_FILE_HEADER.pack_into(file, 0, b'SSEQ', 0xFEFF, 0x100, len(file), 0x10, 1) struct.pack_into('<4sII', file, 0x10, b'DATA', len(file) - 0x10, 0x1C) file[0x1C:] = seqEv return (file, self.unk02, self.bankID, self.volume, self.channelPressure, self.polyphonicPressure, self.playerID)
[docs] def saveToFile(self, filePath): """ Generate file data representing this SSEQ, and save it to a filesystem file. """ d = self.save()[0] with open(filePath, 'wb') as f: f.write(d)
def __str__(self): fields = (f'bankID={self.bankID} volume={self.volume}' f' channelPressure={self.channelPressure}' f' polyphonicPressure={self.polyphonicPressure}' f' playerID={self.playerID}') if not self.parsed: return f'<sseq unparsed {fields}>' linesList = [f'<sseq {fields}'] linesList.append(printSequenceEventList(self.events, {}, ' ' * 2)) linesList.append('>') return '\n'.join(linesList) def __repr__(self): if self.parsed: if len(self.events) > 8: ev = repr(self.events[:8])[:-1] + ', ...]' else: ev = repr(self.events) return (f'{type(self).__name__}.fromEvents({ev}' f', {self.unk02!r}, {self.bankID!r}' f', {self.volume!r}, {self.channelPressure!r}' f', {self.polyphonicPressure!r}' f', {self.playerID!r})') else: data = _common.shortBytesRepr(self.eventsData) return f'{type(self).__name__}({data})'
[docs]class SequenceEvent: """ An abstract base class representing any sequence event in a SSEQ or SSAR file. """ dataLength = 1 def __init__(self, type): self.type = type
[docs] def save(self, eventsToOffsets=None): """ Generate data representing this sequence event. This abstract base class implementation simply returns a single byte containing .type. Subclasses should reimplement this function to append their own data to this byte. """ return self.type.to_bytes(1, 'little')
[docs] @classmethod def fromData(cls, type, data, startOffset=0): """ Create an instance of the SequenceEvent subclass this function is called on, using a particular type value and reading data beginning at some offset. This abstract base class implementation simply raises NotImplementedError. """ raise NotImplementedError('SequenceEvent subclasses that can ' 'load themselves from data without context can implement this.')
def __str__(self): return f'<sequence event {hex(self.type)}>' def __repr__(self): return f'{type(self).__name__}({hex(self.type)})'
def _readVariableLengthInt(data, startOffset, limit=4): """ Read a variable-length integer (as SSEQ encodes them) from `data` beginning at `startOffset`, limiting the number of read bytes to `limit`. While the code below looks complicated, the method to read such an integer is simple: - Read a byte. AND it with 0x7F for the part relevant to the integer value. - If its MSB (that you just trimmed off) is set, left-shift the int value by 7, move on to the next byte and repeat. So you read 7 bits as a time for as long as the MSB continues to be set. """ offset = startOffset value = data[offset] & 0x7F; offset += 1 length = 0 while data[offset - 1] & 0x80: value <<= 7 value |= data[offset] & 0x7F; offset += 1; length += 1 if length > limit: raise ValueError('Read variable-length int past its end') return value def _lengthOfVariableLengthInt(x): """ Returns the length of a variable-length integer `x`, as encoded in SSEQ. See _readVariableLengthInt() for a description of the format. This can be implemented more concisely, but I opted for readability. """ if x < 0: raise ValueError(f'Cannot write a negative variable-length int: {x}') bits = x.bit_length() length = 0 while bits > 0: length += 1 bits -= 7 return max(1, length) def _writeVariableLengthInt(x): """ Find the bytes representing the arbitrarily-large (positive) integer `x` in the format used by SSEQ variable-length integer fields. See _readVariableLengthInt() for a description of this format. """ if x < 0: raise ValueError(f'Cannot write a negative variable-length int: {x}') ret = [] while True: value, x = x & 0x7F, x >> 7 ret.append(value) if x == 0: break ret.reverse() for i, v in enumerate(ret[:-1]): ret[i] = v | 0x80 return bytes(ret)
[docs]class NoteSequenceEvent(SequenceEvent): """ A sequence event that plays a note. This class represents sequence event types 0x00 through 0x7F; the type value actually determines the pitch. """ def __init__(self, type, velocityAndFlag, duration): super().__init__(type) self.velocity = velocityAndFlag & 0x7F self.unknownFlag = bool(velocityAndFlag & 0x80) self.duration = duration @property def name(self): return _common.noteName(self.type) @property def dataLength(self): return 2 + _lengthOfVariableLengthInt(self.duration) @property def pitch(self): return self.type @pitch.setter def pitch(self, value): self.type = value def save(self, eventsToOffsets=None): if self.type < 0: raise ValueError(f'Note pitch must be >= 0 (found:' f' {self.type})') if self.type > 127: raise ValueError(f'Note pitch must be < 128 (found:' f' {self.type})') if self.velocity > 127: raise ValueError(f'Note velocity must be < 128 (found:' f' {self.velocity})') velocityValue = self.velocity | (0x80 if self.unknownFlag else 0) return (super().save() + velocityValue.to_bytes(1, 'little') + _writeVariableLengthInt(self.duration)) @classmethod def fromData(cls, type, data, startOffset=0): velocity = data[startOffset + 1] duration = _readVariableLengthInt(data, startOffset + 2) return cls(type, velocity, duration) def __str__(self): flag = ' unknown-flag' if self.unknownFlag else '' return f'<{self.name} velocity={self.velocity} duration={self.duration}{flag}>' def __repr__(self): velocityValue = self.velocity | (0x80 if self.unknownFlag else 0) return f'{type(self).__name__}({self.type}, {velocityValue!r}, {self.duration!r})'
[docs]class RestSequenceEvent(SequenceEvent): """ A sequence event that causes SSEQ execution to pause for some amount of time before moving on. This is sequence event type 0x80. """ def __init__(self, duration): super().__init__(0x80) self.duration = duration @property def dataLength(self): return 1 + _lengthOfVariableLengthInt(self.duration) def save(self, eventsToOffsets=None): return super().save() + _writeVariableLengthInt(self.duration) @classmethod def fromData(cls, type, data, startOffset=0): duration = _readVariableLengthInt(data, startOffset + 1) return cls(duration) def __str__(self): return f'<rest {self.duration}>' def __repr__(self): return f'{type(self).__name__}({self.duration!r})'
[docs]class InstrumentSwitchSequenceEvent(SequenceEvent): """ A sequence event that causes the track it's placed in to switch to using a different instrument (possibly in a different SBNK). This is sequence event type 0x81. """ def __init__(self, bankID, instrumentID): super().__init__(0x81) self.bankID = bankID self.instrumentID = instrumentID @property def dataLength(self): return 1 + _lengthOfVariableLengthInt( self.bankID << 7 | self.instrumentID) def save(self, eventsToOffsets=None): value = self.instrumentID & 0x7F | self.bankID << 7 return super().save() + _writeVariableLengthInt(value) @classmethod def fromData(cls, type, data, startOffset=0): value = _readVariableLengthInt(data, startOffset + 1) instrumentID = value & 0x7F bankID = value >> 7 return cls(bankID, instrumentID) def __str__(self): return f'<instrument {self.bankID}/{self.instrumentID}>' def __repr__(self): return f'{type(self).__name__}({self.bankID!r}, {self.instrumentID!r})'
[docs]class BeginTrackSequenceEvent(SequenceEvent): """ A sequence event that declares the location in the sequence event data at which a particular track should begin executing. This is sequence event type 0x93. """ dataLength = 5 def __init__(self, trackNumber, firstEvent): super().__init__(0x93) self.trackNumber = trackNumber self.firstEvent = firstEvent def save(self, eventsToOffsets=None): return (super().save() + self.trackNumber.to_bytes(1, 'little') + eventsToOffsets[self.firstEvent].to_bytes(3, 'little')) def __str__(self): return f'<begin track {self.trackNumber} id={id(self.firstEvent)}>' def __repr__(self): return f'{type(self).__name__}({self.trackNumber!r}, {self.firstEvent!r} at {id(self.firstEvent)})'
[docs]class JumpSequenceEvent(SequenceEvent): """ A sequence event that causes execution of the current track to jump to some other location. This is sequence event type 0x94. """ dataLength = 4 def __init__(self, destination): super().__init__(0x94) self.destination = destination def save(self, eventsToOffsets=None): return (super().save() + eventsToOffsets[self.destination].to_bytes(3, 'little')) def __str__(self): return f'<jump id={id(self.destination)}>' def __repr__(self): return f'{type(self).__name__}({self.destination!r} at {id(self.destination)})'
[docs]class CallSequenceEvent(SequenceEvent): """ A sequence event that causes execution of the current track to jump to some other location, and pushes the current event's address to a return-address stack. This is sequence event type 0x95. """ dataLength = 4 def __init__(self, destination): super().__init__(0x95) self.destination = destination def save(self, eventsToOffsets=None): return (super().save() + eventsToOffsets[self.destination].to_bytes(3, 'little')) def __str__(self): return f'<call id={id(self.destination)}>' def __repr__(self): return f'{type(self).__name__}({self.destination!r} at {id(self.destination)})'
[docs]class RandomSequenceEvent(SequenceEvent): """ A sequence event that causes some other event to execute with a randomized argument. This is sequence event type 0xA0. """ def __init__(self, subType, args, randMin, randMax): super().__init__(0xA0) self.subType = subType self.args = args self.randMin = randMin self.randMax = randMax @property def dataLength(self): return self._dataLength_for_subtype(self.subType) @staticmethod def _dataLength_for_subtype(subType): if subType <= 0x7F: return 7 # This is really really hacky, but... works for i in range(5): try: subEvent = _EVENT_TYPES[subType](0, *([0] * i)) return 4 + subEvent.dataLength except TypeError as e: pass raise ValueError("Couldn't determine dataLength for" ' RandomSequenceEvent for some reason...?' f' subType={hex(subType)}') def save(self, eventsToOffsets=None): return (super().save() + bytes([self.subType, *self.args]) + struct.pack('<hh', self.randMin, self.randMax)) @classmethod def fromData(cls, type, data, startOffset=0): subType = data[startOffset + 1] length = cls._dataLength_for_subtype(subType) args = [] offset = startOffset + 2 for i in range(length - 6): args.append(data[offset]) offset += 1 randMin, randMax = struct.unpack_from('<hh', data, offset) return cls(subType, args, randMin, randMax) def __str__(self): return (f'<random {hex(self.subType)}' f' {" ".join([str(x) for x in self.args])}' f'{" " if self.args else ""}' f'{(self.randMin, self.randMax)}>') def __repr__(self): return f'{type(self).__name__}({hex(self.subType)}, {self.args!r}, {self.randMin!r}, {self.randMax!r})'
[docs]class FromVariableSequenceEvent(SequenceEvent): """ A sequence event that executes some other event with its last argument taken from a variable. This is sequence event type 0xA1. """ def __init__(self, subType, variableID, unknown=None): super().__init__(0xA1) self.subType = subType self.variableID = variableID self.unknown = unknown @property def dataLength(self): # Original comment on this logic from sseq2mid.c: # /* loveemu is a lazy person :P */ # (loveemu is the author of sseq2mid.) # Interpret as you will. return 4 if (0xB0 <= self.subType <= 0xBD) else 3 def save(self, eventsToOffsets=None): if self.dataLength == 3: return super().save() + struct.pack('<Bh', self.subType, self.variableID) else: if self.unknown is None: raise ValueError('FromVariableSequenceEvent: trying to' ' save with unknown, but unknown is None!') return super().save() + struct.pack('<Bbbxx', self.subType, self.unknown, self.variableID) @classmethod def fromData(cls, type, data, startOffset=0): subType = data[startOffset + 1] if 0xB0 <= subType <= 0xBD: unknown, variableID = struct.unpack_from('<bb', data, startOffset + 2) else: unknown = None variableID, = struct.unpack_from('<h', data, startOffset + 2) return cls(subType, variableID, unknown) def __str__(self): unk = f' {self.unknown}' if self.unknown is not None else '' return f'<from variable {hex(self.subType)} {self.variableID}{unk}>' def __repr__(self): unk = f', {self.unknown}' if self.unknown is not None else '' return f'{type(self).__name__}({hex(self.subType)}, {self.variableID!r}{unk})'
[docs]class IfSequenceEvent(SequenceEvent): """ A sequence event that causes the next event to be skipped if the conditional flag is currently false. This is sequence event type 0xA2. """ def __init__(self): super().__init__(0xA2) @classmethod def fromData(cls, type, data, startOffset=0): return cls() def __str__(self): return '<if>' def __repr__(self): return f'{type(self).__name__}()'
def _make_arithmetic_sequence_event_class(typeNum, symbol, name, description): """ Helper function to make a SequenceEvent subclass for the arithmetic operations. """ __doc__ = f'A sequence event that {description}. This is sequence event type 0x{typeNum:02X}.' def __init__(self, variableID, value): SequenceEvent.__init__(self, typeNum) self.variableID = variableID self.value = value def save(self, eventsToOffsets=None): return (SequenceEvent.save(self) + struct.pack('<Bh', self.variableID, self.value)) @classmethod def fromData(cls, type, data, startOffset=0): variableID, value = struct.unpack_from('<Bh', data, startOffset + 1) return cls(variableID, value) def __str__(self): return f'<(var {self.variableID}) {symbol.lower()} {self.value}>' def __repr__(self): return f'{type(self).__name__}({self.variableID!r}, {self.value!r})' return type(name, (SequenceEvent,), {'__doc__': __doc__, '__init__': __init__, 'save': save, 'fromData': fromData, '__str__': __str__, '__repr__': __repr__, 'dataLength': 4}) VariableAssignmentSequenceEvent = _make_arithmetic_sequence_event_class(0xB0, '=', 'VariableAssignmentSequenceEvent', 'sets a variable to a given value') VariableAdditionSequenceEvent = _make_arithmetic_sequence_event_class(0xB1, '+=', 'VariableAdditionSequenceEvent', 'increments a variable by a given value') VariableSubtractionSequenceEvent = _make_arithmetic_sequence_event_class(0xB2, '-=', 'VariableSubtractionSequenceEvent', 'decrements a variable by a given value') VariableMultiplicationSequenceEvent = _make_arithmetic_sequence_event_class(0xB3, '*=', 'VariableMultiplicationSequenceEvent', 'multiplies a variable by a given value') VariableDivisionSequenceEvent = _make_arithmetic_sequence_event_class(0xB4, '/=', 'VariableDivisionSequenceEvent', 'divides a variable by a given value') VariableShiftSequenceEvent = _make_arithmetic_sequence_event_class(0xB5, '<<=', 'VariableShiftSequenceEvent', 'left-shifts a variable by a given value') VariableRandSequenceEvent = _make_arithmetic_sequence_event_class(0xB6, '[rand]', 'VariableRandSequenceEvent', 'sets a variable to a random value') # Deprecated VariableUnknownB7SequenceEvent = _make_arithmetic_sequence_event_class(0xB7, '[nop]', 'VariableUnknownB7SequenceEvent', 'does nothing') VariableEqualSequenceEvent = _make_arithmetic_sequence_event_class(0xB8, '==', 'VariableEqualSequenceEvent', 'sets the conditional flag to true if the specified variable contains a given value,' ' or to false otherwise') VariableGreaterThanOrEqualSequenceEvent = _make_arithmetic_sequence_event_class(0xB9, '>=', 'VariableGreaterThanOrEqualSequenceEvent', 'sets the conditional flag to true if the specified variable contains a value greater' ' than or equal to a given value, or to false otherwise') VariableGreaterThanSequenceEvent = _make_arithmetic_sequence_event_class(0xBA, '>', 'VariableGreaterThanSequenceEvent', 'sets the conditional flag to true if the specified variable contains a value greater' ' than a given value, or to false otherwise') VariableLessThanOrEqualSequenceEvent = _make_arithmetic_sequence_event_class(0xBB, '<=', 'VariableLessThanOrEqualSequenceEvent', 'sets the conditional flag to true if the specified variable contains a value less' ' than or equal to a given value, or to false otherwise') VariableLessThanSequenceEvent = _make_arithmetic_sequence_event_class(0xBC, '<', 'VariableLessThanSequenceEvent', 'sets the conditional flag to true if the specified variable contains a value less' ' than a given value, or to false otherwise') VariableNotEqualSequenceEvent = _make_arithmetic_sequence_event_class(0xBD, '!=', 'VariableNotEqualSequenceEvent', 'sets the conditional flag to true if the specified variable does not contain' ' a given value, or to false otherwise') def _make_simple_sequence_event_class(typeNum, shortName, name, description): """ Helper function to make a simple SequenceEvent subclass with one parameter. """ __doc__ = f'A sequence event {description}. This is sequence event type 0x{typeNum:02X}.' def __init__(self, value): SequenceEvent.__init__(self, typeNum) self.value = value def save(self, eventsToOffsets=None): return SequenceEvent.save(self) + self.value.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def __str__(self): return f'<{shortName.lower()} {self.value}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})' return type(name, (SequenceEvent,), {'__doc__': __doc__, '__init__': __init__, 'save': save, 'fromData': fromData, '__str__': __str__, '__repr__': __repr__, 'dataLength': 2}) PanSequenceEvent = _make_simple_sequence_event_class(0xC0, 'Pan', 'PanSequenceEvent', 'that sets the stereo panning value for the current track') TrackVolumeSequenceEvent = _make_simple_sequence_event_class(0xC1, 'Track volume', 'TrackVolumeSequenceEvent', 'that sets the volume of the current track') GlobalVolumeSequenceEvent = _make_simple_sequence_event_class(0xC2, 'Global volume', 'GlobalVolumeSequenceEvent', 'that sets the global volume, for all tracks') TransposeSequenceEvent = _make_simple_sequence_event_class(0xC3, 'Transpose', 'TransposeSequenceEvent', 'that causes NoteSequenceEvents following it in the current track to be transposed') PortamentoSequenceEvent = _make_simple_sequence_event_class(0xC4, 'Portamento', 'PortamentoSequenceEvent', 'related to portamentos') PortamentoRangeSequenceEvent = _make_simple_sequence_event_class(0xC5, 'Portamento range', 'PortamentoRangeSequenceEvent', 'related to portamentos') TrackPrioritySequenceEvent = _make_simple_sequence_event_class(0xC6, 'Track priority', 'TrackPrioritySequenceEvent', 'that sets the priority of the current track')
[docs]class MonoPolySequenceEvent(SequenceEvent): """ A sequence event that switches the current track to mono mode or poly mode. This is sequence event type 0xC7. """ dataLength = 2
[docs] class Value(enum.IntEnum): POLY = 0 MONO = 1
def __init__(self, value): super().__init__(0xC7) self.value = self.Value(value) def save(self, eventsToOffsets=None): return super().save() + self.value.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def __str__(self): return '<mono>' if self.value else '<poly>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
# Change the internal name of the MonoPolySequenceEvent.Value enum to # "MonoPolySequenceEvent.Value" MonoPolySequenceEvent.Value.__name__ = \ f'{MonoPolySequenceEvent.__name__}.{MonoPolySequenceEvent.Value.__name__}'
[docs]class TieSequenceEvent(SequenceEvent): """ A sequence event that enables or disables "tie" mode on the current track. This is sequence event type 0xC8. """ dataLength = 2 def __init__(self, value): super().__init__(0xC8) if value in [0, 1]: value = bool(value) self.value = value def save(self, eventsToOffsets=None): return super().save() + self.value.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def __str__(self): if self.value in [0, 1, False, True]: v = 'on' if self.value else 'off' else: v = self.value return f'<tie {v}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
[docs]class PortamentoFromSequenceEvent(SequenceEvent): """ A sequence event related to portamentos. This is sequence event type 0xC9. """ dataLength = 2 def __init__(self, value): super().__init__(0xC9) self.value = value def save(self, eventsToOffsets=None): return super().save() + self.value.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def __str__(self): return f'<portamento from {_common.noteName(self.value)}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
VibratoDepthSequenceEvent = _make_simple_sequence_event_class(0xCA, 'Vibrato depth', 'VibratoDepthSequenceEvent', 'related to vibratos') VibratoSpeedSequenceEvent = _make_simple_sequence_event_class(0xCB, 'Vibrato speed', 'VibratoSpeedSequenceEvent', 'related to vibratos')
[docs]class VibratoTypeSequenceEvent(SequenceEvent): """ A sequence event that sets the current vibrato type. This is sequence event type 0xCC. """ dataLength = 2
[docs] class Value(enum.IntEnum): PITCH = 0 VOLUME = 1 PAN = 2
def __init__(self, value): super().__init__(0xCC) self.value = self.Value(value) def save(self, eventsToOffsets=None): return super().save() + self.value.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def _valueName(self): if self.value in [0, 1, 2]: return ['pitch', 'volume', 'pan'][self.value] else: return str(self.value) def __str__(self): return f'<vibrato type {self._valueName()}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
# Change the internal name of the VibratoTypeSequenceEvent.Value enum to # "VibratoTypeSequenceEvent.Value" VibratoTypeSequenceEvent.Value.__name__ = \ f'{VibratoTypeSequenceEvent.__name__}.{VibratoTypeSequenceEvent.Value.__name__}' VibratoRangeSequenceEvent = _make_simple_sequence_event_class(0xCD, 'Vibrato range', 'VibratoRangeSequenceEvent', 'related to vibratos')
[docs]class PortamentoOnOffSequenceEvent(SequenceEvent): """ A sequence event that enables or disables portamento mode. This is sequence event type 0xCE. """ dataLength = 2 def __init__(self, value): super().__init__(0xCE) if value in [0, 1]: value = bool(value) self.value = value def save(self, eventsToOffsets=None): return super().save() + self.value.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def __str__(self): if self.value in [0, 1, False, True]: v = 'on' if self.value else 'off' else: v = self.value return f'<portamento {v}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
PortamentoDurationSequenceEvent = _make_simple_sequence_event_class(0xCF, 'Portamento duration', 'PortamentoDurationSequenceEvent', 'related to portamentos') AttackRateSequenceEvent = _make_simple_sequence_event_class(0xD0, 'Attack rate', 'AttackRateSequenceEvent', 'that sets the attack rate for notes in the current track') DecayRateSequenceEvent = _make_simple_sequence_event_class(0xD1, 'Decay rate', 'DecayRateSequenceEvent', 'that sets the decay rate for notes in the current track') SustainRateSequenceEvent = _make_simple_sequence_event_class(0xD2, 'Sustain rate', 'SustainRateSequenceEvent', 'that sets the sustain rate for notes in the current track') ReleaseRateSequenceEvent = _make_simple_sequence_event_class(0xD3, 'Release rate', 'ReleaseRateSequenceEvent', 'that sets the release rate for notes in the current track')
[docs]class BeginLoopSequenceEvent(SequenceEvent): """ A sequence event that begins a loop in the current track. This is sequence event type 0xD4. The end of the loop must be marked by an EndLoopSequenceEvent. """ dataLength = 2 def __init__(self, loopCount): super().__init__(0xD4) self.loopCount = loopCount def save(self, eventsToOffsets=None): return super().save() + self.loopCount.to_bytes(1, 'little') @classmethod def fromData(cls, type, data, startOffset=0): return cls(data[startOffset + 1]) def __str__(self): return f'<begin loop {self.loopCount}>' def __repr__(self): return f'{type(self).__name__}({self.loopCount!r})'
ExpressionSequenceEvent = _make_simple_sequence_event_class(0xD5, 'Expression', 'ExpressionSequenceEvent', 'that is unknown') PrintVariableSequenceEvent = _make_simple_sequence_event_class(0xD6, 'Print variable', 'PrintVariableSequenceEvent', 'that is unknown') VibratoDelaySequenceEvent = _make_simple_sequence_event_class(0xE0, 'Vibrato delay', 'VibratoDelaySequenceEvent', 'related to vibratos')
[docs]class TempoSequenceEvent(SequenceEvent): """ A sequence event that sets the tempo for all tracks in the sequence. This is sequence event type 0xE1. """ dataLength = 3 def __init__(self, value): super().__init__(0xE1) self.value = value def save(self, eventsToOffsets=None): return super().save() + struct.pack('<H', self.value) @classmethod def fromData(cls, type, data, startOffset=0): return cls(struct.unpack_from('<H', data, startOffset + 1)[0]) def __str__(self): return f'<tempo {self.value}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
[docs]class SweepPitchSequenceEvent(SequenceEvent): """ An unknown sequence event type. This is sequence event type 0xE3. """ dataLength = 3 def __init__(self, value): super().__init__(0xE3) self.value = value def save(self, eventsToOffsets=None): return super().save() + struct.pack('<H', self.value) @classmethod def fromData(cls, type, data, startOffset=0): return cls(struct.unpack_from('<H', data, startOffset + 1)[0]) def __str__(self): return f'<sweep pitch {self.value}>' def __repr__(self): return f'{type(self).__name__}({self.value!r})'
[docs]class EndLoopSequenceEvent(SequenceEvent): """ A sequence event that ends a loop previously begun with a BeginLoopSequenceEvent. This is sequence event type 0xFC. """ def __init__(self): super().__init__(0xFC) @classmethod def fromData(cls, type, data, startOffset=0): return cls() def __str__(self): return '<end loop>' def __repr__(self): return f'{type(self).__name__}()'
[docs]class ReturnSequenceEvent(SequenceEvent): """ A sequence event that causes execution of the current track to jump back to the most recently encountered CallSequenceEvent. This is sequence event type 0xFD. """ def __init__(self): super().__init__(0xFD) @classmethod def fromData(cls, type, data, startOffset=0): return cls() def __str__(self): return '<return>' def __repr__(self): return f'{type(self).__name__}()'
[docs]class DefineTracksSequenceEvent(SequenceEvent): """ A sequence event that defines the tracks that will be used in the sequence. This is sequence event type 0xFE. """ dataLength = 3 def __init__(self, trackNumbers): super().__init__(0xFE) self.trackNumbers = trackNumbers def save(self, eventsToOffsets=None): tracksBitfield = 0 for i in range(16): if i in self.trackNumbers: tracksBitfield |= 1 << i return super().save() + struct.pack('<H', tracksBitfield) @classmethod def fromData(cls, type, data, startOffset=0): tracksBitfield, = struct.unpack_from('<H', data, startOffset + 1) trackNumbers = set() for i in range(16): if tracksBitfield & (1 << i): trackNumbers.add(i) return cls(trackNumbers) def __str__(self): return f'<define tracks {" ".join(str(x) for x in sorted(self.trackNumbers))}>' def __repr__(self): return f'{type(self).__name__}({self.trackNumbers!r})'
[docs]class EndTrackSequenceEvent(SequenceEvent): """ A sequence event that ends execution of the current track. This is sequence event type 0xFF. """ def __init__(self): super().__init__(0xFF) @classmethod def fromData(cls, type, data, startOffset=0): return cls() def __str__(self): return '<end track>' def __repr__(self): return f'{type(self).__name__}()'
[docs]class RawDataSequenceEvent(SequenceEvent): """ A dummy sequence event that represents raw binary data that seems to be unreachable as far as ndspy can tell. """ @property def dataLength(self): return len(self.data) def __init__(self, data): super().__init__(None) self.data = data def save(self, eventsToOffsets=None): return self.data def __str__(self): return f'<raw data {bytes(self.data)}>' def __repr__(self): return f'{type(self).__name__}({self.data!r})'
_EVENT_TYPES = { 0x80: RestSequenceEvent, 0x81: InstrumentSwitchSequenceEvent, 0x93: BeginTrackSequenceEvent, 0x94: JumpSequenceEvent, 0x95: CallSequenceEvent, 0xA0: RandomSequenceEvent, 0xA1: FromVariableSequenceEvent, 0xA2: IfSequenceEvent, 0xB0: VariableAssignmentSequenceEvent, 0xB1: VariableAdditionSequenceEvent, 0xB2: VariableSubtractionSequenceEvent, 0xB3: VariableMultiplicationSequenceEvent, 0xB4: VariableDivisionSequenceEvent, 0xB5: VariableShiftSequenceEvent, 0xB6: VariableRandSequenceEvent, 0xB7: VariableUnknownB7SequenceEvent, 0xB8: VariableEqualSequenceEvent, 0xB9: VariableGreaterThanOrEqualSequenceEvent, 0xBA: VariableGreaterThanSequenceEvent, 0xBB: VariableLessThanOrEqualSequenceEvent, 0xBC: VariableLessThanSequenceEvent, 0xBD: VariableNotEqualSequenceEvent, 0xC0: PanSequenceEvent, 0xC1: TrackVolumeSequenceEvent, 0xC2: GlobalVolumeSequenceEvent, 0xC3: TransposeSequenceEvent, 0xC4: PortamentoSequenceEvent, 0xC5: PortamentoRangeSequenceEvent, 0xC6: TrackPrioritySequenceEvent, 0xC7: MonoPolySequenceEvent, 0xC8: TieSequenceEvent, 0xC9: PortamentoFromSequenceEvent, 0xCA: VibratoDepthSequenceEvent, 0xCB: VibratoSpeedSequenceEvent, 0xCC: VibratoTypeSequenceEvent, 0xCD: VibratoRangeSequenceEvent, 0xCE: PortamentoOnOffSequenceEvent, 0xCF: PortamentoDurationSequenceEvent, 0xD0: AttackRateSequenceEvent, 0xD1: DecayRateSequenceEvent, 0xD2: SustainRateSequenceEvent, 0xD3: ReleaseRateSequenceEvent, 0xD4: BeginLoopSequenceEvent, 0xD5: ExpressionSequenceEvent, 0xD6: PrintVariableSequenceEvent, 0xE0: VibratoDelaySequenceEvent, 0xE1: TempoSequenceEvent, 0xE3: SweepPitchSequenceEvent, 0xFC: EndLoopSequenceEvent, 0xFD: ReturnSequenceEvent, 0xFE: DefineTracksSequenceEvent, 0xFF: EndTrackSequenceEvent, } def readSequenceEvents(data, notableOffsets=None): """ Convert raw sequence event data (as seen in SSEQ and SSAR files) to a list of SequenceEvent objects. This is the inverse of saveSequenceEvents(). A second list will also be returned that contains the elements from the first list that appeared in the input data at the offsets given in notableOffsets. """ if notableOffsets is None: notableOffsets = [] events = {} FATE_INPROGRESS = 0 FATE_RETURN = 1 FATE_LOOP = 2 FATE_EOT = 3 fates = {} def parse_at(off): offsetsOfMySequentialEvents = [] while off < len(data): if off in fates: fate = fates[off] if fate == FATE_INPROGRESS: fate = FATE_LOOP for off_ in offsetsOfMySequentialEvents: fates[off_] = fate return fate try: type = data[off] if type == 0x93: # BeginTrack trackNumber = data[off + 1] firstEventOff = int.from_bytes(data[off + 2 : off + 5], 'little') # 3-byte int event = BeginTrackSequenceEvent(trackNumber, None) events[off] = event fates[off] = FATE_INPROGRESS parse_at(firstEventOff) event.firstEvent = events[firstEventOff] elif type == 0x94: # Jump destination = int.from_bytes(data[off + 1 : off + 4], 'little') # 3-byte int event = JumpSequenceEvent(None) events[off] = event fates[off] = FATE_INPROGRESS fate = parse_at(destination) event.destination = events[destination] for off_ in offsetsOfMySequentialEvents: fates[off_] = fate # Should we keep parsing past here? Only if this # is part of an if statement (and thus might be # skipped). x = off - 1 while x not in events and x >= 0: x -= 1 if x == -1: partOfIfStatement = False else: partOfIfStatement = (events[x].type == 0xA2) if not partOfIfStatement: return fate elif type == 0x95: # Call destination = int.from_bytes(data[off + 1 : off + 4], 'little') # 3-byte int event = CallSequenceEvent(None) events[off] = event fates[off] = FATE_INPROGRESS fate = parse_at(destination) event.destination = events[destination] if fate == FATE_EOT: fates[off] = fate for off_ in offsetsOfMySequentialEvents: fates[off_] = fate return fate elif fate == FATE_RETURN: pass elif fate == FATE_LOOP: fates[off] = fate for off_ in offsetsOfMySequentialEvents: fates[off_] = fate return fate elif type == 0xFD: # Return events[off] = ReturnSequenceEvent() fates[off] = FATE_RETURN for off_ in offsetsOfMySequentialEvents: fates[off_] = FATE_RETURN return FATE_RETURN elif type == 0xFF: # EoT events[off] = EndTrackSequenceEvent() fates[off] = FATE_EOT for off_ in offsetsOfMySequentialEvents: fates[off_] = FATE_EOT return FATE_EOT else: if type <= 0x7F: eventCls = NoteSequenceEvent elif type not in _EVENT_TYPES: raise ValueError(f'Event {hex(type)} unrecognized.') else: eventCls = _EVENT_TYPES[type] event = eventCls.fromData(type, data, off) events[off] = event fates[off] = FATE_INPROGRESS offsetsOfMySequentialEvents.append(off) off += event.dataLength except (struct.error, IndexError): raise EOFError('Reached EoF of sequence.') raise EOFError('Reached EoF of sequence.') starts = notableOffsets if not starts: starts = [0] for start in starts: ultimateFate = parse_at(start) assert ultimateFate in (FATE_EOT, FATE_LOOP), f'Starting point {hex(start)} results in fate {ultimateFate}' eventsList = [] i = 0 while i < len(data): if i in events: eventsList.append(events[i]) i += events[i].dataLength else: j = i while j not in events and j < len(data): j += 1 eventsList.append(RawDataSequenceEvent(data[i:j])) i = j notableEvents = [events[off] for off in notableOffsets] return eventsList, notableEvents def saveSequenceEvents(events, notableEvents=None): """ Convert a list of SequenceEvent objects to raw sequence event data. This is the inverse of readSequenceEvents(). A second list will also be returned that contains the offsets in the output data of the elements from notableEvents. """ if notableEvents is None: notableEvents = [] events2Offsets = {} off = 0 for e in events: events2Offsets[e] = off off += e.dataLength data = bytearray(off) for event, offset in events2Offsets.items(): eData = event.save(events2Offsets) data[offset : offset + len(eData)] = eData notableOffsets = [events2Offsets[e] for e in notableEvents] return data, notableOffsets
[docs]def printSequenceEventList(events, labels=None, linePrefix=''): """ Produce a string representation of a list of sequence events. You can optionally provide a dictionary of labels to mark certain events, and a prefix string that will be prepended to every line. """ if labels is None: labels = {} warningsList = [] linesList = [] maxNameLen = 0 i2SfxName = {} for name, event in labels.items(): if event in events: i = events.index(event) elif event is None: linesList.append(f'{linePrefix}{name}: (none)') continue else: warningsList.append(f'{linePrefix}(WARNING: {event!r} ({name}) not in events list!)') continue if name is None: name = str(i) if i not in i2SfxName: i2SfxName[i] = name else: i2SfxName[i] += ', ' + name maxNameLen = max(maxNameLen, len(i2SfxName[i])) maxNameLen = min(maxNameLen, 48) maxNumLen = len(str(len(events) - 1)) maxNameLen += 1 # for the ":" at the end def getDestinationStr(primaryEvent, destination): if destination in events: return str(events.index(destination)) else: warningsList.append(f'{linePrefix}(WARNING: {destination!r} (target of {primaryEvent!r}) not in events list!)') return 'NOWHERE' def printEvent(e): label = i2SfxName[i] + ':' if i in i2SfxName else '' labelLst = [] while label: labelLst.append(label[:maxNameLen]) label = label[maxNameLen:] if labelLst: labelLst[-1] = f'{labelLst[-1]:{maxNameLen}}' else: labelLst = [' ' * maxNameLen] label = f'\n{linePrefix}'.join(labelLst) if isinstance(e, JumpSequenceEvent): etext = f'<jump @{getDestinationStr(e, e.destination)}>' elif isinstance(e, CallSequenceEvent): etext = f'<call @{getDestinationStr(e, e.destination)}>' elif isinstance(e, BeginTrackSequenceEvent): etext = f'<begin track {e.trackNumber} @{getDestinationStr(e, e.firstEvent)}>' else: etext = str(e) return f'{linePrefix}{label} [{i:{maxNumLen}}] {etext},' for i, e in enumerate(events): linesList.append(printEvent(e)) # Remove last "," if events: linesList[-1] = linesList[-1][:-1] return '\n'.join(warningsList + linesList)