Skip to content

Commit

Permalink
Also use black and mypi for linting. Add type information.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmechnich committed Oct 12, 2022
1 parent 3948eff commit 6ba22ab
Show file tree
Hide file tree
Showing 9 changed files with 411 additions and 352 deletions.
47 changes: 24 additions & 23 deletions p600_syxdump
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,55 @@ import sys

import p600syx

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Dump Prophet-600 presets.')
parser.add_argument('-d', '--debug', action='store_true',
help='turn on debug output')
parser.add_argument('-p', '--program', type=int, default=-1,
help='filter for program number')
parser.add_argument('infile', nargs='?',
help='input sysex file')
args = parser.parse_args()
if __name__ == "__main__":
argparser = argparse.ArgumentParser(description="Dump Prophet-600 presets.")
argparser.add_argument(
"-d", "--debug", action="store_true", help="turn on debug output"
)
argparser.add_argument(
"-p", "--program", type=int, default=-1, help="filter for program number"
)
argparser.add_argument("infile", nargs="?", help="input sysex file")
args = argparser.parse_args()

if args.infile:
if not os.path.exists(args.infile):
print(f'File {args.infile} not found, exiting')
print(f"File {args.infile} not found, exiting")
sys.exit(1)
if args.debug:
print('Reading from file {args.infile}', file=sys.stderr)
with open(args.infile, 'rb') as f:
data = f.read()
print("Reading from file {args.infile}", file=sys.stderr)
with open(args.infile, "rb") as f:
raw_data = f.read()
else:
if args.debug:
print('Reading from stdin', file=sys.stderr)
data = sys.stdin.buffer.read()
print("Reading from stdin", file=sys.stderr)
raw_data = sys.stdin.buffer.read()

# Split SysEx stream at the terminating 0xf7, drop empty messages
msgs = [ msg for msg in data.split(b'\xf7') if len(msg) ]
msgs = [msg for msg in raw_data.split(b"\xf7") if len(msg)]
if args.debug:
print(f'Found {len(msgs)} messages')
print(f"Found {len(msgs)} messages")

for i, m in enumerate(msgs):
parser = p600syx.factory.get_parser(m)
if not parser:
print(f'No suitable parser found for message {i}')
print(f"No suitable parser found for message {i}")
continue
if args.debug:
print(f'Using {parser.name} for message {i}', file=sys.stderr)
print(f"Using {parser.name} for message {i}", file=sys.stderr)
program, parameters, data = parser.decode(m)
if args.program > -1 and program != args.program:
continue
print()
print(f'{"Program number":30}: {program:5}')
for name, value in parameters:
if name.startswith('Patch Name') and value > 0:
print(f'{name:30}: {value:5} {repr(chr(value))}')
if name.startswith("Patch Name") and value > 0:
print(f"{name:30}: {value:5} {repr(chr(value))}")
else:
print(f'{name:30}: {value:5}')
print(f"{name:30}: {value:5}")

if args.debug:
print()
print(f'Data length: {len(data)}', file=sys.stderr)
print(f"Data length: {len(data)}", file=sys.stderr)
print(data, file=sys.stderr)
print(file=sys.stderr)
16 changes: 12 additions & 4 deletions p600syx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
the Sequential Circuits Prophet-600 analog synthesizer.
"""

from typing import Optional

from .sysex_parser import SysExParser
from .sequential_sysex_parser import SequentialSysExParser
from .gligli_sysex_parser import GliGliSysExParser
from .imogen7_sysex_parser import Imogen7SysExParser
from .imogen8_sysex_parser import Imogen8SysExParser


class SysExParserFactory:
"""
This factory class is the provider for all registered parser classes.
"""
def __init__(self):
self.parsers = {}
def register_parser(self, parser):

def __init__(self) -> None:
self.parsers: dict[str, SysExParser] = {}

def register_parser(self, parser: SysExParser) -> None:
"""
This function takes a parser object as argument and registers it
with the factory.
Expand All @@ -25,7 +31,8 @@ def register_parser(self, parser):
"""
if not parser.name in self.parsers:
self.parsers[parser.name] = parser
def get_parser(self, msg):

def get_parser(self, msg: bytes) -> Optional[SysExParser]:
"""
This function returns a suitable parser for decoding a SysEx
messages if available, None otherwise.
Expand All @@ -41,6 +48,7 @@ def get_parser(self, msg):
return parser
return None


factory = SysExParserFactory()
factory.register_parser(SequentialSysExParser())
factory.register_parser(GliGliSysExParser())
Expand Down
1 change: 1 addition & 0 deletions p600syx/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module contains error classes used within the related modules.
"""


class ParseError(Exception):
"""
This class represents a generic parse error.
Expand Down
168 changes: 86 additions & 82 deletions p600syx/gligli_sysex_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,86 +4,91 @@
synthesizer.
"""

from .sysex_parser import SysExParser
from .error import ParseError

class GliGliSysExParser:

class GliGliSysExParser(SysExParser):
"""
This class implements the decoding of MIDI SysEx dumps created using
the original GliGli mod for the Sequential Circuits Prophet-600 analog
synthesizer.
"""

parameters = [
('Osc A Frequency' , 2),
('Osc A Volume' , 2),
('Osc A Pulse Width' , 2),
('Osc B Frequency' , 2),
('Osc B Volume' , 2),
('Osc B Pulse Width' , 2),
('Osc B Fine' , 2),
('Cutoff' , 2),
('Resonance' , 2),
('Filter Envelope Amount' , 2),
('Filter Release' , 2),
('Filter Sustain' , 2),
('Filter Decay' , 2),
('Filter Attack' , 2),
('Amp Release' , 2),
('Amp Sustain' , 2),
('Amp Decay' , 2),
('Amp Attack' , 2),
('Poly Mod Filter Amount' , 2),
('Poly Mod Osc B Amount' , 2),
('LFO Frequency' , 2),
('LFO Amount' , 2),
('Glide' , 2),
('Amp Velocity' , 2),
('Filter Velocity' , 2),
('Osc A Saw' , 1),
('Osc A Triangle' , 1),
('Osc A Sqr' , 1),
('Osc A Saw' , 1),
('Osc A Triangle' , 1),
('Osc A Sqr' , 1),
('Sync' , 1),
('Poly Mod Osc A Destination' , 1),
('Poly Mod Filter Destination' , 1),
('LFO Shape' , 1),
('LFO Speed Range' , 1),
('LFO Mode Destination' , 1),
('Keyboard Filter Tracking' , 1),
('Filter EG Exponential/Linear', 1),
('Filter EG Fast/Slow' , 1),
('Amp EG Exponential/Linear' , 1),
('Amp EG Fast/Slow' , 1),
('Unison' , 1),
('Assigner Priority Mode' , 1),
('Pitch Bender Semitones' , 1),
('Pitch Bender Target' , 1),
('Modulation Wheel Range' , 1),
('Osc Pitch Mode' , 1),
('Modulation Delay' , 2),
('Vibrato Frequency' , 2),
('Vibrato Amount' , 2),
('Unison Detune' , 2),
('Arpeggiator/Sequencer clock' , 2),
('Modulation Wheel Target' , 1),
('(padding)' , 1),
('Voice Pattern (1/6 voices)' , 1),
('Voice Pattern (2/6 voices)' , 1),
('Voice Pattern (3/6 voices)' , 1),
('Voice Pattern (4/6 voices)' , 1),
('Voice Pattern (5/6 voices)' , 1),
('Voice Pattern (6/6 voices)' , 1),
("Osc A Frequency", 2),
("Osc A Volume", 2),
("Osc A Pulse Width", 2),
("Osc B Frequency", 2),
("Osc B Volume", 2),
("Osc B Pulse Width", 2),
("Osc B Fine", 2),
("Cutoff", 2),
("Resonance", 2),
("Filter Envelope Amount", 2),
("Filter Release", 2),
("Filter Sustain", 2),
("Filter Decay", 2),
("Filter Attack", 2),
("Amp Release", 2),
("Amp Sustain", 2),
("Amp Decay", 2),
("Amp Attack", 2),
("Poly Mod Filter Amount", 2),
("Poly Mod Osc B Amount", 2),
("LFO Frequency", 2),
("LFO Amount", 2),
("Glide", 2),
("Amp Velocity", 2),
("Filter Velocity", 2),
("Osc A Saw", 1),
("Osc A Triangle", 1),
("Osc A Sqr", 1),
("Osc A Saw", 1),
("Osc A Triangle", 1),
("Osc A Sqr", 1),
("Sync", 1),
("Poly Mod Osc A Destination", 1),
("Poly Mod Filter Destination", 1),
("LFO Shape", 1),
("LFO Speed Range", 1),
("LFO Mode Destination", 1),
("Keyboard Filter Tracking", 1),
("Filter EG Exponential/Linear", 1),
("Filter EG Fast/Slow", 1),
("Amp EG Exponential/Linear", 1),
("Amp EG Fast/Slow", 1),
("Unison", 1),
("Assigner Priority Mode", 1),
("Pitch Bender Semitones", 1),
("Pitch Bender Target", 1),
("Modulation Wheel Range", 1),
("Osc Pitch Mode", 1),
("Modulation Delay", 2),
("Vibrato Frequency", 2),
("Vibrato Amount", 2),
("Unison Detune", 2),
("Arpeggiator/Sequencer clock", 2),
("Modulation Wheel Target", 1),
("(padding)", 1),
("Voice Pattern (1/6 voices)", 1),
("Voice Pattern (2/6 voices)", 1),
("Voice Pattern (3/6 voices)", 1),
("Voice Pattern (4/6 voices)", 1),
("Voice Pattern (5/6 voices)", 1),
("Voice Pattern (6/6 voices)", 1),
]

def __init__(self, name='GliGliSysExParser'):
self.name = name
self.header = b'\xf0\x00\x61\x16\x01'
self.format_id = b'\xa5\x16\x61\x00'
self.format_version = b'\x02'
def __init__(self, name: str = "GliGliSysExParser"):
super().__init__(name)
self.header = b"\xf0\x00\x61\x16\x01"
self.format_id = b"\xa5\x16\x61\x00"
self.format_version = b"\x02"

@classmethod
def pop_and_format(cls, parameter, data):
def pop_and_format(
cls, parameter: tuple[str, int], data: list[int]
) -> tuple[str, int]:
"""
This internal function pops up to two bytes of data from a list and
converts it to the appropriate format for a given parameter.
Expand All @@ -99,16 +104,16 @@ def pop_and_format(cls, parameter, data):
try:
lsb, msb = (
data.pop(0) if len(data) else 0,
data.pop(0) if len(data) and nbytes == 2 else 0
data.pop(0) if len(data) and nbytes == 2 else 0,
)
except:
print(f'Error while reading parameter {name}')
print(f"Error while reading parameter {name}")
raise
value = msb<<8 | lsb
value = msb << 8 | lsb
return (name, value)

@classmethod
def unpack(cls, data):
def unpack(cls, data: bytes) -> list[int]:
"""
This internal function decodes five 7-bit bytes of raw data packed
into four full 8-bit bytes.
Expand All @@ -124,11 +129,11 @@ def unpack(cls, data):
ret = []
while len(data):
for shift in range(4):
ret.append(data[shift] + 128 * (data[4]>>shift & 1))
ret.append(data[shift] + 128 * (data[4] >> shift & 1))
data = data[5:]
return ret

def can_decode(self, msg):
def can_decode(self, msg: bytes) -> bool:
"""
This function checks if the parser can decode a given MIDI SysEx dump
using the header of the data.
Expand All @@ -140,16 +145,15 @@ def can_decode(self, msg):
True if parser can decode dump, False otherwise.
"""
if msg.startswith(self.header):
data = self.unpack(msg[len(self.header):len(self.header)+10]
)
data = self.unpack(msg[len(self.header) : len(self.header) + 10])
# pop program number
_ = data.pop(0)
magic = bytes(data[:5])
if magic == self.format_id + self.format_version:
return True
return False

def decode(self, msg):
def decode(self, msg: bytes) -> tuple[int, list[tuple[str, int]], list[int]]:
"""
This function decodes a MIDI SysEx dump created using
the original GliGli mod for the Sequential Circuits Prophet-600 analog
Expand All @@ -166,16 +170,16 @@ def decode(self, msg):
parameters = []
if not msg.startswith(self.header):
raise ParseError(
f'Header mismatch: expected {self.header},'
f'got {msg[:len(self.header)]}'
f"Header mismatch: expected {self.header!r},"
f"got {msg[:len(self.header)]!r}"
)
data = self.unpack(msg[len(self.header):])
data = self.unpack(msg[len(self.header) :])
program = data.pop(0)
magic = bytes(data[:5])
if magic != self.format_id + self.format_version:
raise ParseError(
f'Storage format ID mismatch:'
f' expected {self.format_id + self.format_version}, got {magic}'
f"Storage format ID mismatch:"
f" expected {self.format_id + self.format_version!r}, got {magic!r}"
)
data = data[5:]
for param in self.parameters:
Expand Down
Loading

0 comments on commit 6ba22ab

Please sign in to comment.