"""Extract sequence,bank and program files from SQ80 dump""" from os.path import * import os from struct import * def getVarLen( value ): """ create a bytearray with VARIABLE QUANTITY values for a given integer (max. 0FFFFFFF) """ buf = value & 0x7F buflist = [] buflist.append((buf)) while ( value >> 7 > 0 ): value = value >> 7 buf |= ((value & 0x7F) | 0x80) buflist.append((buf)) while (True): if (buf & 0x80): buf >>= 8 else: break if (len(buflist) > 4): buflist = [] buflist.reverse() sbuf = bytearray() for x in buflist: sbuf.append((x)) return sbuf def getSmallestOffset( rst ): """ give back an integer with the smallest amount of various offsets """ nr = len(rst) val = [] for eln in range(nr): val.append(rst[eln][0]) vmin=min(val) return vmin def correctRST( rst, vmin ): """ correct a list in every first element with the value vmin """ for el in rst: el[0] -= vmin return rst # next def could be replaced by int.from_bytes(b(), 'big') def highlow( HL ): """ create a integer from a two byte valuefield """ if (len(HL) != 2): return (-1) # error val=HL[0]*256+HL[1] return (val) # next def could be replaced by x.to_bytes(l,'big') or pack-method def hexdata( num, l = 4 ): """ extract a number(integer) to a byte field in big-endian-order the second param is for filling the array with 0-bytes in the left """ nrl=[] w = num while ( w > 0 ): nr = w % 256 nrl.append(chr(nr)) w = int(w / 256) if (l > len(nrl)): while (len(nrl) < l): nrl.insert(0,"\x00") if (len(nrl) > l): return [] # error return nrl def midiNote( note ): midinotes =["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"] notetx = midinotes[note%12] octave = int(note/12) return (notetx + str(octave)) def showSongs(songs, sngnames): text = "" l = len(songs) for a in range(l): text += ("*** %s \n" % sngnames[a]) print (sngnames[a]) steps = int(len(songs[a])/3) for c in range(steps): s = c * 3 # 3 bytes for one song step sqn = (songs[a][s]) + 1 # SEQ-entry in byte starting with 0 rep = (songs[a][s+1]) - 1 trp = (songs[a][s+2]) print ("Step %2i SEQ-Nr.%2.2i Rep.:%2i Transp.:%2i" % (c+1, sqn, rep, trp)) text += ("Step %2i SEQ-Nr.%2.2i Rep.:%2i Transp.:%2i\n" % (c+1, sqn, rep, trp)) return text ####################################################################### # Written 2021-02-06 Gerhard Pichelhofer (c) # in python 3.4 # Finished 2021-03-05 print ("#######################################") print ("# #") print ("# ##### ###### ###### ###### #") print ("# # # # # # # # #") print ("# ##### # # ###### # # #") print ("# # # ## # # # # #") print ("# ##### #######_ ###### ###### #") print ("# #") print ("#######################################") print ("#######################################") print ("# ENSONIQ SQ80 SQX to MIDI maker v2 #") print ("# 2021-03-05 G.Pichelhofer (c)#") print ("#######################################") print ("Please type in the SQ80 (sqx-)filename") inpfile = input("Filename: ") if (exists(inpfile)): if (inpfile.endswith("sqx")): filename=inpfile else: input("This is not a valid Song-/Seq-File (*.sqx)") exit() else: input("File not found ! Please check again") exit() outdir = filename + "-dir" outfile = basename(inpfile) + ".txt" # reading all the input with open(filename, "rb") as fin: g=fin.read() allseq = [] # list for one or all sequences if (g[0:3] == b'm\xb6m'): input("File is empty !") exit() print ("") if (len(g) == 65536): filelist = ("-- OneSequence file\n") print ("-- OneSequence file\n") lensq80 = highlow(g[0:2]) + 1 # length of the ONE sequence seq = g[0:lensq80] # bytefield with the seq data allseq.append(seq) bars = highlow(seq[2:4]) # number of bars seqnum = ("%2.2i" % seq[4]) # seq number seqnums = [seq[4]] filelist += ("SEQ" + seqnum + " " + str(bars) + " Bars\n") print ("SEQ" + seqnum + " " + str(bars) + " Bars") input("Please type a key for continue ") else: filelist = ("-- AllSequence file\n") print ("-- AllSequence file\n") if (g[65536] != 0): input("Sorry\nFile is maybe empty") exit() sngindstart = 65713 # first byte of song-references sngnamstart = 65833 # first byte of song-names sngnames = [] songs = [] # making a list with all the data for the songs for b in range(20): ind = sngindstart + b * 2 sngindex = g[ind:(ind + 2)] flag = g[sngindstart + 100 + b] # flags starting at #65713+100 if (flag == 255): # flag FF for song is empty continue ind = sngnamstart + b * 6 sngname = g[ind:(ind + 6)] sngnames.append(sngname.decode()) beginn = highlow(sngindex) lensng = highlow(g[beginn:(beginn+2)]) # length of the song sng = g[beginn:(beginn + lensng - 1)] # song steps steps = sng[5] # number of steps songs.append(sng[6:]) filelist += ("SNG%2.2i %s %2i Steps\n" % (b ,sngname,steps)) print ("SNG%2.2i %s %2i Steps" % (b ,sngname,steps)) seqindstart = 65593 # first byte of seq-references seqnums = [] sequences = [] # making a list with all the data for the sequences for a in range(60): ind = seqindstart + a * 2 seqindex = g[ind:(ind + 2)] flag = g[seqindstart + 160 + a] # flags starting at #65593+160 if (flag == 255): # flag FF for sequence is empty continue beginn = highlow(seqindex) lensq80 = highlow(g[beginn:(beginn+2)]) + 1 # length of the sequence seq=g[beginn:(beginn + lensq80)] bars = highlow(seq[2:4]) # number of bars if (bars == 0): continue sequences.append(seq) seqnum = ("%2.2i" % (seq[4] + 1)) # seq number seqnums.append(seqnum) filelist += ("SEQ" + seqnum + " " + str(bars) + " Bars\n") print ("SEQ" + seqnum + " " + str(bars) + " Bars") print ("Number of songs: " + str(len(songs))) print ("Number of sequences: " + str(len(sequences))) print ("End of listing") print ("=============================") print ("| Selecting a special SEQ |") print ("| (only the number e.g. 04) |") print ("| or selecting ALL (-1) |") print ("| or selecting SONGS (-2) |") print ("| or quit program (0) |") print ("=============================") snum = input("Please type in a sequencenumber: ") if (snum == "-1"): # adding all sequences allseq = sequences elif (snum == "-2"): sngText = showSongs(songs,sngnames) sngfile = outdir + "/Songsteps.txt" if (exists(outdir) == False): os.mkdir(outdir) with open(sngfile, "w") as fout: fout.write(sngText) elif (snum == '0'): exit() else: if (seqnums.count(snum) == 0): input("Sequence not found") exit() # search the chosen sequence sindex = seqnums.index(snum) seq = sequences[sindex] allseq.append(seq) filelist += ("--------------------------------\n") if (exists(outdir) == False): os.mkdir(outdir) os.chdir(outdir) print ("MIDI/SNG files are saved in directory %s" % outdir) print ("-------------------------------------------------") for oneseq in allseq: print ("----- STARTING -----") lensq80 = highlow(oneseq[0:2]) + 1 # length of the first sequence #f = g[0:lensq80] # bytefield with the seq data barsq80 = highlow(oneseq[2:4]) # number of bars namsq80 = ("%2.2i" % (oneseq[4] + 1)) # seq number fileout = format("SEQ%s.mid" % (namsq80)) # define the filename print ("Output: " + str(fileout)) multipack = highlow(oneseq[5:7]) # multipack y-n sigsq80 = (oneseq[7] & 127) # temp ### time signatur numerator-denominator tx1s=[1,1,2,3,2,4,5,3,6,7,4,8,9,5,10,11,6,12,13,7,14,15,8,16,17,9,18,19,10,20,21] tx2s=[8,4,8,8,4,8,8,4,8,8,4,8,8,4, 8, 8,4, 8, 8,4, 8, 8,4, 8,16,4, 8, 8, 4, 8, 8] tx1 = pack('>b',tx1s[sigsq80]) # numerator tx2 = pack('>b',int((tx2s[sigsq80])/4 + 1)) # denominator #if (sigsq80 < 128): # loop = True # --> no need of this temposq80 = oneseq[8] # tempo in sq80 print ("Tempo: " + str(temposq80)) tempo = int( 1/temposq80*60*1000000 ) # tempo in midi (100bpm -> 600000micSpB) # one quaternote is 600000/4 = 150000 ticks --> not correct in description # value is microseconds per bar trk,trkprog,trkprg,trkchan,trkstat,trkvolm,trkvol = [],[],[],[],[],[],[] status = ['SEQ','LOCAL','MIDI','BOTH'] # trackdata for 1 to 8 for x in range(8): p = x * 3 + 9 trk.append(oneseq[p:(p + 3)]) for x in trk: pr = (x[0] + 1) & 127 trkprg.append(pr) trkprog.append("%5i" % pr) # max. 120 Prog.number if ((x[1] & 16) == 0): trkchan.append('-----') # track is not used else: ch = (x[1] & 15)+ 1 trkchan.append("%5i" % ch) # 1 .. 16 MIDI chan st = x[1] >> 6 trkstat.append("%5s" % status[st]) # 0 .. 3 SEQ/LOCAL/MIDI/BOTH vol = (x[2]>>1) trkvol.append(vol * 2) trkvolm.append("%5i" % (vol)) # 0 .. 63 volume print ("Trackprograms: " + str(trkprog)) print ("MIDIchannels : " + str(trkchan)) print ("Trackstatus : " + str(trkstat)) print ("Trackvolumes : " + str(trkvolm)) filelist += format("SEQ%s %i Bars Tempo:%3i\n" % (namsq80, barsq80, temposq80)) filelist += ("Trackprograms: " + str(trkprog) + "\n") filelist += ("MIDIchannels : " + str(trkchan) + "\n") filelist += ("Trackstatus : " + str(trkstat) + "\n") filelist += ("Trackvolumes : " + str(trkvolm) + "\n") input ("Please continue with enter") print ('====================== EVENTS ===========================') ### Events starting at #34 evsq = oneseq[33:] # skipping the headers rst = [] # for the later note off events evx = bytearray() # summary of all events for writing to file x = 0 # counter offset = False # is set or not while (x < len(evsq)): a = evsq[x] # if SYNC is missing ( start or if event occured without an OFFSET ...) if (offset == False): evx.append(0) offsetval = 0 offset = True if (a < 88): # Note ON note = a + 21 # MIDI Note b = evsq[x+1] track=(b & 7) + 1 # 0 .. 15 (+ 1) MIDI channel veloc=((b & (248)) >> 1) + 2 # velocity c = evsq[x+2] # if bit #7 is 0 an offset is coming too notel=(c & 127) # 0 ... 127 note length print ("NOTE ON: L=%i CH=%i %s" % (notel,track,midiNote(note)), end=' | ') x += 3 if (c < 128): noOffset=False d = evsq[x] # 0 ... 127 print ("Offset " + str(d)) rst.append([d, 0xFF, -1]) # dist for Offset into buffer x += 1 else: noOffset=True print ("NO Offset") rst.append([0, 0xFF, -1]) # dist for noOffset into buffer # make NOTE ON evx.append(0x90 + track) evx.append(note) evx.append(veloc) offset=False if (notel != 0): # NOTE OFF event into buffer rstev = [notel, track, note] rst.append(rstev) # else --> NOTE OFF event is coming later rst.sort(reverse=True) # high to low first arg print (" Buffer: " + str(rst)) if (noOffset): nextEv = rst.pop() if (nextEv[2] == -1): # only offset making print (" setOffset " + str(nextEv[0]), end=' |') # thats always 0 offsetval = nextEv[0] ## offset evx += getVarLen(offsetval) offset = True correctRST(rst, nextEv[0]) print (" >remain" + str(rst)) else: rst2 = rst[:] rst2.reverse() for xl in rst2: if (xl[2] == -1): ####### make Offset ## ################# print (" setOffset d=" + str(xl[0]), end=' |') offsetval = xl[0] evx += getVarLen(offsetval) offset = True rst.remove(xl) correctRST(rst, xl[0]) break else: ####### make NOTE OFF ################# print (" makeNoteOff d=%i %s" % (xl[0], midiNote(xl[2])), end=' |') offsetval = xl[0] ## offset evx += getVarLen(offsetval) evx.append(0x80+xl[1]) ## track evx.append(xl[2]) ## note evx.append(96) ## release velocity (std) offset = False correctRST(rst, xl[0]) rst.remove(xl) print (" >remain" + str(rst)) elif (a < 176): b = evsq[x+1] note = a + 21 - 88 # MIDI Note if (b < 128): # Note OFF track = 0x80 + ( b & 7 ) + 1 evx.append(track) # 0 .. 15 (+ 1) MIDI channel evx.append(note) evx.append(96) # no value coming from SQ80 -> std=96 offset = False print ("NOTE OFF: CH=%i %s" % ((track - 0x80),midiNote(note))) x += 2 else: # Aftertouch val = evsq[x+2] track = 0xA0 + ( b & 7 ) evx.append(track) # 0 .. 15 (+ 1) MIDI channel evx.append(note) evx.append(val) offset = False print ("Aftertouch " + str(track - 0x80) + " " + str(note) + " " + str(val)) x += 3 elif (a < 240): # Controller data b = evsq[x+1] track = 0xB0 + ( a & 7 ) # common controller message # track may be overwritten by #0 and #5 sq80typ = (a >> 3) - 0x16 val = (b & 0x7F) print ("CTRL", (track-0xB0), "SQ80-ctrl:"+str(sq80typ), val) if (sq80typ == 0): # pitchbend (0 ... 127) track = 0xE0 + ( a & 7 ) val12 = val*128 evtyp = val12 % 128 # this is the lower part of value val = int(val12 / 128) # this is the higher part of value elif (sq80typ == 1): # sustain evtyp = 64 elif (sq80typ == 2): # modwheel evtyp = 1 elif (sq80typ == 3): # foot controller evtyp = 4 elif (sq80typ == 4): # xctrl evtyp = 11 elif (sq80typ == 5): # channel pressure track = 0xD0 + ( a & 7 ) elif (sq80typ == 6): # volume evtyp = 7 elif (sq80typ == 7): # data slider evtyp = 6 evx.append(track) # 0 .. 15 (+ 1) MIDI channel if (sq80typ != 5): # no eventtyp for channel pressure evx.append(evtyp) evx.append(val) offset = False x += 2 # pitchbend is a other story .... elif (a == 240): # F0 unknown input("F0 event ocurred. Please press a key") elif (a == 241): # Packet marker nextpack = highlow(evsq[(x+1):(x+3)]) print ("Next pack: " + str(nextpack)) # its ok, nothing to do # input ("Packet marker. Please press a key") x += 3 elif (a == 242): # F2 unknown input("F2 event ocurred. Please press a key") elif (a == 243): # SEQ end print ("SEQ end") # correction §2021-02-28 # error while data is coming after SEQ end x = len(evsq) #x += 2 §2021-02-28 # elif (a == 244): # 2 byte offset evoffset = ( evsq[x+1]*256 + evsq[x+1] ) print ("2 byte offset") if (offset == False): evx += getVarLen(evoffset) input ("Hey. Thats impossible !. Please press a key") else: loffset = len(getVarLen(offsetval)) if (loffset != 1): input (loffset) for z in range(loffset): evx.pop() #offold = evx.pop() # add offset amount to the old one evx += (getVarLen(offsetval + evoffset)) offsetval = offsetval + evoffset offset = True x += 3 elif (a == 245): # SYNC evoffset = evsq[x+1] print ("SYNC " + str(evoffset)) if (offset == False): evx += getVarLen(evoffset) input ("Hey. Thats impossible !. Please press a key") else: loffset = len(getVarLen(offsetval)) if (loffset != 1): input (loffset) for z in range(loffset): evx.pop() # add offset amount to the old one evx += (getVarLen(offsetval + evoffset)) x += 2 offsetval = offsetval + evoffset offset = True else: # unknown Error input("unknown event ocurred: " + str(a)) #now the last events rst.reverse() rst2 = rst[:] for xl in rst2: if (xl[2] > -1): print ("NoteOff d=%i %s" % (xl[0], midiNote(xl[2]))) evx.append(0x80+xl[1]) ## track evx.append(xl[2]) ## note evx.append(96) ## release velocity print (xl) evx.append(xl[0]) ## offset correctRST(rst, xl[0]) #TODO ??? rst = [] nullbyte = b"\x00" f0 = "MThd".encode() f0a = pack('>l',6) f1 = pack('>h',0) # only one track synchron f2 = pack('>h',1) # number of tracks f3 = pack('>h',24) # Delta-time ticks per quarter note (480) f4 = "MTrk".encode() # MTrk lev = 54 + len(evx) + 24 + 24 + 8 f5 = pack('>l',lev) # length track [byte] f6 = b"\xff\x20\x01\x00" # its the base MIDI channel f7 = b"\xff\x01\x0c" # text f7a = "ENSONIQ SQ80".encode() f8 = b"\xff\x03\x05" # sequence name f8a = ("SEQ" + str(namsq80)).encode() f9 = b"\xff\x59\x02\x00\x00" # fine tuning f10 = b"\xff\x58\x04" # time signatur PRE f10a = b"\x18\x08" # time signatur SUF f11 = b"\xff\x51\x03" # new temp (mS per quaterBeat) f11a = pack('>l', tempo) # tempo #480 Delta-time ticks per quater note (instead of \x01\xe0) # data for the 8 channelprograms following ... f12 = b"\x00\xc0" + trkprg[0].to_bytes(1, 'big') f12 += b"\x00\xc1" + trkprg[1].to_bytes(1, 'big') f12 += b"\x00\xc2" + trkprg[2].to_bytes(1, 'big') f12 += b"\x00\xc3" + trkprg[3].to_bytes(1, 'big') f12 += b"\x00\xc4" + trkprg[4].to_bytes(1, 'big') f12 += b"\x00\xc5" + trkprg[5].to_bytes(1, 'big') f12 += b"\x00\xc6" + trkprg[6].to_bytes(1, 'big') f12 += b"\x00\xc7" + trkprg[7].to_bytes(1, 'big') # data for the 8 channelprograms following ... f13 = b"\x00\xb0\x07" + trkvol[0].to_bytes(1, 'big') f13 += b"\x00\xb1\x07" + trkvol[1].to_bytes(1, 'big') f13 += b"\x00\xb2\x07" + trkvol[2].to_bytes(1, 'big') f13 += b"\x00\xb3\x07" + trkvol[3].to_bytes(1, 'big') f13 += b"\x00\xb4\x07" + trkvol[4].to_bytes(1, 'big') f13 += b"\x00\xb5\x07" + trkvol[5].to_bytes(1, 'big') f13 += b"\x00\xb6\x07" + trkvol[6].to_bytes(1, 'big') f13 += b"\x00\xb7\x07" + trkvol[7].to_bytes(1, 'big') fend = b"\xff\x2f\x00" # end # start writing to file fout=open(fileout,"wb") fout.write(f0) fout.write(f0a) fout.write(f1) fout.write(f2) fout.write(f3) fout.write(f4) fout.write(f5) # Laenge track fout.write(nullbyte)# 1 byte fout.write(f6) # 4 byte fout.write(nullbyte)# 1 byte fout.write(f7) # 3 byte fout.write(f7a) # 12 byte fout.write(nullbyte)# 1 byte fout.write(f8) # 3 byte fout.write(f8a) # 5 byte fout.write(nullbyte)# 1 byte fout.write(f9) # 5 byte fout.write(nullbyte)# 1 byte fout.write(f10) # 3 byte fout.write(tx1) # 1 byte fout.write(tx2) # 1 byte fout.write(f10a) # 2 byte fout.write(nullbyte)# 1 byte fout.write(f11) # 3 byte fout.write(f11a[1:])# 3 byte (first byte skipped) fout.write(f12) # 8x3 byte fout.write(f13) # 8x3 byte fout.write(evx) # 8 byte fout.write(fend) # 3 byte - Gesamt 54 byte fout.close() print ('MIDI file created : ' + str(fileout)) os.chdir("..") with open(outfile, "w") as fout: fout.write(filelist) print ("-----------------------------------------------") print ("Tracks 1-8 from the SQ80 are now MIDI-channels,") print ("independent of the MIDI-channel of the Track ") print ("-----------------------------------------------") print ('Listing file created : ' + outfile) print ("-----------------------------------------------") input ('Conversion into MIDI ready. Please type enter ')