tserato 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/README.md +113 -0
- package/dist/builder.d.ts +15 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +187 -0
- package/dist/encoders/baseEncoder.d.ts +8 -0
- package/dist/encoders/baseEncoder.d.ts.map +1 -0
- package/dist/encoders/baseEncoder.js +6 -0
- package/dist/encoders/serato_tags.d.ts +5 -0
- package/dist/encoders/serato_tags.d.ts.map +1 -0
- package/dist/encoders/serato_tags.js +7 -0
- package/dist/encoders/utils.d.ts +3 -0
- package/dist/encoders/utils.d.ts.map +1 -0
- package/dist/encoders/utils.js +24 -0
- package/dist/encoders/v2/v2Mp3Encoder.d.ts +21 -0
- package/dist/encoders/v2/v2Mp3Encoder.d.ts.map +1 -0
- package/dist/encoders/v2/v2Mp3Encoder.js +197 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/model/crate.d.ts +15 -0
- package/dist/model/crate.d.ts.map +1 -0
- package/dist/model/crate.js +76 -0
- package/dist/model/hotCue.d.ts +28 -0
- package/dist/model/hotCue.d.ts.map +1 -0
- package/dist/model/hotCue.js +166 -0
- package/dist/model/hotCueType.d.ts +7 -0
- package/dist/model/hotCueType.d.ts.map +1 -0
- package/dist/model/hotCueType.js +10 -0
- package/dist/model/seratoColor.d.ts +21 -0
- package/dist/model/seratoColor.d.ts.map +1 -0
- package/dist/model/seratoColor.js +24 -0
- package/dist/model/tempo.d.ts +5 -0
- package/dist/model/tempo.d.ts.map +1 -0
- package/dist/model/tempo.js +2 -0
- package/dist/model/track.d.ts +29 -0
- package/dist/model/track.d.ts.map +1 -0
- package/dist/model/track.js +75 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +93 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# pyserato-ts
|
|
2
|
+
|
|
3
|
+
TypeScript port of pyserato (https://github.com/laker-93/pyserato/).
|
|
4
|
+
|
|
5
|
+
## Write Crates
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
// example/testEncoder.ts
|
|
9
|
+
import { Builder, Crate, Track, V2Mp3Encoder, HotCue, HotCueType } from "tserato"
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
// create encoder + builder
|
|
13
|
+
const mp3Encoder = new V2Mp3Encoder();
|
|
14
|
+
const builder = new Builder(mp3Encoder);
|
|
15
|
+
|
|
16
|
+
// create crate
|
|
17
|
+
const crate = new Crate("foojs");
|
|
18
|
+
|
|
19
|
+
// add track
|
|
20
|
+
const track = Track.fromPath("/Users/lukepurnell/test music/Russian Circles - Gnosis/Russian Circles - Gnosis - 06 Betrayal.mp3");
|
|
21
|
+
crate.addTrack(track);
|
|
22
|
+
|
|
23
|
+
// add cues
|
|
24
|
+
track.addHotCue(
|
|
25
|
+
new HotCue({
|
|
26
|
+
name: "mycue1",
|
|
27
|
+
type: HotCueType.CUE,
|
|
28
|
+
start: 500,
|
|
29
|
+
index: 1,
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
track.addHotCue(
|
|
33
|
+
new HotCue({
|
|
34
|
+
name: "mycue2",
|
|
35
|
+
type: HotCueType.CUE,
|
|
36
|
+
start: 1000,
|
|
37
|
+
index: 2,
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
track.addHotCue(
|
|
42
|
+
new HotCue({
|
|
43
|
+
name: "myloop1",
|
|
44
|
+
type: HotCueType.LOOP,
|
|
45
|
+
start: 2000,
|
|
46
|
+
end: 3000,
|
|
47
|
+
index: 3,
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// save crate (writes .crate file + tags)
|
|
52
|
+
await builder.save(crate, undefined, true);
|
|
53
|
+
|
|
54
|
+
console.log("Crate saved successfully!");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main().catch(console.error);
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Write Cues
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
import { Builder, Crate, Track, V2Mp3Encoder, HotCue, HotCueType } from "tserato"
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
// create encoder + builder
|
|
68
|
+
const mp3Encoder = new V2Mp3Encoder();
|
|
69
|
+
const builder = new Builder(mp3Encoder);
|
|
70
|
+
|
|
71
|
+
// create crate
|
|
72
|
+
const crate = new Crate("foojs");
|
|
73
|
+
|
|
74
|
+
// add track
|
|
75
|
+
const track = Track.fromPath("/Users/lukepurnell/test music/Russian Circles - Gnosis/Russian Circles - Gnosis - 06 Betrayal.mp3");
|
|
76
|
+
crate.addTrack(track);
|
|
77
|
+
|
|
78
|
+
// add cues
|
|
79
|
+
track.addHotCue(
|
|
80
|
+
new HotCue({
|
|
81
|
+
name: "mycue1",
|
|
82
|
+
type: HotCueType.CUE,
|
|
83
|
+
start: 500,
|
|
84
|
+
index: 1,
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
track.addHotCue(
|
|
88
|
+
new HotCue({
|
|
89
|
+
name: "mycue2",
|
|
90
|
+
type: HotCueType.CUE,
|
|
91
|
+
start: 1000,
|
|
92
|
+
index: 2,
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
track.addHotCue(
|
|
97
|
+
new HotCue({
|
|
98
|
+
name: "myloop1",
|
|
99
|
+
type: HotCueType.LOOP,
|
|
100
|
+
start: 2000,
|
|
101
|
+
end: 3000,
|
|
102
|
+
index: 3,
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// save crate (writes .crate file + tags)
|
|
107
|
+
await builder.save(crate, undefined, true);
|
|
108
|
+
|
|
109
|
+
console.log("Crate saved successfully!");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch(console.error);
|
|
113
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseEncoder } from './encoders/baseEncoder';
|
|
2
|
+
import { Crate } from './model/crate';
|
|
3
|
+
export declare class Builder {
|
|
4
|
+
private _encoder?;
|
|
5
|
+
constructor(encoder?: BaseEncoder);
|
|
6
|
+
private static _resolvePath;
|
|
7
|
+
private static _parseCrateNames;
|
|
8
|
+
private _buildCrateFilepath;
|
|
9
|
+
parseCratesFromRootPath(subcratePath: string): Record<string, Crate>;
|
|
10
|
+
private _buildCratesFromFilepath;
|
|
11
|
+
private _parseCrateTracks;
|
|
12
|
+
private _construct;
|
|
13
|
+
save(root: Crate, savePath?: string, overwrite?: boolean): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAMtC,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,CAAc;gBACnB,OAAO,CAAC,EAAE,WAAW;IAEjC,OAAO,CAAC,MAAM,CAAE,YAAY;IAc5B,OAAO,CAAC,MAAM,CAAE,gBAAgB;IAKhC,OAAO,CAAE,mBAAmB;IAQ5B,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAiBpE,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAE,iBAAiB;IAkB1B,OAAO,CAAC,UAAU;IA2DlB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAE,MAA8B,EAAE,SAAS,UAAQ;CAO9E"}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Builder = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const crate_1 = require("./model/crate");
|
|
40
|
+
const track_1 = require("./model/track");
|
|
41
|
+
const util_1 = require("./util");
|
|
42
|
+
const DEFAULT_SERATO_FOLDER = path.join(process.env.HOME || process.cwd(), 'Music', '_Serato_');
|
|
43
|
+
class Builder {
|
|
44
|
+
constructor(encoder) { this._encoder = encoder; }
|
|
45
|
+
static *_resolvePath(root) {
|
|
46
|
+
let pathStr = '';
|
|
47
|
+
const stack = [[root, pathStr]];
|
|
48
|
+
while (stack.length) {
|
|
49
|
+
const [crate, p] = stack.pop();
|
|
50
|
+
let newPath = p + `${crate.name}%%`;
|
|
51
|
+
const children = crate.children;
|
|
52
|
+
if (children && children.length) {
|
|
53
|
+
for (const child of children)
|
|
54
|
+
stack.push([child, newPath]);
|
|
55
|
+
}
|
|
56
|
+
yield [crate, newPath.replace(/%%$/, '') + '.crate'];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
static *_parseCrateNames(filepath) {
|
|
60
|
+
const parts = path.basename(filepath).split('%%');
|
|
61
|
+
for (const p of parts)
|
|
62
|
+
yield p.replace('.crate', '');
|
|
63
|
+
}
|
|
64
|
+
*_buildCrateFilepath(crate, seratoFolder) {
|
|
65
|
+
const subcrateFolder = path.join(seratoFolder, 'SubCrates');
|
|
66
|
+
if (!fs.existsSync(subcrateFolder))
|
|
67
|
+
fs.mkdirSync(subcrateFolder, { recursive: true });
|
|
68
|
+
for (const [c, p] of Builder._resolvePath(crate)) {
|
|
69
|
+
yield [c, path.join(subcrateFolder, p)];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
parseCratesFromRootPath(subcratePath) {
|
|
73
|
+
const result = {};
|
|
74
|
+
const entries = fs.readdirSync(subcratePath);
|
|
75
|
+
for (const name of entries) {
|
|
76
|
+
if (!name.endsWith('crate'))
|
|
77
|
+
continue;
|
|
78
|
+
const full = path.join(subcratePath, name);
|
|
79
|
+
const crate = this._buildCratesFromFilepath(full);
|
|
80
|
+
if (result[crate.name]) {
|
|
81
|
+
const merged_crate = crate.plus(result[crate.name]);
|
|
82
|
+
result[crate.name] = merged_crate;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
result[crate.name] = crate;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
_buildCratesFromFilepath(filepath) {
|
|
91
|
+
const crateNames = Array.from(Builder._parseCrateNames(filepath));
|
|
92
|
+
let childCrate = null;
|
|
93
|
+
let crate = null;
|
|
94
|
+
for (const crateName of crateNames.reverse()) {
|
|
95
|
+
if (childCrate == null) {
|
|
96
|
+
crate = new crate_1.Crate(crateName);
|
|
97
|
+
for (const filePath of this._parseCrateTracks(filepath)) {
|
|
98
|
+
const t = track_1.Track.fromPath(filePath);
|
|
99
|
+
crate.addTrack(t);
|
|
100
|
+
}
|
|
101
|
+
childCrate = crate;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
crate = new crate_1.Crate(crateName, [childCrate]);
|
|
105
|
+
childCrate = crate;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!crate)
|
|
109
|
+
throw new Error(`no crates parsed from ${filepath}`);
|
|
110
|
+
return crate;
|
|
111
|
+
}
|
|
112
|
+
*_parseCrateTracks(filepath) {
|
|
113
|
+
let buffer = fs.readFileSync(filepath);
|
|
114
|
+
buffer = Buffer.from(buffer);
|
|
115
|
+
while (buffer.length > 0) {
|
|
116
|
+
const otrkIdx = buffer.indexOf(Buffer.from('otrk'));
|
|
117
|
+
if (otrkIdx < 0)
|
|
118
|
+
break;
|
|
119
|
+
const ptrkIdx = buffer.indexOf(Buffer.from('ptrk'), otrkIdx);
|
|
120
|
+
if (ptrkIdx < 0)
|
|
121
|
+
break;
|
|
122
|
+
const ptrkSection = buffer.slice(ptrkIdx);
|
|
123
|
+
const trackNameLength = ptrkSection.readUInt32BE(4);
|
|
124
|
+
const trackNameEncoded = ptrkSection.slice(8, 8 + trackNameLength);
|
|
125
|
+
let filePath = (0, util_1.seratoDecode)(trackNameEncoded);
|
|
126
|
+
if (!filePath.startsWith('/'))
|
|
127
|
+
filePath = '/' + filePath;
|
|
128
|
+
yield filePath;
|
|
129
|
+
buffer = ptrkSection.slice(8 + trackNameLength);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
_construct(crate) {
|
|
133
|
+
// ----- HEADER -----
|
|
134
|
+
let header = (0, util_1.concatBytes)([
|
|
135
|
+
(0, util_1.latin1Encode)("vrsn"),
|
|
136
|
+
(0, util_1.intToBytes)(0, 1),
|
|
137
|
+
(0, util_1.intToBytes)(0, 1),
|
|
138
|
+
(0, util_1.seratoEncode)("81.0"),
|
|
139
|
+
(0, util_1.seratoEncode)("/Serato ScratchLive Crate"),
|
|
140
|
+
]);
|
|
141
|
+
// ----- DEFAULT COLUMNS -----
|
|
142
|
+
const DEFAULT_COLUMNS = ["track", "artist", "album", "length"];
|
|
143
|
+
const parts = [];
|
|
144
|
+
for (const column of DEFAULT_COLUMNS) {
|
|
145
|
+
parts.push((0, util_1.latin1Encode)("ovct"));
|
|
146
|
+
parts.push((0, util_1.intToBytes)(column.length * 2 + 18, 4));
|
|
147
|
+
parts.push((0, util_1.latin1Encode)("tvcn"));
|
|
148
|
+
parts.push((0, util_1.intToBytes)(column.length * 2, 4));
|
|
149
|
+
parts.push((0, util_1.seratoEncode)(column));
|
|
150
|
+
parts.push((0, util_1.latin1Encode)("tvcw"));
|
|
151
|
+
parts.push((0, util_1.intToBytes)(2, 4));
|
|
152
|
+
parts.push((0, util_1.latin1Encode)("0"));
|
|
153
|
+
parts.push((0, util_1.latin1Encode)("0"));
|
|
154
|
+
}
|
|
155
|
+
const columnSection = (0, util_1.concatBytes)(parts);
|
|
156
|
+
// ----- PLAYLIST SECTION -----
|
|
157
|
+
const playlistParts = [];
|
|
158
|
+
if (crate.tracks) {
|
|
159
|
+
for (const track of crate.tracks) {
|
|
160
|
+
if (this._encoder) {
|
|
161
|
+
this._encoder.write(track);
|
|
162
|
+
}
|
|
163
|
+
const absoluteTrackPath = path.resolve(track.path);
|
|
164
|
+
const otrkSize = (0, util_1.intToBytes)(absoluteTrackPath.length * 2 + 8, 4);
|
|
165
|
+
const ptrkSize = (0, util_1.intToBytes)(absoluteTrackPath.length * 2, 4);
|
|
166
|
+
playlistParts.push((0, util_1.latin1Encode)("otrk"));
|
|
167
|
+
playlistParts.push(otrkSize);
|
|
168
|
+
playlistParts.push((0, util_1.latin1Encode)("ptrk"));
|
|
169
|
+
playlistParts.push(ptrkSize);
|
|
170
|
+
playlistParts.push((0, util_1.seratoEncode)(absoluteTrackPath));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const playlistSection = (0, util_1.concatBytes)(playlistParts);
|
|
174
|
+
// ----- FINAL CONTENTS -----
|
|
175
|
+
const contents = (0, util_1.concatBytes)([header, columnSection, playlistSection]);
|
|
176
|
+
return contents;
|
|
177
|
+
}
|
|
178
|
+
save(root, savePath = DEFAULT_SERATO_FOLDER, overwrite = false) {
|
|
179
|
+
for (const [crate, filepath] of this._buildCrateFilepath(root, savePath)) {
|
|
180
|
+
if (fs.existsSync(filepath) && !overwrite)
|
|
181
|
+
continue;
|
|
182
|
+
const buffer = this._construct(crate);
|
|
183
|
+
fs.writeFileSync(filepath, buffer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.Builder = Builder;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Track } from '../model/track';
|
|
2
|
+
export declare abstract class BaseEncoder {
|
|
3
|
+
abstract get tagName(): string;
|
|
4
|
+
abstract get tagVersion(): Buffer;
|
|
5
|
+
abstract get markersName(): string;
|
|
6
|
+
abstract write(track: Track): void;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=baseEncoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseEncoder.d.ts","sourceRoot":"","sources":["../../src/encoders/baseEncoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,8BAAsB,WAAW;IAC/B,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC;IAC/B,QAAQ,KAAK,UAAU,IAAI,MAAM,CAAC;IAClC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;CACnC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SERATO_MARKERS_V2 = "Serato Markers2";
|
|
2
|
+
export declare const SERATO_OVERVIEW = "Serato Overview";
|
|
3
|
+
export declare const SERATO_MARKERS_V1 = "Serato Markers_";
|
|
4
|
+
export declare const SERATO_ANALYSIS = "Serato Analysis";
|
|
5
|
+
//# sourceMappingURL=serato_tags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serato_tags.d.ts","sourceRoot":"","sources":["../../src/encoders/serato_tags.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,eAAO,MAAM,eAAe,oBAAoB,CAAC;AACjD,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,eAAO,MAAM,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SERATO_ANALYSIS = exports.SERATO_MARKERS_V1 = exports.SERATO_OVERVIEW = exports.SERATO_MARKERS_V2 = void 0;
|
|
4
|
+
exports.SERATO_MARKERS_V2 = 'Serato Markers2';
|
|
5
|
+
exports.SERATO_OVERVIEW = 'Serato Overview';
|
|
6
|
+
exports.SERATO_MARKERS_V1 = 'Serato Markers_';
|
|
7
|
+
exports.SERATO_ANALYSIS = 'Serato Analysis';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/encoders/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS3C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS3C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encode = encode;
|
|
4
|
+
exports.decode = decode;
|
|
5
|
+
function encode(data) {
|
|
6
|
+
const a = data.readUInt8(0);
|
|
7
|
+
const b = data.readUInt8(1);
|
|
8
|
+
const c = data.readUInt8(2);
|
|
9
|
+
const z = c & 0x7F;
|
|
10
|
+
const y = ((c >> 7) | (b << 1)) & 0x7F;
|
|
11
|
+
const x = ((b >> 6) | (a << 2)) & 0x7F;
|
|
12
|
+
const w = a >> 5;
|
|
13
|
+
return Buffer.from([w, x, y, z]);
|
|
14
|
+
}
|
|
15
|
+
function decode(data) {
|
|
16
|
+
const w = data.readUInt8(0);
|
|
17
|
+
const x = data.readUInt8(1);
|
|
18
|
+
const y = data.readUInt8(2);
|
|
19
|
+
const z = data.readUInt8(3);
|
|
20
|
+
const c = (z & 0x7F) | ((y & 0x01) << 7);
|
|
21
|
+
const b = ((y & 0x7F) >> 1) | ((x & 0x03) << 6);
|
|
22
|
+
const a = ((x & 0x7F) >> 2) | ((w & 0x07) << 5);
|
|
23
|
+
return Buffer.from([a, b, c]);
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HotCue } from '../../../src/model/hotCue';
|
|
2
|
+
import { Track } from '../../model/track';
|
|
3
|
+
import { BaseEncoder } from '../baseEncoder';
|
|
4
|
+
export declare class V2Mp3Encoder extends BaseEncoder {
|
|
5
|
+
get tagName(): string;
|
|
6
|
+
get tagVersion(): Buffer;
|
|
7
|
+
get markersName(): string;
|
|
8
|
+
write(track: Track): void;
|
|
9
|
+
readCues(track: Track): HotCue[];
|
|
10
|
+
private _decode;
|
|
11
|
+
private _removeNullPadding;
|
|
12
|
+
private _getEntryName;
|
|
13
|
+
private _padEncodedData;
|
|
14
|
+
private _write;
|
|
15
|
+
private _encode;
|
|
16
|
+
private _pad;
|
|
17
|
+
private _removeEncodedDataPad;
|
|
18
|
+
private _padPayload;
|
|
19
|
+
private _enrichPayload;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=v2Mp3Encoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v2Mp3Encoder.d.ts","sourceRoot":"","sources":["../../../src/encoders/v2/v2Mp3Encoder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAiC7C,qBAAa,YAAa,SAAQ,WAAW;IAE3C,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAKzB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE;IAoBhC,OAAO,CAAE,OAAO;IAmDhB,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,MAAM;IAkCd,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,cAAc;CASvB"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.V2Mp3Encoder = void 0;
|
|
7
|
+
const hotCue_1 = require("../../../src/model/hotCue");
|
|
8
|
+
const hotCueType_1 = require("../../model/hotCueType");
|
|
9
|
+
const baseEncoder_1 = require("../baseEncoder");
|
|
10
|
+
const serato_tags_1 = require("../serato_tags");
|
|
11
|
+
const mp3tag_js_1 = __importDefault(require("mp3tag.js"));
|
|
12
|
+
const util_1 = require("../../util");
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
/**
|
|
15
|
+
* Replacement for Python's BytesIO
|
|
16
|
+
*/
|
|
17
|
+
class BufferReader {
|
|
18
|
+
constructor(buffer) {
|
|
19
|
+
this.buffer = buffer;
|
|
20
|
+
this.offset = 0;
|
|
21
|
+
}
|
|
22
|
+
read(n) {
|
|
23
|
+
const chunk = this.buffer.slice(this.offset, this.offset + n);
|
|
24
|
+
this.offset += n;
|
|
25
|
+
return chunk;
|
|
26
|
+
}
|
|
27
|
+
readUInt8() {
|
|
28
|
+
const value = this.buffer.readUInt8(this.offset);
|
|
29
|
+
this.offset += 1;
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
readUInt32BE() {
|
|
33
|
+
const value = this.buffer.readUInt32BE(this.offset);
|
|
34
|
+
this.offset += 4;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class V2Mp3Encoder extends baseEncoder_1.BaseEncoder {
|
|
39
|
+
get tagName() {
|
|
40
|
+
return serato_tags_1.SERATO_MARKERS_V2;
|
|
41
|
+
}
|
|
42
|
+
get tagVersion() {
|
|
43
|
+
return Buffer.from([0x01, 0x01]);
|
|
44
|
+
}
|
|
45
|
+
get markersName() {
|
|
46
|
+
return "Serato Markers2";
|
|
47
|
+
}
|
|
48
|
+
write(track) {
|
|
49
|
+
const payload = this._encode(track);
|
|
50
|
+
this._write(track, payload);
|
|
51
|
+
}
|
|
52
|
+
readCues(track) {
|
|
53
|
+
// Read the buffer of an audio file
|
|
54
|
+
const buffer = fs.readFileSync(track.path.toString());
|
|
55
|
+
// Now, pass it to MP3Tag
|
|
56
|
+
const mp3tag = new mp3tag_js_1.default(buffer, true);
|
|
57
|
+
mp3tag.read();
|
|
58
|
+
const geob = mp3tag.tags.v2?.GEOB;
|
|
59
|
+
if (!geob || !geob) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const data = Buffer.from(geob[0].object);
|
|
63
|
+
return Array.from(this._decode(data));
|
|
64
|
+
}
|
|
65
|
+
*_decode(data) {
|
|
66
|
+
const fp = new BufferReader(data);
|
|
67
|
+
// Expect version 0x01, 0x01
|
|
68
|
+
const a = fp.readUInt8();
|
|
69
|
+
const b = fp.readUInt8();
|
|
70
|
+
if (a !== 0x01 || b !== 0x01) {
|
|
71
|
+
throw new Error("Invalid Serato markers header");
|
|
72
|
+
}
|
|
73
|
+
const payload = fp.read(data.length - 2);
|
|
74
|
+
let processed = this._removeNullPadding(payload);
|
|
75
|
+
processed = this._padEncodedData(processed);
|
|
76
|
+
const decoded = Buffer.from(processed.toString(), "base64");
|
|
77
|
+
const fp2 = new BufferReader(decoded);
|
|
78
|
+
const a2 = fp2.readUInt8();
|
|
79
|
+
const b2 = fp2.readUInt8();
|
|
80
|
+
if (a2 !== 0x01 || b2 !== 0x01) {
|
|
81
|
+
throw new Error("Invalid decoded header");
|
|
82
|
+
}
|
|
83
|
+
while (true) {
|
|
84
|
+
const entryName = this._getEntryName(fp2);
|
|
85
|
+
if (entryName.length === 0) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const structLength = fp2.readUInt32BE();
|
|
89
|
+
if (structLength <= 0)
|
|
90
|
+
throw new Error("Invalid struct length");
|
|
91
|
+
const entryData = fp2.read(structLength);
|
|
92
|
+
switch (entryName) {
|
|
93
|
+
case "COLOR":
|
|
94
|
+
// not yet implemented
|
|
95
|
+
continue;
|
|
96
|
+
case "CUE":
|
|
97
|
+
yield hotCue_1.HotCue.fromBytes(entryData, hotCueType_1.HotCueType.CUE);
|
|
98
|
+
continue;
|
|
99
|
+
case "LOOP":
|
|
100
|
+
yield hotCue_1.HotCue.fromBytes(entryData, hotCueType_1.HotCueType.LOOP);
|
|
101
|
+
continue;
|
|
102
|
+
case "BPMLOCK":
|
|
103
|
+
// not yet implemented
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
_removeNullPadding(payload) {
|
|
109
|
+
const nullIndex = payload.indexOf(0x00);
|
|
110
|
+
return nullIndex >= 0 ? payload.slice(0, nullIndex) : payload;
|
|
111
|
+
}
|
|
112
|
+
_getEntryName(fp) {
|
|
113
|
+
const bytes = [];
|
|
114
|
+
while (true) {
|
|
115
|
+
const byte = fp.read(1)[0];
|
|
116
|
+
if (byte === 0x00)
|
|
117
|
+
break;
|
|
118
|
+
bytes.push(byte);
|
|
119
|
+
}
|
|
120
|
+
return Buffer.from(bytes).toString("utf-8");
|
|
121
|
+
}
|
|
122
|
+
_padEncodedData(data) {
|
|
123
|
+
const len = data.length;
|
|
124
|
+
let padding;
|
|
125
|
+
if (len % 4 === 1) {
|
|
126
|
+
padding = Buffer.from("A==");
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
padding = Buffer.from("=".repeat((-len % 4 + 4) % 4));
|
|
130
|
+
}
|
|
131
|
+
return Buffer.concat([data, padding]);
|
|
132
|
+
}
|
|
133
|
+
_write(track, payload) {
|
|
134
|
+
// Read the buffer of an audio file
|
|
135
|
+
const buffer = fs.readFileSync(track.path.toString());
|
|
136
|
+
const mp3tag = new mp3tag_js_1.default(buffer, true);
|
|
137
|
+
mp3tag.read();
|
|
138
|
+
// Write the ID3v2 tags.
|
|
139
|
+
// See https://mp3tag.js.org/docs/frames.html for the list of supported ID3v2 frames
|
|
140
|
+
const object = Array.from(payload);
|
|
141
|
+
mp3tag.tags.v2.GEOB = [
|
|
142
|
+
{
|
|
143
|
+
format: 'application/octet-stream',
|
|
144
|
+
filename: "",
|
|
145
|
+
object: object,
|
|
146
|
+
description: this.markersName,
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
// Save the tags
|
|
150
|
+
mp3tag.save({ id3v2: { encoding: "latin1" } });
|
|
151
|
+
// Handle error if there's any
|
|
152
|
+
if (mp3tag.error !== '')
|
|
153
|
+
throw new Error(mp3tag.error);
|
|
154
|
+
// Read the new buffer again
|
|
155
|
+
mp3tag.read();
|
|
156
|
+
// Write the new buffer to file
|
|
157
|
+
fs.writeFileSync(track.path.toString(), mp3tag.buffer);
|
|
158
|
+
}
|
|
159
|
+
_encode(track) {
|
|
160
|
+
let payload = Buffer.alloc(0);
|
|
161
|
+
for (const cue of track.hotCues) {
|
|
162
|
+
payload = Buffer.concat([payload, cue.toV2Bytes()]);
|
|
163
|
+
}
|
|
164
|
+
for (const cue of track.cueLoops) {
|
|
165
|
+
payload = Buffer.concat([payload, cue.toV2Bytes()]);
|
|
166
|
+
}
|
|
167
|
+
return this._pad(payload);
|
|
168
|
+
}
|
|
169
|
+
_pad(payload, entriesCount) {
|
|
170
|
+
// prepend tag version
|
|
171
|
+
payload = Buffer.concat([this.tagVersion, payload]);
|
|
172
|
+
payload = this._removeEncodedDataPad(Buffer.from(payload.toString("base64")));
|
|
173
|
+
payload = this._padPayload((0, util_1.splitString)(payload));
|
|
174
|
+
payload = this._enrichPayload(payload, entriesCount);
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
177
|
+
_removeEncodedDataPad(data) {
|
|
178
|
+
return Buffer.from(data.toString().replace(/=/g, "A"));
|
|
179
|
+
}
|
|
180
|
+
_padPayload(payload) {
|
|
181
|
+
const length = payload.length;
|
|
182
|
+
if (length < 468) {
|
|
183
|
+
return Buffer.concat([payload, Buffer.alloc(468 - length)]);
|
|
184
|
+
}
|
|
185
|
+
return Buffer.concat([payload, Buffer.alloc(982 - length), Buffer.from([0x00])]);
|
|
186
|
+
}
|
|
187
|
+
_enrichPayload(payload, entriesCount) {
|
|
188
|
+
let header = this.tagVersion;
|
|
189
|
+
if (entriesCount !== undefined) {
|
|
190
|
+
const buf = Buffer.alloc(4);
|
|
191
|
+
buf.writeUInt32BE(entriesCount, 0);
|
|
192
|
+
header = Buffer.concat([header, buf]);
|
|
193
|
+
}
|
|
194
|
+
return Buffer.concat([header, payload]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.V2Mp3Encoder = V2Mp3Encoder;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Builder } from './builder';
|
|
2
|
+
export { V2Mp3Encoder } from './encoders/v2/v2Mp3Encoder';
|
|
3
|
+
export { Crate } from './model/crate';
|
|
4
|
+
export { Track } from './model/track';
|
|
5
|
+
export { HotCue } from './model/hotCue';
|
|
6
|
+
export { HotCueType } from './model/hotCueType';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HotCueType = exports.HotCue = exports.Track = exports.Crate = exports.V2Mp3Encoder = exports.Builder = void 0;
|
|
4
|
+
var builder_1 = require("./builder");
|
|
5
|
+
Object.defineProperty(exports, "Builder", { enumerable: true, get: function () { return builder_1.Builder; } });
|
|
6
|
+
var v2Mp3Encoder_1 = require("./encoders/v2/v2Mp3Encoder");
|
|
7
|
+
Object.defineProperty(exports, "V2Mp3Encoder", { enumerable: true, get: function () { return v2Mp3Encoder_1.V2Mp3Encoder; } });
|
|
8
|
+
var crate_1 = require("./model/crate");
|
|
9
|
+
Object.defineProperty(exports, "Crate", { enumerable: true, get: function () { return crate_1.Crate; } });
|
|
10
|
+
var track_1 = require("./model/track");
|
|
11
|
+
Object.defineProperty(exports, "Track", { enumerable: true, get: function () { return track_1.Track; } });
|
|
12
|
+
var hotCue_1 = require("./model/hotCue");
|
|
13
|
+
Object.defineProperty(exports, "HotCue", { enumerable: true, get: function () { return hotCue_1.HotCue; } });
|
|
14
|
+
var hotCueType_1 = require("./model/hotCueType");
|
|
15
|
+
Object.defineProperty(exports, "HotCueType", { enumerable: true, get: function () { return hotCueType_1.HotCueType; } });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Track } from './track';
|
|
2
|
+
export declare class Crate {
|
|
3
|
+
private _children;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
private _tracks;
|
|
6
|
+
constructor(name: string, children?: Crate[]);
|
|
7
|
+
get children(): Crate[];
|
|
8
|
+
get tracks(): Set<Track>;
|
|
9
|
+
addTrack(track: Track): void;
|
|
10
|
+
toString(): string;
|
|
11
|
+
plus(other: Crate): Crate;
|
|
12
|
+
deepCopy(memodict?: Map<any, any>): Crate;
|
|
13
|
+
equals(other: Crate): boolean;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=crate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crate.d.ts","sourceRoot":"","sources":["../../src/model/crate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,qBAAa,KAAK;IAChB,OAAO,CAAC,SAAS,CAAU;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,OAAO,CAAa;gBAEhB,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,KAAK,EAAO;IAMhD,IAAI,QAAQ,IAAI,KAAK,EAAE,CAEtB;IAED,IAAI,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAEvB;IAED,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAO5B,QAAQ,IAAI,MAAM;IAQlB,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK;IAazB,QAAQ,CAAC,QAAQ,gBAAsB,GAAG,KAAK;IAa/C,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;CAqB9B"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Crate = void 0;
|
|
4
|
+
const util_1 = require("../util");
|
|
5
|
+
class Crate {
|
|
6
|
+
constructor(name, children = []) {
|
|
7
|
+
this._children = [...children];
|
|
8
|
+
this.name = (0, util_1.sanitizeFilename)(name);
|
|
9
|
+
this._tracks = new Set();
|
|
10
|
+
}
|
|
11
|
+
get children() {
|
|
12
|
+
return this._children;
|
|
13
|
+
}
|
|
14
|
+
get tracks() {
|
|
15
|
+
return this._tracks;
|
|
16
|
+
}
|
|
17
|
+
addTrack(track) {
|
|
18
|
+
if (this._tracks.has(track)) {
|
|
19
|
+
throw new util_1.DuplicateTrackError(`track ${track} is already in the crate ${this.name}`);
|
|
20
|
+
}
|
|
21
|
+
this._tracks.add(track);
|
|
22
|
+
}
|
|
23
|
+
toString() {
|
|
24
|
+
return `Crate<${this.name}>`;
|
|
25
|
+
}
|
|
26
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
27
|
+
return this.toString();
|
|
28
|
+
}
|
|
29
|
+
plus(other) {
|
|
30
|
+
if (this.name !== other.name) {
|
|
31
|
+
throw new Error("Cannot merge crates with different names");
|
|
32
|
+
}
|
|
33
|
+
const childrenCopy = [...this._children, ...other._children].map((c) => c.deepCopy());
|
|
34
|
+
const merged = new Crate(this.name, childrenCopy);
|
|
35
|
+
const allTracks = new Set([...this._tracks, ...other._tracks]);
|
|
36
|
+
for (const track of allTracks) {
|
|
37
|
+
merged.addTrack(track);
|
|
38
|
+
}
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
deepCopy(memodict = new Map()) {
|
|
42
|
+
if (memodict.has(this)) {
|
|
43
|
+
return memodict.get(this);
|
|
44
|
+
}
|
|
45
|
+
const childrenCopy = this._children.map((c) => c.deepCopy(memodict));
|
|
46
|
+
const copy = new Crate(this.name, childrenCopy);
|
|
47
|
+
memodict.set(this, copy);
|
|
48
|
+
for (const track of this._tracks) {
|
|
49
|
+
copy.addTrack(track);
|
|
50
|
+
}
|
|
51
|
+
return copy;
|
|
52
|
+
}
|
|
53
|
+
equals(other) {
|
|
54
|
+
if (this.name !== other.name)
|
|
55
|
+
return false;
|
|
56
|
+
if (this._tracks.size !== other._tracks.size)
|
|
57
|
+
return false;
|
|
58
|
+
// compare tracks by reference equality (like Python set)
|
|
59
|
+
for (const track of this._tracks) {
|
|
60
|
+
if (!other._tracks.has(track))
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (this._children.length || other._children.length) {
|
|
64
|
+
const sortedChildren = [...this._children].sort((a, b) => a._tracks.size - b._tracks.size);
|
|
65
|
+
const sortedOtherChildren = [...other._children].sort((a, b) => a._tracks.size - b._tracks.size);
|
|
66
|
+
if (sortedChildren.length !== sortedOtherChildren.length)
|
|
67
|
+
return false;
|
|
68
|
+
for (let i = 0; i < sortedChildren.length; i++) {
|
|
69
|
+
if (!sortedChildren[i].equals(sortedOtherChildren[i]))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.Crate = Crate;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { HotCueType } from './hotCueType';
|
|
2
|
+
import { SeratoColor } from './seratoColor';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
export interface HotCueData {
|
|
5
|
+
name: string;
|
|
6
|
+
type: HotCueType;
|
|
7
|
+
start: number;
|
|
8
|
+
index: number;
|
|
9
|
+
end?: number | null;
|
|
10
|
+
isLocked?: boolean;
|
|
11
|
+
color?: SeratoColor;
|
|
12
|
+
}
|
|
13
|
+
export declare class HotCue implements HotCueData {
|
|
14
|
+
name: string;
|
|
15
|
+
type: HotCueType;
|
|
16
|
+
start: number;
|
|
17
|
+
index: number;
|
|
18
|
+
end?: number | null;
|
|
19
|
+
isLocked?: boolean;
|
|
20
|
+
color: SeratoColor;
|
|
21
|
+
constructor(data: HotCueData);
|
|
22
|
+
toString(): string;
|
|
23
|
+
toV2Bytes(): Buffer;
|
|
24
|
+
private loopToV2Bytes;
|
|
25
|
+
private cueToV2Bytes;
|
|
26
|
+
static fromBytes(data: Buffer, hotcueType: HotCueType): HotCue;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=hotCue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hotCue.d.ts","sourceRoot":"","sources":["../../src/model/hotCue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AA6BhC,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,qBAAa,MAAO,YAAW,UAAU;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,WAAW,CAAC;gBAEP,IAAI,EAAE,UAAU;IAU5B,QAAQ,IAAI,MAAM;IAMlB,SAAS,IAAI,MAAM;IAMnB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,YAAY;IAiDpB,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM;CAiE/D"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HotCue = void 0;
|
|
4
|
+
const hotCueType_1 = require("./hotCueType");
|
|
5
|
+
const seratoColor_1 = require("./seratoColor");
|
|
6
|
+
const buffer_1 = require("buffer");
|
|
7
|
+
function writeNullTerminatedString(str) {
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const strBytes = encoder.encode(str);
|
|
10
|
+
const result = new Uint8Array(strBytes.length + 1); // +1 for null terminator
|
|
11
|
+
result.set(strBytes, 0);
|
|
12
|
+
result[strBytes.length] = 0;
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function concatUint8Arrays(arrays) {
|
|
16
|
+
let totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
17
|
+
let result = new Uint8Array(totalLength);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
arrays.forEach(arr => {
|
|
20
|
+
result.set(arr, offset);
|
|
21
|
+
offset += arr.length;
|
|
22
|
+
});
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function encodeElement(name, data) {
|
|
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
|
+
class HotCue {
|
|
32
|
+
constructor(data) {
|
|
33
|
+
this.name = data.name;
|
|
34
|
+
this.type = data.type;
|
|
35
|
+
this.start = data.start;
|
|
36
|
+
this.index = data.index;
|
|
37
|
+
this.end = data.end ?? null;
|
|
38
|
+
this.isLocked = data.isLocked ?? false;
|
|
39
|
+
this.color = data.color ?? seratoColor_1.SeratoColor.RED;
|
|
40
|
+
}
|
|
41
|
+
toString() {
|
|
42
|
+
const startStr = `${this.start}ms`;
|
|
43
|
+
const endStr = this.end != null ? ` | End: ${this.end}ms` : '';
|
|
44
|
+
return `Start: ${startStr}${endStr} | Index: ${String(this.index).padStart(2)} | Name: ${this.name} | Color: ${this.color}`;
|
|
45
|
+
}
|
|
46
|
+
toV2Bytes() {
|
|
47
|
+
if (this.type === hotCueType_1.HotCueType.CUE)
|
|
48
|
+
return this.cueToV2Bytes();
|
|
49
|
+
if (this.type === hotCueType_1.HotCueType.LOOP)
|
|
50
|
+
return this.loopToV2Bytes();
|
|
51
|
+
throw new Error(`unsupported hotcue type ${this.type}`);
|
|
52
|
+
}
|
|
53
|
+
loopToV2Bytes() {
|
|
54
|
+
let nameBytes = writeNullTerminatedString(this.name);
|
|
55
|
+
let buf = new Uint8Array(0x14 + nameBytes.length);
|
|
56
|
+
let dv = new DataView(buf.buffer);
|
|
57
|
+
buf[0x0] = 0x00; // flags? (unused in parse)
|
|
58
|
+
buf[0x1] = this.index;
|
|
59
|
+
dv.setUint32(0x02, this.start, false);
|
|
60
|
+
dv.setUint32(0x06, this.end, false);
|
|
61
|
+
buf[0x0e] = 0;
|
|
62
|
+
buf[0x0f] = 0;
|
|
63
|
+
buf[0x10] = 255;
|
|
64
|
+
buf[0x11] = 255;
|
|
65
|
+
buf[0x13] = 1;
|
|
66
|
+
buf.set(nameBytes, 0x14);
|
|
67
|
+
return buffer_1.Buffer.from(encodeElement("LOOP", buf));
|
|
68
|
+
}
|
|
69
|
+
cueToV2Bytes() {
|
|
70
|
+
const parts = [];
|
|
71
|
+
// >B (unsigned char, big-endian)
|
|
72
|
+
parts.push(buffer_1.Buffer.from([0]));
|
|
73
|
+
// >B self.index
|
|
74
|
+
parts.push(buffer_1.Buffer.from([this.index]));
|
|
75
|
+
// >I self.start (4-byte big-endian unsigned int)
|
|
76
|
+
const startBuf = buffer_1.Buffer.alloc(4);
|
|
77
|
+
startBuf.writeUInt32BE(this.start, 0);
|
|
78
|
+
parts.push(startBuf);
|
|
79
|
+
// >B 0
|
|
80
|
+
parts.push(buffer_1.Buffer.from([0]));
|
|
81
|
+
// >3s color (3 raw bytes from hex string)
|
|
82
|
+
const colorBuf = buffer_1.Buffer.from(this.color, "hex");
|
|
83
|
+
if (colorBuf.length !== 3) {
|
|
84
|
+
throw new Error(`Color must be 3 bytes (e.g. "ff0000"), got ${this.color}`);
|
|
85
|
+
}
|
|
86
|
+
parts.push(colorBuf);
|
|
87
|
+
// >B 0
|
|
88
|
+
parts.push(buffer_1.Buffer.from([0]));
|
|
89
|
+
parts.push(buffer_1.Buffer.from([1]));
|
|
90
|
+
parts.push(buffer_1.Buffer.from(this.name, "utf8"));
|
|
91
|
+
// >B 0 (null terminator after name)
|
|
92
|
+
parts.push(buffer_1.Buffer.from([0]));
|
|
93
|
+
const data = buffer_1.Buffer.concat(parts);
|
|
94
|
+
const lenBuf = buffer_1.Buffer.alloc(4);
|
|
95
|
+
lenBuf.writeUInt32BE(data.length, 0);
|
|
96
|
+
const payload = buffer_1.Buffer.concat([
|
|
97
|
+
buffer_1.Buffer.from("CUE", "ascii"),
|
|
98
|
+
buffer_1.Buffer.from([0]),
|
|
99
|
+
lenBuf,
|
|
100
|
+
data,
|
|
101
|
+
]);
|
|
102
|
+
return payload;
|
|
103
|
+
}
|
|
104
|
+
static fromBytes(data, hotcueType) {
|
|
105
|
+
let offset = 1; // skip first null
|
|
106
|
+
if (hotcueType === hotCueType_1.HotCueType.CUE) {
|
|
107
|
+
const index = data.readUInt8(offset);
|
|
108
|
+
offset += 1;
|
|
109
|
+
const start = data.readUInt32BE(offset);
|
|
110
|
+
offset += 4;
|
|
111
|
+
offset += 1; // skip null / position end
|
|
112
|
+
offset += 0; // skip placeholder
|
|
113
|
+
const colorBytes = data.slice(offset, offset + 3);
|
|
114
|
+
offset += 3;
|
|
115
|
+
offset += 1; // skip null
|
|
116
|
+
const locked = data.readUInt8(offset) !== 0;
|
|
117
|
+
offset += 1;
|
|
118
|
+
const nameBuf = data.slice(offset).subarray(0, data.slice(offset).indexOf(0));
|
|
119
|
+
const name = nameBuf.toString("utf-8");
|
|
120
|
+
const colorHex = colorBytes.toString("hex").toUpperCase();
|
|
121
|
+
const color = (Object.values(seratoColor_1.SeratoColor).includes(colorHex)
|
|
122
|
+
? colorHex
|
|
123
|
+
: seratoColor_1.SeratoColor.RED);
|
|
124
|
+
return new HotCue({
|
|
125
|
+
name,
|
|
126
|
+
type: hotCueType_1.HotCueType.CUE,
|
|
127
|
+
index,
|
|
128
|
+
start,
|
|
129
|
+
color,
|
|
130
|
+
isLocked: locked,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else if (hotcueType === hotCueType_1.HotCueType.LOOP) {
|
|
134
|
+
const index = data.readUInt8(offset);
|
|
135
|
+
offset += 1;
|
|
136
|
+
const start = data.readUInt32BE(offset);
|
|
137
|
+
offset += 4;
|
|
138
|
+
const end = data.readUInt32BE(offset);
|
|
139
|
+
offset += 4;
|
|
140
|
+
// Skip to offset 0x0E
|
|
141
|
+
offset = 0x0E;
|
|
142
|
+
// skip color placeholder bytes (0x0E–0x11)
|
|
143
|
+
offset += 2; // 0x0E, 0x0F
|
|
144
|
+
offset += 2; // 0x10, 0x11
|
|
145
|
+
offset += 1; // 0x12 (padding)
|
|
146
|
+
const isLocked = Boolean(data.readUInt8(offset));
|
|
147
|
+
offset += 1;
|
|
148
|
+
// read null-terminated name
|
|
149
|
+
const nameEnd = data.indexOf(0x00, offset);
|
|
150
|
+
const name = data.toString("utf8", offset, nameEnd === -1 ? data.length : nameEnd);
|
|
151
|
+
return new HotCue({
|
|
152
|
+
name,
|
|
153
|
+
type: hotCueType_1.HotCueType.CUE,
|
|
154
|
+
index,
|
|
155
|
+
start,
|
|
156
|
+
end: end,
|
|
157
|
+
color: seratoColor_1.SeratoColor.RED,
|
|
158
|
+
isLocked: isLocked,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
throw new Error(`Unknown hotcue type: ${hotcueType}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.HotCue = HotCue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hotCueType.d.ts","sourceRoot":"","sources":["../../src/model/hotCueType.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,GAAG,IAAI;IACP,IAAI,IAAI;IACR,OAAO,KAAK;IACZ,WAAW,MAAM;CAClB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HotCueType = void 0;
|
|
4
|
+
var HotCueType;
|
|
5
|
+
(function (HotCueType) {
|
|
6
|
+
HotCueType[HotCueType["CUE"] = 0] = "CUE";
|
|
7
|
+
HotCueType[HotCueType["LOOP"] = 1] = "LOOP";
|
|
8
|
+
HotCueType[HotCueType["INVALID"] = 99] = "INVALID";
|
|
9
|
+
HotCueType[HotCueType["PASSTHROUGH"] = 100] = "PASSTHROUGH";
|
|
10
|
+
})(HotCueType || (exports.HotCueType = HotCueType = {}));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare 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
|
+
}
|
|
21
|
+
//# sourceMappingURL=seratoColor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seratoColor.d.ts","sourceRoot":"","sources":["../../src/model/seratoColor.ts"],"names":[],"mappings":"AAAA,oBAAY,WAAW;IACrB,GAAG,WAAW;IACd,MAAM,WAAW;IACjB,KAAK,WAAW;IAChB,MAAM,WAAW;IACjB,UAAU,WAAW;IACrB,YAAY,WAAW;IACvB,KAAK,WAAW;IAChB,UAAU,WAAW;IACrB,UAAU,WAAW;IACrB,IAAI,WAAW;IACf,QAAQ,WAAW;IACnB,IAAI,WAAW;IACf,SAAS,WAAW;IACpB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,OAAO,WAAW;IAClB,IAAI,WAAW;IACf,QAAQ,WAAW;CACpB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SeratoColor = void 0;
|
|
4
|
+
var SeratoColor;
|
|
5
|
+
(function (SeratoColor) {
|
|
6
|
+
SeratoColor["RED"] = "CC0000";
|
|
7
|
+
SeratoColor["ORANGE"] = "CC4400";
|
|
8
|
+
SeratoColor["AMBER"] = "CC8800";
|
|
9
|
+
SeratoColor["YELLOW"] = "CCCC00";
|
|
10
|
+
SeratoColor["LIME_GREEN"] = "88CC00";
|
|
11
|
+
SeratoColor["GREEN_YELLOW"] = "44CC00";
|
|
12
|
+
SeratoColor["GREEN"] = "00CC00";
|
|
13
|
+
SeratoColor["MINT_GREEN"] = "00CC44";
|
|
14
|
+
SeratoColor["TEAL_GREEN"] = "00CC88";
|
|
15
|
+
SeratoColor["TEAL"] = "00CCCC";
|
|
16
|
+
SeratoColor["SKY_BLUE"] = "0088CC";
|
|
17
|
+
SeratoColor["BLUE"] = "0044CC";
|
|
18
|
+
SeratoColor["DARK_BLUE"] = "0000CC";
|
|
19
|
+
SeratoColor["PURPLE"] = "4400CC";
|
|
20
|
+
SeratoColor["VIOLET"] = "8800CC";
|
|
21
|
+
SeratoColor["MAGENTA"] = "CC00CC";
|
|
22
|
+
SeratoColor["PINK"] = "CC0088";
|
|
23
|
+
SeratoColor["RED_PINK"] = "CC0044";
|
|
24
|
+
})(SeratoColor || (exports.SeratoColor = SeratoColor = {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tempo.d.ts","sourceRoot":"","sources":["../../src/model/tempo.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HotCue } from './hotCue';
|
|
2
|
+
import { Tempo } from './tempo';
|
|
3
|
+
export declare class Track {
|
|
4
|
+
path: string;
|
|
5
|
+
trackId: string;
|
|
6
|
+
averageBpm: number;
|
|
7
|
+
dateAdded: string;
|
|
8
|
+
playCount: string;
|
|
9
|
+
tonality: string;
|
|
10
|
+
totalTime: number;
|
|
11
|
+
beatgrid: Tempo[];
|
|
12
|
+
hotCues: HotCue[];
|
|
13
|
+
cueLoops: HotCue[];
|
|
14
|
+
constructor(trackPath: string, params?: {
|
|
15
|
+
trackId?: string;
|
|
16
|
+
averageBpm?: number;
|
|
17
|
+
dateAdded?: string;
|
|
18
|
+
playCount?: string;
|
|
19
|
+
tonality?: string;
|
|
20
|
+
totalTime?: number;
|
|
21
|
+
beatgrid?: Tempo[];
|
|
22
|
+
hotCues?: HotCue[];
|
|
23
|
+
cueLoops?: HotCue[];
|
|
24
|
+
});
|
|
25
|
+
static fromPath(trackPath: string, userRoot?: string): Track;
|
|
26
|
+
addBeatgridMarker(tempo: Tempo): void;
|
|
27
|
+
addHotCue(hotCue: HotCue): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=track.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../../src/model/track.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,qBAAa,KAAK;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAElB,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;gBAGjB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE;QACN,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KAChB;IAeR,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK;IAK5D,iBAAiB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAIrC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAehC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Track = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const hotCueType_1 = require("./hotCueType");
|
|
39
|
+
class Track {
|
|
40
|
+
constructor(trackPath, params = {}) {
|
|
41
|
+
this.path = trackPath;
|
|
42
|
+
this.trackId = params.trackId ?? "";
|
|
43
|
+
this.averageBpm = params.averageBpm ?? 0.0;
|
|
44
|
+
this.dateAdded = params.dateAdded ?? "";
|
|
45
|
+
this.playCount = params.playCount ?? "";
|
|
46
|
+
this.tonality = params.tonality ?? "";
|
|
47
|
+
this.totalTime = params.totalTime ?? 0.0;
|
|
48
|
+
this.beatgrid = params.beatgrid ?? [];
|
|
49
|
+
this.hotCues = params.hotCues ?? [];
|
|
50
|
+
this.cueLoops = params.cueLoops ?? [];
|
|
51
|
+
}
|
|
52
|
+
static fromPath(trackPath, userRoot) {
|
|
53
|
+
const resolved = userRoot ? path.resolve(userRoot, trackPath) : path.resolve(trackPath);
|
|
54
|
+
return new Track(resolved);
|
|
55
|
+
}
|
|
56
|
+
addBeatgridMarker(tempo) {
|
|
57
|
+
this.beatgrid.push(tempo);
|
|
58
|
+
}
|
|
59
|
+
addHotCue(hotCue) {
|
|
60
|
+
if (this.hotCues.length >= 8) {
|
|
61
|
+
throw new Error("cannot have more than 8 hot cues on a track");
|
|
62
|
+
}
|
|
63
|
+
if (this.cueLoops.length >= 4) {
|
|
64
|
+
throw new Error("cannot have more than 4 loops on a track");
|
|
65
|
+
}
|
|
66
|
+
const atIndex = hotCue.index;
|
|
67
|
+
if (hotCue.type === hotCueType_1.HotCueType.LOOP) {
|
|
68
|
+
this.cueLoops.splice(atIndex, 0, hotCue);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.hotCues.splice(atIndex, 0, hotCue);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.Track = Track;
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
export declare function splitString(input: Buffer, after?: number, delimiter?: Buffer): Buffer;
|
|
3
|
+
export declare function seratoDecode(s: Buffer): string;
|
|
4
|
+
export declare function concatBytes(arrays: Uint8Array[]): Uint8Array;
|
|
5
|
+
export declare function latin1Encode(str: string): Uint8Array;
|
|
6
|
+
export declare function intToBytes(value: number, length: number): Uint8Array;
|
|
7
|
+
export declare function seratoEncode(s: string): Uint8Array;
|
|
8
|
+
export declare function sanitizeFilename(filename: string): string;
|
|
9
|
+
export declare class DuplicateTrackError extends Error {
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,EAAE,SAAS,GAAE,MAA0B,GAAG,MAAM,CAY5G;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B9C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAS5D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAEpD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAOpE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAWlD;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,qBAAa,mBAAoB,SAAQ,KAAK;CAAG"}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DuplicateTrackError = void 0;
|
|
4
|
+
exports.splitString = splitString;
|
|
5
|
+
exports.seratoDecode = seratoDecode;
|
|
6
|
+
exports.concatBytes = concatBytes;
|
|
7
|
+
exports.latin1Encode = latin1Encode;
|
|
8
|
+
exports.intToBytes = intToBytes;
|
|
9
|
+
exports.seratoEncode = seratoEncode;
|
|
10
|
+
exports.sanitizeFilename = sanitizeFilename;
|
|
11
|
+
const buffer_1 = require("buffer");
|
|
12
|
+
const INVALID_CHARACTERS_REGEX = /[^A-Za-z0-9_ ]/i;
|
|
13
|
+
function splitString(input, after = 72, delimiter = buffer_1.Buffer.from('\n')) {
|
|
14
|
+
const pieces = [];
|
|
15
|
+
let buf = buffer_1.Buffer.from(input);
|
|
16
|
+
while (buf.length > 0) {
|
|
17
|
+
pieces.push(buf.slice(0, after));
|
|
18
|
+
buf = buf.slice(after);
|
|
19
|
+
}
|
|
20
|
+
return buffer_1.Buffer.concat(pieces.reduce((acc, p, i) => {
|
|
21
|
+
if (i)
|
|
22
|
+
acc.push(delimiter);
|
|
23
|
+
acc.push(p);
|
|
24
|
+
return acc;
|
|
25
|
+
}, []));
|
|
26
|
+
}
|
|
27
|
+
function seratoDecode(s) {
|
|
28
|
+
// Decodes Serato 4-byte blocks into UTF-16-encoded string
|
|
29
|
+
const out = [];
|
|
30
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
31
|
+
const block = s.slice(i, i + 4);
|
|
32
|
+
if (block.length < 4)
|
|
33
|
+
break;
|
|
34
|
+
const w = block.readUInt8(0);
|
|
35
|
+
const x = block.readUInt8(1);
|
|
36
|
+
const y = block.readUInt8(2);
|
|
37
|
+
const z = block.readUInt8(3);
|
|
38
|
+
const c = (z & 0x7F) | ((y & 0x01) << 7);
|
|
39
|
+
const b = ((y & 0x7F) >> 1) | ((x & 0x03) << 6);
|
|
40
|
+
const a = ((x & 0x7F) >> 2) | ((w & 0x07) << 5);
|
|
41
|
+
out.push(a, b, c);
|
|
42
|
+
}
|
|
43
|
+
// Interpret as UTF-16-BE pairs
|
|
44
|
+
// Convert bytes to string by grouping into 2-byte code units
|
|
45
|
+
const bytes = buffer_1.Buffer.from(out);
|
|
46
|
+
let str = '';
|
|
47
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
|
48
|
+
const hi = bytes[i];
|
|
49
|
+
const lo = (i + 1) < bytes.length ? bytes[i + 1] : 0;
|
|
50
|
+
const code = (hi << 8) | lo;
|
|
51
|
+
if (code === 0)
|
|
52
|
+
break;
|
|
53
|
+
str += String.fromCharCode(code);
|
|
54
|
+
}
|
|
55
|
+
return str;
|
|
56
|
+
}
|
|
57
|
+
function concatBytes(arrays) {
|
|
58
|
+
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
59
|
+
const result = new Uint8Array(totalLength);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
for (const a of arrays) {
|
|
62
|
+
result.set(a, offset);
|
|
63
|
+
offset += a.length;
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function latin1Encode(str) {
|
|
68
|
+
return Uint8Array.from(buffer_1.Buffer.from(str, "latin1"));
|
|
69
|
+
}
|
|
70
|
+
function intToBytes(value, length) {
|
|
71
|
+
const bytes = new Uint8Array(length);
|
|
72
|
+
for (let i = 0; i < length; i++) {
|
|
73
|
+
// big-endian
|
|
74
|
+
bytes[length - 1 - i] = (value >> (8 * i)) & 0xff;
|
|
75
|
+
}
|
|
76
|
+
return bytes;
|
|
77
|
+
}
|
|
78
|
+
function seratoEncode(s) {
|
|
79
|
+
const result = [];
|
|
80
|
+
for (const c of s) {
|
|
81
|
+
const codePoint = c.charCodeAt(0); // UTF-16 code unit
|
|
82
|
+
// Big-endian: high byte first, then low byte
|
|
83
|
+
result.push((codePoint >> 8) & 0xff);
|
|
84
|
+
result.push(codePoint & 0xff);
|
|
85
|
+
}
|
|
86
|
+
return Uint8Array.from(result);
|
|
87
|
+
}
|
|
88
|
+
function sanitizeFilename(filename) {
|
|
89
|
+
return filename.replace(INVALID_CHARACTERS_REGEX, '-');
|
|
90
|
+
}
|
|
91
|
+
class DuplicateTrackError extends Error {
|
|
92
|
+
}
|
|
93
|
+
exports.DuplicateTrackError = DuplicateTrackError;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tserato",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/laker-93/tserato.git"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^24.5.1",
|
|
16
|
+
"typescript": "^5.3.3",
|
|
17
|
+
"vitest": "^0.34.6"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"base-64": "^1.0.0",
|
|
21
|
+
"mp3tag.js": "^3.14.1",
|
|
22
|
+
"node-id3": "^0.2.6"
|
|
23
|
+
}
|
|
24
|
+
}
|