@jxgame2020/dsh-token-quota 0.1.27 → 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 +123 -182
- package/lib/index.js +1 -1
- package/lib/types/client/music.d.ts +23 -27
- package/lib/types/client/music.js +62 -126
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -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
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"input": "NPLLIq_input",
|
|
204
|
+
"gearBtn": "NPLLIq_gearBtn",
|
|
205
|
+
"title": "NPLLIq_title",
|
|
206
|
+
"checkErrorHint": "NPLLIq_checkErrorHint",
|
|
207
|
+
"collapse": "NPLLIq_collapse",
|
|
208
|
+
"dialogTitle": "NPLLIq_dialogTitle",
|
|
209
|
+
"logDayCol": "NPLLIq_logDayCol",
|
|
210
|
+
"upgradeBannerText": "NPLLIq_upgradeBannerText",
|
|
211
|
+
"fillOver": "NPLLIq_fillOver",
|
|
213
212
|
"monitorHint": "NPLLIq_monitorHint",
|
|
213
|
+
"logClearDialog": "NPLLIq_logClearDialog",
|
|
214
|
+
"dialogClose": "NPLLIq_dialogClose",
|
|
214
215
|
"monitorName": "NPLLIq_monitorName",
|
|
215
|
-
"checkErrorHint": "NPLLIq_checkErrorHint",
|
|
216
|
-
"fillWarn": "NPLLIq_fillWarn",
|
|
217
|
-
"dialogBtn": "NPLLIq_dialogBtn",
|
|
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",
|
|
226
|
-
"upgradeCommandRow": "NPLLIq_upgradeCommandRow",
|
|
227
216
|
"dialog": "NPLLIq_dialog",
|
|
228
|
-
"upgradeHint": "NPLLIq_upgradeHint",
|
|
229
|
-
"dialogHeader": "NPLLIq_dialogHeader",
|
|
230
|
-
"rowMeta": "NPLLIq_rowMeta",
|
|
231
|
-
"upgradeBannerText": "NPLLIq_upgradeBannerText",
|
|
232
|
-
"checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
|
|
233
|
-
"dialogLabel": "NPLLIq_dialogLabel",
|
|
234
|
-
"logPagerBtn": "NPLLIq_logPagerBtn",
|
|
235
217
|
"logClearBtn": "NPLLIq_logClearBtn",
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"panelDim": "NPLLIq_panelDim",
|
|
239
|
-
"body": "NPLLIq_body",
|
|
240
|
-
"dialogClose": "NPLLIq_dialogClose",
|
|
218
|
+
"rowMeta": "NPLLIq_rowMeta",
|
|
219
|
+
"logSearch": "NPLLIq_logSearch",
|
|
241
220
|
"selectBtn": "NPLLIq_selectBtn",
|
|
242
|
-
"
|
|
243
|
-
"
|
|
221
|
+
"panelDim": "NPLLIq_panelDim",
|
|
222
|
+
"upgradeBanner": "NPLLIq_upgradeBanner",
|
|
223
|
+
"rowHeader": "NPLLIq_rowHeader",
|
|
224
|
+
"checkBtn": "NPLLIq_checkBtn",
|
|
225
|
+
"dialogHint": "NPLLIq_dialogHint",
|
|
226
|
+
"logPagerInfo": "NPLLIq_logPagerInfo",
|
|
227
|
+
"logModelCol": "NPLLIq_logModelCol",
|
|
228
|
+
"upgradeClose": "NPLLIq_upgradeClose",
|
|
229
|
+
"resetRow": "NPLLIq_resetRow",
|
|
244
230
|
"dialogActions": "NPLLIq_dialogActions",
|
|
245
|
-
"
|
|
246
|
-
"dialogTitle": "NPLLIq_dialogTitle",
|
|
247
|
-
"headerText": "NPLLIq_headerText",
|
|
248
|
-
"collapse": "NPLLIq_collapse",
|
|
231
|
+
"upgradeCommandRow": "NPLLIq_upgradeCommandRow",
|
|
249
232
|
"currentBadge": "NPLLIq_currentBadge",
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"gearBtn": "NPLLIq_gearBtn",
|
|
254
|
-
"logPageSizeSelect": "NPLLIq_logPageSizeSelect",
|
|
255
|
-
"dialogHint": "NPLLIq_dialogHint",
|
|
233
|
+
"upgradeHint": "NPLLIq_upgradeHint",
|
|
234
|
+
"dialogHeader": "NPLLIq_dialogHeader",
|
|
235
|
+
"logScroll": "NPLLIq_logScroll",
|
|
256
236
|
"versionNew": "NPLLIq_versionNew",
|
|
257
|
-
"
|
|
258
|
-
"upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
|
|
259
|
-
"switch": "NPLLIq_switch",
|
|
237
|
+
"header": "NPLLIq_header",
|
|
260
238
|
"logToolbar": "NPLLIq_logToolbar",
|
|
261
|
-
"
|
|
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",
|
|
239
|
+
"logPageSizeSelect": "NPLLIq_logPageSizeSelect",
|
|
273
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",
|
|
250
|
+
"checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
|
|
251
|
+
"copyBtnDone": "NPLLIq_copyBtnDone",
|
|
252
|
+
"logHeader": "NPLLIq_logHeader",
|
|
253
|
+
"headerText": "NPLLIq_headerText",
|
|
254
|
+
"fillWarn": "NPLLIq_fillWarn",
|
|
255
|
+
"upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
|
|
256
|
+
"fullNotice": "NPLLIq_fullNotice",
|
|
257
|
+
"upgradeCommand": "NPLLIq_upgradeCommand",
|
|
274
258
|
"dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
|
|
259
|
+
"logPagerPages": "NPLLIq_logPagerPages",
|
|
275
260
|
"upgradeCommands": "NPLLIq_upgradeCommands",
|
|
276
|
-
"
|
|
261
|
+
"save": "NPLLIq_save",
|
|
262
|
+
"logClearDate": "NPLLIq_logClearDate",
|
|
263
|
+
"settingsBtn": "NPLLIq_settingsBtn",
|
|
277
264
|
"logPager": "NPLLIq_logPager",
|
|
278
|
-
"
|
|
265
|
+
"dialogOverlay": "NPLLIq_dialogOverlay",
|
|
279
266
|
"logDialog": "NPLLIq_logDialog",
|
|
280
|
-
"
|
|
267
|
+
"headerActions": "NPLLIq_headerActions",
|
|
281
268
|
"monitorList": "NPLLIq_monitorList",
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"tabLabel": "NPLLIq_tabLabel",
|
|
286
|
-
"notice": "NPLLIq_notice",
|
|
287
|
-
"upgradeCommand": "NPLLIq_upgradeCommand",
|
|
269
|
+
"bar": "NPLLIq_bar",
|
|
270
|
+
"input": "NPLLIq_input",
|
|
271
|
+
"noticeError": "NPLLIq_noticeError",
|
|
288
272
|
"fillIdle": "NPLLIq_fillIdle",
|
|
289
|
-
"
|
|
273
|
+
"copyBtn": "NPLLIq_copyBtn",
|
|
274
|
+
"logPagerControls": "NPLLIq_logPagerControls",
|
|
275
|
+
"logPagerBtn": "NPLLIq_logPagerBtn",
|
|
276
|
+
"radioRow": "NPLLIq_radioRow",
|
|
277
|
+
"rowName": "NPLLIq_rowName",
|
|
278
|
+
"controls": "NPLLIq_controls",
|
|
279
|
+
"resetSelect": "NPLLIq_resetSelect",
|
|
280
|
+
"tab": "NPLLIq_tab",
|
|
281
|
+
"notice": "NPLLIq_notice",
|
|
282
|
+
"body": "NPLLIq_body",
|
|
283
|
+
"tabIcon": "NPLLIq_tabIcon",
|
|
284
|
+
"panelDimStrong": "NPLLIq_panelDimStrong",
|
|
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,
|
|
@@ -1235,21 +1235,25 @@ window.__ModuleLoader__.load({
|
|
|
1235
1235
|
* 古风 live-composed soundtrack driven by host actions.
|
|
1236
1236
|
*
|
|
1237
1237
|
* Design: when a request starts the engine composes a *piece* — a guzheng-like
|
|
1238
|
-
* plucked-string voice
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
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.
|
|
1244
|
+
*
|
|
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.
|
|
1245
1250
|
*
|
|
1246
|
-
* Anti-runaway guards
|
|
1251
|
+
* Anti-runaway guards:
|
|
1247
1252
|
* - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
|
|
1248
|
-
* engine
|
|
1249
|
-
* - every MIDI NoteOn is registered and swept;
|
|
1253
|
+
* engine stops (a dead stream means no stop event will ever come),
|
|
1254
|
+
* - every MIDI NoteOn is registered and swept; stops also send
|
|
1250
1255
|
* All Notes Off (CC 123),
|
|
1251
|
-
* - Web Audio
|
|
1252
|
-
* explicitly stopped.
|
|
1256
|
+
* - every Web Audio oscillator is explicitly stopped at envelope end.
|
|
1253
1257
|
*
|
|
1254
1258
|
* @module @jxgame2020/dsh-token-quota/music
|
|
1255
1259
|
*/
|
|
@@ -1489,11 +1493,9 @@ window.__ModuleLoader__.load({
|
|
|
1489
1493
|
playing = false;
|
|
1490
1494
|
tonicIndex = 0;
|
|
1491
1495
|
phraseIndex = 0;
|
|
1496
|
+
/** True when the next piece start should play the full intro glissando. */
|
|
1497
|
+
introPending = true;
|
|
1492
1498
|
phraseTimer;
|
|
1493
|
-
droneOscs = [];
|
|
1494
|
-
droneGain = null;
|
|
1495
|
-
droneFilter = null;
|
|
1496
|
-
droneActive = false;
|
|
1497
1499
|
lastStreamActivity = 0;
|
|
1498
1500
|
watchdogTimer;
|
|
1499
1501
|
activeMidiNotes = /* @__PURE__ */ new Set();
|
|
@@ -1567,6 +1569,7 @@ window.__ModuleLoader__.load({
|
|
|
1567
1569
|
this.stopPiece(false);
|
|
1568
1570
|
this.tonicIndex = 0;
|
|
1569
1571
|
this.phraseIndex = 0;
|
|
1572
|
+
this.introPending = true;
|
|
1570
1573
|
break;
|
|
1571
1574
|
case "request/header":
|
|
1572
1575
|
if (!this.playing) this.startPiece();
|
|
@@ -1575,9 +1578,10 @@ window.__ModuleLoader__.load({
|
|
|
1575
1578
|
case "step/start": {
|
|
1576
1579
|
const prog = PROGRESSION[this.style];
|
|
1577
1580
|
this.tonicIndex = (this.tonicIndex + 1) % prog.length;
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
+
if (this.playing) {
|
|
1582
|
+
const newRoot = base + (prog[this.tonicIndex] ?? 0);
|
|
1583
|
+
this.insertFill(newRoot);
|
|
1584
|
+
}
|
|
1581
1585
|
break;
|
|
1582
1586
|
}
|
|
1583
1587
|
case "tool/call":
|
|
@@ -1600,6 +1604,7 @@ window.__ModuleLoader__.load({
|
|
|
1600
1604
|
break;
|
|
1601
1605
|
case "assistant/message":
|
|
1602
1606
|
this.stopMelody();
|
|
1607
|
+
this.playing = false;
|
|
1603
1608
|
this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
|
|
1604
1609
|
break;
|
|
1605
1610
|
case "turn/end":
|
|
@@ -1607,29 +1612,36 @@ window.__ModuleLoader__.load({
|
|
|
1607
1612
|
break;
|
|
1608
1613
|
}
|
|
1609
1614
|
}
|
|
1610
|
-
/** Start a new piece:
|
|
1615
|
+
/** Start a new piece: intro run, then the phrase chain. */
|
|
1611
1616
|
startPiece() {
|
|
1612
1617
|
if (!this.audio || this.playing) return;
|
|
1613
1618
|
this.playing = true;
|
|
1614
|
-
this.tonicIndex
|
|
1615
|
-
this.phraseIndex = 0;
|
|
1616
|
-
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][0] ?? 0);
|
|
1617
|
-
this.startDrone(root);
|
|
1619
|
+
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
|
|
1618
1620
|
const scale = SCALES[this.style];
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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);
|
|
1625
1637
|
}
|
|
1626
|
-
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
|
|
1627
1638
|
}
|
|
1628
1639
|
/** Play one phrase, then chain into the next after a one-beat rest. */
|
|
1629
1640
|
playPhrase() {
|
|
1630
1641
|
if (!this.playing || !this.audio) return;
|
|
1631
1642
|
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
|
|
1632
1643
|
const scale = SCALES[this.style];
|
|
1644
|
+
this.pluck(root - 12, .28, 1.6);
|
|
1633
1645
|
const phrase = PHRASES[this.phraseIndex % PHRASES.length];
|
|
1634
1646
|
this.phraseIndex += 1;
|
|
1635
1647
|
let t = 0;
|
|
@@ -1657,7 +1669,7 @@ window.__ModuleLoader__.load({
|
|
|
1657
1669
|
}
|
|
1658
1670
|
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
|
|
1659
1671
|
}
|
|
1660
|
-
/** Stop the
|
|
1672
|
+
/** Stop the phrase chain. */
|
|
1661
1673
|
stopMelody() {
|
|
1662
1674
|
if (this.phraseTimer !== void 0) {
|
|
1663
1675
|
clearTimeout(this.phraseTimer);
|
|
@@ -1673,8 +1685,7 @@ window.__ModuleLoader__.load({
|
|
|
1673
1685
|
const scale = SCALES[this.style];
|
|
1674
1686
|
let t = 0;
|
|
1675
1687
|
for (let i = 7; i >= 0; i--) {
|
|
1676
|
-
const
|
|
1677
|
-
const note = base + 12 * Math.floor(i / scale.length) + (scale[deg] ?? 0);
|
|
1688
|
+
const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
|
|
1678
1689
|
setTimeout(() => this.pluck(note, .3), t);
|
|
1679
1690
|
t += 120;
|
|
1680
1691
|
}
|
|
@@ -1685,86 +1696,16 @@ window.__ModuleLoader__.load({
|
|
|
1685
1696
|
this.pluck(base - 12, .5);
|
|
1686
1697
|
}, t + 150);
|
|
1687
1698
|
}
|
|
1688
|
-
this.fadeDrone(final ? 1.2 : .8);
|
|
1689
1699
|
this.allMidiOff();
|
|
1690
1700
|
}
|
|
1691
1701
|
/** Hard stop everything (master switch off / teardown). */
|
|
1692
1702
|
stopAll() {
|
|
1693
1703
|
this.stopPiece(false);
|
|
1694
1704
|
}
|
|
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;
|
|
1763
|
-
}
|
|
1764
1705
|
/**
|
|
1765
|
-
* Guzheng-like pluck:
|
|
1766
|
-
* lowpass, fast attack and a long exponential decay (notes
|
|
1767
|
-
* keeps the piece continuous instead of choppy).
|
|
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).
|
|
1768
1709
|
*/
|
|
1769
1710
|
pluck(midi, velocity, decaySec = 1.3) {
|
|
1770
1711
|
if (!this.audio) return;
|
|
@@ -1788,9 +1729,9 @@ window.__ModuleLoader__.load({
|
|
|
1788
1729
|
osc1.frequency.value = freq;
|
|
1789
1730
|
const osc2 = this.audio.createOscillator();
|
|
1790
1731
|
osc2.type = "sine";
|
|
1791
|
-
osc2.frequency.value = freq * 2
|
|
1732
|
+
osc2.frequency.value = freq * 2;
|
|
1792
1733
|
const octGain = this.audio.createGain();
|
|
1793
|
-
octGain.gain.value = .
|
|
1734
|
+
octGain.gain.value = .25;
|
|
1794
1735
|
osc1.connect(filter);
|
|
1795
1736
|
osc2.connect(octGain);
|
|
1796
1737
|
octGain.connect(filter);
|
package/lib/index.js
CHANGED
|
@@ -515,7 +515,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
515
515
|
return this.upgradeCheck;
|
|
516
516
|
}
|
|
517
517
|
async doRefreshUpgrade() {
|
|
518
|
-
const current = "0.1.
|
|
518
|
+
const current = "0.1.28";
|
|
519
519
|
let latest;
|
|
520
520
|
try {
|
|
521
521
|
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
* 古风 live-composed soundtrack driven by host actions.
|
|
3
3
|
*
|
|
4
4
|
* Design: when a request starts the engine composes a *piece* — a guzheng-like
|
|
5
|
-
* plucked-string voice
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* breath note and a descending glissando.
|
|
5
|
+
* plucked-string voice (Web Audio synthesis; the MIDI output is switched to
|
|
6
|
+
* the Koto program so hardware synths match). The piece plays phrase after
|
|
7
|
+
* phrase so it stays alive while the model thinks or streams; incoming events
|
|
8
|
+
* steer the piece instead of interrupting it: a step change rotates the mode
|
|
9
|
+
* centre and inserts a fast 过门 (fill); tool calls get a high plink; the
|
|
10
|
+
* ending gets a dizi-like breath note and a descending glissando.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
12
|
+
* **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
|
|
13
|
+
* or a fixed-duration breath note — there is no drone/pad oscillator that
|
|
14
|
+
* depends on a later stop event. A lost SSE event can at worst leave the
|
|
15
|
+
* phrase chain running, and the stream watchdog below stops that too, so a
|
|
16
|
+
* hanging "呜呜" hum is physically impossible.
|
|
17
|
+
*
|
|
18
|
+
* Anti-runaway guards:
|
|
14
19
|
* - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
|
|
15
|
-
* engine
|
|
16
|
-
* - every MIDI NoteOn is registered and swept;
|
|
20
|
+
* engine stops (a dead stream means no stop event will ever come),
|
|
21
|
+
* - every MIDI NoteOn is registered and swept; stops also send
|
|
17
22
|
* All Notes Off (CC 123),
|
|
18
|
-
* - Web Audio
|
|
19
|
-
* explicitly stopped.
|
|
23
|
+
* - every Web Audio oscillator is explicitly stopped at envelope end.
|
|
20
24
|
*
|
|
21
25
|
* @module @jxgame2020/dsh-token-quota/music
|
|
22
26
|
*/
|
|
@@ -36,11 +40,9 @@ export declare class TokenQuotaMusic {
|
|
|
36
40
|
private playing;
|
|
37
41
|
private tonicIndex;
|
|
38
42
|
private phraseIndex;
|
|
43
|
+
/** True when the next piece start should play the full intro glissando. */
|
|
44
|
+
private introPending;
|
|
39
45
|
private phraseTimer;
|
|
40
|
-
private droneOscs;
|
|
41
|
-
private droneGain;
|
|
42
|
-
private droneFilter;
|
|
43
|
-
private droneActive;
|
|
44
46
|
private lastStreamActivity;
|
|
45
47
|
private watchdogTimer;
|
|
46
48
|
private readonly activeMidiNotes;
|
|
@@ -64,28 +66,22 @@ export declare class TokenQuotaMusic {
|
|
|
64
66
|
markStreamActivity(): void;
|
|
65
67
|
/** Handle one incoming host action and steer the piece. */
|
|
66
68
|
onAction(action: TokenQuotaMusicAction): void;
|
|
67
|
-
/** Start a new piece:
|
|
69
|
+
/** Start a new piece: intro run, then the phrase chain. */
|
|
68
70
|
private startPiece;
|
|
69
71
|
/** Play one phrase, then chain into the next after a one-beat rest. */
|
|
70
72
|
private playPhrase;
|
|
71
73
|
/** Insert a 过门 fill: pause the chain, play the run, resume after. */
|
|
72
74
|
private insertFill;
|
|
73
|
-
/** Stop the
|
|
75
|
+
/** Stop the phrase chain. */
|
|
74
76
|
private stopMelody;
|
|
75
77
|
/** Stop the whole piece; `final` adds the closing cadence gestures. */
|
|
76
78
|
private stopPiece;
|
|
77
79
|
/** Hard stop everything (master switch off / teardown). */
|
|
78
80
|
private stopAll;
|
|
79
|
-
/** Start the sustained open fifth (root + fifth below octave root). */
|
|
80
|
-
private startDrone;
|
|
81
|
-
/** Glide the drone to a new tonic (mode rotation). */
|
|
82
|
-
private glideDrone;
|
|
83
|
-
/** Fade the drone out and stop its oscillators. */
|
|
84
|
-
private fadeDrone;
|
|
85
81
|
/**
|
|
86
|
-
* Guzheng-like pluck:
|
|
87
|
-
* lowpass, fast attack and a long exponential decay (notes
|
|
88
|
-
* keeps the piece continuous instead of choppy).
|
|
82
|
+
* Guzheng-like pluck: a triangle voice plus a soft octave partial through a
|
|
83
|
+
* sweeping lowpass, fast attack and a long exponential decay (notes
|
|
84
|
+
* overlap, which keeps the piece continuous instead of choppy).
|
|
89
85
|
*/
|
|
90
86
|
private pluck;
|
|
91
87
|
/** Dizi-like breath note: sine with vibrato and a soft envelope. */
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
* 古风 live-composed soundtrack driven by host actions.
|
|
3
3
|
*
|
|
4
4
|
* Design: when a request starts the engine composes a *piece* — a guzheng-like
|
|
5
|
-
* plucked-string voice
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* breath note and a descending glissando.
|
|
5
|
+
* plucked-string voice (Web Audio synthesis; the MIDI output is switched to
|
|
6
|
+
* the Koto program so hardware synths match). The piece plays phrase after
|
|
7
|
+
* phrase so it stays alive while the model thinks or streams; incoming events
|
|
8
|
+
* steer the piece instead of interrupting it: a step change rotates the mode
|
|
9
|
+
* centre and inserts a fast 过门 (fill); tool calls get a high plink; the
|
|
10
|
+
* ending gets a dizi-like breath note and a descending glissando.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
12
|
+
* **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
|
|
13
|
+
* or a fixed-duration breath note — there is no drone/pad oscillator that
|
|
14
|
+
* depends on a later stop event. A lost SSE event can at worst leave the
|
|
15
|
+
* phrase chain running, and the stream watchdog below stops that too, so a
|
|
16
|
+
* hanging "呜呜" hum is physically impossible.
|
|
17
|
+
*
|
|
18
|
+
* Anti-runaway guards:
|
|
14
19
|
* - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
|
|
15
|
-
* engine
|
|
16
|
-
* - every MIDI NoteOn is registered and swept;
|
|
20
|
+
* engine stops (a dead stream means no stop event will ever come),
|
|
21
|
+
* - every MIDI NoteOn is registered and swept; stops also send
|
|
17
22
|
* All Notes Off (CC 123),
|
|
18
|
-
* - Web Audio
|
|
19
|
-
* explicitly stopped.
|
|
23
|
+
* - every Web Audio oscillator is explicitly stopped at envelope end.
|
|
20
24
|
*
|
|
21
25
|
* @module @jxgame2020/dsh-token-quota/music
|
|
22
26
|
*/
|
|
@@ -74,12 +78,9 @@ export class TokenQuotaMusic {
|
|
|
74
78
|
playing = false;
|
|
75
79
|
tonicIndex = 0;
|
|
76
80
|
phraseIndex = 0;
|
|
81
|
+
/** True when the next piece start should play the full intro glissando. */
|
|
82
|
+
introPending = true;
|
|
77
83
|
phraseTimer;
|
|
78
|
-
// Drone layer (sustained open fifth under the plucks)
|
|
79
|
-
droneOscs = [];
|
|
80
|
-
droneGain = null;
|
|
81
|
-
droneFilter = null;
|
|
82
|
-
droneActive = false;
|
|
83
84
|
// Watchdog: host heartbeat freshness (ms timestamp)
|
|
84
85
|
lastStreamActivity = 0;
|
|
85
86
|
watchdogTimer;
|
|
@@ -138,7 +139,7 @@ export class TokenQuotaMusic {
|
|
|
138
139
|
this.lastStreamActivity = Date.now();
|
|
139
140
|
this.watchdogTimer = setInterval(() => {
|
|
140
141
|
if (this.playing && Date.now() - this.lastStreamActivity > TokenQuotaMusic.STREAM_TIMEOUT_MS) {
|
|
141
|
-
// Dead stream: no stop event will ever arrive —
|
|
142
|
+
// Dead stream: no stop event will ever arrive — stop the piece.
|
|
142
143
|
this.stopPiece(false);
|
|
143
144
|
}
|
|
144
145
|
}, 4_000);
|
|
@@ -170,6 +171,7 @@ export class TokenQuotaMusic {
|
|
|
170
171
|
this.stopPiece(false);
|
|
171
172
|
this.tonicIndex = 0;
|
|
172
173
|
this.phraseIndex = 0;
|
|
174
|
+
this.introPending = true;
|
|
173
175
|
break;
|
|
174
176
|
case 'request/header': {
|
|
175
177
|
if (!this.playing)
|
|
@@ -179,13 +181,13 @@ export class TokenQuotaMusic {
|
|
|
179
181
|
break;
|
|
180
182
|
}
|
|
181
183
|
case 'step/start': {
|
|
182
|
-
// New phase: rotate the
|
|
184
|
+
// New phase: rotate the mode centre and insert a 过门 fill.
|
|
183
185
|
const prog = PROGRESSION[this.style];
|
|
184
186
|
this.tonicIndex = (this.tonicIndex + 1) % prog.length;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (this.playing)
|
|
187
|
+
if (this.playing) {
|
|
188
|
+
const newRoot = base + (prog[this.tonicIndex] ?? 0);
|
|
188
189
|
this.insertFill(newRoot);
|
|
190
|
+
}
|
|
189
191
|
break;
|
|
190
192
|
}
|
|
191
193
|
case 'tool/call':
|
|
@@ -211,9 +213,10 @@ export class TokenQuotaMusic {
|
|
|
211
213
|
setTimeout(() => this.pluck(base + 5, 0.4, 0.8), 220);
|
|
212
214
|
break;
|
|
213
215
|
case 'assistant/message':
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
+
// Sentence end: stop the phrase chain and breathe. The next
|
|
217
|
+
// request/header (next step or next turn) starts a fresh phrase.
|
|
216
218
|
this.stopMelody();
|
|
219
|
+
this.playing = false;
|
|
217
220
|
this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
|
|
218
221
|
break;
|
|
219
222
|
case 'turn/end':
|
|
@@ -222,27 +225,37 @@ export class TokenQuotaMusic {
|
|
|
222
225
|
}
|
|
223
226
|
}
|
|
224
227
|
// ── piece lifecycle ───────────────────────────────────────────────────
|
|
225
|
-
/** Start a new piece:
|
|
228
|
+
/** Start a new piece: intro run, then the phrase chain. */
|
|
226
229
|
startPiece() {
|
|
227
230
|
if (!this.audio || this.playing)
|
|
228
231
|
return;
|
|
229
232
|
this.playing = true;
|
|
230
|
-
this.tonicIndex
|
|
231
|
-
this.phraseIndex = 0;
|
|
232
|
-
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][0] ?? 0);
|
|
233
|
-
this.startDrone(root);
|
|
234
|
-
// Intro glissando — the guzheng sweep that announces the piece.
|
|
233
|
+
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
|
|
235
234
|
const scale = SCALES[this.style];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
235
|
+
if (this.introPending) {
|
|
236
|
+
// Full intro: the ascending guzheng glissando that announces the piece.
|
|
237
|
+
this.introPending = false;
|
|
238
|
+
let t = 0;
|
|
239
|
+
for (let i = 0; i < 8; i++) {
|
|
240
|
+
const note = root + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (this.playing)
|
|
243
|
+
this.pluck(note, 0.3);
|
|
244
|
+
}, t);
|
|
245
|
+
t += 150;
|
|
246
|
+
}
|
|
247
|
+
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Restart within the same turn: a short three-note pickup, no glissando.
|
|
251
|
+
for (let i = 0; i < 3; i++) {
|
|
252
|
+
setTimeout(() => {
|
|
253
|
+
if (this.playing)
|
|
254
|
+
this.pluck(root + 12 + (scale[i] ?? 0), 0.3);
|
|
255
|
+
}, i * 140);
|
|
256
|
+
}
|
|
257
|
+
this.phraseTimer = setTimeout(() => this.playPhrase(), 480);
|
|
243
258
|
}
|
|
244
|
-
// First phrase starts right after the glissando.
|
|
245
|
-
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
|
|
246
259
|
}
|
|
247
260
|
/** Play one phrase, then chain into the next after a one-beat rest. */
|
|
248
261
|
playPhrase() {
|
|
@@ -250,6 +263,9 @@ export class TokenQuotaMusic {
|
|
|
250
263
|
return;
|
|
251
264
|
const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
|
|
252
265
|
const scale = SCALES[this.style];
|
|
266
|
+
// Soft low root pluck under each phrase — a foundation that decays away
|
|
267
|
+
// instead of a sustained drone (nothing here can hang).
|
|
268
|
+
this.pluck(root - 12, 0.28, 1.6);
|
|
253
269
|
const phrase = PHRASES[this.phraseIndex % PHRASES.length];
|
|
254
270
|
this.phraseIndex += 1;
|
|
255
271
|
let t = 0;
|
|
@@ -282,7 +298,7 @@ export class TokenQuotaMusic {
|
|
|
282
298
|
}
|
|
283
299
|
this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
|
|
284
300
|
}
|
|
285
|
-
/** Stop the
|
|
301
|
+
/** Stop the phrase chain. */
|
|
286
302
|
stopMelody() {
|
|
287
303
|
if (this.phraseTimer !== undefined) {
|
|
288
304
|
clearTimeout(this.phraseTimer);
|
|
@@ -299,9 +315,7 @@ export class TokenQuotaMusic {
|
|
|
299
315
|
const scale = SCALES[this.style];
|
|
300
316
|
let t = 0;
|
|
301
317
|
for (let i = 7; i >= 0; i--) {
|
|
302
|
-
const
|
|
303
|
-
const oct = Math.floor(i / scale.length);
|
|
304
|
-
const note = base + 12 * oct + (scale[deg] ?? 0);
|
|
318
|
+
const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
|
|
305
319
|
setTimeout(() => this.pluck(note, 0.3), t);
|
|
306
320
|
t += 120;
|
|
307
321
|
}
|
|
@@ -312,95 +326,17 @@ export class TokenQuotaMusic {
|
|
|
312
326
|
this.pluck(base - 12, 0.5);
|
|
313
327
|
}, t + 150);
|
|
314
328
|
}
|
|
315
|
-
this.fadeDrone(final ? 1.2 : 0.8);
|
|
316
329
|
this.allMidiOff();
|
|
317
330
|
}
|
|
318
331
|
/** Hard stop everything (master switch off / teardown). */
|
|
319
332
|
stopAll() {
|
|
320
333
|
this.stopPiece(false);
|
|
321
334
|
}
|
|
322
|
-
// ── drone layer ───────────────────────────────────────────────────────
|
|
323
|
-
/** Start the sustained open fifth (root + fifth below octave root). */
|
|
324
|
-
startDrone(root) {
|
|
325
|
-
if (!this.audio || this.droneActive)
|
|
326
|
-
return;
|
|
327
|
-
const now = this.audio.currentTime;
|
|
328
|
-
const gain = this.audio.createGain();
|
|
329
|
-
gain.gain.setValueAtTime(0, now);
|
|
330
|
-
const filter = this.audio.createBiquadFilter();
|
|
331
|
-
filter.type = 'lowpass';
|
|
332
|
-
filter.frequency.value = 620;
|
|
333
|
-
filter.Q.value = 0.4;
|
|
334
|
-
gain.connect(filter);
|
|
335
|
-
filter.connect(this.audio.destination);
|
|
336
|
-
const oscs = [];
|
|
337
|
-
for (const interval of [-12, -5]) {
|
|
338
|
-
const osc = this.audio.createOscillator();
|
|
339
|
-
osc.type = 'sine';
|
|
340
|
-
osc.frequency.value = midiToFreq(root + interval);
|
|
341
|
-
osc.connect(gain);
|
|
342
|
-
osc.start(now);
|
|
343
|
-
oscs.push(osc);
|
|
344
|
-
}
|
|
345
|
-
this.droneOscs = oscs;
|
|
346
|
-
this.droneGain = gain;
|
|
347
|
-
this.droneFilter = filter;
|
|
348
|
-
this.droneActive = true;
|
|
349
|
-
gain.gain.linearRampToValueAtTime(this.volume * 0.07, now + 1.4);
|
|
350
|
-
}
|
|
351
|
-
/** Glide the drone to a new tonic (mode rotation). */
|
|
352
|
-
glideDrone(root) {
|
|
353
|
-
if (!this.audio || !this.droneActive)
|
|
354
|
-
return;
|
|
355
|
-
const now = this.audio.currentTime;
|
|
356
|
-
const targets = [root - 12, root - 5];
|
|
357
|
-
this.droneOscs.forEach((osc, i) => {
|
|
358
|
-
const target = targets[i] ?? root;
|
|
359
|
-
osc.frequency.setValueAtTime(osc.frequency.value, now);
|
|
360
|
-
osc.frequency.exponentialRampToValueAtTime(midiToFreq(target), now + 0.9);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
/** Fade the drone out and stop its oscillators. */
|
|
364
|
-
fadeDrone(seconds) {
|
|
365
|
-
if (!this.audio || !this.droneActive || this.droneGain === null)
|
|
366
|
-
return;
|
|
367
|
-
const now = this.audio.currentTime;
|
|
368
|
-
const gain = this.droneGain;
|
|
369
|
-
gain.gain.cancelScheduledValues(now);
|
|
370
|
-
gain.gain.setValueAtTime(Math.max(gain.gain.value, 0.0001), now);
|
|
371
|
-
gain.gain.exponentialRampToValueAtTime(0.0001, now + seconds);
|
|
372
|
-
const oscs = this.droneOscs;
|
|
373
|
-
const filter = this.droneFilter;
|
|
374
|
-
setTimeout(() => {
|
|
375
|
-
for (const osc of oscs) {
|
|
376
|
-
try {
|
|
377
|
-
osc.stop();
|
|
378
|
-
}
|
|
379
|
-
catch { /* ignore */ }
|
|
380
|
-
try {
|
|
381
|
-
osc.disconnect();
|
|
382
|
-
}
|
|
383
|
-
catch { /* ignore */ }
|
|
384
|
-
}
|
|
385
|
-
try {
|
|
386
|
-
filter?.disconnect();
|
|
387
|
-
}
|
|
388
|
-
catch { /* ignore */ }
|
|
389
|
-
try {
|
|
390
|
-
gain.disconnect();
|
|
391
|
-
}
|
|
392
|
-
catch { /* ignore */ }
|
|
393
|
-
}, seconds * 1000 + 120);
|
|
394
|
-
this.droneActive = false;
|
|
395
|
-
this.droneOscs = [];
|
|
396
|
-
this.droneGain = null;
|
|
397
|
-
this.droneFilter = null;
|
|
398
|
-
}
|
|
399
335
|
// ── voices ────────────────────────────────────────────────────────────
|
|
400
336
|
/**
|
|
401
|
-
* Guzheng-like pluck:
|
|
402
|
-
* lowpass, fast attack and a long exponential decay (notes
|
|
403
|
-
* keeps the piece continuous instead of choppy).
|
|
337
|
+
* Guzheng-like pluck: a triangle voice plus a soft octave partial through a
|
|
338
|
+
* sweeping lowpass, fast attack and a long exponential decay (notes
|
|
339
|
+
* overlap, which keeps the piece continuous instead of choppy).
|
|
404
340
|
*/
|
|
405
341
|
pluck(midi, velocity, decaySec = 1.3) {
|
|
406
342
|
if (!this.audio)
|
|
@@ -426,9 +362,9 @@ export class TokenQuotaMusic {
|
|
|
426
362
|
osc1.frequency.value = freq;
|
|
427
363
|
const osc2 = this.audio.createOscillator();
|
|
428
364
|
osc2.type = 'sine';
|
|
429
|
-
osc2.frequency.value = freq * 2
|
|
365
|
+
osc2.frequency.value = freq * 2; // octave partial (no detune beating)
|
|
430
366
|
const octGain = this.audio.createGain();
|
|
431
|
-
octGain.gain.value = 0.
|
|
367
|
+
octGain.gain.value = 0.25;
|
|
432
368
|
osc1.connect(filter);
|
|
433
369
|
osc2.connect(octGain);
|
|
434
370
|
octGain.connect(filter);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxgame2020/dsh-token-quota",
|
|
3
3
|
"description": "Daily per-model token quota accounting, enforcement, and a floating quota panel for the DeepSeek Harness — a fully self-contained plugin (no core-Harness wiring, no generated RPC contract)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.28",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|