# autotracker-bottomup - the quite a few times more ultimate audio experience # by Ben "GreaseMonkey" Russell, 2011. Public domain. # # # # BUGS: # - sometimes gets stuck in an infinite loop. attempted to alleviate it but it doesn't work. # - i think it sometimes jumps further than an octave in some situations # oh and: # - has moments where it sounds bad. if you can fix this for good, let me know! import sys, struct, random import math # tunables. SMP_FREQ = 44100 SMP_16BIT = True # IT format constants. leave these alone. IT_FLAG_STEREO = 0x01 IT_FLAG_VOL0MIX = 0x02 # absolutely useless since 1.04. IT_FLAG_INSTR = 0x04 IT_FLAG_LINEAR = 0x08 IT_FLAG_OLDEFF = 0x10 # don't enable this, it's not well documented. IT_FLAG_COMPATGXX = 0x20 # don't enable this, it's not well documented. IT_FLAG_PWHEEL = 0x40 # MIDI-related, don't use IT_FLAG_USEMIDI = 0x80 # undocumented MIDI crap, don't use IT_SPECIAL_MESSAGE = 0x01 # MIDI-related, don't use IT_SPECIAL_UNK1 = 0x02 # undocumented MIDI crap, don't use IT_SPECIAL_UNK2 = 0x04 # undocumented MIDI crap, don't use IT_SPECIAL_HASMIDI = 0x08 # undocumented MIDI crap, don't use IT_SAMPLE_EXISTS = 0x01 IT_SAMPLE_16BIT = 0x02 IT_SAMPLE_STEREO = 0x04 # don't use, it's a modplugism. IT_SAMPLE_IT214 = 0x08 # not supported yet - don't use. IT_SAMPLE_LOOP = 0x10 IT_SAMPLE_SUS = 0x20 # mikmod doesn't like this, so be wary. IT_SAMPLE_LOOPBIDI = 0x40 IT_SAMPLE_SUSBIDI = 0x80 # IT_CONVERT_* refers to the sample conversion flags. # this is a VERY internal feature and not widely implemented. # please ensure you ONLY use IT_CONVERT_SIGNED for normal samples. # EXCEPTION: IT_CONVERT_DELTA + IT_SAMPLE_IT214 = IT215 compression. IT_CONVERT_SIGNED = 0x01 IT_CONVERT_BIGEND = 0x02 IT_CONVERT_DELTA = 0x04 IT_CONVERT_BYTEDELT = 0x08 IT_CONVERT_TXWAVE = 0x10 IT_CONVERT_STEREO = 0x20 # anyhow, here's some code. enjoy. IT_BASEFLG_SAMPLE = ( IT_SAMPLE_EXISTS | (IT_SAMPLE_16BIT if SMP_16BIT else 0) ) ########################## # # # IT MODULE HANDLING # # # ########################## # NOTE: Currently only writes data. class ITFile: def __init__(self): self.name = "autotracker-bu module" self.flags = IT_FLAG_STEREO self.highlight = 0x1004 self.ordlist = [] self.inslist = [] self.smplist = [] self.patlist = [] self.chnpan = [32 for i in xrange(64)] self.chnvol = [64 for i in xrange(64)] self.version = 0x0217 self.vercompat = 0x0200 self.flags = ( IT_FLAG_STEREO | IT_FLAG_VOL0MIX # in the exceptionally rare case it may help... | IT_FLAG_LINEAR ) self.special = ( IT_SPECIAL_MESSAGE ) self.gvol = 128 self.mvol = 48 self.tempo = random.randint(90,160) self.speed = 3 self.pitchwheel = 0 self.pansep = 128 self.message = ( "Generated with Autotracker-Bu\n" + "2011 Ben \"GreaseMonkey\" Russell - Public domain\n" ) def enqueue_ptr(self, call): self.ptrq.append((self.fp.tell(),call)) self.write("00PS") def save(self, fname): self.fp = open(fname,"wb") self.message_fixed = self.message.replace("\n\r","\n").replace("\n","\r") + "\x00\x00" self.ptrq = [] self.doheader(self) while self.ptrq: pos, f = self.ptrq.pop(0) t = self.fp.tell() self.fp.seek(pos) self.write(struct.pack(" h: h = v amp = self.boost / max(-l,h) #print amp for i in xrange(len(self.data)): self.data[i] *= amp def generate(self, *args, **kwargs): return [] class Pattern: def __init__(self, rows): self.rows = rows assert rows >= 4, "too few rows" # note, this is just so modplug doesn't whinge. IT can handle 1-row patterns. assert rows <= 200, "too many rows" # on the other hand, IT chunders if you have more than 200 rows. self.data = [[[253,0,255,0,0] for j in xrange(64)] for i in xrange(rows)] # these are the defaults... # - note = 253 (0xFD) # - instrument = 0 # - volume = 255 (0xFF) # - effect type = 0 # - effect parameter = 0 def write(self, fp): self.dopack() fp.write(struct.pack("= delay, "KS sample length cannot be less than its period" # DC filter dq = 0.0 # prefilter with filt0 qn = 0.0 q = 0.0 dl = -0.001 dh = 0.001 nvolcur = 1.0 nvoldec = 1.0 / (decay * SMP_FREQ) nlfsr = random.randint(1,0x7FFF) # generate up to "length" samples qf = 0.0 #noise[-1] l = [] i = 0 for j in xrange(intlen): #ov = noise[i] if nvolcur > 0.0: if nfrqctr >= 1.0: #nfrqval = random.random()*2.0-1.0 nfrqval = (1.0 if (nlfsr & 1) else -1.0) * 1.0 # skip a value to balance it a bit better if nlfsr == 1: nlfsr = 0x4000 if nlfsr & 1: nlfsr = (nlfsr>>1) ^ 0x6000 else: nlfsr >>= 1 nfrqctr -= 1.0 nfrqctr += nfrqmul qn = (nfrqval * nvolcur - qn) * filt0 + qn nvolcur -= nvoldec noise[i] += qn ov = q = noise[i] = (noise[i] - q) * filtn + q qf = (ov - qf) * filtf + qf dq += (qf - dq) * filtdc l.append(qf - dq) i = (i+1) % delay # set stuff self.lpend = intlen self.lpbeg = intlen - delay # return return l class Sample_Kicker(Sample): name = "Kicker" flags = IT_BASEFLG_SAMPLE boost = 1.8 def generate(self): vol_noise = 0.8 vol_sine = 1.2 vol_noise_decay = 1.0 / (SMP_FREQ * 0.01) vol_sine_decay = 1.0 / (SMP_FREQ * 0.2) q_noise = 0.0 kickmul = math.pi*2.0*150.0/SMP_FREQ offs_sine = 0.0 offs_sine_speed = kickmul offs_sine_decay = 0.9995 intlen = int(SMP_FREQ*0.25) l = [] for j in xrange(intlen): sv = max(-0.7,min(0.7,math.sin(offs_sine))) offs_sine += offs_sine_speed offs_sine_speed *= offs_sine_decay nv = (random.random()*2.0-1.0) q_noise += (nv - q_noise) * 0.1 nv = q_noise l.append(nv*vol_noise + sv*vol_sine) vol_noise -= vol_noise_decay if vol_noise < 0.0: vol_noise = 0.0 vol_sine -= vol_sine_decay if vol_sine < 0.0: vol_sine = 0.0 return l class Sample_NoiseHit(Sample): name = "Noise hit generator" flags = IT_BASEFLG_SAMPLE boost = 1.0 def generate(self, decay, filtl = 1.0, filth = 0.0): vol_noise = 1.0 vol_noise_decay = 1.0 / (SMP_FREQ * decay) ql = 0.0 qh = 0.0 intlen = int(SMP_FREQ*decay) l = [] for j in xrange(intlen): nv = (random.random()*2.0-1.0) ql += (nv - ql) * filtl qh += (nv - qh) * filth nv = ql - qh l.append(nv*vol_noise) vol_noise -= vol_noise_decay if vol_noise < 0.0: vol_noise = 0.0 return l class Sample_Hoover(Sample): name = "Hoover" flags = IT_BASEFLG_SAMPLE | IT_SAMPLE_LOOP boost = 1.0 def generate(self, freq): oscfrq = [ int(freq*(v + v*(random.random()*2.0-1.0)*0.002))/float(SMP_FREQ) for v in [0.25, 0.5, 1.0, 2.0] ] oscvibspeed = [float(random.randint(1,5))*2.0*math.pi/SMP_FREQ for i in xrange(4)] oscvibdepth = [0.5,0.4,0.2,0.2] oscoffs = [random.random() for i in xrange(4)] oscviboffs = [random.random() for i in xrange(4)] oscvol = [1.0, 1.0, 1.0, 0.55] attack = 0.03 atkvol = 0.0 atkspd = 1.0/(attack*SMP_FREQ) intlen = int(SMP_FREQ*(attack+1.0)) l = [] for i in xrange(intlen): v = 0.0 for j in xrange(4): ov = oscoffs[j]*2.0-1.0 vib = math.sin(oscviboffs[j])*oscvibdepth[j] oscoffs[j] += oscfrq[j] * (2.0**(vib/12.0)) if oscoffs[j] > 1.0: oscoffs[j] %= 1.0 oscviboffs[j] += oscvibspeed[j] v += oscvol[j]*ov atkvol += atkspd if atkvol > 1.0: atkvol = 1.0 l.append(v*atkvol) self.lpend = intlen self.lpbeg = int(intlen - SMP_FREQ*1.0 + 0.5) return l ########################## # # # RANDOCHORD FACTORY # # # ########################## class Key: def __init__(self): pass def get_base_note(self): return 60 def has_note(self, n): return True class Key_GenericOctave(Key): MASK = [True]*12 def __init__(self, basenote): self.basenote = basenote def get_base_note(self): return self.basenote def has_note(self, n): return self.MASK[(n-self.basenote)%12] class Key_Major(Key_GenericOctave): MASK = [ True,False, True,False, True, True,False, True,False, True,False, True, ] class Key_Minor(Key_GenericOctave): MASK = [ True,False, True, True,False, True,False, True, True,False, True,False, ] class Key_Major_Pentatonic(Key_GenericOctave): MASK = [ True,False, True,False, True, False,False, True,False, True,False, False, ] class Key_Minor_Pentatonic(Key_GenericOctave): MASK = [ True,False, False, True,False, True,False, True, False,False, True,False, ] class Strategy: def __init__(self, *args, **kwargs): self.setup(*args,**kwargs) self.gens = [] self.chused = 0 def setup(self, *args, **kwargs): self.key = Key_GenericOctave(60) def gen_add(self, gen): self.gens.append((self.chused,gen)) self.chused += gen.size() def get_key(self): return self.key class Strategy_Main(Strategy): def setup(self, basenote, keytype, patsize, blocksize, *args, **kwargs): self.basenote = basenote self.keytype = keytype self.patsize = patsize self.blocksize = blocksize self.key = keytype(basenote) self.pats = [] self.rspeed = 2**random.randint(2,3) self.rhythm = [3]+[0]*(self.rspeed-1)+[1]+[0]*(self.rspeed-1) self.rhythm *= (self.patsize//len(self.rhythm)) self.pat_idx = 0 self.newkseq() def newkseq(self): self.kseq = random.choice({ Key_Minor: [ [(0,Key_Minor),(-4,Key_Major),(5,Key_Major),(-2,Key_Major)], [(0,Key_Minor),(-2,Key_Major),(-4,Key_Major),(-5,Key_Minor)], ], Key_Major: [ [(0,Key_Major),(-5,Key_Major),(-3,Key_Minor),(5,Key_Major)], [(0,Key_Major),(0,Key_Major),(-7,Key_Minor),(-5,Key_Major)], ], }[self.keytype]) self.kseq2 = random.choice({ Key_Minor: [ [(3,Key_Major),(0,Key_Minor),(-4,Key_Major),(-2,Key_Major)], [(-4,Key_Major),(-2,Key_Major),(0,Key_Minor),(-2,Key_Major)], ], Key_Major: [ [(2,Key_Minor),(0,Key_Major),(-3,Key_Minor),(0,Key_Major)], [(-3,Key_Minor),(-5,Key_Major),(-7,Key_Major),(-5,Key_Major)], ], }[self.keytype]) def get_pattern(self): pat = Pattern(self.patsize) kseq = self.kseq2[:] if self.pat_idx % 8 >= 4 else self.kseq[:] for i in xrange(0,self.patsize,self.blocksize): k,kt = kseq.pop(0) kchord = kt(self.basenote+k) for chn,gen in self.gens: gen.apply_notes(chn, pat, self, self.rhythm, i, self.blocksize, self.key, kchord) kseq.append(k) self.pats.append(pat) self.pat_idx += 1 return pat def get_key(self): return self.key class Generator: def __init__(self, *args, **kwargs): pass def size(self): return 1 def apply_notes(self, chn, pat, strat, rhythm, bbeg, blen, kroot, kchord): pass class Generator_Bass(Generator): def __init__(self, smp, *args, **kwargs): self.smp = smp def size(self): return 1 def apply_notes(self, chn, pat, strat, rhythm, bbeg, blen, kroot, kchord): base = kchord.get_base_note() leadin = 0 for row in xrange(bbeg, bbeg+blen, 1): if rhythm[row]&1: n = base-12 if random.random() < 0.5 else base pat.data[row][chn] = [n, self.smp, 255, 0, 0] if leadin != 0 and random.random() < 0.4: gran = 2 count = 1 #if random.random() < 0.2: # gran = 1 if leadin > gran*2 and random.random() < 0.4: count += 1 if leadin > gran*3 and random.random() < 0.4: count += 1 for j in xrange(count): pat.data[row-(j+1)*gran][chn] = [ base+12 if random.random() < 0.5 else base ,self.smp ,0xFF ,ord('S')-ord('A')+1 ,0xC0 + random.randint(1,2) ] if random.random() < 0.2: pat.data[row][chn][0] += 12 if random.random() < 0.4: pat.data[row][chn][3] = ord('S')-ord('A')+1 pat.data[row][chn][4] = 0xC0 + random.randint(1,2) else: pat.data[row+2][chn] = [254, self.smp, 255, 0, 0] leadin = 0 else: leadin += 1 class Generator_AmbientMelody(Generator): MOTIF_PROSPECTS = [ # 1-steps [1], [2], [3], # 2-steps [1,3], [2,3], [2,4], # niceties [5,7], [5,12], [7,12], [7], [5], [12], # 3-chords [3,7], [4,7], # 4-chords [3,7,10], [3,7,11], [4,7,10], [4,7,11], # turns and stuff [1,0], [2,0], [1,-1,0], [1,-2,0], [2,-1,0], [2,-2,0], ] def __init__(self, smp, *args, **kwargs): self.smp = smp self.beatrow = 2**random.randint(2,3) self.lq = 60 self.ln = -1 self.mq = [] self.nq = [] def size(self): return 1 def apply_notes(self, chn, pat, strat, rhythm, bbeg, blen, kroot, kchord): base = kchord.get_base_note() if bbeg == 0: self.lq = base self.ln = -1 self.mq = [] self.nq = [] pat.data[bbeg][chn] = [self.lq, self.smp, 255, 0, 0] self.nq.append(bbeg) #self.ln = self.lq stabbing = False row = bbeg while row < bbeg+blen: if pat.data[row][chn][0] != 253: self.nq.append(row) row += self.beatrow continue q = 60 if self.mq: if stabbing or random.random() < 0.9: n = self.mq.pop(0) self.ln = n pat.data[row][chn] = [n, self.smp, 255, 0, 0] self.nq.append(row) if not self.mq: self.lq = n if random.random() < 0.2 or stabbing: row += self.beatrow // 2 stabbing = not stabbing else: row += self.beatrow else: row += self.beatrow elif row-bbeg >= 2*self.beatrow and random.random() < 0.3: backstep = random.randint(3,min(10,row//(self.beatrow//2)))*(self.beatrow//2) print "back", row, backstep for i in xrange(backstep): if row-bbeg >= blen: break pat.data[row][chn] = pat.data[row-backstep][chn][:] n = pat.data[row][chn][0] if n != 253: self.ln = self.lq = n row += 1 else: if len(self.nq) > 5: self.nq = self.nq[-5:] while True: kk = False while True: rbi = random.choice(self.nq) rbn = pat.data[rbi][chn][0] if self.ln != -1 and abs(rbn-self.ln) > 12: continue break m = None print rbn for j in xrange(20): m = random.choice(self.MOTIF_PROSPECTS) down = random.random() < (8.0+(self.ln-base))/8.0 if self.ln != -1 else 0.5 print m,rbn,down,base if down: m = [rbn-v for v in m] else: m = [rbn+v for v in m] if self.ln == m[0]: continue k = True for v in m: if not (kchord.has_note(v) and kroot.has_note(v)): k = False break if k: kk = True break if kk: break if rbn != self.ln: m = [rbn] + m print m self.mq += m # repeat at same row class Generator_Drums(Generator): def __init__(self, s_kick, s_hhc, s_hho, s_snare, *args, **kwargs): self.s_kick = s_kick self.s_hhc = s_hhc self.s_hho = s_hho self.s_snare = s_snare self.beatrow = 2**random.randint(1,2) def size(self): return 3 def apply_notes(self, chn, pat, strat, rhythm, bbeg, blen, kroot, kchord): for row in xrange(bbeg,bbeg+blen,self.beatrow): vol = 255 smp = self.s_hhc if not (rhythm[row]&2): if (row&8): vol = 48 if (row&4): vol = 32 if (row&2): vol = 16 if (row&1): vol = 8 if random.random() < 0.2: smp = self.s_hho pat.data[row][chn] = [60, smp, vol, 0, 0] for row in xrange(bbeg,bbeg+blen,2): if random.random() < 0.1 and not rhythm[row]&1: pat.data[row][chn+1] = [60,self.s_kick,255,0,0] did_kick = False for row in xrange(bbeg,bbeg+blen,1): if rhythm[row]&1: if did_kick: pat.data[row][chn+2] = [60,self.s_snare,255,0,0] else: if random.random() < 0.1: pat.data[row+2][chn+1] = [60,self.s_kick,255,0,0] else: pat.data[row][chn+1] = [60,self.s_kick,255,0,0] did_kick = not did_kick ################# # # # BOOTSTRAP # # # ################# MIDDLE_C = 220.0 * (2.0 ** (3.0 / 12.0)) print "Creating module" itf = ITFile() print "Generating samples" # these could do with some work, they're a bit crap ATM --GM # note: commented a couple out as they use a fair whack of space and are unused. SMP_GUITAR = itf.smp_add(Sample_KS(name = "KS Guitar", freq = MIDDLE_C/2, decay = 0.005, nfrqmul = 1.0, filt0 = 0.1, filtn = 0.6, filtf = 0.0004, length_sec = 1.0)) SMP_BASS = itf.smp_add(Sample_KS(name = "KS Bass", freq = MIDDLE_C/4, decay = 0.005, nfrqmul = 0.5, filt0 = 0.2, filtn = 0.2, filtf = 0.005, length_sec = 0.7)) #SMP_PIANO = itf.smp_add(Sample_KS(name = "KS Piano", freq = MIDDLE_C, decay = 0.07, nfrqmul = 0.02, filtdc = 0.1, filt0 = 0.09, filtn = 0.6, filtf = 0.4, length_sec = 1.0)) #SMP_HOOVER = itf.smp_add(Sample_Hoover(name = "Hoover", freq = MIDDLE_C)) SMP_KICK = itf.smp_add(Sample_Kicker(name = "Kick")) SMP_HHC = itf.smp_add(Sample_NoiseHit(name = "NH Hihat Closed", gvol = 32, decay = 0.03, filtl = 0.99, filth = 0.20)) SMP_HHO = itf.smp_add(Sample_NoiseHit(name = "NH Hihat Open", gvol = 32, decay = 0.5, filtl = 0.99, filth = 0.20)) SMP_SNARE = itf.smp_add(Sample_NoiseHit(name = "NH Snare", decay = 0.12, filtl = 0.15, filth = 0.149)) print "Generating patterns" strat = Strategy_Main(random.randint(50,50+12-1)+12, Key_Minor if random.random() < 0.6 else Key_Major, 128, 32) strat.gen_add(Generator_Drums(s_kick = SMP_KICK, s_snare = SMP_SNARE, s_hhc = SMP_HHC, s_hho = SMP_HHO)) strat.gen_add(Generator_AmbientMelody(smp = SMP_GUITAR)) strat.gen_add(Generator_Bass(smp = SMP_BASS)) for i in xrange(6): itf.ord_add(itf.pat_add(strat.get_pattern())) print "Saving" # pick a random name RN_NOUNS = [ ("cat","cats"),("kitten","kittens"), ("dog","dogs"),("puppy","puppies"), ("elf","elves"),("knight","knights"), ("wizard","wizards"),("witch","witches"),("leprechaun","leprechauns"), ("dwarf","dwarves"),("golem","golems"),("troll","trolls"), ("city","cities"),("castle","castles"),("town","towns"),("village","villages"), ("journey","journeys"),("flight","flights"),("place","places"), ("bird","birds"), ("ocean","oceans"),("sea","seas"), ("boat","boats"),("ship","ships"), ("whale","whales"), ("brother","brothers"),("sister","sisters"), ("viking","vikings"),("ghost","ghosts"), ("garden","gardens"),("park","parks"), ("forest","forests"),("ogre","ogres"), ("sweet","sweets"),("candy","candies"), ("hand","hands"),("foot","feet"),("arm","arms"),("leg","legs"), ("body","bodies"),("head","heads"),("wing","wings"), ("gorilla","gorillas"),("ninja","ninjas"),("bear","bears"), ("vertex","vertices"),("matrix","matrices"),("simplex","simplices"), ("shape","shapes"), ("apple","apples"),("pear","pears"),("banana","bananas"), ("orange","oranges"), ("demoscene","demoscenes"), ("sword","swords"),("shield","shields"),("gun","guns"),("cannon","cannons"), ("report","reports"),("sign","signs"),("year","years"),("age","ages"), ("blood","bloods"),("breed","breeds"),("monument","monuments"), ("cheese","cheeses"),("horse","horses"),("sheep","sheep"),("fish","fish"), ("dock","docks"),("tube","tubes"),("road","roads"),("path","paths"), ("tunnel","tunnels"),("retort","retorts"), ("toaster","toasters"),("goat","goats"), ("tofu","tofus"),("vine","vines"),("branch","branches"), ] RN_ADJECTIVES = [ "tense","grand","pleasing","absurd","offensive","crazed", "magic","lovely","tired","lively","tasty","jealous", "red","orange","yellow","green","blue","purple","pink","brown", "white","black","cheap","blazed","biased","sweet", "invisible","hidden","secret","long","short","tall","broken", "random","fighting","hunting","eating","drinking","drunk", "weary","walking","running","flying","strong","weak", "woeful","tearful","rich","poor","awoken","sacred", ] RN_VERBS = [ # TODO ] RN_PATTERNS = [ "the (n[0])'s (n[0,1])", "(N[0])'s (n[0,1])", "(n[0,1]) of (N[0,1])", "on the (n[0])'s (n[0,1])", "(n[0,1]) of the (n[0,1])", "the (a) (n[0,1])", "(A) (n[0])", "(a) (n[1])", "(a) and (a)", "(N[0,1]) and (N[0,1])", ] def randoname(): pat = random.choice(RN_PATTERNS) while "(" in pat: ps, po, pp = pat.partition("(") p, pc, pn = pp.partition(")") assert pc == ")", "expected ')' in name pattern" p = random.choice(p.split("|")) if p.startswith("n") or p.startswith("N"): idx = random.choice(eval(p[1:])) w = random.choice(RN_NOUNS)[idx] if idx in [0] and p.startswith("N"): if w[0] in "aeiouAEIOU": p = "an " + w else: p = "a " + w else: p = w elif p.startswith("a") or p.startswith("A"): w = random.choice(RN_ADJECTIVES) if p.startswith("A"): if w[0] in "aeiouAEIOU": p = "an " + w else: p = "a " + w else: p = w else: raise Exception("invalid name pattern type") pat = ps + p + pn return pat name = randoname() itf.name = name fname = "bu-%s.it" % name.replace(" ","-").replace("'","") if len(sys.argv) > 1: fname = sys.argv[1] itf.save(fname) print "Done" print "Saved as \"%s\"" % fname