Created
March 4, 2024 06:51
-
-
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.
This file contains hidden or 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
# MDFPWM encoder | |
# Uses ffmpeg to encode a WAV file as dfpwm | |
# Assumes ffmpeg is on PATH | |
# CC0, whatever | |
# credit [email protected] 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
As should be clear from the rest of the comment (as well as the code itself), I use
\x55
for "correctness" (going back and forth on ~0dB is technically what blank audio should be).