Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| pythonista_als_to_midifile_converter [2019/12/09 09:30] – Initial version _ki | pythonista_als_to_midifile_converter [2020/04/22 18:40] (current) – MrBlaschke | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Pythonista | + | ====== 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. | + | This Pythonista script installs a share extension to convert |
| + | {{youtube> | ||
| + | |||
| + | \\ | ||
| How to install: | How to install: | ||
| - | * Download the python script | + | |
| + | * Goto the [[https:// | ||
| + | * ' | ||
| + | * **Another approach** is to use the Readle Documents browser that allows to download the file to and then open that file to get a ' | ||
| + | * 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 ' | ||
| + | * Paste the clipboard, the content should be 1836 lines long. | ||
| + | |||
| + | * After installing the above file, either | ||
| + | | ||
| + | * or | ||
| + | * Copy the script code block, open Pythonista, create a new file named ALS_to_MIDI.py and paste the clipboard | ||
| * In Pythonista settings/ | * In Pythonista settings/ | ||
| * 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 | ||
| # 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 32: | 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 | + | from xml.etree.ElementTree |
| import console | import console | ||
| import io | import io | ||
| Line 39: | 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:// | ||
| + | #switch to the " | ||
| + | #place it in the " | ||
| + | #in a new file called " | ||
| + | from midiutil_v1_2_1 import MIDIFile | ||
| + | |||
| + | |||
| def main(): | def main(): | ||
| Line 49: | Line 77: | ||
| inputFile = appex.get_file_path() | inputFile = appex.get_file_path() | ||
| outfile = os.path.splitext(os.path.basename(inputFile))[0] + " | outfile = os.path.splitext(os.path.basename(inputFile))[0] + " | ||
| + | targetCC = -1 | ||
| - | #check if we have an ALS which is not renamed | + | #some global cleverness |
| - | #so basically a zip-archive with ALS extension | + | |
| haveZIP = False | haveZIP = False | ||
| + | haveGadget = False | ||
| try: | try: | ||
| with ZipFile(inputFile) as zf: | with ZipFile(inputFile) as zf: | ||
| Line 58: | Line 87: | ||
| haveZIP = True | haveZIP = True | ||
| except BadZipfile: | except BadZipfile: | ||
| - | print(" | + | print(" |
| - | if inputFile.endswith(" | + | if inputFile.endswith(" |
| print(" | print(" | ||
| with ZipFile(inputFile, | with ZipFile(inputFile, | ||
| Line 69: | Line 98: | ||
| for elem in listOfiles: | for elem in listOfiles: | ||
| if not elem.startswith(" | if not elem.startswith(" | ||
| - | print(' | + | |
| infile = ablezip.extract(elem) | infile = ablezip.extract(elem) | ||
| elif inputFile.endswith(" | elif inputFile.endswith(" | ||
| - | print(" | ||
| infile = inputFile | infile = inputFile | ||
| + | with open(infile, | ||
| + | #Is true if file is gzip | ||
| + | if binascii.hexlify(test_f.read(2)) == b' | ||
| + | print(" | ||
| + | haveGadget = True | ||
| + | with gzip.open(inputFile, | ||
| + | gadgetContents = f.read().decode(" | ||
| + | else: | ||
| + | print(" | ||
| else: | else: | ||
| print(" | print(" | ||
| sys.exit() | sys.exit() | ||
| - | track = 0 | ||
| - | channel | ||
| - | time = 0 # In beats | ||
| - | duration | ||
| - | tempo = 60 # In BPM | ||
| - | volume | ||
| - | # Rather parse the file because parsing strings will not clean up bad characters in XML | + | track = 0 |
| - | | + | channel |
| - | | + | time = 0 # In beats |
| + | | ||
| + | | ||
| + | volume | ||
| + | toffset | ||
| + | timeoff | ||
| + | |||
| + | #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(' | for master in root.iter(' | ||
| - | | + | |
| - | tempo = int(float(child.get(' | + | tempo = int(float(child.get(' |
| - | # | + | |
| #get amount of tracks to be allocated | #get amount of tracks to be allocated | ||
| for tracks in root.iter(' | for tracks in root.iter(' | ||
| - | | + | |
| - | print(' | + | print(' |
| - | #Opening | + | #Preparing |
| - | MyMIDI = MIDIFile(numTracks, | + | MyMIDI = MIDIFile(numTracks, |
| MyMIDI.addTempo(track, | MyMIDI.addTempo(track, | ||
| - | # Process every MIDI track found | + | #Give me aaaallll you've got |
| - | for tracks | + | for miditrack |
| - | for miditracks | + | #resetting the time offset data |
| - | print(' | + | toffset = 0 |
| + | timeoff = 0 | ||
| + | |||
| + | #getting track data (name, etc) | ||
| + | for uname in miditrack.findall(' | ||
| + | trackname = uname.attrib.get(' | ||
| + | print(' | ||
| + | MyMIDI.addTrackName(track, | ||
| + | |||
| + | for clipslot in miditrack.findall(' | ||
| + | #looping the amount of clips | ||
| + | for midiclip in clipslot.findall(' | ||
| + | #raising the time offset for the next clip inside this track | ||
| + | toffset = toffset + timeoff | ||
| + | |||
| + | #get the clip-length | ||
| + | for loopinfo in midiclip.findall(' | ||
| + | | ||
| + | #store the next time offset | ||
| + | timeoff = float(le.attrib.get(' | ||
| + | |||
| + | | ||
| + | print(' | ||
| + | |||
| + | for keytracks in noteinfo: | ||
| + | for key in keytracks.findall(' | ||
| + | keyt = int(key.attrib.get(' | ||
| + | print(' | ||
| + | #getting the notes | ||
| + | for notes in keytracks.findall(' | ||
| + | tim = float(notes.attrib.get(' | ||
| + | dur = float(notes.attrib.get(' | ||
| + | vel = int(notes.attrib.get(' | ||
| + | MyMIDI.addNote(track, channel, keyt, tim, dur, vel) | ||
| - | | + | |
| - | for child in miditracks.iter('UserName'): | + | for envelopes |
| - | | + | for clipenv in envelopes: |
| - | #print(uName) | + | #get the automation internal id |
| - | | + | autoid |
| + | if autoid == 16200: | ||
| + | targetCC = 0 | ||
| + | | ||
| + | elif autoid == 16203: | ||
| + | targetCC = 1 | ||
| + | print(' | ||
| + | elif autoid == 16111: | ||
| + | targetCC = 74 | ||
| + | print(' | ||
| + | else: | ||
| + | targetCC = -1 | ||
| + | print(' | ||
| - | | + | |
| - | for keytracks | + | for automs |
| - | for child in keytracks.iter('MidiKey'): | + | for aevents |
| - | | + | eventvals = aevents.attrib |
| - | | + | ccTim = float(eventvals.get('Time')) |
| + | | ||
| + | if ccTim < 0: | ||
| + | ccTim = 0 | ||
| - | | + | |
| - | | + | if targetCC == 0: |
| - | | + | MyMIDI.addPitchWheelEvent(track, channel, |
| - | tim = midiData.get(' | + | |
| - | dur = midiData.get(' | + | |
| - | vel = midiData.get(' | + | |
| - | #print(tim, dur, vel) | + | |
| - | #writing the actual note information to file | + | |
| - | # | + | |
| - | | + | |
| - | mycount = mycount + 1 | + | |
| - | print(' | + | |
| - | | + | #writing other CC values |
| + | if targetCC != -1 and targetCC != 0: | ||
| + | MyMIDI.addControllerEvent(track, | ||
| + | | ||
| with tempfile.NamedTemporaryFile(suffix=' | with tempfile.NamedTemporaryFile(suffix=' | ||
| Line 140: | 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 |
| 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 233: | ||
| if __name__ == ' | if __name__ == ' | ||
| - | | + | |
| </ | </ | ||
| - | {{tag> | + | {{tag> |