node-rackrec
v0.1.0
Published
Node.js client for rack-mount audio recorders. Supports Denon DN-700R / DN-900R and Tascam SS-CDR250N / SS-R250N over telnet (and RS-232C-compatible serial framing).
Maintainers
Readme
node-rackrec
Node.js client library for rack-mount network audio recorders. Talks to the units over their telnet (and RS-232C-compatible) control protocols, exposing a typed TypeScript API for monitoring stats, controlling transport, and reacting to state-change events.
Supported devices
| Vendor | Models | Verified | Connection | |---|---|---|---| | Denon Professional | DN-700R, DN-900R | ✅ tested live | Telnet over TCP/23 | | Tascam | SS-R250N, SS-CDR250N | ✅ tested live (SS-R250N FW 1.40 and FW 2.12) | Telnet over TCP/23 (login required) |
Per-vendor protocol details, every typed method, and known firmware quirks are documented in:
Why two clients
The two vendors speak completely different control protocols:
- Denon uses an
@0-prefixed ASCII frame with\rterminators, a single ACK byte (0x06) before status payloads, and per-byte command codes. - Tascam uses a positional ASCII frame (machine ID + 2-byte command + data, CR/LF terminated), a fixed login banner over telnet, and category/sub-command vendor extensions.
So the library exposes one class per vendor — DenonRecorder and
TascamRecorder — with common patterns (Promise-based queries, EventEmitter
for auto-status pushes) but vendor-specific methods.
Install
pnpm add node-rackrec
# or
npm install node-rackrecRequires Node.js ≥ 18. Zero runtime dependencies (uses only node:net and
node:events).
Quick start — Denon
import { DenonRecorder } from 'node-rackrec';
const r = new DenonRecorder({ host: '192.168.1.50' });
await r.connect();
console.log('machine:', await r.getMachineName());
console.log('status :', await r.getStatus()); // 'recording', 'stopped', etc.
console.log('format :', await r.getRecordingFormat()); // { type: 'mp3', bitrateKbps: 128 }
console.log('rec time remaining:', await r.getRemainingTime());
const free = await r.estimateFreeBytes();
console.log(`~${(free.bytes! / 1024**3).toFixed(2)} GB free`);
r.on('deviceState', (s) => console.log('state changed:', s));
await r.disconnect();If the unit has IP Control Auth enabled, pass a password and the client will
log in automatically.
new DenonRecorder({ host: '...', password: 'admin1234' });Quick start — Tascam
import { TascamRecorder } from 'node-rackrec';
const r = new TascamRecorder({
host: '192.168.1.60',
model: 'SS-R250N', // selects the factory default password ("SS-R250N")
});
await r.connect();
console.log('mecha :', await r.getMechaStatus()); // { status: 'recording' | 'stop' | ... }
console.log('media :', await r.getMediaStatus()); // { presence: 'mediaLoaded', ... }
console.log('total :', await r.getTotalTrackTime()); // { totalTracks, minutes, seconds, frames }
// Free-space estimate. Tascam doesn't expose recording format or card capacity,
// so you must supply both.
const free = await r.estimateFreeBytes({
format: { type: 'mp3', bitrateKbps: 128, channels: 'mono' },
capacityGb: 32,
});
console.log(`~${(free.bytes! / 1024**3).toFixed(2)} GB free`);
await r.disconnect();If the customer changed the password from the factory default, pass it explicitly:
new TascamRecorder({ host: '...', password: 'mySecret' });Free-space estimation
The two vendors' protocols both have firmware-version-dependent gaps in their storage commands, so the library always returns a typed estimate with the strategy used to derive it.
Denon (DN-700R / DN-900R)
Uses @0?RT (remaining record time, hh h mm m ss s) × the active recording
bitrate. Bitrate is queried from the unit (@0?AF / @0?FS / @0?CH). For
PCM, it's looked up in the manual's published bitrate chart; for MP3 it's
embedded directly in the AF response.
const e = await denon.estimateFreeBytes();
// { remainingSeconds, bitrateKbps, bytes }The official @0?FE (Media Free Space) and @0?SF (Media Size) commands are
exposed as getRawFreeSpace() / getRawMediaSize() for completeness, but
both diverge from the spec on tested firmware — see DENON.md for details.
Tascam (SS-R250N / SS-CDR250N)
Two strategies, picked automatically:
- Firmware ≥ 2.10:
7F1010CURRENT MEDIA RECORDABLE TIME × your suppliedbitrateKbps(orformat). - Older firmware:
(capacityGb × 1024³) − (used_seconds × bitrate), whereused_secondscomes from5DTOTAL TRACK TIME andcapacityGbis supplied by the caller.
const e = await tascam.estimateFreeBytes({
format: { type: 'mp3', bitrateKbps: 128, channels: 'mono' },
capacityGb: 32, // only used by the firmware-1.x fallback
});
// { source: 'recordableTime' | 'capacityMinusUsed', bytes, ... }Why the format must be supplied: the Tascam protocol does not expose the active
recording format (no REC FORMAT SENSE command). You either know it from
configuration, or you can read it from a recently recorded file's MP3/WAV
header over the unit's FTP server.
Examples
See EXAMPLES.md for runnable code samples covering:
- Reading stats from a Denon recorder
- Logging in to a Tascam recorder and reading stats
- Free-space estimation on both vendors (and both Tascam firmware paths)
- Subscribing to auto-status push events
- Polling multiple recorders concurrently
- Sending raw protocol commands the typed API doesn't cover
Errors
All thrown errors extend DenonError, despite the name (legacy from when this
was Denon-only). Common subclasses:
| Class | Where it's thrown |
|---|---|
| TimeoutError | Request didn't get a response within the configured timeout |
| NackError | Denon returned NACK (0x15) — unknown/invalid command |
| TascamIllegalStatusError | Tascam returned ILLEGAL STATUS (F2) — same idea |
| TascamAuthError | Tascam telnet login failed (bad password / closed before completion) |
| NotConnectedError | Method called before connect() or after socket close |
| ParseError | Response payload didn't match the expected format |
Every error carries a code field and (for transport errors) the sent
command for debugging.
Caveats
- Concurrency: Denon allows only one network client at a time; Tascam allows three. The library serializes outgoing commands so multiple awaited queries on a single client are safe.
- Recording session interference: switching media slots (
selectMedia) on an actively-recording unit will fail or disrupt the recording. Stick to status reads on production units. - Firmware divergences are documented in each vendor's
.mdfile. Notable examples: Denon FW 1.40 returns no payload for@0?FE; Tascam FW < 2.10 returnsILLEGALfor7F1010. - Time-vs-bytes: both vendors expose remaining record time, not bytes. Conversion to bytes requires knowing the bitrate, which the library derives where the protocol exposes it (Denon) and asks the caller for where it doesn't (Tascam).
Development
pnpm install
pnpm build # tsc → dist/
pnpm watch # tsc --watchSource layout:
src/
├── index.ts re-exports both vendor namespaces
├── denon/
│ ├── transport.ts ACK-framed TCP transport
│ ├── recorder.ts DenonRecorder class
│ ├── codec.ts payload encode/decode helpers
│ ├── bitrate.ts PCM bitrate lookup + free-bytes estimator
│ ├── constants.ts enums and code mappings
│ └── index.ts
├── tascam/
│ ├── transport.ts login-banner + line-framed TCP transport
│ ├── recorder.ts TascamRecorder class
│ ├── constants.ts command codes + format types
│ └── index.ts
└── shared/
└── errors.ts common error classesSee EXAMPLES.md for usage samples.
License
MIT
