Skip to content

Commit

Permalink
update after api change
Browse files Browse the repository at this point in the history
  • Loading branch information
albho committed Jul 25, 2024
1 parent a79a859 commit b174773
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 162 deletions.
3 changes: 0 additions & 3 deletions binding/nodejs/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import PvSpeakerStatus from "./pv_speaker_status_t";
class PvSpeakerStatusOutOfMemoryError extends Error {}
class PvSpeakerStatusInvalidArgumentError extends Error {}
class PvSpeakerStatusInvalidStateError extends Error {}
class PvSpeakerStatusOverflowBufferError extends Error {}
class PvSpeakerStatusBackendError extends Error {}
class PvSpeakerStatusDeviceAlreadyInitializedError extends Error {}
class PvSpeakerStatusDeviceNotInitializedError extends Error {}
Expand All @@ -30,8 +29,6 @@ function pvSpeakerStatusToException(status: PvSpeakerStatus, errorMessage: strin
return new PvSpeakerStatusInvalidArgumentError(errorMessage);
case PvSpeakerStatus.INVALID_STATE:
return new PvSpeakerStatusInvalidStateError(errorMessage);
case PvSpeakerStatus.OVERFLOW_BUFFER:
return new PvSpeakerStatusOverflowBufferError(errorMessage);
case PvSpeakerStatus.BACKEND_ERROR:
return new PvSpeakerStatusBackendError(errorMessage);
case PvSpeakerStatus.DEVICE_ALREADY_INITIALIZED:
Expand Down
76 changes: 38 additions & 38 deletions binding/nodejs/src/pv_speaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,33 @@ class PvSpeaker {
private readonly _handle: number;
private readonly _sampleRate: number;
private readonly _bitsPerSample: number;
private readonly _frameLength: number;
private readonly _bufferSizeSecs: number;
private readonly _version: string;

/**
* PvSpeaker constructor.
*
* @param sampleRate The sample rate of the audio to be played.
* @param bitsPerSample The number of bits per sample.
* @param deviceIndex The audio device index to use to play audio. A value of (-1) will use machine's default audio device.
* @param frameLength Length of the audio frames to send per write call.
* @param bufferedFramesCount The number of audio frames buffered internally for writing - i.e. internal circular buffer
* will be of size `frameLength` * `bufferedFramesCount`. If this value is too low, buffer overflows could occur
* and audio frames could be dropped. A higher value will increase memory usage.
* @param options Optional configuration arguments.
* @param options.bufferSizeSecs The size in seconds of the internal buffer used to buffer PCM data
* - i.e. internal circular buffer will be of size `sampleRate` * `bufferSizeSecs`.
* @param options.deviceIndex The index of the audio device to use. A value of (-1) will resort to default device.
*/
constructor(
sampleRate: number,
bitsPerSample: number,
deviceIndex: number = -1,
frameLength: number = 512,
bufferedFramesCount = 50,
options: {
bufferSizeSecs?: number;
deviceIndex?: number;
} = {}
) {
let pvSpeakerHandleAndStatus;
const { bufferSizeSecs = 20, deviceIndex = -1 } = options;
try {
pvSpeakerHandleAndStatus = PvSpeaker._pvSpeaker.init(
sampleRate, bitsPerSample, deviceIndex, frameLength, bufferedFramesCount);
pvSpeakerHandleAndStatus = PvSpeaker._pvSpeaker.init(sampleRate, bitsPerSample, bufferSizeSecs, deviceIndex);
} catch (err: any) {
pvSpeakerStatusToException(err.code, err);
throw pvSpeakerStatusToException(err.code, err);
}
const status = pvSpeakerHandleAndStatus.status;
if (status !== PvSpeakerStatus.SUCCESS) {
Expand All @@ -62,7 +62,7 @@ class PvSpeaker {
this._handle = pvSpeakerHandleAndStatus.handle;
this._sampleRate = sampleRate;
this._bitsPerSample = bitsPerSample;
this._frameLength = frameLength;
this._bufferSizeSecs = bufferSizeSecs;
this._version = PvSpeaker._pvSpeaker.version();
}

Expand All @@ -81,10 +81,10 @@ class PvSpeaker {
}

/**
* @returns Length of the audio frames to send per write call.
* @returns The size in seconds of the internal buffer used to buffer PCM data.
*/
get frameLength(): number {
return this._frameLength;
get bufferSizeSecs(): number {
return this._bufferSizeSecs;
}

/**
Expand All @@ -95,14 +95,14 @@ class PvSpeaker {
}

/**
* @returns Whether PvSpeaker has started and is available to receive pcm frames or not.
* @returns Whether PvSpeaker has started and is available to receive PCM frames or not.
*/
get isStarted(): boolean {
return PvSpeaker._pvSpeaker.get_is_started(this._handle);
}

/**
* Starts the audio output device. After starting, pcm frames can be sent to the audio output device via `write`.
* Starts the audio output device. After starting, PCM frames can be sent to the audio output device via `write`.
*/
public start(): void {
const status = PvSpeaker._pvSpeaker.start(this._handle);
Expand All @@ -122,34 +122,34 @@ class PvSpeaker {
}

/**
* Synchronous call to write a frame of audio data.
* Synchronous call to write PCM data to the internal circular buffer for audio playback.
* Only writes as much PCM data as the internal circular buffer can currently fit, and
* returns the length of the PCM data that was successfully written.
*
* @returns {Boolean}
* @returns {number}
*/
public write(pcm: ArrayBuffer): void {
let i = 0;
const frameLength = this._frameLength * this._bitsPerSample / 8;
while (i < pcm.byteLength) {
const isLastFrame = i + frameLength >= pcm.byteLength;
const writeFrameLength = isLastFrame ? pcm.byteLength - i : frameLength;
const frame = pcm.slice(i, i + writeFrameLength);
const status = PvSpeaker._pvSpeaker.write(this._handle, this._bitsPerSample, frame);
if (status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(status, "PvSpeaker failed to write audio data frame.");
}

i += frameLength;
public write(pcm: ArrayBuffer): number {
const result = PvSpeaker._pvSpeaker.write(this._handle, this._bitsPerSample, pcm);
if (result.status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(result.status, "PvSpeaker failed to write audio data frame.");
}

return result.written_length;
}

/**
* Enable or disable debug logging for PvSpeaker. Debug logs will indicate when there are overflows in the internal
* frame buffer and when an audio source is generating frames of silence.
* Synchronous call to write PCM data to the internal circular buffer for audio playback.
* This call blocks the thread until all PCM data has been successfully written and played.
*
* @param isDebugLoggingEnabled Boolean indicating whether the debug logging is enabled or disabled.
* @returns {number}
*/
public setDebugLogging(isDebugLoggingEnabled: boolean): void {
PvSpeaker._pvSpeaker.set_debug_logging(this._handle, isDebugLoggingEnabled);
public flush(pcm: ArrayBuffer = new ArrayBuffer(0)): number {
const result = PvSpeaker._pvSpeaker.flush(this._handle, this._bitsPerSample, pcm);
if (result.status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(result.status, "PvSpeaker failed to flush audio data frame.");
}

return result.written_length;
}

/**
Expand Down
1 change: 0 additions & 1 deletion binding/nodejs/src/pv_speaker_status_t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ enum PvSpeakerStatus {
OUT_OF_MEMORY,
INVALID_ARGUMENT,
INVALID_STATE,
OVERFLOW_BUFFER,
BACKEND_ERROR,
DEVICE_ALREADY_INITIALIZED,
DEVICE_NOT_INITIALIZED,
Expand Down
101 changes: 57 additions & 44 deletions binding/nodejs/test/pv_speaker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,63 @@ import { PvSpeaker } from "../src";

const SAMPLE_RATE = 22050;
const BITS_PER_SAMPLE = 16;
const FRAME_LENGTH = 512;
const BUFFER_SIZE_SECS = 10;

describe("Test PvSpeaker", () => {
test("invalid device index", () => {
const f = () => {
new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, -2);
};

expect(f).toThrow(Error);
test("invalid sample rate", () => {
expect(() => {
new PvSpeaker(0, BITS_PER_SAMPLE);
}).toThrow(Error);
});

test("invalid frame length", () => {
const f = () => {
new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, 0, 0);
};

expect(f).toThrow(Error);
test("invalid bits per sample", () => {
expect(() => {
new PvSpeaker(SAMPLE_RATE, 0);
}).toThrow(Error);
});

test("invalid buffered frames count", () => {
const f = () => {
new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, 0, FRAME_LENGTH, 0);
};
test("invalid buffer size secs", () => {
expect(() => {
new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, { bufferSizeSecs: 0 });
}).toThrow(Error);
});

expect(f).toThrow(Error);
test("invalid device index", () => {
expect(() => {
new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, { deviceIndex: -2 });
}).toThrow(Error);
});

test("start stop", async () => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE);
speaker.start();

const f = async () => {
const frames = new Int16Array(FRAME_LENGTH * 2).buffer;
speaker.write(frames);
expect(() => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE);
speaker.start();
const pcm = new ArrayBuffer(SAMPLE_RATE);
speaker.write(pcm);
speaker.flush(pcm);
speaker.flush();
speaker.stop();
speaker.release();
}

expect(f).not.toThrow(Error);
}).not.toThrow(Error);
});

test("set debug logging", () => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE);
speaker.setDebugLogging(true);
speaker.setDebugLogging(false);
test("write flow", async () => {
const bufferSizeSecs = 1;
const circularBufferSize = SAMPLE_RATE * bufferSizeSecs;
const bytesPerSample = (BITS_PER_SAMPLE / 8)

Check warning on line 48 in binding/nodejs/test/pv_speaker.test.ts

View workflow job for this annotation

GitHub Actions / check-nodejs-codestyle

Missing semicolon
const pcm = new ArrayBuffer(circularBufferSize * bytesPerSample + bytesPerSample);
const pcmLength = pcm.byteLength / bytesPerSample;

const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, { bufferSizeSecs });
speaker.start();

let writeCount = speaker.write(pcm);
expect(writeCount).toBe(circularBufferSize);
writeCount = speaker.flush(pcm);
expect(writeCount).toBe(pcmLength);
writeCount = speaker.flush();
expect(writeCount).toBe(0);

speaker.release();
});

Expand Down Expand Up @@ -90,32 +103,32 @@ describe("Test PvSpeaker", () => {
speaker.release();
});

test("bits per sample", () => {
test("sample rate", () => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE);

expect(speaker.bitsPerSample).toBeDefined();
expect(typeof speaker.bitsPerSample).toBe("number");
expect(speaker.bitsPerSample).toBeGreaterThan(0);
expect(speaker.sampleRate).toBeDefined();
expect(typeof speaker.sampleRate).toBe("number");
expect(speaker.sampleRate).toBe(SAMPLE_RATE);

speaker.release();
});

test("sample rate", () => {
test("bits per sample", () => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE);

expect(speaker.sampleRate).toBeDefined();
expect(typeof speaker.sampleRate).toBe("number");
expect(speaker.sampleRate).toBeGreaterThan(0);
expect(speaker.bitsPerSample).toBeDefined();
expect(typeof speaker.bitsPerSample).toBe("number");
expect(speaker.bitsPerSample).toBe(BITS_PER_SAMPLE);

speaker.release();
});

test("frame length", () => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE);
test("buffer size secs", () => {
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, { bufferSizeSecs: BUFFER_SIZE_SECS });

expect(speaker.frameLength).toBeDefined();
expect(typeof speaker.frameLength).toBe("number");
expect(speaker.frameLength).toBeGreaterThan(0);
expect(speaker.bufferSizeSecs).toBeDefined();
expect(typeof speaker.bufferSizeSecs).toBe("number");
expect(speaker.bufferSizeSecs).toBe(BUFFER_SIZE_SECS);

speaker.release();
});
Expand Down
Loading

0 comments on commit b174773

Please sign in to comment.