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