chart-preview 1.0.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +38 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +8 -0
- package/README.md +541 -0
- package/dev/index.html +299 -0
- package/dev/main.ts +525 -0
- package/dev/tsconfig.json +16 -0
- package/dist/index.d.ts +496 -0
- package/dist/index.js +2158 -0
- package/dist/index.mjs +2125 -0
- package/package.json +57 -0
- package/src/ChartPreview.ts +1346 -0
- package/src/ChartPreviewPlayer.ts +1163 -0
- package/src/SharedAudioContext.ts +83 -0
- package/src/SngLoader.ts +330 -0
- package/src/index.ts +37 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +18 -0
package/dev/main.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChartPreview,
|
|
3
|
+
ChartPreviewPlayer,
|
|
4
|
+
extractSngFile,
|
|
5
|
+
getInstrumentType,
|
|
6
|
+
type Difficulty,
|
|
7
|
+
type Instrument,
|
|
8
|
+
} from "../src";
|
|
9
|
+
import { parseChartFile, scanChartFolder, type ScannedChart } from "scan-chart";
|
|
10
|
+
|
|
11
|
+
// LocalStorage keys
|
|
12
|
+
const STORAGE_KEY_LAST_CHART = "chartPreview_lastChart";
|
|
13
|
+
const STORAGE_KEY_LAST_INSTRUMENT = "chartPreview_lastInstrument";
|
|
14
|
+
const STORAGE_KEY_LAST_DIFFICULTY = "chartPreview_lastDifficulty";
|
|
15
|
+
|
|
16
|
+
// Instrument display names
|
|
17
|
+
const instrumentNames: Record<Instrument, string> = {
|
|
18
|
+
guitar: "Lead Guitar",
|
|
19
|
+
guitarcoop: "Co-op Guitar",
|
|
20
|
+
rhythm: "Rhythm Guitar",
|
|
21
|
+
bass: "Bass Guitar",
|
|
22
|
+
drums: "Drums",
|
|
23
|
+
keys: "Keys",
|
|
24
|
+
guitarghl: "GHL Guitar",
|
|
25
|
+
guitarcoopghl: "GHL Co-op Guitar",
|
|
26
|
+
rhythmghl: "GHL Rhythm Guitar",
|
|
27
|
+
bassghl: "GHL Bass",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const difficultyNames: Record<Difficulty, string> = {
|
|
31
|
+
expert: "Expert",
|
|
32
|
+
hard: "Hard",
|
|
33
|
+
medium: "Medium",
|
|
34
|
+
easy: "Easy",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// DOM elements
|
|
38
|
+
const chooseFolderBtn = document.getElementById(
|
|
39
|
+
"choose-folder-btn"
|
|
40
|
+
) as HTMLButtonElement;
|
|
41
|
+
const chooseSngBtn = document.getElementById(
|
|
42
|
+
"choose-sng-btn"
|
|
43
|
+
) as HTMLButtonElement;
|
|
44
|
+
const folderInput = document.getElementById("folder-input") as HTMLInputElement;
|
|
45
|
+
const sngInput = document.getElementById("sng-input") as HTMLInputElement;
|
|
46
|
+
const lastChartHint = document.getElementById(
|
|
47
|
+
"last-chart-hint"
|
|
48
|
+
) as HTMLDivElement;
|
|
49
|
+
const lastChartName = document.getElementById(
|
|
50
|
+
"last-chart-name"
|
|
51
|
+
) as HTMLSpanElement;
|
|
52
|
+
const chartInfoEl = document.getElementById("chart-info") as HTMLDivElement;
|
|
53
|
+
const songNameEl = document.getElementById("song-name") as HTMLParagraphElement;
|
|
54
|
+
const songArtistEl = document.getElementById(
|
|
55
|
+
"song-artist"
|
|
56
|
+
) as HTMLParagraphElement;
|
|
57
|
+
const songCharterEl = document.getElementById(
|
|
58
|
+
"song-charter"
|
|
59
|
+
) as HTMLParagraphElement;
|
|
60
|
+
const songLengthEl = document.getElementById(
|
|
61
|
+
"song-length"
|
|
62
|
+
) as HTMLParagraphElement;
|
|
63
|
+
const trackOptionsEl = document.getElementById(
|
|
64
|
+
"track-options"
|
|
65
|
+
) as HTMLDivElement;
|
|
66
|
+
const instrumentSelect = document.getElementById(
|
|
67
|
+
"instrument"
|
|
68
|
+
) as HTMLSelectElement;
|
|
69
|
+
const difficultySelect = document.getElementById(
|
|
70
|
+
"difficulty"
|
|
71
|
+
) as HTMLSelectElement;
|
|
72
|
+
const loadBtn = document.getElementById("load-btn") as HTMLButtonElement;
|
|
73
|
+
const statusEl = document.getElementById("status") as HTMLDivElement;
|
|
74
|
+
const playerEventsEl = document.getElementById(
|
|
75
|
+
"player-events"
|
|
76
|
+
) as HTMLDivElement;
|
|
77
|
+
|
|
78
|
+
// Get the Web Component player
|
|
79
|
+
const player = document.getElementById("player") as ChartPreviewPlayer;
|
|
80
|
+
|
|
81
|
+
// State
|
|
82
|
+
let currentChartFiles: { fileName: string; data: Uint8Array }[] = [];
|
|
83
|
+
let currentScannedChart: ScannedChart | null = null;
|
|
84
|
+
let currentChartName = "";
|
|
85
|
+
|
|
86
|
+
// Available tracks from the scanned chart
|
|
87
|
+
interface AvailableTrack {
|
|
88
|
+
instrument: Instrument;
|
|
89
|
+
difficulties: Difficulty[];
|
|
90
|
+
}
|
|
91
|
+
let availableTracks: AvailableTrack[] = [];
|
|
92
|
+
|
|
93
|
+
// Log player events
|
|
94
|
+
function logEvent(type: string, detail?: unknown) {
|
|
95
|
+
const eventEl = document.createElement("div");
|
|
96
|
+
eventEl.className = `event ${type}`;
|
|
97
|
+
const time = new Date().toLocaleTimeString();
|
|
98
|
+
eventEl.textContent = `[${time}] ${type}${
|
|
99
|
+
detail ? `: ${JSON.stringify(detail)}` : ""
|
|
100
|
+
}`;
|
|
101
|
+
playerEventsEl.insertBefore(eventEl, playerEventsEl.firstChild);
|
|
102
|
+
// Keep only last 20 events
|
|
103
|
+
while (playerEventsEl.children.length > 20) {
|
|
104
|
+
playerEventsEl.removeChild(playerEventsEl.lastChild!);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Setup player event listeners
|
|
109
|
+
player.addEventListener("player-statechange", (e: Event) => {
|
|
110
|
+
const detail = (e as CustomEvent).detail;
|
|
111
|
+
logEvent("state", detail);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
player.addEventListener("player-progress", (e: Event) => {
|
|
115
|
+
// Only log every ~10%
|
|
116
|
+
const detail = (e as CustomEvent).detail;
|
|
117
|
+
if (
|
|
118
|
+
Math.round(detail.percent * 10) % 1 === 0 &&
|
|
119
|
+
Math.round(detail.percent * 100) % 10 === 0
|
|
120
|
+
) {
|
|
121
|
+
logEvent("progress", { percent: Math.round(detail.percent * 100) + "%" });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
player.addEventListener("player-end", () => {
|
|
126
|
+
logEvent("end");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
player.addEventListener("player-error", (e: Event) => {
|
|
130
|
+
const detail = (e as CustomEvent).detail;
|
|
131
|
+
logEvent("error", detail.error?.message || "Unknown error");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Initialize: Show last chart hint if available
|
|
135
|
+
function initializeLastChartHint() {
|
|
136
|
+
const lastChart = localStorage.getItem(STORAGE_KEY_LAST_CHART);
|
|
137
|
+
if (lastChart) {
|
|
138
|
+
lastChartName.textContent = lastChart;
|
|
139
|
+
lastChartHint.classList.add("visible");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
initializeLastChartHint();
|
|
143
|
+
|
|
144
|
+
// Button click handlers
|
|
145
|
+
chooseFolderBtn.addEventListener("click", () => folderInput.click());
|
|
146
|
+
chooseSngBtn.addEventListener("click", () => sngInput.click());
|
|
147
|
+
|
|
148
|
+
// Folder selection
|
|
149
|
+
folderInput.addEventListener("change", async () => {
|
|
150
|
+
const files = folderInput.files;
|
|
151
|
+
if (!files || files.length === 0) return;
|
|
152
|
+
|
|
153
|
+
setStatus("Reading chart folder...");
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Convert FileList to our format
|
|
157
|
+
const chartFiles: { fileName: string; data: Uint8Array }[] = [];
|
|
158
|
+
|
|
159
|
+
// Get the folder name from the first file's path
|
|
160
|
+
const firstFilePath = files[0].webkitRelativePath;
|
|
161
|
+
const folderName = firstFilePath.split("/")[0];
|
|
162
|
+
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
// Get relative path within the folder
|
|
165
|
+
const relativePath = file.webkitRelativePath
|
|
166
|
+
.split("/")
|
|
167
|
+
.slice(1)
|
|
168
|
+
.join("/");
|
|
169
|
+
if (!relativePath) continue; // Skip if it's the folder itself
|
|
170
|
+
|
|
171
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
172
|
+
chartFiles.push({ fileName: relativePath, data });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await processChartFiles(chartFiles, folderName);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error("Error reading folder:", error);
|
|
178
|
+
setStatus(
|
|
179
|
+
`Error: ${
|
|
180
|
+
error instanceof Error ? error.message : "Failed to read folder"
|
|
181
|
+
}`,
|
|
182
|
+
true
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// .sng file selection
|
|
188
|
+
sngInput.addEventListener("change", async () => {
|
|
189
|
+
const file = sngInput.files?.[0];
|
|
190
|
+
if (!file) return;
|
|
191
|
+
|
|
192
|
+
setStatus("Reading .sng file...");
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
196
|
+
const chartFiles = await extractSngFile(data);
|
|
197
|
+
const chartName = file.name.replace(/\.sng$/i, "");
|
|
198
|
+
await processChartFiles(chartFiles, chartName);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error("Error reading .sng file:", error);
|
|
201
|
+
setStatus(
|
|
202
|
+
`Error: ${
|
|
203
|
+
error instanceof Error ? error.message : "Failed to read .sng file"
|
|
204
|
+
}`,
|
|
205
|
+
true
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Process chart files (from folder or .sng)
|
|
211
|
+
async function processChartFiles(
|
|
212
|
+
files: { fileName: string; data: Uint8Array }[],
|
|
213
|
+
chartName: string
|
|
214
|
+
) {
|
|
215
|
+
try {
|
|
216
|
+
setStatus("Scanning chart...");
|
|
217
|
+
|
|
218
|
+
// Store for later use
|
|
219
|
+
currentChartFiles = files;
|
|
220
|
+
currentChartName = chartName;
|
|
221
|
+
|
|
222
|
+
// Scan the chart folder
|
|
223
|
+
currentScannedChart = scanChartFolder(files);
|
|
224
|
+
|
|
225
|
+
if (!currentScannedChart.playable || !currentScannedChart.notesData) {
|
|
226
|
+
setStatus("This chart has no playable tracks.", true);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Extract available tracks
|
|
231
|
+
availableTracks = [];
|
|
232
|
+
const instruments = currentScannedChart.notesData.instruments;
|
|
233
|
+
const noteCounts = currentScannedChart.notesData.noteCounts;
|
|
234
|
+
|
|
235
|
+
for (const instrument of instruments) {
|
|
236
|
+
const difficulties = noteCounts
|
|
237
|
+
.filter((nc) => nc.instrument === instrument && nc.count > 0)
|
|
238
|
+
.map((nc) => nc.difficulty);
|
|
239
|
+
|
|
240
|
+
if (difficulties.length > 0) {
|
|
241
|
+
availableTracks.push({ instrument, difficulties });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (availableTracks.length === 0) {
|
|
246
|
+
setStatus("No tracks with notes found in this chart.", true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Update UI
|
|
251
|
+
updateChartInfo(currentScannedChart);
|
|
252
|
+
populateInstrumentSelect();
|
|
253
|
+
|
|
254
|
+
// Save to localStorage
|
|
255
|
+
localStorage.setItem(STORAGE_KEY_LAST_CHART, chartName);
|
|
256
|
+
lastChartName.textContent = chartName;
|
|
257
|
+
lastChartHint.classList.add("visible");
|
|
258
|
+
|
|
259
|
+
// Try to restore last used instrument/difficulty
|
|
260
|
+
const lastInstrument = localStorage.getItem(STORAGE_KEY_LAST_INSTRUMENT);
|
|
261
|
+
const lastDifficulty = localStorage.getItem(STORAGE_KEY_LAST_DIFFICULTY);
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
lastInstrument &&
|
|
265
|
+
availableTracks.some((t) => t.instrument === lastInstrument)
|
|
266
|
+
) {
|
|
267
|
+
instrumentSelect.value = lastInstrument;
|
|
268
|
+
populateDifficultySelect();
|
|
269
|
+
|
|
270
|
+
const track = availableTracks.find(
|
|
271
|
+
(t) => t.instrument === lastInstrument
|
|
272
|
+
);
|
|
273
|
+
if (
|
|
274
|
+
lastDifficulty &&
|
|
275
|
+
track?.difficulties.includes(lastDifficulty as Difficulty)
|
|
276
|
+
) {
|
|
277
|
+
difficultySelect.value = lastDifficulty;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
updateLoadButtonState();
|
|
282
|
+
setStatus(
|
|
283
|
+
`Loaded: ${chartName} - ${availableTracks.length} instrument(s) available`,
|
|
284
|
+
false,
|
|
285
|
+
true
|
|
286
|
+
);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error("Error processing chart:", error);
|
|
289
|
+
setStatus(
|
|
290
|
+
`Error: ${
|
|
291
|
+
error instanceof Error ? error.message : "Failed to process chart"
|
|
292
|
+
}`,
|
|
293
|
+
true
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update chart info display
|
|
299
|
+
function updateChartInfo(chart: ScannedChart) {
|
|
300
|
+
songNameEl.textContent = chart.name || currentChartName || "Unknown Song";
|
|
301
|
+
songArtistEl.textContent = chart.artist || "Unknown Artist";
|
|
302
|
+
songCharterEl.textContent = chart.charter ? `Charter: ${chart.charter}` : "";
|
|
303
|
+
|
|
304
|
+
if (chart.song_length) {
|
|
305
|
+
const minutes = Math.floor(chart.song_length / 60000);
|
|
306
|
+
const seconds = Math.floor((chart.song_length % 60000) / 1000);
|
|
307
|
+
songLengthEl.textContent = `Length: ${minutes}:${seconds
|
|
308
|
+
.toString()
|
|
309
|
+
.padStart(2, "0")}`;
|
|
310
|
+
} else {
|
|
311
|
+
songLengthEl.textContent = "";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
chartInfoEl.classList.add("visible");
|
|
315
|
+
trackOptionsEl.classList.add("visible");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Populate instrument dropdown
|
|
319
|
+
function populateInstrumentSelect() {
|
|
320
|
+
instrumentSelect.innerHTML = "";
|
|
321
|
+
|
|
322
|
+
for (const track of availableTracks) {
|
|
323
|
+
const option = document.createElement("option");
|
|
324
|
+
option.value = track.instrument;
|
|
325
|
+
option.textContent = instrumentNames[track.instrument] || track.instrument;
|
|
326
|
+
instrumentSelect.appendChild(option);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
instrumentSelect.disabled = false;
|
|
330
|
+
populateDifficultySelect();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Populate difficulty dropdown based on selected instrument
|
|
334
|
+
function populateDifficultySelect() {
|
|
335
|
+
const selectedInstrument = instrumentSelect.value as Instrument;
|
|
336
|
+
const track = availableTracks.find(
|
|
337
|
+
(t) => t.instrument === selectedInstrument
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
difficultySelect.innerHTML = "";
|
|
341
|
+
|
|
342
|
+
if (track) {
|
|
343
|
+
// Sort difficulties: expert, hard, medium, easy
|
|
344
|
+
const sortOrder: Difficulty[] = ["expert", "hard", "medium", "easy"];
|
|
345
|
+
const sortedDifficulties = track.difficulties.sort(
|
|
346
|
+
(a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b)
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
for (const difficulty of sortedDifficulties) {
|
|
350
|
+
const option = document.createElement("option");
|
|
351
|
+
option.value = difficulty;
|
|
352
|
+
option.textContent = difficultyNames[difficulty] || difficulty;
|
|
353
|
+
difficultySelect.appendChild(option);
|
|
354
|
+
}
|
|
355
|
+
difficultySelect.disabled = false;
|
|
356
|
+
} else {
|
|
357
|
+
difficultySelect.disabled = true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
updateLoadButtonState();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Update load button state
|
|
364
|
+
function updateLoadButtonState() {
|
|
365
|
+
loadBtn.disabled =
|
|
366
|
+
!instrumentSelect.value ||
|
|
367
|
+
!difficultySelect.value ||
|
|
368
|
+
currentChartFiles.length === 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Instrument selection change
|
|
372
|
+
instrumentSelect.addEventListener("change", () => {
|
|
373
|
+
populateDifficultySelect();
|
|
374
|
+
localStorage.setItem(STORAGE_KEY_LAST_INSTRUMENT, instrumentSelect.value);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Difficulty selection change
|
|
378
|
+
difficultySelect.addEventListener("change", () => {
|
|
379
|
+
localStorage.setItem(STORAGE_KEY_LAST_DIFFICULTY, difficultySelect.value);
|
|
380
|
+
updateLoadButtonState();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Load preview
|
|
384
|
+
loadBtn.addEventListener("click", async () => {
|
|
385
|
+
if (currentChartFiles.length === 0 || !currentScannedChart) {
|
|
386
|
+
setStatus("Please select a chart first.", true);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const instrument = instrumentSelect.value as Instrument;
|
|
391
|
+
const difficulty = difficultySelect.value as Difficulty;
|
|
392
|
+
|
|
393
|
+
if (!instrument || !difficulty) {
|
|
394
|
+
setStatus("Please select an instrument and difficulty.", true);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
setStatus("Loading chart preview...");
|
|
400
|
+
loadBtn.disabled = true;
|
|
401
|
+
|
|
402
|
+
// Dispose previous player state
|
|
403
|
+
player.dispose();
|
|
404
|
+
|
|
405
|
+
// Find chart file
|
|
406
|
+
const chartFile = currentChartFiles.find(
|
|
407
|
+
(f) =>
|
|
408
|
+
f.fileName.toLowerCase().endsWith(".chart") ||
|
|
409
|
+
f.fileName.toLowerCase().endsWith(".mid")
|
|
410
|
+
);
|
|
411
|
+
if (!chartFile) {
|
|
412
|
+
setStatus("No .chart or .mid file found in the chart folder.", true);
|
|
413
|
+
loadBtn.disabled = false;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const format = chartFile.fileName.toLowerCase().endsWith(".mid")
|
|
418
|
+
? "mid"
|
|
419
|
+
: "chart";
|
|
420
|
+
|
|
421
|
+
// Build chart modifiers from scanned chart
|
|
422
|
+
const modifiers = {
|
|
423
|
+
song_length: currentScannedChart.song_length || 0,
|
|
424
|
+
hopo_frequency: currentScannedChart.hopo_frequency || 0,
|
|
425
|
+
eighthnote_hopo: currentScannedChart.eighthnote_hopo || false,
|
|
426
|
+
multiplier_note: currentScannedChart.multiplier_note || 0,
|
|
427
|
+
sustain_cutoff_threshold:
|
|
428
|
+
currentScannedChart.sustain_cutoff_threshold ?? -1,
|
|
429
|
+
chord_snap_threshold: currentScannedChart.chord_snap_threshold || 0,
|
|
430
|
+
five_lane_drums: currentScannedChart.five_lane_drums || false,
|
|
431
|
+
pro_drums: currentScannedChart.pro_drums || false,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// Parse chart
|
|
435
|
+
const parsedChart = parseChartFile(chartFile.data, format, modifiers);
|
|
436
|
+
|
|
437
|
+
// Check if the track exists
|
|
438
|
+
const track = parsedChart.trackData.find(
|
|
439
|
+
(t) => t.instrument === instrument && t.difficulty === difficulty
|
|
440
|
+
);
|
|
441
|
+
if (!track) {
|
|
442
|
+
setStatus(
|
|
443
|
+
`No ${difficulty} ${instrument} track found in this chart.`,
|
|
444
|
+
true
|
|
445
|
+
);
|
|
446
|
+
loadBtn.disabled = false;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Find audio files
|
|
451
|
+
const audioExtensions = [".ogg", ".mp3", ".wav", ".opus"];
|
|
452
|
+
const audioFiles: Uint8Array[] = [];
|
|
453
|
+
for (const file of currentChartFiles) {
|
|
454
|
+
const ext = file.fileName
|
|
455
|
+
.toLowerCase()
|
|
456
|
+
.slice(file.fileName.lastIndexOf("."));
|
|
457
|
+
if (audioExtensions.includes(ext)) {
|
|
458
|
+
audioFiles.push(file.data);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Load textures
|
|
463
|
+
setStatus("Loading textures...");
|
|
464
|
+
const instrumentType = getInstrumentType(instrument);
|
|
465
|
+
const textures = await ChartPreview.loadTextures(instrumentType);
|
|
466
|
+
|
|
467
|
+
// Calculate audio length
|
|
468
|
+
const lastNote = parsedChart.trackData
|
|
469
|
+
.flatMap((t) => t.noteEventGroups.flat())
|
|
470
|
+
.reduce((max, note) => Math.max(max, note.msTime + note.msLength), 0);
|
|
471
|
+
const audioLengthMs = currentScannedChart.song_length || lastNote + 5000;
|
|
472
|
+
|
|
473
|
+
// Get start delay from chart metadata
|
|
474
|
+
const startDelayMs =
|
|
475
|
+
(currentScannedChart.delay || 0) +
|
|
476
|
+
(currentScannedChart.chart_offset || 0) * 1000;
|
|
477
|
+
|
|
478
|
+
// Calculate initial seek position from preview_start_time if available
|
|
479
|
+
const initialSeekPercent = currentScannedChart.preview_start_time
|
|
480
|
+
? currentScannedChart.preview_start_time / audioLengthMs
|
|
481
|
+
: 0;
|
|
482
|
+
|
|
483
|
+
// Load the chart into the Web Component player
|
|
484
|
+
setStatus("Creating preview...");
|
|
485
|
+
await player.loadChart({
|
|
486
|
+
parsedChart,
|
|
487
|
+
textures,
|
|
488
|
+
audioFiles,
|
|
489
|
+
instrument,
|
|
490
|
+
difficulty,
|
|
491
|
+
startDelayMs,
|
|
492
|
+
audioLengthMs,
|
|
493
|
+
initialSeekPercent,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Save selections
|
|
497
|
+
localStorage.setItem(STORAGE_KEY_LAST_INSTRUMENT, instrument);
|
|
498
|
+
localStorage.setItem(STORAGE_KEY_LAST_DIFFICULTY, difficulty);
|
|
499
|
+
|
|
500
|
+
loadBtn.disabled = false;
|
|
501
|
+
|
|
502
|
+
const displayName = currentScannedChart.name || currentChartName;
|
|
503
|
+
setStatus(
|
|
504
|
+
`Loaded: ${displayName} - ${instrumentNames[instrument]} (${difficultyNames[difficulty]})`,
|
|
505
|
+
false,
|
|
506
|
+
true
|
|
507
|
+
);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error("Error loading preview:", error);
|
|
510
|
+
setStatus(
|
|
511
|
+
`Error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
512
|
+
true
|
|
513
|
+
);
|
|
514
|
+
loadBtn.disabled = false;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
function setStatus(message: string, isError = false, isSuccess = false) {
|
|
519
|
+
statusEl.textContent = message;
|
|
520
|
+
statusEl.className = isError
|
|
521
|
+
? "status error"
|
|
522
|
+
: isSuccess
|
|
523
|
+
? "status success"
|
|
524
|
+
: "status";
|
|
525
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
11
|
+
"paths": {
|
|
12
|
+
"chart-preview": ["../src/index.ts"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["./**/*"]
|
|
16
|
+
}
|