CassetteFlow is a hybrid analog-digital audio system that bridges physical compact cassette tapes with digital audio playback. By recording an inaudible or dedicated-track Frequency Shift Keying (FSK) timecode and metadata stream onto an analog cassette tape, CassetteFlow decodes the tape head position in real time using DSP demodulation and synchronizes lossless digital audio playback (MP3, FLAC, WAV) locally or across a local network.
+-----------------------------------------------------------------------------------+
| CassetteFlow Core Engine |
| |
| [ Line-In / Mic ] ──► JMinimodem (1200 Baud FSK Demodulator / DSP) |
| │ |
| ▼ |
| Tape Decoder Engine |
| (Timecode / Track Hash Sync) |
| │ |
| ┌───────────────────┴───────────────────┐ |
| ▼ ▼ |
| Local Playback Engine Built-in CassetteFlowServer |
| (WavPlayer / JavaSound) (Port 8192) |
| ┌───────────────────┼───────────────────┐ |
| ▼ ▼ ▼ |
| /music/* /raw & /player |
| (HTTP 206 /dcs_lite Telemetry (Web App) |
| Audio Streaming) │ │ |
+────────────────────────────────────────────────────┼───────────────────┼──────────+
▼ ▼
┌───────────────────────────────────┐
| Any Browser / Phone / M5Core / PC |
| (http://<host>:8192/player) |
└───────────────────────────────────┘
The CassetteFlow desktop interface is organized into distinct workflow tabs:
-
Multi-Function Directory & Smart Search/Import Input:
The text field at the top of the ENCODE tab (defaulting to
C:\mp3files) serves as an interactive multi-purpose command and input bar with 4 distinct operational modes:-
Audio Directory Path & Indexing:
- Displays and sets the active working audio library directory (
currentAudioDirectory). - Typing a directory path and loading will recursively scan, parse ID3 tags, and index all audio files.
- Designates the base storage directory for generated tape data files (
<AudioDir>\TapeFiles).
- Displays and sets the active working audio library directory (
-
Live Library Keyword Filter:
- Typing any search string (e.g. artist name, song title, album, or filename) and pressing Enter (or clicking the (F) filter button) instantly filters the library list in real time.
- Clearing the text field and pressing Enter clears the filter and restores the full library list and directory display.
-
Spotify Playlist & Track Importer:
- Pasting a Spotify URL or URI and pressing Enter automatically detects the media type via the integrated Spotify connector and loads tracks directly into the track list:
-
https://open.spotify.com/playlist/<id>$\to$ Loads entire Spotify playlist. -
https://open.spotify.com/album/<id>$\to$ Loads full Spotify album tracks. -
https://open.spotify.com/track/<id>$\to$ Imports a single Spotify track. -
spotify:...URI$\to$ Imports active Spotify player queue.
-
- Pasting a Spotify URL or URI and pressing Enter automatically detects the media type via the integrated Spotify connector and loads tracks directly into the track list:
-
YouTube & DeckCast Playlist Importer:
- Pasting a YouTube playlist URL (e.g.
https://www.youtube.com/playlist?list=...) or ayoutube:...URI and pressing Enter communicates with the DeckCast connector to automatically extract and populate video audio tracks into the playlist builder.
- Pasting a YouTube playlist URL (e.g.
-
Audio Directory Path & Indexing:
-
Audio Library Browser:
- Scans your configured audio directory and displays all indexed MP3 and FLAC tracks.
- Shows track duration, bitrate, filename, and ID3 tags (Title, Artist, Album, Year, Genre).
-
Side A & Side B Tracklist Builder:
- Add selected tracks to Side A or Side B with dedicated controls.
- Displays total running time for each side and compares it against standard tape lengths (C-46, C-60, C-90, C-120) with remaining minute/second indicators.
- Allows reordering tracks and setting custom inter-track mute padding (default 4 seconds).
-
Dynamic Content Track (DCT) vs. Standard FSK:
- Standard FSK: Encodes individual track hashes, track numbers, and playback seconds directly onto the tape.
-
DCT (Dynamic Content Track): Encodes a continuous 29-character tape timecode (
DCT0A_01_aaaaaaaaaa_0945_0945), allowing the tape to be mapped dynamically to any playlist in software without re-recording the analog data track.
-
J-Card Generator & Export:
- Generates formatted J-Card cassette inserts matching the exact tracklist, timings, and tape side distributions.
- Integrates with J-Card templates for clean printing and cassette jewel case inserts.
- Exports tracklist and tape configurations to JSON and text formats (
template.jcard.json,tracklist.txt). -
Open J-Cardopens the J-Card template app bundled with CassetteFlow (/jcard?tape=<id>) with that tape's sides already filled in, andSave Templatedownloads the same.jcard.jsonfor offline use. See 6.2 Bundled J-Card Template.
The DECODE tab provides live tape decoding, DSP demodulation, and automated digital playback synchronization.
-
JMinimodem Demodulator:
- Captures line-in audio from your tape deck and decodes 1200-baud Bell 202 FSK audio tones directly in software without external modem hardware.
-
Live Speed & Telemetry Monitoring:
- Instantaneous Baud Rate: Real-time edge-transition tracking (smoothed to ~1200 Baud).
-
Speed Offset / Drift (
$\pm%$ ): Measures exact tape motor speed variation relative to nominal speed (e.g.+0.8%or-1.2%). - Modem SNR (Confidence Ratio): Measures signal-to-noise ratio in decibels (dB) to evaluate tape head alignment and signal quality.
- Signal Strength (%): Displays incoming audio carrier amplitude percentage.
-
Tape Synchronization & Playback Controls:
- START: Initializes the line-in audio stream and begins continuous decoding.
- STOP: Halts decoding and pauses audio playback.
- RESET: Clears telemetry counters, error tallies, and carrier holdover timers.
- Carrier Detect: Detects active FSK carrier tone and automatically manages holdover timers during inter-track pauses or head lifts.
The STREAM PLAY tab enables synchronization between physical cassette tape playback and online/network streaming sources.
- DeckCast & Spotify Integration:
- Connects tape timecode offsets to online streams and local streaming daemons.
- Synchronizes Spotify track selection and playback position directly from tape head timecode.
- Stream Controls:
- Supports dynamic stream offsets, track jumping, and remote streaming endpoints.
The SETUP / CONSOLE tab configures hardware devices, directory paths, modem parameters, and provides raw system diagnostic logs.
- Audio Hardware Configuration:
- Input Mixer (Microphone / Line-In): Selects the audio input device capturing the cassette deck output.
- Output Mixer: Selects the sound card or speaker output for digital audio playback.
- Modem Parameters:
- Configures nominal baud rate (default:
1200), mark frequency (1200 Hz), and space frequency (2200 Hz).
- Configures nominal baud rate (default:
- Directory & Property Management:
- Configure default audio directory path (
audio.directoryincassetteFlow.properties). - Set log file paths and external service URLs.
- Configure default audio directory path (
- Console Monitor:
- Live terminal logging showing raw FSK records, tape timecodes, decoded metadata, and HTTP server events.
A dedicated diagnostic tool designed to test, measure, and calibrate cassette deck hardware.
- Capabilities:
- Measures motor speed accuracy, drift, and wow & flutter over time.
- Computes real-time modem confidence ratio (SNR) to test azimuth and head cleanliness.
- Interactive CLI console commands:
[S]- Display comprehensive speed & telemetry statistics.[R]- Reset statistical accumulators.[X]- Exit tester.
- Synthetic Stream Calibration (
DurationInputStream):- Generates test DCT audio streams of precise durations for calibration and loopback verification.
- TapeDatabaseFrame: Inspects, edits, and manages multi-tape collections saved in
tapedb.txt. - TrackFinderFrame: Searches indexed tracks across large offline libraries and matches them against tape playlists.
CassetteFlow can run completely headless without a graphical user interface—ideal for Raspberry Pi, home servers, or background services.
# Basic CLI mode (Headless server & streamer, local host audio muted by default)
java -jar dist/CassetteFlow.jar -cli
# CLI mode with local host speaker audio playback enabled on device 0
java -jar dist/CassetteFlow.jar -cli -play -d 0
# Headless mode with audio index rebuilt on startup
java -jar dist/CassetteFlow.jar -cli -index| Flag | Parameter | Description |
|---|---|---|
-cli |
None | Runs in Command Line Interface (headless) mode |
-play, -audio |
None | Enables local speaker audio playback on host machine |
-noaudio, -mute |
None | Disables local speaker audio playback on host machine (default in CLI) |
-d, --device |
<index> |
Selects audio output mixer by numeric index (e.g. -d 0) |
-index |
None | Scans audio directory and rebuilds audiodb.bin & audiodb.txt |
-dir |
<path> |
Loads audio files from specified custom directory |
-dct |
<tapeId> |
Maps DCT timecode stream to a specific Tape ID (e.g. -dct CS00) |
-dct-load |
None | Explicitly loads saved tape.dct file on startup |
-nodct |
None | Explicitly disables DCT loading (clean raw stream mode) |
-h, --help |
None | Displays CLI help message |
-
Windows (PowerShell Live Monitor):
while($true) { Clear-Host; Get-Process java | Select-Object Id, CPU, @{Name="RAM (MB)"; Expression={[math]::round($_.CPU, 1)}}, @{Name="RAM (MB)"; Expression={[math]::round($_.WorkingSet64/1MB, 1)}}; Start-Sleep 2 }
-
Linux / Raspberry Pi:
htop -p $(pgrep -f CassetteFlow)
CassetteFlow includes a high-performance, multithreaded embedded HTTP server (CassetteFlowServer.java) listening on port 8192. It serves the web player, streams audio with byte-range seek support, and exposes REST endpoints for microcontrollers (ESP32, M5Core) and web clients.
| Endpoint | Method | Response Format | Description |
|---|---|---|---|
/player |
GET |
text/html |
Serves the single-page CassetteFlow Web Player |
/player.html |
GET |
text/html |
Web player alias |
/library |
GET |
text/html |
Serves the Library & Tape Builder web app (web version of the ENCODE tab, playback only) |
/library.html, /encode |
GET |
text/html |
Library app aliases |
/api/library |
GET |
application/json (gzip) |
Audio library with tag metadata (id, ids, name, title, artist, album, genre, year, length, lengthAsTime, bitrate, file, sd, root, noFile). audioInfoDB stores most files under two hashes — the current relative-path hash plus an alias of the older filename hash kept for backward compatibility — so the endpoint collapses each file into one row (same rule as the desktop Track Finder) and returns the extra hashes in ids so old tape records still resolve. root marks files in the default audio directory, noFile marks stream/title-only records with no playable file, and indexKeys reports the raw hash count. Gzipped when the client sends Accept-Encoding: gzip |
/api/tapedb |
POST |
application/json |
Stores a tape record into tapedb.txt. Body: {"tapeId":"0042","sideA":["hash",…],"sideB":[…],"overwrite":false,"backup":true}. Returns 400 (bad tape id / unknown hash), 409 (record exists and overwrite is false) or 200 with the written record keys |
/jcard/template |
GET |
application/json |
The default J-Card template — the same template.jcard.json the desktop application loads |
/jcard, /jcard/ |
GET |
text/html |
Serves the bundled J-Card Template web app (vendored from ed7n/jcard-template, BSD 2-Clause). Add ?tape=0042 (optionally &combine=1, &title=…, &artist=…) to open it with that tape's card already filled in, or ?data=<base64url json> to pass a card built by another page |
/jcard/res/**, /jcard/cf-bridge.js |
GET |
CSS/JS/fonts/images | The vendored J-Card assets and the CassetteFlow bridge that fills the card in |
/api/jcard?tape=0042[&combine=1][&title=…][&artist=…] |
GET |
application/json |
The J-Card data for a tape: template fields plus titleUpper, titleLower, noteLower, sideALabel, sideBLabel, sideAContents, sideBContents. Returns 400 when no tape id is supplied |
/music/* |
GET, HEAD |
Audio (audio/mpeg, audio/flac, etc.) |
HTTP 206 Partial Content audio stream supporting byte-range scrubbing and seek (Range: bytes=start-end) |
/music?id=<hash> |
GET |
Audio | Streams audio track resolved directly by 10-character hash |
/raw |
GET |
text/plain (Chunked) |
Real-time persistent stream of raw decoded FSK line records |
/rawdct |
GET |
text/plain (Chunked) |
Real-time persistent stream of raw FSK records with DCT translation |
/dcs_lite |
GET |
application/json |
Compact telemetry JSON for ESP32/web clients (baud, speedOffset, snr, sig, data_errors, total_recs) |
/dcs |
GET |
application/json |
Full decode state JSON including track lists and playback status |
/api/status |
GET |
application/json |
Alias for /dcs_lite providing unified telemetry |
/diag |
GET |
text/plain |
Formatted tape diagnostics and speed telemetry summary |
/audiodb.txt |
GET |
text/plain |
5-column standard audio database (from disk or memory) |
/tapedb.txt |
GET |
text/plain |
Tape collection database (from disk or memory) |
/tracklist.txt |
GET |
text/plain |
Current active tracklist |
/dcc?command=<cmd> |
GET |
text/plain |
Executes remote decoder commands (start, stop, reset) |
/playing |
GET, POST |
application/json |
Client Now Playing synchronization (receives track title/artist/hash on track change) |
/play?track=<num> |
GET |
text/plain |
Triggers playback of specified track |
/stop |
GET |
text/plain |
Stops playback |
Access the player by navigating to http://localhost:8192/player (or http://<server-ip>:8192/player) on any device on your network.
- Cassette Visualizer: Animated vintage cassette reels that turn dynamically with tape playback state.
- Tracks View: View active Side A, Side B, or Combined tracklists with track numbers, titles, artists, and durations.
-
Tape DB View: Switch between any tape in your collection (
Tape CS00,Tape EX01, etc.) and preview sides. - Node Folders View: Browse albums and subfolders naturally. The Root folder isolates top-level test tracks, while nested albums appear in clean folder groupings.
-
Modem & Signal Telemetry:
- Baud Rate: Live instantaneous modem baud rate.
-
Speed Drift: Live percentage offset (
$\pm%$ ). - Modem SNR: Live carrier SNR in dB.
-
Decode Errors: Displays
errors / total_records(e.g.0 / 142).
- Built-in Terminal: Live scrollable terminal logging every incoming raw FSK line record, side auto-flips, and drift adjustments.
Access it at http://localhost:8192/library. This is the web equivalent of the desktop ENCODE tab, but for audio playback only — it does not perform any FSK encoding, DCT creation or waveform generation.
- Track List: On load it lists only the audio files sitting in the default audio directory (same as the desktop ENCODE tab) instead of the whole index, so the list stays short and fast.
Defaultreloads that directory list,Filternarrows it by substring andClearempties the working list. - Find Dialog: Searches the complete audio index by Title, Artist, Genre, Album, Year or Folder with an
ALL/MP3/FLACformat filter, a metadata preview pane andView Allgrouping (artist/genre/album/year/folder).Add To Track Listappends the results to the working list (duplicates are skipped) and leaves the dialog open so several searches can be added in a row. - Side A / Side B Builder:
Add,Remove,Remove All,Shuffle(fills the side up to the selected tape length, skipping tracks already on the other side),Move Up/Move DownandCheckfor duplicate detection. ThePlay Timelabel turns red with*** Max Time Exceeded ***when a side is too long. - Playback:
Playplays the selected library track,Play Sideplays a whole side in order and honours theMute (s)setting as a countdown gap between tracks. Clicking another file in the track list while audio is already playing switches to it immediately (same behaviour as the desktopaudioJListValueChanged); Ctrl/Shift-click only builds a selection and does not interrupt playback. Playback uses/musicbyte-range streaming and reports Now Playing to/playing. - View Tape DB Dialog: Browse and filter
tapedb.txtrecords, preview both sides (including tracklist sub-tracks for long mixes) with total play time, optionallyCombine A&B, thenLoad Tapeinto the builder. - Store Dialog: Writes the current Side A/B into
tapedb.txtviaPOST /api/tapedb, with overwrite confirmation,tapedb.txt.bakbackup and automatic refresh of the tape list. - J-Card Dialog: The web version of the desktop "Copy/Export Track List" tab.
Load/Refreshbuilds theSIDE A/SIDE Blist from the current sides using each track's basic name (AudioInfo.getBasicName()),Open Templateloads an existing.jcard.jsonas the base,Save Templatefills intitleUpper/titleLower/noteLower(today's date) plussideAContents/sideBContentsand downloads the pretty printed JSON, andOpen J-Cardlaunches the J-Card template app that ships with CassetteFlow with the card already loaded.Open J-Cardpasses the card to/jcard?data=<base64url json>; the bundled app reads no card data from its query string, so a small bridge injects it and the card renders ready to print — no file dialog, no internet connection. See 6.2 Bundled J-Card Template.
The browser caches
audiodb.txt/tapedb.txtand the library JSON in IndexedDB, so reloads are instant; press⟳-style reload (Defaultor a forced refresh) after re-indexing the music library.
Open http://localhost:8192/jcard for the J-Card template app, or http://localhost:8192/jcard?tape=0042 to have tape 0042 already filled in. Both the library app (Open J-Card) and the desktop J-Card tab (Open J-Card) point here, so neither needs the public site or a manual file round trip any more.
- Offline: every asset the page needs — stylesheets, module scripts and fonts — is served by CassetteFlow from
src/jcard/, so the page renders and prints with no internet connection and sends nothing to a third party. - How the card gets in: upstream accepts no card data via URL. CassetteFlow serves the page with
window.CF_JCARD_DATA(built byCassetteFlow.buildJCardData(…)) and a<base href="/jcard/">, then/jcard/cf-bridge.jsfills the app's own form entries and refreshes the card.?data=<base64url json>is the equivalent for a card built elsewhere (the library dialog). /api/jcard?tape=…returns the same JSON for scripts, e.g.curl "http://localhost:8192/api/jcard?tape=0010".combine=1joins side A and B into a single side, matching the desktopCombine A&Boption.
The bundled app (src/jcard/, everything under res/) is a verbatim copy of ed7n/jcard-template at commit ffa66b7, with only three changes (relative stylesheet paths, and the Switch Interface and Offline Access/PWA sections removed). See src/jcard/VENDOR.md for the full provenance, the licence breakdown (BSD 2-Clause for the app and the freeware font, CC BY-SA 4.0 for the six EDEN Online portal stylesheets copied in for offline use) and the re-sync procedure (tools/fetch-jcard.ps1).
EX01A_01_a516a5e955_012_012
│ │ │ │ └─ Total tape elapsed seconds (012)
│ │ │ └───── Track playback seconds (012, or 012M for mute)
│ │ └──────────── 10-character track hash ID
│ └──────────────────── Track number (01)
└───────────────────────── Tape ID and Side (EX01 Side A)
DCT0A_01_aaaaaaaaaa_0945_0945
│ │ │ │ └─ Total tape elapsed seconds (4 digits: 0945)
│ │ │ └────── Side elapsed seconds (4 digits: 0945)
│ │ └────────────── 10-character dummy placeholder hash (aaaaaaaaaa)
│ └────────────────────── Track indicator / segment number (2 digits: 01)
└─────────────────────────── DCT Header & Side (DCT0A = Side A, DCT0B = Side B)
### NOCARRIER ###- Carrier tone dropped (head lifted, tape stopped, or end of side).### NOISY ###- Signal degraded below demodulation threshold.
<Hash10C>\t<DurationSec>\t<Bitrate>\t/sdcard/<SDFilename>\t<RelativePath>
- Example:
a516a5e955 245 320 /sdcard/01_Song.mp3 Rock/Led Zeppelin/IV/01_Song.mp3
<TapeSideID>\t<Hash1>\t<Hash2>\t<Hash3>...
- Example:
EX01A a516a5e955 b627c8f102 c938d4e311 EX01B d149e5f422 e250f6a533 f361a7b644
# CassetteFlow Global Configuration
audio.directory=C\:\\mp3files
baud.rate=1200
download.server=http\://192.168.1.45/~pi/mp3/
jcard.site=http\://localhost\:8192/jcard
minimodem.log.file=C\:\\mp3files\\TapeFiles\\tape.log| Key | Purpose |
|---|---|
audio.directory |
Where the audio files and the audiodb/tapedb/tracklist databases live |
jcard.site |
Base URL the desktop Open J-Card button opens (the tape id is appended as ?tape=<id>). Defaults to the bundled app at http\://localhost\:8192/jcard; set it to another host to use a different J-Card template |