..
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.soundStream``: Sound Streams
====================================
.. module:: ndspy.soundStream
The ``ndspy.soundStream`` module lets you edit and create *STRM* streamed audio
files. These are intended to be used for background music.
A *STRM* is conceptually similar to a standard *WAV* audio file. They define a
waveform rather than sequence events. They support multiple channels, and
longer loops than *SWAV*\s do. These are less commonly used than *SSEQ*
sequenced audio files due to their much larger file size.
.. warning::
All *STRM*\s I've found in retail games which have multiple blocks per
channel use the ADPCM wave type (:py:attr:`waveType` ``=``
:py:data:`ndspy.WaveType.ADPCM`). This may be due to a hardware limitation
explained `on GBATEK `_
(see the section about the "Hold Flag (appears useless/bugged)"). You may
run into issues playing *STRM*\s with multiple blocks per channel and some
other wave data format.
.. seealso::
If you aren't familiar with how *SDAT* files are structured, consider
reading :doc:`the appendix explaining this <../appendices/sdat-structure>`.
.. py:class:: STRM([file[, unk02[, volume[, priority[, playerID[, unk07]]]]]])
A *STRM* streamed audio file. This is a piece of music, usually used for
background music or jingles.
:param bytes file: The data to be read as an *STRM* file. If this is not
provided, the *STRM* object will initially be empty.
:param unk02: The initial value for the :py:attr:`unk02` attribute.
:param volume: The initial value for the :py:attr:`volume` attribute.
:param priority: The initial value for the :py:attr:`priority` attribute.
:param playerID: The initial value for the :py:attr:`playerID` attribute.
:param unk07: The initial value for the :py:attr:`unk07` attribute.
.. py:attribute:: channels
This attribute contains the *STRM*'s raw waveform data, organized by
channel and block.
This attribute is a list where each element represents a channel. Each
of these channels is itself a list of :py:class:`bytes` objects
representing blocks of wave data. Many *STRM*\s have channels that are
only one block long, but some have hundreds or even thousands of
blocks per channel.
.. warning::
There are some restrictions on what you're allowed to put in here,
and you'll experience errors upon trying to save (:py:func:`save`)
if you don't follow them:
* All channels must have the same number of blocks:
.. code-block:: python
for c in strm.channels:
assert len(c) == len(strm.channels[0])
* All of the blocks in a given channel must be of the same size,
except for the last block, which can be a different size from
the others (typically shorter, but this isn't enforced):
.. code-block:: python
for c in strm.channels:
for b in c[:-1]:
assert len(b) == len(c[0])
* The lengths of the blocks in all channels must match:
.. code-block:: python
for c in strm.channels:
for i, b in enumerate(c):
assert len(b) == len(strm.channels[0][i])
.. note::
If the :py:attr:`waveType` is :py:data:`ndspy.WaveType.ADPCM`,
every block must begin with its own ADPCM header. More information
about ADPCM headers can be found on `GBATEK
`_.
:type: :py:class:`list` of :py:class:`list` of :py:class:`bytes`
:default: ``[]``
.. py:attribute:: dataMergeOptimizationID
When saving a *SDAT* file containing multiple *STRM* 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 *STRM* files, which you can use to exclude
particular ones from this optimization.
Since this defaults to 0 for all *STRM*\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 *STRM* file or in the
*SDAT* file containing it.
:type: :py:class:`int`
:default: 0
.. py:attribute:: isLooped
Whether the *STRM* is looped or just plays through once.
.. seealso::
You can use :py:attr:`loopOffset` to control the beginning of the
looped region.
:type: :py:class:`bool`
:default: ``False``
.. py:attribute:: loopOffset
The beginning of the looped portion of the *STRM* data, measured in
samples.
.. seealso::
In order to loop a *STRM*, you also need to set :py:attr:`isLooped`
to ``True``.
:type: :py:class:`int`
:default: 0
.. py:attribute:: playerID
The ID of the stream player that will be used to play this stream.
:type: :py:class:`int`
:default: 0
.. py:attribute:: priority
The stream's "priority." The exact meaning of this is unclear.
:type: :py:class:`int`
:default: 64
.. py:attribute:: sampleRate
The sample rate the *STRM* should be played at.
:type: :py:class:`int`
:default: 8000
.. py:attribute:: samplesInLastBlock
The length in samples of each channel's last block of waveform data
(:py:attr:`channels`).
.. seealso::
:py:attr:`samplesPerBlock` -- the corresponding attribute that
defines the number of samples in all blocks except for each
channel's last one.
:type: :py:class:`int`
:default: 0
.. py:attribute:: samplesPerBlock
The length in samples of each individual block of waveform data (in
:py:attr:`channels`), per channel, ignoring the final block of each
channel.
.. seealso::
:py:attr:`samplesInLastBlock` -- the corresponding attribute that
defines the number of samples in each channel's last block.
:type: :py:class:`int`
:default: 0
.. py:attribute:: time
A value of unclear meaning. This is pretty much always set to the
following:
.. code-block:: python
strm.time = int(1.0 / strm.sampleRate * 16756991 / 32)
.. note::
This can optionally be recalculated for you automatically upon
saving the *STRM*. For more information about this, see the
documentation for the :py:func:`save` function.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk02
The value following the *STRM*'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 *STRM* file, but it is
saved in the *SDAT* file if the *STRM* is within one.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk07
The value following the *STRM*'s player 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 *STRM* file, but it is
saved in the *SDAT* file if the *STRM* is within one.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk03
A value of unknown purpose at offset 0x1B (relative to the beginning
of the file) in the *STRM* file header.
Based on its location relative to surrounding values, this could be a
meaningless padding byte for alignment.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk28
A value of unknown purpose at offset 0x40 (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk2C
A value of unknown purpose at offset 0x44 (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk30
A value of unknown purpose at offset 0x48 (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk34
A value of unknown purpose at offset 0x4C (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk38
A value of unknown purpose at offset 0x50 (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk3C
A value of unknown purpose at offset 0x54 (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk40
A value of unknown purpose at offset 0x58 (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: unk44
A value of unknown purpose at offset 0x5C (relative to the beginning
of the file) in the *STRM* file header.
:type: :py:class:`int`
:default: 0
.. py:attribute:: volume
The overall volume of the stream. This is an integer between 0 and
127, inclusive. You should usually leave this as 127.
:type: :py:class:`int`
:default: 127
.. py:attribute:: waveType
The format that this *STRM*'s waveform data (:py:attr:`channels`) is
in.
:type: :py:class:`ndspy.WaveType` (or :py:class:`int`)
:default: :py:data:`ndspy.WaveType.PCM8`
.. py:classmethod:: fromChannels(channels[, unk02[, volume[, priority[, playerID[, unk07]]]]])
Create a *STRM* from a list of channels.
:param channels: The initial value for the :py:attr:`channels`
attribute.
:param unk02: The initial value for the :py:attr:`unk02` attribute.
:param volume: The initial value for the :py:attr:`volume` attribute.
:param priority: The initial value for the :py:attr:`priority`
attribute.
:param playerID: The initial value for the :py:attr:`playerID`
attribute.
:param unk07: The initial value for the :py:attr:`unk07` attribute.
:returns: The *STRM* object.
:rtype: :py:class:`STRM`
.. py:classmethod:: fromFile(filePath[, ...])
Load a *STRM* from a filesystem file. This is a convenience function.
:param filePath: The path to the *STRM* 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 *STRM* object.
:rtype: :py:class:`STRM`
.. py:function:: save(*[, updateTime=False])
Generate file data representing this *STRM*, and then return that data,
:py:attr:`unk02`, :py:attr:`volume`, :py:attr:`priority`,
:py:attr:`playerID`, and :py:attr:`unk07`, as a 6-tuple. This matches
the parameters of the default class constructor.
:param bool updateTime: If this is ``True``, :py:attr:`time` will be
updated based on the sample rate, using the formula found in the
documentation for the :py:attr:`time` attribute.
:default: ``False``
:returns: The *STRM* file data, :py:attr:`unk02`, :py:attr:`volume`,
:py:attr:`priority`, :py:attr:`playerID`, and :py:attr:`unk07`.
:rtype: ``(data, unk02, volume, priority, playerID, unk07)``, where
``data`` is of type :py:class:`bytes` and all of the other elements
are of type :py:class:`int`
.. py:function:: saveToFile(filePath, *[, updateTime=False])
Generate file data representing this *STRM*, and save it to a
filesystem file. This is a convenience function.
:param filePath: The path to the *STRM* file to save to.
:type filePath: :py:class:`str` or other path-like object
:param bool updateTime: If this is ``True``, :py:attr:`time` will be
updated based on the sample rate, using the formula found in the
documentation for the :py:attr:`time` attribute.
:default: ``False``