Last active
June 5, 2025 01:47
-
-
Save quotepilgrim/2b0546321af879629fbff315685c5c95 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """autotracker-bottomup - the quite a few times more ultimate audio experience | |
| by Ben "GreaseMonkey" Russell, 2011. Public domain. | |
| Modified to work with Python 3 by QuotePilgrim. This was probably not done in | |
| the most elegant way, all I did was run the script through 2to3 and added an if | |
| statement to the write() function to convert strings to binary data. The correct | |
| encoding that resulted in a working .it file was found by trial and error. | |
| A copy of the original file can be found at: | |
| https://github.com/wibblymat/ld24/blob/master/autotracker.py | |
| 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 range(64)] | |
| self.chnvol = [64 for i in range(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("<I",t)) | |
| self.fp.seek(t) | |
| f(self) | |
| self.fp.close() | |
| def w_msg(self, fp): | |
| fp.write(self.message_fixed) | |
| def doheader(self, fp): | |
| ordlist = self.ordlist[:] | |
| if (not ordlist) or ordlist[-1] != 0xFF: | |
| ordlist.append(0xFF) | |
| fp.write("IMPM") | |
| fp.write_padded(25, self.name) | |
| fp.write(struct.pack("<HHHHHHHHH" | |
| ,self.highlight | |
| ,len(ordlist),len(self.inslist),len(self.smplist),len(self.patlist) | |
| ,self.version,self.vercompat,self.flags,self.special | |
| )) | |
| fp.write(struct.pack("<BBBBBBH" | |
| ,self.gvol,self.mvol,self.speed,self.tempo | |
| ,self.pansep,self.pitchwheel,len(self.message_fixed) | |
| )) | |
| fp.enqueue_ptr(self.w_msg) | |
| fp.write("AtBu") | |
| fp.write(''.join(chr(v) for v in self.chnpan)) | |
| fp.write(''.join(chr(v) for v in self.chnvol)) | |
| fp.write(''.join(chr(v) for v in ordlist)) | |
| l = self.inslist + self.smplist + self.patlist | |
| for i in range(len(l)): | |
| self.enqueue_ptr(l[i].write) | |
| fp.write(struct.pack("<HI",0,0)) | |
| def write(self, data): | |
| if isinstance(data, str): | |
| data = data.encode(encoding="latin_1") | |
| self.fp.write(data) | |
| def write_padded(self, length, s, addnull = True): | |
| if len(s) < length: | |
| s += "\x00"*(length-len(s)) | |
| else: | |
| s = s[:length] | |
| assert len(s) == length, "STUPID! write_padded gave wrong length!" | |
| self.write(s) | |
| if addnull: | |
| self.write("\x00") | |
| def write_sample_array(self, data): | |
| # TODO: allow for a compressor / proper saturator | |
| if SMP_16BIT: | |
| for v in data: | |
| d = max(-0x8000,min(0x7FFF,int(v * 0x7FFF))) | |
| self.write(struct.pack("<h", d)) | |
| else: | |
| for v in data: | |
| d = max(-0x80,min(0x7F,int(v * 0x7F))) | |
| self.write(struct.pack("<b",d)) | |
| def smp_add(self, smp): | |
| self.smplist.append(smp) | |
| idx = len(self.smplist) | |
| return idx | |
| def pat_add(self, pat): | |
| idx = len(self.patlist) | |
| self.patlist.append(pat) | |
| return idx | |
| def ord_add(self, order): | |
| self.ordlist.append(order) | |
| class Sample: | |
| name = "feed me with a sample :)" | |
| flags = 0 | |
| boost = 1.0 | |
| fname = "AUTOTRKR.BU" | |
| gvol = 64 | |
| vol = 64 | |
| defpan = 32 # NOTE: set top bit (0x80) to actually use default pan | |
| convert = IT_CONVERT_SIGNED | |
| lpbeg = 0 | |
| lpend = 0 | |
| freq = SMP_FREQ | |
| susbeg = 0 | |
| susend = 0 | |
| vibspeed = 0 | |
| vibdepth = 0 | |
| vibrate = 0 | |
| vibtype = 0 | |
| def __init__(self, name = None, gvol = None, *args, **kwargs): | |
| if name != None: | |
| self.name = name | |
| if gvol != None: | |
| self.gvol = gvol | |
| self.data = self.generate(*args, **kwargs) | |
| self.length = len(self.data) | |
| self.amplify() | |
| def write(self, fp): | |
| fp.write("IMPS") | |
| fp.write_padded(12, self.fname) | |
| fp.write(struct.pack("<BBB", self.gvol, self.flags, self.vol)) | |
| fp.write_padded(25, self.name) | |
| fp.write(struct.pack("<BB", self.convert, self.defpan)) | |
| fp.write(struct.pack("<IIIIII" | |
| ,self.length, self.lpbeg, self.lpend, self.freq | |
| ,self.susbeg, self.susend)) | |
| fp.enqueue_ptr(self.write_data) | |
| fp.write(struct.pack("<BBBB", self.vibspeed, self.vibdepth, self.vibrate, self.vibtype)) | |
| def write_data(self, fp): | |
| fp.write_sample_array(self.data) | |
| def amplify(self): | |
| l = -0.0000000001 | |
| h = 0.0000000001 | |
| for v in self.data:#[len(self.data)//32:]: | |
| if v < l: | |
| l = v | |
| if v > h: | |
| h = v | |
| amp = self.boost / max(-l,h) | |
| #print amp | |
| for i in range(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 range(64)] for i in range(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("<HH", len(self.packbuf), len(self.data))) | |
| fp.write("AtBu") | |
| fp.write(''.join(chr(v) for v in self.packbuf)) | |
| def dopack(self): | |
| self.packbuf = [] | |
| lc = [[253,0,255,0,0] for j in range(64)] | |
| lm = [0x00 for j in range(64)] | |
| for l in self.data: | |
| for i in range(64): | |
| c = l[i] | |
| m = 0x00 | |
| if c[0] != 253: | |
| m |= 0x10 if c[0] == lc[i][0] else 0x01 | |
| if c[1] != 0: | |
| m |= 0x20 if c[1] == lc[i][1] else 0x02 | |
| if c[2] != 255: | |
| m |= 0x40 if c[2] == lc[i][2] else 0x04 | |
| if c[3] != 0 or c[4] != 0: | |
| m |= 0x80 if c[3] == lc[i][3] and c[4] == lc[i][4] else 0x08 | |
| v = i+1 | |
| if m != lm[i]: | |
| v |= 0x80 | |
| self.packbuf.append(v) | |
| if v & 0x80: | |
| self.packbuf.append(m) | |
| lm[i] = m | |
| if m & 0x01: | |
| self.packbuf.append(c[0]) | |
| lc[i][0] = c[0] | |
| if m & 0x02: | |
| self.packbuf.append(c[1]) | |
| lc[i][1] = c[1] | |
| if m & 0x04: | |
| self.packbuf.append(c[2]) | |
| lc[i][2] = c[2] | |
| if m & 0x08: | |
| self.packbuf.append(c[3]) | |
| self.packbuf.append(c[4]) | |
| lc[i][3] = c[3] | |
| lc[i][4] = c[4] | |
| self.packbuf.append(0) | |
| ######################### | |
| # # | |
| # BLEEPS 'N' BLOOPS # | |
| # # | |
| ######################### | |
| # YES! We actually have an almost decent sample synth! | |
| class Sample_KS(Sample): | |
| name = "Karplus-Strong synth" | |
| flags = IT_BASEFLG_SAMPLE | IT_SAMPLE_LOOP | |
| boost = 1.0 | |
| def generate(self, freq, decay, filtn, length_sec, nfrqmul = 1.0, filt0 = 1.0, filtf = 1.0, filtdc = 0.01): | |
| # generate waveform | |
| delay = int(SMP_FREQ/freq) | |
| noise = [0 for i in range(delay)] | |
| nfrqctr = 1.0 | |
| nfrqval = 0.0 | |
| intlen = int(SMP_FREQ*length_sec) | |
| assert intlen >= 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 range(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 range(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 range(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 range(4)] | |
| oscvibdepth = [0.5,0.4,0.2,0.2] | |
| oscoffs = [random.random() for i in range(4)] | |
| oscviboffs = [random.random() for i in range(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 range(intlen): | |
| v = 0.0 | |
| for j in range(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 range(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 range(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 range(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 range(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 range(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 range(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 range(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 range(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 range(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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment