Skip to content

Instantly share code, notes, and snippets.

@MineRobber9000
Created March 4, 2024 06:51
Show Gist options
  • Save MineRobber9000/f5db1f6a9e8cd275e1e2ec41a52763f2 to your computer and use it in GitHub Desktop.
Save MineRobber9000/f5db1f6a9e8cd275e1e2ec41a52763f2 to your computer and use it in GitHub Desktop.
MDFPWM converter in Python. Assumes you have up-to-date ffmpeg in PATH.
# MDFPWM encoder
# Uses ffmpeg to encode a WAV file as dfpwm
# Assumes ffmpeg is on PATH
# CC0, whatever
# credit Drucifer@SwitchCraft.kst for the format
import subprocess, tempfile, os, os.path, struct, json
def I(n):
return struct.pack("<I",n)
def s1(s):
ba = bytearray([len(s)&0xFF])
ba.extend(s.encode('utf-8'))
return bytes(ba)
def safe_get(d,*args):
r = d
for name in args:
r = r.get(name,dict())
return r
def get_metadata(filename):
md = subprocess.check_output(["ffprobe","-show_entries","format_tags","-of","json",filename])
metadata = safe_get(json.loads(md),"format","tags")
ret = {}
if (title:=metadata.get("title")): ret["title"]=title
if (artist:=metadata.get("artist")): ret["artist"]=artist
if (album:=metadata.get("album")): ret["album"]=album
return ret
def split_to_dfpwm(filename):
try:
with tempfile.TemporaryDirectory() as td:
left_fn = os.path.join(td,"left.dfpwm")
right_fn = os.path.join(td,"right.dfpwm")
subprocess.run(["ffmpeg","-y","-i",filename,"-ar","48000","-af","pan=mono|c0=c0",left_fn,"-ar","48000","-af","pan=mono|c0=c1",right_fn]).check_returncode()
with open(left_fn,"rb") as tf_left: left = tf_left.read()
with open(right_fn,"rb") as tf_right: right = tf_right.read()
assert len(left)==len(right), f"left size ({len(left)}) != right size ({len(left)})"
return left, right
except subprocess.CalledProcessError:
print("Error calling ffmpeg; does your ffmpeg installation support dfpwm?")
raise
# The spec wants us to pad to a full second if we have a fractional second at the end
# The "correct" way to do this is to decode to raw PCM, pad with 0/whatever, and then re-encode to DFPWM...
# ...or we can cheat by telling the encoder to move back and forth on a small spot until the second is over
def pad_dfpwm(b):
return (b+(b"\x55"*6000))[:6000]
# converts an audio file supported by ffmpeg into a MDFPWM file
# input_filename: the... input filename
# output_filename: take a guess. defaults to input_filename + ".mdfpwm"
# metadata_override: a dict containing overrides for the metadata, which is otherwise extracted from the input file
# "title", "artist", and "album" are the keys we use
def convert(input_filename,output_filename=None,metadata_override={}):
if output_filename is None: output_filename = input_filename+".mdfpwm"
metadata = get_metadata(input_filename)
metadata.update(metadata_override)
left, right = split_to_dfpwm(input_filename)
data = bytearray()
data.extend(b'MDFPWM\x03') # header: MDFPWM v3
data.extend(I(len(left)+len(right))) # sample length
data.extend(s1(metadata.get("title",""))) # title
data.extend(s1(metadata.get("artist",""))) # artist
data.extend(s1(metadata.get("album",""))) # album
n = 0
while n<len(left):
data.extend(pad_dfpwm(left[n:n+6000]))
data.extend(pad_dfpwm(right[n:n+6000]))
n+=6000
with open(output_filename,"wb") as f:
f.write(data)
import argparse
if __name__=="__main__":
parser = argparse.ArgumentParser(description="Converts audio files to MDFPWMv3.")
parser.add_argument("-t","--title",help="The title of the song. Defaults to the embedded title metadata.")
parser.add_argument("-a","--artist",help="The title of the song. Defaults to the embedded title metadata.")
parser.add_argument("-b","--album",help="The title of the song. Defaults to the embedded title metadata.")
parser.add_argument("input_filename",help="The filename of the input audio file.")
parser.add_argument("output_filename",nargs="?",help="The filename of the output MDFPWMv3 file. Defaults to the input filename plus \".dfpwm\".")
args = parser.parse_args()
override = {}
if args.title: override["title"]=args.title
if args.artist: override["artist"]=args.artist
if args.album: override["album"]=args.album
convert(args.input_filename,args.output_filename,override)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment