-
-
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:]) |
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?
i sat with ChatGPT to fix the "file is poorly formatted error", i was running python 3 so many errors were because of that.. but i was able to fix it and run it with correct results.. if anyone wants, here is the full script which works on python 3
#!/usr/bin/env python3
morphagene_ableton.py
import sys
import getopt
import gzip
import struct
import xml.etree.ElementTree as ET
import numpy as np
from scipy import interpolate
def float32_wav_file(file_name, sample_array, sample_rate, markers=None):
channels, samples = sample_array.shape
byte_count = channels * samples * 4
wav_file = b""
# WAV header
wav_file += struct.pack('<4sI4s', b'RIFF', byte_count + 36, b'WAVE')
wav_file += struct.pack('<4sIHHIIHH', b'fmt ', 16, 3, channels, sample_rate,
sample_rate * channels * 4, channels * 4, 32)
wav_file += struct.pack('<4sI', b'data', byte_count)
reordered = sample_array.T.flatten()
wav_file += struct.pack('<%df' % len(reordered), *reordered)
with open(file_name, 'wb') as fid:
fid.write(wav_file)
if markers:
if isinstance(markers[0], dict):
labels = [m['label'] for m in markers]
positions = [m['position'] for m in markers]
else:
labels = ['' for _ in markers]
positions = markers
# Cue chunk
fid.write(b'cue ')
fid.write(struct.pack('<ii', 4 + 24 * len(positions), len(positions)))
for i, pos in enumerate(positions):
fid.write(struct.pack('<iiiiii', i + 1, pos, 0x64617461, 0, 0, pos))
# Labels
lbls = b''
for i, lbl in enumerate(labels):
label_bytes = lbl.encode('ascii') + b'\x00'
if len(label_bytes) % 2 == 1:
label_bytes += b'\x00'
lbls += b'labl' + struct.pack('<ii', len(label_bytes) + 4, i + 1) + label_bytes
fid.write(b'LIST')
fid.write(struct.pack('<i', len(lbls) + 4))
fid.write(b'adtl')
fid.write(lbls)
import wave
def wav_file_read(filename):
with wave.open(filename, 'rb') as wf:
num_channels = wf.getnchannels()
sample_rate = wf.getframerate()
sampwidth = wf.getsampwidth() # bytes per sample
nframes = wf.getnframes()
raw = wf.readframes(nframes)
if sampwidth == 2:
dtype = np.int16
divisor = 32768.0
elif sampwidth == 4:
dtype = np.int32
divisor = 2147483648.0
else:
raise ValueError(f"Unsupported sample width: {sampwidth*8} bits")
audio = np.frombuffer(raw, dtype=dtype).astype(np.float32) / divisor
audio = audio.reshape(-1, num_channels).T
return audio, sample_rate, num_channels, sampwidth*8
def load_ableton_labels(label_file):
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: {:.1f}, BPS: {:.1f}".format(bpm, bps))
for locator in root.iter('Locator'):
v = float(locator.find('Time').get('Value', 'nan'))
print(f"Locator {locator.get('Id')} found at: {v / bps:.1f}")
markers.append(v / bps)
return np.array(markers, dtype=float)
def change_samplerate_interp(old_audio, old_rate, new_rate):
if old_rate == new_rate:
return old_audio
duration = old_audio.shape[1] / old_rate
time_old = np.linspace(0, duration, old_audio.shape[1])
time_new = np.linspace(0, duration, int(old_audio.shape[1] * new_rate / old_rate))
interpolator = interpolate.interp1d(time_old, old_audio, kind='linear', axis=1)
return interpolator(time_new)
def main(argv):
inputwavefile = ''
inputlabelfile = ''
outputfile = ''
morph_srate = 48000
try:
opts, _ = getopt.getopt(argv, "hw:l:o:", ["wavfile=", "labelfile=", "outputfile="])
except getopt.GetoptError:
print('Usage: morphagene_ableton.py -w <inputwavfile> -l <inputlabels> -o <outputfile>')
sys.exit(2)
for opt, arg in opts:
if opt == '-h':
print('Usage: 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:", inputwavefile)
print("Input label file:", inputlabelfile)
print("Output Morphagene reel:", outputfile)
audac_labs = load_ableton_labels(inputlabelfile)
try:
array, sample_rate, num_channels, bits_per_sample = wav_file_read(inputwavefile)
except Exception as e:
print('Error reading .wav file "%s": %s' % (inputwavefile, e))
sys.exit()
print("Audio shape:", array.shape)
print("Sample rate:", sample_rate)
print("Number of markers:", len(audac_labs))
if sample_rate != morph_srate:
print(f"Resampling from {sample_rate} Hz to {morph_srate} Hz")
array = change_samplerate_interp(array, sample_rate, morph_srate)
scale = morph_srate / sample_rate
frame_labs = (audac_labs * morph_srate).astype(np.int32)
else:
frame_labs = (audac_labs * sample_rate).astype(np.int32)
frame_dict = [{'position': l, 'label': f'marker{i+1}'} for i, l in enumerate(frame_labs)]
print("Calling float32_wav_file...")
float32_wav_file(outputfile, array, morph_srate, markers=frame_dict)
print(f"Write complete. {len(frame_labs)} markers written to {outputfile}")
if name == "main":
main(sys.argv[1:])
@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.