A simple Tidal- style beat generator for Sunvox
justin@justin-XPS-13-9360:~/work python beats.py
. ./sunvox.sh
[open beats.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) |
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 |
key
with simple mod
note_for
to rendering