Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active May 28, 2023 00:58
Show Gist options
  • Save jhw/3bbe6a5f4132fbc66915f7e8f1047b2d to your computer and use it in GitHub Desktop.
Save jhw/3bbe6a5f4132fbc66915f7e8f1047b2d to your computer and use it in GitHub Desktop.
Tidal patterns for Sunvox
bin
include
lib
share
data
*.pyc
*.sunvox

A simple Tidal- style beat generator for Sunvox

justin@justin-XPS-13-9360:~/work python beats.py 
. ./sunvox.sh

[open beats.sunvox]

"""
- Project needs to be imported ahead of Note else will fail when trying to fetch ALL_NOTES (?)
"""
from rv.api import Project as RVProject
from rv.pattern import Pattern as RVPattern
from rv.note import Note as RVNote
import re
Samples={"bd": sorted([i+12*j for i in range(1, 5) for j in range(10)]),
"ht": sorted([i+12*j for i in range(5, 8) for j in range(10)]),
"sn": sorted([i+12*j for i in range(8, 13) for j in range(10)])}
def NoteFor(token, samples=Samples):
key, i = token[:2], token[2:]
if key not in samples:
raise SyntaxError("%s not recognised as sample" % key)
i=0 if i=='' else int(i)
return samples[key][i % len(samples[key])]
ModuleOffset=2
class Modules(list):
def __init__(self, klasses, x0=512, dx=256):
list.__init__(self)
for i, klass in enumerate(klasses):
x=x0-dx*(len(klasses)-i)
mod=klass(x=x)
self.append(mod)
def indexOf(self, name, offset=ModuleOffset):
for i, mod in enumerate(reversed(self)):
if re.search(name, mod.name, re.I)!=None:
return i+offset
raise RuntimeError("module '%s' not found" % name)
def attach(self, proj):
for i, mod in enumerate(reversed(self)):
proj.attach_module(mod)
proj.connect(proj.modules[i+1],
proj.modules[i])
class Tracks(dict):
@classmethod
def KeyFn(self, note):
return "%s/%s" % (note["inst"],
note["beat"])
def __init__(self, notefn, mods, lines):
dict.__init__(self, {})
self.notefn=notefn
self.mods=mods
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, note, velocity=60):
key=Tracks.KeyFn(note)
self.spawn(key)
note_=self.notefn(note["beat"])
mod=self.mods.indexOf(note["inst"])
rvnote=RVNote(note=note_,
module=mod,
vel=velocity)
self[key][note["i"]]=rvnote
return self
def flatten(self):
return [self[key]
for key in sorted(self.keys())]
def render_pattern(notes,
notefn,
mods,
lines):
tracks=Tracks(notefn=notefn,
mods=mods,
lines=lines)
for note in notes:
tracks.add(note)
pat=RVPattern(lines=lines,
tracks=len(tracks))
grid=tracks.flatten()
def notefn(self, i, j):
return grid[j][i] if grid[j][i] else RVNote()
pat.set_via_fn(notefn)
return pat
def Beats(tracks, lines, mod="drum"):
def is_expansion(token):
return re.search("\\*\\d+$", token)
def expand(token):
tokens=token.split("*")
pat, n = tokens[0], int(tokens[1])
return [pat for i in range(n)]
def is_note(token):
return re.search("^\\D{2}\\d*$", token)!=None
def itergen(notes, track, lines, i):
lines_=int(lines/len(track))
for j, token in enumerate(track):
i_=i+j*lines_
if isinstance(token, list):
itergen(notes, token, lines_, i_)
elif is_expansion(token):
itergen(notes, expand(token), lines_, i_)
elif is_note(token):
note={"inst": mod,
"beat": token,
"i": i_}
notes.append(note)
notes=[]
for track in tracks:
itergen(notes, track, lines, 0)
return notes
def generate(program, lines=16):
proj=RVProject()
from rv.modules.drumsynth import DrumSynth
mods=Modules([DrumSynth])
mods.attach(proj)
notes=Beats(program, lines)
pat=render_pattern(notes,
notefn=NoteFor,
mods=mods,
lines=lines)
proj.patterns.append(pat)
return proj
if __name__=="__main__":
try:
from lis import parse
proj=generate(parse("((bd2*4) (~ sn2 ~ sn2) (ht2*2 ht2 ht2*4 ht2*2))"))
with open("beats.sunvox", 'wb') as f:
proj.write_to(f)
except SyntaxError as error:
print ("Error: %s" % str(error))
import re, yaml
def dsl(tracks, lines):
def is_expansion(token):
return re.search("\\*\\d+$", token)
def expand(token):
tokens=token.split("*")
pat, n = tokens[0], int(tokens[1])
return [pat for i in range(n)]
def is_note(token):
return re.search("^\\D{2}\\d*$", token)!=None
def itergen(notes, track, lines, i):
lines_=int(lines/len(track))
for j, token in enumerate(track):
i_=i+j*lines_
if isinstance(token, list):
itergen(notes, token, lines_, i_)
elif is_expansion(token):
itergen(notes, expand(token), lines_, i_)
elif is_note(token):
note={"beat": token,
"i": i_}
notes.append(note)
notes=[]
for track in tracks:
itergen(notes, track, lines, 0)
return notes
if __name__=="__main__":
from lis import parse
program=parse("((bd*4) (~ sn ~ (sn sn ~ sn*4)))")
print (yaml.safe_dump(dsl(program, lines=64),
default_flow_style=False))
import re, yaml
def dsl(tracks, lines):
def is_note(token):
return re.search("^\\D{2}\\d*$", token)!=None
def itergen(notes, track, lines, i):
lines_=int(lines/len(track))
for j, token in enumerate(track):
i_=i+j*lines_
if isinstance(token, list):
itergen(notes, token, lines_, i_)
elif is_note(token):
note={"beat": token,
"i": i_}
notes.append(note)
notes=[]
for track in tracks:
itergen(notes, track, lines, 0)
return notes
if __name__=="__main__":
from lis import parse
program=parse("((bd bd bd bd) (~ sn ~ (sn sn ~ (sn sn sn sn))))")
print (yaml.safe_dump(dsl(program, lines=64),
default_flow_style=False))
import re, yaml
def dsl(tracks, lines):
def is_note(token):
return re.search("^\\D{2}\\d*$", token)!=None
def itergen(notes, track, lines, i):
lines_=int(lines/len(track))
for j, token in enumerate(track):
i_=i+j*lines_
if is_note(token):
note={"beat": token,
"i": i_}
notes.append(note)
notes=[]
for track in tracks:
itergen(notes, track, lines, 0)
return notes
if __name__=="__main__":
from lis import parse
program=parse("((bd bd bd bd) (~ sn ~ sn))")
print (yaml.safe_dump(dsl(program, lines=64),
default_flow_style=False))
################ 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)

lisp 13/1/20

  • really think there could be something in this lisp/python/symbiosis
  • trick is not to think too far ahead
  • because you could beats as being tracks of tokens
  • but you could imagine parameter locks needing to be the output of functions
  • the lisp stage converts the input representation into actual data to be rendered by the program
  • this may required function evaluation (for lfos)
  • it may require parameter definition (since you may want to insert a reference somewhere)
  • it may require file references (if you want to load a particular structure)
  • it may require quoted lists (eg beats)
  • you parse it post- lisp evaluation
from lis import parse
import yaml
print (yaml.safe_dump(parse("((bd2*4) (~ sn2 ~ sn2) (ht2*2 ht2 ht2*4 ht2*2))"),
default_flow_style=False))
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

short

medium

POCs

  • serialisation
  • lisp
  • tidal fill/every/apply
  • polyrhythms
  • city dreams TB03
  • chords
  • pico argeggiator
  • noise hats
  • kicker

thoughts

  • multiple/comma beats ?
    • not worth it
  • unit tests ?
    • just not worth it

done

  • replace key with simple mod
  • move note_for to rendering
  • use lis.py
  • render to determine module from key
  • convert grid to be a dict
  • un- decorate render function
  • switch lines and i
  • remove track indicator from beats
  • rename grid as tracks, sz as lines
  • replace estimating number of tracks with note key and spawn
  • add dedicated track class with spawn
  • rotate tracks
  • fix x positioning
  • x position is messed up
  • add module name indexation facilities
  • tracks to reference modules by name
  • add notes re lisp
  • abstract module chain as object with indexation facilities
  • convert to single lisp structure
  • remove wrap_lisp stuff
  • multiple lines
    • pass array of programs into eval
    • iterate over one at a time, increasing track number each time
  • merge grid and pattern code
  • hardcoded single track
  • remove Tracks / lookup track size dynamically
  • expand_grid to return pattern
  • remove multi notes
  • rendering
  • function to return closure
  • add mod, velocity to expansion
  • expansion decorator
  • add samples
  • add sample lookup function
  • add rests
  • rename expansions
  • add multiple notes
  • expansions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment