-
-
Save knandersen/a1da6859e3ef84f3c0ce1979536d85c8 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python2 | |
# -*- coding: utf-8 -*- | |
""" | |
USAGE: | |
morphagene_ableton.py -w <inputwavfile> -l <inputlabels> -o <outputfile>' | |
Instructions in Ableton: | |
Insert locators as splice markers in your project (Create > Add Locator) | |
Export Audio/Video with | |
Sample Rate: 48000 Hz | |
Encode PCM: enabled | |
File Type: WAV | |
Bit Depth: 16 | |
Save your Ableton project. | |
The associated Ableton Live Set .als-file will serve as the inputlabels argument | |
Used to convert Ableton Locators from an Ableton Live Set file on .WAV files into | |
single 32-bit float .WAV with CUE markers within the file, directly | |
compatible with the Make Noise Morphagene. | |
Does not require input file to be 48000Hz, only that the Ableton label matches | |
the .WAV file that generated it, and that the input .WAV is stereo. | |
See the Morphagene manual for naming conventions of output files: | |
http://www.makenoisemusic.com/content/manuals/morphagene-manual.pdf | |
# see http://stackoverflow.com/questions/15576798/create-32bit-float-wav-file-in-python | |
# see... http://blog.theroyweb.com/extracting-wav-file-header-information-using-a-python-script | |
# marker code from Joseph Basquin [https://gist.github.com/josephernest/3f22c5ed5dabf1815f16efa8fa53d476] | |
""" | |
import sys, getopt | |
import struct | |
import numpy as np | |
from scipy import interpolate | |
import gzip | |
import xml.etree.ElementTree as ET | |
def float32_wav_file(file_name, sample_array, sample_rate, | |
markers=None, verbose=False): | |
(M,N)=sample_array.shape | |
#print "len sample_array=(%d,%d)" % (M,N) | |
byte_count = M * N * 4 # (len(sample_array)) * 4 # 32-bit floats | |
wav_file = "" | |
# write the header | |
wav_file += struct.pack('<ccccIccccccccIHHIIHH', | |
'R', 'I', 'F', 'F', | |
byte_count + 0x2c - 8, # header size | |
'W', 'A', 'V', 'E', 'f', 'm', 't', ' ', | |
0x10, # size of 'fmt ' header | |
3, # format 3 = floating-point PCM | |
M, # channels | |
sample_rate, # samples / second | |
sample_rate * 4, # bytes / second | |
4, # block alignment | |
32) # bits / sample | |
wav_file += struct.pack('<ccccI', | |
'd', 'a', 't', 'a', byte_count) | |
if verbose: | |
print("packing...") | |
# flatten data in an alternating fashion | |
# see: http://soundfile.sapp.org/doc/WaveFormat/ | |
reordered_wav = [sample_array[k,j] for j in range(N) for k in range(M)] | |
wav_file += struct.pack('<%df' % len(reordered_wav), *reordered_wav) | |
if verbose: | |
print("saving audio...") | |
fid=open(file_name,'wb') | |
for value in wav_file: | |
fid.write(value) | |
if markers: # != None and != [] | |
if verbose: | |
print("saving cue markers...") | |
if isinstance(markers[0], dict):# then we have [{'position': 100, 'label': 'marker1'}, ...] | |
labels = [m['label'] for m in markers] | |
markers = [m['position'] for m in markers] | |
else: | |
labels = ['' for m in markers] | |
fid.write(b'cue ') | |
size = 4 + len(markers) * 24 | |
fid.write(struct.pack('<ii', size, len(markers))) | |
for i, c in enumerate(markers): | |
s = struct.pack('<iiiiii', i + 1, c, 1635017060, 0, 0, c)# 1635017060 is struct.unpack('<i',b'data') | |
fid.write(s) | |
lbls = '' | |
for i, lbl in enumerate(labels): | |
lbls += b'labl' | |
label = lbl + ('\x00' if len(lbl) % 2 == 1 else '\x00\x00') | |
size = len(lbl) + 1 + 4 # because \x00 | |
lbls += struct.pack('<ii', size, i + 1) | |
lbls += label | |
fid.write(b'LIST') | |
size = len(lbls) + 4 | |
fid.write(struct.pack('<i', size)) | |
fid.write(b'adtl')# https://web.archive.org/web/20141226210234/http://www.sonicspot.com/guide/wavefiles.html#list | |
fid.write(lbls) | |
fid.close() | |
def wav_file_read(filename,verbose=False): | |
# read file and close | |
fi=open(filename,'rb') | |
data=fi.read() | |
fi.close() | |
# take raw data and read subsections for important format data | |
A,B,C,D=struct.unpack('4c', data[0:4]) # 'RIFF' | |
ChunkSize=struct.unpack('<l', data[4:8])[0] #4+(8+SubChunk1Size)+8+SubChunk2Size) | |
A,B,C,D=struct.unpack('4c', data[8:12]) # 'WAVE' | |
A,B,C,D=struct.unpack('4c', data[12:16]) # 'fmt ' | |
Subchunk1Size=struct.unpack('<l', data[16:20])[0] # LITTLE ENDIAN, long, 16 | |
AudioFormat=struct.unpack('<h', data[20:22])[0] # LITTLE ENDIAN, short, 1 | |
NumChannels=struct.unpack('<h', data[22:24])[0] # LITTLE ENDIAN, short, Mono = 1, Stereo = 2 | |
SampleRate =struct.unpack('<l', data[24:28])[0] # LITTLE ENDIAN, long, sample rate in samples per second | |
ByteRate=struct.unpack('<l', data[28:32])[0] # self.SampleRate * self.NumChannels * self.BitsPerSample/8)) # (ByteRate) LITTLE ENDIAN, long | |
BlockAlign=struct.unpack('<h', data[32:34])[0] # self.NumChannels * self.BitsPerSample/8)) # (BlockAlign) LITTLE ENDIAN, short | |
BitsPerSample=struct.unpack('<h', data[34:36])[0] # LITTLE ENDIAN, short | |
A,B,C,D=struct.unpack('4c', data[36:40]) # BIG ENDIAN, char*4 | |
SubChunk2Size=struct.unpack('<l', data[40:44])[0] # LITTLE ENDIAN, long | |
waveData=data[44:] | |
(M,N)=(len(waveData),len(waveData[0])) | |
if verbose: | |
print("ChunkSize =%d\nSubchunk1Size =%d\nAudioFormat =%d\nNumChannels =%d\nSampleRate =%d\nByteRate =%d\nBlockAlign =%d\nBitsPerSample =%d\nA:%c, B:%c, C:%c, D:%c\nSubChunk2Size =%d" % | |
(ChunkSize , | |
Subchunk1Size, | |
AudioFormat , | |
NumChannels , | |
SampleRate , | |
ByteRate , | |
BlockAlign , | |
BitsPerSample , | |
A, B, C, D , | |
SubChunk2Size )) | |
# convert audio data to float based on bitdepth | |
if BitsPerSample==8: | |
if verbose: | |
print("Unpacking 8 bits on len(waveData)=%d" % len(waveData)) | |
d=np.fromstring(waveData,np.uint8) | |
floatdata=d.astype(np.float64)/np.float(127) | |
elif BitsPerSample==16: | |
if verbose: | |
print("Unpacking 16 bits on len(waveData)=%d" % len(waveData)) | |
d=np.zeros(SubChunk2Size/2, dtype=np.int16) | |
j=0 | |
for k in range(0, SubChunk2Size, 2): | |
d[j]=struct.unpack('<h',waveData[k:k+2])[0] | |
j=j+1 | |
floatdata=d.astype(np.float64)/np.float(32767) | |
elif BitsPerSample==24: | |
if verbose: | |
print("Unpacking 24 bits on len(waveData)=%d" % len(waveData)) | |
d=np.zeros(SubChunk2Size/3, dtype=np.int32) | |
j=0 | |
for k in range(0, SubChunk2Size, 3): | |
d[j]=struct.unpack('<l',struct.pack('c',waveData[k])+waveData[k:k+3])[0] | |
j=j+1 | |
floatdata=d.astype(np.float64)/np.float(2147483647) | |
else: # anything else will be considered 32 bits | |
if verbose: | |
print("Unpacking 32 bits on len(waveData)=%d" % len(waveData)) | |
d=np.fromstring(waveData,np.int32) | |
floatdata=d.astype(np.float64)/np.float(2147483647) | |
v=floatdata[0::NumChannels] | |
for i in range(1,NumChannels): | |
v=np.vstack((v,floatdata[i::NumChannels])) | |
#return (np.vstack((floatdata[0::2],floatdata[1::2])), SampleRate, NumChannels, BitsPerSample) | |
return (v, SampleRate, NumChannels, BitsPerSample) | |
def load_ableton_labels(label_file): | |
''' | |
Loads Ableton Live locators and calculates the timecode based on tempo and locator measure | |
''' | |
# Open Ableton ALS file as gzip and read tempo and locator data as XML | |
with gzip.open(label_file, mode='r') as f: | |
data = f.read() | |
root = ET.fromstring(data) | |
bpm = None | |
markers = [] | |
for tempo in root.iter('Tempo'): | |
for manual in tempo.findall('Manual'): | |
bpm = float(manual.get('Value')) | |
bps = bpm / 60 | |
print("BPM: {0}, BPS: {1}".format(bpm,bps)) | |
for locator in root.iter('Locator'): | |
v = float(locator.find('Time').get('Value', 'nan')) | |
print("Locator {0} found at: {1}".format(locator.get('Id'),v/bps)) | |
markers.append(v/bps) | |
return np.array(markers).astype('float') | |
def change_samplerate_interp(old_audio,old_rate,new_rate): | |
''' | |
Change sample rate to new sample rate by simple interpolation. | |
If old_rate > new_rate, there may be aliasing / data loss. | |
Input should be in column format, as the interpolation will be completed | |
on each channel this way. | |
Modified from: | |
https://stackoverflow.com/questions/33682490/how-to-read-a-wav-file-using-scipy-at-a-different-sampling-rate | |
''' | |
if old_rate != new_rate: | |
# duration of audio | |
duration = old_audio.shape[0] / old_rate | |
# length of old and new audio | |
time_old = np.linspace(0, duration, old_audio.shape[0]) | |
time_new = np.linspace(0, duration, int(old_audio.shape[0] * new_rate / old_rate)) | |
# fit old_audio into new_audio length by interpolation | |
interpolator = interpolate.interp1d(time_old, old_audio.T) | |
new_audio = interpolator(time_new).T | |
return new_audio | |
else: | |
print('Conversion not needed, old and new rates match') | |
return old_audio # conversion not needed | |
def main(argv): | |
inputwavefile = '' | |
inputlabelfile = '' | |
outputfile = '' | |
try: | |
opts, args = getopt.getopt(argv,"hw:l:o:",["wavfile=","labelfile=","outputfile="]) | |
except getopt.GetoptError: | |
print('Error in usage, correct format:\n'+\ | |
'morphagene_ableton.py -w <inputwavfile> -l <inputlabels> -o <outputfile>') | |
sys.exit(2) | |
for opt, arg in opts: | |
if opt == '-h': | |
print('morphagene_ableton.py -w <inputwavfile> -l <inputlabels> -o <outputfile>') | |
sys.exit() | |
elif opt in ("-w", "--wavfile"): | |
inputwavefile = arg | |
elif opt in ("-l", "--labelfile"): | |
inputlabelfile = arg | |
elif opt in ("-o", "--outputfile"): | |
outputfile = arg | |
print('Input wave file: %s'%inputwavefile) | |
print('Input label file: %s'%inputlabelfile) | |
print('Output Morphagene reel: %s'%outputfile) | |
########################################################################### | |
''' | |
Write single file, edited in Ableton with labels, to Morphagene 32bit | |
WAV file at 48000hz sample rate. | |
''' | |
########################################################################### | |
morph_srate = 48000 # required samplerate for Morphagene | |
# read labels from stereo Audacity label file, ignore text, and use one channel | |
audac_labs = load_ableton_labels(inputlabelfile) | |
# read pertinent info from audio file, exit if input wave file is broken | |
try: | |
(array,sample_rate,num_channels,bits_per_sample)=wav_file_read(inputwavefile) | |
except: | |
print('Input .wav file %s is poorly formatted, exiting'%inputwavefile) | |
sys.exit() | |
# check if input wav has a different rate than desired Morphagene rate, | |
# and correct by interpolation | |
if sample_rate != morph_srate: | |
print("Correcting input sample rate %iHz to Morphagene rate %iHz"%(sample_rate,morph_srate)) | |
# perform interpolation on each channel, then transpose back | |
array = change_samplerate_interp(array.T,float(sample_rate),float(morph_srate)).T | |
# convert labels in seconds to labels in frames, adjusting for change | |
# in rate | |
sc = float(morph_srate) / float(sample_rate) | |
frame_labs = (audac_labs * sample_rate * sc).astype(np.int) | |
else: | |
frame_labs = (audac_labs * sample_rate).astype(np.int) | |
frame_dict = [{'position': l, 'label': 'marker%i'%(i+1)} for i,l in enumerate(frame_labs)] | |
# write wav file with additional cue markers from labels | |
float32_wav_file(outputfile,array,morph_srate,markers=frame_dict) | |
print('Saved Morphagene reel with %i splices: %s'%(len(frame_labs),outputfile)) | |
if __name__ == "__main__": | |
main(sys.argv[1:]) |
Hi, glad to hear! No it doesn't handle the upload process, but should be possible to write a separate script that uploads all the reel to the SD card. I don't mind just doing that manually and a script would have be concerned with handling existing reels on the card, name conflicts etc.
Hi. Sorry I meant can you give me instructions on how and where to install your program to make it work. Noob over here!
oh, sure, I can try! If you don't know how to run python scripts, here's a tutorial for most operating systems: https://www.pythonforbeginners.com/development/how-run-your-python-scripts
Did you read the USAGE-section of the script? There are instructions for what to do inside Ableton to prep your Ableton file and wav-file. With those, you can run the script by following the instructions in the tutorial I just mentioned.
Hey! first of all thank you for sharing your work :) I did everything as described above but I'm getting this message "Input .wav file is poorly formatted, exiting" I've tried everything, any ideas?
Hey! first of all thank you for sharing your work :) I did everything as described above but I'm getting this message "Input .wav file is poorly formatted, exiting" I've tried everything, any ideas?
@alevillejo sorry you're experiencing issues! Can you give me a bit of troubleshooting info, like what kind of input file are you using? bit rate, sample rate, number of channels? could also help to know where you got the file from, i.e. which software generated the file in the first place
Hello, I am having the same problem that @alevillejo was having. I have exported using the specs in the instructions, WAV input file, ALS input labels. 16bit, 48000 Hz, stereo/2 channels, PCM enabled. Still getting the "Input .wav file is poorly formatted, exiting". If anyone has advice, I'd appreciate it.
@bullpencatcher sorry about that! Would it be possible to share some kind of sample I can use to test with? Might take a look at it over the weekend.
I just ran test and works fine here.
When asking for help in the future, would be really helpful that you tell me:
- Error message
- Command you are executing
- System info: Which OS are you running, which version of Python, etc.
Alternatively you can also try a web-based version I made for taking a single audio file, adding markers and exporting as a morphagene-compatible reel: https://knandersen.github.io/morphaweb/
Hey @knandersen,
The issue I'm getting specifically if I raise an exception for the same error that others have noted occurs at line 121:
https://gist.github.com/knandersen/a1da6859e3ef84f3c0ce1979536d85c8#file-morphagene_ableton-py-L121
Traceback (most recent call last):
File "/Users/jak/Desktop/music/samples/morphagene/morphagene_ableton.py", line 257, in main
(array,sample_rate,num_channels,bits_per_sample)=wav_file_read(inputwavefile)
File "/Users/jak/Desktop/music/samples/morphagene/morphagene_ableton.py", line 120, in wav_file_read
(M,N)=(len(waveData),len(waveData[0]))
TypeError: object of type 'int' has no len()
This is occurring because the first index of the byte information in waveData
returns an int
(0 for me, though I suppose 1 would be possible too 😄).
When I comment out that line (since the offending variables aren't used), I get a new error that has to do with the wav_file
byte object that is getting packed at line 47:
https://gist.github.com/knandersen/a1da6859e3ef84f3c0ce1979536d85c8#file-morphagene_ableton-py-L47
Traceback (most recent call last):
File "/Users/jak/Desktop/music/samples/morphagene/morphagene_ableton.py", line 292, in <module>
main(sys.argv[1:])
File "/Users/jak/Desktop/music/samples/morphagene/morphagene_ableton.py", line 288, in main
float32_wav_file(outputfile,array,morph_srate,markers=frame_dict)
File "/Users/jak/Desktop/music/samples/morphagene/morphagene_ableton.py", line 48, in float32_wav_file
wav_file += struct.pack('<ccccIccccccccIHHIIHH',
struct.error: char format requires a bytes object of length 1
This one is definitely beyond me but would love to understand!
Some specs:
- 2.7.18 virtualenv (since your script specifies python 2)
- OSX 12.3.1
- I've also exported all of the audio parameters as you've specified
*Also, the webapp you made is super cool, thank you so much for creating it!!
*Also, the webapp you made is super cool, thank you so much for creating it!!
Sorry you're having trouble with the python-script. Works on my side, also using 2.7.18 on OSX 13.0 and is hard to debug because of so many things that could be going wrong. That's one of the reasons I created the webapp, which I'm super happy that you like!
Hi, I'm very late to the party. I am also getting the "File is poorly formatted" error. Has anyone had any luck?
Hi. Thank you, this is just what I am looking to be able to do. Can you advise me on the upload process? Does this get uploaded to the morphogene as firmware right? Its not just a file on the sd card. TIA