.. 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 . ``ndspy.soundSequence``: Sound Sequences ======================================== .. module:: ndspy.soundSequence The ``ndspy.soundSequence`` module contains classes and enumerations related to *SSEQ* sound seqeuence files and sound sequence events. If you're interested in *SSAR* sound sequence archive files, you'll need to use :py:mod:`ndspy.soundSequenceArchive` in addition to this one. Sound sequences are conceptually similar to `MIDI files `_. A sound sequence is essentially just a list of "events." Notes are the most common type of event, but there are also a wide variety of events that can change things like volume, panning, and flow of control. Documentation for sequence event classes can be found on the :doc:`soundSequence-events` page. .. note:: While the terms "channel" and "track" are often used interchangeably in other documentation, ndspy consistently uses "channel" to refer to hardware channels and "track" to refer to *SSEQ* tracks at the software level, as defined by the :py:class:`DefineTracksSequenceEvent`. .. seealso:: If you aren't familiar with how *SDAT* files are structured, consider reading :doc:`the appendix explaining this <../appendices/sdat-structure>`. .. toctree to cause soundSequence-events to be a subpage .. toctree:: :maxdepth: 2 :caption: Subpages soundSequence-events .. _parsed-vs-unparsed-sseqs: Parsed and Unparsed *SSEQ*\s and *SSAR*\s ----------------------------------------- In general, *SSEQ* and *SSAR* events data (hereafter referred to as just "*SSEQ* files", since they're both the same in this regard) cannot always be represented as a list of event objects. Not only does one encounter the halting problem because *SSEQ* supports variables and conditional branching, but sequence events could conceivably use overlapping data (although this has never actually been seen in practice). This complicates ndspy's efforts to be both easy-to-use and compatible with a wide range of valid input files. On one hand, most real-life *SSEQ* files don't have a very complicated structure, and their events data usually can in fact be represented as a list of events. Being able to access a *SSEQ*\'s data in this way is very intuitive and powerful. On the other hand, ndspy should be useful for editing any valid *SSEQ* file, including ones with events data too complicated for it to parse correctly. It should therefore let users see and manipulate the raw binary events data if they want to, or if it can't be parsed automatically. To handle this, in ndspy, a *SSEQ* can be in one of two states: "unparsed" or "parsed." An unparsed *SSEQ* can become parsed, but once a *SSEQ* has been parsed, it cannot go back to being unparsed. A *SSEQ* loaded from file data (including file data within a *SDAT*) will initially be "unparsed." At this stage, ndspy has not yet attempted to parse the events data, and that data can be found in the ``.eventsData`` attribute. If you want to access events data as a list, you need to call the ``.parse()`` function on the *SSEQ* to switch it to the "parsed" state. This function (largely powered by :py:func:`readsoundSequence-events`) attempts to parse the events data; if it's succcessful, the events will be placed in the ``.events`` attribute. The function is pretty intelligent, but it nevertheless has a relatively high failure rate due to the sheer complexity of events data, so it's good practice to wrap the call in a ``try:``/``except Exception:`` block. If it throws an exception, the *SSEQ* will remain in the unparsed state. You can check the state of a *SSEQ* using the ``.parsed`` attribute. Once a *SSEQ* has been parsed, it can never be unparsed (at least not directly), and ``.eventsData`` becomes inaccessible. If you really need to work with binary event data from a parsed *SSEQ*, a technique that might work for you is to call ``.save()`` and then create a new :py:class:`SSEQ` (or :py:class:`SSAR`) object from the resulting file data. Be aware, though, that there's no guarantee that saving an unmodified *SSEQ* will reproduce the original file data exactly, especially if it's been parsed. .. _multi-track-sseqs: Multi-track Sequences --------------------- An *SSEQ* or *SSAR* sequence can have up to 16 tracks. The game will automatically begin executing the sequence event data at the very beginning (or, for *SSAR*\s, at whichever event the sequence indicates) on track 0. If you intend to use more than one track, you must have a :py:class:`DefineTracksSequenceEvent` as the first event in your sequence. This declares all of the track IDs you intend to use. This should be followed by one :py:class:`BeginTrackSequenceEvent` per track (except for track 0), all in a row, which state where the events for each track begin in the events list. Since track 0 is the default track which is executing all of these track-definition events, don't add a :py:class:`BeginTrackSequenceEvent` for it -- just put its events starting immediately after the final :py:class:`BeginTrackSequenceEvent`. .. _sseq-variables: Sequence Variables ------------------ The sequence player for *SSEQ* and *SSAR* keeps track of an array of 16-bit [1]_ signed [2]_ integers [3]_ that you can use for whatever you like. These are known as sequence variables (or just "variables"), and are referenced by ID number (array index). You can use the following sequence events to perform mathematical operations on variables: * ``(variable) = value``: :py:class:`VariableAssignmentSequenceEvent` * ``(variable) += value``: :py:class:`VariableAdditionSequenceEvent` * ``(variable) -= value``: :py:class:`VariableSubtractionSequenceEvent` * ``(variable) *= value``: :py:class:`VariableMultiplicationSequenceEvent` * ``(variable) /= value``: :py:class:`VariableDivisionSequenceEvent` * ``(variable) <<= value``: :py:class:`VariableShiftSequenceEvent` * ``(variable) = random_int_between(0, value)``: :py:class:`VariableRandSequenceEvent` Once you have values in variables, there are two [4]_ primary ways you can use them: Using Variable Values as Sequence Event Arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:class:`FromVariableSequenceEvent` lets you use a variable's value as the last argument to some other sequence event. Conditional Execution ^^^^^^^^^^^^^^^^^^^^^ In addition to variables, the sequence player also keeps track of a conditional flag that can be used to skip over certain sequence events. The following sequence events update the conditional flag: * ``condFlag = ((variable) == value)`` :py:class:`VariableEqualSequenceEvent` * ``condFlag = ((variable) >= value)`` :py:class:`VariableGreaterThanOrEqualSequenceEvent` * ``condFlag = ((variable) > value)`` :py:class:`VariableGreaterThanSequenceEvent` * ``condFlag = ((variable) <= value)`` :py:class:`VariableLessThanOrEqualSequenceEvent` * ``condFlag = ((variable) < value)`` :py:class:`VariableLessThanSequenceEvent` * ``condFlag = ((variable) != value)`` :py:class:`VariableNotEqualSequenceEvent` After running one of these, you can use an :py:class:`IfSequenceEvent` to perform conditional execution -- the sequence event immediately following the :py:class:`IfSequenceEvent` will be skiped if the conditional flag is false. .. todo:: How many variables exist? Can we double-check that variables are per-sequence rather than per-track? How about the conditional flag? .. [1] This has been proven by checking that ``0xFFFF + 1 == 0``. .. [2] This has been proven by checking that ``1 - 2 < 0``. .. [3] This has been proven by checking that ``(3 / 2) * 2 == 2``. .. [4] There also exists a :py:class:`PrintVariableSequenceEvent`, which is not well-understood. .. py:class:: SSEQ([file[, unk02[, bankID[, volume[, channelPressure[, polyphonicPressure[, playerID]]]]]]]) A *SSEQ* sequence file. This is a piece of music, usually used for background music or jingles (such as the "you died" theme in *New Super Mario Bros.*). :param bytes file: The data to be read as an *SSEQ* file. If this is not provided, the *SSEQ* object will initially be empty. :param unk02: The initial value for the :py:attr:`unk02` attribute. :param bankID: The initial value for the :py:attr:`bankID` attribute. :param volume: The initial value for the :py:attr:`volume` attribute. :param channelPressure: The initial value for the :py:attr:`channelPressure` attribute. :param polyphonicPressure: The initial value for the :py:attr:`polyphonicPressure` attribute. :param playerID: The initial value for the :py:attr:`playerID` attribute. .. py:attribute:: bankID The ID of the instrument bank (*SBNK*) that this sequence will use. :type: :py:class:`int` :default: 0 .. py:attribute:: channelPressure The channel pressure for the sequence. The exact meaning of this is unclear. :type: :py:class:`int` :default: 64 .. py:attribute:: dataMergeOptimizationID When saving a *SDAT* file containing multiple *SSEQ* files, ndspy will check if any of them save to identical data. If it finds any, it will only encode the data for them once and then reference it multiple times, to save some space. This attribute is an extra field that is also compared between *SSEQ* files, which you can use to exclude particular ones from this optimization. Since this defaults to 0 for all *SSEQ*\s created from scratch, this optimization will happen by default. It's unlikely that you will need to use this attribute to disable the optimization, but you can. .. note:: This value is not explicitly saved in the *SSEQ* file or in the *SDAT* file containing it. :type: :py:class:`int` :default: 0 .. py:attribute:: events The list of sequence events contained in this *SSEQ*. This is only available in parsed *SSEQ*\s (ones with :py:attr:`parsed` set to ``True``). .. seealso:: :ref:`parsed-vs-unparsed-sseqs` -- the introductory text explaining the difference between parsed and unparsed *SSEQ*\s. :py:attr:`eventsData` -- the equivalent attribute that is available before parsing. :type: :py:class:`list` of :py:class:`SequenceEvent` :default: ``[]`` .. py:attribute:: eventsData The raw event data contained in this *SSEQ*. This is only available in unparsed *SSEQ*\s (ones with :py:attr:`parsed` set to ``False``). .. seealso:: :ref:`parsed-vs-unparsed-sseqs` -- the introductory text explaining the difference between parsed and unparsed *SSEQ*\s. :py:attr:`events` -- the equivalent attribute that becomes available after parsing. :type: :py:class:`bytes` .. py:attribute:: parsed Whether :py:func:`parse` has ever been called on this *SSEQ* object. This determines whether :py:attr:`eventsData` or :py:attr:`events` is available. This attribute is read-only. .. seealso:: :ref:`parsed-vs-unparsed-sseqs` -- the introductory text explaining the difference between parsed and unparsed *SSEQ*\s. :type: :py:class:`bool` :default: ``True`` .. py:attribute:: playerID The ID of the sequence player that will be used to play this sequence. :type: :py:class:`int` :default: 0 .. py:attribute:: polyphonicPressure The polyphonic pressure for the sequence. The exact meaning of this is unclear. :type: :py:class:`int` :default: 50 .. py:attribute:: unk02 The value following the *SSEQ*'s file ID in the "INFO" section of the *SDAT* file it is contained in. Its purpose is unknown. .. note:: This value is not explicitly saved in the *SSEQ* file, but it is saved in the *SDAT* file if the *SSEQ* is within one. :type: :py:class:`int` :default: 0 .. py:attribute:: volume The overall volume of the sequence. This is an integer between 0 and 127, inclusive. It's a good idea to leave this set to 127 and adjust volume using other, more precise methods (such as setting the track volume or individual note velocities). :type: :py:class:`int` :default: 127 .. py:classmethod:: fromEvents(events[, unk02[, bankID[, volume[, channelPressure[, polyphonicPressure[, playerID]]]]]]) Create a new *SSEQ* object from a list of sequence events. :param events: The list of sequence events in the new *SSEQ*. :type events: :py:class:`list` of :py:class:`SequenceEvent` :param unk02: The initial value for the :py:attr:`unk02` attribute. :param bankID: The initial value for the :py:attr:`bankID` attribute. :param volume: The initial value for the :py:attr:`volume` attribute. :param channelPressure: The initial value for the :py:attr:`channelPressure` attribute. :param polyphonicPressure: The initial value for the :py:attr:`polyphonicPressure` attribute. :param playerID: The initial value for the :py:attr:`playerID` attribute. .. py:classmethod:: fromFile(filePath[, ...]) Load an *SSEQ* from a filesystem file. This is a convenience function. :param filePath: The path to the *SSEQ* file to open. :type filePath: :py:class:`str` or other path-like object Further parameters are the same as those of the default constructor. :returns: The *SSEQ* object. :rtype: :py:class:`SSEQ` .. py:function:: parse() Attempt to process :py:attr:`eventsData` to create :py:attr:`events`. If successful, this switches the *SSEQ* from the unparsed to the parsed state (see :ref:`parsed-vs-unparsed-sseqs` for a more detailed explanation). Parsing events data is complex and even completely impossible in some cases. If unsuccessful, this function will raise an exception and the *SSEQ* will remain in the unparsed state. This function is idempotent, meaning that calling it on a *SSEQ* already in the parsed state will do nothing. .. py:function:: save() Generate file data representing this *SSEQ*, and then return that data, :py:attr:`unk02`, :py:attr:`bankID`, :py:attr:`volume`, :py:attr:`channelPressure`, :py:attr:`polyphonicPressure`, and :py:attr:`playerID` as a 7-tuple. This matches the parameters of the default class constructor. :returns: The *SSEQ* file data, :py:attr:`unk02`, :py:attr:`bankID`, :py:attr:`volume`, :py:attr:`channelPressure`, :py:attr:`polyphonicPressure`, and :py:attr:`playerID`. :rtype: ``(data, unk02, bankID, volume, channelPressure, polyphonicPressure, playerID)``, where ``data`` is of type :py:class:`bytes` and all of the other elements are of type :py:class:`int` .. py:function:: saveToFile(filePath) Generate file data representing this *SSEQ*, and save it to a filesystem file. This is a convenience function. :param filePath: The path to the *SSEQ* file to save to. :type filePath: :py:class:`str` or other path-like object .. py:function:: printSequenceEventList(events[, labels[, 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. .. note:: This is a relatively low-level function, mainly intended to power :py:class:`SSEQ` and :py:class:`ndspy.soundSequenceArchive.SSAR`. If you're using those classes, you can simply call the ``str()`` function on them to get a nice printout of their contents instead of calling this function directly. :param events: The sequence events to be printed. :type events: :py:class:`list` of :py:class:`SequenceEvent` :param labels: A dictionary containing any labels you would like to apply to particular events in the output string. If you specify multiple labels for the same event, all of them will be included. You can also provide entries with values set to ``None``; these labels will be included in the output without pointing to any event. :default: ``{}`` :type labels: :py:class:`dict`: ``{name: event}`` (where ``name`` is of type :py:class:`str` and ``event`` is of type :py:class:`SequenceEvent`) :param linePrefix: A string that will be prepended to every line in the output string. (This is mainly useful for indenting the string.) :default: ``''`` :type linePrefix: :py:class:`str` :returns: A string representing the list of sequence events. :rtype: :py:class:`str` .. py:function:: readsoundSequence-events(data[, notableOffsets]) Convert raw sequence event data (as seen in *SSEQ* and *SSAR* files) to a list of :py:class:`SequenceEvent` objects. This is the inverse of :py:func:`savesoundSequence-events`. 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``. This is useful if the data can be played from multiple different starting offsets, as is the case with *SSAR* files. It is safe to assume that every element of this second list is also an element of the first list, and that the length of the second list will match the length of ``notableOffsets``. .. note:: This is a relatively low-level function. Most of the time, you should use the :py:class:`SSEQ` and :py:class:`ndspy.soundSequenceArchive.SSAR` classes, which call this function for you in their respective ``parse()`` methods. .. warning:: Parsing events data into a list of event objects is complex, and even completely impossible in some extreme cases. As such, you should wrap calls to this function in ``try``/``except Exception:`` blocks, and implement fallback strategies in case the call fails. :param bytes data: The raw sequence events data. :param notableOffsets: A list of offsets into the data that point to sequence events you'd like to receive references to. :default: ``[]`` :type notableOffsets: :py:class:`list` of :py:class:`int` :returns: A list of sequence events that represents the data, and a list containing references to the event objects that were indicated in the ``notableOffsets`` argument. :rtype: ``(events, notableEvents)``, where both elements are :py:class:`list`\s of :py:class:`SequenceEvent` .. py:function:: savesoundSequence-events(events[, notableEvents]) Convert a list of :py:class:`SequenceEvent` objects to raw sequence event data. This is the inverse of :py:func:`readsoundSequence-events`. A second list will also be returned that contains the offsets in the output data of the elements from ``notableEvents``. This is useful if the data can be played from multiple different starting points, as is the case with *SSAR* files. Every element of ``notableEvents`` must also appear in ``events``. It is safe to assume that the length of the second list will match the length of ``notableEvents``. .. note:: This is a relatively low-level function. Most of the time, you should use the :py:class:`SSEQ` and :py:class:`ndspy.soundSequenceArchive.SSAR` classes, which call this function for you in their respective ``save()`` methods. :param events: The sequence events to be saved. :type events: :py:class:`list` of :py:class:`SequenceEvent` :param notableEvents: A list of sequence events in ``events`` for which you would like to know the offsets in the output data. :default: ``[]`` :type notableEvents: :py:class:`list` of :py:class:`SequenceEvent` :returns: The raw sequence event data, and a list containing offsets into it that point to the events given in the ``notableEvents`` argument. :rtype: ``(data, notableOffsets)``, where ``data`` is of type :py:class:`bytes` and ``notableOffsets`` is a :py:class:`list` of :py:class:`int`