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]) |