#!/usr/bin/env python3 scriptName = "Auto-Techno Generator" version = "0.1.1" author = "Chris Beckstrom" author_email = "chris@chrisbeckstrom.com" # a deterministic techno generator for tidalcycles # input an integer value as a seed # and it spits out some techno ready to play # # use it in emacs like this: # it will generate a random seed # # (defun autotechno() # (interactive) # (insert (shell-command-to-string (concat "/usr/bin/python ~/tidal-auto-techno/auto-techno.py " (number-to-string(random)))))) # ################################################## ########### SETTINGS ############################# # what are the lowest and highest bpm values you want? lowestBpm = 92 highestBpm = 135 # swing # see https://tidalcycles.org/docs/patternlib/tour/time/#swing minSwing = 0 maxSwing = 0.2 # force everything to happen within 1 bar oneBar = 1 # stack name # this doesn't matter, can be any string stackName = "techno" ####### PERCUSSION ############################## # some settings for rhythm generation # for euclidean rhythms, define the choices for the 2nd number # eg "[t(3,NN)]" #dividers = [4,8,16] dividers = [16] # how many percussive/rhythmic elements to generate? percCount = 5 # this setting doesn't do anything right now, use "percNames" instead percMidiChanStart = 0 # the starting MIDI channel for perc tracks # perc track names # a percussion track will be generated for each item in this list percNames = ["kick", "hh","sd","ohh","rim","cp","tom","ride","cymbal"] # for use generating sequences of 0's and 1's # # perc notes # set some notes for percussion tracks # these notes correspond to notes (C3, C#3, etc) # each percussion track will pick one of these notes # and send note on/off messages to that note percNotes = [0,1,2,3,4,5,6,7] # ####### MELODY ################################# # NOTE about melodic sequences: # as of October 04, 2021 all melodic sequences are all notes # ie no rests. the rhythm/rests is determined by a "struct" value # just like with percussion # for melodic sequences, define the choices for range of the notes # eg 12 is one octave, 24 is two, etc noteRange = [12,24] # define the shortest and longest sequence you want # eg if shortest is 7, there will be no melodic sequences shorter than 7 shortestMelodic = 3 longestMelodic = 10 # for melodic lines, what divisions do you want to choose from? # eg 4 = quarter notes, 8 = eighth notes # tip: adding more of the same number will weight the randomness toward that melodicDivisions = [8,16,16,16] # keep melodies within 1 bar #melodicOneBar = 1 # how many # how many melodic elements to generate? melodyCount = 1 melodyMidiChanStart = 10 ############### PADS ########################### # how many pad tracks to generate padCount = 1 # how long should the pad pattern be? # tip: add more of the same number to weight that choice padBars = [1,1,1,2,4] # at what speed should the pad chord change? # every bar = 1, every 2 bars = 2, etc padSpeed = [1,2,2,2,4] # what MIDI channel should it start on? # padMidiChanStart = melodyMidiChanStart + 1 will start it right after the last melody track padMidiChanStart = melodyMidiChanStart + 1 ################ MIDI ##################### # on what channel should we send MIDI CC messages? midiCCChannel = 15 # list of MIDI CC's: midiCCs = [0,1,2,3,4,5,6]] ################# SETUP ################# ## import stuff import sys import random import math import re import time import decimal from decimal import Decimal ############## PARSE ARGUMENT(S) ############### # this is the 1st argument given via command line # this is used to calculate everything rawinput = sys.argv[1] # let's assume it's a hex value # convert to decimal #input = int(hex,int(89)) ################ SEED HEX ##################### # make sure the seed is a hex value # why? it doesn't really matter technically # I just think it would be cool # plus, hex values for long numbers are shorter # and easier to read/type # and might be good track names # TODO - this is in progress # if the input is a hex value, we'll know because it starts with 0x if (rawinput.startswith("0x")): # the input is a hex number #print("string starts with 0x") seed = rawinput.upper() else: # the input is NOT a hex number # convert to a hex number seed = hex(int(rawinput)).upper() # by now the seed should start with 0X # let's change that X to lowercase seed = seed.replace("X","x") #print("seed: "+seed) # here is the seed in decimal format (eg 3892423) decimalSeed = int(seed,16) #################### FRONTMATTER ### # put some stuff at the top of the tidal code print("-- raw input: " + str(rawinput)) print("-- hex seed: "+seed) print("-- decimal seed: "+str(decimalSeed)) print("-- generated at "+str(time.time())) print("-- "+scriptName+" v"+str(version)) print("") ########### THIS CHANGES EVERYTHING! ############# # set the random seed based on user input random.seed(decimalSeed) ################################################## indent = " " # generate a bpm between lowest and highest # looks like: cps = (BPM/60/4) bpm = "setcps ("+str(random.randrange(lowestBpm, highestBpm, 1))+"/60/4)" #print(bpm) ####################### FUNCTIONS ################### def swing(minSwing,maxSwing): """ Generate a swing value for the entire stack """ # the goal is something like this: # swingBy 0.2 8 #print("We will swing!!") # choose the first value #firstSwing = (decimal.Decimal(random.range())).quantize(Decimal('.001')) firstSwing = str(decimal.Decimal(random.randrange(0, 20))/100) secondSwing = 8 # if 8: swing 16th notes, if 4: swing 8th notes return "swingBy "+str(firstSwing)+" "+str(secondSwing) def generateBinary(busyness): """ busy-ness (sic) Create a list of 0's and 1's from which patterns can be created. The higher the busyness, the more 1's in the list, which in turn creates more events in the generated sequence busyness range from 0 to 9, where 9 is most busy, and 0 is no activity at all """ if (busyness == 0): binary = [0,0,0,0,0,0,0,0,0,0,0,1] elif (busyness == 1): binary = [0,0,0,0,0,0,0,0,1] elif (busyness == 2): binary = [0,0,0,0,0,0,0,1,1] elif (busyness == 3): binary = [0,0,0,0,0,0,1,1,1] elif (busyness == 4): binary = [0,0,0,0,0,1,1,1,1] elif (busyness == 5): binary = [0,0,0,0,1,1,1,1,1] elif (busyness == 6): binary = [0,0,0,1,1,1,1,1,1] elif (busyness == 7): binary = [0,0,1,1,1,1,1,1,1] elif (busyness == 8): binary = [0,0,0,0,0,0,1,1,1,1,1,1,1] elif (busyness == 9): binary = [0,0,0,0,1,1,1,1,1,1,1,1,1] return binary def genOnOff(limit,busyness,struct): "generate a series of 0's and 1's" zeroesOnes = "" # here we can change the list of 0's and 1's # the more 1's, the busier the rhythm will be # and we want to set this per-track # so... # for i in range(limit): zeroesOnes = zeroesOnes + str(random.choice(generateBinary(busyness))) # TODO # put a space between each 4 characters # https://stackoverflow.com/questions/9475241/split-string-every-nth-character#9475354 #>>> n = 2 #>>> [line[i:i+n] for i in range(0, len(line), n)] #['12', '34', '56', '78', '90'] #n = 4 #zeroesOnes = [zeroesOnes[i:i+n] for i in range(0, len(zeroesOnes), n)] # >>> re.findall('.{1,2}', '123456789') # ['12', '34', '56', '78', '9'] #zeroesOnes = re.findall('.{1,2}', zeroesOnes) if (struct == 1): #zeroesOnes = "struct \"{"+str(zeroesOnes)+"}%16\"" zeroesOnes = "[{"+str(zeroesOnes)+"}%16]" return(zeroesOnes) else: return(zeroesOnes) def genVelocity(limit): """ Generate a series of decimals < 1 to control velocity This adds a more organic feel to the rhythms, and makes them about 20% funkier """ velocities = "" for i in range(limit): #velocities = str((decimal.Decimal(random.choice([0.5,0.75,1],[0.5,75,1]]))).quantize(Decimal('.01')))+" "+str(velocities) velocities = str(random.choice([0.85,0.9,1]))+" "+velocities degradeAmt = (decimal.Decimal(random.random())).quantize(Decimal('.01')) # remove trailing space velocities = str(velocities).rstrip() return velocities def euclid(divider,max): """ generate a euclidean rhythm for tidalcycles the "divider" argument is the 2nd number, and the "max" argument is the maximum integer of the 1st number ... because we don't always want a super dense rhythm! """ # like struct "[t(3,8)]" # 3,8 10,16 4,16 #secondEuclid = str(random.choice(dividers)) secondEuclid = divider firstEuclid = str(random.randrange(2,max)) #print("firstEuclid:"+firstEuclid) #print("secondEuclid:"+str(secondEuclid)) euclidSeed = random.random() # decide whether there are going to be changes in the euclid seq #if (random.random() < random.random()): if (1 > 0): # the first and the second euclid numbers will stay the same # eg no <3 6 5> firstEuclid = firstEuclid else: # the first euclid number WILL sequence (eg <4,8>) #firstEuclid = "<"+str(random.randrange(2,int(random.choice(dividers)))+" "+str(random.randrange(2,int(random.choice(dividers))))+">" if (percName == "kick"): divider = 5 # limit kicks to 5 firstEuclid = "<"+str(random.randrange(2,max))+" "+str(random.randrange(2,16))+">" # decide whether the second number will seq if (random.random() > random.random()): # we will do like [t(??,<16,8>)] secondEuclid = "<"+str(random.choice(dividers))+" "+str(random.choice(dividers))+">" # [t(<3 8>,16)] #return ("struct \"[t("+str(firstEuclid)+","+str(secondEuclid)+")]\"") return ("[t("+str(firstEuclid)+","+str(secondEuclid)+")]") def genNotes(melodicOneBar): """ Generate a series of numbers for use as midi notes accepts 1 or 0, where 1 limits the melody to 16 16th notes and 0 sets the melody to somewhere between the values of shortestMelodic and longestMelodic """ # between let's say 4 notes and 16 notes if (melodicOneBar == 1): melodyLength = 16 else: melodyLength = random.randrange(shortestMelodic,longestMelodic) melody = "" for i in range(melodyLength): melody = str(random.randrange(0,random.choice(noteRange))) + " " + str(melody) return ("n "+"\"[{"+melody+"}%"+str(random.choice(melodicDivisions))+"]\"") def offset(): amounts = ["1/4","1/8","1/16","1/16","1/16"] return ("(\""+str(random.choice(amounts))+"\" ~>)") def every(): # TODO add a switch to choose divisible by 2/4/8 or not bars = random.randint(2,16) return (indent+indent+"$ every "+str(bars)+" ") def within(): firstnum = random.choice([0,0.25,0.5,0.75]) secondnum = random.choice([0.25,0.5,0.75,1]) return (indent+indent+"within ("+str(firstnum)+","+str(secondnum)+" ") def scramble(): divisions = [4,8,16] return ("scramble "+str(random.choice(divisions))) def whenmod(): limit = 16 lower = random.randint(4,limit) return ("$ whenmod "+str(limit)+" "+str(lower)+" ") def stut(): """ Stutter stut REPEATS VOLUME TIME """ # choose how many repeats repeats = random.choice([1,1,1,1,1,1,2,2,2,3,3,3,4,4,4,4,4,5]) time = random.choice([0.125,0.125,0.125,0.0625,0.0625]) return("stut "+str(repeats)+" 1 \""+str(time)+"\"") def bite(style): """ Generate something like bite 16 "{2*4 1*2 10*4 8*8}%16 if style = nice, just do quarter note repeats if style = wild, do whatever """ if (style == "nice"): # do something nice here # either repeat half notes or quarter notes number = random.choice([2,4]) # which beat to repeat? beat = random.choice([0,1,2,3]) # the goal is something like # bite 4 "{0 0 0 0}%4" bit = beat+beat+beat+beat bit = str(bit) else: divisions = [4,8] # number = the divisions to use # like 'bite 16' where 16 is the 'number' number = str(random.choice(divisions)) # generate like 1*4 8*4 2*2 1*2 9*1 repeatChoices = [1,2,4] # how long should it be? length = int(random.randint(4,8)) bit = "" for i in range(length): bit = str(random.randint(0,int(number)))+"*"+str(random.choice(repeatChoices))+" "+bit # since bite will always be encapsulated by ( ) # eg whenmod 8 7 (bite 4 "2*4") # we don't need to prepend with $ or return ("bite "+str(number)+ " \"{"+bit+"}%"+str(number)+"\"") def offset(): offsetChoices = [0.125,0.125,0.125,0.125,0.1875,0.1875,0.1875] return ("("+str(random.choice(offsetChoices))+" ~>)") def degrade(): degradeAmt = (decimal.Decimal(random.random())).quantize(Decimal('.01')) return "degradeBy "+str(degradeAmt) def fills(): """ Generate some things that would be cool at the end of phrases, eg whenmod 8 7 (do something) But nothing too intense """ # like whenmod 8 7 fillChoices = ["rev","rev",scramble(),bite("nice"),bite("nice")] # how many filles do we want? fills = "" for i in range(1,10): # roll the dice whenmods = ["8 7","16 15","16 14","32 31","64 63","32 30","8 7"] fills = str(random.choice(fillChoices))+" "+fills return "whenmod "+str(random.choice(whenmods))+" ("+fills.rstrip()+")" def sometimesBeQuiet(): """ Generate a gain pattern so that sometimes a track is silent, and other times it isn't """ #def genOnOff(limit,busyness,struct): # decide whether to do this at all if (random.randint(0,10) > 0): # for now, 8 bar phrases # (# gain "[0 1 0 0 1]/8") # how many cycles to make? quietCycles = random.choice([8]) # this generates a sequence like 011010110 quietBinary = genOnOff(quietCycles,4,0) # split 001001 into 0 0 1 0 0 1 quietBinary = list(quietBinary) finalQuietBinary = "" for i in quietBinary: finalQuietBinary = i + " " + finalQuietBinary # now the pattern looks like 0 1 0 0 1 0 0 1 print(indent+indent+"$ degradeBy \"<"+str(finalQuietBinary).rstrip()+">/8\"") ########################### LISTS ########################## # a collection of typical-ish kick drum patterns bdPats=["[t*4]","[tttt*2]","[tttt*2?]","t(3,8)","[t~~~~~<~t~~>~~~t~~<~t>~~]"] hhPats=["[t*16]","[tt*2]*4","[~t]*4","[t*16]"] sdPats=["[~t]*2","[t(3,8)]","[[~~~t][~t]]*2"] cpPats=["[~t]*2","[t(3,8)]","[[~~~t][~t]]*2"] cyPats=["[t*4]","[t*8]","[t~~~]","[t~~[~~~t]]","[~~~]"] # a collection of misc rhythms rhythmLibrary = ["[1*4]","[1*8]","[1*16]","[01]*2","[0101]","[1*16]","[1*16?]","[111111[1001][01]]/2","[{100}%16]","[{101011010101}%16]","[{10100}%16]","[{10001}%16]","[{10101}%16]","[{1001010}%16]","[{1011010}%16]","[{10101000100}%16]","[{10101001010}%16]","[{10010010010}%16]","[{1010101001010}%16]","[{1001001001010}%16]","[11*2]*4","[100000<0100>000100<01>00]","[1[10000001]]","[1111*2]","[[0001][01]]*2","[0010000100000001]","[[0001][01]]*2","[[01]1[1001][01]]","[[01]1[1001][0001]]","[11000010]*2","[[0001]000]","[[0001]]*4"] ############### It's assemble time!! ################### print("do") #print("") # create "let" statements print(indent+"let fourToTheFloor = 0") print(indent+" -- control density per track") for i in percNames: #print("let "+i+"M = \"["+genOnOff(4,5,0)+"]\"") print(indent+" "+i+"M = \"[1111]\"") #print("") # actually assemble the stack! print(indent+"p \""+stackName+"\" ") # swing? # roll the dice to see if there will be any swing at all if (random.randint(0,10) > 7): print(indent+indent+"$ "+str(swing(minSwing,maxSwing))) # stack stuff like bite, scramble, etc. will go here print(indent+indent+"-- fills") # roll the dice up to N times for i in range(1,8): if (random.randint(0,10) > 3): print(indent+indent+"$ "+fills()) # how many "fun" things to add? # a list of things to do to patterns to make them more interesting # these happen BEFORE struct, note, etc. #interestingThings = [stut(),bite(),"rev",scramble(),offset()] interestingThings = [stut(),"rev",scramble(),offset(),degrade()] def funThings(): wholeEnchilada = "" # add between 1 and 4 "fun" things # (things like `every 3 (scramble 4)`) funThings = random.randint(1,3) for i in range(funThings): #print(str(every) + "$ " + str(output)) # for each iteration, choose an interestingThing # indent + "every N" + ( rev ) wholeEnchilada = str(every()+"("+str(random.choice(interestingThings))+")\n"+wholeEnchilada).rstrip() return wholeEnchilada # the top of the stack print(indent+indent+"$ stack [") # this is an empty track to make dealing with commas easier # just 4 to the floor in case you want it print(indent+indent+"-- four to the floor") print(indent+indent+"degradeBy fourToTheFloor $ struct \"[t*4]\" $ n \"0\" # midichan 0,") # generate rhythm tracks # set the starting midi channel percMidiChan = 0 ######################################################## ## this is where each "track" is generated ## percCount = how many rhythmic (non-melodic) tracks ## will be created for i in percNames: percName = i # print the name of the track (from the percNames list) print(indent+indent+"-------------- "+i+" ---------------") # MASK: make this so once the entire thing is generated, it's easy to # alter how dense the pattern is, by altering "let nnM = "[1111] print(indent+indent+"mask "+i+"M") # sometimes be quiet #if (i != "kick"): # sometimesBeQuiet() #### VARIATIONS ##### # insert some funStuff, maybe if (random.randint(0,10) > 3): print(funThings()) #### OFFSET ### # offset the pattern by a certain amount # this can make things much funkier if (random.randint(0,10) > 6): print(indent+indent+"$ "+offset()) ##### THE RHYTHMS ##### # how many bars to create? rhythmLengthChoices = [1,1,1,1,1,1,2,2] rhythmLength = random.choice(rhythmLengthChoices) # for the number of bars, do this: fullRhythm = "" for i in range(rhythmLength): # choose between random, type-specific (bd,hh,etc), # rhythm library, or euclidean rhythms rChoice = random.choice([0,1,1,1,2,3]) rhythm = "" # placeholder for the rhythm if (rChoice == 0): # if choice = 0, choose hardcoded rhythms # if this is the first iteration, ie "kick" # choose from some hard-coded kick rhythms # 2nd? same for hh # 3rd? same for sd if (i == 0): rhythm = str(random.choice(bdPats)) elif (i == 1): rhythm = str(random.choice(hhPats)) elif (i == 2): rhythm = str(random.choice(sdPats)) elif (i == 3): rhythm = str(random.choice(cpPats)) elif (i == 4): rhythm = str(random.choice(cyPats)) else: # print("LIBRARY") rhythm = str(random.choice(rhythmLibrary)) #rhythm = "struct \""+rhythm+"\"" elif (rChoice == 1): # GENERATE A SERIES OF 0's and 1's # choose how long the rhythm will be # if (oneBar == 1): # percLength = 16 # else: # percLength = int(random.randrange(4,8)) percLength = int(random.randrange(4,10)) #percSequence = genOnOff(percLength) #print(","+str(genOnOff(10))) #print(str(percSequence)) # how busy will it be?a # choose a random value # guide the random value based on the name # since we don't really want a kick on every 16th note # lots of hh = good, lots of cymbal = maybe not if (percName == "kick"): busyness = random.randrange(1,3) elif (percName == "sd"): busyness = random.randrange(0,4) elif (percName == "cp"): busyness = random.randrange(0,4) elif (percName == "hh"): busyness = random.randrange(3,9) elif (percName == "ride"): busyness = random.randrange(0,6) elif (percName == "cymbal"): busyness = random.randrange(0,1) else: busyness = random.randrange(0,7) generateBinary(busyness) rhythm = str(genOnOff(percLength,busyness,1)) elif (rChoice == 2): # GENERATE EUCLIDEAN RHYTHMS # treating the kick like the other tracks often generates # way too many events (like [t(11,16)]) which is usually # way too much for a techno kick! # here's I'm trying to moderate that if (percName == "kick"): # do something maxEuclidEvents = 5 else: maxEuclidEvents = 13 #print("MAX EUCLID EVENTS: "+str(maxEuclidEvents)) rhythm = str(euclid(random.choice(dividers),maxEuclidEvents)) #print(indent+indent+euclid(random.choice(dividers))) # else: # print("LIBRARY") rhythm = str(random.choice(rhythmLibrary)) #rhythm = "struct \""+rhythm+"\"" fullRhythm = str(rhythm) + " " + fullRhythm # remove the trailing space fullRhythm = fullRhythm.rstrip() # add "struct" to the beginning # and enclose in < > print(indent+indent+"$ struct \"<"+fullRhythm+">\"") # roll the dice to get a length of the velocity pattern velocityLength = random.randint(2,16) notes = indent+indent+"$ n "+str(random.choice(percNotes)) velocity = indent+indent+"# gain \"{"+genVelocity(int(random.randrange(2,16)))+"}%16\"" midi = indent+indent+"# midichan "+str(percMidiChan) #print (# midichan "+str(percMidiChan)+" \n"+indent+indent"# gain \"{"+genVelocity(int(random.randrange(2,16)))+"}%16"+",") print(notes+"\n"+velocity+"\n"+midi+",") percMidiChan = percMidiChan + 1 # generate melodic tracks melodicMidiChan = melodyMidiChanStart #melodyMidiChanStart = 7 for i in range(melodyCount): print(indent+indent+"------------ bassline -------") print(indent+indent+"degradeBy 0 ") #print(", ") busyness = random.choice([0,1,2,3,4,5,6,6,6,6,7,7,7,7,8,8,8,9,9]) if (oneBar == 1): print(indent+indent+"$ struct \""+genOnOff(16,(busyness),1)+"\"") notes = genNotes(1) print(indent+indent+"$ "+notes) else: # how many 16th notes? thisLength = random.randint(3,16) #genOnOff(8,(busyness)) print(indent+indent+"$ struct \""+genOnOff(thisLength,(busyness),1)+"\"") notes = genNotes(0) print(indent+indent+"$ "+str(notes)) print(indent+indent+"# midichan "+str(melodicMidiChan)+" + n (-24),") melodicMidiChan = melodicMidiChan + 1 #### generate pads/chords # method: grab some of the notes from the last melodic/bassline # and build some chords with them that go with the bassline # sort of like the reverb trick, to create a pad from any melodic material # after we're done making the bassline, the melodicMidiChan will be # the last midi channel +1, so we can use that for the pad channel padMidiChan = melodicMidiChan for i in range(padCount): print(indent+indent+"------------ pad -------") # the goal: # n "[3,12,20,34]" # where all those numbers are played by the bassline # get the numbers from the bassline # it's already stored in the variable "notes" # but with brackets like this: [{23 6 5 23}%16] # we'll use regex to remove the extra stuff padNotes = notes.replace('[{','') padNotes = padNotes.replace(']','') padNotes = padNotes.replace('}','') padNotes = padNotes.replace('"','') padNotes = padNotes.replace('%16','') padNotes = padNotes.replace('n ','') padNotes = padNotes.rstrip() #print("notes:"+str(padNotes)) # now that we have the notes, as strings, put them into a list padNotesList = padNotes.split(" ") # convert this list to a dictionary so there aren't duplicates # https://www.w3schools.com/python/python_howto_remove_duplicates.asp padNotes = list(dict.fromkeys(padNotes)) #print(str(padNotesList)) # make a chord padsStart = "\"<" padsEnd = ">" # generate a sequence of chords pads = "" for i in range(random.choice(padBars)): pads = "["+ random.choice(padNotesList)+","+random.choice(padNotesList)+","+random.choice(padNotesList)+","+random.choice(padNotesList)+"]"+str(pads) # build the track print(indent+indent+"degradeBy 0 ") sometimesBeQuiet() # roll the dice, choose between a long steady pad # or more rhythmic hits #if (random.choice([1,1]) == 1): if (random.choice([0,1]) == 1): print(indent+indent+"$ struct \""+genOnOff(16,(busyness),1)+"\"") print(indent+indent+"$ n "+padsStart+pads+padsEnd+"/"+str(random.choice(padSpeed))+"\"") print(indent+indent+"# midichan "+str(padMidiChan)+" + n (0),") ############## MIDI CCS ############# # the goal: # do reverb/delay throws for i in range(midiCCs): print("midicc: "+i) ## put the finishing touches on the beautiful stack! :-) :-) print(indent+indent+"] # sunvox")