tserato 0.1.0 → 0.1.10

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.
@@ -0,0 +1,206 @@
1
+ import { HotCueType } from './hotCueType';
2
+ import { SeratoColor } from './seratoColor';
3
+ import { Buffer } from 'buffer';
4
+
5
+ function writeNullTerminatedString(str: string): Uint8Array {
6
+ const encoder = new TextEncoder();
7
+ const strBytes = encoder.encode(str);
8
+ const result = new Uint8Array(strBytes.length + 1); // +1 for null terminator
9
+ result.set(strBytes, 0);
10
+ result[strBytes.length] = 0;
11
+ return result;
12
+ }
13
+
14
+ function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
15
+ let totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
16
+ let result = new Uint8Array(totalLength);
17
+ let offset = 0;
18
+ arrays.forEach(arr => {
19
+ result.set(arr, offset);
20
+ offset += arr.length;
21
+ });
22
+ return result;
23
+ }
24
+
25
+ function encodeElement(name: string, data: Uint8Array): Uint8Array {
26
+ const nameBytes = writeNullTerminatedString(name);
27
+ const lengthBytes = new Uint8Array(4);
28
+ new DataView(lengthBytes.buffer).setUint32(0, data.length, false); // big endian
29
+ return concatUint8Arrays([nameBytes, lengthBytes, data]);
30
+ }
31
+
32
+ export interface HotCueData {
33
+ name: string;
34
+ type: HotCueType;
35
+ start: number;
36
+ index: number;
37
+ end?: number | null;
38
+ isLocked?: boolean;
39
+ color?: SeratoColor;
40
+ }
41
+
42
+ export class HotCue implements HotCueData {
43
+ name: string;
44
+ type: HotCueType;
45
+ start: number;
46
+ index: number;
47
+ end?: number | null;
48
+ isLocked?: boolean;
49
+ color: SeratoColor;
50
+
51
+ constructor(data: HotCueData) {
52
+ this.name = data.name;
53
+ this.type = data.type;
54
+ this.start = data.start;
55
+ this.index = data.index;
56
+ this.end = data.end ?? null;
57
+ this.isLocked = data.isLocked ?? false;
58
+ this.color = data.color ?? SeratoColor.RED;
59
+ }
60
+
61
+ toString(): string {
62
+ const startStr = `${this.start}ms`;
63
+ const endStr = this.end != null ? ` | End: ${this.end}ms` : '';
64
+ return `Start: ${startStr}${endStr} | Index: ${String(this.index).padStart(2)} | Name: ${this.name} | Color: ${this.color}`;
65
+ }
66
+
67
+ toV2Bytes(): Buffer {
68
+ if (this.type === HotCueType.CUE) return this.cueToV2Bytes();
69
+ if (this.type === HotCueType.LOOP) return this.loopToV2Bytes();
70
+ throw new Error(`unsupported hotcue type ${this.type}`);
71
+ }
72
+
73
+ private loopToV2Bytes(): Buffer {
74
+ let nameBytes = writeNullTerminatedString(this.name);
75
+ let buf = new Uint8Array(0x14 + nameBytes.length);
76
+ let dv = new DataView(buf.buffer);
77
+
78
+ buf[0x0] = 0x00; // flags? (unused in parse)
79
+ buf[0x1] = this.index;
80
+ dv.setUint32(0x02, this.start, false);
81
+ dv.setUint32(0x06, this.end!, false);
82
+ buf[0x0e] = 0;
83
+ buf[0x0f] = 0;
84
+ buf[0x10] = 255;
85
+ buf[0x11] = 255;
86
+ buf[0x13] = 1;
87
+ buf.set(nameBytes, 0x14);
88
+
89
+ return Buffer.from(encodeElement("LOOP", buf));
90
+ }
91
+
92
+ private cueToV2Bytes(): Buffer {
93
+ const parts: Buffer[] = [];
94
+
95
+ // >B (unsigned char, big-endian)
96
+ parts.push(Buffer.from([0]));
97
+
98
+ // >B self.index
99
+ parts.push(Buffer.from([this.index]));
100
+
101
+ // >I self.start (4-byte big-endian unsigned int)
102
+ const startBuf = Buffer.alloc(4);
103
+ startBuf.writeUInt32BE(this.start, 0);
104
+ parts.push(startBuf);
105
+
106
+ // >B 0
107
+ parts.push(Buffer.from([0]));
108
+
109
+ // >3s color (3 raw bytes from hex string)
110
+ const colorBuf = Buffer.from(this.color, "hex");
111
+ if (colorBuf.length !== 3) {
112
+ throw new Error(`Color must be 3 bytes (e.g. "ff0000"), got ${this.color}`);
113
+ }
114
+ parts.push(colorBuf);
115
+
116
+ // >B 0
117
+ parts.push(Buffer.from([0]));
118
+
119
+ parts.push(Buffer.from([1]));
120
+
121
+ parts.push(Buffer.from(this.name, "utf8"));
122
+
123
+ // >B 0 (null terminator after name)
124
+ parts.push(Buffer.from([0]));
125
+
126
+ const data = Buffer.concat(parts);
127
+
128
+ const lenBuf = Buffer.alloc(4);
129
+ lenBuf.writeUInt32BE(data.length, 0);
130
+
131
+ const payload = Buffer.concat([
132
+ Buffer.from("CUE", "ascii"),
133
+ Buffer.from([0]),
134
+ lenBuf,
135
+ data,
136
+ ]);
137
+
138
+ return payload;
139
+ }
140
+
141
+ static fromBytes(data: Buffer, hotcueType: HotCueType): HotCue {
142
+ let offset = 1; // skip first null
143
+ if (hotcueType === HotCueType.CUE) {
144
+ const index = data.readUInt8(offset);
145
+ offset += 1;
146
+ const start = data.readUInt32BE(offset);
147
+ offset += 4;
148
+ offset += 1; // skip null / position end
149
+ offset += 0; // skip placeholder
150
+ const colorBytes = data.slice(offset, offset + 3);
151
+ offset += 3;
152
+ offset += 1; // skip null
153
+ const locked = data.readUInt8(offset) !== 0;
154
+ offset += 1;
155
+ const nameBuf = data.slice(offset).subarray(0, data.slice(offset).indexOf(0));
156
+ const name = nameBuf.toString("utf-8");
157
+
158
+ const colorHex = colorBytes.toString("hex").toUpperCase();
159
+ const color = (Object.values(SeratoColor).includes(colorHex as SeratoColor)
160
+ ? (colorHex as SeratoColor)
161
+ : SeratoColor.RED);
162
+
163
+ return new HotCue({
164
+ name,
165
+ type: HotCueType.CUE,
166
+ index,
167
+ start,
168
+ color,
169
+ isLocked: locked,
170
+ });
171
+ }
172
+ else if (hotcueType === HotCueType.LOOP) {
173
+ const index = data.readUInt8(offset); offset += 1;
174
+ const start = data.readUInt32BE(offset); offset += 4;
175
+ const end = data.readUInt32BE(offset); offset += 4;
176
+
177
+ // Skip to offset 0x0E
178
+ offset = 0x0E;
179
+
180
+ // skip color placeholder bytes (0x0E–0x11)
181
+ offset += 2; // 0x0E, 0x0F
182
+ offset += 2; // 0x10, 0x11
183
+ offset += 1; // 0x12 (padding)
184
+
185
+ const isLocked = Boolean(data.readUInt8(offset)); offset += 1;
186
+
187
+ // read null-terminated name
188
+ const nameEnd = data.indexOf(0x00, offset);
189
+ const name = data.toString("utf8", offset, nameEnd === -1 ? data.length : nameEnd);
190
+ return new HotCue({
191
+ name,
192
+ type: HotCueType.CUE,
193
+ index,
194
+ start,
195
+ end: end,
196
+ color: SeratoColor.RED,
197
+ isLocked: isLocked,
198
+ });
199
+ }
200
+
201
+ else {
202
+ throw new Error(`Unknown hotcue type: ${hotcueType}`);
203
+ }
204
+
205
+ }
206
+ }
@@ -0,0 +1,6 @@
1
+ export enum HotCueType {
2
+ CUE = 0,
3
+ LOOP = 1,
4
+ INVALID = 99,
5
+ PASSTHROUGH = 100
6
+ }
@@ -0,0 +1,20 @@
1
+ export enum SeratoColor {
2
+ RED = 'CC0000',
3
+ ORANGE = 'CC4400',
4
+ AMBER = 'CC8800',
5
+ YELLOW = 'CCCC00',
6
+ LIME_GREEN = '88CC00',
7
+ GREEN_YELLOW = '44CC00',
8
+ GREEN = '00CC00',
9
+ MINT_GREEN = '00CC44',
10
+ TEAL_GREEN = '00CC88',
11
+ TEAL = '00CCCC',
12
+ SKY_BLUE = '0088CC',
13
+ BLUE = '0044CC',
14
+ DARK_BLUE = '0000CC',
15
+ PURPLE = '4400CC',
16
+ VIOLET = '8800CC',
17
+ MAGENTA = 'CC00CC',
18
+ PINK = 'CC0088',
19
+ RED_PINK = 'CC0044'
20
+ }
@@ -0,0 +1,4 @@
1
+ export interface Tempo {
2
+ position?: number | null;
3
+ bpm?: number | null;
4
+ }
@@ -0,0 +1,73 @@
1
+ import * as path from 'path';
2
+ import { HotCue } from './hotCue';
3
+ import { HotCueType } from './hotCueType';
4
+ import { Tempo } from './tempo';
5
+
6
+ export class Track {
7
+ path: string;
8
+ trackId: string;
9
+ averageBpm: number;
10
+ dateAdded: string;
11
+ playCount: string;
12
+ tonality: string;
13
+ totalTime: number;
14
+
15
+ beatgrid: Tempo[];
16
+ hotCues: HotCue[];
17
+ cueLoops: HotCue[];
18
+
19
+ constructor(
20
+ trackPath: string,
21
+ params: {
22
+ trackId?: string;
23
+ averageBpm?: number;
24
+ dateAdded?: string;
25
+ playCount?: string;
26
+ tonality?: string;
27
+ totalTime?: number;
28
+ beatgrid?: Tempo[];
29
+ hotCues?: HotCue[];
30
+ cueLoops?: HotCue[];
31
+ } = {}
32
+ ) {
33
+ this.path = trackPath;
34
+ this.trackId = params.trackId ?? "";
35
+ this.averageBpm = params.averageBpm ?? 0.0;
36
+ this.dateAdded = params.dateAdded ?? "";
37
+ this.playCount = params.playCount ?? "";
38
+ this.tonality = params.tonality ?? "";
39
+ this.totalTime = params.totalTime ?? 0.0;
40
+
41
+ this.beatgrid = params.beatgrid ?? [];
42
+ this.hotCues = params.hotCues ?? [];
43
+ this.cueLoops = params.cueLoops ?? [];
44
+ }
45
+
46
+ static fromPath(trackPath: string, userRoot?: string): Track {
47
+ const resolved =
48
+ userRoot != null
49
+ ? path.resolve(userRoot, trackPath)
50
+ : path.resolve(trackPath);
51
+ return new Track(resolved);
52
+ }
53
+
54
+ addBeatgridMarker(tempo: Tempo): void {
55
+ this.beatgrid.push(tempo);
56
+ }
57
+
58
+ addHotCue(hotCue: HotCue): void {
59
+ if (this.hotCues.length >= 8) {
60
+ throw new Error("cannot have more than 8 hot cues on a track");
61
+ }
62
+ if (this.cueLoops.length >= 4) {
63
+ throw new Error("cannot have more than 4 loops on a track");
64
+ }
65
+
66
+ const atIndex = hotCue.index;
67
+ if (hotCue.type === HotCueType.LOOP) {
68
+ this.cueLoops.splice(atIndex, 0, hotCue);
69
+ } else {
70
+ this.hotCues.splice(atIndex, 0, hotCue);
71
+ }
72
+ }
73
+ }
package/src/util.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { Buffer } from 'buffer';
2
+
3
+ const INVALID_CHARACTERS_REGEX = /[^A-Za-z0-9_ ]/i;
4
+
5
+ export function splitString(input: Buffer, after: number = 72, delimiter: Buffer = Buffer.from('\n')): Buffer {
6
+ const pieces: Buffer[] = [];
7
+ let buf = Buffer.from(input);
8
+ while (buf.length > 0) {
9
+ pieces.push(buf.slice(0, after));
10
+ buf = buf.slice(after);
11
+ }
12
+ return Buffer.concat(pieces.reduce<Buffer[]>((acc, p, i) => {
13
+ if (i) acc.push(delimiter);
14
+ acc.push(p);
15
+ return acc;
16
+ }, []));
17
+ }
18
+
19
+
20
+ export function seratoDecode(buffer: Buffer): string {
21
+ let result = "";
22
+
23
+ for (let i = 0; i + 1 < buffer.length; i += 2) {
24
+ // Take 2 bytes
25
+ const chunk = buffer.slice(i, i + 2);
26
+
27
+ // Reverse bytes (Python: chunk[::-1])
28
+ const reversed = Buffer.from([chunk[1], chunk[0]]);
29
+
30
+ // Decode as UTF-16LE (Node uses LE explicitly)
31
+ result += reversed.toString("utf16le");
32
+ }
33
+
34
+ return result;
35
+ }
36
+
37
+
38
+ export function concatBytes(arrays: Uint8Array[]): Uint8Array {
39
+ const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
40
+ const result = new Uint8Array(totalLength);
41
+ let offset = 0;
42
+ for (const a of arrays) {
43
+ result.set(a, offset);
44
+ offset += a.length;
45
+ }
46
+ return result;
47
+ }
48
+
49
+ export function latin1Encode(str: string): Uint8Array {
50
+ return Uint8Array.from(Buffer.from(str, "latin1"));
51
+ }
52
+
53
+ export function intToBytes(value: number, length: number): Uint8Array {
54
+ const bytes = new Uint8Array(length);
55
+ for (let i = 0; i < length; i++) {
56
+ // big-endian
57
+ bytes[length - 1 - i] = (value >> (8 * i)) & 0xff;
58
+ }
59
+ return bytes;
60
+ }
61
+
62
+ export function seratoEncode(s: string): Uint8Array {
63
+ const result: number[] = [];
64
+
65
+ for (const c of s) {
66
+ const codePoint = c.charCodeAt(0); // UTF-16 code unit
67
+ // Big-endian: high byte first, then low byte
68
+ result.push((codePoint >> 8) & 0xff);
69
+ result.push(codePoint & 0xff);
70
+ }
71
+
72
+ return Uint8Array.from(result);
73
+ }
74
+
75
+ export function sanitizeFilename(filename: string): string {
76
+ return filename.replace(INVALID_CHARACTERS_REGEX, '-');
77
+ }
78
+
79
+ export class DuplicateTrackError extends Error {}
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "declarationMap": true,
5
+ "emitDeclarationOnly": false,
6
+ "target": "ES2020",
7
+ "module": "CommonJS",
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": [
15
+ "src/**/*"
16
+ ],
17
+ "exclude": ["node_modules", "tests", "dist"]
18
+ }