Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/audio/AudioBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,10 @@ export class AudioBus {
const outputNode = audioContext.createGain();

// Internal chain: input → [effects...] → pan → output
outputNode.gain.setTargetAtTime(this._muted ? 0 : this._volume, audioContext.currentTime, 0.01);
// Set, not ramped: the node is new and nothing flows through it yet. A
// ramp from the GainNode default of unity would play the first sound
// after the unlock gesture at full level until it settled.
outputNode.gain.setValueAtTime(this._muted ? 0 : this._volume, audioContext.currentTime);
panNode.pan.setTargetAtTime(this._pan, audioContext.currentTime, 0.01);

this._setup = { audioContext, inputNode, outputNode, panNode };
Expand Down
6 changes: 5 additions & 1 deletion src/audio/BaseVoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ export abstract class BaseVoice implements Voice {
this._volume = clamp(init.volume, 0, 1);
this._spatialConfig = { ...defaultSpatialConfig };

this._output.gain.setTargetAtTime(this._volume, this._audioContext.currentTime, 0.01);
// Set, not ramped: a fresh GainNode starts at unity, and the source has not
// started yet, so there is nothing to click. A ramp from unity would let a
// one-shot's first milliseconds - usually its loudest transient - through
// at full level before the requested volume took hold.
this._output.gain.setValueAtTime(this._volume, this._audioContext.currentTime);

if (init.autoConnect !== false) {
this._connectOutput();
Expand Down
29 changes: 26 additions & 3 deletions test/audio/audio-bus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,29 @@ describe('AudioBus', () => {
expect(bus.pan).toBe(0.5);
});

test('a new bus starts at its volume instead of ramping down from unity', () => {
const spy = spyOnBusCreation();

const bus = new AudioBus('initial-volume-test', { volume: 0.4 });

expect(spy.outputNode.gain.setValueAtTime).toHaveBeenCalledWith(0.4, expect.any(Number));
expect(spy.outputNode.gain.setTargetAtTime).not.toHaveBeenCalled();

spy.restore();
bus.destroy();
});

test('a bus created muted starts silent', () => {
const spy = spyOnBusCreation();

const bus = new AudioBus('initial-muted-test', { volume: 0.4, muted: true });

expect(spy.outputNode.gain.setValueAtTime).toHaveBeenCalledWith(0, expect.any(Number));

spy.restore();
bus.destroy();
});

// 5. Muted zeroes the gain via setTargetAtTime
test('setting muted=true calls setTargetAtTime(0) on outputNode', () => {
const spy = spyOnBusCreation();
Expand Down Expand Up @@ -709,7 +732,7 @@ describe('AudioBus', () => {
vi.resetModules();
const fakeSignal = new Signal<[AudioContext]>();

const makeFakeGain = () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: { setTargetAtTime: vi.fn() } });
const makeFakeGain = () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: { setTargetAtTime: vi.fn(), setValueAtTime: vi.fn() } });
const fakeCtx = {
currentTime: 0,
destination: {},
Expand Down Expand Up @@ -746,7 +769,7 @@ describe('AudioBus', () => {
vi.resetModules();
const fakeSignal = new Signal<[AudioContext]>();

const makeFakeGain = () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: { setTargetAtTime: vi.fn() } });
const makeFakeGain = () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: { setTargetAtTime: vi.fn(), setValueAtTime: vi.fn() } });
const fakeCtx = {
currentTime: 0,
destination: {},
Expand Down Expand Up @@ -787,7 +810,7 @@ describe('AudioBus', () => {
vi.resetModules();
const fakeSignal = new Signal<[AudioContext]>();

const makeFakeGain = () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: { setTargetAtTime: vi.fn() } });
const makeFakeGain = () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: { setTargetAtTime: vi.fn(), setValueAtTime: vi.fn() } });
const fakeCtx = {
currentTime: 0,
destination: {},
Expand Down
6 changes: 3 additions & 3 deletions test/audio/audio-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ describe('AudioGenerator', () => {

system.play(gen, { muted: true, volume: 0.8 });

expect(spy.gains[0].gain.setTargetAtTime).toHaveBeenCalledWith(0, expect.any(Number), expect.any(Number));
expect(spy.gains[0].gain.setValueAtTime).toHaveBeenCalledWith(0, expect.any(Number));

spy.restore();
gen.destroy();
Expand All @@ -498,7 +498,7 @@ describe('AudioGenerator', () => {

system.play(gen, { volume: 0.3 });

expect(spy.gains[0].gain.setTargetAtTime).toHaveBeenCalledWith(0.3, expect.any(Number), expect.any(Number));
expect(spy.gains[0].gain.setValueAtTime).toHaveBeenCalledWith(0.3, expect.any(Number));

spy.restore();
gen.destroy();
Expand All @@ -511,7 +511,7 @@ describe('AudioGenerator', () => {

system.play(gen);

expect(spy.gains[0].gain.setTargetAtTime).toHaveBeenCalledWith(0, expect.any(Number), expect.any(Number));
expect(spy.gains[0].gain.setValueAtTime).toHaveBeenCalledWith(0, expect.any(Number));

spy.restore();
gen.destroy();
Expand Down
32 changes: 32 additions & 0 deletions test/audio/base-voice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,38 @@ describe('BaseVoice — post-ended no-ops', () => {
});
});

describe('BaseVoice - initial volume', () => {
afterEach(() => vi.restoreAllMocks());

test.each([0, 0.05, 0.25, 1])('a voice played at volume %s starts there instead of ramping down from unity', volume => {
const system = new AudioSystem(); // before capture, so bus gains are not captured
const out = captureVoiceOutput();
const sound = new Sound(createAudioBufferStub());

system.play(sound, { volume });

expect(out.node!.gain.setValueAtTime).toHaveBeenCalledWith(volume, expect.any(Number));
expect(out.node!.gain.setTargetAtTime).not.toHaveBeenCalled();

out.restore();
});

test('a later volume change still ramps rather than jumping', () => {
const system = new AudioSystem();
const out = captureVoiceOutput();
const sound = new Sound(createAudioBufferStub());
const voice = system.play(sound, { volume: 0.5 });

out.node!.gain.setValueAtTime.mockClear();
voice.volume = 0.1;

expect(out.node!.gain.setTargetAtTime).toHaveBeenCalledWith(0.1, expect.any(Number), 0.01);
expect(out.node!.gain.setValueAtTime).not.toHaveBeenCalled();

out.restore();
});
});

describe('BaseVoice — fade()', () => {
afterEach(() => vi.restoreAllMocks());

Expand Down
Loading