Created
August 2, 2023 14:52
-
-
Save tattlemuss/97c0518db56a86d559e55b772b065955 to your computer and use it in GitHub Desktop.
A very simple and hacky Python3 script to convert 16-bit uncompressed WAV files to MPC-3000 .SND files.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
A very simple and hacky Python3 script to convert 16-bit uncompressed WAV | |
files to MPC-3000 .SND files. | |
The output is modelled to copy the output of Wav2Snd | |
(http://www.mpc3000.com/wavsnd.htm) but this might be a bit more portable | |
to run on modern machines. | |
Usage in a terminal/command line: | |
python3 wavtosnd.py <input.wav> <output.snd> | |
It should be easy to rewrite the main input function to e.g. iterate | |
over a whole directory of WAV files, wrap in a GUI, or whatever you want | |
really. | |
PLEASE NOTE: I don't have an MPC-3000. I make no claims that this script | |
is reliable or won't cause your MPC-3000 to fail in some way. Use at | |
your own risk. Hence the MIT licence appended below. | |
Limitations: | |
- The filename must use ASCII characters since it is used as the sample name | |
in the file. | |
- Only uncompressed 16-bit signed WAV files are supported. | |
Steven Tattersall | |
----- | |
Copyright 2023 Steven Tattersall | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the “Software”), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
of the Software, and to permit persons to whom the Software is furnished to do so, | |
subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A | |
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
""" | |
import struct, math | |
import os, array | |
# File format notes. | |
# Ref: https://github.com/maseology/pyMPC2000xl/ | |
# The MPC-3000 format appears to be very similar to the MPC-2000XL, | |
# but with a different magic number at the start and the last 3 fields | |
# omitted (from "Loop Mode" onwards) | |
# For stereo samples, the channel data is not interleaved i.e. the | |
# entire left channel of 16-bit audio is written in a block, then the | |
# entire right channel. (This differs from WAV files.) | |
def make_tag(a,b,c,d): | |
return (ord(a) << 0) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24) | |
def read_struct(fh, fmt): | |
size = struct.calcsize(fmt) | |
data = fh.read(size) | |
return struct.unpack(fmt, data) | |
def write(fh, fmt, *data): | |
fh.write(struct.pack(fmt, *data)) | |
def convert(src, dst): | |
""" Convert a single WAV file. | |
src - input filename | |
dst - output filename | |
Returns an error code (0 if successful, -1 if failed) | |
""" | |
base, path = os.path.split(src) | |
root, _ = os.path.splitext(path) | |
src_fh = open(src, 'rb') | |
bits = -1 | |
# Read WAV header | |
(chunkid, restsize, wave) = read_struct(src_fh, '<III') | |
if chunkid != make_tag('R', 'I', 'F', 'F'): | |
print("Unknown WAV header") | |
return -1 | |
while True: | |
offset = src_fh.tell() | |
try: | |
(chunk_id, chunk_size) = read_struct(src_fh, '<II') | |
except struct.error: | |
break | |
print("[offset %08x] Read WAV chunk with ID 0x%x, size %x" % (offset, chunk_id, chunk_size)) | |
if chunk_id == make_tag('f', 'm', 't', ' '): | |
# Read format | |
(AudioFormat, NumChannels, SampleRate, ByteRate, BlockAlign, BitsPerSample) = read_struct(src_fh, '<HHIIHH') | |
print("AudioFormat: %d BPS: %d Channels: %d" % (AudioFormat, BitsPerSample, NumChannels)) | |
if (AudioFormat != 1): | |
print("Compressed format, can't decode") | |
return -2 | |
if (NumChannels != 1 and NumChannels != 2): | |
print("Not mono or stereo, can't decode") | |
return -3 | |
if BitsPerSample != 16: | |
print("Not 16-bit, can't decode") | |
return -4 | |
bits = BitsPerSample | |
print("ByteRate:", ByteRate) | |
print("Freq", ByteRate * 8 / BitsPerSample / NumChannels) | |
if (chunk_id == make_tag('d', 'a', 't', 'a')): | |
vals = array.array('h') | |
# Note: data is little-endian, signed | |
if bits == 16: | |
num_samples = int(chunk_size / 2) | |
for x in range(0, num_samples): | |
[val] = read_struct(src_fh, '<h') | |
# Convert to unsigned | |
vals.append(val) | |
else: | |
print("Unsupported bits-per-sample") | |
return -5 | |
# Now create the data | |
l = len(vals) | |
left = [vals[k] for k in range(0, l, NumChannels)] | |
right = None | |
if NumChannels == 2: | |
right = [vals[k] for k in range(1, l, NumChannels)] | |
with open(dst, 'wb') as dst_fh: | |
# Pad out the name to 16 characters | |
name = bytes(root, 'ascii').ljust(16)[:16] | |
write(dst_fh, "<BB", 1,2) # file header | |
dst_fh.write(name) # 16-char string | |
write(dst_fh, "<B", 0) # terminator | |
write(dst_fh, "<B", 100) # level | |
write(dst_fh, "<B", 0) # tuning | |
write(dst_fh, "<B", NumChannels - 1) # 0 = mono, 1 = stereo | |
write(dst_fh, "<I", 0) # start | |
write(dst_fh, "<I", len(left)) # loop end | |
write(dst_fh, "<I", len(left)) # end | |
write(dst_fh, "<H", 0) # ??? possibly... | |
write(dst_fh, "<H", 0) # ??? ... loop length | |
left = array.array('h', left) | |
left.tofile(dst_fh) | |
if right != None: | |
right = array.array('h', right) | |
right.tofile(dst_fh) | |
print("Converted to %s OK" % dst) | |
break | |
# Compensate for odd-size chunks and even-address alignment | |
# that I found in some files. | |
chunk_size = (chunk_size + 1) & 0xfffffffe | |
# Move to next WAV chunk | |
src_fh.seek(offset + chunk_size + 8, os.SEEK_SET) | |
src_fh.close() | |
return 0 | |
if __name__ == '__main__': | |
import sys | |
# Read command line input/output arguments | |
src = sys.argv[1] | |
dst = sys.argv[2] | |
r = convert(src, dst) | |
sys.exit(r) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment