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:

  • First you need to install a newer version of the midiutil
    • Goto the midiutils official github webpage and press the RAW button to open the source.
    • 'Select all' is currently broken in Safarai for IOS 13, so you need to double-tap to start a selection and move the first selection marker to the start of the text and then the second selection marker to the bottom of this about 2000lines long text. (Thank Apple for this inconvience). Then select copy to copy the whole source code to the clipboard.
    • Another approach is to use the Readle Documents browser that allows to download the file to and then open that file to get a 'Select All' and 'Copy' action
    • Open Pythonista and create a new file using the + button, choose 'Emtpy script'
    • In the following dialog enter the name midiutil_v1_2_1.py exactly, select site-package-3 as output folder and press 'Create'
    • Paste the clipboard, the content should be 1836 lines long.
  • After installing the above file, either
    • Download the python script by using the button above the code, 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.1575930936.txt.gz
  • Last modified: 2019/12/10 09:35
  • by _ki