From e8dcb586a388a798ea2a7a1bf47208f8bd168d23 Mon Sep 17 00:00:00 2001 From: hkc Date: Tue, 17 Oct 2023 15:28:43 +0300 Subject: [PATCH] Added multi-track tape player --- tape-playlist.lua | 88 +++++++++++++++++++++++++++++++++++++++++++++++ tape-playlist.py | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 tape-playlist.lua create mode 100644 tape-playlist.py diff --git a/tape-playlist.lua b/tape-playlist.lua new file mode 100644 index 0000000..873c694 --- /dev/null +++ b/tape-playlist.lua @@ -0,0 +1,88 @@ +local drive = peripheral.find("tape_drive") +if not tape then + printError("no tape drive found") + printError("it's kinda required to play tapes, you know?") + return +end + +local running = true + +local table_of_contents = {} + +local function read32() + local v = 0 + for i = 1, 4 do + local b = drive.read() + v = bit32.bor(bit32.lshift(v), b) + end + return v +end + +local function readstr(len) + local out = "" + for i = 1, len do + out = out .. drive.read() + end + return out +end + +local function bytes2time(b) + local s = math.floor(b / 6000) + if s < 60 then return string.format("%ds", s) end + return string.format("%dm, %ds", math.floor(s / 60), s % 60) +end + + +parallel.waitForAll( +function() + while running do + for i, track in ipairs(table_of_contents) do + term.setCursorPos(1, i) + term.write(string.format("%s (%s)", track.title, bytes2time(track.length))) + end + os.sleep(0.1) + end +end, +function() + while running do + local evd = { os.pullEvent() } + local ev, evd = table.remove(evd, 1), evd + + if ev == "mouse_click" then + local x, y = table.unpack(evd, 1) + elseif ev == "tape_present" then + table_of_contents = {} + if evd[1] then + drive.seek(-drive.getSize()) + for i = 1, 48 do + local offset = read32() + local length = read32() + local title = readstr(117) + table.insert(table_of_contents, { + title = title, + offset = offset, + length = length, + ending = offset + length + }) + end + end + end + end +end, +function() + local tape_was_present = nil + local drive_old_state = nil + while running do + local tape_present = drive.isReady() + if tape_present ~= tape_was_present then + os.queueEvent("tape_present", tape_present) + end + + local drive_state = drive.getState() + if drive_old_state ~= drive_state then + os.queueEvent("drive_state", drive_state) + end + + os.sleep(0.25) + end +end) diff --git a/tape-playlist.py b/tape-playlist.py new file mode 100644 index 0000000..3c65914 --- /dev/null +++ b/tape-playlist.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# x-run: python3 % /mnt/windows/Users/user/Music/vgm/night-in-the-woods/2014-lost-constellation /tmp/lost-constellation.dfpwm + +from tempfile import TemporaryDirectory +from subprocess import Popen, PIPE +from sys import argv +from pathlib import Path +from os.path import splitext +from math import ceil + +DFPWM_ENCODER_EXECUTABLE = "aucmp" +FFMPEG_EXECUTABLE = "ffmpeg" + +DFPWM_SAMPLE_RATE = 6000 + +TAPE_SIZES = [ 2, 4, 8, 16, 32, 64, 128 ] + +input_folder = Path(argv[1]) +output_file = Path(argv[2]) + +with TemporaryDirectory(prefix="tapedrive") as tmpdir_str: + tmpdir = Path(tmpdir_str) + + filelist: list[Path] = [] + for i, name in enumerate(input_folder.rglob("*.mp3")): + if i >= 48: + print(f"more than 48 tracks, skipping {name}") + continue + encoded_file = tmpdir / (splitext(name.name)[0] + ".dfpwm") + filelist.append(encoded_file) + with encoded_file.open("wb") as fout: + with Popen([ DFPWM_ENCODER_EXECUTABLE ], stdout=fout, stdin=PIPE) as dfpwm: + with Popen([ FFMPEG_EXECUTABLE, "-i", name, "-f", "s8", "-ac", "1", + "-ar", "48k", "-" ], stdout=dfpwm.stdin) as ffmpeg: + ffmpeg.wait() + + offset: int = 6000 + titles: list[bytes] = [] + positions: list[tuple[int, int]] = [] + for file in filelist: + titles.append(file.name.removesuffix(".dfpwm").encode("ascii", errors="replace")) + size = ceil(file.stat().st_size / DFPWM_SAMPLE_RATE) * DFPWM_SAMPLE_RATE + positions.append((offset, size)) + offset += size + + with output_file.open("wb") as fout: + print("Writing header...") + written_bytes: int = 0 + for i in range(48): + name = (titles[i] if i < len(titles) else b"")[:117] + pos = positions[i] if i < len(titles) else (0, 0) + written_bytes += fout.write(pos[0].to_bytes(4, "little")) + written_bytes += fout.write(pos[0].to_bytes(4, "little")) + written_bytes += fout.write(name) + written_bytes += fout.write(b"\x00" * (117 - len(name))) + if written_bytes != 6000: + print(f"!!! expected 6000 bytes to be written in header, but it's {written_bytes}") + + print("Writing files...") + for i, (pos, file) in enumerate(zip(positions, filelist)): + print(f"Writing {file.name}") + if written_bytes != pos[0]: + print(f"!!! expected to be at {pos[0]}, rn at {written_bytes}") + with file.open("rb") as fin: + file_size: int = 0 + while (chunk := fin.read(DFPWM_SAMPLE_RATE)): + file_size += fout.write(chunk) + written_bytes += file_size + padding_size = DFPWM_SAMPLE_RATE - (written_bytes % DFPWM_SAMPLE_RATE) + written_bytes += fout.write(b"\x00" * padding_size) + print("Use any of those tapes to store properly:") + for size in filter(lambda sz: sz * DFPWM_SAMPLE_RATE * 60 >= written_bytes, TAPE_SIZES): + print(f"{size} minutes ({size * DFPWM_SAMPLE_RATE * 60} bytes, {written_bytes * 100 / (size * DFPWM_SAMPLE_RATE * 60):7.3f}% used)")