jsbeeb 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/6502.js +26 -21
- package/src/6502.opcodes.js +20 -15
- package/src/acia.js +21 -7
- package/src/bbc-palette.js +24 -0
- package/src/cmos.js +12 -1
- package/src/config.js +23 -0
- package/src/fake6502.js +9 -2
- package/src/fdc.js +51 -54
- package/src/keyboard.js +7 -4
- package/src/machine-session.js +13 -3
- package/src/main.js +93 -32
- package/src/models.js +111 -83
- package/src/soundchip.js +78 -0
- package/src/speech-output.js +85 -0
- package/src/teletext.js +31 -16
- package/src/via.js +1 -1
- package/src/video.js +206 -34
- package/src/wd-fdc.js +6 -1
- package/src/web/audio-handler.js +1 -1
package/src/main.js
CHANGED
|
@@ -27,6 +27,7 @@ import { toHfe } from "./disc-hfe.js";
|
|
|
27
27
|
import { Keyboard } from "./keyboard.js";
|
|
28
28
|
import { GamepadSource } from "./gamepad-source.js";
|
|
29
29
|
import { MicrophoneInput } from "./microphone-input.js";
|
|
30
|
+
import { SpeechOutput } from "./speech-output.js";
|
|
30
31
|
import { MouseJoystickSource } from "./mouse-joystick-source.js";
|
|
31
32
|
import { getFilterForMode } from "./canvas.js";
|
|
32
33
|
import {
|
|
@@ -97,6 +98,7 @@ const paramTypes = {
|
|
|
97
98
|
logFdcStateChanges: ParamTypes.BOOL,
|
|
98
99
|
coProcessor: ParamTypes.BOOL,
|
|
99
100
|
mouseJoystickEnabled: ParamTypes.BOOL,
|
|
101
|
+
speechOutput: ParamTypes.BOOL,
|
|
100
102
|
|
|
101
103
|
// Numeric parameters
|
|
102
104
|
speed: ParamTypes.INT,
|
|
@@ -105,6 +107,7 @@ const paramTypes = {
|
|
|
105
107
|
audiofilterfreq: ParamTypes.FLOAT,
|
|
106
108
|
audiofilterq: ParamTypes.FLOAT,
|
|
107
109
|
cpuMultiplier: ParamTypes.FLOAT,
|
|
110
|
+
tubeCpuMultiplier: ParamTypes.INT,
|
|
108
111
|
microphoneChannel: ParamTypes.INT,
|
|
109
112
|
|
|
110
113
|
// String parameters (these are the default but listed for clarity)
|
|
@@ -125,7 +128,6 @@ let keyLayout = window.localStorage.keyLayout || "physical";
|
|
|
125
128
|
|
|
126
129
|
const BBC = utils.BBC;
|
|
127
130
|
const keyCodes = utils.keyCodes;
|
|
128
|
-
const emuKeyHandlers = {};
|
|
129
131
|
let cpuMultiplier = 1;
|
|
130
132
|
let fastAsPossible = false;
|
|
131
133
|
let fastTape = false;
|
|
@@ -181,33 +183,22 @@ const printerPort = {
|
|
|
181
183
|
},
|
|
182
184
|
};
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
// Accessibility switch state — bits 0-7 correspond to switches 1-8.
|
|
187
|
+
// Active low: 0xff = no switches pressed; clearing a bit = that switch is pressed.
|
|
188
|
+
let switchState = 0xff;
|
|
185
189
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (down) switchState &= 0xff ^ bit;
|
|
193
|
-
else switchState |= bit;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
for (let idx = utils.keyCodes.K1; idx <= utils.keyCodes.K8; ++idx) {
|
|
197
|
-
emuKeyHandlers[idx] = switchKey;
|
|
198
|
-
}
|
|
199
|
-
userPort = {
|
|
200
|
-
write: function () {},
|
|
201
|
-
read: function () {
|
|
202
|
-
return switchState;
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
}
|
|
190
|
+
const userPort = {
|
|
191
|
+
write() {},
|
|
192
|
+
read() {
|
|
193
|
+
return switchState;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
206
196
|
|
|
207
197
|
const emulationConfig = {
|
|
208
198
|
keyLayout: keyLayout,
|
|
209
199
|
coProcessor: parsedQuery.coProcessor,
|
|
210
200
|
cpuMultiplier: cpuMultiplier,
|
|
201
|
+
tubeCpuMultiplier: parsedQuery.tubeCpuMultiplier || 2,
|
|
211
202
|
videoCyclesBatch: parsedQuery.videoCyclesBatch,
|
|
212
203
|
extraRoms: extraRoms,
|
|
213
204
|
userPort: userPort,
|
|
@@ -222,6 +213,11 @@ const emulationConfig = {
|
|
|
222
213
|
},
|
|
223
214
|
};
|
|
224
215
|
|
|
216
|
+
// Speech output: initialised from URL param; can be toggled at runtime via the Settings panel.
|
|
217
|
+
// Must be created before Config so the onClose callback and setSpeechOutput() call can reference it.
|
|
218
|
+
const speechOutput = new SpeechOutput();
|
|
219
|
+
speechOutput.enabled = !!parsedQuery.speechOutput;
|
|
220
|
+
|
|
225
221
|
const config = new Config(
|
|
226
222
|
function onChange(changed) {
|
|
227
223
|
if (changed.displayMode) {
|
|
@@ -263,6 +259,16 @@ const config = new Config(
|
|
|
263
259
|
setupMicrophone().then(() => {});
|
|
264
260
|
}
|
|
265
261
|
}
|
|
262
|
+
if (changed.speechOutput !== undefined) {
|
|
263
|
+
speechOutput.enabled = !!changed.speechOutput;
|
|
264
|
+
}
|
|
265
|
+
if (changed.tubeCpuMultiplier !== undefined) {
|
|
266
|
+
emulationConfig.tubeCpuMultiplier = changed.tubeCpuMultiplier;
|
|
267
|
+
config.setTubeCpuMultiplier(changed.tubeCpuMultiplier);
|
|
268
|
+
if (processor.tube && processor.tube.cpuMultiplier !== undefined) {
|
|
269
|
+
processor.tube.cpuMultiplier = changed.tubeCpuMultiplier;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
266
272
|
updateUrl();
|
|
267
273
|
},
|
|
268
274
|
);
|
|
@@ -273,11 +279,13 @@ config.mapLegacyModels(parsedQuery);
|
|
|
273
279
|
config.setModel(parsedQuery.model || guessModelFromHostname(window.location.hostname));
|
|
274
280
|
config.setKeyLayout(keyLayout);
|
|
275
281
|
config.set65c02(parsedQuery.coProcessor);
|
|
282
|
+
config.setTubeCpuMultiplier(parsedQuery.tubeCpuMultiplier || 2);
|
|
276
283
|
config.setEconet(parsedQuery.hasEconet);
|
|
277
284
|
config.setMusic5000(parsedQuery.hasMusic5000);
|
|
278
285
|
config.setTeletext(parsedQuery.hasTeletextAdaptor);
|
|
279
286
|
config.setMicrophoneChannel(parsedQuery.microphoneChannel);
|
|
280
287
|
config.setMouseJoystickEnabled(parsedQuery.mouseJoystickEnabled);
|
|
288
|
+
config.setSpeechOutput(speechOutput.enabled);
|
|
281
289
|
let displayMode = parsedQuery.displayMode || "rgb";
|
|
282
290
|
config.setDisplayMode(displayMode);
|
|
283
291
|
|
|
@@ -362,7 +370,13 @@ video = new Video(model.isMaster, canvas.fb32, function paint(minx, miny, maxx,
|
|
|
362
370
|
if (parsedQuery.fakeVideo !== undefined) video = new FakeVideo();
|
|
363
371
|
|
|
364
372
|
const audioStatsNode = document.getElementById("audio-stats");
|
|
365
|
-
const audioHandler = new AudioHandler(
|
|
373
|
+
const audioHandler = new AudioHandler({
|
|
374
|
+
warningNode: $("#audio-warning"),
|
|
375
|
+
statsNode: audioStatsNode,
|
|
376
|
+
audioFilterFreq,
|
|
377
|
+
audioFilterQ,
|
|
378
|
+
noSeek,
|
|
379
|
+
});
|
|
366
380
|
if (!parsedQuery.audioDebug) audioStatsNode.style.display = "none";
|
|
367
381
|
// Firefox will report that audio is suspended even when it will
|
|
368
382
|
// start playing without user interaction, so we need to delay a
|
|
@@ -578,21 +592,19 @@ function checkPrinterWindow() {
|
|
|
578
592
|
processor.uservia.setca1(true);
|
|
579
593
|
}
|
|
580
594
|
|
|
581
|
-
processor = new Cpu6502(
|
|
582
|
-
model,
|
|
595
|
+
processor = new Cpu6502(model, {
|
|
583
596
|
dbgr,
|
|
584
597
|
video,
|
|
585
|
-
audioHandler.soundChip,
|
|
586
|
-
audioHandler.ddNoise,
|
|
587
|
-
model.hasMusic5000 ? audioHandler.music5000 : null,
|
|
598
|
+
soundChip: audioHandler.soundChip,
|
|
599
|
+
ddNoise: audioHandler.ddNoise,
|
|
600
|
+
music5000: model.hasMusic5000 ? audioHandler.music5000 : null,
|
|
588
601
|
cmos,
|
|
589
|
-
emulationConfig,
|
|
602
|
+
config: emulationConfig,
|
|
590
603
|
econet,
|
|
591
|
-
);
|
|
604
|
+
});
|
|
592
605
|
|
|
593
606
|
// Create input sources
|
|
594
607
|
const gamepadSource = new GamepadSource(emulationConfig.getGamepads);
|
|
595
|
-
|
|
596
608
|
// Create MicrophoneInput but don't enable by default
|
|
597
609
|
const microphoneInput = new MicrophoneInput();
|
|
598
610
|
microphoneInput.setErrorCallback((message) => {
|
|
@@ -603,9 +615,28 @@ microphoneInput.setErrorCallback((message) => {
|
|
|
603
615
|
const screenCanvas = document.getElementById("screen");
|
|
604
616
|
const mouseJoystickSource = new MouseJoystickSource(screenCanvas);
|
|
605
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Attach an RS-423 composite handler to the ACIA that combines the touchscreen
|
|
620
|
+
* (which sends position data to the BBC) with the speech output (which speaks
|
|
621
|
+
* text the BBC sends out). Call this once after processor.initialise() and
|
|
622
|
+
* again whenever speechOutput.enabled changes.
|
|
623
|
+
*/
|
|
624
|
+
function setupRs423Handler() {
|
|
625
|
+
const touchScreen = processor.touchScreen;
|
|
626
|
+
processor.acia.setRs423Handler({
|
|
627
|
+
onTransmit(val) {
|
|
628
|
+
touchScreen.onTransmit(val);
|
|
629
|
+
speechOutput.onTransmit(val);
|
|
630
|
+
},
|
|
631
|
+
tryReceive(rts) {
|
|
632
|
+
return touchScreen.tryReceive(rts);
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
606
637
|
// Helper to manage ADC source configuration
|
|
607
638
|
function updateAdcSources(mouseJoystickEnabled, microphoneChannel) {
|
|
608
|
-
// Default all channels to gamepad
|
|
639
|
+
// Default all channels to the gamepad source.
|
|
609
640
|
for (let ch = 0; ch < 4; ch++) {
|
|
610
641
|
processor.adconverter.setChannelSource(ch, gamepadSource);
|
|
611
642
|
}
|
|
@@ -753,6 +784,30 @@ keyboard.registerKeyHandler(
|
|
|
753
784
|
{ alt: false, ctrl: true },
|
|
754
785
|
);
|
|
755
786
|
|
|
787
|
+
// Register accessibility switch key handlers.
|
|
788
|
+
// Keys 1–8 (K1–K8) and function keys F1–F8 both map to user port bits 0–7
|
|
789
|
+
// (active low: pressing the key clears the corresponding bit in &FE60).
|
|
790
|
+
//
|
|
791
|
+
// On real hardware, the Brilliant Computing switch interface box and special-ed
|
|
792
|
+
// joystick connect to the User Port only — they do not touch the analogue port
|
|
793
|
+
// or the System VIA fire buttons (PB4/PB5), which belong to the standard
|
|
794
|
+
// analogue joystick connector. So we only update switchState here.
|
|
795
|
+
{
|
|
796
|
+
const handleSwitch = (bit) => (down) => {
|
|
797
|
+
if (down) switchState &= ~(1 << bit);
|
|
798
|
+
else switchState |= 1 << bit;
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Alt+1–8 and Alt+F1–F8 trigger the switches. Using Alt means the underlying
|
|
802
|
+
// key is never forwarded to the BBC Micro (keyboard.js bails out early when a
|
|
803
|
+
// handler fires), so typing numbers or using function keys works normally.
|
|
804
|
+
const altMod = { alt: true, ctrl: false };
|
|
805
|
+
for (let i = 0; i < 8; i++) {
|
|
806
|
+
keyboard.registerKeyHandler(utils.keyCodes.K1 + i, handleSwitch(i), altMod);
|
|
807
|
+
keyboard.registerKeyHandler(utils.keyCodes.F1 + i, handleSwitch(i), altMod);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
756
811
|
// Setup key handlers
|
|
757
812
|
document.addEventListener("keydown", (evt) => {
|
|
758
813
|
audioHandler.tryResume().then(() => {});
|
|
@@ -825,6 +880,9 @@ async function tapeSthClick(item) {
|
|
|
825
880
|
}
|
|
826
881
|
|
|
827
882
|
const $sthModal = new bootstrap.Modal(document.getElementById("sth"));
|
|
883
|
+
document.getElementById("sth").addEventListener("shown.bs.modal", () => {
|
|
884
|
+
document.getElementById("sth-filter").focus();
|
|
885
|
+
});
|
|
828
886
|
|
|
829
887
|
function makeOnCat(onClick) {
|
|
830
888
|
return function (cat) {
|
|
@@ -1340,6 +1398,9 @@ syncLights = function () {
|
|
|
1340
1398
|
const startPromise = (async () => {
|
|
1341
1399
|
await Promise.all([audioHandler.initialise(), processor.initialise()]);
|
|
1342
1400
|
|
|
1401
|
+
// Wire up the composite RS-423 handler now that the touchscreen exists.
|
|
1402
|
+
setupRs423Handler();
|
|
1403
|
+
|
|
1343
1404
|
// Ideally would start the loads first. But their completion needs the FDC from the processor
|
|
1344
1405
|
const imageLoads = [];
|
|
1345
1406
|
|
package/src/models.js
CHANGED
|
@@ -11,7 +11,7 @@ const CpuModel = Object.freeze({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
class Model {
|
|
14
|
-
constructor(name, synonyms, os, cpuModel, isMaster, swram, fdc, tube, cmosOverride) {
|
|
14
|
+
constructor({ name, synonyms, os, cpuModel, isMaster, swram, fdc, tube, cmosOverride } = {}) {
|
|
15
15
|
this.name = name;
|
|
16
16
|
this.synonyms = synonyms;
|
|
17
17
|
this.os = os;
|
|
@@ -97,77 +97,81 @@ const masterSwram = [
|
|
|
97
97
|
];
|
|
98
98
|
|
|
99
99
|
export const allModels = [
|
|
100
|
-
new Model(
|
|
101
|
-
"BBC B with DFS 1.2",
|
|
102
|
-
["B-DFS1.2"],
|
|
103
|
-
["os.rom", "BASIC.ROM", "b/DFS-1.2.rom"],
|
|
104
|
-
CpuModel.MOS6502,
|
|
105
|
-
false,
|
|
106
|
-
beebSwram,
|
|
107
|
-
NoiseAwareIntelFdc,
|
|
108
|
-
),
|
|
109
|
-
new Model(
|
|
110
|
-
"BBC B with DFS 0.9",
|
|
111
|
-
["B-DFS0.9", "B"],
|
|
112
|
-
["os.rom", "BASIC.ROM", "b/DFS-0.9.rom"],
|
|
113
|
-
CpuModel.MOS6502,
|
|
114
|
-
false,
|
|
115
|
-
beebSwram,
|
|
116
|
-
NoiseAwareIntelFdc,
|
|
117
|
-
),
|
|
118
|
-
new Model(
|
|
119
|
-
"BBC B with 1770 (DFS)",
|
|
120
|
-
["B1770"],
|
|
121
|
-
["os.rom", "BASIC.ROM", "b1770/dfs1770.rom", "b1770/zADFS.ROM"],
|
|
122
|
-
CpuModel.MOS6502,
|
|
123
|
-
false,
|
|
124
|
-
beebSwram,
|
|
125
|
-
NoiseAwareWdFdc,
|
|
126
|
-
),
|
|
100
|
+
new Model({
|
|
101
|
+
name: "BBC B with DFS 1.2",
|
|
102
|
+
synonyms: ["B-DFS1.2"],
|
|
103
|
+
os: ["os.rom", "BASIC.ROM", "b/DFS-1.2.rom"],
|
|
104
|
+
cpuModel: CpuModel.MOS6502,
|
|
105
|
+
isMaster: false,
|
|
106
|
+
swram: beebSwram,
|
|
107
|
+
fdc: NoiseAwareIntelFdc,
|
|
108
|
+
}),
|
|
109
|
+
new Model({
|
|
110
|
+
name: "BBC B with DFS 0.9",
|
|
111
|
+
synonyms: ["B-DFS0.9", "B"],
|
|
112
|
+
os: ["os.rom", "BASIC.ROM", "b/DFS-0.9.rom"],
|
|
113
|
+
cpuModel: CpuModel.MOS6502,
|
|
114
|
+
isMaster: false,
|
|
115
|
+
swram: beebSwram,
|
|
116
|
+
fdc: NoiseAwareIntelFdc,
|
|
117
|
+
}),
|
|
118
|
+
new Model({
|
|
119
|
+
name: "BBC B with 1770 (DFS)",
|
|
120
|
+
synonyms: ["B1770"],
|
|
121
|
+
os: ["os.rom", "BASIC.ROM", "b1770/dfs1770.rom", "b1770/zADFS.ROM"],
|
|
122
|
+
cpuModel: CpuModel.MOS6502,
|
|
123
|
+
isMaster: false,
|
|
124
|
+
swram: beebSwram,
|
|
125
|
+
fdc: NoiseAwareWdFdc,
|
|
126
|
+
}),
|
|
127
127
|
// putting ADFS in a higher ROM slot gives it priority
|
|
128
|
-
new Model(
|
|
129
|
-
"BBC B with 1770 (ADFS)",
|
|
130
|
-
["B1770A"],
|
|
131
|
-
["os.rom", "BASIC.ROM", "b1770/zADFS.ROM", "b1770/dfs1770.rom"],
|
|
132
|
-
CpuModel.MOS6502,
|
|
133
|
-
false,
|
|
134
|
-
beebSwram,
|
|
135
|
-
NoiseAwareWdFdc,
|
|
136
|
-
),
|
|
137
|
-
new Model(
|
|
138
|
-
"BBC Master 128 (DFS)",
|
|
139
|
-
["Master"],
|
|
140
|
-
["master/mos3.20"],
|
|
141
|
-
CpuModel.CMOS65C12,
|
|
142
|
-
true,
|
|
143
|
-
masterSwram,
|
|
144
|
-
NoiseAwareWdFdc,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
["
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
128
|
+
new Model({
|
|
129
|
+
name: "BBC B with 1770 (ADFS)",
|
|
130
|
+
synonyms: ["B1770A"],
|
|
131
|
+
os: ["os.rom", "BASIC.ROM", "b1770/zADFS.ROM", "b1770/dfs1770.rom"],
|
|
132
|
+
cpuModel: CpuModel.MOS6502,
|
|
133
|
+
isMaster: false,
|
|
134
|
+
swram: beebSwram,
|
|
135
|
+
fdc: NoiseAwareWdFdc,
|
|
136
|
+
}),
|
|
137
|
+
new Model({
|
|
138
|
+
name: "BBC Master 128 (DFS)",
|
|
139
|
+
synonyms: ["Master"],
|
|
140
|
+
os: ["master/mos3.20"],
|
|
141
|
+
cpuModel: CpuModel.CMOS65C12,
|
|
142
|
+
isMaster: true,
|
|
143
|
+
swram: masterSwram,
|
|
144
|
+
fdc: NoiseAwareWdFdc,
|
|
145
|
+
cmosOverride: pickDfs,
|
|
146
|
+
}),
|
|
147
|
+
new Model({
|
|
148
|
+
name: "BBC Master 128 (ADFS)",
|
|
149
|
+
synonyms: ["MasterADFS"],
|
|
150
|
+
os: ["master/mos3.20"],
|
|
151
|
+
cpuModel: CpuModel.CMOS65C12,
|
|
152
|
+
isMaster: true,
|
|
153
|
+
swram: masterSwram,
|
|
154
|
+
fdc: NoiseAwareWdFdc,
|
|
155
|
+
cmosOverride: pickAdfs,
|
|
156
|
+
}),
|
|
157
|
+
new Model({
|
|
158
|
+
name: "BBC Master 128 (ANFS)",
|
|
159
|
+
synonyms: ["MasterANFS"],
|
|
160
|
+
os: ["master/mos3.20"],
|
|
161
|
+
cpuModel: CpuModel.CMOS65C12,
|
|
162
|
+
isMaster: true,
|
|
163
|
+
swram: masterSwram,
|
|
164
|
+
fdc: NoiseAwareWdFdc,
|
|
165
|
+
cmosOverride: pickAnfs,
|
|
166
|
+
}),
|
|
167
|
+
// Although this can not be explicitly selected as a model, it is required by the configuration builder later
|
|
168
|
+
new Model({
|
|
169
|
+
name: "Tube65C02",
|
|
170
|
+
synonyms: [],
|
|
171
|
+
os: ["tube/6502Tube.rom"],
|
|
172
|
+
cpuModel: CpuModel.CMOS65C02,
|
|
173
|
+
isMaster: false,
|
|
174
|
+
}),
|
|
171
175
|
];
|
|
172
176
|
|
|
173
177
|
export function findModel(name) {
|
|
@@ -182,19 +186,43 @@ export function findModel(name) {
|
|
|
182
186
|
return null;
|
|
183
187
|
}
|
|
184
188
|
|
|
185
|
-
export const TEST_6502 = new Model(
|
|
189
|
+
export const TEST_6502 = new Model({
|
|
190
|
+
name: "TEST",
|
|
191
|
+
synonyms: ["TEST"],
|
|
192
|
+
os: [],
|
|
193
|
+
cpuModel: CpuModel.MOS6502,
|
|
194
|
+
isMaster: false,
|
|
195
|
+
swram: beebSwram,
|
|
196
|
+
fdc: NoiseAwareIntelFdc,
|
|
197
|
+
});
|
|
186
198
|
TEST_6502.isTest = true;
|
|
187
|
-
export const TEST_65C02 = new Model(
|
|
199
|
+
export const TEST_65C02 = new Model({
|
|
200
|
+
name: "TEST",
|
|
201
|
+
synonyms: ["TEST"],
|
|
202
|
+
os: [],
|
|
203
|
+
cpuModel: CpuModel.CMOS65C02,
|
|
204
|
+
isMaster: false,
|
|
205
|
+
swram: masterSwram,
|
|
206
|
+
fdc: NoiseAwareIntelFdc,
|
|
207
|
+
});
|
|
188
208
|
TEST_65C02.isTest = true;
|
|
189
|
-
export const TEST_65C12 = new Model(
|
|
209
|
+
export const TEST_65C12 = new Model({
|
|
210
|
+
name: "TEST",
|
|
211
|
+
synonyms: ["TEST"],
|
|
212
|
+
os: [],
|
|
213
|
+
cpuModel: CpuModel.CMOS65C12,
|
|
214
|
+
isMaster: false,
|
|
215
|
+
swram: masterSwram,
|
|
216
|
+
fdc: NoiseAwareIntelFdc,
|
|
217
|
+
});
|
|
190
218
|
TEST_65C12.isTest = true;
|
|
191
219
|
|
|
192
|
-
export const basicOnly = new Model(
|
|
193
|
-
"Basic only",
|
|
194
|
-
["Basic only"],
|
|
195
|
-
["master/mos3.20"],
|
|
196
|
-
CpuModel.CMOS65C12,
|
|
197
|
-
true,
|
|
198
|
-
masterSwram,
|
|
199
|
-
NoiseAwareWdFdc,
|
|
200
|
-
);
|
|
220
|
+
export const basicOnly = new Model({
|
|
221
|
+
name: "Basic only",
|
|
222
|
+
synonyms: ["Basic only"],
|
|
223
|
+
os: ["master/mos3.20"],
|
|
224
|
+
cpuModel: CpuModel.CMOS65C12,
|
|
225
|
+
isMaster: true,
|
|
226
|
+
swram: masterSwram,
|
|
227
|
+
fdc: NoiseAwareWdFdc,
|
|
228
|
+
});
|
package/src/soundchip.js
CHANGED
|
@@ -288,6 +288,84 @@ export class SoundChip {
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/**
|
|
292
|
+
* InstrumentedSoundChip - wraps a real SoundChip and captures all writes
|
|
293
|
+
* for debugging purposes. Provides the same interface as SoundChip.
|
|
294
|
+
*/
|
|
295
|
+
export class InstrumentedSoundChip extends SoundChip {
|
|
296
|
+
constructor() {
|
|
297
|
+
super(() => {}); // no audio output callback needed in headless mode
|
|
298
|
+
this._capturedWrites = [];
|
|
299
|
+
this._capturing = false;
|
|
300
|
+
this._totalCycles = 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
poke(value) {
|
|
304
|
+
if (this._capturing) {
|
|
305
|
+
this._capturedWrites.push({ cycle: this._totalCycles, value });
|
|
306
|
+
}
|
|
307
|
+
super.poke(value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Start capturing SN76489 writes. Clears any previous capture. */
|
|
311
|
+
startCapture() {
|
|
312
|
+
this._capturedWrites = [];
|
|
313
|
+
this._capturing = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Stop capturing and return the captured writes. */
|
|
317
|
+
stopCapture() {
|
|
318
|
+
this._capturing = false;
|
|
319
|
+
return this._capturedWrites;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Get the captured writes without stopping. */
|
|
323
|
+
getCapturedWrites() {
|
|
324
|
+
return this._capturedWrites;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Clear captured writes. */
|
|
328
|
+
clearCapture() {
|
|
329
|
+
this._capturedWrites = [];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Track total cycles for timestamps. Called by the scheduler. */
|
|
333
|
+
advance(cycles) {
|
|
334
|
+
this._totalCycles += cycles;
|
|
335
|
+
super.advance(cycles);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Read current SN76489 register state in a friendly format. */
|
|
339
|
+
getState() {
|
|
340
|
+
return {
|
|
341
|
+
tone: [
|
|
342
|
+
this.registers[0],
|
|
343
|
+
this.registers[1],
|
|
344
|
+
this.registers[2],
|
|
345
|
+
],
|
|
346
|
+
noise: this.registers[3],
|
|
347
|
+
volume: [
|
|
348
|
+
this._attenuationFromVolume(0),
|
|
349
|
+
this._attenuationFromVolume(1),
|
|
350
|
+
this._attenuationFromVolume(2),
|
|
351
|
+
this._attenuationFromVolume(3),
|
|
352
|
+
],
|
|
353
|
+
lfsr: this.lfsr,
|
|
354
|
+
latchedRegister: this.latchedRegister,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Convert internal float volume back to 0-15 attenuation. */
|
|
359
|
+
_attenuationFromVolume(channel) {
|
|
360
|
+
const v = this.volume[channel];
|
|
361
|
+
// volumeTable[15] = 0 (silent), volumeTable[0] = loudest
|
|
362
|
+
for (let i = 0; i < 16; i++) {
|
|
363
|
+
if (Math.abs(volumeTable[i] - v) < 0.001) return i;
|
|
364
|
+
}
|
|
365
|
+
return 15; // silent
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
291
369
|
export class FakeSoundChip {
|
|
292
370
|
reset() {}
|
|
293
371
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RS-423 handler that routes transmitted bytes to the Web Speech API.
|
|
5
|
+
*
|
|
6
|
+
* BBC programs use *FX3,1 (or *FX3,3) to send OSWRCH output to the RS-423
|
|
7
|
+
* serial port, which on real hardware fed a Votrax Type 'N Talk synthesiser.
|
|
8
|
+
* We intercept at the ACIA hardware boundary and route to speechSynthesis.
|
|
9
|
+
*
|
|
10
|
+
* Byte handling is based on the Votrax Type 'N Talk Operator's Manual (1981):
|
|
11
|
+
* - Printable ASCII 0x20–0x7E: accumulated into the text buffer.
|
|
12
|
+
* - CR (0x0D): "TALK-CLR" — speaks the buffer and clears it. Multiple CR-
|
|
13
|
+
* terminated lines queue naturally via speechSynthesis.speak(). A future
|
|
14
|
+
* improvement could heuristically combine lines that arrive within a single
|
|
15
|
+
* frame (~20 ms) into one utterance, which would give modern TTS engines a
|
|
16
|
+
* better sentence to work with — but the simple per-line queue works well
|
|
17
|
+
* enough and has no surprising pauses or dropped output.
|
|
18
|
+
* - LF (0x0A): explicitly listed as null data in the manual; ignored.
|
|
19
|
+
* - ESC (0x1B): unit-select prefix for daisy-chained TNT units. ESC plus
|
|
20
|
+
* the following byte are consumed silently (not passed to speechSynthesis).
|
|
21
|
+
* - All other bytes: null data per the manual; ignored.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// From the TNT Operator's Manual: "The input buffer can hold more than 750
|
|
25
|
+
// characters".
|
|
26
|
+
export const MAX_BUFFER = 750;
|
|
27
|
+
|
|
28
|
+
export class SpeechOutput {
|
|
29
|
+
constructor() {
|
|
30
|
+
this._buffer = "";
|
|
31
|
+
this._escapeNext = false;
|
|
32
|
+
this._enabled = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get enabled() {
|
|
36
|
+
return this._enabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set enabled(value) {
|
|
40
|
+
this._enabled = !!value;
|
|
41
|
+
if (!this._enabled) {
|
|
42
|
+
this._buffer = "";
|
|
43
|
+
if (typeof speechSynthesis !== "undefined") speechSynthesis.cancel();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** RS-423 handler interface: called for each byte the BBC transmits. */
|
|
48
|
+
onTransmit(byte) {
|
|
49
|
+
if (!this._enabled) return;
|
|
50
|
+
|
|
51
|
+
if (this._escapeNext) {
|
|
52
|
+
this._escapeNext = false;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
switch (byte) {
|
|
57
|
+
case 0x1b: // ESC — next byte is a unit-select code, not text.
|
|
58
|
+
this._escapeNext = true;
|
|
59
|
+
return;
|
|
60
|
+
|
|
61
|
+
case 0x0d: // CR — TALK-CLR: speak current buffer and clear it.
|
|
62
|
+
this._flush();
|
|
63
|
+
return;
|
|
64
|
+
|
|
65
|
+
default:
|
|
66
|
+
if (byte >= 0x20 && byte <= 0x7e) {
|
|
67
|
+
this._buffer += String.fromCharCode(byte);
|
|
68
|
+
if (this._buffer.length >= MAX_BUFFER) this._flush();
|
|
69
|
+
}
|
|
70
|
+
// Everything else is null data — silently ignored.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** RS-423 handler interface: nothing to send back to the BBC. */
|
|
75
|
+
tryReceive() {
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_flush() {
|
|
80
|
+
const text = this._buffer.trim();
|
|
81
|
+
this._buffer = "";
|
|
82
|
+
if (!text || typeof speechSynthesis === "undefined") return;
|
|
83
|
+
speechSynthesis.speak(new SpeechSynthesisUtterance(text));
|
|
84
|
+
}
|
|
85
|
+
}
|