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()
  • pythonista_als_to_midifile_converter.1575925763.txt.gz
  • Last modified: 2019/12/10 08:09
  • by _ki