pythonista_als_to_midifile_converter

Differences

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

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
Next revisionBoth sides next revision
pythonista_als_to_midifile_converter [2019/12/09 09:37] – Backlink to ALS description page _kipythonista_als_to_midifile_converter [2019/12/10 09:35] – Added demo video _ki
Line 3: Line 3:
 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.  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>G-pDfys2H7DzM}}
 +
 +\\ 
 How to install: How to install:
-  * Download the python script into Pythonista +  * 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'   * In Pythonista settings/App Extensions select 'Share Extension Shortcuts'
     * Use the + sign to add a extension     * Use the + sign to add a extension
Line 18: Line 33:
  
 \\ \\
- 
 <file py ALS_to_MIDI.py> <file py ALS_to_MIDI.py>
 # Ableton MIDI clip zip export to MIDI file converter # Ableton MIDI clip zip export to MIDI file converter
Line 32: Line 46:
 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 
 +#thisis the old (default) library which does not support pitch-bend-data 
 +#from midiutil import MIDIFile
 import console import console
 import io import io
Line 39: Line 55:
 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 49: Line 76:
     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 58: Line 86:
             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 72: Line 100:
                     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 
-    channel     = 0 
-    time        = 0     # In beats 
-    duration    = 1     # In beats 
-    tempo       = 60    # In BPM 
-    volume      = 100   # 0-127, as per the MIDI standard 
  
-    # Rather parse the file because parsing strings will not clean up bad characters in XML +    track           = 0 
-    tree ET.parse(str(infile)) +    channel         = 0 
-    root tree.getroot()+    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     #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)+            #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(tracks.getchildren()) 
-            print('Found',str(numTracks),'tracks')+        print('Found',str(numTracks),'tracks')
  
-    #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)        #One track, defaults to format 1 (tempo track is created automatically)
     MyMIDI.addTempo(track, time, tempo)     MyMIDI.addTempo(track, time, tempo)
  
-    # Process every MIDI track found+    #Process every MIDI track found
     for tracks in root.iter('Tracks'):     for tracks in root.iter('Tracks'):
-                    for miditracks in tracks.iter('MidiTrack'): +        for miditracks in tracks.iter('MidiTrack'): 
-                            print('\nMIDITRACK ', track)+            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'):
  
-                            #getting the track-name          +                                ccVal = int(autoevent.get('Value')) 
-                            for child in miditracks.iter('UserName'): +                                ccTim float(autoevent.get('Time')) 
-                                uName child.get('Value') +                                if ccTim < 0: 
-                                #print(uName+                                    ccTim = 0
-                                MyMIDI.addTrackName(track, 0, uName)+
  
-                            #getting the key(s) per miditrack +                                #writing pitchbend informations 
-                            for keytracks in miditracks.iter('KeyTrack'): +                                if targetCC == 0
-                                for child in keytracks.iter('MidiKey')+                                    #print('pitchbend/ time: ', ccTim, - val: ', ccVal) 
-                                    keyt = int(child.get('Value')) +                                    MyMIDI.addPitchWheelEvent(trackchannelccTim, ccVal)
-                                    print('key:', str(keyt) + ','end=' ')+
  
-                                    #getting the notes +                                #writing other CC values 
-                                    mycount = 0 +                                if targetCC != -1 and targetCC != 0: 
-                                    for midiData in keytracks.iter('MidiNoteEvent'): +                                    MyMIDI.addControllerEvent(track, channel, ccTimtargetCCccVal)
-                                        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, keytfloat(tim), float(dur), int(vel)) +
-                                        mycount = mycount + 1 +
-                                    print('processed',int(mycount),'note events')+
  
-                            track = track + 1+            track = track + 1
  
     with tempfile.NamedTemporaryFile(suffix='.mid') as fp:     with tempfile.NamedTemporaryFile(suffix='.mid') as fp:
Line 140: Line 218:
         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 147: Line 225:
  
 if __name__ == '__main__': if __name__ == '__main__':
-        main() +    main()
 </file> </file>
  
-{{tag>ableton_live_set midi_scripting }}+{{tag>ableton_live_set midi_scripting video}}
  • pythonista_als_to_midifile_converter.txt
  • Last modified: 2020/04/22 18:40
  • by MrBlaschke