Skip to content

Instantly share code, notes, and snippets.

@jhw
Last active August 21, 2024 14:30
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.project import SVProject
from sv.sampler import SVBanks, SVBank
from sv.utils import is_online
from sv.utils.banks.s3 import init_s3_banks
from sv.utils.export import export_wav
from sv.utils.naming import random_name
from sv.utils.slicing import slice_audio_segment_custom
from pydub import AudioSegment
from machines import SVMachinesPatch
from parse import parse_line
import boto3
import cmd
import datetime
import json
import os
import random
import re
import readline
import yaml
import zipfile
def load_yaml(attr):
return yaml.safe_load(open(f"{attr}.yaml").read())
Env = load_yaml("env")
Machines = load_yaml("machines")
Modules = load_yaml("modules")
Terms = load_yaml("terms")
AppName = "sv-euclid-beats"
HistorySize = 100
"""
- https://stackoverflow.com/questions/7331462/check-if-a-string-is-a-possible-abbrevation-for-a-name
"""
def is_abbrev(abbrev, text):
abbrev = abbrev.lower()
text = text.lower()
words = text.split()
if not abbrev:
return True
if abbrev and not text:
return False
if abbrev[0] != text[0]:
return False
else:
return (is_abbrev(abbrev[1:],' '.join(words[1:])) or
any(is_abbrev(abbrev[1:],text[i+1:])
for i in range(len(words[0]))))
def timestamp():
return datetime.datetime.utcnow().strftime("%Y-%m-%d-%H-%M-%S")
def render_patches(prefix):
def decorator(fn):
def wrapped(self, *args, **kwargs):
self.project_name = random_name()
self.file_name = "%s-%s-%s" % (timestamp(),
prefix,
self.project_name)
print ("INFO: %s" % self.file_name)
self.patches = fn(self, *args, **kwargs)
self.dump_json()
self.dump_sunvox()
return wrapped
return decorator
def assert_project(fn):
def wrapped(self, *args, **kwargs):
if not self.patches:
raise RuntimeError("no patches found")
return fn(self, *args, **kwargs)
return wrapped
class SVEnvironment(dict):
def __init__(self, item = {}):
dict.__init__(self, item)
def lookup(self, abbrev):
matches = []
for key in self:
if is_abbrev(abbrev, key):
matches.append(key)
if matches == []:
raise RuntimeError("%s not found" % abbrev)
elif len(matches) > 1:
raise RuntimeError("multiple key matches for %s" % abbrev)
return matches.pop()
class SVBaseCli(cmd.Cmd):
prompt = ">>> "
def __init__(self,
s3,
bucket_name,
env,
modules,
history_size = HistorySize):
cmd.Cmd.__init__(self)
self.s3 = s3
self.bucket_name = bucket_name
self.out_dir = "tmp"
self.init_sub_dirs()
self.modules = modules
self.env = SVEnvironment(env)
self.patches = None
self.project_name = None
self.file_name = None
self.history_file = os.path.expanduser("%s/.clihistory" % self.out_dir)
self.history_size = history_size
def init_sub_dirs(self, sub_dirs = ["json", "sunvox", "stems"]):
for sub_dir in sub_dirs:
path = "%s/%s" % (self.out_dir, sub_dir)
if not os.path.exists(path):
os.makedirs(path)
def preloop(self):
if os.path.exists(self.history_file):
readline.read_history_file(self.history_file)
def dump_json(self):
file_name = "%s/json/%s.json" % (self.out_dir,
self.file_name)
struct = {"patches": self.patches}
with open(file_name, 'w') as f:
f.write(json.dumps(struct,
indent = 2))
def dump_sunvox(self):
file_name = "%s/sunvox/%s.sunvox" % (self.out_dir,
self.file_name)
with open(file_name, 'wb') as f:
rendered_patches = [patch.render(n_ticks = self.env["nticks"],
density = self.env["density"],
temperature = self.env["temperature"])
for patch in self.patches]
project = SVProject().render_project(patches = rendered_patches,
modules = self.modules,
banks = self.banks,
bpm = self.env["bpm"])
project.write_to(f)
@parse_line()
def do_show_params(self):
for key in sorted(self.env.keys()):
print ("%s: %s" % (key, self.env[key]))
@parse_line(config = [{"name": "frag",
"type": "str"},
{"name": "value",
"type": "number"}])
def do_set_param(self, frag, value):
key = self.env.lookup(frag)
if key:
self.env[key] = value
print ("INFO: %s=%s" % (key, self.env[key]))
else:
print ("WARNING: %s not found" % frag)
@parse_line()
def do_list_projects(self):
for file_name in sorted(os.listdir(self.out_dir + "/json")):
print (file_name.split(".")[0])
@parse_line(config = [{"name": "stem",
"type": "str"}])
def do_load_project(self, stem):
matches = [file_name
for file_name in sorted(os.listdir(self.out_dir + "/json"))
if stem in file_name]
if matches == []:
print ("WARNING: no matches")
elif len(matches) == 1:
self.file_name = matches.pop().split(".")[0]
self.project_name = "-".join(self.file_name.split("-")[-2:])
print ("INFO: %s" % self.project_name)
abspath = "%s/json/%s.json" % (self.out_dir, self.file_name)
struct = json.loads(open(abspath).read())
self.patches = [SVMachinesPatch(**patch)
for patch in struct["patches"]]
else:
print ("WARNING: multiple matches")
@parse_line()
def do_clean_projects(self, sub_dirs = ["json", "sunvox", "stems"]):
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, _):
return self.do_quit(None)
def do_quit(self, _):
print ("INFO: exiting")
return True
def postloop(self):
readline.set_history_length(self.history_size)
readline.write_history_file(self.history_file)
class SVBankCli(SVBaseCli):
def __init__(self,
banks,
pool,
mapping,
*args,
**kwargs):
SVBaseCli.__init__(self, *args, **kwargs)
self.banks = banks
self.pool = pool
self.mapping = mapping
@parse_line()
def do_show_tags(self):
print (yaml.safe_dump(self.pool.tags,
default_flow_style = False))
@parse_line()
def do_show_mapping(self):
for key in sorted(self.mapping.keys()):
print ("%s: %s" % (key, self.mapping[key]))
@parse_line()
def do_randomise_mapping(self):
tags = list(self.pool.tags.keys())
for key in sorted(self.mapping.keys()):
self.mapping[key] = random.choice(tags)
print ("%s: %s" % (key, self.mapping[key]))
@parse_line(config = [{"name": "key",
"type": "str"},
{"name": "value",
"type": "str"}])
def do_set_mapping(self, key, value):
if key not in self.mapping:
raise RuntimeError("%s not found in mapping" % key)
tags = list(self.pool.tags.keys())
if value not in tags:
raise RuntimeError("%s not found in tags" % value)
self.mapping[key] = value
class SVCli(SVBankCli):
intro = f"Welcome to the {AppName} CLI ;)"
def __init__(self,
machines,
*args,
**kwargs):
self.machines = machines
mapping = {machine["tag"]: machine["default"]
for machine in machines
if "tag" in machine}
SVBankCli.__init__(self, mapping = mapping, *args, **kwargs)
@parse_line()
@render_patches(prefix = "random")
def do_randomise_patches(self):
patches = []
for i in range(self.env["npatches"]):
patch = SVMachinesPatch.randomise(machines = self.machines,
pool = self.pool,
mapping = self.mapping)
patches.append(patch)
return patches
"""
mutating samples within patch creates too much noise
"""
@parse_line(config =[{"name": "i",
"type": "int"}])
@assert_project
@render_patches(prefix = "mutation")
def do_mutate_patch(self, i, attrs = "level|volume|trig|pattern".split("|")):
root = self.patches[i % len(self.patches)]
patches = [root]
for i in range(self.env["npatches"]-1):
patch = root.clone()
for machine in patch["machines"]:
for attr in attrs:
if attr in machine["seeds"]:
machine["seeds"][attr] = int(1e8*random.random())
patches.append(patch)
return patches
"""
Expectation is that Transfer Tool ignores the name of the zipfile and just uses the paths therein contained, when creating the internal Digitakt folder structure
"""
"""
This is all far far too complex but is really due to the hard coupling of three samplers and a sequencer together
Should be much easier when each instrument is independent
In particular having to mute stuff rather than just not render stuff feels like a terrible hard coded code smell
"""
@parse_line()
@assert_project
def do_export_stems(self, fade = 3):
class Sequencers(dict):
def __init__(self, sequencers):
dict.__init__(self, {sequencer["tag"]:sequencer["name"]
for sequencer in sequencers})
def mutes(self, solo_key):
return [self[key] for key in self
if key != solo_key]
class Items(list):
def __init__(self, sequencers, patches, env):
list.__init__(self, [])
for solo_key in sequencers:
mutes = sequencers.mutes(solo_key)
for i, patch in enumerate(patches):
index = "{:02}".format(i+1)
# patch_key = f"{solo_key}/{index}"
patch_key = f"{solo_key}-{index}"
rendered = patch.render(n_ticks = env["nticks"],
density = env["density"],
temperature = env["temperature"],
mutes = mutes)
self.append((patch_key, rendered))
@property
def keys(self):
return [item[0] for item in self]
@property
def values(self):
return [item[1] for item in self]
def init_zip_items(audio_io, items):
block_sz = int(len(audio_io) / len(items))
chunk_sz = int(block_sz / 3) # wash, patch, break
zip_items = []
for i, patch_key in enumerate(items.keys):
for chunk_type, chunk_offset in [("cln", 0),
("drt", chunk_sz)]:
start_time = i * block_sz + chunk_offset
end_time = start_time + chunk_sz
# zip_path = f"{AppName}-{self.env['bpm']}/{self.project_name}/{chunk_type}/{patch_key}.wav"
zip_path = f"{chunk_type}-{patch_key}-{self.project_name}.wav"
zip_item = {"zip_path": zip_path,
"start_time": start_time,
"end_time": end_time}
zip_items.append(zip_item)
return zip_items
print ("INFO: generating patches")
sequencers = Sequencers([machine for machine in self.machines
if "tag" in machine]) # because only sequencers have tags
items = Items(sequencers = sequencers,
patches = self.patches,
env = self.env)
print ("INFO: rendering project")
project = SVProject().render_project(patches = items.values,
modules = self.modules,
banks = self.banks,
bpm = self.env["bpm"],
wash = True,
breaks = True)
print ("INFO: generating wav and slicing stems")
wav_io = export_wav(project=project)
audio_io = AudioSegment.from_file(wav_io, format = "wav")
zip_items = init_zip_items(audio_io = audio_io,
items = items)
zip_buffer = slice_audio_segment_custom(audio_io = audio_io,
zip_items = zip_items)
file_name = f"{self.out_dir}/stems/{self.file_name}.zip"
with open(file_name, 'wb') as f:
f.write(zip_buffer.getvalue())
def load_banks(cache_dir = "tmp/banks"):
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 = "tmp/banks"):
if not os.path.exists(cache_dir):
os.mkdir(cache_dir)
for bank in banks:
bank.dump_zipfile(cache_dir)
if __name__ == "__main__":
try:
bucket_name = os.environ["ASSETS_BUCKET"]
if bucket_name in ["", None]:
raise RuntimeError("ASSETS_BUCKET does not exist")
s3 = boto3.client("s3")
if os.path.exists("tmp/banks"):
print ("INFO: loading banks from local cache")
banks = load_banks()
elif is_online():
print ("INFO: loading banks from S3")
banks = init_s3_banks(s3, bucket_name)
print ("INFO: caching banks locally")
save_banks(banks)
else:
raise RuntimeError("no cached banks and not online, sorry")
pool, _ = SVBanks(banks).spawn_pool(tag_mapping = Terms)
SVCli(s3 = s3,
machines = Machines,
bucket_name = bucket_name,
env = Env,
modules = Modules,
banks = banks,
pool = pool).cmdloop()
except RuntimeError as error:
print ("ERROR: %s" % str(error))
nticks: 16
npatches: 16
density: 0.75
temperature: 0.5
bpm: 120
tpb: 4 # ticks per beat
from sv.model import SVPatch
import importlib
import random
def random_seed():
return int(1e8*random.random())
def Q(seed):
q = random.Random()
q.seed(seed)
return q
def load_class(path):
try:
tokens = path.split(".")
mod_path, class_name = ".".join(tokens[:-1]), tokens[-1]
module=importlib.import_module(mod_path)
return getattr(module, class_name)
except AttributeError as error:
raise RuntimeError(str(error))
except ModuleNotFoundError as error:
raise RuntimeError(str(error))
class SVMachines(list):
@classmethod
def randomise(self,
machines,
**kwargs):
return SVMachines([load_class(machine["class"]).randomise(machine = machine,
**kwargs)
for machine in machines])
def __init__(self, machines):
list.__init__(self, [load_class(machine["class"])(machine = machine)
for machine in machines])
def clone(self):
return SVMachines([machine.clone()
for machine in self])
class SVMachinesPatch(dict):
@classmethod
def randomise(self, machines, **kwargs):
return SVMachinesPatch(machines = SVMachines.randomise(machines = machines,
**kwargs))
def __init__(self,
machines):
dict.__init__(self, {"machines": SVMachines(machines)})
def clone(self):
return SVMachinesPatch(machines = self["machines"].clone())
def render(self,
n_ticks,
density,
temperature,
mutes = []):
trigs = []
for machine in self["machines"]:
volume = 1 if machine["name"] not in mutes else 0
for trig in machine.render(n_ticks = n_ticks,
density = density,
temperature = temperature,
volume = volume):
trigs.append(trig)
return SVPatch(trigs = trigs,
n_ticks = n_ticks)
if __name__ == "__main__":
pass
- class: sequencers.EuclidSequencer
name: LoSampler
tag: lo
default: clap
params:
density: 0.33333
modulation:
sample:
step: 4
threshold: 0.5
pattern:
step: 4
threshold: 0.5
nsamples: 4
- class: sequencers.EuclidSequencer
name: MidSampler
tag: mid
default: kick
params:
density: 0.66666
modulation:
sample:
step: 4
threshold: 0.5
pattern:
step: 4
threshold: 0.5
nsamples: 4
- class: sequencers.EuclidSequencer
name: HiSampler
tag: hi
default: hat
params:
density: 0.9
modulation:
sample:
step: 4
threshold: 0.5
pattern:
step: 4
threshold: 0.5
nsamples: 4
- class: modulators.SampleHoldModulator
name: Echo/wet
params:
increment: 0.25
range:
- 0
- 1
step: 4
- class: modulators.SampleHoldModulator
name: Echo/feedback
params:
increment: 0.25
range:
- 0
- 1
step: 4
from machines import Q, random_seed
from sv.model import SVFXTrig
import copy
class SampleHoldModulator(dict):
@classmethod
def randomise(self,
machine,
**kwargs):
return SampleHoldModulator({"name": machine["name"],
"class": machine["class"],
"params": machine["params"],
"seeds": {"level": random_seed()}})
def __init__(self, machine):
dict.__init__(self, machine)
for k, v in machine["params"].items():
setattr(self, k, v)
def clone(self):
return SampleHoldModulator({"name": self["name"],
"class": self["class"],
"params": copy.deepcopy(self["params"]),
"seeds": dict(self["seeds"])})
def render(self, n_ticks, min_value = '0000', max_value = '8000', **kwargs):
min_val, max_val = (int(min_value, 16),
int(max_value, 16))
q = Q(self["seeds"]["level"])
for i in range(n_ticks):
v = self.sample_hold(q, 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, q, i):
if 0 == i % self.step:
floor, ceil = self.range
v = floor + (ceil - floor) * q.random()
return self.increment * int(0.5 + v / self.increment)
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

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
pydub
pyyaml
git+ssh://git@github.com/jhw/salient-voices.git@0.2.4
from machines import Q, random_seed
from sv.algos.euclid import bjorklund, TidalPatterns
from sv.model import SVNoteTrig, SVFXTrig
import copy
import random
import yaml
class EuclidSequencer(dict):
@classmethod
def randomise(self,
machine,
pool,
mapping):
samples = pool.filter_by_tag(mapping[machine["tag"]])
return EuclidSequencer({"name": machine["name"],
"class": machine["class"],
"params": machine["params"],
"samples": [random.choice(samples)
for i in range(machine["params"]["nsamples"])],
"seeds": {k:random_seed()
for k in "sample|trig|pattern|volume".split("|")}})
def __init__(self, machine, patterns = TidalPatterns):
dict.__init__(self, machine)
for k, v in machine["params"].items():
setattr(self, k, v)
self.patterns = patterns
def clone(self):
return EuclidSequencer({"name": self["name"],
"class": self["class"],
"params": copy.deepcopy(self["params"]),
"samples": copy.deepcopy(self["samples"]),
"seeds": dict(self["seeds"])})
def random_pattern(self, q):
pulses, steps = q["pattern"].choice(self.patterns)[:2] # because some of Tidal euclid rhythms have 3 parameters
return bjorklund(pulses = pulses,
steps = steps)
def switch_pattern(self, q, i, temperature):
return (0 == i % self.modulation["pattern"]["step"] and
q["pattern"].random() < self.modulation["pattern"]["threshold"] * temperature)
def random_sample(self, q):
return q["sample"].choice(self["samples"])
def switch_sample(self, q, i, temperature):
return (0 == i % self.modulation["sample"]["step"] and
q["sample"].random() < self.modulation["sample"]["threshold"] * temperature)
def groove(self, q, i, n = 5, var = 0.1, drift = 0.1):
for j in range(n + 1):
k = 2 ** (n - j)
if 0 == i % k:
sigma = q.gauss(0, var)
return 1 - max(0, min(1, j * drift + sigma))
"""
volume used for muting
muting needs to be volume based in avoid messing with seeds (trig order would be messed up if you simply deleted trigs
"""
def render(self, n_ticks, density, temperature, volume = 1.0, **kwargs):
q = {k:Q(v) for k, v in self["seeds"].items()}
sample, pattern = (self.random_sample(q),
self.random_pattern(q))
for i in range(n_ticks):
if self.switch_sample(q, i, temperature):
sample = self.random_sample(q)
elif self.switch_pattern(q, i, temperature):
pattern = self.random_pattern(q)
beat = bool(pattern[i % len(pattern)])
if q["trig"].random() < (self.density * density) and beat:
trigvol = self.groove(q["volume"], i) * volume
if trigvol > 0:
yield SVNoteTrig(mod = self["name"],
sample = sample,
vel = trigvol,
i = i)
if __name__=="__main__":
pass
#!/usr/bin/env bash
export ASSETS_BUCKET=octavox-assets
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

medium

  • add controller s&h range to env

thoughts?

  • remove density?
    • no because default density to make it sound good seems to need to be 0.75
  • remove json machines saving and related actions?
    • no is really an integrated part of having these higher level machines
    • and is the part that should be replaced by new sv instruments

done

  • export mutes not working
  • add local bank management tools
  • add filter_by_tag code to pool
  • add offline bank support
  • use sv pool code
  • use sv bjorklund algo
  • abstract zip_items code
    • add notes saying is unnecessarily complex
  • add support for random filenames
  • integrate slice_wav_custom
  • patch loading should set project name
  • integrate random filenames
  • retire local slicing
  • retire local banks
  • render blank patch if no beats to stop patterns shifting upwards
  • something wrong with sample and hold implementation
  • better online/offline handling
  • some mismatch between pool (creating dict with sample and tags attrs) and need to pass sample arouns as strings
  • samples still being treated as dicts somewhere
  • refactor s3 banks to use banks as list
  • add main block to s3 banks
  • update refs to s3 banks loading
  • update refs to SVMachines, SVMachinesPatch
  • local version of old SVPatch
    • will need to be renamed to avoid conflict with new SVPatch class
  • local version of old SVMachines
  • local versions of helper functions in core.py
  • include links as part of modules
  • add awscore/boto3/botocore to requirements
  • replace usage of self.file_name with timestamp + local prefix
  • remove self.filename
  • add timestamp to stem export
  • add self.project name
  • export_stems to use self.project_name
  • separate timestamp
  • include bpm in stems output
  • rationalise nticks
  • rationalise min|maxval
  • rationalise imports
  • export project names as part of stems
  • format zip file numbers to 2 significant figures
  • machine map helper
  • single concatenated project
  • pydub fade in/out to prevent clipping
  • refactor mutes as volume=0 to preserve seed structure
  • add wash parameter
  • breaks
  • fails if you don't break after one iteration
  • create local project prior to rendering
  • map and unmap machines
  • add arg to apply solos/mutes
  • mutes
  • per- instrument exports
  • check digitakt treatment of zipfile name
  • include patch name in filename
  • export to patch name
  • export to stems subdir
  • slice variations
  • demo to create AudioSegment from BytesIO and slic
  • generate zipfile
  • pydub
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment