pythonista_als_to_midifile_converter

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
pythonista_als_to_midifile_converter [2019/12/09 09:13] – Initial version _kipythonista_als_to_midifile_converter [2020/04/22 18:40] (current) MrBlaschke
Line 1: Line 1:
-====== Pythonista Ableton Live Set (ALS) to MIDIfile converter script ======+====== PythonistaAbleton Live Set (ALS) to MIDI converter script ======
  
 +This Pythonista script installs a share extension to convert [[Ableton|Ableton Live Set export files]] into MIDI files containing the notes of the exported tracks. 
  
 +{{youtube>pDfys2H7DzM}}
  
-<code># Ableton MIDI clip zip export to MIDI file converter+\\  
 +How to install: 
 +  * First you need to install a newer version of the midiutil  
 +    * Goto the [[https://github.com/MarkCWirt/MIDIUtil/blob/develop/src/midiutil/MidiFile.py|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 
 + 
 +\\ 
 +<file py ALS_to_MIDI.py> 
 +# Ableton MIDI clip zip export to MIDI file converter
 # Original script by MrBlaschke # Original script by MrBlaschke
 # Usability enhancements by rs2000 # Usability enhancements by rs2000
-# Dec 8, 2019+# Dec 11, 2019, V.04 
 +
 +# greatly enhanced version that handles multiple scenes and clip offsets 
 +# resulted in new parser engine 
 +# request by @SpookyZoo
 # #
 # Original request and idea by Svetlovska # Original request and idea by Svetlovska
Line 15: Line 50:
 import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
 import xml.etree as XTree import xml.etree as XTree
-from midiutil import MIDIFile+from xml.etree.ElementTree import fromstring, ElementTree
 import console import console
 import io import io
Line 22: Line 57:
 from zipfile import ZipFile from zipfile import ZipFile
 from zipfile import BadZipfile from zipfile import BadZipfile
 +import gzip
 +import binascii
 from time import sleep 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(): def main():
Line 32: Line 77:
     inputFile = appex.get_file_path()     inputFile = appex.get_file_path()
     outfile = os.path.splitext(os.path.basename(inputFile))[0] + ".mid"     outfile = os.path.splitext(os.path.basename(inputFile))[0] + ".mid"
 +    targetCC = -1
  
-    #check if we have an ALS which is not renamed +    #some global cleverness digital post-it's
-    #so basically a zip-archive with ALS extension is that of relevance?+
     haveZIP = False     haveZIP = False
 +    haveGadget = False
     try:     try:
         with ZipFile(inputFile) as zf:         with ZipFile(inputFile) as zf:
Line 41: Line 87:
             haveZIP = True             haveZIP = True
     except BadZipfile:     except BadZipfile:
-        print("Info: It is a pure ALS file")+        print("Info: It is an ALS or Gadget file")
  
-    if inputFile.endswith(".zip"or haveZIP == True:+    if inputFile.endswith(".zip"and haveZIP == True:
         print("Importing ZIP archive...")         print("Importing ZIP archive...")
         with ZipFile(inputFile, 'r') as ablezip:         with ZipFile(inputFile, 'r') as ablezip:
Line 52: Line 98:
             for elem in listOfiles:             for elem in listOfiles:
                 if not elem.startswith("__") and elem.endswith(".als"):                 if not elem.startswith("__") and elem.endswith(".als"):
-                    print('Found:', elem, end=' ')+                    #print('Found:', elem, end=' ')
                     infile = ablezip.extract(elem)                     infile = ablezip.extract(elem)
     elif inputFile.endswith(".als"):     elif inputFile.endswith(".als"):
-        print("Input is direct Ableton ALS file") 
         infile = inputFile         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:     else:
         print("filetype not supported...")         print("filetype not supported...")
         sys.exit()         sys.exit()
 +
  
     track           = 0     track           = 0
-    channel     = 0+    channel         = 0
     time            = 0     # In beats     time            = 0     # In beats
-    duration    = 1     # In beats +    duration        = 1     # In beats 
-    tempo           = 60        # In BPM +    tempo           = 60    # In BPM 
-    volume      = 100   # 0-127, as per the MIDI standard+    volume          = 100   # 0-127, as per the MIDI standard
  
-    # Rather parse the file because parsing strings will not clean up bad characters in XML +    toffset                 = 0         for calculating time-offsets in multi scenes 
-    tree ET.parse(str(infile)) +    timeoff                 0         # store for temp offsets
-    root = tree.getroot()+
  
 +    #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     #getting the tempo/bpm (rounded) from the Ableton file
     for master in root.iter('Tempo'):     for master in root.iter('Tempo'):
-            for child in master.iter('FloatEvent'): +        for child in master.iter('FloatEvent'): 
-                    tempo = int(float(child.get('Value'))+            tempo = int(float(child.get('Value')))
-                    #print('tempo: ', tempo)+
  
     #get amount of tracks to be allocated     #get amount of tracks to be allocated
     for tracks in root.iter('Tracks'):     for tracks in root.iter('Tracks'):
-            numTracks = len(tracks.getchildren()) +        numTracks = len(list(tracks.findall('MidiTrack'))) 
-            print('Found',str(numTracks),'tracks')+        print('Found',str(numTracks),'track(s) with', tempo, 'BPM')
  
-    #Opening the target MIDI-file +    #Preparing the target MIDI-file 
-    MyMIDI = MIDIFile(numTracks, adjust_origin=True)    One track, defaults to format 1 (tempo track is created automatically)+    MyMIDI = MIDIFile(numTracks, adjust_origin=True)        #tempo track is created automatically
     MyMIDI.addTempo(track, time, tempo)     MyMIDI.addTempo(track, time, tempo)
  
-    # Process every MIDI track found +    #Give me aaaallll you've got 
-    for tracks in root.iter('Tracks'): +    for miditrack in root.findall('.//MidiTrack'): 
-                    for miditracks in tracks.iter('MidiTrack'): +        #resetting the time offset data 
-                            print('\nMIDITRACK ', track)+        toffset = 0 
 +        timeoff = 0 
 + 
 +        #getting track data (name, etc) 
 +        for uname in miditrack.findall('.//UserName'): 
 +            trackname = uname.attrib.get('Value'
 +            print('\nProcessing track: ', trackname) 
 +            MyMIDI.addTrackName(track, 0, trackname) 
 + 
 +        for clipslot in miditrack.findall('.//MainSequencer/ClipSlotList/ClipSlot'): 
 +            #looping the amount of clips 
 +            for midiclip in clipslot.findall('.//ClipSlot/Value/MidiClip'): 
 +                #raising the time offset for the next clip inside this track 
 +                toffset = toffset + timeoff 
 + 
 +                #get the clip-length 
 +                for loopinfo in midiclip.findall('.//Loop'): 
 +                    le = loopinfo.find('LoopEnd'
 +                    #store the next time offset 
 +                    timeoff = float(le.attrib.get('Value')) 
 + 
 +                for noteinfo in midiclip.findall('.//Notes/KeyTracks'): 
 +                    print('\tAmount of note events: ', len(noteinfo.getchildren())) 
 + 
 +                    for keytracks in noteinfo: 
 +                        for key in keytracks.findall('.//MidiKey'): 
 +                            keyt = int(key.attrib.get('Value')) 
 +                            print('\t\tProcessing key: ', str(keyt)) 
 +                        #getting the notes 
 +                        for notes in keytracks.findall('.//Notes/MidiNoteEvent'): 
 +                            tim = float(notes.attrib.get('Time')) + float(toffset) 
 +                            dur = float(notes.attrib.get('Duration')) 
 +                            vel = int(notes.attrib.get('Velocity')) 
 +                            MyMIDI.addNote(track, channel, keyt, tim, dur, vel)
  
-                            #getting the track-name          +                #getting automation data 
-                            for child in miditracks.iter('UserName'): +                for envelopes in midiclip.findall('.//Envelopes/Envelopes'): 
-                                uName child.get('Value'+                    for clipenv in envelopes: 
-                                #print(uName+                        #get the automation internal id 
-                                MyMIDI.addTrackName(track, 0, uName)+                        autoid int(clipenv.find('.//EnvelopeTarget/PointeeId').attrib.get('Value')
 +                        if autoid == 16200:            #pitchbend 
 +                            targetCC = 0 
 +                            print('\tFound CC-data for: Pitch'
 +                        elif autoid == 16203:          #mod-wheel 
 +                            targetCC = 1 
 +                            print('\tFound CC-data for: Modulation'
 +                        elif autoid == 16111:          #cutoff? 
 +                            targetCC = 74 
 +                            print('\tFound CC-data for: Cutoff'
 +                        else: 
 +                            targetCC = -1 
 +                            print('\n!! Found unhandled CC data. Contact developer for integration. Thanks!')
  
-                            #getting the key(s) per miditrack +                        #get the automation values for each envelope 
-                            for keytracks in miditracks.iter('KeyTrack'): +                        for automs in clipenv.findall('.//Automation/Events'): 
-                                for child in keytracks.iter('MidiKey'): +                            for aevents in automs: 
-                                    keyt = int(child.get('Value')) +                                eventvals = aevents.attrib 
-                                    print('key:', str(keyt) + ',', end=' ')+                                ccTim = float(eventvals.get('Time')) 
 +                                ccVal = int(eventvals.get('Value')) 
 +                                if ccTim < 0: 
 +                                    ccTim 0   
  
-                                    #getting the notes +                                #writing pitchbend informations 
-                                    mycount = 0 +                                if targetCC == 0: 
-                                    for midiData in keytracks.iter('MidiNoteEvent'): +                                    MyMIDI.addPitchWheelEvent(track, channel, ccTimccVal)
-                                        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')+
  
-                            track = track + 1+                                #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:     with tempfile.NamedTemporaryFile(suffix='.mid') as fp:
Line 123: Line 226:
         fp.seek(0)         fp.seek(0)
         fp.read()         fp.read()
-        # Open the MIDI file in your app of choice :)+        # Open the MIDI file in your app of choice - aka 'bring out the gimp'
         console.open_in(str(fp.name))         console.open_in(str(fp.name))
         #closing and deleting the temporary file         #closing and deleting the temporary file
Line 130: Line 233:
  
 if __name__ == '__main__': if __name__ == '__main__':
-        main()  +    main() 
-</code>+</file>
  
-{{tag>ableton_live_set midi_scripting }}+{{tag>ableton_live_set midi_scripting video}}
  • pythonista_als_to_midifile_converter.1575843219.txt.gz
  • Last modified: 2019/12/09 09:13
  • by _ki