- tmp/city_dreams/Gen-Fil-Dis-Ech/1664.sunvox
- virtualenv -p /usr/bin/python3.6 .
- source bin/activate
- pip install -r requirements.txt
- {...}
- deactivate
bin | |
include | |
lib | |
share | |
data | |
*.pyc | |
tmp | |
.sunvox* | |
tb03.sunvox |
""" | |
- script to decompile `city_dreams.sunvox` into constituent parts | |
""" | |
if __name__=="__main__": | |
filename, output_dir = "city_dreams.sunvox", "tmp" | |
dirname=filename.split("/")[-1].split(".")[0] | |
import os | |
os.makedirs(f"{output_dir}/{dirname}", exist_ok=True) | |
from rv.api import read_sunvox_file | |
proj=read_sunvox_file(filename) | |
props={"bpm": proj.initial_bpm, "tpl": proj.initial_tpl} | |
from rv.tools.patch_decompiler import decompile, dump | |
patches=decompile(proj) | |
npatches=len(list(set([patch["name"] for patch in patches]))) | |
nversions=len(patches) | |
print (f"dumping {npatches} patches [{nversions} versions] to {output_dir}/{dirname}") | |
for patch in patches: | |
dump(props, patch, output_dir, dirname) |
""" | |
- script to modify `city_dreams_bass.sunvox` and initialise it as `tb03_base.sunvox` | |
""" | |
from rv.api import read_sunvox_file | |
import enum, yaml | |
Ignore=["Echo"] | |
Output="Output" | |
Overrides=yaml.load(""" | |
Generator: | |
waveform: 1 | |
attack: 8 | |
release: 64 | |
sustain: false | |
Filter: | |
freq: 256 | |
""", Loader=yaml.FullLoader) | |
def detach_modules(proj): | |
for i in range(len(proj.modules)-1): | |
proj.disconnect(proj.modules[i+1], | |
proj.modules[i]) | |
def serialise_chain(modules): | |
def apply_overrides(fn): | |
def wrapped(mod, key, value): | |
if (mod.name in Overrides and | |
key in Overrides[mod.name]): | |
return Overrides[mod.name][key] | |
return fn(mod, key, value) | |
return wrapped | |
@apply_overrides | |
def filter_value(mod, key, value): | |
return value.value if isinstance(value, enum.Enum) else value | |
def init_module(mod): | |
return "rv.modules.%s" % mod.name.lower() | |
def init_params(mod): | |
return {key:filter_value(mod, key, value) | |
for key, value in mod.controller_values.items()} | |
return [{"class": mod.name, | |
"module": init_module(mod), | |
"params": init_params(mod)} | |
for mod in reversed(modules) | |
if mod.name!=Output] | |
if __name__=="__main__": | |
proj=read_sunvox_file("city_dreams_bass.sunvox") | |
detach_modules(proj) | |
modules=[mod.clone() | |
for mod in proj.modules | |
if mod.name not in Ignore] | |
params=serialise_chain(modules) | |
with open("tb03.yaml", 'w') as f: | |
f.write(yaml.safe_dump(params, | |
default_flow_style=False)) | |
from rv.api import Project as RVProject | |
from rv.pattern import Pattern as RVPattern | |
from rv.note import Note as RVNote | |
import re, yaml | |
Generator, Filter, Freq, Release = "Generator", "Filter", "freq", "release" | |
CtlMultiplier=2**8 | |
def hungarorise(name): | |
return "".join([tok.capitalize() | |
for tok in name.split("/")]) | |
def note_sym_to_int(sym, notes="CcDdEFfGgAaB"): | |
i, j = sym[0], int(sym[1]) | |
return 1+(j*len(notes))+notes.index(i) | |
class Modules(list): | |
def init_module(self, kwargs): | |
def import_class(modname, classname): | |
mod=__import__(modname, fromlist=[classname]) | |
return getattr(mod, classname) | |
klass=import_class(kwargs["module"], | |
kwargs["class"]) | |
mod=klass() | |
for key, value in kwargs["params"].items(): | |
mod.set_raw(key, value) | |
return mod | |
def __init__(self, modules): | |
list.__init__(self, [self.init_module(mod) | |
for mod in modules]) | |
def arrange(self, dx=128): | |
offset=len(self) | |
for i, mod in enumerate(reversed(self)): | |
mod.x=(offset-i)*dx | |
def attach(self, proj): | |
for i, mod in enumerate(reversed(self)): | |
proj.attach_module(mod) | |
proj.connect(proj.modules[i+1], | |
proj.modules[i]) | |
def mod_index(self, _name, offset=2): | |
name=hungarorise(_name) | |
modnames=[mod.name | |
for mod in reversed(self)] | |
if name not in modnames: | |
raise RuntimeError("mod '%s' not found" % name) | |
return offset+modnames.index(name) | |
def ctl_index(self, key): | |
modules={mod.name:mod | |
for mod in self} | |
tokens=key.split("/") | |
modname, ctlname = hungarorise(tokens[0]), tokens[1].lower() | |
if modname not in modules: | |
raise RuntimeError("mod '%s' not found" % modname) | |
mod=modules[modname] | |
ctlnames=list(mod.controllers.keys()) | |
if ctlname not in ctlnames: | |
raise RuntimeError("ctl '%s' not found in mod '%s'" % (modname, | |
ctlname)) | |
return 1+ctlnames.index(ctlname) | |
def set_ctl(self, key, value): | |
modules={mod.name:mod | |
for mod in self} | |
tokens=key.split("/") | |
modname, ctlname = hungarorise(tokens[0]), tokens[1].lower() | |
if modname not in modules: | |
raise RuntimeError("mod '%s' not found" % modname) | |
mod=modules[modname] | |
mod.set_raw(ctlname, value) | |
class TracksBase(dict): | |
def __init__(self, keyfn, lines): | |
dict.__init__(self) | |
self.keyfn=keyfn | |
self.lines=lines | |
def spawn(self, key): | |
if key not in self: | |
self[key]=[None | |
for i in range(self.lines)] | |
return self | |
def add(self, group): | |
notes, i = group | |
for note in notes: | |
key=self.keyfn(note) | |
self.spawn(key) | |
self[key][i]=RVNote(**note) | |
return self | |
def flatten(self): | |
return [self[key] | |
for key in sorted(self.keys())] | |
def render(self, ast): | |
for group in ast: | |
self.add(group) | |
pat=RVPattern(lines=self.lines, | |
tracks=len(self)) | |
grid=self.flatten() | |
def notefn(self, i, j): | |
return grid[j][i] if grid[j][i] else RVNote() | |
pat.set_via_fn(notefn) | |
return pat | |
""" | |
- could have a series of different modules here | |
- simplest example is one track per module | |
- DrumSynth would need one track per module/note combination (since want to play more than one note in single epoch) | |
- chords would need something different - multiple tracks but not confined to specific notes | |
- could maybe have something which dynamically spawns new tracks but seems over- complicated | |
""" | |
class ModTracks(TracksBase): | |
@classmethod | |
def KeyFn(self, note): | |
if "ctl" in note: | |
return "%s/%s" % (note["module"], | |
note["ctl"]) | |
return str(note["module"]) | |
def __init__(self, lines): | |
TracksBase.__init__(self, | |
lines=lines, | |
keyfn=ModTracks.KeyFn) | |
""" | |
- `Generator` ref should be abstracted | |
""" | |
def dsl(modules, tracks, accents, lines): | |
def is_note(sym, accents): | |
suffix="".join(["\\%s?" % accent | |
for accent in accents.keys()]) | |
pattern="^\\D\\d%s$" % suffix | |
return re.search(pattern, sym)!=None | |
def add_note(ast, sym, accents, i): | |
notes=[] | |
beat={"note": note_sym_to_int(sym), | |
"module": modules.mod_index(Generator), | |
"vel": 60} | |
notes.append(beat) | |
for accentkey in sym[2:]: | |
for ctlkey in accents[accentkey]: | |
modname=ctlkey.split("/")[0] | |
accent={"module": modules.mod_index(modname), | |
"ctl": modules.ctl_index(ctlkey)*CtlMultiplier, | |
"val": accents[accentkey][ctlkey]} | |
notes.append(accent) | |
ast.append((notes, i)) | |
def itergen(ast, track, _lines, _i=0): | |
lines=int(_lines/len(track)) | |
for j, sym in enumerate(track): | |
i=_i+j*lines | |
if isinstance(sym, list): | |
itergen(ast, sym, lines, i) | |
elif is_note(sym, accents): | |
add_note(ast, sym, accents, i) | |
ast=[] | |
for track in tracks: | |
itergen(ast, track, lines) | |
return ast | |
SampleAccents=yaml.load(""" | |
--- | |
"|": | |
Filter/freq: 1024 | |
Generator/release: 3072 | |
"!": | |
Filter/freq: 2048 | |
Generator/release: 6144 | |
"?": | |
Filter/freq: 512 | |
Generator/release: 6144 | |
""", Loader=yaml.FullLoader) | |
if __name__=="__main__": | |
try: | |
modules=Modules(yaml.load(open("tb03.yaml").read(), | |
Loader=yaml.FullLoader)) | |
modules.arrange() | |
proj=RVProject() | |
modules.attach(proj) | |
from lis import parse | |
program, lines = parse("((C0| C0| ~ C0! C0? ~ C0| (C0| C1!)))"), 16 | |
tracks=ModTracks(lines=lines) | |
ast=dsl(modules, program, SampleAccents, lines=lines) | |
pat=tracks.render(ast) | |
proj.patterns.append(pat) | |
with open("tb03.sunvox", 'wb') as f: | |
proj.write_to(f) | |
except RuntimeError as error: | |
print ("Error: %s" % str(error)) |
################ Lispy: Scheme Interpreter in Python | |
## (c) Peter Norvig, 2010-16; See http://norvig.com/lispy.html | |
from __future__ import division | |
import math | |
import operator as op | |
################ Types | |
Symbol = str # A Lisp Symbol is implemented as a Python str | |
List = list # A Lisp List is implemented as a Python list | |
Number = (int, float) # A Lisp Number is implemented as a Python int or float | |
################ Parsing: parse, tokenize, and read_from_tokens | |
def parse(program): | |
"Read a Scheme expression from a string." | |
return read_from_tokens(tokenize(program)) | |
def tokenize(s): | |
"Convert a string into a list of tokens." | |
return s.replace('(',' ( ').replace(')',' ) ').split() | |
def read_from_tokens(tokens): | |
"Read an expression from a sequence of tokens." | |
if len(tokens) == 0: | |
raise SyntaxError('unexpected EOF while reading') | |
token = tokens.pop(0) | |
if '(' == token: | |
L = [] | |
while tokens[0] != ')': | |
L.append(read_from_tokens(tokens)) | |
tokens.pop(0) # pop off ')' | |
return L | |
elif ')' == token: | |
raise SyntaxError('unexpected )') | |
else: | |
return atom(token) | |
def atom(token): | |
"Numbers become numbers; every other token is a symbol." | |
try: return int(token) | |
except ValueError: | |
try: return float(token) | |
except ValueError: | |
return Symbol(token) | |
################ Environments | |
def standard_env(): | |
"An environment with some Scheme standard procedures." | |
env = Env() | |
env.update(vars(math)) # sin, cos, sqrt, pi, ... | |
env.update({ | |
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, | |
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, | |
'abs': abs, | |
'append': op.add, | |
# 'apply': apply, | |
'begin': lambda *x: x[-1], | |
'car': lambda x: x[0], | |
'cdr': lambda x: x[1:], | |
'cons': lambda x,y: [x] + y, | |
'eq?': op.is_, | |
'equal?': op.eq, | |
'length': len, | |
'list': lambda *x: list(x), | |
'list?': lambda x: isinstance(x,list), | |
'map': map, | |
'max': max, | |
'min': min, | |
'not': op.not_, | |
'null?': lambda x: x == [], | |
'number?': lambda x: isinstance(x, Number), | |
'procedure?': callable, | |
'round': round, | |
'symbol?': lambda x: isinstance(x, Symbol), | |
}) | |
return env | |
class Env(dict): | |
"An environment: a dict of {'var':val} pairs, with an outer Env." | |
def __init__(self, parms=(), args=(), outer=None): | |
self.update(zip(parms, args)) | |
self.outer = outer | |
def find(self, var): | |
"Find the innermost Env where var appears." | |
return self if (var in self) else self.outer.find(var) | |
global_env = standard_env() | |
################ Interaction: A REPL | |
def repl(prompt='lis.py> '): | |
"A prompt-read-eval-print loop." | |
while True: | |
val = eval(parse(raw_input(prompt))) | |
if val is not None: | |
print(lispstr(val)) | |
def lispstr(exp): | |
"Convert a Python object back into a Lisp-readable string." | |
if isinstance(exp, List): | |
return '(' + ' '.join(map(lispstr, exp)) + ')' | |
else: | |
return str(exp) | |
################ Procedures | |
class Procedure(object): | |
"A user-defined Scheme procedure." | |
def __init__(self, parms, body, env): | |
self.parms, self.body, self.env = parms, body, env | |
def __call__(self, *args): | |
return eval(self.body, Env(self.parms, args, self.env)) | |
################ eval | |
def eval(x, env=global_env): | |
"Evaluate an expression in an environment." | |
if isinstance(x, Symbol): # variable reference | |
return env.find(x)[x] | |
elif not isinstance(x, List): # constant literal | |
return x | |
elif x[0] == 'quote': # (quote exp) | |
(_, exp) = x | |
return exp | |
elif x[0] == 'if': # (if test conseq alt) | |
(_, test, conseq, alt) = x | |
exp = (conseq if eval(test, env) else alt) | |
return eval(exp, env) | |
elif x[0] == 'define': # (define var exp) | |
(_, var, exp) = x | |
env[var] = eval(exp, env) | |
elif x[0] == 'set!': # (set! var exp) | |
(_, var, exp) = x | |
env.find(var)[var] = eval(exp, env) | |
elif x[0] == 'lambda': # (lambda (var...) body) | |
(_, parms, body) = x | |
return Procedure(parms, body, env) | |
else: # (proc arg...) | |
proc = eval(x[0], env) | |
args = [eval(exp, env) for exp in x[1:]] | |
return proc(*args) |
two interesting aspects of pico seq - pattern play mode and pattern length
could these be applied via lisp ?
certainly feels like they should be able to since they seem like modificationw hich are applied after the initial pattern is created
remember the dsl generates the abstract syntax tree
so these are modifications which modify that AST after initial creation
so maybe you want to do a lisp example which creates an initial AST and then has a second function which modifies it
this is interesting as before had assumed lisp functions would augment the AST with extra note information
in fact what we're doing here is applying a transformation to that AST
what would sunvox sound like if u played it through an analog heat ?
relatedly, where to specify parameters like accent levels ?
there must be a way to set these within the lisp expression
attrs==19.1.0 # https://stackoverflow.com/questions/58189683/typeerror-attrib-got-an-unexpected-keyword-argument-convert | |
git+https://github.com//metrasynth/radiant-voices@jhw-fix-reader |
#!/bin/sh | |
~/packages/sunvox/sunvox/linux_x86_64/sunvox |
- class: Generator | |
module: rv.modules.generator | |
params: | |
attack: 8 | |
duty_cycle: 511 | |
freq_modulation_input: 0 | |
mode: 1 | |
panning: 0 | |
polyphony_ch: 3 | |
release: 64 | |
sustain: false | |
volume: 50 | |
waveform: 1 | |
- class: Filter | |
module: rv.modules.filter | |
params: | |
exponential_freq: false | |
freq: 256 | |
impulse: 0 | |
lfo_amp: 0 | |
lfo_freq: 8 | |
lfo_freq_unit: 0 | |
lfo_waveform: 0 | |
mix: 256 | |
mode: 3 | |
resonance: 1356 | |
response: 19 | |
roll_off: 0 | |
set_lfo_phase: 0 | |
type: 0 | |
volume: 256 | |
- class: Distortion | |
module: rv.modules.distortion | |
params: | |
bit_depth: 16 | |
freq_hz: 44100 | |
noise: 0 | |
power: 247 | |
type: 1 | |
volume: 31 |
C0
, c0
etc