Source code for pyboy.api.sound

#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
"""
This class gives access to the sound buffer of PyBoy.
"""

import numpy as np

from pyboy import utils
from pyboy.logging import get_logger

logger = get_logger(__name__)


[docs] class Sound: """ As part of the emulation, we generate a sound buffer for each frame on the screen. This class has several helper methods to make it possible to read this buffer out. When the game enables/disables the LCD, the timing will be shorter than 70224 emulated cycles. Therefore the sound buffer will also be shorter than a normal 16.74ms frame. Because a Game Boy frame is about 59.73Hz, the number of samples per frame fluctuates with the configured sample rate. At 24,000Hz, a normal frame contains about 402 samples. When the LCD enables or disables, a frame can be shorter, with perhaps 30, 143, or 200 samples. This timespan represents what the real hardware would have shown. If you're working with encoding the screen and sound in a video stream, you could drop these shorter frames, if they cause problems. They usually only happen in transitions from menu to game or similar. """ def __init__(self, mb): self.mb = mb self.sample_rate = self.mb.sound.sample_rate """ Read-only. Changing this, will not change the sample rate. See `PyBoy` constructor instead. The sample rate is reported per second, while a Game Boy frame is about 59.73Hz. The sample count per frame fluctuates; see the explanation above. ```python >>> pyboy.sound.sample_rate # in Hz 48000 >>> (pyboy.sound.sample_rate * 70224 + 4194304 - 1) // 4194304 804 >>> (804+1) * 2 # Stereo buffer capacity, including one extra sample 1610 >>> 1610 == pyboy.sound.raw_buffer_length True ``` Returns ------- int: The sample rate in Hz (samples per second) """ self.raw_buffer_format = self.mb.sound.buffer_format """ Returns the sample format of the raw sound buffer. **This format is subject to change.** See how to interpret the format on: https://docs.python.org/3/library/struct.html#format-characters Example: ```python >>> pyboy.sound.raw_buffer_format 'b' ``` Returns ------- str: Struct format of the raw sound buffer. E.g. 'b' for signed 8-bit """ self.raw_buffer_length = self.mb.sound.audiobuffer_length """ Read-only. Changing this, will not change the buffer length. This is the total length of the allocated raw buffer. Use this only to allocate an appropriate buffer in your script. The length of the valid data in the buffer is found using `Sound.raw_buffer_head`. Returns ------- int: Total raw buffer length """ self.raw_buffer = memoryview(self.mb.sound.audiobuffer).cast( self.raw_buffer_format, shape=(self.mb.sound.audiobuffer_length,) ) """ Provides a raw, unfiltered `memoryview` object with the data from sound buffer. Check `Sound.raw_buffer_format` to see which dataformat is used. **The returned type and dataformat are subject to change.** The sound buffer is interleaved stereo: Even indexes are the left channel and odd indexes are the right channel. Use this, only if you need to bypass the overhead of `Sound.ndarray`. Use `Sound.raw_buffer_head` as not all frames have the same number of samples. Example: ```python >>> from array import array >>> sound_buffer = array(pyboy.sound.raw_buffer_format, pyboy.sound.raw_buffer[:pyboy.sound.raw_buffer_head]) >>> sound_buffer array('b', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]) ``` Returns ------- memoryview: memoryview of sound data. """ self.raw_ndarray = None if self.mb.sound.emulate: self.raw_ndarray = np.frombuffer( self.mb.sound.audiobuffer, dtype=np.int8, ).reshape(self.mb.sound.audiobuffer_length // 2, 2) else: self.raw_ndarray = utils.SoundEnabledError() @property def raw_buffer_head(self): """ Number of valid interleaved channel values in `raw_buffer`. There are two values per stereo sample frame. Use this as the stop index when slicing `raw_buffer`. Returns ------- int: Number of valid channel values in the raw buffer. """ return self.mb.sound.audiobuffer_head @property def ndarray(self): """ References the valid sound data in NumPy format. **Remember to copy this object** if you intend to store it. The backing buffer will update, but it will be the same `ndarray` object. The format is given by `pyboy.api.sound.Sound.raw_buffer_format`. The sound buffer is in stereo format, so the first column is the left channel and the second column is the right channel. The first dimension contains only the sample frames generated since the last call to `PyBoy.tick`. If sound emulation is disabled, accessing this property raises `PyBoyFeatureDisabledError`. Example: ```python >>> pyboy.sound.ndarray.shape[1] # Number of stereo channels 2 >>> pyboy.sound.ndarray array([[0, 0], [0, 0], ... [0, 0], [0, 0]], dtype=int8) ``` Returns ------- numpy.ndarray: Signed 8-bit samples with shape `(valid_sample_frames, 2)`. """ if self.mb.sound.emulate: return self.raw_ndarray[: self.mb.sound.audiobuffer_head // 2] else: raise utils.PyBoyFeatureDisabledError("Sound is not enabled!")