@jxgame2020/dsh-token-quota 0.1.24 → 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 +664 -313
- package/lib/index.js +15 -4
- package/lib/types/client/TokenQuotaPanel.d.ts +3 -1
- package/lib/types/client/TokenQuotaPanel.js +3 -2
- package/lib/types/client/index.js +31 -8
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.js +6 -2
- package/lib/types/client/music.d.ts +73 -45
- package/lib/types/client/music.js +392 -180
- package/lib/types/client/store.d.ts +2 -0
- package/lib/types/client/store.js +3 -1
- package/lib/types/index.js +17 -1
- package/lib/types/types.d.ts +7 -0
- package/lib/types/types.js +2 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -39,7 +39,8 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
dimWhenIdle: false,
|
|
40
40
|
musicEnabled: false,
|
|
41
41
|
musicVolume: .5,
|
|
42
|
-
musicStyle: "
|
|
42
|
+
musicStyle: "pentatonic",
|
|
43
|
+
musicOnlyCurrentSession: true,
|
|
43
44
|
upgrade: null,
|
|
44
45
|
upgradeDismissed: false,
|
|
45
46
|
checkingUpdates: false,
|
|
@@ -90,8 +91,11 @@ window.__ModuleLoader__.load({
|
|
|
90
91
|
setMusicStyle: (d, style) => {
|
|
91
92
|
d.musicStyle = style;
|
|
92
93
|
},
|
|
94
|
+
setMusicOnlyCurrentSession: (d, only) => {
|
|
95
|
+
d.musicOnlyCurrentSession = only;
|
|
96
|
+
},
|
|
93
97
|
setUpgrade: (d, upgrade) => {
|
|
94
|
-
const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.
|
|
98
|
+
const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.27" ? upgrade : null;
|
|
95
99
|
if (d.upgrade === null || d.upgrade.latestVersion !== normalized?.latestVersion) d.upgradeDismissed = false;
|
|
96
100
|
d.upgrade = normalized;
|
|
97
101
|
},
|
|
@@ -197,92 +201,92 @@ window.__ModuleLoader__.load({
|
|
|
197
201
|
document.head.appendChild(tag);
|
|
198
202
|
}
|
|
199
203
|
var TokenQuotaPanel_module_css_default = {
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"row": "NPLLIq_row",
|
|
204
|
-
"controls": "NPLLIq_controls",
|
|
205
|
-
"logSearch": "NPLLIq_logSearch",
|
|
206
|
-
"logPagerControls": "NPLLIq_logPagerControls",
|
|
207
|
-
"logModelCol": "NPLLIq_logModelCol",
|
|
208
|
-
"logUsedCol": "NPLLIq_logUsedCol",
|
|
204
|
+
"bar": "NPLLIq_bar",
|
|
205
|
+
"tab": "NPLLIq_tab",
|
|
206
|
+
"rowHeader": "NPLLIq_rowHeader",
|
|
209
207
|
"logTable": "NPLLIq_logTable",
|
|
210
|
-
"
|
|
211
|
-
"
|
|
208
|
+
"logPagerInfo": "NPLLIq_logPagerInfo",
|
|
209
|
+
"copyBtn": "NPLLIq_copyBtn",
|
|
210
|
+
"dialogBody": "NPLLIq_dialogBody",
|
|
211
|
+
"logClearDialog": "NPLLIq_logClearDialog",
|
|
212
|
+
"input": "NPLLIq_input",
|
|
213
|
+
"monitorHint": "NPLLIq_monitorHint",
|
|
212
214
|
"monitorName": "NPLLIq_monitorName",
|
|
213
|
-
"
|
|
214
|
-
"settingsBtnActive": "NPLLIq_settingsBtnActive",
|
|
215
|
-
"upgradeBannerText": "NPLLIq_upgradeBannerText",
|
|
216
|
-
"upgradeClose": "NPLLIq_upgradeClose",
|
|
217
|
-
"fullNotice": "NPLLIq_fullNotice",
|
|
218
|
-
"radioRow": "NPLLIq_radioRow",
|
|
219
|
-
"dialogHint": "NPLLIq_dialogHint",
|
|
220
|
-
"rowHeader": "NPLLIq_rowHeader",
|
|
221
|
-
"dialogClose": "NPLLIq_dialogClose",
|
|
222
|
-
"selectBtn": "NPLLIq_selectBtn",
|
|
223
|
-
"upgradeCommands": "NPLLIq_upgradeCommands",
|
|
215
|
+
"checkErrorHint": "NPLLIq_checkErrorHint",
|
|
224
216
|
"fillWarn": "NPLLIq_fillWarn",
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
|
|
230
|
-
"logPager": "NPLLIq_logPager",
|
|
231
|
-
"dialogOverlay": "NPLLIq_dialogOverlay",
|
|
232
|
-
"dialogSection": "NPLLIq_dialogSection",
|
|
233
|
-
"logClearDialog": "NPLLIq_logClearDialog",
|
|
234
|
-
"headerText": "NPLLIq_headerText",
|
|
235
|
-
"notice": "NPLLIq_notice",
|
|
236
|
-
"tabLabel": "NPLLIq_tabLabel",
|
|
237
|
-
"dialogActions": "NPLLIq_dialogActions",
|
|
217
|
+
"dialogBtn": "NPLLIq_dialogBtn",
|
|
218
|
+
"logModelCol": "NPLLIq_logModelCol",
|
|
219
|
+
"fullNotice": "NPLLIq_fullNotice",
|
|
220
|
+
"logPagerPages": "NPLLIq_logPagerPages",
|
|
238
221
|
"subtitle": "NPLLIq_subtitle",
|
|
222
|
+
"upgradeClose": "NPLLIq_upgradeClose",
|
|
239
223
|
"logClearDate": "NPLLIq_logClearDate",
|
|
240
|
-
"
|
|
241
|
-
"dialogTitle": "NPLLIq_dialogTitle",
|
|
242
|
-
"logClearBtn": "NPLLIq_logClearBtn",
|
|
243
|
-
"dialogBody": "NPLLIq_dialogBody",
|
|
244
|
-
"title": "NPLLIq_title",
|
|
245
|
-
"upgradeHint": "NPLLIq_upgradeHint",
|
|
246
|
-
"version": "NPLLIq_version",
|
|
247
|
-
"body": "NPLLIq_body",
|
|
248
|
-
"logPagerPages": "NPLLIq_logPagerPages",
|
|
249
|
-
"monitorList": "NPLLIq_monitorList",
|
|
224
|
+
"row": "NPLLIq_row",
|
|
250
225
|
"logHeader": "NPLLIq_logHeader",
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"panelDimStrong": "NPLLIq_panelDimStrong",
|
|
256
|
-
"logToolbar": "NPLLIq_logToolbar",
|
|
257
|
-
"logPagerInfo": "NPLLIq_logPagerInfo",
|
|
258
|
-
"logScroll": "NPLLIq_logScroll",
|
|
259
|
-
"checkErrorHint": "NPLLIq_checkErrorHint",
|
|
226
|
+
"upgradeCommandRow": "NPLLIq_upgradeCommandRow",
|
|
227
|
+
"dialog": "NPLLIq_dialog",
|
|
228
|
+
"upgradeHint": "NPLLIq_upgradeHint",
|
|
229
|
+
"dialogHeader": "NPLLIq_dialogHeader",
|
|
260
230
|
"rowMeta": "NPLLIq_rowMeta",
|
|
231
|
+
"upgradeBannerText": "NPLLIq_upgradeBannerText",
|
|
232
|
+
"checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
|
|
233
|
+
"dialogLabel": "NPLLIq_dialogLabel",
|
|
261
234
|
"logPagerBtn": "NPLLIq_logPagerBtn",
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
235
|
+
"logClearBtn": "NPLLIq_logClearBtn",
|
|
236
|
+
"noticeError": "NPLLIq_noticeError",
|
|
237
|
+
"logUsedCol": "NPLLIq_logUsedCol",
|
|
238
|
+
"panelDim": "NPLLIq_panelDim",
|
|
239
|
+
"body": "NPLLIq_body",
|
|
240
|
+
"dialogClose": "NPLLIq_dialogClose",
|
|
241
|
+
"selectBtn": "NPLLIq_selectBtn",
|
|
242
|
+
"dialogOverlay": "NPLLIq_dialogOverlay",
|
|
243
|
+
"header": "NPLLIq_header",
|
|
244
|
+
"dialogActions": "NPLLIq_dialogActions",
|
|
245
|
+
"logDayCol": "NPLLIq_logDayCol",
|
|
246
|
+
"dialogTitle": "NPLLIq_dialogTitle",
|
|
247
|
+
"headerText": "NPLLIq_headerText",
|
|
248
|
+
"collapse": "NPLLIq_collapse",
|
|
249
|
+
"currentBadge": "NPLLIq_currentBadge",
|
|
265
250
|
"panel": "NPLLIq_panel",
|
|
251
|
+
"fillOver": "NPLLIq_fillOver",
|
|
252
|
+
"settingsBtn": "NPLLIq_settingsBtn",
|
|
266
253
|
"gearBtn": "NPLLIq_gearBtn",
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
"
|
|
254
|
+
"logPageSizeSelect": "NPLLIq_logPageSizeSelect",
|
|
255
|
+
"dialogHint": "NPLLIq_dialogHint",
|
|
256
|
+
"versionNew": "NPLLIq_versionNew",
|
|
257
|
+
"save": "NPLLIq_save",
|
|
270
258
|
"upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
|
|
271
|
-
"copyBtn": "NPLLIq_copyBtn",
|
|
272
259
|
"switch": "NPLLIq_switch",
|
|
273
|
-
"
|
|
260
|
+
"logToolbar": "NPLLIq_logToolbar",
|
|
261
|
+
"tabIcon": "NPLLIq_tabIcon",
|
|
262
|
+
"radioRow": "NPLLIq_radioRow",
|
|
263
|
+
"settingsBtnActive": "NPLLIq_settingsBtnActive",
|
|
264
|
+
"copyBtnDone": "NPLLIq_copyBtnDone",
|
|
265
|
+
"headerActions": "NPLLIq_headerActions",
|
|
266
|
+
"dialogSection": "NPLLIq_dialogSection",
|
|
267
|
+
"upgradeBanner": "NPLLIq_upgradeBanner",
|
|
268
|
+
"logScroll": "NPLLIq_logScroll",
|
|
269
|
+
"resetSelect": "NPLLIq_resetSelect",
|
|
270
|
+
"logSearch": "NPLLIq_logSearch",
|
|
271
|
+
"checkBtn": "NPLLIq_checkBtn",
|
|
272
|
+
"panelDimStrong": "NPLLIq_panelDimStrong",
|
|
274
273
|
"monitorRow": "NPLLIq_monitorRow",
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
274
|
+
"dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
|
|
275
|
+
"upgradeCommands": "NPLLIq_upgradeCommands",
|
|
276
|
+
"checkUpdatesRow": "NPLLIq_checkUpdatesRow",
|
|
277
|
+
"logPager": "NPLLIq_logPager",
|
|
278
|
+
"logPagerControls": "NPLLIq_logPagerControls",
|
|
279
|
+
"logDialog": "NPLLIq_logDialog",
|
|
278
280
|
"rowName": "NPLLIq_rowName",
|
|
279
|
-
"
|
|
280
|
-
"
|
|
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",
|
|
281
288
|
"fillIdle": "NPLLIq_fillIdle",
|
|
282
|
-
"
|
|
283
|
-
"logDialog": "NPLLIq_logDialog",
|
|
284
|
-
"checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
|
|
285
|
-
"header": "NPLLIq_header"
|
|
289
|
+
"controls": "NPLLIq_controls"
|
|
286
290
|
};
|
|
287
291
|
//#endregion
|
|
288
292
|
//#region lib/types/client/TokenQuotaPanel.js
|
|
@@ -369,7 +373,7 @@ window.__ModuleLoader__.load({
|
|
|
369
373
|
* @param props - composed slot props.
|
|
370
374
|
* @returns the panel element tree.
|
|
371
375
|
*/
|
|
372
|
-
function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, useStore, actions, useSessions }) {
|
|
376
|
+
function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions }) {
|
|
373
377
|
const [collapsed, setCollapsed] = (0, react.useState)(false);
|
|
374
378
|
const [drafts, setDrafts] = (0, react.useState)({});
|
|
375
379
|
const [editingKey, setEditingKey] = (0, react.useState)(null);
|
|
@@ -403,6 +407,7 @@ window.__ModuleLoader__.load({
|
|
|
403
407
|
const musicEnabled = useStore((s) => s.musicEnabled);
|
|
404
408
|
const musicVolume = useStore((s) => s.musicVolume);
|
|
405
409
|
const musicStyle = useStore((s) => s.musicStyle);
|
|
410
|
+
const musicOnlyCurrentSession = useStore((s) => s.musicOnlyCurrentSession);
|
|
406
411
|
const upgrade = useStore((s) => s.upgrade);
|
|
407
412
|
const upgradeDismissed = useStore((s) => s.upgradeDismissed);
|
|
408
413
|
const checkingUpdates = useStore((s) => s.checkingUpdates);
|
|
@@ -587,13 +592,13 @@ window.__ModuleLoader__.load({
|
|
|
587
592
|
title: t("upgradeAvailableHint"),
|
|
588
593
|
children: [
|
|
589
594
|
"v",
|
|
590
|
-
"0.1.
|
|
595
|
+
"0.1.27",
|
|
591
596
|
" → v",
|
|
592
597
|
upgrade.latestVersion
|
|
593
598
|
]
|
|
594
599
|
}) : (0, react_jsx_runtime.jsxs)("span", {
|
|
595
600
|
className: TokenQuotaPanel_module_css_default.version,
|
|
596
|
-
children: ["v", "0.1.
|
|
601
|
+
children: ["v", "0.1.27"]
|
|
597
602
|
})]
|
|
598
603
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
599
604
|
className: TokenQuotaPanel_module_css_default.subtitle,
|
|
@@ -956,41 +961,58 @@ window.__ModuleLoader__.load({
|
|
|
956
961
|
className: TokenQuotaPanel_module_css_default.dialogHint,
|
|
957
962
|
children: t("musicHint")
|
|
958
963
|
}),
|
|
959
|
-
musicEnabled && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
964
|
+
musicEnabled && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
965
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
966
|
+
className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
|
|
967
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("musicVolume") }), (0, react_jsx_runtime.jsx)("input", {
|
|
968
|
+
type: "range",
|
|
969
|
+
min: 0,
|
|
970
|
+
max: 1,
|
|
971
|
+
step: .05,
|
|
972
|
+
value: musicVolume,
|
|
973
|
+
onChange: (event) => {
|
|
974
|
+
setMusicVolume(Number(event.target.value));
|
|
975
|
+
}
|
|
976
|
+
})]
|
|
977
|
+
}),
|
|
978
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
979
|
+
className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
|
|
980
|
+
children: [(0, react_jsx_runtime.jsx)("span", { children: t("musicStyle") }), (0, react_jsx_runtime.jsxs)("select", {
|
|
981
|
+
value: musicStyle,
|
|
982
|
+
onChange: (event) => {
|
|
983
|
+
setMusicStyle(event.target.value);
|
|
984
|
+
},
|
|
985
|
+
children: [
|
|
986
|
+
(0, react_jsx_runtime.jsx)("option", {
|
|
987
|
+
value: "major",
|
|
988
|
+
children: t("musicStyleMajor")
|
|
989
|
+
}),
|
|
990
|
+
(0, react_jsx_runtime.jsx)("option", {
|
|
991
|
+
value: "minor",
|
|
992
|
+
children: t("musicStyleMinor")
|
|
993
|
+
}),
|
|
994
|
+
(0, react_jsx_runtime.jsx)("option", {
|
|
995
|
+
value: "pentatonic",
|
|
996
|
+
children: t("musicStylePentatonic")
|
|
997
|
+
})
|
|
998
|
+
]
|
|
999
|
+
})]
|
|
1000
|
+
}),
|
|
1001
|
+
(0, react_jsx_runtime.jsxs)("label", {
|
|
1002
|
+
className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
|
|
1003
|
+
children: [(0, react_jsx_runtime.jsx)("input", {
|
|
1004
|
+
type: "checkbox",
|
|
1005
|
+
checked: musicOnlyCurrentSession,
|
|
1006
|
+
onChange: (event) => {
|
|
1007
|
+
setMusicOnlyCurrentSession(event.target.checked);
|
|
1008
|
+
}
|
|
1009
|
+
}), (0, react_jsx_runtime.jsx)("span", { children: t("musicOnlyCurrentSessionLabel") })]
|
|
1010
|
+
}),
|
|
1011
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1012
|
+
className: TokenQuotaPanel_module_css_default.dialogHint,
|
|
1013
|
+
children: t("musicOnlyCurrentSessionHint")
|
|
1014
|
+
})
|
|
1015
|
+
] })
|
|
994
1016
|
]
|
|
995
1017
|
}),
|
|
996
1018
|
(0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1210,20 +1232,28 @@ window.__ModuleLoader__.load({
|
|
|
1210
1232
|
//#endregion
|
|
1211
1233
|
//#region lib/types/client/music.js
|
|
1212
1234
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
* etc.). Outputs through Web Audio (built-in synthesis) by default, and also
|
|
1215
|
-
* sends MIDI NoteOn/NoteOff to the first available MIDI output when one is
|
|
1216
|
-
* detected (both play simultaneously when MIDI is present — use the panel
|
|
1217
|
-
* volume for overall level, the MIDI output has its own gain on the receiver).
|
|
1235
|
+
* 古风 live-composed soundtrack driven by host actions.
|
|
1218
1236
|
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
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.
|
|
1245
|
+
*
|
|
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.
|
|
1223
1253
|
*
|
|
1224
1254
|
* @module @jxgame2020/dsh-token-quota/music
|
|
1225
1255
|
*/
|
|
1226
|
-
/** Scale degrees in semitones from the
|
|
1256
|
+
/** Scale degrees in semitones from the (current) tonic. */
|
|
1227
1257
|
const SCALES = {
|
|
1228
1258
|
major: [
|
|
1229
1259
|
0,
|
|
@@ -1252,10 +1282,11 @@ window.__ModuleLoader__.load({
|
|
|
1252
1282
|
]
|
|
1253
1283
|
};
|
|
1254
1284
|
/**
|
|
1255
|
-
*
|
|
1256
|
-
* 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.
|
|
1257
1288
|
*/
|
|
1258
|
-
const
|
|
1289
|
+
const PROGRESSION = {
|
|
1259
1290
|
major: [
|
|
1260
1291
|
0,
|
|
1261
1292
|
9,
|
|
@@ -1264,80 +1295,221 @@ window.__ModuleLoader__.load({
|
|
|
1264
1295
|
],
|
|
1265
1296
|
minor: [
|
|
1266
1297
|
0,
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1298
|
+
8,
|
|
1299
|
+
3,
|
|
1300
|
+
10
|
|
1270
1301
|
],
|
|
1271
1302
|
pentatonic: [
|
|
1272
1303
|
0,
|
|
1304
|
+
9,
|
|
1273
1305
|
7,
|
|
1274
|
-
|
|
1275
|
-
9
|
|
1306
|
+
5
|
|
1276
1307
|
]
|
|
1277
1308
|
};
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
+
}
|
|
1284
1344
|
],
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
+
}
|
|
1290
1370
|
],
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
+
}
|
|
1296
1434
|
]
|
|
1297
|
-
|
|
1298
|
-
/**
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
+
];
|
|
1471
|
+
/** Turn a MIDI note number into a frequency (equal temperament, A4 = 440). */
|
|
1302
1472
|
function midiToFreq(midi) {
|
|
1303
1473
|
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
1304
1474
|
}
|
|
1305
|
-
/**
|
|
1475
|
+
/** General-MIDI program closest to a guzheng: 108 Koto (0-indexed 106). */
|
|
1476
|
+
const GM_PROGRAM_KOTO = 106;
|
|
1477
|
+
/**
|
|
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).
|
|
1481
|
+
*/
|
|
1306
1482
|
var TokenQuotaMusic = class TokenQuotaMusic {
|
|
1307
1483
|
audio = null;
|
|
1308
1484
|
midiOutput = null;
|
|
1309
1485
|
midiDetected = false;
|
|
1310
1486
|
enabled = false;
|
|
1311
1487
|
volume = .5;
|
|
1312
|
-
style = "
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
*/
|
|
1324
|
-
|
|
1325
|
-
/** Direction of the progress walk: +1 up the scale, -1 down. */
|
|
1326
|
-
progressDir = 1;
|
|
1327
|
-
/** Scale degree of the next progress note. */
|
|
1328
|
-
progressDegree = 0;
|
|
1329
|
-
/** Base MIDI note of the current 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. */
|
|
1330
1501
|
static BASE_MIDI = 60;
|
|
1331
|
-
/**
|
|
1332
|
-
static
|
|
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;
|
|
1333
1506
|
/** True once the audio context has been started (needs a user gesture). */
|
|
1334
1507
|
get started() {
|
|
1335
1508
|
return this.audio !== null;
|
|
1336
1509
|
}
|
|
1337
1510
|
/**
|
|
1338
|
-
* Start the audio context and
|
|
1339
|
-
*
|
|
1340
|
-
* 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.
|
|
1341
1513
|
*/
|
|
1342
1514
|
async ensureStarted() {
|
|
1343
1515
|
if (this.audio !== null) return true;
|
|
@@ -1357,15 +1529,24 @@ window.__ModuleLoader__.load({
|
|
|
1357
1529
|
this.midiOutput = output;
|
|
1358
1530
|
break;
|
|
1359
1531
|
}
|
|
1532
|
+
try {
|
|
1533
|
+
this.midiOutput?.send([192, GM_PROGRAM_KOTO]);
|
|
1534
|
+
} catch {}
|
|
1360
1535
|
} catch {
|
|
1361
1536
|
this.midiOutput = null;
|
|
1362
1537
|
}
|
|
1363
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
|
+
}
|
|
1364
1545
|
return true;
|
|
1365
1546
|
}
|
|
1366
1547
|
setEnabled(value) {
|
|
1367
1548
|
this.enabled = value;
|
|
1368
|
-
if (!value) this.
|
|
1549
|
+
if (!value) this.stopAll();
|
|
1369
1550
|
}
|
|
1370
1551
|
setVolume(value) {
|
|
1371
1552
|
this.volume = Math.max(0, Math.min(1, value));
|
|
@@ -1373,164 +1554,324 @@ window.__ModuleLoader__.load({
|
|
|
1373
1554
|
setStyle(value) {
|
|
1374
1555
|
this.style = value;
|
|
1375
1556
|
}
|
|
1376
|
-
/**
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
if (this.progressTimer !== void 0) return;
|
|
1380
|
-
this.progressDegree = 0;
|
|
1381
|
-
this.progressDir = 1;
|
|
1382
|
-
const tick = () => {
|
|
1383
|
-
if (!this.audio || !this.enabled) return;
|
|
1384
|
-
const scale = SCALES[this.style];
|
|
1385
|
-
const degree = this.progressDegree % scale.length;
|
|
1386
|
-
const note = TokenQuotaMusic.BASE_MIDI + 12 + (scale[degree] ?? 0);
|
|
1387
|
-
this.playNote(note, .18, .22);
|
|
1388
|
-
const next = this.progressDegree + this.progressDir;
|
|
1389
|
-
if (next >= scale.length) {
|
|
1390
|
-
this.progressDir = -1;
|
|
1391
|
-
this.progressDegree = scale.length - 2;
|
|
1392
|
-
} else if (next < 0) {
|
|
1393
|
-
this.progressDir = 1;
|
|
1394
|
-
this.progressDegree = 0;
|
|
1395
|
-
} else this.progressDegree = next;
|
|
1396
|
-
};
|
|
1397
|
-
tick();
|
|
1398
|
-
this.progressTimer = setInterval(tick, TokenQuotaMusic.PROGRESS_TICK_MS);
|
|
1557
|
+
/** Feed the engine one SSE activity timestamp (action or heartbeat). */
|
|
1558
|
+
markStreamActivity() {
|
|
1559
|
+
this.lastStreamActivity = Date.now();
|
|
1399
1560
|
}
|
|
1400
|
-
/**
|
|
1401
|
-
stopProgress() {
|
|
1402
|
-
if (this.progressTimer !== void 0) {
|
|
1403
|
-
clearInterval(this.progressTimer);
|
|
1404
|
-
this.progressTimer = void 0;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
/** Handle one incoming host action and turn it into sound. */
|
|
1561
|
+
/** Handle one incoming host action and steer the piece. */
|
|
1408
1562
|
onAction(action) {
|
|
1409
1563
|
if (!this.enabled || this.audio === null) return;
|
|
1410
1564
|
const base = TokenQuotaMusic.BASE_MIDI;
|
|
1411
1565
|
switch (action.type) {
|
|
1412
1566
|
case "turn/start":
|
|
1413
|
-
this.
|
|
1414
|
-
this.
|
|
1415
|
-
this.
|
|
1416
|
-
this.toolCount = 0;
|
|
1417
|
-
this.playChord(base, this.currentChordQuality(), 1.6, .55);
|
|
1567
|
+
this.stopPiece(false);
|
|
1568
|
+
this.tonicIndex = 0;
|
|
1569
|
+
this.phraseIndex = 0;
|
|
1418
1570
|
break;
|
|
1419
|
-
case "
|
|
1420
|
-
|
|
1421
|
-
this.
|
|
1422
|
-
this.playNote(this.currentChordRoot(base) - 12, 1.2, .35);
|
|
1423
|
-
const root = this.currentChordRoot(base);
|
|
1424
|
-
const [d1, d2, d3] = this.arpeggioDegrees();
|
|
1425
|
-
setTimeout(() => this.playNote(root + d1, .2, .4), 0);
|
|
1426
|
-
setTimeout(() => this.playNote(root + d2, .2, .4), 80);
|
|
1427
|
-
setTimeout(() => this.playNote(root + d3, .35, .4), 160);
|
|
1571
|
+
case "request/header":
|
|
1572
|
+
if (!this.playing) this.startPiece();
|
|
1573
|
+
else this.pluck(base + 24, .3);
|
|
1428
1574
|
break;
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
this.
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
this.
|
|
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);
|
|
1435
1581
|
break;
|
|
1436
1582
|
}
|
|
1437
1583
|
case "tool/call":
|
|
1438
|
-
this.
|
|
1439
|
-
this.
|
|
1440
|
-
this.playNote(base - 12, .08, .25);
|
|
1584
|
+
this.pluck(base + 26, .45, .5);
|
|
1585
|
+
this.pluck(base - 12, .3, .35);
|
|
1441
1586
|
break;
|
|
1442
1587
|
case "tool/result":
|
|
1443
1588
|
if (action.error) {
|
|
1444
|
-
this.
|
|
1445
|
-
setTimeout(() => this.
|
|
1589
|
+
this.pluck(base + 13, .4, .6);
|
|
1590
|
+
setTimeout(() => this.pluck(base + 12, .4, .9), 90);
|
|
1446
1591
|
} else {
|
|
1447
|
-
this.
|
|
1448
|
-
setTimeout(() => this.
|
|
1592
|
+
this.pluck(base + 9, .35, .6);
|
|
1593
|
+
setTimeout(() => this.pluck(base + 11, .35, .9), 70);
|
|
1449
1594
|
}
|
|
1450
1595
|
break;
|
|
1451
1596
|
case "assistant/attempt":
|
|
1452
|
-
this.
|
|
1453
|
-
this.
|
|
1454
|
-
setTimeout(() => this.
|
|
1455
|
-
setTimeout(() => this.playNote(base + 5, .25, .4), 200);
|
|
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);
|
|
1456
1600
|
break;
|
|
1457
1601
|
case "assistant/message":
|
|
1458
|
-
this.
|
|
1459
|
-
|
|
1460
|
-
else this.playChord(this.currentChordRoot(base), this.currentChordQuality(), 1.1, .55);
|
|
1602
|
+
this.stopMelody();
|
|
1603
|
+
this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
|
|
1461
1604
|
break;
|
|
1462
1605
|
case "turn/end":
|
|
1463
|
-
this.
|
|
1464
|
-
this.playChord(base, this.style === "minor" ? "minor" : "major", 2.2, .65);
|
|
1465
|
-
this.chordIndex = 0;
|
|
1606
|
+
this.stopPiece(true);
|
|
1466
1607
|
break;
|
|
1467
1608
|
}
|
|
1468
1609
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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);
|
|
1472
1644
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1645
|
+
/** Insert a 过门 fill: pause the chain, play the run, resume after. */
|
|
1646
|
+
insertFill(root) {
|
|
1647
|
+
if (!this.audio) return;
|
|
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);
|
|
1476
1659
|
}
|
|
1477
|
-
/**
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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;
|
|
1698
|
+
const now = this.audio.currentTime;
|
|
1699
|
+
const gain = this.audio.createGain();
|
|
1700
|
+
gain.gain.setValueAtTime(0, now);
|
|
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);
|
|
1707
|
+
const oscs = [];
|
|
1708
|
+
for (const interval of [-12, -5]) {
|
|
1709
|
+
const osc = this.audio.createOscillator();
|
|
1710
|
+
osc.type = "sine";
|
|
1711
|
+
osc.frequency.value = midiToFreq(root + interval);
|
|
1712
|
+
osc.connect(gain);
|
|
1713
|
+
osc.start(now);
|
|
1714
|
+
oscs.push(osc);
|
|
1715
|
+
}
|
|
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);
|
|
1721
|
+
}
|
|
1722
|
+
/** Glide the drone to a new tonic (mode rotation). */
|
|
1723
|
+
glideDrone(root) {
|
|
1724
|
+
if (!this.audio || !this.droneActive) return;
|
|
1725
|
+
const now = this.audio.currentTime;
|
|
1726
|
+
const targets = [root - 12, root - 5];
|
|
1727
|
+
this.droneOscs.forEach((osc, i) => {
|
|
1728
|
+
const target = targets[i] ?? root;
|
|
1729
|
+
osc.frequency.setValueAtTime(osc.frequency.value, now);
|
|
1730
|
+
osc.frequency.exponentialRampToValueAtTime(midiToFreq(target), now + .9);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
/** Fade the drone out and stop its oscillators. */
|
|
1734
|
+
fadeDrone(seconds) {
|
|
1735
|
+
if (!this.audio || !this.droneActive || this.droneGain === null) return;
|
|
1736
|
+
const now = this.audio.currentTime;
|
|
1737
|
+
const gain = this.droneGain;
|
|
1738
|
+
gain.gain.cancelScheduledValues(now);
|
|
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;
|
|
1743
|
+
setTimeout(() => {
|
|
1744
|
+
for (const osc of oscs) {
|
|
1745
|
+
try {
|
|
1746
|
+
osc.stop();
|
|
1747
|
+
} catch {}
|
|
1748
|
+
try {
|
|
1749
|
+
osc.disconnect();
|
|
1750
|
+
} catch {}
|
|
1751
|
+
}
|
|
1752
|
+
try {
|
|
1753
|
+
filter?.disconnect();
|
|
1754
|
+
} catch {}
|
|
1755
|
+
try {
|
|
1756
|
+
gain.disconnect();
|
|
1757
|
+
} catch {}
|
|
1758
|
+
}, seconds * 1e3 + 120);
|
|
1759
|
+
this.droneActive = false;
|
|
1760
|
+
this.droneOscs = [];
|
|
1761
|
+
this.droneGain = null;
|
|
1762
|
+
this.droneFilter = null;
|
|
1492
1763
|
}
|
|
1493
|
-
/**
|
|
1494
|
-
|
|
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) {
|
|
1495
1770
|
if (!this.audio) return;
|
|
1771
|
+
const peak = this.volume * velocity * .4;
|
|
1772
|
+
if (peak <= 0) return;
|
|
1496
1773
|
const now = this.audio.currentTime;
|
|
1497
|
-
const
|
|
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);
|
|
1802
|
+
}
|
|
1803
|
+
/** Dizi-like breath note: sine with vibrato and a soft envelope. */
|
|
1804
|
+
breath(midi, durationSec) {
|
|
1805
|
+
if (!this.audio) return;
|
|
1806
|
+
const peak = this.volume * .22;
|
|
1498
1807
|
if (peak <= 0) return;
|
|
1808
|
+
const now = this.audio.currentTime;
|
|
1499
1809
|
const osc = this.audio.createOscillator();
|
|
1500
1810
|
osc.type = "sine";
|
|
1501
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);
|
|
1502
1818
|
const gain = this.audio.createGain();
|
|
1503
1819
|
gain.gain.setValueAtTime(0, now);
|
|
1504
|
-
gain.gain.linearRampToValueAtTime(peak, now + .
|
|
1505
|
-
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);
|
|
1506
1823
|
osc.connect(gain);
|
|
1507
1824
|
gain.connect(this.audio.destination);
|
|
1508
1825
|
osc.start(now);
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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 {
|
|
1512
1837
|
this.midiOutput.send([
|
|
1513
1838
|
144,
|
|
1514
|
-
|
|
1839
|
+
note,
|
|
1515
1840
|
vel
|
|
1516
1841
|
]);
|
|
1517
|
-
|
|
1842
|
+
} catch {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
this.activeMidiNotes.add(note);
|
|
1846
|
+
setTimeout(() => {
|
|
1847
|
+
try {
|
|
1518
1848
|
this.midiOutput?.send([
|
|
1519
1849
|
128,
|
|
1520
|
-
|
|
1850
|
+
note,
|
|
1521
1851
|
0
|
|
1522
1852
|
]);
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1853
|
+
} catch {}
|
|
1854
|
+
this.activeMidiNotes.delete(note);
|
|
1855
|
+
}, Math.max(80, durationSec * 1e3));
|
|
1525
1856
|
}
|
|
1526
|
-
/**
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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 {}
|
|
1534
1875
|
}
|
|
1535
1876
|
};
|
|
1536
1877
|
//#endregion
|
|
@@ -1609,7 +1950,9 @@ window.__ModuleLoader__.load({
|
|
|
1609
1950
|
musicStyle: "风格",
|
|
1610
1951
|
musicStyleMajor: "大调",
|
|
1611
1952
|
musicStyleMinor: "小调",
|
|
1612
|
-
musicStylePentatonic: "
|
|
1953
|
+
musicStylePentatonic: "古风",
|
|
1954
|
+
musicOnlyCurrentSessionLabel: "只播当前会话",
|
|
1955
|
+
musicOnlyCurrentSessionHint: "开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲",
|
|
1613
1956
|
checkUpdatesNow: "立即检查",
|
|
1614
1957
|
checkingUpdates: "检查中…",
|
|
1615
1958
|
checkUpToDate: "已是最新",
|
|
@@ -1683,7 +2026,9 @@ window.__ModuleLoader__.load({
|
|
|
1683
2026
|
musicStyle: "Style",
|
|
1684
2027
|
musicStyleMajor: "Major",
|
|
1685
2028
|
musicStyleMinor: "Minor",
|
|
1686
|
-
musicStylePentatonic: "
|
|
2029
|
+
musicStylePentatonic: "Gufeng",
|
|
2030
|
+
musicOnlyCurrentSessionLabel: "Current session only",
|
|
2031
|
+
musicOnlyCurrentSessionHint: "When on, only the visible session drives the soundtrack (background tasks stay silent); when off, every session contributes.",
|
|
1687
2032
|
checkUpdatesNow: "Check now",
|
|
1688
2033
|
checkingUpdates: "Checking…",
|
|
1689
2034
|
checkUpToDate: "Up to date",
|
|
@@ -1728,8 +2073,15 @@ window.__ModuleLoader__.load({
|
|
|
1728
2073
|
let lastMonitored = null;
|
|
1729
2074
|
let lastOnFull = "stop";
|
|
1730
2075
|
let lastReset = null;
|
|
2076
|
+
let lastOnlyCurrentSession = true;
|
|
1731
2077
|
const music = new TokenQuotaMusic();
|
|
1732
2078
|
const sse = new EventSource("/token-quota/events");
|
|
2079
|
+
sse.addEventListener("hb", () => {
|
|
2080
|
+
music.markStreamActivity();
|
|
2081
|
+
});
|
|
2082
|
+
sse.addEventListener("open", () => {
|
|
2083
|
+
music.markStreamActivity();
|
|
2084
|
+
});
|
|
1733
2085
|
sse.addEventListener("action", (event) => {
|
|
1734
2086
|
let action;
|
|
1735
2087
|
try {
|
|
@@ -1737,12 +2089,19 @@ window.__ModuleLoader__.load({
|
|
|
1737
2089
|
} catch {
|
|
1738
2090
|
return;
|
|
1739
2091
|
}
|
|
1740
|
-
if (lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
|
|
2092
|
+
if (lastOnlyCurrentSession && lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
|
|
1741
2093
|
music.onAction(action);
|
|
1742
2094
|
});
|
|
1743
2095
|
ctx.effect(() => () => {
|
|
1744
2096
|
sse.close();
|
|
1745
2097
|
}, "token-quota-ui: music SSE");
|
|
2098
|
+
/** Build a writable music-settings clone with defaults applied. */
|
|
2099
|
+
const musicDefaults = (raw) => ({
|
|
2100
|
+
enabled: raw?.enabled ?? false,
|
|
2101
|
+
volume: raw?.volume ?? .5,
|
|
2102
|
+
style: raw?.style ?? "pentatonic",
|
|
2103
|
+
onlyCurrentSession: raw?.onlyCurrentSession ?? true
|
|
2104
|
+
});
|
|
1746
2105
|
const keyOf = (selection) => {
|
|
1747
2106
|
if (selection === null) return void 0;
|
|
1748
2107
|
return `${selection.provider}/${selection.model}`;
|
|
@@ -1852,18 +2211,15 @@ window.__ModuleLoader__.load({
|
|
|
1852
2211
|
bound?.setCheckUpdates(doc?.checkUpdates ?? true);
|
|
1853
2212
|
bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
|
|
1854
2213
|
bound?.setReset(lastReset);
|
|
1855
|
-
const musicSettings =
|
|
1856
|
-
|
|
1857
|
-
volume: .5,
|
|
1858
|
-
style: "major",
|
|
1859
|
-
...doc?.music ?? {}
|
|
1860
|
-
};
|
|
2214
|
+
const musicSettings = musicDefaults(doc?.music);
|
|
2215
|
+
lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
|
|
1861
2216
|
music.setEnabled(musicSettings.enabled);
|
|
1862
2217
|
music.setVolume(musicSettings.volume);
|
|
1863
2218
|
music.setStyle(musicSettings.style);
|
|
1864
2219
|
bound?.setMusicEnabled(musicSettings.enabled);
|
|
1865
2220
|
bound?.setMusicVolume(musicSettings.volume);
|
|
1866
2221
|
bound?.setMusicStyle(musicSettings.style);
|
|
2222
|
+
bound?.setMusicOnlyCurrentSession(musicSettings.onlyCurrentSession);
|
|
1867
2223
|
fetch("/token-quota", { headers: { accept: "application/json" } }).then((response) => {
|
|
1868
2224
|
if (!response.ok) {
|
|
1869
2225
|
bound?.setError(`quota route: ${String(response.status)}`);
|
|
@@ -1934,36 +2290,30 @@ window.__ModuleLoader__.load({
|
|
|
1934
2290
|
scope.set("dimWhenIdle", enabled);
|
|
1935
2291
|
};
|
|
1936
2292
|
const setMusicEnabled = (enabled) => {
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
volume: .5,
|
|
1940
|
-
style: "major",
|
|
1941
|
-
...scope.getSnapshot().value?.music ?? {}
|
|
1942
|
-
};
|
|
2293
|
+
const doc = scope.getSnapshot().value;
|
|
2294
|
+
const current = musicDefaults(doc?.music);
|
|
1943
2295
|
current.enabled = enabled;
|
|
1944
2296
|
scope.set("music", current);
|
|
1945
2297
|
if (enabled) music.ensureStarted();
|
|
1946
2298
|
};
|
|
1947
2299
|
const setMusicVolume = (volume) => {
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
volume: .5,
|
|
1951
|
-
style: "major",
|
|
1952
|
-
...scope.getSnapshot().value?.music ?? {}
|
|
1953
|
-
};
|
|
2300
|
+
const doc = scope.getSnapshot().value;
|
|
2301
|
+
const current = musicDefaults(doc?.music);
|
|
1954
2302
|
current.volume = Math.max(0, Math.min(1, volume));
|
|
1955
2303
|
scope.set("music", current);
|
|
1956
2304
|
};
|
|
1957
2305
|
const setMusicStyle = (style) => {
|
|
1958
|
-
const
|
|
1959
|
-
|
|
1960
|
-
volume: .5,
|
|
1961
|
-
style: "major",
|
|
1962
|
-
...scope.getSnapshot().value?.music ?? {}
|
|
1963
|
-
};
|
|
2306
|
+
const doc = scope.getSnapshot().value;
|
|
2307
|
+
const current = musicDefaults(doc?.music);
|
|
1964
2308
|
current.style = style;
|
|
1965
2309
|
scope.set("music", current);
|
|
1966
2310
|
};
|
|
2311
|
+
const setMusicOnlyCurrentSession = (only) => {
|
|
2312
|
+
const doc = scope.getSnapshot().value;
|
|
2313
|
+
const current = musicDefaults(doc?.music);
|
|
2314
|
+
current.onlyCurrentSession = only;
|
|
2315
|
+
scope.set("music", current);
|
|
2316
|
+
};
|
|
1967
2317
|
/** Ask the Host to check the registry right now, then refresh the snapshot. */
|
|
1968
2318
|
const checkUpdatesNow = () => {
|
|
1969
2319
|
bound?.setCheckingUpdates(true);
|
|
@@ -2050,7 +2400,8 @@ window.__ModuleLoader__.load({
|
|
|
2050
2400
|
setDimWhenIdle,
|
|
2051
2401
|
setMusicEnabled,
|
|
2052
2402
|
setMusicVolume,
|
|
2053
|
-
setMusicStyle
|
|
2403
|
+
setMusicStyle,
|
|
2404
|
+
setMusicOnlyCurrentSession
|
|
2054
2405
|
};
|
|
2055
2406
|
};
|
|
2056
2407
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|