Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active October 27, 2024 13:38
Show Gist options
  • Save jhw/72d061156d7b7e9da23532453efb7d21 to your computer and use it in GitHub Desktop.
Save jhw/72d061156d7b7e9da23532453efb7d21 to your computer and use it in GitHub Desktop.
Euclidian beat generator rendered with the aid of Sunvox and Radiant Voices
*.pyc
__pycache__
env
tmp

Overview

Euclidian beat generator rendered with the aid of Sunvox and Radiant Voices

https://www.warmplace.ru/soft/sunvox/

https://github.com/metrasynth/radiant-voices

Usage

(env) jhw@Justins-MacBook-Air 72d061156d7b7e9da23532453efb7d21 % python cli.py 
Welcome to the sv-euclid-beats CLI ;)
>>> randomise_patches
INFO: 2024-08-04-12-18-10-random-which-gene
>>> mutate_patch 0
INFO: 2024-08-04-12-18-18-mutation-ill-selection
>>> export_stems
INFO: generating patches
INFO: rendering project
INFO: exporting to wav
SOUND: sundog_sound_deinit() begin
SOUND: sundog_sound_deinit() end
Max memory used: 9663370
Not freed: 9598335
MEMORY CLEANUP: 3712, 984, 2000, 6144, 160, 16, 3112, 4096, 512, 3528, 3528, 65536, 65536, 112, 4096, 5144, 112, 8192, 78336, 8, 640, 1152, 4608, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96, 640, 96...
INFO: slicing stems
from sv.sampler import SVBank
from sv.utils import is_online
from sv.utils.banks.s3 import init_s3_banks
import logging
import os
def load_banks(cache_dir):
banks = []
for file_name in os.listdir(cache_dir):
zip_path = f"{cache_dir}/{file_name}"
bank = SVBank.load_zipfile(zip_path)
banks.append(bank)
return banks
def save_banks(banks, cache_dir):
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
for bank in banks:
bank.dump_zipfile(cache_dir)
def init_banks(s3, bucket_name, cache_dir = "tmp/banks"):
if os.path.exists(cache_dir):
logging.info(f"loading banks from {cache_dir}")
return load_banks(cache_dir)
elif is_online():
logging.info(f"loading banks from s3 {bucket_name}")
banks = init_s3_banks(s3, bucket_name)
logging.info(f"saving banks to {cache_dir}")
save_banks(banks, cache_dir)
return banks
else:
raise RuntimeError("no cached banks and not online, sorry")
if __name__ == "__main__":
pass
from sv.project import SVProject
from sv.sampler import SVBanks
from sv.utils.naming import random_name
from model import Patch, Patches
from parse import parse_line
from banks import init_banks
import boto3
import cmd
import datetime
import json
import logging
import os
import sys
import yaml
logging.basicConfig(stream=sys.stdout,
level=logging.INFO,
format="%(levelname)s: %(message)s")
# Load configuration files
def load_yaml(attr):
return yaml.safe_load(open(f"{attr}.yaml").read())
MachineConf = load_yaml("machines")
ModuleConf = load_yaml("modules")
Terms = load_yaml("terms")
# Default environment configuration
Env = yaml.safe_load("""
nticks: 16
npatches: 16
density: 0.75
temperature: 0.5
bpm: 120
tpb: 4 # ticks per beat
""")
TagMapping = yaml.safe_load("""
LoSampler: clap
MidSampler: kick
HiSampler: hat
""")
AppName = "sv-pico-beats"
# Decorator for rendering patches with a random name and timestamp
def render_patches(prefix):
def decorator(fn):
def wrapped(self, *args, **kwargs):
self.project_name = random_name()
timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H-%M-%S")
self.file_name = f"{timestamp}-{prefix}-{self.project_name}"
logging.info(self.file_name)
self.patches = fn(self, *args, **kwargs)
self.dump_sunvox()
self.dump_json()
return wrapped
return decorator
class BeatsCLI(cmd.Cmd):
prompt = ">>> "
intro = f"Welcome to the {AppName} randomisation CLI ;)"
def __init__(self, s3, bucket_name, env, modules, banks, pool):
super().__init__()
self.s3 = s3
self.bucket_name = bucket_name
self.out_dir = "tmp"
self.init_sub_dirs()
self.modules = modules
self.env = env
self.banks = banks
self.pool = pool
self.machines = MachineConf
self.patches = None
self.file_name = None
def init_sub_dirs(self, sub_dirs=["sunvox", "json"]):
for sub_dir in sub_dirs:
path = f"{self.out_dir}/{sub_dir}"
if not os.path.exists(path):
os.makedirs(path)
def dump_json(self):
file_name = "%s/json/%s.json" % (self.out_dir,
self.file_name)
struct = self.patches.to_json()
with open(file_name, 'w') as f:
f.write(json.dumps(struct,
indent = 2))
def dump_sunvox(self):
file_name = f"{self.out_dir}/sunvox/{self.file_name}.sunvox"
with open(file_name, 'wb') as f:
project = SVProject().render_project(
patches=self.patches.render(),
modules=self.modules,
banks=self.banks,
bpm=self.env["bpm"]
)
project.write_to(f)
@parse_line()
def do_list_projects(self):
for file_name in sorted(os.listdir(self.out_dir + "/json")):
logging.info(file_name.split(".")[0])
@parse_line(config = [{"name": "stem",
"type": "str"}])
def do_load_project(self, stem):
try:
matches = [file_name
for file_name in sorted(os.listdir(self.out_dir + "/json"))
if stem in file_name]
if matches == []:
raise RuntimeError("no matches found")
elif len(matches) != 1:
raise RuntimeError("multiple matches found")
self.file_name = matches.pop().split(".")[0]
self.project_name = "-".join(self.file_name.split("-")[-2:])
logging.info(self.project_name)
abspath = "%s/json/%s.json" % (self.out_dir, self.file_name)
struct = json.loads(open(abspath).read())
self.patches = Patches.from_json(struct)
except RuntimeError as error:
logging.warning(f"{error}")
@parse_line()
@render_patches(prefix="random")
def do_randomise_patches(self, mapping = TagMapping):
"""Generate random patches and save as a SunVox project."""
patches = Patches(self.env)
for i in range(self.env["npatches"]):
patch = Patch.randomise(machine_conf = self.machines,
pool = self.pool,
mapping = mapping)
patches.append(patch)
return patches
@parse_line()
def do_clean_projects(self, sub_dirs = ["json", "sunvox", "wav"]):
for sub_dir in sub_dirs:
os.system("rm -rf %s/%s" % (self.out_dir, sub_dir))
self.init_sub_dirs()
def do_exit(self, _):
"""Exit the CLI."""
return self.do_quit(None)
def do_quit(self, _):
"""Quit command to close the CLI."""
logging.info("Exiting CLI")
return True
if __name__ == "__main__":
try:
if len(sys.argv) < 2:
raise RuntimeError("Please enter the S3 banks bucket name as an argument.")
bucket_name = sys.argv[1]
s3 = boto3.client("s3")
banks = init_banks(s3, bucket_name)
pool, _ = SVBanks(banks).spawn_pool(tag_mapping=Terms)
BeatsCLI(s3=s3,
bucket_name=bucket_name,
env=Env,
modules=ModuleConf,
banks=banks,
pool=pool).cmdloop()
except RuntimeError as error:
logging.error(str(error))
from sv.algos.euclid import bjorklund, TidalPatterns
from sv.model import SVNoteTrig, SVFXTrig
def class_name(self):
return str(self.__class__).split("'")[1]
class EuclidSequencer:
def __init__(self,
name,
params,
samples,
patterns = TidalPatterns,
**kwargs):
self.name = name
self.modulation = params["modulation"]
self.density = params["density"]
self.samples = samples
self.patterns = patterns
@property
def params(self):
return {"modulation": self.modulation,
"density": self.density}
def to_json(self):
return {"class": class_name(self),
"name": self.name,
"params": self.params,
"samples": self.samples}
def random_pattern(self, rand):
pulses, steps = rand["pattern"].choice(self.patterns)[:2] # because some of Tidal euclid rhythms have 3 parameters
return bjorklund(pulses = pulses,
steps = steps)
def switch_pattern(self, rand, i, temperature):
return (0 == i % self.modulation["pattern"]["step"] and
rand["pattern"].random() < self.modulation["pattern"]["threshold"] * temperature)
def random_sample(self, rand):
return rand["sample"].choice(self.samples)
def switch_sample(self, rand, i, temperature):
return (0 == i % self.modulation["sample"]["step"] and
rand["sample"].random() < self.modulation["sample"]["threshold"] * temperature)
def groove(self, rand, i, n = 5, var = 0.1, drift = 0.1):
for j in range(n + 1):
k = 2 ** (n - j)
if 0 == i % k:
sigma = rand.gauss(0, var)
return 1 - max(0, min(1, j * drift + sigma))
def __call__(self, rand, nticks, density, temperature, volume = 1.0, **kwargs):
sample = self.random_sample(rand)
pattern = self.random_pattern(rand)
for i in range(nticks):
if self.switch_sample(rand, i, temperature):
sample = self.random_sample(rand)
elif self.switch_pattern(rand, i, temperature):
pattern = self.random_pattern(rand)
beat = bool(pattern[i % len(pattern)])
if rand["trig"].random() < (self.density * density) and beat:
trigvol = self.groove(rand["volume"], i) * volume
if trigvol > 0:
yield SVNoteTrig(mod = self.name,
sample = sample,
vel = trigvol,
i = i)
class SampleHoldModulator:
def __init__(self,
name,
params,
**kwargs):
self.name = name
self.step = params["step"]
self.range = params["range"]
self.increment = params["increment"]
@property
def params(self):
return {"step": self.step,
"range": self.range,
"increment": self.increment}
def to_json(self):
return {"class": class_name(self),
"name": self.name,
"params": self.params}
def __call__(self,
rand,
nticks,
min_val = int('0000', 16),
max_val = int('8000', 16),
**kwargs):
for i in range(nticks):
v = self.sample_hold(rand, i)
if v != None: # explicit because could return zero
value = int(v * (max_val - min_val) + min_val)
yield SVFXTrig(target = self.name,
value = value,
i = i)
def sample_hold(self, rand, i):
if 0 == i % self.step:
floor, ceil = self.range
v = floor + (ceil - floor) * rand["level"].random()
return self.increment * int(0.5 + v / self.increment)
if __name__=="__main__":
pass
- class: machines.EuclidSequencer
name: LoSampler
params:
density: 0.33333
modulation:
sample:
step: 4
threshold: 0.5
pattern:
step: 4
threshold: 0.5
nsamples: 4
- class: machines.EuclidSequencer
name: MidSampler
params:
density: 0.66666
modulation:
sample:
step: 4
threshold: 0.5
pattern:
step: 4
threshold: 0.5
nsamples: 4
- class: machines.EuclidSequencer
name: HiSampler
params:
density: 0.9
modulation:
sample:
step: 4
threshold: 0.5
pattern:
step: 4
threshold: 0.5
nsamples: 4
- class: machines.SampleHoldModulator
name: Echo/wet
params:
increment: 0.25
range:
- 0
- 1
step: 4
- class: machines.SampleHoldModulator
name: Echo/feedback
params:
increment: 0.25
range:
- 0
- 1
step: 4
from sv.model import SVPatch
from sv.project import load_class
from random import Random
import random
def init_machine(machine):
return load_class(machine["class"])(**machine)
class Patch:
@staticmethod
def from_json(patch):
return Patch(machines = [init_machine(machine)
for machine in patch["machines"]],
seeds = patch["seeds"])
@staticmethod
def randomise(machine_conf, pool, mapping):
seeds = {k:int(1e8 * random.random())
for k in "sample|trig|pattern|volume|level".split("|")}
machines = []
for _machine in machine_conf:
if _machine["name"] in mapping:
tag = mapping[_machine["name"]]
samples = pool.filter_by_tag(tag)
_machine["samples"] = [random.choice(samples)
for i in range(_machine["params"]["nsamples"])]
machine = init_machine(_machine)
machines.append(machine)
return Patch(machines = machines,
seeds = seeds)
def __init__(self, machines, seeds):
self.machines = machines
self.seeds = seeds
def to_json(self):
return {"machines": [machine.to_json()
for machine in self.machines],
"seeds": self.seeds}
def render(self, env):
rand = {key:Random(seed)
for key, seed in self.seeds.items()}
trigs = []
for machine in self.machines:
for trig in machine(rand = rand,
**env):
trigs.append(trig)
return SVPatch(trigs = trigs,
n_ticks = env["nticks"])
class Patches(list):
@staticmethod
def from_json(patches):
return Patches(patches = [Patch.from_json(patch)
for patch in patches["patches"]],
env = patches["env"])
def __init__(self, env, patches = []):
list.__init__(self, patches)
self.env = env
def to_json(self):
return {"env": self.env,
"patches": [patch.to_json()
for patch in self]}
def render(self):
return [patch.render(self.env)
for patch in self]
if __name__ == "__main__":
pass
- name: MidSampler
class: sv.sampler.SVSlotSampler
links:
- Echo
- name: LoSampler
class: sv.sampler.SVSlotSampler
links:
- Echo
- name: HiSampler
class: sv.sampler.SVSlotSampler
links:
- Echo
- name: Echo
class: rv.modules.echo.Echo
defaults:
dry: 256
wet: 256
delay: 36
delay_unit: 3 # tick
links:
- Output

mutate_arranger 24/10/24

  • mutations of a root see likely then first step
  • then you have a pattern (012 etc)
  • then you have mappings which map mutations onto patterns
  • mutations always have fixed first root
  • you can re mutate
  • you can shuffle pattern
  • you can shuffle mapping
  • but you might want certain mapping elements to be fixed
  • you can overlay density lfo over patterns for variation

digitakt export 01/08/24

  • need to create a single zip file with paths #{patch}/#{index}/#{instrument.zip}
  • audio is exported on a per instrument basis but with the same FX, so you should be able to load the instruments on 3 Digitakt tracks, play them at the same time and have the sound like the Sunvox original
  • load multiple samples into the pool, play on separate tracks with trig [0] only enabled, load multiple samples into sample pool, switch assingments with Src / [encoder D], resample entire pattern
  • hence you have an arranger, and a workflow that should work as the basis of an octavox/digitakt API
  • now you just have to see what's possible with octavox stem generation, whether beats/bass/arps

  • add back breaks
  • muting -> can a mute be configured or does it have to be removed?
  • export three separate wavs
  • slice with pydub
  • arrange into zip file

pico play modes 28/07/24

  • instead of the existing mutate you could implement pico play modes
  • you could reverse the patterns [holding fx fixed]
  • you could reverse one line only
  • could you implement ping pong and ping pong with repeat
  • you could randomise

freezewash 20/07/24

  • ability to mark a patch as freezewashed
  • one that sounds good when it washes into another one
  • freezewash switch
  • if freezewash is on then the freezewash patch is rendered before every standard patch
  • export then cuts out the freezewash patches

303 20/07/24

  • take mikey303 samples together with the adsr/soundctl patch
  • experiment with the following
    • adsr envelopes
    • slide effects
    • filter levels
  • figure out different patterns that sounds good
  • randomise automation, possibly with filter lfo multiplier

ideas 04/05/23

  • 303
  • freeze/echo wash
  • vordhosbn
  • FM percussion
  • breakbeats slicing
  • vocals and vocoding
  • city dreams
  • chords and sweeps
  • strudel
import re
import traceback
def matches_number(value):
return re.search("^\\-?\\d+(\\.\\d+)?$", value) != None
def matches_int(value):
return re.search("^\\-?\\d+$", value) != None
def matches_str(value):
return True
def parse_number(value):
return int(value) if matches_int(value) else float(value)
def parse_int(value):
return int(value)
def parse_str(value):
return value
def parse_line(config = []):
def decorator(fn):
def wrapped(self, line):
try:
args = [tok for tok in line.split(" ") if tok != '']
if len(args) < len(config):
raise RuntimeError("please enter %s" % ", ".join([item["name"]
for item in config]))
kwargs = {}
for item, arg_val in zip(config, args[:len(config)]):
matcher_fn = eval("matches_%s" % item["type"])
matcher_args = [arg_val]
if not matcher_fn(*matcher_args):
raise RuntimeError("%s value is invalid" % item["name"])
parser_fn = eval("parse_%s" % item["type"])
kwargs[item["name"]] = parser_fn(arg_val)
return fn(self, **kwargs)
except RuntimeError as error:
print ("ERROR: %s" % str(error))
except Exception as error:
print ("EXCEPTION: %s" % ''.join(traceback.TracebackException.from_exception(error).format()))
return wrapped
return decorator
if __name__ == "__main__":
pass
awscli
boto3
botocore
pyyaml
git+ssh://[email protected]/jhw/[email protected]
#!/usr/bin/env bash
export AWS_DEFAULT_OUTPUT=table
export AWS_PROFILE=woldeploy
export PYTHONPATH=.
kick: (kick)|(kik)|(kk)|(bd)
bass: (bass)
kick-bass: (kick)|(kik)|(kk)|(bd)|(bass)
snare: (snare)|(sn)|(sd)
clap: (clap)|(clp)|(cp)|(hc)
snare-clap: (snare)|(sn)|(sd)|(clap)|(clp)|(cp)|(hc)
hat: (oh)|( ch)|(open)|(closed)|(hh)|(hat)
perc: (perc)|(prc)|(rim)|(tom)|(cow)|(rs)
sync: (syn)|(blip)
all-perc: (oh)|( ch)|(open)|(closed)|(hh)|(hat)|(perc)|(prc)|(rim)|(tom)|(cow)|(rs)|(syn)|(blip)
arcade: (arc)
# break: (break)|(brk)|(cut)
# chord: (chord)
# drone: (dron)
# fm: (fm)
fx: (fx)
glitch: (devine)|(glitch)|(gltch)
noise: (nois)
# pad: (pad)
# stab: (chord)|(stab)|(piano)
# sweep: (swp)|(sweep)

short

  • integrate sv 0.3

medium

  • git- based arrangment/mutation [notes]

features

  • simple mutation
  • tag mapping
  • param setting
  • mutes and export

sv

  • remove chromatic sampler
  • remove vitling
  • refactor demo naming
  • refactor s3 bank loading
  • complete 909 example

gists

  • arranger workflow
  • trimming/reversing/repeating
  • resampler arranger

thoughts

  • move iteration over n steps into patch / convert machines to iterator?
    • no is a step too far
    • sv 303 doesn't currently have this

done

  • load project
  • initialise json directory
  • save project
  • patch deserialisation
  • patch serialisation
  • add back machine serialisation
  • remove volume mutes
  • remove machine tag/default > replace with local mapping
  • merge clis
  • consider how sv.instrument.play() will work in this context
  • refactor sample selection in patch.randomise
  • add machine state variables for sample and pattern
  • add back patches model
  • env should be a patch state variable
  • remove mapping
  • remove tag from machine
  • remove nsamples from machines
  • move sample randomisation into patch
  • separate nticks from env
  • pass rand to render
  • move seeds into patch
  • pass env to render
  • move render stuff into top level CLIs
  • rename clis as randomiser and mutate_arranger
  • only render lo/mid/hi on export
  • move patch randomisation from random_cli to Patch/Patches classes
  • do youneed both cli project_name and file_name?
  • move init_machine into machines
  • abstract init_banks
  • does show_tags still work
  • init_machine needs to be in base
  • abstract model (Patches, Patch)
  • use load_class from sv
  • remove env.yaml
  • abstract init_machine
  • pass bucket as command line arg
  • format hi-lo-mid
  • render and export mutes
  • wav export to zip
  • export to be separate task
  • add back export
  • cli save_banks should os.makedirs() not os.mkdir()
  • logging.info() not print()
  • tag being overwritten by name on save
  • why does render_project require cast to Patches?
  • Patches class
  • stop passing klass around
    • indef from class
  • rename klass as class
  • patch from_json
  • move init_machine
  • cli helper to initialise classes
  • load_project needs to convert machines to instances
  • remove dual handling at machines constructor level
  • Patch should not be a dict
  • do machines need cloning?
  • single machines.py
  • just simple dicts for machines
  • replace cli randomise classmethods
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment