Skip to content

Commit

Permalink
add write_to_file
Browse files Browse the repository at this point in the history
  • Loading branch information
albho committed Aug 8, 2024
1 parent fc0b33e commit c902154
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const { PvSpeaker } = require("@picovoice/pvspeaker-node");
const sampleRate = 22050;
const bitsPerSample = 16;
const deviceIndex = 0;
const speaker = new PvSpeaker(sampleRate, bitsPerSample, deviceIndex);
const speaker = new PvSpeaker(sampleRate, bitsPerSample, { deviceIndex });

speaker.start()
```
Expand Down
2 changes: 1 addition & 1 deletion binding/nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const devices = PvSpeaker.getAvailableDevices()
const sampleRate = 22050;
const bitsPerSample = 16;
const deviceIndex = 0;
const speaker = new PvSpeaker(sampleRate, bitsPerSample, deviceIndex);
const speaker = new PvSpeaker(sampleRate, bitsPerSample, { deviceIndex });

speaker.start()
```
Expand Down
43 changes: 27 additions & 16 deletions binding/nodejs/src/pv_speaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class PvSpeaker {
* @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.
* @param options.deviceIndex The audio device index to use to play audio. A value of (-1) will use the machine's default audio device.
*/
constructor(
sampleRate: number,
Expand All @@ -57,7 +57,7 @@ class PvSpeaker {
}
const status = pvSpeakerHandleAndStatus.status;
if (status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(status, "PvSpeaker failed to initialize.");
throw pvSpeakerStatusToException(status, "Failed to initialize PvSpeaker.");
}
this._handle = pvSpeakerHandleAndStatus.handle;
this._sampleRate = sampleRate;
Expand All @@ -67,57 +67,57 @@ class PvSpeaker {
}

/**
* @returns The sample rate of the audio to be played.
* @returns {number} The sample rate matching the value passed to the constructor.
*/
get sampleRate(): number {
return this._sampleRate;
}

/**
* @returns The number of bits per sample.
* @returns {number} The bits per sample matching the value passed to the constructor.
*/
get bitsPerSample(): number {
return this._bitsPerSample;
}

/**
* @returns The size in seconds of the internal buffer used to buffer PCM data.
* @returns {number} The buffer size in seconds matching the value passed to the constructor.
*/
get bufferSizeSecs(): number {
return this._bufferSizeSecs;
}

/**
* @returns the version of the PvSpeaker
* @returns {number} The current version of PvSpeaker library.
*/
get version(): string {
return this._version;
}

/**
* @returns Whether PvSpeaker has started and is available to receive PCM frames or not.
* @returns {boolean} Whether the speaker 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` and/or `flush`.
* Starts the audio output device.
*/
public start(): void {
const status = PvSpeaker._pvSpeaker.start(this._handle);
if (status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(status, "PvSpeaker failed to start.");
throw pvSpeakerStatusToException(status, "Failed to start device.");
}
}

/**
* Stops playing audio.
* Stops the audio output device.
*/
public stop(): void {
const status = PvSpeaker._pvSpeaker.stop(this._handle);
if (status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(status, "PvSpeaker failed to stop.");
throw pvSpeakerStatusToException(status, "Failed to stop device.");
}
}

Expand All @@ -127,12 +127,12 @@ class PvSpeaker {
* returns the length of the PCM data that was successfully written.
*
* @param {ArrayBuffer} pcm PCM data to be played.
* @returns {number} The length of the PCM data that was successfully written.
* @returns {number} Length of the PCM data that was successfully written.
*/
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 PCM data.");
throw pvSpeakerStatusToException(result.status, "Failed to write to device.");
}

return result.written_length;
Expand All @@ -141,22 +141,33 @@ class PvSpeaker {
/**
* 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.
* To simply wait for previously written PCM data to finish playing, call `flush` with no arguments.
*
* @param {ArrayBuffer} pcm PCM data to be played.
* @returns {number} The length of the PCM data that was successfully written.
*/
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 PCM data.");
throw pvSpeakerStatusToException(result.status, "Failed to flush PCM data.");
}

return result.written_length;
}

/**
* Returns the name of the selected device used to play audio.
* Writes PCM data passed to PvSpeaker to a specified WAV file.
*
* @param {string} outputPath Path to the output WAV file where the PCM data will be written.
*/
public writeToFile(outputPath: string): void {
const status = PvSpeaker._pvSpeaker.write_to_file(this._handle, outputPath);
if (status !== PvSpeakerStatus.SUCCESS) {
throw pvSpeakerStatusToException(status, "Failed to open FILE object. PCM data will not be written.");
}
}

/**
* Gets the audio device that the given PvSpeaker instance is using.
*
* @returns {string} Name of the selected audio device.
*/
Expand Down
16 changes: 14 additions & 2 deletions binding/nodejs/test/pv_speaker.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { PvSpeaker } from "../src";

const fs = require('fs');
const path = require('path');

const SAMPLE_RATE = 22050;
const BITS_PER_SAMPLE = 16;
const BUFFER_SIZE_SECS = 10;
Expand Down Expand Up @@ -46,19 +49,28 @@ describe("Test PvSpeaker", () => {
test("write flow", async () => {
const bufferSizeSecs = 1;
const circularBufferSize = SAMPLE_RATE * bufferSizeSecs;
const bytesPerSample = (BITS_PER_SAMPLE / 8)
const bytesPerSample = (BITS_PER_SAMPLE / 8);
const pcm = new ArrayBuffer(circularBufferSize * bytesPerSample + bytesPerSample);
const pcmLength = pcm.byteLength / bytesPerSample;
const speaker = new PvSpeaker(SAMPLE_RATE, BITS_PER_SAMPLE, { bufferSizeSecs });

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

const outputPath = path.join(__dirname, "tmp.wav");
speaker.writeToFile(outputPath);
const fileExists = fs.existsSync(outputPath);
expect(fileExists).toBe(true);

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.stop();
speaker.release();
fs.unlinkSync(outputPath);
});

test("is started", () => {
Expand Down
31 changes: 18 additions & 13 deletions demo/nodejs/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ const { PvSpeaker } = require("@picovoice/pvspeaker-node");
program
.option(
"-s, --show_audio_devices",
"show the list of available devices"
"List of audio devices currently available for use."
).option(
"-d, --audio_device_index <number>",
"index of audio device to use to play audio",
"Index of input audio device.",
Number,
-1
).option(
"-i, --input_wav_path <string>",
"path to PCM WAV file to be played"
"Path to PCM WAV file to be played."
).option(
"-b, --buffer_size_secs <number>",
"size of internal PCM buffer in seconds",
"Size of internal PCM buffer in seconds.",
Number,
20
).option(
"-o, --output_wav_path <string>",
"Path to the output WAV file where the PCM data passed to PvSpeaker will be written."
);

if (process.argv.length < 2) {
Expand All @@ -57,6 +60,7 @@ async function runDemo() {
let deviceIndex = program["audio_device_index"];
let inputWavPath = program["input_wav_path"];
let bufferSizeSecs = program["buffer_size_secs"];
let outputPath = program["output_wav_path"];

if (showAudioDevices) {
const devices = PvSpeaker.getAvailableDevices();
Expand Down Expand Up @@ -92,19 +96,20 @@ async function runDemo() {

speaker.start();

if (outputPath) {
speaker.writeToFile(outputPath);
}

console.log("Playing audio...");
const bytesPerSample = bitsPerSample / 8;
const pcmChunks = splitArrayBuffer(pcmBuffer, sampleRate * bytesPerSample);

pcmChunks.forEach(pcmChunk => {
let sublistLength = pcmChunk.byteLength / bytesPerSample;
let totalWrittenLength = 0;
while (totalWrittenLength < sublistLength) {
let remainingBuffer = pcmChunk.slice(totalWrittenLength);
let writtenLength = speaker.write(remainingBuffer);
totalWrittenLength += writtenLength;
for (const pcmChunk of pcmChunks) {
let totalWrittenByteLength = 0;
while (totalWrittenByteLength < pcmChunk.byteLength) {
const writtenLength = speaker.write(pcmChunk.slice(totalWrittenByteLength));
totalWrittenByteLength += (writtenLength * bytesPerSample);
}
});
}

console.log("Waiting for audio to finish...");
speaker.flush();
Expand Down
Loading

0 comments on commit c902154

Please sign in to comment.