Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
266 lines (221 sloc)
8.85 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """The sound effects section of a PICO-8 cart. | |
| The sound effects region consists of 4352 bytes. The .p8 | |
| representation is 64 lines of 168 hexadecimal digits (84 bytes). | |
| Each line represents one sound effect/music pattern. The values are as follows: | |
| 0 The editor mode: 0 for pitch mode, 1 for note entry mode. | |
| 1 The note duration, in multiples of 1/128 second. | |
| 2 Loop range start, as a note number (0-63). | |
| 3 Loop range end, as a note number (0-63). | |
| 4-84 32 notes: | |
| 0: pitch (0-63): c-0 to d#-5, chromatic scale | |
| 1-high: waveform (0-F): | |
| 0 sine, 1 triangle, 2 sawtooth, 3 long square, 4 short square, | |
| 5 ringing, 6 noise, 7 ringing sine; 8-F are the custom instruments | |
| corresponding to sfx 0-7 | |
| 1-low: volume (0-7) | |
| 2-high: effect (0-7): | |
| 0 none, 1 slide, 2 vibrato, 3 drop, 4 fade_in, 5 fade_out, | |
| 6 arp fast, 7 arp slow; arpeggio commands loop over groups of | |
| four notes at speed 2 (fast) and 4 (slow) | |
| One note uses five nibbles, so two notes use five bytes. | |
| The RAM representation is different. Each pattern is 68 bytes, with | |
| two bytes for each of 32 notes, one byte for the editor mode, one byte | |
| for the speed, and two bytes for the loop range (start, end). Each | |
| note is encoded in 16 bits, LSB first, like so: | |
| w2-w1-pppppp c-eee-vvv-w3 | |
| eee: effect (0-7) | |
| vvv: volume (0-7) | |
| w3w2w1: waveform (0-7) | |
| pppppp: pitch (0-63) | |
| c: if 1, waveform is a custom instrument corresponding to sfx 0-7; | |
| otherwise it's one of the eight built-in waveforms | |
| (Considering waveform as a value from 0-15, c is w4.) | |
| """ | |
| __all__ = ['Sfx'] | |
| from .. import util | |
| WAVEFORM_SINE = 0 | |
| WAVEFORM_TRIANGLE = 1 | |
| WAVEFORM_SAWTOOTH = 2 | |
| WAVEFORM_LONG_SQUARE = 3 | |
| WAVEFORM_SHORT_SQUARE = 4 | |
| WAVEFORM_RINGING = 5 | |
| WAVEFORM_NOISE = 6 | |
| WAVEFORM_RINGING_SINE = 7 | |
| WAVEFORM_CUSTOM_0 = 8 | |
| WAVEFORM_CUSTOM_1 = 9 | |
| WAVEFORM_CUSTOM_2 = 10 | |
| WAVEFORM_CUSTOM_3 = 11 | |
| WAVEFORM_CUSTOM_4 = 12 | |
| WAVEFORM_CUSTOM_5 = 13 | |
| WAVEFORM_CUSTOM_6 = 14 | |
| WAVEFORM_CUSTOM_7 = 15 | |
| EFFECT_NONE = 0 | |
| EFFECT_SLIDE = 1 | |
| EFFECT_VIBRATO = 2 | |
| EFFECT_DROP = 3 | |
| EFFECT_FADE_IN = 4 | |
| EFFECT_FADE_OUT = 5 | |
| EFFECT_ARP_FAST = 6 | |
| EFFECT_ARP_SLOW = 7 | |
| class Sfx(util.BaseSection): | |
| """The sfx region of a PICO-8 cart.""" | |
| HEX_LINE_LENGTH_BYTES = 84 | |
| @classmethod | |
| def empty(cls, version): | |
| """Creates an empty instance. | |
| Args: | |
| version: The PICO-8 file version. | |
| Returns: | |
| A Sfx instance. | |
| """ | |
| result = cls(data=bytearray(b'\x00' * 4352), version=version) | |
| # Emulate PICO-8 defaults: | |
| result.set_properties(0, note_duration=1) | |
| for i in range(1, 64): | |
| result.set_properties(i, note_duration=16) | |
| return result | |
| @classmethod | |
| def from_lines(cls, lines, version): | |
| """Create an instance based on .p8 data lines. | |
| Args: | |
| lines: .p8 lines for the section. | |
| version: The PICO-8 data version from the game file header. | |
| """ | |
| result = cls.empty(version=version) | |
| id = 0 | |
| for line in lines: | |
| if len(line) != 169: | |
| continue | |
| editor_mode = int(line[0:2], 16) | |
| note_duration = int(line[2:4], 16) | |
| loop_start = int(line[4:6], 16) | |
| loop_end = int(line[6:8], 16) | |
| result.set_properties(id, | |
| editor_mode=editor_mode, | |
| note_duration=note_duration, | |
| loop_start=loop_start, | |
| loop_end=loop_end) | |
| note = 0 | |
| for i in range(8, 168, 5): | |
| pitch = int(line[i:i+2], 16) | |
| waveform = int(line[i+2:i+3], 16) | |
| volume = int(line[i+3:i+4], 16) | |
| effect = int(line[i+4:i+5], 16) | |
| result.set_note(id, note, | |
| pitch=pitch, | |
| waveform=waveform, | |
| volume=volume, | |
| effect=effect) | |
| note += 1 | |
| id += 1 | |
| return result | |
| def to_lines(self): | |
| """Generates lines of ASCII-encoded hexadecimal strings. | |
| Yields: | |
| One line of a hex string. | |
| """ | |
| for id in range(0, 64): | |
| hexstrs = [bytes(util.bytes_to_hex( | |
| bytes(self.get_properties(id))), encoding='ascii')] | |
| for note in range(0, 32): | |
| pitch, waveform, volume, effect = self.get_note(id, note) | |
| hexstrs.append(bytes(util.bytes_to_hex( | |
| bytes([pitch, waveform << 4 | volume])), encoding='ascii')) | |
| hexstrs.append(bytes(util.bytes_to_hex( | |
| bytes([effect]))[1], encoding='ascii')) | |
| yield b''.join(hexstrs) + b'\n' | |
| def get_note(self, id, note): | |
| """Gets a note from a pattern. | |
| pitch is a value (0-63), representing the notes on a chromatic scale | |
| from c-0 to d#-5. | |
| waveform is one of the WAVEFORM_* constants (0-15). | |
| volume is 0-7: 0 is off, 7 is loudest. | |
| effect is one of the EFFECT_* constants (0-7). | |
| Args: | |
| id: The pattern ID. (0-63) | |
| note: The note number. (0-31) | |
| Returns: | |
| A tuple: (pitch, waveform, volume, effect). | |
| """ | |
| lsb = self._data[id * 68 + note * 2] | |
| msb = self._data[id * 68 + note * 2 + 1] | |
| pitch = lsb & 0x3f | |
| waveform = ((msb & 0x80) >> 4) | ( | |
| (msb & 0x01) << 2) | ((lsb & 0xc0) >> 6) | |
| volume = (msb & 0x0e) >> 1 | |
| effect = (msb & 0x70) >> 4 | |
| return pitch, waveform, volume, effect | |
| def set_note(self, id, note, pitch=None, waveform=None, volume=None, | |
| effect=None): | |
| """Sets a note in a pattern. | |
| (See get_note() for definitions.) | |
| Args: | |
| id: The pattern ID. (0-63) | |
| note: The note number. (0-31) | |
| pitch: The pitch value, or None to leave unchanged. (0-63) | |
| waveform: The waveform type, or None to leave unchanged. (0-15) | |
| volume: The volume level, or None to leave unchanged. (0-7) | |
| effect: The effect type, or None to leave unchanged. (0-7) | |
| """ | |
| lsb = self._data[id * 68 + note * 2] | |
| msb = self._data[id * 68 + note * 2 + 1] | |
| if pitch is not None: | |
| assert 0 <= pitch <= 63 | |
| lsb = (lsb & 0xc0) | pitch | |
| if waveform is not None: | |
| assert 0 <= waveform <= 15 | |
| lsb = (lsb & 0x3f) | ((waveform & 3) << 6) | |
| msb = (msb & 0x7e) | ((waveform & 4) >> 2) | ((waveform & 8) << 4) | |
| if volume is not None: | |
| assert 0 <= volume <= 7 | |
| msb = (msb & 0xf1) | (volume << 1) | |
| if effect is not None: | |
| assert 0 <= effect <= 7 | |
| msb = (msb & 0x8f) | (effect << 4) | |
| self._data[id * 68 + note * 2] = lsb | |
| self._data[id * 68 + note * 2 + 1] = msb | |
| def get_properties(self, id): | |
| """Gets properties for a pattern. | |
| editor_mode is 0 for pitch mode, 1 for note mode. | |
| note_duration is the duration of each note, in 1/128ths of a second. | |
| (0-255) | |
| loop_start is the loop range start, as a note number. (0-63) | |
| loop_end is the loop range end, as a note number. (0-63) | |
| Args: | |
| id: The pattern ID. (0-63) | |
| Returns: | |
| A tuple: (editor_mode, note_duration, loop_start, loop_end). | |
| """ | |
| return (self._data[id * 68 + 64], | |
| self._data[id * 68 + 65], | |
| self._data[id * 68 + 66], | |
| self._data[id * 68 + 67]) | |
| def set_properties(self, id, editor_mode=None, note_duration=None, | |
| loop_start=None, loop_end=None): | |
| """Sets properteis for a pattern. | |
| Args: | |
| id: The pattern ID. (0-63) | |
| editor_mode: 0 for pitch mode, 1 for note mode, None to leave | |
| unchanged. | |
| note_duration: The duration for each note in the pattern, in 1/128ths | |
| of a second. (0-255) None to leave unchanged. | |
| loop_start: The loop range start, as a note number (0-63). None to | |
| leave unchanged. | |
| loop_end: The loop range end, as a note number (0-63). None to | |
| leave unchanged. | |
| """ | |
| # (The asserts are only appropriate if the cart uses the sfx memory | |
| # for actual sfx, which not all carts do. Keeping them for | |
| # documentation purposes.) | |
| if editor_mode is not None: | |
| # assert 0 <= editor_mode <= 1 | |
| self._data[id * 68 + 64] = editor_mode | |
| if note_duration is not None: | |
| # assert 0 <= note_duration <= 255 | |
| self._data[id * 68 + 65] = note_duration | |
| if loop_start is not None: | |
| # assert 0 <= loop_start <= 63 | |
| self._data[id * 68 + 66] = loop_start | |
| if loop_end is not None: | |
| # assert 0 <= loop_end <= 63 | |
| self._data[id * 68 + 67] = loop_end |