Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/sounds/tape/motor_off.wav
Binary file not shown.
Binary file added public/sounds/tape/motor_on.wav
Binary file not shown.
5 changes: 3 additions & 2 deletions src/6502.js
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ function is1MHzAccess(addr) {
}

export class Cpu6502 extends Base6502 {
constructor(model, dbgr, video_, soundChip_, ddNoise_, music5000_, cmos, config, econet_) {
constructor(model, dbgr, video_, soundChip_, ddNoise_, relayNoise_, music5000_, cmos, config, econet_) {
super(model);
this.config = fixUpConfig(config);
this.debugFlags = this.config.debugFlags;
Expand All @@ -582,6 +582,7 @@ export class Cpu6502 extends Base6502 {
this.soundChip = soundChip_;
this.music5000 = music5000_;
this.ddNoise = ddNoise_;
this.relayNoise = relayNoise_;
this.memStatOffsetByIFetchBank = 0;
this.memStatOffset = 0;
this.memStat = new Uint8Array(512);
Expand Down Expand Up @@ -630,7 +631,7 @@ export class Cpu6502 extends Base6502 {
this.config.getGamepads,
);
this.uservia = new via.UserVia(this, this.scheduler, this.model.isMaster, this.config.userPort);
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.touchScreen);
this.acia = new Acia(this, this.soundChip.toneGenerator, this.scheduler, this.touchScreen, this.relayNoise);
this.serial = new Serial(this.acia);
this.adconverter = new Adc(this.sysvia, this.scheduler);
this.soundChip.setScheduler(this.scheduler);
Expand Down
9 changes: 8 additions & 1 deletion src/acia.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
// http://www.classiccmp.org/dunfield/r/6850.pdf

export class Acia {
constructor(cpu, toneGen, scheduler, rs423Handler) {
constructor(cpu, toneGen, scheduler, rs423Handler, relayNoise) {
this.cpu = cpu;
this.toneGen = toneGen;
this.rs423Handler = rs423Handler;
this.relayNoise = relayNoise;

this.sr = 0x00;
this.cr = 0x00;
Expand Down Expand Up @@ -58,10 +59,16 @@ export class Acia {
setMotor(on) {
if (on && !this.motorOn) {
this.runTape();
if (this.relayNoise) {
this.relayNoise.motorOn();
}
} else if (!on && this.motorOn) {
this.toneGen.mute();
this.runTapeTask.cancel();
this.setTapeCarrier(false);
if (this.relayNoise) {
this.relayNoise.motorOff();
}
}
this.motorOn = on;
}
Expand Down
56 changes: 56 additions & 0 deletions src/audio-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use strict";
import * as utils from "./utils.js";
import _ from "underscore";

export async function loadSounds(context, sounds) {
const loaded = await Promise.all(
_.map(sounds, async (sound) => {
// Safari doesn't support the Promise stuff directly, so we create
// our own Promise here.
const data = await utils.loadData(sound);
return await new Promise((resolve) => {
context.decodeAudioData(data.buffer, (decodedData) => {
resolve(decodedData);
});
});
}),
);
const keys = _.keys(sounds);
const result = {};
for (let i = 0; i < keys.length; ++i) {
result[keys[i]] = loaded[i];
}
return result;
}

export class BaseAudioNoise {
constructor(context, volume = 0.25) {
this.context = context;
this.sounds = {};
this.gain = context.createGain();
this.gain.gain.value = volume;
this.gain.connect(context.destination);
// workaround for older safaris that GC sounds when they're playing...
this.playing = [];
this.volume = volume;
}

oneShot(sound) {
const duration = sound.duration;
const context = this.context;
if (context.state !== "running") return duration;
const source = context.createBufferSource();
source.buffer = sound;
source.connect(this.gain);
source.start();
return duration;
}

mute() {
this.gain.gain.value = 0;
}

unmute() {
this.gain.gain.value = this.volume;
}
}
50 changes: 3 additions & 47 deletions src/ddnoise.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
"use strict";
import * as utils from "./utils.js";
import { loadSounds, BaseAudioNoise } from "./audio-utils.js";
import _ from "underscore";

const IDLE = 0;
const SPIN_UP = 1;
const SPINNING = 2;
const VOLUME = 0.25;

export class DdNoise {
export class DdNoise extends BaseAudioNoise {
constructor(context) {
this.context = context;
this.sounds = {};
super(context, 0.25);
this.state = IDLE;
this.motor = null;
this.gain = context.createGain();
this.gain.gain.value = VOLUME;
this.gain.connect(context.destination);
// workaround for older safaris that GC sounds when they're playing...
this.playing = [];
}
async initialise() {
const sounds = await loadSounds(this.context, {
Expand All @@ -31,16 +24,6 @@ export class DdNoise {
});
this.sounds = sounds;
}
oneShot(sound) {
const duration = sound.duration;
const context = this.context;
if (context.state !== "running") return duration;
const source = context.createBufferSource();
source.buffer = sound;
source.connect(this.gain);
source.start();
return duration;
}
play(sound, loop) {
if (this.context.state !== "running") return Promise.reject();
return new Promise((resolve) => {
Expand Down Expand Up @@ -94,33 +77,6 @@ export class DdNoise {
else if (diff <= 40) return this.oneShot(this.sounds.seek2);
else return this.oneShot(this.sounds.seek3);
}
mute() {
this.gain.gain.value = 0;
}
unmute() {
this.gain.gain.value = VOLUME;
}
}

async function loadSounds(context, sounds) {
const loaded = await Promise.all(
_.map(sounds, async (sound) => {
// Safari doesn't support the Promise stuff directly, so we create
// our own Promise here.
const data = await utils.loadData(sound);
return await new Promise((resolve) => {
context.decodeAudioData(data.buffer, (decodedData) => {
resolve(decodedData);
});
});
}),
);
const keys = _.keys(sounds);
const result = {};
for (let i = 0; i < keys.length; ++i) {
result[keys[i]] = loaded[i];
}
return result;
}

export class FakeDdNoise {
Expand Down
12 changes: 11 additions & 1 deletion src/fake6502.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FakeVideo } from "./video.js";
import { FakeSoundChip } from "./soundchip.js";
import { findModel, TEST_6502, TEST_65C02, TEST_65C12 } from "./models.js";
import { FakeDdNoise } from "./ddnoise.js";
import { FakeRelayNoise } from "./relaynoise.js";
import { Cpu6502 } from "./6502.js";
import { Cmos } from "./cmos.js";
import { FakeMusic5000 } from "./music5000.js";
Expand All @@ -20,7 +21,16 @@ export function fake6502(model, opts) {
const video = opts.video || fakeVideo;
model = model || TEST_6502;
if (opts.tube) model.tube = findModel("Tube65c02");
return new Cpu6502(model, dbgr, video, soundChip, new FakeDdNoise(), new FakeMusic5000(), new Cmos());
return new Cpu6502(
model,
dbgr,
video,
soundChip,
new FakeDdNoise(),
new FakeRelayNoise(),
new FakeMusic5000(),
new Cmos(),
);
}

export function fake65C02() {
Expand Down
1 change: 1 addition & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ processor = new Cpu6502(
video,
audioHandler.soundChip,
audioHandler.ddNoise,
audioHandler.relayNoise,
model.hasMusic5000 ? audioHandler.music5000 : null,
cmos,
emulationConfig,
Expand Down
39 changes: 39 additions & 0 deletions src/relaynoise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use strict";
import { loadSounds, BaseAudioNoise } from "./audio-utils.js";

export class RelayNoise extends BaseAudioNoise {
constructor(context) {
super(context, 0.25);
}

async initialise() {
const sounds = await loadSounds(this.context, {
motorOn: "sounds/tape/motor_on.wav",
motorOff: "sounds/tape/motor_off.wav",
});
this.sounds = sounds;
}

motorOn() {
if (this.sounds.motorOn) {
this.oneShot(this.sounds.motorOn);
}
}

motorOff() {
if (this.sounds.motorOff) {
this.oneShot(this.sounds.motorOff);
}
}
}

export class FakeRelayNoise {
constructor() {}
initialise() {
return Promise.resolve();
}
motorOn() {}
motorOff() {}
mute() {}
unmute() {}
}
6 changes: 6 additions & 0 deletions src/web/audio-handler.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SmoothieChart, TimeSeries } from "smoothie";
import { FakeSoundChip, SoundChip } from "../soundchip.js";
import { DdNoise, FakeDdNoise } from "../ddnoise.js";
import { RelayNoise, FakeRelayNoise } from "../relaynoise.js";
import { Music5000, FakeMusic5000 } from "../music5000.js";

// Using this approach means when jsbeeb is embedded in other projects, vite doesn't have a fit.
Expand Down Expand Up @@ -35,6 +36,7 @@ export class AudioHandler {
this.audioContext.onstatechange = () => this.checkStatus();
this.soundChip = new SoundChip((buffer, time) => this._onBuffer(buffer, time));
this.ddNoise = noSeek ? new FakeDdNoise() : new DdNoise(this.audioContext);
this.relayNoise = new RelayNoise(this.audioContext);
this._setup(audioFilterFreq, audioFilterQ).then();
} else {
if (this.audioContext && !this.audioContext.audioWorklet) {
Expand All @@ -52,6 +54,7 @@ export class AudioHandler {
}
this.soundChip = new FakeSoundChip();
this.ddNoise = new FakeDdNoise();
this.relayNoise = new FakeRelayNoise();
}

this.warningNode.on("mousedown", () => this.tryResume());
Expand Down Expand Up @@ -132,15 +135,18 @@ export class AudioHandler {

async initialise() {
await this.ddNoise.initialise();
await this.relayNoise.initialise();
}

mute() {
this.soundChip.mute();
this.ddNoise.mute();
this.relayNoise.mute();
}

unmute() {
this.soundChip.unmute();
this.ddNoise.unmute();
this.relayNoise.unmute();
}
}
66 changes: 66 additions & 0 deletions tests/unit/test-acia-relay-integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { Acia } from "../../src/acia.js";

describe("ACIA relay noise integration", () => {
let mockCpu, mockToneGen, mockScheduler, mockRs423Handler, mockRelayNoise;
let acia;

beforeEach(() => {
mockCpu = { interrupt: 0 };
mockToneGen = { mute: vi.fn(), tone: vi.fn() };
mockScheduler = {
newTask: vi.fn((_fn) => ({
cancel: vi.fn(),
ensureScheduled: vi.fn(),
})),
};
mockRs423Handler = {};
mockRelayNoise = {
motorOn: vi.fn(),
motorOff: vi.fn(),
};

acia = new Acia(mockCpu, mockToneGen, mockScheduler, mockRs423Handler, mockRelayNoise);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe("setMotor with relay noise", () => {
it("should call relay noise motorOn when motor turns on", () => {
acia.motorOn = false;

acia.setMotor(true);

expect(mockRelayNoise.motorOn).toHaveBeenCalledOnce();
expect(acia.motorOn).toBe(true);
});

it("should call relay noise motorOff when motor turns off", () => {
acia.motorOn = true;

acia.setMotor(false);

expect(mockRelayNoise.motorOff).toHaveBeenCalledOnce();
expect(acia.motorOn).toBe(false);
});

it("should not call relay noise methods when motor state doesn't change", () => {
acia.motorOn = true;

acia.setMotor(true);

expect(mockRelayNoise.motorOn).not.toHaveBeenCalled();
expect(mockRelayNoise.motorOff).not.toHaveBeenCalled();
});

it("should handle missing relay noise gracefully", () => {
const aciaWithoutRelayNoise = new Acia(mockCpu, mockToneGen, mockScheduler, mockRs423Handler, null);
aciaWithoutRelayNoise.motorOn = false;

expect(() => aciaWithoutRelayNoise.setMotor(true)).not.toThrow();
expect(aciaWithoutRelayNoise.motorOn).toBe(true);
});
});
});
Loading