Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active May 28, 2023 00:57
Show Gist options
  • Save jhw/ff2a33b819f651ed37e76c5401ff9504 to your computer and use it in GitHub Desktop.
Save jhw/ff2a33b819f651ed37e76c5401ff9504 to your computer and use it in GitHub Desktop.
Sunvox TB03

city_dreams_bass.sunvox

  • tmp/city_dreams/Gen-Fil-Dis-Ech/1664.sunvox

virtualenv

  • 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)

pico patterns 12/2/20

  • 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

short

  • abstract Generator ref in dsl
  • shortcut key lookups
  • tracks to be sorted in reverse module order
  • figure out val formats (hex?)
  • ability to set volume as effect
  • setting of default (non- accented) levels
  • function for main block
  • lisp demo

medium

  • functional accents
  • pitch bend
  • module insertion (echo, reverb)
  • improved layout generator
  • pico/lisp [notes]
  • sunbeam

thoughts

  • positioning to observe module width ?
    • just not worth it
  • expansion of groups rather than just plain symbols ?
    • not a priority for now

done

  • yaml accents
  • pass effect operator map to dsl (!, ?)
  • multiple effects per note
    • eg generator release
  • accent mapping
  • refactor multiplier requires for ctl level
  • lists of notes
    • (i, notes)
  • extend module keyfn to include controller
  • add filter note for every note played
  • add dsl support for accented notes
  • set mod filter level for accented notes
  • module param index function
  • investigate rv args for setting filter params
    • mod, ctl, val
  • add key to ast for track lookup
  • pattern renderings
  • nesting
  • replace beat generation with tidal- style dsl
  • reference notes a la sunvox eg C0, c0 etc
  • simple beat stream
  • random rests
  • arpeggiator
  • module param setter
  • module wrapper class
  • position modules
  • test tb03 sound
  • initialise project with modules
  • connect modules
  • don't bother modifying tb03 params ?
    • modify post- serialisation ?
  • simplify dumped enum data
  • add enum values
  • instantiate each class
  • add non- enum values
  • dump yaml params
  • explicitly disconnect modules
  • ignore echo
  • explicitly reconnect modules
  • load city_dreams_bass.sunvox
  • remove echo
  • turn off sustain
  • add attack and release
  • save file to test round trip
  • isolate bass
  • decompile city dreams
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment