View difference between Paste ID: EbHZq4uU and HVJkzHiL
SHOW: | | - or go back to the newest paste.
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python3
2-
import subprocess
2+
import subprocess
3-
import tempfile
3+
import tempfile
4-
import os
4+
import os
5-
import os.path
5+
import os.path
6-
import collections
6+
import collections
7-
import json
7+
import json
8-
import sys
8+
import sys
9-
import multiprocessing
9+
import multiprocessing
10-
import struct
10+
import struct
11-
11+
12-
ProcessedTrack = collections.namedtuple("ProcessedTrack", ["dfpwm_file", "metadata"])
12+
ProcessedTrack = collections.namedtuple("ProcessedTrack", ["dfpwm_file", "metadata"])
13-
13+
14-
def convert_wav_dfpwm(infile, outfile):
14+
def convert_wav_dfpwm(infile, outfile):
15-
    subprocess.run(["java", "-jar", "LionRay.jar", infile, outfile])
15+
    subprocess.run(["java", "-jar", "LionRay.jar", infile, outfile])
16-
16+
17-
def convert_any_wav(infile, outfile):
17+
def convert_any_wav(infile, outfile):
18-
    subprocess.run(["ffmpeg", "-hide_banner", "-i", infile, "-ac", "1", outfile], stderr=subprocess.PIPE)
18+
    subprocess.run(["ffmpeg", "-hide_banner", "-i", infile, "-ac", "1", outfile], stderr=subprocess.PIPE)
19-
19+
20-
def read_meta(path):
20+
def read_meta(path):
21-
    proc = subprocess.run(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", path], stdout=subprocess.PIPE)
21+
    proc = subprocess.run(["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", path], stdout=subprocess.PIPE)
22-
    data = json.loads(proc.stdout)
22+
    data = json.loads(proc.stdout)
23-
    meta = {}
23+
    meta = {}
24-
    # These are the two locations I've found tags in in my not very thorough testing
24+
    # These are the two locations I've found tags in in my not very thorough testing
25-
    try:
25+
    try:
26-
        meta.update(data["format"]["tags"])
26+
        meta.update(data["format"]["tags"])
27-
    except KeyError: pass
27+
    except KeyError: pass
28-
    try:
28+
    try:
29-
        meta.update(data["streams"][0]["tags"])
29+
        meta.update(data["streams"][0]["tags"])
30-
    except KeyError: pass
30+
    except KeyError: pass
31-
    # lowercase all keys because in Opus files these seem to be uppercase sometimes
31+
    # lowercase all keys because in Opus files these seem to be uppercase sometimes
32-
    return { k.lower(): v for k, v in meta.items() }
32+
    return { k.lower(): v for k, v in meta.items() }
33-
33+
34-
def process_file(filename):
34+
def process_file(filename):
35-
    meta = read_meta(filename)
35+
    meta = read_meta(filename)
36-
    wav_dest = tempfile.mktemp(".wav")
36+
    wav_dest = tempfile.mktemp(".wav")
37-
    convert_any_wav(filename, wav_dest)
37+
    convert_any_wav(filename, wav_dest)
38-
    dfpwm_dest = tempfile.mktemp(".dfpwm")
38+
    dfpwm_dest = tempfile.mktemp(".dfpwm")
39-
    convert_wav_dfpwm(wav_dest, dfpwm_dest)
39+
    convert_wav_dfpwm(wav_dest, dfpwm_dest)
40-
    os.remove(wav_dest)
40+
    os.remove(wav_dest)
41-
    print(filename)
41+
    print(filename)
42-
    return ProcessedTrack(dfpwm_dest, {
42+
    return ProcessedTrack(dfpwm_dest, {
43-
        "title": meta["title"],
43+
        "title": meta["title"],
44-
        "artist": meta.get("artist", None) or meta.get("artists", None),
44+
        "artist": meta.get("artist", None) or meta.get("artists", None),
45-
        "album": meta.get("album", None)
45+
        "album": meta.get("album", None)
46-
    })
46+
    })
47-
47+
48-
def read_binary(filename):
48+
def read_binary(filename):
49-
    with open(filename, "rb") as f:
49+
    with open(filename, "rb") as f:
50-
        return f.read()
50+
        return f.read()
51-
51+
52-
def process_dir(dirname):
52+
def process_dir(dirname):
53-
    files = list(map(lambda file: os.path.join(dirname, file), os.listdir(dirname)))
53+
    files = list(map(lambda file: os.path.join(dirname, file), os.listdir(dirname)))
54-
    with multiprocessing.Pool(8) as p:
54+
    with multiprocessing.Pool(8) as p:
55-
        tracks = p.map(process_file, files)
55+
        tracks = p.map(process_file, files)
56-
    tape_image = b""
56+
    tape_image = b""
57-
    tracks_meta = []
57+
    tracks_meta = []
58-
    for track in tracks:
58+
    for track in tracks:
59-
        track.metadata["start"] = len(tape_image)
59+
        track.metadata["start"] = len(tape_image)
60-
        data = read_binary(track.dfpwm_file)
60+
        data = read_binary(track.dfpwm_file)
61-
        os.remove(track.dfpwm_file)
61+
        os.remove(track.dfpwm_file)
62-
        track.metadata["end"] = track.metadata["start"] + len(data)
62+
        track.metadata["end"] = track.metadata["start"] + len(data)
63-
        tape_image += data
63+
        tape_image += data
64-
        tracks_meta.append(track.metadata)
64+
        tracks_meta.append(track.metadata)
65-
    meta = json.dumps({ "tracks": tracks_meta }).encode("utf-8")
65+
    # dump in a compact format to save space
66-
    assert(len(meta) < 65536)
66+
    meta = json.dumps({ "tracks": tracks_meta }, separators=(',', ':')).encode("utf-8")
67-
    # new format - 0x54 marker byte, then metadata length as 2-byte big endian integer, then metadata, then concatenated DFPWM files
67+
    assert(len(meta) < 65536)
68-
    # start is now not an absolute position but just how far after the metadata it is
68+
    # new format - 0x54 marker byte, then metadata length as 2-byte big endian integer, then metadata, then concatenated DFPWM files
69-
    tape_image = b"\x54" + struct.pack(">H", len(meta)) + meta + tape_image
69+
    # start is now not an absolute position but just how far after the metadata it is
70-
    with open("tape.bin", "wb") as f:
70+
    tape_image = b"\x54" + struct.pack(">H", len(meta)) + meta + tape_image
71-
        f.write(tape_image)
71+
    with open("tape.bin", "wb") as f:
72-
72+
        f.write(tape_image)
73
    # Tape lengths are measured in minutes. 6000 bytes are played per second because they use a 48000Hz sample rate and DFPWM is somehow 1 bit per sample.
74
    length_minutes = len(tape_image) / (6000*60)
75
    print(length_minutes, "minute tape required")
76
77
process_dir(sys.argv[1])