discstation 0.1.31 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/arduino/DiscStation/DiscStation.ino +14 -2
- package/docs/PLATFORM_SUPPORT.md +43 -15
- package/package.json +1 -1
- package/src/discstation.py +66 -6
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ remote can talk to the host over Wi-Fi instead of a USB cable.
|
|
|
25
25
|
the remote starts an open `DiscStation-XXXX` access point; join it from a
|
|
26
26
|
phone, the captive portal (or `http://192.168.4.1`) lists nearby networks
|
|
27
27
|
— pick yours, enter the password, done. It reconnects on every boot after
|
|
28
|
-
that. Change networks later: **hold
|
|
29
|
-
to wipe the credentials and reopen the portal.
|
|
28
|
+
that. Change networks later: **hold HOME/BACK for 10 s** on the home
|
|
29
|
+
screen to wipe the credentials and reopen the portal.
|
|
30
30
|
Power users can instead `cp arduino/<board>/secrets.h.example secrets.h`
|
|
31
31
|
and set `WIFI_SSID` / `WIFI_PASS` at build time (git-ignored).
|
|
32
32
|
- **Host side:** nothing to configure. With no USB cable present the host
|
|
@@ -47,7 +47,7 @@ remote can talk to the host over Wi-Fi instead of a USB cable.
|
|
|
47
47
|
| **Burn Video DVD** | YouTube URL or local file → ffmpeg 2-pass → DVD-Video disc |
|
|
48
48
|
| **Burn Data DVD** | Any files/folders → ISO/Joliet data disc (no quality loss) |
|
|
49
49
|
| **Burn MPG** | Re-burn a previously converted movie.mpg |
|
|
50
|
-
| **Play** | Playback via mpv (DVD-Video, Audio CD, VCD, SVCD) |
|
|
50
|
+
| **Play** | Playback via mpv (DVD-Video, Audio CD, VCD, SVCD) — the OLED remote shows a live spectrum visualizer of the actual audio (Linux only for now; see `docs/PLATFORM_SUPPORT.md`) |
|
|
51
51
|
| **Rip** | Audio CD → FLAC (MusicBrainz); DVD-Video → VIDEO_TS mirror or HandBrake MKV (TMDb naming) |
|
|
52
52
|
|
|
53
53
|
## Project Structure
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
#define VU_ENTRY_DELAY_MS 10000 // stay on the text status screen this long after entering PLAY
|
|
48
48
|
#define VU_RESUME_DELAY_MS 7000 // ...and this long after any input while already in PLAY
|
|
49
49
|
|
|
50
|
-
#define WIFI_RESET_HOLD_MS 10000 // hold
|
|
50
|
+
#define WIFI_RESET_HOLD_MS 10000 // hold HOME/BACK this long on HOME to wipe Wi-Fi creds
|
|
51
51
|
#define WIFI_CONNECT_TIMEOUT_MS 18000 // give a stored-creds join this long before falling to the portal
|
|
52
52
|
#define WIFI_RETRY_MS 15000 // if a live link drops, force a re-join after this
|
|
53
53
|
|
|
@@ -796,6 +796,8 @@ void parseMessage(String msg) {
|
|
|
796
796
|
line2 = "Playing disc";
|
|
797
797
|
playSeekMode = false; // always start in volume mode
|
|
798
798
|
vuSuppressUntil = millis() + VU_ENTRY_DELAY_MS; // text screen first, bars after a beat
|
|
799
|
+
lastVuAt = 0; // fresh session - unknown yet whether the host even sends VU: at all
|
|
800
|
+
visualizerActive = false;
|
|
799
801
|
Out.print("POT:"); // push the last-used volume so playback starts at it
|
|
800
802
|
Out.println(playVolume);
|
|
801
803
|
drawPlay();
|
|
@@ -1326,9 +1328,19 @@ void loop() {
|
|
|
1326
1328
|
// PLAY is included because a static "PLAYING" screen is just as low-current
|
|
1327
1329
|
// as HOME/STANDBY were - the power bank doesn't care what's on screen, only
|
|
1328
1330
|
// that the draw stays static this long.
|
|
1331
|
+
//
|
|
1332
|
+
// In PLAY specifically, if this session has never received a single VU:
|
|
1333
|
+
// (lastVuAt == 0 - the host/platform doesn't support the visualizer, e.g.
|
|
1334
|
+
// macOS today), don't make the user wait out the full generic idle timer
|
|
1335
|
+
// for an animation - drop into the spinning-disc screensaver as soon as
|
|
1336
|
+
// the text-hold window (vuSuppressUntil) expires. Once any VU: does
|
|
1337
|
+
// arrive this stops applying (lastVuAt is no longer 0) and PLAY behaves
|
|
1338
|
+
// exactly as before, showing bars instead.
|
|
1339
|
+
bool playSkippingToScreensaver = (uiState == UI_PLAY) && (lastVuAt == 0) &&
|
|
1340
|
+
(long)(millis() - vuSuppressUntil) >= 0;
|
|
1329
1341
|
if (displayOk && !displayBlank &&
|
|
1330
1342
|
(uiState == UI_HOME || uiState == UI_STANDBY || uiState == UI_PLAY) &&
|
|
1331
|
-
(long)(millis() - lastInputTime) >= IDLE_BLANK_MS) {
|
|
1343
|
+
((long)(millis() - lastInputTime) >= IDLE_BLANK_MS || playSkippingToScreensaver)) {
|
|
1332
1344
|
displayBlank = true;
|
|
1333
1345
|
saverStep = 0;
|
|
1334
1346
|
lastSaverFrame = 0;
|
package/docs/PLATFORM_SUPPORT.md
CHANGED
|
@@ -27,21 +27,49 @@ Set `DISC_DEVICE` if automatic drive detection fails. The one known limitation
|
|
|
27
27
|
is **audio-CD *burning*** — `cdrdao` is the only option and often cannot claim
|
|
28
28
|
the drive on recent macOS; DiscStation reports this clearly instead of hanging.
|
|
29
29
|
|
|
30
|
-
### ESP32 remote: OLED spectrum visualizer
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
### ESP32 remote: OLED spectrum visualizer — not currently working on macOS
|
|
31
|
+
|
|
32
|
+
**Confirmed broken, not just a setup step.** The visualizer's macOS capture
|
|
33
|
+
path (`ffmpeg -f avfoundation` reading the BlackHole loopback device) was
|
|
34
|
+
built and the BlackHole/Multi-Output-Device routing was set up and verified
|
|
35
|
+
correct on real hardware — but `ffmpeg`'s capture consistently returns
|
|
36
|
+
silence (valid-looking output, zero errors, every sample exactly `0`) even
|
|
37
|
+
with the native macOS Sound input level meter confirmed showing real,
|
|
38
|
+
moving signal on BlackHole at the same moment. That combination — real
|
|
39
|
+
signal present at the OS/driver level, silently zeroed once it reaches an
|
|
40
|
+
app's capture buffer — is macOS's system-audio-capture privacy protection
|
|
41
|
+
muting an untrusted capturer, not a permission checkbox that was missed:
|
|
42
|
+
Microphone and Screen & System Audio Recording were both granted to
|
|
43
|
+
`ffmpeg` with no change.
|
|
44
|
+
|
|
45
|
+
The legitimate modern API for this (CoreAudio's Process Tap,
|
|
46
|
+
`AudioHardwareCreateProcessTap`, macOS 14.2+) requires a properly
|
|
47
|
+
code-signed app bundle requesting a specific entitlement, with its own
|
|
48
|
+
dedicated system consent dialog — a bare ad-hoc-signed Homebrew CLI binary
|
|
49
|
+
like `ffmpeg` structurally cannot satisfy that, regardless of which Privacy
|
|
50
|
+
& Security toggles are flipped. Making the visualizer work on macOS would
|
|
51
|
+
mean building a small signed helper app around that API — a real, separate
|
|
52
|
+
project, not started.
|
|
53
|
+
|
|
54
|
+
**Everything else works normally** — audio-CD/DVD/video playback, ripping,
|
|
55
|
+
burning, and the web UI are all unaffected. PLAY just always shows the
|
|
56
|
+
normal text status screen on macOS instead of ever switching to bars.
|
|
57
|
+
|
|
58
|
+
If BlackHole is already installed from an earlier attempt at this, it's
|
|
59
|
+
harmless to leave in place — it just won't do anything useful for
|
|
60
|
+
DiscStation until/unless the above gets built.
|
|
61
|
+
|
|
62
|
+
### Troubleshooting playback ("mpv not found" / play fails to start)
|
|
63
|
+
|
|
64
|
+
- **`install-macos.sh` installs `mpv` via Homebrew** — if playback fails to
|
|
65
|
+
start, confirm it actually landed: `mpv --version` in Terminal. If that
|
|
66
|
+
fails, either the installer never ran to completion or was interrupted;
|
|
67
|
+
re-run `install-macos.sh` (or `discstation-setup`) rather than installing
|
|
68
|
+
`mpv` in isolation, since other steps may be incomplete too.
|
|
69
|
+
- **Apple Music auto-opening on disc insert can hold the drive**, so
|
|
70
|
+
DiscStation's own `mpv` can fail or hang trying to claim it right after.
|
|
71
|
+
System Settings → **CDs & DVDs** (only shown with an optical drive
|
|
72
|
+
connected) → set "When you insert a music CD" to **Ignore**.
|
|
45
73
|
|
|
46
74
|
## Windows
|
|
47
75
|
|
package/package.json
CHANGED
package/src/discstation.py
CHANGED
|
@@ -3448,7 +3448,29 @@ def start_vu_visualizer(ser):
|
|
|
3448
3448
|
return stop_event, pause_event
|
|
3449
3449
|
|
|
3450
3450
|
|
|
3451
|
-
def
|
|
3451
|
+
def _audio_cd_track_seek(delta, track_starts):
|
|
3452
|
+
"""Seek to the start of the next/previous track using the CD TOC's own
|
|
3453
|
+
per-track offsets (already in seconds from track 1's start - see
|
|
3454
|
+
audio_track_metadata) instead of mpv's own chapter list. Works whether
|
|
3455
|
+
or not mpv has track chapters for this stream: macOS's piped
|
|
3456
|
+
cd-paranoia audio has none, since mpv just sees one continuous stream,
|
|
3457
|
+
not the disc itself - time-pos still works fine either way."""
|
|
3458
|
+
if not track_starts:
|
|
3459
|
+
mpv_command(["add", "chapter", delta])
|
|
3460
|
+
return
|
|
3461
|
+
track = mpv_query(["get_property", "chapter"])
|
|
3462
|
+
if not isinstance(track, (int, float)):
|
|
3463
|
+
position = mpv_query(["get_property", "time-pos"])
|
|
3464
|
+
track = max((i for i, start in enumerate(track_starts) if start <= position), default=0) \
|
|
3465
|
+
if isinstance(position, (int, float)) else 0
|
|
3466
|
+
target = max(0, min(len(track_starts) - 1, int(track) + delta))
|
|
3467
|
+
mpv_command(["seek", track_starts[target], "absolute"])
|
|
3468
|
+
|
|
3469
|
+
|
|
3470
|
+
def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None, stdin_proc=None):
|
|
3471
|
+
"""stdin_proc: an already-started subprocess whose stdout feeds mpv's
|
|
3472
|
+
stdin (e.g. macOS's cd-paranoia-into-mpv audio CD pipe) - mpv reads `-`
|
|
3473
|
+
as its input in cmd in that case. Stopped alongside mpv on cleanup."""
|
|
3452
3474
|
try:
|
|
3453
3475
|
os.unlink(MPV_SOCKET)
|
|
3454
3476
|
except OSError:
|
|
@@ -3472,7 +3494,14 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
|
|
|
3472
3494
|
# writes block, wedging the whole play loop. We drive mpv over the IPC
|
|
3473
3495
|
# socket, so none of that output is wanted.
|
|
3474
3496
|
proc = subprocess.Popen(run_as_desktop_user(cmd), env=env,
|
|
3497
|
+
stdin=(stdin_proc.stdout if stdin_proc else None),
|
|
3475
3498
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
3499
|
+
if stdin_proc:
|
|
3500
|
+
# Close our own copy of the read end now that mpv's had it duped into
|
|
3501
|
+
# its own stdin - otherwise we're also holding it open, so cd-paranoia
|
|
3502
|
+
# never gets SIGPIPE (and just hangs writing into a full pipe buffer)
|
|
3503
|
+
# if mpv exits first.
|
|
3504
|
+
stdin_proc.stdout.close()
|
|
3476
3505
|
vu_stop = None
|
|
3477
3506
|
|
|
3478
3507
|
try:
|
|
@@ -3540,7 +3569,7 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
|
|
|
3540
3569
|
|
|
3541
3570
|
elif line == "FF:BIG":
|
|
3542
3571
|
if kind == "audio_cd":
|
|
3543
|
-
|
|
3572
|
+
_audio_cd_track_seek(1, track_starts)
|
|
3544
3573
|
send(ser, "PLAY_STATUS:Next track")
|
|
3545
3574
|
else:
|
|
3546
3575
|
mpv_command(["seek", 120])
|
|
@@ -3554,7 +3583,7 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
|
|
|
3554
3583
|
except ValueError:
|
|
3555
3584
|
continue
|
|
3556
3585
|
if kind == "audio_cd":
|
|
3557
|
-
|
|
3586
|
+
_audio_cd_track_seek(1, track_starts)
|
|
3558
3587
|
send(ser, "PLAY_STATUS:Next track")
|
|
3559
3588
|
else:
|
|
3560
3589
|
mpv_command(["seek", seek_sec])
|
|
@@ -3564,7 +3593,7 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
|
|
|
3564
3593
|
|
|
3565
3594
|
elif line == "REW:BIG":
|
|
3566
3595
|
if kind == "audio_cd":
|
|
3567
|
-
|
|
3596
|
+
_audio_cd_track_seek(-1, track_starts)
|
|
3568
3597
|
send(ser, "PLAY_STATUS:Prev track")
|
|
3569
3598
|
else:
|
|
3570
3599
|
mpv_command(["seek", -120])
|
|
@@ -3578,7 +3607,7 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
|
|
|
3578
3607
|
except ValueError:
|
|
3579
3608
|
continue
|
|
3580
3609
|
if kind == "audio_cd":
|
|
3581
|
-
|
|
3610
|
+
_audio_cd_track_seek(-1, track_starts)
|
|
3582
3611
|
send(ser, "PLAY_STATUS:Prev track")
|
|
3583
3612
|
else:
|
|
3584
3613
|
mpv_command(["seek", -seek_sec])
|
|
@@ -3604,6 +3633,8 @@ def _run_mpv(ser, cmd, label, kind=None, track_titles=None, track_starts=None):
|
|
|
3604
3633
|
vu_stop.set()
|
|
3605
3634
|
if proc.poll() is None:
|
|
3606
3635
|
discstation_burn.stop_process(proc)
|
|
3636
|
+
if stdin_proc and stdin_proc.poll() is None:
|
|
3637
|
+
discstation_burn.stop_process(stdin_proc)
|
|
3607
3638
|
try:
|
|
3608
3639
|
os.unlink(MPV_SOCKET)
|
|
3609
3640
|
except OSError:
|
|
@@ -3774,11 +3805,40 @@ def play_flow(ser):
|
|
|
3774
3805
|
|
|
3775
3806
|
elif kind == "audio_cd":
|
|
3776
3807
|
_, track_titles, track_starts = audio_track_metadata(device)
|
|
3777
|
-
|
|
3808
|
+
system = discstation_host.system_name()
|
|
3809
|
+
if system == "windows":
|
|
3778
3810
|
# mpv on Windows has no libcdio - cdda:// is unavailable there
|
|
3779
3811
|
# ("disabled at compile-time"). Windows Media Player's own COM
|
|
3780
3812
|
# control plays it fine via Windows' native CD-audio support.
|
|
3781
3813
|
_play_audio_cd_windows(ser, device, track_titles)
|
|
3814
|
+
elif system == "darwin":
|
|
3815
|
+
# Homebrew's mpv formula doesn't depend on libcdio either (no
|
|
3816
|
+
# build option to add it) - confirmed live: `mpv cdda://` says
|
|
3817
|
+
# "protocol ... disabled at compile-time" and --cdrom-device
|
|
3818
|
+
# isn't even a recognized option. Same shape of gap as Windows,
|
|
3819
|
+
# different fix: stream the disc via cd-paranoia (already used
|
|
3820
|
+
# for ripping) into mpv's stdin instead of mpv opening the
|
|
3821
|
+
# drive itself. mpv still does all actual playback + IPC
|
|
3822
|
+
# control (pause/volume/stop), just fed a pipe instead of the
|
|
3823
|
+
# disc directly.
|
|
3824
|
+
paranoia = None
|
|
3825
|
+
for name in ("cd-paranoia", "cdparanoia"):
|
|
3826
|
+
try:
|
|
3827
|
+
paranoia = discstation_burn.tool(name)
|
|
3828
|
+
break
|
|
3829
|
+
except FileNotFoundError:
|
|
3830
|
+
continue
|
|
3831
|
+
if not paranoia:
|
|
3832
|
+
raise RuntimeError("cd-paranoia not installed (brew install libcdio-paranoia)")
|
|
3833
|
+
rip_proc = subprocess.Popen(
|
|
3834
|
+
[paranoia, "-d", rip_device(device), "1-", "-"],
|
|
3835
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
|
3836
|
+
audio_device = discstation_host.audio_output_device()
|
|
3837
|
+
cmd = [mpv, "--input-ipc-server=" + MPV_SOCKET, "--force-window=no", "--idle=no", "-"]
|
|
3838
|
+
if audio_device:
|
|
3839
|
+
cmd.insert(1, "--audio-device=" + audio_device)
|
|
3840
|
+
print(f"Audio CD output: {audio_device}")
|
|
3841
|
+
_run_mpv(ser, cmd, "Playing audio CD", kind, track_titles, track_starts, stdin_proc=rip_proc)
|
|
3782
3842
|
else:
|
|
3783
3843
|
audio_device = discstation_host.audio_output_device()
|
|
3784
3844
|
cmd = [
|