Skip to content

Commit

Permalink
fix: move code out of __init__
Browse files Browse the repository at this point in the history
  • Loading branch information
vberlier committed Apr 10, 2022
1 parent 13d823b commit 6dd5f98
Show file tree
Hide file tree
Showing 2 changed files with 303 additions and 301 deletions.
302 changes: 1 addition & 301 deletions pynbs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,304 +1,4 @@
__version__ = "1.0.0-beta.0"


from collections import namedtuple
from struct import Struct

__all__ = [
"read",
"new_file",
"Parser",
"Writer",
"File",
"Header",
"Note",
"Layer",
"Instrument",
]

CURRENT_NBS_VERSION = 5

BYTE = Struct("<B")
SHORT = Struct("<H")
SSHORT = Struct("<h")
INT = Struct("<I")


Instrument = namedtuple("Instrument", ["id", "name", "file", "pitch", "press_key"])


class Note(
namedtuple(
"Note", ["tick", "layer", "instrument", "key", "velocity", "panning", "pitch"]
)
):
def __new__(cls, tick, layer, instrument, key, velocity=100, panning=0, pitch=0):
return super().__new__(
cls, tick, layer, instrument, key, velocity, panning, pitch
)


class Layer(namedtuple("Layer", ["id", "name", "lock", "volume", "panning"])):
def __new__(cls, id, name="", lock=False, volume=100, panning=0):
return super().__new__(cls, id, name, lock, volume, panning)


def read(filename):
with open(filename, "rb") as buff:
return Parser(buff).read_file()


def new_file(**header):
return File(Header(**header), [], [Layer(0, "", False, 100, 0)], [])


class Header(object):
def __init__(self, **header):
header_values = {
"version": header.get("version", CURRENT_NBS_VERSION),
"default_instruments": header.get("default_instruments", 16),
"song_length": header.get("song_length", 0),
"song_layers": header.get("song_layers", 0),
"song_name": header.get("song_name", ""),
"song_author": header.get("song_author", ""),
"original_author": header.get("original_author", ""),
"description": header.get("description", ""),
"tempo": header.get("tempo", 10.0),
"auto_save": header.get("auto_save", False),
"auto_save_duration": header.get("auto_save_duration", 10),
"time_signature": header.get("time_signature", 4),
"minutes_spent": header.get("minutes_spent", 0),
"left_clicks": header.get("left_clicks", 0),
"right_clicks": header.get("right_clicks", 0),
"blocks_added": header.get("blocks_added", 0),
"blocks_removed": header.get("blocks_removed", 0),
"song_origin": header.get("song_origin", ""),
"loop": header.get("loop", False),
"max_loop_count": header.get("max_loop_count", 0),
"loop_start": header.get("loop_start", 0),
}

for key, value in header_values.items():
setattr(self, key, value)


class File(object):
def __init__(self, header, notes, layers, instruments):
self.header = header
self.notes = notes
self.layers = layers
self.instruments = instruments

def update_header(self, version):
self.header.version = version
if self.notes:
self.header.song_length = self.notes[-1].tick
self.header.song_layers = len(self.layers)

def save(self, filename, version=CURRENT_NBS_VERSION):
self.update_header(version)
with open(filename, "wb") as buff:
Writer(buff).encode_file(self, version)

def __iter__(self):
if not self.notes:
return
chord = []
current_tick = self.notes[0].tick

for note in sorted(self.notes, key=lambda n: n.tick):
if note.tick == current_tick:
chord.append(note)
else:
chord.sort(key=lambda n: n.layer)
yield current_tick, chord
current_tick, chord = note.tick, [note]
yield current_tick, chord


class Parser(object):
def __init__(self, buff):
self.buffer = buff

def read_file(self):
values = self.parse_header()
header = Header(**values)
version = values["version"]
return File(
header,
list(self.parse_notes(version)),
list(self.parse_layers(header.song_layers, version)),
list(self.parse_instruments(version)),
)

def read_numeric(self, fmt):
return fmt.unpack(self.buffer.read(fmt.size))[0]

def read_string(self):
length = self.read_numeric(INT)
return self.buffer.read(length).decode(encoding="cp1252")

def jump(self):
value = -1
while True:
jump = self.read_numeric(SHORT)
if not jump:
break
value += jump
yield value

def parse_header(self):
song_length = self.read_numeric(SHORT)
if song_length == 0:
# A song length of 0 indicates the Open Note Block Studio format
version = self.read_numeric(BYTE)
else:
version = 0

return {
"version": version,
"default_instruments": self.read_numeric(BYTE) if version > 0 else 10,
"song_length": self.read_numeric(SHORT) if version >= 3 else song_length,
"song_layers": self.read_numeric(SHORT),
"song_name": self.read_string(),
"song_author": self.read_string(),
"original_author": self.read_string(),
"description": self.read_string(),
"tempo": self.read_numeric(SHORT) / 100.0,
"auto_save": self.read_numeric(BYTE) == 1,
"auto_save_duration": self.read_numeric(BYTE),
"time_signature": self.read_numeric(BYTE),
"minutes_spent": self.read_numeric(INT),
"left_clicks": self.read_numeric(INT),
"right_clicks": self.read_numeric(INT),
"blocks_added": self.read_numeric(INT),
"blocks_removed": self.read_numeric(INT),
"song_origin": self.read_string(),
"loop": self.read_numeric(BYTE) == 1 if version >= 4 else False,
"max_loop_count": self.read_numeric(BYTE) if version >= 4 else 0,
"loop_start": self.read_numeric(SHORT) if version >= 4 else 0,
}

def parse_notes(self, version):
for current_tick in self.jump():
for current_layer in self.jump():
instrument = self.read_numeric(BYTE)
key = self.read_numeric(BYTE)
velocity = self.read_numeric(BYTE) if version >= 4 else 100
panning = self.read_numeric(BYTE) - 100 if version >= 4 else 0
pitch = self.read_numeric(SSHORT) if version >= 4 else 0
yield Note(
current_tick,
current_layer,
instrument,
key,
velocity,
panning,
pitch,
)

def parse_layers(self, layers_count, version):
for i in range(layers_count):
name = self.read_string()
lock = self.read_numeric(BYTE) == 1 if version >= 4 else False
volume = self.read_numeric(BYTE)
panning = self.read_numeric(BYTE) - 100 if version >= 2 else 0
yield Layer(i, name, lock, volume, panning)

def parse_instruments(self, version):
for i in range(self.read_numeric(BYTE)):
name = self.read_string()
sound_file = self.read_string()
pitch = self.read_numeric(BYTE)
press_key = self.read_numeric(BYTE) == 1
yield Instrument(i, name, sound_file, pitch, press_key)


class Writer(object):
def __init__(self, buff):
self.buffer = buff

def encode_file(self, nbs_file, version):
self.write_header(nbs_file, version)
self.write_notes(nbs_file, version)
self.write_layers(nbs_file, version)
self.write_instruments(nbs_file, version)

def encode_numeric(self, fmt, value):
self.buffer.write(fmt.pack(value))

def encode_string(self, value):
self.encode_numeric(INT, len(value))
self.buffer.write(value.encode(encoding="cp1252"))

def write_header(self, nbs_file, version):
header = nbs_file.header

if version > 0:
self.encode_numeric(SHORT, 0)
self.encode_numeric(BYTE, version)
self.encode_numeric(BYTE, header.default_instruments)
else:
self.encode_numeric(SHORT, header.song_length)
if version >= 3:
self.encode_numeric(SHORT, header.song_length)
self.encode_numeric(SHORT, header.song_layers)
self.encode_string(header.song_name)
self.encode_string(header.song_author)
self.encode_string(header.original_author)
self.encode_string(header.description)

self.encode_numeric(SHORT, int(header.tempo * 100))
self.encode_numeric(BYTE, int(header.auto_save))
self.encode_numeric(BYTE, header.auto_save_duration)
self.encode_numeric(BYTE, header.time_signature)

self.encode_numeric(INT, header.minutes_spent)
self.encode_numeric(INT, header.left_clicks)
self.encode_numeric(INT, header.right_clicks)
self.encode_numeric(INT, header.blocks_added)
self.encode_numeric(INT, header.blocks_removed)
self.encode_string(header.song_origin)

if version >= 4:
self.encode_numeric(BYTE, int(header.loop))
self.encode_numeric(BYTE, header.max_loop_count)
self.encode_numeric(SHORT, header.loop_start)

def write_notes(self, nbs_file, version):
current_tick = -1

for tick, chord in nbs_file:
self.encode_numeric(SHORT, tick - current_tick)
current_tick = tick
current_layer = -1

for note in chord:
self.encode_numeric(SHORT, note.layer - current_layer)
current_layer = note.layer
self.encode_numeric(BYTE, note.instrument)
self.encode_numeric(BYTE, note.key)
if version >= 4:
self.encode_numeric(BYTE, note.velocity)
self.encode_numeric(BYTE, note.panning + 100)
self.encode_numeric(SSHORT, note.pitch)

self.encode_numeric(SHORT, 0)
self.encode_numeric(SHORT, 0)

def write_layers(self, nbs_file, version):
for layer in nbs_file.layers:
self.encode_string(layer.name)
if version >= 4:
self.encode_numeric(BYTE, int(layer.lock))
self.encode_numeric(BYTE, layer.volume)
if version >= 2:
self.encode_numeric(BYTE, layer.panning + 100)

def write_instruments(self, nbs_file, version):
self.encode_numeric(BYTE, len(nbs_file.instruments))
for instrument in nbs_file.instruments:
self.encode_string(instrument.name)
self.encode_string(instrument.file)
self.encode_numeric(BYTE, instrument.pitch)
self.encode_numeric(BYTE, int(instrument.press_key))
from .file import *
Loading

0 comments on commit 6dd5f98

Please sign in to comment.