nixamp 0.1.0
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/LICENSE +21 -0
- package/README.md +180 -0
- package/bin/nixamp.mjs +7 -0
- package/dist/audio.d.ts +60 -0
- package/dist/audio.js +206 -0
- package/dist/fft.d.ts +52 -0
- package/dist/fft.js +154 -0
- package/dist/main.d.ts +47 -0
- package/dist/main.js +307 -0
- package/dist/manage.d.ts +25 -0
- package/dist/manage.js +117 -0
- package/dist/meta.d.ts +2 -0
- package/dist/meta.js +12 -0
- package/dist/playlist.d.ts +11 -0
- package/dist/playlist.js +66 -0
- package/dist/protocol.d.ts +56 -0
- package/dist/protocol.js +53 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +10 -0
- package/dist/server.d.ts +105 -0
- package/dist/server.js +617 -0
- package/package.json +65 -0
- package/src/audio.ts +233 -0
- package/src/fft.ts +165 -0
- package/src/main.ts +311 -0
- package/src/manage.ts +134 -0
- package/src/meta.ts +12 -0
- package/src/playlist.ts +66 -0
- package/src/protocol.ts +91 -0
- package/src/serve.ts +11 -0
- package/src/server.ts +649 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/index-BGKWWaIx.css +1 -0
- package/web/dist/assets/index-Dhja5wxB.js +1 -0
- package/web/dist/icons/icon-192-maskable.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/index.html +110 -0
- package/web/dist/install.sh +299 -0
- package/web/dist/manifest.webmanifest +50 -0
- package/web/dist/sw.js +91 -0
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire format between a running nixamp and any remote that drives it.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of node imports on purpose: the browser client type-checks against
|
|
5
|
+
* this same file, so a change to the protocol breaks both sides at once rather
|
|
6
|
+
* than one of them at runtime.
|
|
7
|
+
*/
|
|
8
|
+
export const COMMAND_TYPES = ["play", "toggle", "stop", "next", "prev", "select"];
|
|
9
|
+
/**
|
|
10
|
+
* Commands arrive as untrusted JSON from a browser on the LAN, so nothing is
|
|
11
|
+
* assumed: an unknown type or a non-integer index is a null, not a throw.
|
|
12
|
+
*/
|
|
13
|
+
export function parseCommand(input) {
|
|
14
|
+
if (typeof input !== "object" || input === null)
|
|
15
|
+
return null;
|
|
16
|
+
const record = input;
|
|
17
|
+
const type = record.type;
|
|
18
|
+
if (typeof type !== "string")
|
|
19
|
+
return null;
|
|
20
|
+
const index = record.index;
|
|
21
|
+
const validIndex = typeof index === "number" && Number.isInteger(index) && index >= 0;
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "toggle":
|
|
24
|
+
case "stop":
|
|
25
|
+
case "next":
|
|
26
|
+
case "prev":
|
|
27
|
+
return { type };
|
|
28
|
+
case "play":
|
|
29
|
+
return validIndex ? { type: "play", index: index } : { type: "play" };
|
|
30
|
+
case "select":
|
|
31
|
+
return validIndex ? { type: "select", index: index } : null;
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The name a remote shows for a track. */
|
|
37
|
+
export function remoteName(track) {
|
|
38
|
+
return track.artist ? `${track.artist} — ${track.title}` : track.title;
|
|
39
|
+
}
|
|
40
|
+
export function emptySnapshot() {
|
|
41
|
+
return {
|
|
42
|
+
revision: 0,
|
|
43
|
+
tracks: [],
|
|
44
|
+
index: 0,
|
|
45
|
+
playing: false,
|
|
46
|
+
position: 0,
|
|
47
|
+
bars: [],
|
|
48
|
+
levels: [0, 0],
|
|
49
|
+
silent: true,
|
|
50
|
+
note: "",
|
|
51
|
+
root: "",
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server on its own, with no terminal UI attached.
|
|
3
|
+
*
|
|
4
|
+
* `nixamp serve` reaches the same code through main.ts, but the desktop app
|
|
5
|
+
* spawns this file directly: it pulls in no TUI, so what it has to load is the
|
|
6
|
+
* four modules that decode and serve and nothing else.
|
|
7
|
+
*/
|
|
8
|
+
import { version } from "./meta.js";
|
|
9
|
+
import { serve } from "./server.js";
|
|
10
|
+
await serve(process.argv.slice(2), version());
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
|
+
import { type Tools, type Track } from "./audio.ts";
|
|
3
|
+
import { type Command, type RemoteTrack, type Snapshot } from "./protocol.ts";
|
|
4
|
+
export declare const SERVE_BAND_COUNT = 24;
|
|
5
|
+
export declare const DEFAULT_PORT = 4321;
|
|
6
|
+
export interface ServeOptions {
|
|
7
|
+
root: string;
|
|
8
|
+
port: number;
|
|
9
|
+
host: string;
|
|
10
|
+
/** Directory of built PWA files to serve at `/`, when there is one. */
|
|
11
|
+
web: string | null;
|
|
12
|
+
/** Stream the library's bytes to remotes. Off keeps the audio on this box. */
|
|
13
|
+
media: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Flags are parsed by hand: three of them do not justify a dependency, and the
|
|
17
|
+
* failure mode of a wrong `--port` should be a message rather than NaN.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseServeArgs(argv: string[]): ServeOptions;
|
|
20
|
+
export declare function contentType(path: string): string;
|
|
21
|
+
export interface ByteRange {
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `Range: bytes=0-` and friends. Anything malformed, unsatisfiable or
|
|
27
|
+
* multi-range is a null, which the caller answers with the whole file —
|
|
28
|
+
* the behaviour a browser expects when it cannot have the range it asked for.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseRange(header: string | undefined, size: number): ByteRange | null;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a URL path inside a directory, or null when it escapes.
|
|
33
|
+
* `..` in a request path is the oldest bug in static file serving.
|
|
34
|
+
*/
|
|
35
|
+
export declare function safeJoin(rootDir: string, urlPath: string): string | null;
|
|
36
|
+
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
37
|
+
export interface Engine {
|
|
38
|
+
snapshot(): Snapshot;
|
|
39
|
+
command(command: Command): void;
|
|
40
|
+
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
41
|
+
/** Absolute path of a track, or undefined when the index is not one. */
|
|
42
|
+
trackPath(index: number): string | undefined;
|
|
43
|
+
stop(): void;
|
|
44
|
+
}
|
|
45
|
+
export declare function toRemoteTracks(tracks: Track[]): RemoteTrack[];
|
|
46
|
+
/**
|
|
47
|
+
* The headless player: the terminal app's engine without the terminal.
|
|
48
|
+
* One ffmpeg decodes, ffplay makes the sound, and every sample is measured on
|
|
49
|
+
* its way past so a remote can draw the same spectrum the TUI would.
|
|
50
|
+
*/
|
|
51
|
+
export declare class PlayerEngine implements Engine {
|
|
52
|
+
private readonly tracks;
|
|
53
|
+
private readonly root;
|
|
54
|
+
/** Frames a second pushed to remotes. */
|
|
55
|
+
private readonly fps;
|
|
56
|
+
private readonly listeners;
|
|
57
|
+
private readonly analyser;
|
|
58
|
+
private readonly edges;
|
|
59
|
+
private readonly stream;
|
|
60
|
+
private pending;
|
|
61
|
+
private revision;
|
|
62
|
+
/** Pushes are coalesced: the analyser fires far faster than a remote can draw. */
|
|
63
|
+
private timer;
|
|
64
|
+
private dirty;
|
|
65
|
+
private state;
|
|
66
|
+
constructor(tracks: Track[], root: string, tools: Tools,
|
|
67
|
+
/** Frames a second pushed to remotes. */
|
|
68
|
+
fps?: number);
|
|
69
|
+
private readonly silent;
|
|
70
|
+
private consume;
|
|
71
|
+
snapshot(): Snapshot;
|
|
72
|
+
trackPath(index: number): string | undefined;
|
|
73
|
+
command(command: Command): void;
|
|
74
|
+
private clamp;
|
|
75
|
+
private step;
|
|
76
|
+
private start;
|
|
77
|
+
private halt;
|
|
78
|
+
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
79
|
+
private push;
|
|
80
|
+
stop(): void;
|
|
81
|
+
}
|
|
82
|
+
/** An engine with no library behind it, for the hosted PWA. */
|
|
83
|
+
export declare class EmptyEngine implements Engine {
|
|
84
|
+
private readonly note;
|
|
85
|
+
constructor(note?: string);
|
|
86
|
+
snapshot(): Snapshot;
|
|
87
|
+
command(): void;
|
|
88
|
+
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
89
|
+
trackPath(): undefined;
|
|
90
|
+
stop(): void;
|
|
91
|
+
}
|
|
92
|
+
export interface HandlerOptions {
|
|
93
|
+
web: string | null;
|
|
94
|
+
media: boolean;
|
|
95
|
+
version: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The whole HTTP surface, as a plain function of a request — so a test can
|
|
99
|
+
* drive it with a real socket and no ffmpeg in sight.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createHandler(engine: Engine, options: HandlerOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
102
|
+
export declare function createServer(engine: Engine, options: HandlerOptions): Server;
|
|
103
|
+
/** Where a remote on another device should point its browser. */
|
|
104
|
+
export declare function addressesFor(host: string, port: number): string[];
|
|
105
|
+
export declare function serve(argv: string[], version?: string): Promise<void>;
|