@jxgame2020/dsh-token-quota 0.1.26 → 0.1.28
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/lib/client.js +505 -305
- package/lib/index.js +10 -2
- package/lib/types/client/index.js +6 -1
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/music.d.ts +64 -49
- package/lib/types/client/music.js +314 -255
- package/lib/types/client/store.js +1 -1
- package/lib/types/index.js +15 -1
- package/lib/types/types.js +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -39,7 +39,7 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
dimWhenIdle: false,
|
|
40
40
|
musicEnabled: false,
|
|
41
41
|
musicVolume: .5,
|
|
42
|
-
musicStyle: "
|
|
42
|
+
musicStyle: "pentatonic",
|
|
43
43
|
musicOnlyCurrentSession: true,
|
|
44
44
|
upgrade: null,
|
|
45
45
|
upgradeDismissed: false,
|
|
@@ -95,7 +95,7 @@ window.__ModuleLoader__.load({
|
|
|
95
95
|
d.musicOnlyCurrentSession = only;
|
|
96
96
|
},
|
|
97
97
|
setUpgrade: (d, upgrade) => {
|
|
98
|
-
const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.
|
|
98
|
+
const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.28" ? upgrade : null;
|
|
99
99
|
if (d.upgrade === null || d.upgrade.latestVersion !== normalized?.latestVersion) d.upgradeDismissed = false;
|
|
100
100
|
d.upgrade = normalized;
|
|
101
101
|
},
|
|
@@ -201,92 +201,92 @@ window.__ModuleLoader__.load({
|
|
|
201
201
|
document.head.appendChild(tag);
|
|
202
202
|
}
|
|
203
203
|
var TokenQuotaPanel_module_css_default = {
|
|
204
|
-
"
|
|
205
|
-
"body": "NPLLIq_body",
|
|
206
|
-
"dialogLabel": "NPLLIq_dialogLabel",
|
|
207
|
-
"upgradeBanner": "NPLLIq_upgradeBanner",
|
|
208
|
-
"dialogActions": "NPLLIq_dialogActions",
|
|
209
|
-
"currentBadge": "NPLLIq_currentBadge",
|
|
210
|
-
"dialogBody": "NPLLIq_dialogBody",
|
|
211
|
-
"notice": "NPLLIq_notice",
|
|
212
|
-
"rowMeta": "NPLLIq_rowMeta",
|
|
204
|
+
"gearBtn": "NPLLIq_gearBtn",
|
|
213
205
|
"title": "NPLLIq_title",
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"bar": "NPLLIq_bar",
|
|
217
|
-
"noticeError": "NPLLIq_noticeError",
|
|
218
|
-
"tab": "NPLLIq_tab",
|
|
219
|
-
"logScroll": "NPLLIq_logScroll",
|
|
206
|
+
"checkErrorHint": "NPLLIq_checkErrorHint",
|
|
207
|
+
"collapse": "NPLLIq_collapse",
|
|
220
208
|
"dialogTitle": "NPLLIq_dialogTitle",
|
|
221
|
-
"selectBtn": "NPLLIq_selectBtn",
|
|
222
|
-
"monitorList": "NPLLIq_monitorList",
|
|
223
|
-
"switch": "NPLLIq_switch",
|
|
224
|
-
"monitorRow": "NPLLIq_monitorRow",
|
|
225
|
-
"resetSelect": "NPLLIq_resetSelect",
|
|
226
|
-
"dialogBtn": "NPLLIq_dialogBtn",
|
|
227
|
-
"logHeader": "NPLLIq_logHeader",
|
|
228
|
-
"panelDim": "NPLLIq_panelDim",
|
|
229
|
-
"logToolbar": "NPLLIq_logToolbar",
|
|
230
|
-
"upgradeCommands": "NPLLIq_upgradeCommands",
|
|
231
|
-
"upgradeHint": "NPLLIq_upgradeHint",
|
|
232
209
|
"logDayCol": "NPLLIq_logDayCol",
|
|
233
|
-
"
|
|
234
|
-
"tabLabel": "NPLLIq_tabLabel",
|
|
235
|
-
"dialogClose": "NPLLIq_dialogClose",
|
|
210
|
+
"upgradeBannerText": "NPLLIq_upgradeBannerText",
|
|
236
211
|
"fillOver": "NPLLIq_fillOver",
|
|
212
|
+
"monitorHint": "NPLLIq_monitorHint",
|
|
213
|
+
"logClearDialog": "NPLLIq_logClearDialog",
|
|
214
|
+
"dialogClose": "NPLLIq_dialogClose",
|
|
215
|
+
"monitorName": "NPLLIq_monitorName",
|
|
216
|
+
"dialog": "NPLLIq_dialog",
|
|
217
|
+
"logClearBtn": "NPLLIq_logClearBtn",
|
|
218
|
+
"rowMeta": "NPLLIq_rowMeta",
|
|
219
|
+
"logSearch": "NPLLIq_logSearch",
|
|
220
|
+
"selectBtn": "NPLLIq_selectBtn",
|
|
221
|
+
"panelDim": "NPLLIq_panelDim",
|
|
222
|
+
"upgradeBanner": "NPLLIq_upgradeBanner",
|
|
237
223
|
"rowHeader": "NPLLIq_rowHeader",
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
224
|
+
"checkBtn": "NPLLIq_checkBtn",
|
|
225
|
+
"dialogHint": "NPLLIq_dialogHint",
|
|
226
|
+
"logPagerInfo": "NPLLIq_logPagerInfo",
|
|
241
227
|
"logModelCol": "NPLLIq_logModelCol",
|
|
228
|
+
"upgradeClose": "NPLLIq_upgradeClose",
|
|
229
|
+
"resetRow": "NPLLIq_resetRow",
|
|
230
|
+
"dialogActions": "NPLLIq_dialogActions",
|
|
242
231
|
"upgradeCommandRow": "NPLLIq_upgradeCommandRow",
|
|
243
|
-
"
|
|
244
|
-
"
|
|
232
|
+
"currentBadge": "NPLLIq_currentBadge",
|
|
233
|
+
"upgradeHint": "NPLLIq_upgradeHint",
|
|
234
|
+
"dialogHeader": "NPLLIq_dialogHeader",
|
|
235
|
+
"logScroll": "NPLLIq_logScroll",
|
|
236
|
+
"versionNew": "NPLLIq_versionNew",
|
|
237
|
+
"header": "NPLLIq_header",
|
|
238
|
+
"logToolbar": "NPLLIq_logToolbar",
|
|
239
|
+
"logPageSizeSelect": "NPLLIq_logPageSizeSelect",
|
|
240
|
+
"monitorRow": "NPLLIq_monitorRow",
|
|
241
|
+
"dialogSection": "NPLLIq_dialogSection",
|
|
242
|
+
"logUsedCol": "NPLLIq_logUsedCol",
|
|
243
|
+
"subtitle": "NPLLIq_subtitle",
|
|
244
|
+
"tabLabel": "NPLLIq_tabLabel",
|
|
245
|
+
"dialogLabel": "NPLLIq_dialogLabel",
|
|
246
|
+
"row": "NPLLIq_row",
|
|
247
|
+
"dialogBtn": "NPLLIq_dialogBtn",
|
|
248
|
+
"dialogBody": "NPLLIq_dialogBody",
|
|
249
|
+
"version": "NPLLIq_version",
|
|
245
250
|
"checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
|
|
251
|
+
"copyBtnDone": "NPLLIq_copyBtnDone",
|
|
252
|
+
"logHeader": "NPLLIq_logHeader",
|
|
253
|
+
"headerText": "NPLLIq_headerText",
|
|
246
254
|
"fillWarn": "NPLLIq_fillWarn",
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"settingsBtnActive": "NPLLIq_settingsBtnActive",
|
|
251
|
-
"radioRow": "NPLLIq_radioRow",
|
|
252
|
-
"logClearBtn": "NPLLIq_logClearBtn",
|
|
253
|
-
"logPageSizeSelect": "NPLLIq_logPageSizeSelect",
|
|
254
|
-
"dialogHint": "NPLLIq_dialogHint",
|
|
255
|
-
"logClearDialog": "NPLLIq_logClearDialog",
|
|
256
|
-
"monitorName": "NPLLIq_monitorName",
|
|
255
|
+
"upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
|
|
256
|
+
"fullNotice": "NPLLIq_fullNotice",
|
|
257
|
+
"upgradeCommand": "NPLLIq_upgradeCommand",
|
|
257
258
|
"dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
|
|
258
|
-
"
|
|
259
|
+
"logPagerPages": "NPLLIq_logPagerPages",
|
|
260
|
+
"upgradeCommands": "NPLLIq_upgradeCommands",
|
|
261
|
+
"save": "NPLLIq_save",
|
|
259
262
|
"logClearDate": "NPLLIq_logClearDate",
|
|
260
|
-
"
|
|
261
|
-
"
|
|
263
|
+
"settingsBtn": "NPLLIq_settingsBtn",
|
|
264
|
+
"logPager": "NPLLIq_logPager",
|
|
262
265
|
"dialogOverlay": "NPLLIq_dialogOverlay",
|
|
263
|
-
"
|
|
266
|
+
"logDialog": "NPLLIq_logDialog",
|
|
267
|
+
"headerActions": "NPLLIq_headerActions",
|
|
268
|
+
"monitorList": "NPLLIq_monitorList",
|
|
269
|
+
"bar": "NPLLIq_bar",
|
|
264
270
|
"input": "NPLLIq_input",
|
|
265
|
-
"
|
|
266
|
-
"row": "NPLLIq_row",
|
|
267
|
-
"monitorHint": "NPLLIq_monitorHint",
|
|
268
|
-
"header": "NPLLIq_header",
|
|
269
|
-
"logPagerPages": "NPLLIq_logPagerPages",
|
|
270
|
-
"versionNew": "NPLLIq_versionNew",
|
|
271
|
-
"subtitle": "NPLLIq_subtitle",
|
|
272
|
-
"version": "NPLLIq_version",
|
|
273
|
-
"fullNotice": "NPLLIq_fullNotice",
|
|
274
|
-
"resetRow": "NPLLIq_resetRow",
|
|
275
|
-
"logTable": "NPLLIq_logTable",
|
|
271
|
+
"noticeError": "NPLLIq_noticeError",
|
|
276
272
|
"fillIdle": "NPLLIq_fillIdle",
|
|
277
|
-
"
|
|
278
|
-
"tabIcon": "NPLLIq_tabIcon",
|
|
273
|
+
"copyBtn": "NPLLIq_copyBtn",
|
|
279
274
|
"logPagerControls": "NPLLIq_logPagerControls",
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"dialogSection": "NPLLIq_dialogSection",
|
|
275
|
+
"logPagerBtn": "NPLLIq_logPagerBtn",
|
|
276
|
+
"radioRow": "NPLLIq_radioRow",
|
|
277
|
+
"rowName": "NPLLIq_rowName",
|
|
284
278
|
"controls": "NPLLIq_controls",
|
|
285
|
-
"
|
|
279
|
+
"resetSelect": "NPLLIq_resetSelect",
|
|
280
|
+
"tab": "NPLLIq_tab",
|
|
281
|
+
"notice": "NPLLIq_notice",
|
|
282
|
+
"body": "NPLLIq_body",
|
|
283
|
+
"tabIcon": "NPLLIq_tabIcon",
|
|
286
284
|
"panelDimStrong": "NPLLIq_panelDimStrong",
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
285
|
+
"switch": "NPLLIq_switch",
|
|
286
|
+
"settingsBtnActive": "NPLLIq_settingsBtnActive",
|
|
287
|
+
"panel": "NPLLIq_panel",
|
|
288
|
+
"checkUpdatesRow": "NPLLIq_checkUpdatesRow",
|
|
289
|
+
"logTable": "NPLLIq_logTable"
|
|
290
290
|
};
|
|
291
291
|
//#endregion
|
|
292
292
|
//#region lib/types/client/TokenQuotaPanel.js
|
|
@@ -592,13 +592,13 @@ window.__ModuleLoader__.load({
|
|
|
592
592
|
title: t("upgradeAvailableHint"),
|
|
593
593
|
children: [
|
|
594
594
|
"v",
|
|
595
|
-
"0.1.
|
|
595
|
+
"0.1.28",
|
|
596
596
|
" → v",
|
|
597
597
|
upgrade.latestVersion
|
|
598
598
|
]
|
|
599
599
|
}) : (0, react_jsx_runtime.jsxs)("span", {
|
|
600
600
|
className: TokenQuotaPanel_module_css_default.version,
|
|
601
|
-
children: ["v", "0.1.
|
|
601
|
+
children: ["v", "0.1.28"]
|
|
602
602
|
})]
|
|
603
603
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
604
604
|
className: TokenQuotaPanel_module_css_default.subtitle,
|
|
@@ -1232,23 +1232,32 @@ window.__ModuleLoader__.load({
|
|
|
1232
1232
|
//#endregion
|
|
1233
1233
|
//#region lib/types/client/music.js
|
|
1234
1234
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1235
|
+
* 古风 live-composed soundtrack driven by host actions.
|
|
1236
|
+
*
|
|
1237
|
+
* Design: when a request starts the engine composes a *piece* — a guzheng-like
|
|
1238
|
+
* plucked-string voice (Web Audio synthesis; the MIDI output is switched to
|
|
1239
|
+
* the Koto program so hardware synths match). The piece plays phrase after
|
|
1240
|
+
* phrase so it stays alive while the model thinks or streams; incoming events
|
|
1241
|
+
* steer the piece instead of interrupting it: a step change rotates the mode
|
|
1242
|
+
* centre and inserts a fast 过门 (fill); tool calls get a high plink; the
|
|
1243
|
+
* ending gets a dizi-like breath note and a descending glissando.
|
|
1237
1244
|
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1245
|
+
* **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
|
|
1246
|
+
* or a fixed-duration breath note — there is no drone/pad oscillator that
|
|
1247
|
+
* depends on a later stop event. A lost SSE event can at worst leave the
|
|
1248
|
+
* phrase chain running, and the stream watchdog below stops that too, so a
|
|
1249
|
+
* hanging "呜呜" hum is physically impossible.
|
|
1243
1250
|
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
* (
|
|
1247
|
-
*
|
|
1251
|
+
* Anti-runaway guards:
|
|
1252
|
+
* - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
|
|
1253
|
+
* engine stops (a dead stream means no stop event will ever come),
|
|
1254
|
+
* - every MIDI NoteOn is registered and swept; stops also send
|
|
1255
|
+
* All Notes Off (CC 123),
|
|
1256
|
+
* - every Web Audio oscillator is explicitly stopped at envelope end.
|
|
1248
1257
|
*
|
|
1249
1258
|
* @module @jxgame2020/dsh-token-quota/music
|
|
1250
1259
|
*/
|
|
1251
|
-
/** Scale degrees in semitones from the
|
|
1260
|
+
/** Scale degrees in semitones from the (current) tonic. */
|
|
1252
1261
|
const SCALES = {
|
|
1253
1262
|
major: [
|
|
1254
1263
|
0,
|
|
@@ -1277,10 +1286,11 @@ window.__ModuleLoader__.load({
|
|
|
1277
1286
|
]
|
|
1278
1287
|
};
|
|
1279
1288
|
/**
|
|
1280
|
-
*
|
|
1281
|
-
* step advance
|
|
1289
|
+
* Tonic offsets for the progression — the piece rotates its mode centre on
|
|
1290
|
+
* every step advance. For pentatonic this is 宫 → 羽 → 徵 → 商 (mode
|
|
1291
|
+
* rotation on one scale), which is the classic gufeng colour.
|
|
1282
1292
|
*/
|
|
1283
|
-
const
|
|
1293
|
+
const PROGRESSION = {
|
|
1284
1294
|
major: [
|
|
1285
1295
|
0,
|
|
1286
1296
|
9,
|
|
@@ -1289,56 +1299,189 @@ window.__ModuleLoader__.load({
|
|
|
1289
1299
|
],
|
|
1290
1300
|
minor: [
|
|
1291
1301
|
0,
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1302
|
+
8,
|
|
1303
|
+
3,
|
|
1304
|
+
10
|
|
1295
1305
|
],
|
|
1296
1306
|
pentatonic: [
|
|
1297
1307
|
0,
|
|
1308
|
+
9,
|
|
1298
1309
|
7,
|
|
1299
|
-
|
|
1300
|
-
9
|
|
1310
|
+
5
|
|
1301
1311
|
]
|
|
1302
1312
|
};
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1313
|
+
/**
|
|
1314
|
+
* Phrase bank — pentatonic-degree melodies that also read fine on 7-note
|
|
1315
|
+
* scales. Degrees index the current scale (values ≥ scale length wrap up an
|
|
1316
|
+
* octave).
|
|
1317
|
+
*/
|
|
1318
|
+
const PHRASES = [
|
|
1319
|
+
[
|
|
1320
|
+
{
|
|
1321
|
+
deg: 0,
|
|
1322
|
+
beats: 2
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
deg: 1,
|
|
1326
|
+
beats: 1
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
deg: 2,
|
|
1330
|
+
beats: 1
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
deg: 3,
|
|
1334
|
+
beats: 2
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
deg: 2,
|
|
1338
|
+
beats: 1
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
deg: 1,
|
|
1342
|
+
beats: 1
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
deg: 0,
|
|
1346
|
+
beats: 3
|
|
1347
|
+
}
|
|
1309
1348
|
],
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1349
|
+
[
|
|
1350
|
+
{
|
|
1351
|
+
deg: 5,
|
|
1352
|
+
beats: 1
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
deg: 4,
|
|
1356
|
+
beats: 1
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
deg: 3,
|
|
1360
|
+
beats: 2
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
deg: 2,
|
|
1364
|
+
beats: 1
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
deg: 1,
|
|
1368
|
+
beats: 2
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
deg: 0,
|
|
1372
|
+
beats: 3
|
|
1373
|
+
}
|
|
1315
1374
|
],
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1375
|
+
[
|
|
1376
|
+
{
|
|
1377
|
+
deg: 2,
|
|
1378
|
+
beats: 1
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
deg: 3,
|
|
1382
|
+
beats: 1
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
deg: 4,
|
|
1386
|
+
beats: 2
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
deg: 5,
|
|
1390
|
+
beats: 1
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
deg: 4,
|
|
1394
|
+
beats: 1
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
deg: 3,
|
|
1398
|
+
beats: 2
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
deg: 1,
|
|
1402
|
+
beats: 2
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
deg: 0,
|
|
1406
|
+
beats: 2
|
|
1407
|
+
}
|
|
1408
|
+
],
|
|
1409
|
+
[
|
|
1410
|
+
{
|
|
1411
|
+
deg: 4,
|
|
1412
|
+
beats: 2
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
deg: 3,
|
|
1416
|
+
beats: 1
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
deg: 2,
|
|
1420
|
+
beats: 1
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
deg: 3,
|
|
1424
|
+
beats: 2
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
deg: 1,
|
|
1428
|
+
beats: 1
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
deg: 2,
|
|
1432
|
+
beats: 1
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
deg: 0,
|
|
1436
|
+
beats: 4
|
|
1437
|
+
}
|
|
1321
1438
|
]
|
|
1322
|
-
|
|
1439
|
+
];
|
|
1440
|
+
/** 过门 — a fast run played when a step (new phase) starts. */
|
|
1441
|
+
const FILL = [
|
|
1442
|
+
{
|
|
1443
|
+
deg: 0,
|
|
1444
|
+
beats: .5
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
deg: 1,
|
|
1448
|
+
beats: .5
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
deg: 2,
|
|
1452
|
+
beats: .5
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
deg: 3,
|
|
1456
|
+
beats: .5
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
deg: 4,
|
|
1460
|
+
beats: .5
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
deg: 5,
|
|
1464
|
+
beats: .5
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
deg: 4,
|
|
1468
|
+
beats: .5
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
deg: 3,
|
|
1472
|
+
beats: .5
|
|
1473
|
+
}
|
|
1474
|
+
];
|
|
1323
1475
|
/** Turn a MIDI note number into a frequency (equal temperament, A4 = 440). */
|
|
1324
1476
|
function midiToFreq(midi) {
|
|
1325
1477
|
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
1326
1478
|
}
|
|
1327
|
-
/**
|
|
1328
|
-
|
|
1329
|
-
return [
|
|
1330
|
-
0,
|
|
1331
|
-
quality === "major" ? 4 : quality === "minor" ? 3 : 3,
|
|
1332
|
-
quality === "dim" ? 6 : 7
|
|
1333
|
-
];
|
|
1334
|
-
}
|
|
1479
|
+
/** General-MIDI program closest to a guzheng: 108 Koto (0-indexed 106). */
|
|
1480
|
+
const GM_PROGRAM_KOTO = 106;
|
|
1335
1481
|
/**
|
|
1336
|
-
* Live-composed
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
* (the panel switch), and only produces sound while `enabled` is true AND a
|
|
1340
|
-
* request is in flight (pad + bass + melody are gated by `startPad` /
|
|
1341
|
-
* `stopPad`).
|
|
1482
|
+
* Live-composed gufeng engine. Idle until `ensureStarted()` runs inside a
|
|
1483
|
+
* user gesture; silent while `enabled` is false; the piece runs only between
|
|
1484
|
+
* `request/header` (start) and `assistant/message` / `turn/end` (stop).
|
|
1342
1485
|
*/
|
|
1343
1486
|
var TokenQuotaMusic = class TokenQuotaMusic {
|
|
1344
1487
|
audio = null;
|
|
@@ -1346,32 +1489,29 @@ window.__ModuleLoader__.load({
|
|
|
1346
1489
|
midiDetected = false;
|
|
1347
1490
|
enabled = false;
|
|
1348
1491
|
volume = .5;
|
|
1349
|
-
style = "
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
melodyDegree = 0;
|
|
1361
|
-
currentRoot = 60;
|
|
1362
|
-
currentQuality = "major";
|
|
1363
|
-
/** Base MIDI note of the key. C4 = middle C. */
|
|
1492
|
+
style = "pentatonic";
|
|
1493
|
+
playing = false;
|
|
1494
|
+
tonicIndex = 0;
|
|
1495
|
+
phraseIndex = 0;
|
|
1496
|
+
/** True when the next piece start should play the full intro glissando. */
|
|
1497
|
+
introPending = true;
|
|
1498
|
+
phraseTimer;
|
|
1499
|
+
lastStreamActivity = 0;
|
|
1500
|
+
watchdogTimer;
|
|
1501
|
+
activeMidiNotes = /* @__PURE__ */ new Set();
|
|
1502
|
+
/** Base MIDI note of the tonic. C4 = middle C. */
|
|
1364
1503
|
static BASE_MIDI = 60;
|
|
1365
|
-
/** Beat length in ms —
|
|
1366
|
-
static BEAT_MS =
|
|
1504
|
+
/** Beat length in ms — the phrase rhythm is measured in these. */
|
|
1505
|
+
static BEAT_MS = 480;
|
|
1506
|
+
/** Stop everything if the SSE stream goes silent this long. */
|
|
1507
|
+
static STREAM_TIMEOUT_MS = 12e3;
|
|
1367
1508
|
/** True once the audio context has been started (needs a user gesture). */
|
|
1368
1509
|
get started() {
|
|
1369
1510
|
return this.audio !== null;
|
|
1370
1511
|
}
|
|
1371
1512
|
/**
|
|
1372
|
-
* Start the audio context and
|
|
1373
|
-
*
|
|
1374
|
-
* AudioContext can resume. Safe to call multiple times.
|
|
1513
|
+
* Start the audio context and probe for a MIDI output (once). Must be
|
|
1514
|
+
* called inside a user gesture so the AudioContext can resume.
|
|
1375
1515
|
*/
|
|
1376
1516
|
async ensureStarted() {
|
|
1377
1517
|
if (this.audio !== null) return true;
|
|
@@ -1391,10 +1531,19 @@ window.__ModuleLoader__.load({
|
|
|
1391
1531
|
this.midiOutput = output;
|
|
1392
1532
|
break;
|
|
1393
1533
|
}
|
|
1534
|
+
try {
|
|
1535
|
+
this.midiOutput?.send([192, GM_PROGRAM_KOTO]);
|
|
1536
|
+
} catch {}
|
|
1394
1537
|
} catch {
|
|
1395
1538
|
this.midiOutput = null;
|
|
1396
1539
|
}
|
|
1397
1540
|
}
|
|
1541
|
+
if (this.watchdogTimer === void 0) {
|
|
1542
|
+
this.lastStreamActivity = Date.now();
|
|
1543
|
+
this.watchdogTimer = setInterval(() => {
|
|
1544
|
+
if (this.playing && Date.now() - this.lastStreamActivity > TokenQuotaMusic.STREAM_TIMEOUT_MS) this.stopPiece(false);
|
|
1545
|
+
}, 4e3);
|
|
1546
|
+
}
|
|
1398
1547
|
return true;
|
|
1399
1548
|
}
|
|
1400
1549
|
setEnabled(value) {
|
|
@@ -1403,222 +1552,267 @@ window.__ModuleLoader__.load({
|
|
|
1403
1552
|
}
|
|
1404
1553
|
setVolume(value) {
|
|
1405
1554
|
this.volume = Math.max(0, Math.min(1, value));
|
|
1406
|
-
if (this.padGain !== null && this.audio !== null) this.padGain.gain.setValueAtTime(this.volume * this.padTarget, this.audio.currentTime);
|
|
1407
1555
|
}
|
|
1408
1556
|
setStyle(value) {
|
|
1409
1557
|
this.style = value;
|
|
1410
|
-
this.melodyDegree = 0;
|
|
1411
|
-
this.melodyDirection = 1;
|
|
1412
1558
|
}
|
|
1413
|
-
/**
|
|
1559
|
+
/** Feed the engine one SSE activity timestamp (action or heartbeat). */
|
|
1560
|
+
markStreamActivity() {
|
|
1561
|
+
this.lastStreamActivity = Date.now();
|
|
1562
|
+
}
|
|
1563
|
+
/** Handle one incoming host action and steer the piece. */
|
|
1414
1564
|
onAction(action) {
|
|
1415
1565
|
if (!this.enabled || this.audio === null) return;
|
|
1416
1566
|
const base = TokenQuotaMusic.BASE_MIDI;
|
|
1417
1567
|
switch (action.type) {
|
|
1418
1568
|
case "turn/start":
|
|
1419
|
-
this.
|
|
1420
|
-
this.
|
|
1421
|
-
this.
|
|
1422
|
-
this.
|
|
1423
|
-
this.currentRoot = base;
|
|
1424
|
-
this.currentQuality = CHORD_QUALITIES[this.style][0] ?? "major";
|
|
1425
|
-
this.playChord(base, this.currentQuality, 1.6, .6);
|
|
1569
|
+
this.stopPiece(false);
|
|
1570
|
+
this.tonicIndex = 0;
|
|
1571
|
+
this.phraseIndex = 0;
|
|
1572
|
+
this.introPending = true;
|
|
1426
1573
|
break;
|
|
1427
|
-
case "
|
|
1428
|
-
|
|
1429
|
-
this.
|
|
1430
|
-
const root = base + (CHORD_PROGRESSIONS[this.style][this.chordIndex] ?? 0);
|
|
1431
|
-
const quality = CHORD_QUALITIES[this.style][this.chordIndex] ?? "major";
|
|
1432
|
-
this.currentRoot = root;
|
|
1433
|
-
this.currentQuality = quality;
|
|
1434
|
-
if (this.padActive) this.crossfadePad(root, quality);
|
|
1574
|
+
case "request/header":
|
|
1575
|
+
if (!this.playing) this.startPiece();
|
|
1576
|
+
else this.pluck(base + 24, .3);
|
|
1435
1577
|
break;
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
this.
|
|
1439
|
-
this.
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1578
|
+
case "step/start": {
|
|
1579
|
+
const prog = PROGRESSION[this.style];
|
|
1580
|
+
this.tonicIndex = (this.tonicIndex + 1) % prog.length;
|
|
1581
|
+
if (this.playing) {
|
|
1582
|
+
const newRoot = base + (prog[this.tonicIndex] ?? 0);
|
|
1583
|
+
this.insertFill(newRoot);
|
|
1584
|
+
}
|
|
1443
1585
|
break;
|
|
1444
1586
|
}
|
|
1445
1587
|
case "tool/call":
|
|
1446
|
-
this.
|
|
1447
|
-
this.
|
|
1448
|
-
this.playNote(base - 12, .08, .3);
|
|
1588
|
+
this.pluck(base + 26, .45, .5);
|
|
1589
|
+
this.pluck(base - 12, .3, .35);
|
|
1449
1590
|
break;
|
|
1450
1591
|
case "tool/result":
|
|
1451
1592
|
if (action.error) {
|
|
1452
|
-
this.
|
|
1453
|
-
setTimeout(() => this.
|
|
1593
|
+
this.pluck(base + 13, .4, .6);
|
|
1594
|
+
setTimeout(() => this.pluck(base + 12, .4, .9), 90);
|
|
1454
1595
|
} else {
|
|
1455
|
-
this.
|
|
1456
|
-
setTimeout(() => this.
|
|
1596
|
+
this.pluck(base + 9, .35, .6);
|
|
1597
|
+
setTimeout(() => this.pluck(base + 11, .35, .9), 70);
|
|
1457
1598
|
}
|
|
1458
1599
|
break;
|
|
1459
1600
|
case "assistant/attempt":
|
|
1460
|
-
this.
|
|
1461
|
-
setTimeout(() => this.
|
|
1462
|
-
setTimeout(() => this.
|
|
1601
|
+
this.pluck(base + 8, .4, .8);
|
|
1602
|
+
setTimeout(() => this.pluck(base + 7, .4, .8), 110);
|
|
1603
|
+
setTimeout(() => this.pluck(base + 5, .4, .8), 220);
|
|
1463
1604
|
break;
|
|
1464
1605
|
case "assistant/message":
|
|
1465
|
-
this.
|
|
1466
|
-
this.
|
|
1467
|
-
|
|
1468
|
-
else this.playChord(this.currentRoot, this.currentQuality, 1.1, .6);
|
|
1606
|
+
this.stopMelody();
|
|
1607
|
+
this.playing = false;
|
|
1608
|
+
this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
|
|
1469
1609
|
break;
|
|
1470
1610
|
case "turn/end":
|
|
1471
|
-
this.
|
|
1472
|
-
this.playChord(base, this.style === "minor" ? "minor" : "major", 2.4, .7);
|
|
1473
|
-
this.chordIndex = 0;
|
|
1611
|
+
this.stopPiece(true);
|
|
1474
1612
|
break;
|
|
1475
1613
|
}
|
|
1476
1614
|
}
|
|
1477
|
-
/** Start
|
|
1478
|
-
|
|
1479
|
-
if (!this.audio) return;
|
|
1480
|
-
|
|
1481
|
-
const
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1615
|
+
/** Start a new piece: intro run, then the phrase chain. */
|
|
1616
|
+
startPiece() {
|
|
1617
|
+
if (!this.audio || this.playing) return;
|
|
1618
|
+
this.playing = true;
|
|
1619
|
+
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
|
|
1620
|
+
const scale = SCALES[this.style];
|
|
1621
|
+
if (this.introPending) {
|
|
1622
|
+
this.introPending = false;
|
|
1623
|
+
let t = 0;
|
|
1624
|
+
for (let i = 0; i < 8; i++) {
|
|
1625
|
+
const note = root + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
|
|
1626
|
+
setTimeout(() => {
|
|
1627
|
+
if (this.playing) this.pluck(note, .3);
|
|
1628
|
+
}, t);
|
|
1629
|
+
t += 150;
|
|
1630
|
+
}
|
|
1631
|
+
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
|
|
1632
|
+
} else {
|
|
1633
|
+
for (let i = 0; i < 3; i++) setTimeout(() => {
|
|
1634
|
+
if (this.playing) this.pluck(root + 12 + (scale[i] ?? 0), .3);
|
|
1635
|
+
}, i * 140);
|
|
1636
|
+
this.phraseTimer = setTimeout(() => this.playPhrase(), 480);
|
|
1494
1637
|
}
|
|
1495
|
-
this.padOscs = oscs;
|
|
1496
|
-
this.padGain = gain;
|
|
1497
|
-
this.padTarget = .18;
|
|
1498
|
-
gain.gain.linearRampToValueAtTime(this.volume * this.padTarget, now + .6);
|
|
1499
|
-
this.padActive = true;
|
|
1500
1638
|
}
|
|
1501
|
-
/**
|
|
1502
|
-
|
|
1503
|
-
if (!this.
|
|
1504
|
-
const
|
|
1505
|
-
const
|
|
1506
|
-
this.
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1639
|
+
/** Play one phrase, then chain into the next after a one-beat rest. */
|
|
1640
|
+
playPhrase() {
|
|
1641
|
+
if (!this.playing || !this.audio) return;
|
|
1642
|
+
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
|
|
1643
|
+
const scale = SCALES[this.style];
|
|
1644
|
+
this.pluck(root - 12, .28, 1.6);
|
|
1645
|
+
const phrase = PHRASES[this.phraseIndex % PHRASES.length];
|
|
1646
|
+
this.phraseIndex += 1;
|
|
1647
|
+
let t = 0;
|
|
1648
|
+
for (const step of phrase) {
|
|
1649
|
+
const note = root + (scale[step.deg % scale.length] ?? 0) + 12 * Math.floor(step.deg / scale.length);
|
|
1650
|
+
setTimeout(() => {
|
|
1651
|
+
if (this.playing) this.pluck(note, .34);
|
|
1652
|
+
}, t);
|
|
1653
|
+
t += step.beats * TokenQuotaMusic.BEAT_MS;
|
|
1654
|
+
}
|
|
1655
|
+
this.phraseTimer = setTimeout(() => this.playPhrase(), t + TokenQuotaMusic.BEAT_MS);
|
|
1511
1656
|
}
|
|
1512
|
-
/**
|
|
1513
|
-
|
|
1514
|
-
if (!this.audio
|
|
1515
|
-
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}, 850);
|
|
1534
|
-
this.padActive = false;
|
|
1535
|
-
this.padOscs = [];
|
|
1536
|
-
this.padGain = null;
|
|
1657
|
+
/** Insert a 过门 fill: pause the chain, play the run, resume after. */
|
|
1658
|
+
insertFill(root) {
|
|
1659
|
+
if (!this.audio) return;
|
|
1660
|
+
this.stopMelody();
|
|
1661
|
+
const scale = SCALES[this.style];
|
|
1662
|
+
let t = 0;
|
|
1663
|
+
for (const step of FILL) {
|
|
1664
|
+
const note = root + (scale[step.deg % scale.length] ?? 0) + 12 * Math.floor(step.deg / scale.length);
|
|
1665
|
+
setTimeout(() => {
|
|
1666
|
+
if (this.playing) this.pluck(note, .32, .7);
|
|
1667
|
+
}, t);
|
|
1668
|
+
t += step.beats * TokenQuotaMusic.BEAT_MS;
|
|
1669
|
+
}
|
|
1670
|
+
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
|
|
1671
|
+
}
|
|
1672
|
+
/** Stop the phrase chain. */
|
|
1673
|
+
stopMelody() {
|
|
1674
|
+
if (this.phraseTimer !== void 0) {
|
|
1675
|
+
clearTimeout(this.phraseTimer);
|
|
1676
|
+
this.phraseTimer = void 0;
|
|
1677
|
+
}
|
|
1537
1678
|
}
|
|
1538
|
-
/**
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
this.
|
|
1542
|
-
this.
|
|
1543
|
-
|
|
1544
|
-
const tick = () => {
|
|
1545
|
-
if (!this.audio || !this.enabled) return;
|
|
1546
|
-
if (this.beatIndex % 2 === 0) this.playNote(this.currentRoot - 12, .45, .3);
|
|
1679
|
+
/** Stop the whole piece; `final` adds the closing cadence gestures. */
|
|
1680
|
+
stopPiece(final) {
|
|
1681
|
+
this.stopMelody();
|
|
1682
|
+
this.playing = false;
|
|
1683
|
+
if (final && this.audio) {
|
|
1684
|
+
const base = TokenQuotaMusic.BASE_MIDI;
|
|
1547
1685
|
const scale = SCALES[this.style];
|
|
1548
|
-
|
|
1549
|
-
let
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
next = scale.length - 2;
|
|
1554
|
-
} else if (next < 0) {
|
|
1555
|
-
this.melodyDirection = 1;
|
|
1556
|
-
next = 0;
|
|
1557
|
-
}
|
|
1558
|
-
this.melodyDegree = next;
|
|
1559
|
-
degree = next;
|
|
1560
|
-
if (this.beatIndex % 2 === 0) {
|
|
1561
|
-
const rootRel = (scale[degree] ?? 0) % 12;
|
|
1562
|
-
if (!chordTones.has(rootRel)) {
|
|
1563
|
-
this.melodyDegree += this.melodyDirection;
|
|
1564
|
-
degree = this.melodyDegree;
|
|
1565
|
-
}
|
|
1686
|
+
let t = 0;
|
|
1687
|
+
for (let i = 7; i >= 0; i--) {
|
|
1688
|
+
const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
|
|
1689
|
+
setTimeout(() => this.pluck(note, .3), t);
|
|
1690
|
+
t += 120;
|
|
1566
1691
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
stopBeatClock() {
|
|
1575
|
-
if (this.beatTimer !== void 0) {
|
|
1576
|
-
clearInterval(this.beatTimer);
|
|
1577
|
-
this.beatTimer = void 0;
|
|
1692
|
+
setTimeout(() => {
|
|
1693
|
+
this.pluck(base, .5);
|
|
1694
|
+
this.pluck(base + 7, .45);
|
|
1695
|
+
this.pluck(base + 12, .4);
|
|
1696
|
+
this.pluck(base - 12, .5);
|
|
1697
|
+
}, t + 150);
|
|
1578
1698
|
}
|
|
1699
|
+
this.allMidiOff();
|
|
1579
1700
|
}
|
|
1701
|
+
/** Hard stop everything (master switch off / teardown). */
|
|
1580
1702
|
stopAll() {
|
|
1581
|
-
this.
|
|
1582
|
-
this.stopPad();
|
|
1703
|
+
this.stopPiece(false);
|
|
1583
1704
|
}
|
|
1584
|
-
/**
|
|
1585
|
-
|
|
1705
|
+
/**
|
|
1706
|
+
* Guzheng-like pluck: a triangle voice plus a soft octave partial through a
|
|
1707
|
+
* sweeping lowpass, fast attack and a long exponential decay (notes
|
|
1708
|
+
* overlap, which keeps the piece continuous instead of choppy).
|
|
1709
|
+
*/
|
|
1710
|
+
pluck(midi, velocity, decaySec = 1.3) {
|
|
1586
1711
|
if (!this.audio) return;
|
|
1587
|
-
const peak = this.volume * velocity * .
|
|
1712
|
+
const peak = this.volume * velocity * .4;
|
|
1713
|
+
if (peak <= 0) return;
|
|
1714
|
+
const now = this.audio.currentTime;
|
|
1715
|
+
const freq = midiToFreq(midi);
|
|
1716
|
+
const gain = this.audio.createGain();
|
|
1717
|
+
const filter = this.audio.createBiquadFilter();
|
|
1718
|
+
filter.type = "lowpass";
|
|
1719
|
+
filter.frequency.setValueAtTime(2400, now);
|
|
1720
|
+
filter.frequency.exponentialRampToValueAtTime(420, now + decaySec);
|
|
1721
|
+
filter.Q.value = .6;
|
|
1722
|
+
gain.gain.setValueAtTime(1e-4, now);
|
|
1723
|
+
gain.gain.exponentialRampToValueAtTime(peak, now + .006);
|
|
1724
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, now + decaySec);
|
|
1725
|
+
filter.connect(gain);
|
|
1726
|
+
gain.connect(this.audio.destination);
|
|
1727
|
+
const osc1 = this.audio.createOscillator();
|
|
1728
|
+
osc1.type = "triangle";
|
|
1729
|
+
osc1.frequency.value = freq;
|
|
1730
|
+
const osc2 = this.audio.createOscillator();
|
|
1731
|
+
osc2.type = "sine";
|
|
1732
|
+
osc2.frequency.value = freq * 2;
|
|
1733
|
+
const octGain = this.audio.createGain();
|
|
1734
|
+
octGain.gain.value = .25;
|
|
1735
|
+
osc1.connect(filter);
|
|
1736
|
+
osc2.connect(octGain);
|
|
1737
|
+
octGain.connect(filter);
|
|
1738
|
+
osc1.start(now);
|
|
1739
|
+
osc1.stop(now + decaySec + .1);
|
|
1740
|
+
osc2.start(now);
|
|
1741
|
+
osc2.stop(now + decaySec + .1);
|
|
1742
|
+
this.midiNote(midi, decaySec, velocity);
|
|
1743
|
+
}
|
|
1744
|
+
/** Dizi-like breath note: sine with vibrato and a soft envelope. */
|
|
1745
|
+
breath(midi, durationSec) {
|
|
1746
|
+
if (!this.audio) return;
|
|
1747
|
+
const peak = this.volume * .22;
|
|
1588
1748
|
if (peak <= 0) return;
|
|
1589
1749
|
const now = this.audio.currentTime;
|
|
1590
1750
|
const osc = this.audio.createOscillator();
|
|
1591
1751
|
osc.type = "sine";
|
|
1592
1752
|
osc.frequency.value = midiToFreq(midi);
|
|
1753
|
+
const lfo = this.audio.createOscillator();
|
|
1754
|
+
lfo.frequency.value = 5.2;
|
|
1755
|
+
const lfoGain = this.audio.createGain();
|
|
1756
|
+
lfoGain.gain.value = 4.5;
|
|
1757
|
+
lfo.connect(lfoGain);
|
|
1758
|
+
lfoGain.connect(osc.frequency);
|
|
1593
1759
|
const gain = this.audio.createGain();
|
|
1594
1760
|
gain.gain.setValueAtTime(0, now);
|
|
1595
|
-
gain.gain.linearRampToValueAtTime(peak, now + .
|
|
1596
|
-
gain.gain.
|
|
1761
|
+
gain.gain.linearRampToValueAtTime(peak, now + .18);
|
|
1762
|
+
gain.gain.setValueAtTime(peak, now + durationSec * .6);
|
|
1763
|
+
gain.gain.linearRampToValueAtTime(1e-4, now + durationSec);
|
|
1597
1764
|
osc.connect(gain);
|
|
1598
1765
|
gain.connect(this.audio.destination);
|
|
1599
1766
|
osc.start(now);
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1767
|
+
lfo.start(now);
|
|
1768
|
+
osc.stop(now + durationSec + .1);
|
|
1769
|
+
lfo.stop(now + durationSec + .1);
|
|
1770
|
+
this.midiNote(midi, durationSec, .5);
|
|
1771
|
+
}
|
|
1772
|
+
/** Send one MIDI note with a registered NoteOff (anti hanging note). */
|
|
1773
|
+
midiNote(midi, durationSec, velocity) {
|
|
1774
|
+
if (this.midiOutput === null) return;
|
|
1775
|
+
const note = midi & 127;
|
|
1776
|
+
const vel = Math.max(1, Math.min(127, Math.floor(velocity * 127)));
|
|
1777
|
+
try {
|
|
1603
1778
|
this.midiOutput.send([
|
|
1604
1779
|
144,
|
|
1605
|
-
|
|
1780
|
+
note,
|
|
1606
1781
|
vel
|
|
1607
1782
|
]);
|
|
1608
|
-
|
|
1783
|
+
} catch {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
this.activeMidiNotes.add(note);
|
|
1787
|
+
setTimeout(() => {
|
|
1788
|
+
try {
|
|
1609
1789
|
this.midiOutput?.send([
|
|
1610
1790
|
128,
|
|
1611
|
-
|
|
1791
|
+
note,
|
|
1612
1792
|
0
|
|
1613
1793
|
]);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1794
|
+
} catch {}
|
|
1795
|
+
this.activeMidiNotes.delete(note);
|
|
1796
|
+
}, Math.max(80, durationSec * 1e3));
|
|
1616
1797
|
}
|
|
1617
|
-
/**
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
for (const
|
|
1621
|
-
|
|
1798
|
+
/** Silence every registered note and send All Notes Off. */
|
|
1799
|
+
allMidiOff() {
|
|
1800
|
+
if (this.midiOutput === null) return;
|
|
1801
|
+
for (const note of this.activeMidiNotes) try {
|
|
1802
|
+
this.midiOutput.send([
|
|
1803
|
+
128,
|
|
1804
|
+
note,
|
|
1805
|
+
0
|
|
1806
|
+
]);
|
|
1807
|
+
} catch {}
|
|
1808
|
+
this.activeMidiNotes.clear();
|
|
1809
|
+
try {
|
|
1810
|
+
this.midiOutput.send([
|
|
1811
|
+
176,
|
|
1812
|
+
123,
|
|
1813
|
+
0
|
|
1814
|
+
]);
|
|
1815
|
+
} catch {}
|
|
1622
1816
|
}
|
|
1623
1817
|
};
|
|
1624
1818
|
//#endregion
|
|
@@ -1697,7 +1891,7 @@ window.__ModuleLoader__.load({
|
|
|
1697
1891
|
musicStyle: "风格",
|
|
1698
1892
|
musicStyleMajor: "大调",
|
|
1699
1893
|
musicStyleMinor: "小调",
|
|
1700
|
-
musicStylePentatonic: "
|
|
1894
|
+
musicStylePentatonic: "古风",
|
|
1701
1895
|
musicOnlyCurrentSessionLabel: "只播当前会话",
|
|
1702
1896
|
musicOnlyCurrentSessionHint: "开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲",
|
|
1703
1897
|
checkUpdatesNow: "立即检查",
|
|
@@ -1773,7 +1967,7 @@ window.__ModuleLoader__.load({
|
|
|
1773
1967
|
musicStyle: "Style",
|
|
1774
1968
|
musicStyleMajor: "Major",
|
|
1775
1969
|
musicStyleMinor: "Minor",
|
|
1776
|
-
musicStylePentatonic: "
|
|
1970
|
+
musicStylePentatonic: "Gufeng",
|
|
1777
1971
|
musicOnlyCurrentSessionLabel: "Current session only",
|
|
1778
1972
|
musicOnlyCurrentSessionHint: "When on, only the visible session drives the soundtrack (background tasks stay silent); when off, every session contributes.",
|
|
1779
1973
|
checkUpdatesNow: "Check now",
|
|
@@ -1823,6 +2017,12 @@ window.__ModuleLoader__.load({
|
|
|
1823
2017
|
let lastOnlyCurrentSession = true;
|
|
1824
2018
|
const music = new TokenQuotaMusic();
|
|
1825
2019
|
const sse = new EventSource("/token-quota/events");
|
|
2020
|
+
sse.addEventListener("hb", () => {
|
|
2021
|
+
music.markStreamActivity();
|
|
2022
|
+
});
|
|
2023
|
+
sse.addEventListener("open", () => {
|
|
2024
|
+
music.markStreamActivity();
|
|
2025
|
+
});
|
|
1826
2026
|
sse.addEventListener("action", (event) => {
|
|
1827
2027
|
let action;
|
|
1828
2028
|
try {
|
|
@@ -1840,7 +2040,7 @@ window.__ModuleLoader__.load({
|
|
|
1840
2040
|
const musicDefaults = (raw) => ({
|
|
1841
2041
|
enabled: raw?.enabled ?? false,
|
|
1842
2042
|
volume: raw?.volume ?? .5,
|
|
1843
|
-
style: raw?.style ?? "
|
|
2043
|
+
style: raw?.style ?? "pentatonic",
|
|
1844
2044
|
onlyCurrentSession: raw?.onlyCurrentSession ?? true
|
|
1845
2045
|
});
|
|
1846
2046
|
const keyOf = (selection) => {
|