Skip to content

Instantly share code, notes, and snippets.

@rizumu
Forked from tattlemuss/wavtosnd.py
Created December 2, 2023 07:07
Show Gist options
  • Save rizumu/79dd1cbb72d8ada0be3418dcc78e275b to your computer and use it in GitHub Desktop.
Save rizumu/79dd1cbb72d8ada0be3418dcc78e275b 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.
#!/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