Skip to content

Instantly share code, notes, and snippets.

@tattlemuss
Created April 27, 2021 20:48
Show Gist options
  • Save tattlemuss/8bf99da4c8f33b922b432f0e1f01c4d8 to your computer and use it in GitHub Desktop.
Save tattlemuss/8bf99da4c8f33b922b432f0e1f01c4d8 to your computer and use it in GitHub Desktop.
"""
A very simple and hacky script to convert 16-bit or 24-bit WAV sample files to 4-bit.
Files must be mono and uncompressed PCM data.
Output is a 4-bits-per-sample output, with an end marker byte, suitable for TTRAK playback.
"""
import struct, math
import os, array
# These are the output voltages as set in the Hatari source code,
# which we use as "nearest-neighbour"
voltages = [
0, 369, 438, 521, 619, 735, 874, 1039,
1234, 1467, 1744, 2072, 2463, 2927, 3479, 4135,
4914, 5841, 6942, 8250, 9806,11654,13851,16462,
19565,23253,27636,32845,39037,46395,55141,65535
]
YmVolume4to5 = [ 0,1,5,7,9,11,13,15,17,19,21,23,25,27,29,31 ]
def make_tag(a,b,c,d):
return (ord(a) << 0) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24)
def nearest16(x):
""" find the nearest entry to our candidate level """
# This isn't a perfect approach since it doesn't take the logarithmic
# factor into account, but it's a reasonable first approximation.
best_off = 65536
best_cand = 0
for index, val in enumerate(YmVolume4to5):
off = abs(voltages[val] - x)
if off < best_off:
best_cand = index
best_off = off
assert(best_cand <= 0xf)
return best_cand
def read_struct(fh, fmt):
size = struct.calcsize(fmt)
data = fh.read(size)
return struct.unpack(fmt, data)
def convert(src, dst):
print (src)
src_fh = open(src, 'rb')
bits = -1
# Read header
(chunkid, restsize, wave) = read_struct(src_fh, '<III')
if chunkid != make_tag('R', 'I', 'F', 'F'): # "RIFF" in bigendian
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("Read WAV chunk with ID 0x%x" % chunk_id)
if chunk_id == make_tag('f', 'm', 't', ' '):
# Read format
(AudioFormat, NumChannels, SampleRate, ByteRate, BlockAlign, BitsPerSample) = read_struct(src_fh, '<HHIIHH')
print("AudioFormat:", AudioFormat)
print("BPS:", BitsPerSample)
print("NumChannels:", NumChannels)
print("ByteRate:", ByteRate)
if (AudioFormat != 1):
print("Compressed format, can't decode")
return -1
if (NumChannels != 1):
print("Not mono, can't decode")
return -1
if BitsPerSample != 16 and BitsPerSample != 24:
print("Not 16-bit or 24-bit, can't decode")
return -1
bits = BitsPerSample
if (chunk_id == make_tag('d', 'a', 't', 'a')):
print("Found sample data")
vals = array.array('l')
# Note: data is little-endian, signed
if bits == 16:
num_samples = int(chunk_size / 2)
for x in range(0, num_samples):
(lo, hi) = read_struct(src_fh, '<BB')
# Convert to unsigned
uns = (((lo) | (hi << 8)) + 0x8000) & 0xffff
vals.append(uns)
elif bits == 24:
num_samples = int(chunk_size / 3)
for x in range(0, num_samples):
# Read 24 but only use 16
(dummy, lo, hi) = read_struct(src_fh, '<BBB')
# Convert to unsigned
uns = (((lo) | (hi << 8)) + 0x8000) & 0xffff
vals.append(uns)
else:
print("Unsupported bit count")
return -1
# Convert (badly)
fourbit_list = [nearest16(val) for val in vals]
# end marker
fourbit_list[-1] |= 0x80
with open(dst, 'wb') as dst_fh:
dst_fh.write(bytearray(fourbit_list))
dst_fh.close()
print("Converted to %s OK" % dst)
break
# Move to next
src_fh.seek(offset + chunk_size + 8, os.SEEK_SET)
src_fh.close()
if __name__ == '__main__':
import sys
src = sys.argv[1]
dst = sys.argv[2]
convert(src, dst)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment