This is an old revision of the document!
Pythonista: Ableton Live Set (ALS) to MIDI converter script
This Pythonista script installs a share extension to convert Ableton Live Set export files into MIDI files containing the notes of the exported tracks.
How to install:
- Either
- Download the python script, long press the file in the files app, select share, choose 'Run Pythonista Script' and then 'Import File'
- or
- Copy the script code block, open Pythonista, create a new file named ALS_to_MIDI.py and paste the clipboard
- In Pythonista settings/App Extensions select 'Share Extension Shortcuts'
- Use the + sign to add a extension
- Select the downloaded python script
- Set the custom title: ALS to MIDI
- Select 'Primaries_Expand' as icon
- Select a pleasing icon color
How to use:
- In the files app, long press the exported ALS file you want to convert
- In the options popup, select share
- In the share popup, select 'Run Pythonista Script' and then select the 'ALS to MIDI' extension
- ALS_to_MIDI.py
# Ableton MIDI clip zip export to MIDI file converter # Original script by MrBlaschke # Usability enhancements by rs2000 # Dec 8, 2019 # # Original request and idea by Svetlovska import sys import os import tempfile import xml.etree.ElementTree as ET import xml.etree as XTree from xml.etree.ElementTree import fromstring, ElementTree #thisis the old (default) library which does not support pitch-bend-data #from midiutil import MIDIFile import console import io import appex import ui from zipfile import ZipFile from zipfile import BadZipfile import gzip import binascii from time import sleep #custom (newer) version - ahead of the Pythonista version #get the code from: https://github.com/MarkCWirt/MIDIUtil/blob/develop/src/midiutil/MidiFile.py #switch to the "RAW" mode and copy all you see on that big text-page #place it in the "Python Modules/site-packages-3" directory #in a new file called "midiutil_v1_2_1.py" from midiutil_v1_2_1 import MIDIFile def main(): if not appex.is_running_extension(): print('This script is intended to be run from the sharing extension.') return # Catch zip file from external "Open in..." dialog inputFile = appex.get_file_path() outfile = os.path.splitext(os.path.basename(inputFile))[0] + ".mid" targetCC = -1 #some global cleverness - digital post-it's haveZIP = False haveGadget = False try: with ZipFile(inputFile) as zf: print("Info: we have a real ZIP archive") haveZIP = True except BadZipfile: print("Info: It is an ALS or Gadget file") if inputFile.endswith(".zip") and haveZIP == True: print("Importing ZIP archive...") with ZipFile(inputFile, 'r') as ablezip: # Iterate over the list of file names in given archive # filter out possible hidden files in "__MACOSX" directories for manually created ZIPs, etc listOfiles = ablezip.namelist() for elem in listOfiles: if not elem.startswith("__") and elem.endswith(".als"): print('Found:', elem, end=' ') infile = ablezip.extract(elem) elif inputFile.endswith(".als"): infile = inputFile with open(infile, 'rb') as test_f: #Is true if file is gzip if binascii.hexlify(test_f.read(2)) == b'1f8b': print("Input is Gadget ALS file") haveGadget = True with gzip.open(inputFile, 'rb') as f: gadgetContents = f.read().decode("utf-8") else: print("Input is plain ALS file") else: print("filetype not supported...") sys.exit() track = 0 channel = 0 time = 0 # In beats duration = 1 # In beats tempo = 60 # In BPM volume = 100 # 0-127, as per the MIDI standard #Parse the data/file because parsing strings will not clean up bad characters in XML if haveGadget == True: #some people need always special treatment - handle them with care... tree = ElementTree(fromstring(gadgetContents)) else: tree = ET.parse(str(infile)) root = tree.getroot() #getting the tempo/bpm (rounded) from the Ableton file for master in root.iter('Tempo'): for child in master.iter('FloatEvent'): tempo = int(float(child.get('Value'))) #print('tempo: ', tempo) #get amount of tracks to be allocated for tracks in root.iter('Tracks'): numTracks = len(tracks.getchildren()) print('Found',str(numTracks),'tracks') #Preparing the target MIDI-file MyMIDI = MIDIFile(numTracks, adjust_origin=True) #One track, defaults to format 1 (tempo track is created automatically) MyMIDI.addTempo(track, time, tempo) #Process every MIDI track found for tracks in root.iter('Tracks'): for miditracks in tracks.iter('MidiTrack'): print('\nMIDITRACK ', track) #getting the track-name for child in miditracks.iter('UserName'): uName = child.get('Value') #print(uName) MyMIDI.addTrackName(track, 0, uName) #getting the key(s) per miditrack for keytracks in miditracks.iter('KeyTrack'): for child in keytracks.iter('MidiKey'): keyt = int(child.get('Value')) print('key:', str(keyt) + ',', end=' ') #getting the notes mycount = 0 for midiData in keytracks.iter('MidiNoteEvent'): tim = midiData.get('Time') dur = midiData.get('Duration') vel = midiData.get('Velocity') #print(tim, dur, vel) #writing the actual note information to file #MIDIFile.addNote(track, channel, pitch, time, duration, volume, annotation=None MyMIDI.addNote(track, channel, keyt, float(tim), float(dur), int(vel)) mycount = mycount + 1 print('processed',int(mycount),'note events') #handling CC stuff for envs in miditracks.iter('Envelopes'): for clipenvs in envs.iter('ClipEnvelope'): for envtarget in clipenvs.iter('EnvelopeTarget'): for child in envtarget: #this might be the CC-ID target based on 16200 #it is possibly not that easy because i found values of 16111 which makes up for a CC of -88 #damnit #print(child.tag, child.attrib) if int(child.get('Value')) == 16200: #pitchbend targetCC = 0 elif int(child.get('Value')) == 16203: #mod-wheel targetCC = 1 elif int(child.get('Value')) == 16111: #cutoff? targetCC = 74 else: targetCC = -1 for autos in clipenvs.iter('Automation'): for events in autos.iter('Events'): for autoevent in events.iter('FloatEvent'): ccVal = int(autoevent.get('Value')) ccTim = float(autoevent.get('Time')) if ccTim < 0: ccTim = 0 #writing pitchbend informations if targetCC == 0: #print('pitchbend/ time: ', ccTim, ' - val: ', ccVal) MyMIDI.addPitchWheelEvent(track, channel, ccTim, ccVal) #writing other CC values if targetCC != -1 and targetCC != 0: MyMIDI.addControllerEvent(track, channel, ccTim, targetCC, ccVal) track = track + 1 with tempfile.NamedTemporaryFile(suffix='.mid') as fp: MyMIDI.writeFile(fp) fp.seek(0) fp.read() # Open the MIDI file in your app of choice - aka 'bring out the gimp' console.open_in(str(fp.name)) #closing and deleting the temporary file fp.close() print ('done.') if __name__ == '__main__': main()