diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d68eee5..8589aa0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -46,6 +46,14 @@ jobs: with: python-version: ${{ matrix.python-version }} + # GitHub Actions runners do not have sound cards, so a virtual one must be created in order for unit tests to run. + - name: Install PulseAudio on Ubuntu + run: | + sudo apt-get update + sudo apt-get install -y pulseaudio + pulseaudio --check || pulseaudio --start + pactl load-module module-null-sink + - name: Test run: python3 test_pv_speaker.py @@ -61,6 +69,19 @@ jobs: with: submodules: recursive + - name: Install PulseAudio + if: matrix.machine == 'rpi3-32' || + matrix.machine == 'rpi3-64' || + matrix.machine == 'rpi4-32' || + matrix.machine == 'rpi4-64' || + matrix.machine == 'rpi5-32' || + matrix.machine == 'rpi5-64' + run: | + sudo apt-get update + sudo apt-get install -y pulseaudio + pulseaudio --check || pulseaudio --start + pactl load-module module-null-sink + - name: Test run: python3 test_pv_speaker.py if: ${{ matrix.machine != 'pv-windows' }} diff --git a/README.md b/README.md index b208f16..75fa817 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ With a working speaker connected to your device run the following in the termina pv_speaker_demo --input_wav_path {INPUT_WAV_PATH} ``` -Replace `{INPUT_WAV_PATH}` with the path to the pcm `wav` file you wish to play. +Replace `{INPUT_WAV_PATH}` with the path to the PCM WAV file you wish to play. For more information about the Python demos go to [demo/python](demo/python). @@ -89,7 +89,7 @@ Play from a single-channel PCM WAV file with a given audio device index: ./pv_speaker_demo -i test.wav -d 2 ``` -Hit `Ctrl+C` if you wish to stop audio playback before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. +Hit `Ctrl+C` if you wish to stop playing audio before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. For more information about the C demo, go to [demo/c](demo/c). @@ -119,13 +119,21 @@ def get_next_audio_frame(): speaker.write(get_next_audio_frame()) ``` -When all frames have been written, run `stop()` on the instance: +When all frames have been written, run `flush()` to wait for all buffered PCM data to be played: + +```python +speaker.flush() +``` + +To stop playing audio, run `stop()`: ```python speaker.stop() ``` -Once you are done, free the resources acquired by PvSpeaker. You do not have to call `stop()` before `delete()`: +Note that in order to stop the audio before it finishes playing, `stop` must be run on a separate thread from `flush`. + +Once you are done (i.e. no longer need PvSpeaker to write and/or play PCM), free the resources acquired by PvSpeaker by calling `delete`. Be sure to first call `stop` if the audio is still playing. Otherwise, if the audio has already finished playing, you do not have to call `stop` before `delete`: ```python speaker.delete() diff --git a/binding/python/README.md b/binding/python/README.md index 9eb992f..7857924 100644 --- a/binding/python/README.md +++ b/binding/python/README.md @@ -25,6 +25,7 @@ from pvspeaker import PvSpeaker speaker = PvSpeaker( sample_rate=22050, bits_per_sample=16, + buffer_size_secs=20, device_index=0) speaker.start() @@ -42,6 +43,7 @@ devices = PvSpeaker.get_available_devices() speaker = PvSpeaker( sample_rate=22050, bits_per_sample=16, + buffer_size_secs=20, device_index=0) speaker.start() @@ -56,13 +58,21 @@ def get_next_audio_frame(): speaker.write(get_next_audio_frame()) ``` -When all frames have been written, run `stop()` on the instance: +When all frames have been written, run `flush()` to wait for all buffered pcm data to be played: + +```python +speaker.flush() +``` + +To stop playing audio, run `stop()`: ```python speaker.stop() ``` -Once you are done, free the resources acquired by PvSpeaker. You do not have to call `stop()` before `delete()`: +Note that in order to stop the audio before it finishes playing, `stop` must be run on a separate thread from `flush`. + +Once you are done (i.e. no longer need PvSpeaker to write and/or play PCM), free the resources acquired by PvSpeaker by calling `delete`. Be sure to first call `stop` if the audio is still playing. Otherwise, if the audio has already finished playing, you do not have to call `stop` before `delete`: ```python speaker.delete() diff --git a/binding/python/_pvspeaker.py b/binding/python/_pvspeaker.py index 1ec0950..6eee654 100644 --- a/binding/python/_pvspeaker.py +++ b/binding/python/_pvspeaker.py @@ -57,7 +57,6 @@ class PvSpeakerStatuses(Enum): OUT_OF_MEMORY = 1 INVALID_ARGUMENT = 2 INVALID_STATE = 3 - BUFFER_OVERFLOW = 3 BACKEND_ERROR = 4 DEVICE_ALREADY_INITIALIZED = 5 DEVICE_NOT_INITIALIZED = 6 @@ -68,7 +67,6 @@ class PvSpeakerStatuses(Enum): PvSpeakerStatuses.OUT_OF_MEMORY: MemoryError, PvSpeakerStatuses.INVALID_ARGUMENT: ValueError, PvSpeakerStatuses.INVALID_STATE: ValueError, - PvSpeakerStatuses.BUFFER_OVERFLOW: IOError, PvSpeakerStatuses.BACKEND_ERROR: SystemError, PvSpeakerStatuses.DEVICE_ALREADY_INITIALIZED: ValueError, PvSpeakerStatuses.DEVICE_NOT_INITIALIZED: ValueError, @@ -86,19 +84,16 @@ def __init__( self, sample_rate: int, bits_per_sample: int, - device_index: int = -1, - frame_length: int = 512, - buffered_frames_count: int = 50): + buffer_size_secs: int = 20, + device_index: int = -1): """ Constructor :param sample_rate: The sample rate of the audio to be played. :param bits_per_sample: The number of bits per sample. + :param buffer_size_secs: The size in seconds of the internal buffer used to buffer pcm data + - i.e. internal circular buffer will be of size `sample_rate` * `buffer_size_secs`. :param device_index: The index of the audio device to use. A value of (-1) will resort to default device. - :param frame_length: The maximum length of audio frame that will be passed to each write call. - :param buffered_frames_count: The number of audio frames buffered internally for writing - i.e. internal - circular buffer will be of size `frame_length` * `buffered_frames_count`. If this value is too low, - buffer overflows could occur audio frames could be dropped. A higher value will increase memory usage. """ library = self._get_library() @@ -109,18 +104,17 @@ def __init__( c_int32, c_int32, c_int32, - c_int32, POINTER(POINTER(self.CPvSpeaker)) ] init_func.restype = self.PvSpeakerStatuses self._handle = POINTER(self.CPvSpeaker)() self._sample_rate = sample_rate - self._frame_length = frame_length self._bits_per_sample = bits_per_sample + self._buffer_size_secs = buffer_size_secs status = init_func( - sample_rate, frame_length, bits_per_sample, device_index, buffered_frames_count, byref(self._handle)) + sample_rate, bits_per_sample, buffer_size_secs, device_index, byref(self._handle)) if status is not self.PvSpeakerStatuses.SUCCESS: raise self._PVSPEAKER_STATUS_TO_EXCEPTION[status]("Failed to initialize PvSpeaker.") @@ -136,14 +130,14 @@ def __init__( self._stop_func.argtypes = [POINTER(self.CPvSpeaker)] self._stop_func.restype = self.PvSpeakerStatuses - self._set_debug_logging_func = library.pv_speaker_set_debug_logging - self._set_debug_logging_func.argtypes = [POINTER(self.CPvSpeaker), c_bool] - self._set_debug_logging_func.restype = None - self._write_func = library.pv_speaker_write - self._write_func.argtypes = [POINTER(self.CPvSpeaker), c_int32, c_void_p] + self._write_func.argtypes = [POINTER(self.CPvSpeaker), c_char_p, c_int32, POINTER(c_int32)] self._write_func.restype = self.PvSpeakerStatuses + self._flush_func = library.pv_speaker_flush + self._flush_func.argtypes = [POINTER(self.CPvSpeaker), c_char_p, c_int32, POINTER(c_int32)] + self._flush_func.restype = self.PvSpeakerStatuses + self._get_is_started_func = library.pv_speaker_get_is_started self._get_is_started_func.argtypes = [POINTER(self.CPvSpeaker)] self._get_is_started_func.restype = c_bool @@ -169,49 +163,54 @@ def start(self) -> None: raise self._PVSPEAKER_STATUS_TO_EXCEPTION[status]("Failed to start device.") def stop(self) -> None: - """Stops playing audio.""" + """Stops the device.""" status = self._stop_func(self._handle) if status is not self.PvSpeakerStatuses.SUCCESS: raise self._PVSPEAKER_STATUS_TO_EXCEPTION[status]("Failed to stop device.") - def write(self, pcm) -> None: - """Synchronous call to write pcm frames to selected device for audio playback.""" - - i = 0 - while i < len(pcm): - is_last_frame = i + self._frame_length >= len(pcm) - write_frame_length = len(pcm) - i if is_last_frame else self._frame_length - - start_index = i - end_index = i + write_frame_length - frame = pcm[start_index:end_index] - - byte_data = None - if self._bits_per_sample == 8: - byte_data = pack('B' * len(frame), *frame) - elif self._bits_per_sample == 16: - byte_data = pack('h' * len(frame), *frame) - elif self._bits_per_sample == 24: - byte_data = b''.join(pack(' bytes: + byte_data = None + if self._bits_per_sample == 8: + byte_data = pack('B' * len(pcm), *pcm) + elif self._bits_per_sample == 16: + byte_data = pack('h' * len(pcm), *pcm) + elif self._bits_per_sample == 24: + byte_data = b''.join(pack(' int: + """ + 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. + """ - status = self._write_func(self._handle, c_int32(len(frame)), c_char_p(byte_data)) - if status is not self.PvSpeakerStatuses.SUCCESS: - raise self._PVSPEAKER_STATUS_TO_EXCEPTION[status]("Failed to write to device.") + written_length = c_int32() + status = self._write_func( + self._handle, c_char_p(self._pcm_to_bytes(pcm)), c_int32(len(pcm)), byref(written_length)) + if status is not self.PvSpeakerStatuses.SUCCESS: + raise self._PVSPEAKER_STATUS_TO_EXCEPTION[status]("Failed to write to device.") - i += self._frame_length + return written_length.value - def set_debug_logging(self, is_debug_logging_enabled: bool) -> None: + def flush(self, pcm=None) -> int: """ - Enable or disable debug logging for PvSpeaker. Debug logs will indicate when there are overflows - in the internal frame buffer. - - :param is_debug_logging_enabled: Boolean indicating whether the debug logging is enabled or disabled. + 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. """ - self._set_debug_logging_func(self._handle, is_debug_logging_enabled) + if pcm is None: + pcm = [] + written_length = c_int32() + status = self._flush_func( + self._handle, c_char_p(self._pcm_to_bytes(pcm)), c_int32(len(pcm)), byref(written_length)) + if status is not self.PvSpeakerStatuses.SUCCESS: + raise self._PVSPEAKER_STATUS_TO_EXCEPTION[status]("Failed to write to device.") + + return written_length.value @property def is_started(self) -> bool: @@ -239,18 +238,18 @@ def sample_rate(self) -> int: return self._sample_rate - @property - def frame_length(self) -> int: - """Gets the frame length matching the value given to `__init__()`.""" - - return self._frame_length - @property def bits_per_sample(self) -> int: """Gets the bits per sample matching the value given to `__init__()`.""" return self._bits_per_sample + @property + def buffer_size_secs(self) -> int: + """Gets the buffer size in seconds matching the value given to `__init__()`.""" + + return self._buffer_size_secs + @staticmethod def get_available_devices() -> List[str]: """Gets the list of available audio devices that can be used for playing. diff --git a/binding/python/setup.py b/binding/python/setup.py index f8373b4..55c8500 100644 --- a/binding/python/setup.py +++ b/binding/python/setup.py @@ -56,7 +56,7 @@ setuptools.setup( name="pvspeaker", - version="1.0.0", + version="1.0.1", author="Picovoice", author_email="hello@picovoice.ai", description="Speaker library for Picovoice.", diff --git a/binding/python/test_pv_speaker.py b/binding/python/test_pv_speaker.py index 302b882..571b2a9 100644 --- a/binding/python/test_pv_speaker.py +++ b/binding/python/test_pv_speaker.py @@ -20,47 +20,57 @@ class PvSpeakerTestCase(unittest.TestCase): def setUpClass(cls): PvSpeaker.set_default_library_path(os.path.join('..', '..')) - def test_invalid_device_index(self): + def test_invalid_sample_rate(self): with self.assertRaises(ValueError): - _ = PvSpeaker(16000, 16, -2) + _ = PvSpeaker(0, 16, 20, 0) - def test_invalid_frame_length(self): + def test_invalid_bits_per_sample(self): with self.assertRaises(ValueError): - _ = PvSpeaker(16000, 16, 0, 0) + _ = PvSpeaker(16000, 0, 20, 0) - def test_invalid_buffered_frame_count(self): + def test_invalid_buffer_size_secs(self): with self.assertRaises(ValueError): - _ = PvSpeaker(16000, 16, 0, 512, 0) + _ = PvSpeaker(16000, 16, 0, 0) - def test_set_frame_length(self): - speaker = PvSpeaker(16000, 16, 0, 256) - frame_length = speaker.frame_length - self.assertEqual(frame_length, 256) - self.assertIsInstance(frame_length, int) - speaker.delete() + def test_invalid_device_index(self): + with self.assertRaises(ValueError): + _ = PvSpeaker(16000, 16, 20, -2) def test_start_stop(self): error = False try: - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) speaker.start() - frame = [0] * (512 * 2) - speaker.write(frame) + pcm = [0] * (512 * 2) + speaker.write(pcm) + speaker.flush(pcm) + speaker.flush() speaker.stop() speaker.delete() except ValueError or IOError: error = True self.assertFalse(error) - def test_set_debug_logging(self): - speaker = PvSpeaker(16000, 16, 0) - speaker.set_debug_logging(True) - speaker.set_debug_logging(False) - self.assertIsNotNone(speaker) + def test_write_flow(self): + sample_rate = 16000 + buffer_size_secs = 1 + circular_buffer_size = sample_rate * buffer_size_secs + pcm = [0] * (circular_buffer_size + 1) + + speaker = PvSpeaker(sample_rate, 16, buffer_size_secs) + speaker.start() + + write_count = speaker.write(pcm) + self.assertEqual(write_count, circular_buffer_size) + write_count = speaker.flush(pcm) + self.assertEqual(write_count, len(pcm)) + write_count = speaker.flush() + self.assertEqual(write_count, 0) + speaker.delete() def test_is_started(self): - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) speaker.start() self.assertTrue(speaker.is_started) speaker.stop() @@ -68,14 +78,14 @@ def test_is_started(self): speaker.delete() def test_selected_device(self): - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) device = speaker.selected_device self.assertIsNotNone(device) self.assertIsInstance(device, str) speaker.delete() def test_get_available_devices(self): - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) devices = speaker.get_available_devices() self.assertIsNotNone(devices) for device in devices: @@ -84,33 +94,33 @@ def test_get_available_devices(self): speaker.delete() def test_version(self): - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) version = speaker.version self.assertGreater(len(version), 0) self.assertIsInstance(version, str) speaker.delete() def test_sample_rate(self): - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) sample_rate = speaker.sample_rate self.assertEqual(sample_rate, 16000) self.assertIsInstance(sample_rate, int) speaker.delete() - def test_frame_length(self): - speaker = PvSpeaker(16000, 16, 0) - frame_length = speaker.frame_length - self.assertEqual(frame_length, 512) - self.assertIsInstance(frame_length, int) - speaker.delete() - def test_bits_per_sample(self): - speaker = PvSpeaker(16000, 16, 0) + speaker = PvSpeaker(16000, 16, 20) bits_per_sample = speaker.bits_per_sample self.assertEqual(bits_per_sample, 16) self.assertIsInstance(bits_per_sample, int) speaker.delete() + def test_buffer_size_secs(self): + speaker = PvSpeaker(16000, 16, 20) + buffer_size_secs = speaker.buffer_size_secs + self.assertEqual(buffer_size_secs, 20) + self.assertIsInstance(buffer_size_secs, int) + speaker.delete() + if __name__ == '__main__': unittest.main() diff --git a/demo/c/README.md b/demo/c/README.md index 4ae6557..d0f8f6a 100644 --- a/demo/c/README.md +++ b/demo/c/README.md @@ -49,4 +49,4 @@ Play from a single-channel PCM WAV file with a given audio device index: ./pv_speaker_demo -i test.wav -d 2 ``` -Hit `Ctrl+C` if you wish to stop audio playback before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. +Hit `Ctrl+C` if you wish to stop playing audio before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. diff --git a/demo/c/pv_speaker_demo.c b/demo/c/pv_speaker_demo.c index ed91fb7..a7ebcc6 100644 --- a/demo/c/pv_speaker_demo.c +++ b/demo/c/pv_speaker_demo.c @@ -20,20 +20,25 @@ static volatile bool is_interrupted = false; +pv_speaker_t *speaker = NULL; + void interrupt_handler(int _) { (void) _; is_interrupted = true; + pv_speaker_stop(speaker); + fprintf(stdout, "\nStopped...\n"); } static struct option long_options[] = { {"show_audio_devices", no_argument, NULL, 's'}, {"input_wav_path", required_argument, NULL, 'i'}, - {"audio_device_index", required_argument, NULL, 'd'} + {"audio_device_index", required_argument, NULL, 'd'}, + {"buffer_size_secs", required_argument, NULL, 'b'} }; static void print_usage(const char *program_name) { fprintf(stderr, - "Usage : %s -i INPUT_WAV_PATH [-d AUDIO_DEVICE_INDEX]\n" + "Usage : %s -i INPUT_WAV_PATH [-d AUDIO_DEVICE_INDEX] [-b BUFFER_SIZE_SECS]\n" " %s --show_audio_devices\n", program_name, program_name); @@ -107,13 +112,12 @@ void *read_wav_file(const char *filename, uint32_t *num_samples, uint32_t *sampl } *sample_rate = header.sample_rate; - *bits_per_sample = header.bits_per_sample; - uint32_t bytes_per_sample = header.bits_per_sample / 8; *num_samples = header.subchunk2_size / bytes_per_sample; void *pcm_data = malloc(header.subchunk2_size); + if (!pcm_data) { perror("Memory allocation failed"); fclose(file); @@ -130,9 +134,10 @@ void *read_wav_file(const char *filename, uint32_t *num_samples, uint32_t *sampl int main(int argc, char *argv[]) { const char *input_wav_path = NULL; int32_t device_index = -1; + int32_t buffer_size_secs = 20; int c; - while ((c = getopt_long(argc, argv, "si:d:", long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, "si:d:b:", long_options, NULL)) != -1) { switch (c) { case 's': show_audio_devices(); @@ -143,6 +148,9 @@ int main(int argc, char *argv[]) { case 'd': device_index = (int32_t) strtol(optarg, NULL, 10); break; + case 'b': + buffer_size_secs = (int32_t) strtol(optarg, NULL, 10); + break; default: exit(1); } @@ -158,25 +166,20 @@ int main(int argc, char *argv[]) { uint32_t num_samples, sample_rate; uint16_t bits_per_sample; - void *pcm = read_wav_file(input_wav_path, &num_samples, &sample_rate, &bits_per_sample); + void *pcm_data = read_wav_file(input_wav_path, &num_samples, &sample_rate, &bits_per_sample); fprintf(stdout, "Initializing pv_speaker...\n"); - const int32_t frame_length = 512; - pv_speaker_t *speaker = NULL; pv_speaker_status_t status = pv_speaker_init( sample_rate, - frame_length, bits_per_sample, + buffer_size_secs, device_index, - 10, &speaker); if (status != PV_SPEAKER_STATUS_SUCCESS) { fprintf(stderr, "Failed to initialize device with %s.\n", pv_speaker_status_to_string(status)); exit(1); } - pv_speaker_set_debug_logging(speaker, true); - const char *selected_device = pv_speaker_get_selected_device(speaker); fprintf(stdout, "Selected device: %s.\n", selected_device); @@ -187,31 +190,35 @@ int main(int argc, char *argv[]) { } fprintf(stdout, "Playing audio...\n"); - if (pcm) { - char *pcmData = (char *) pcm; - for (int i = 0; i < num_samples; i += frame_length) { - bool is_last_frame = i + frame_length >= num_samples; + if (pcm_data) { + int8_t *pcm = (int8_t *) pcm_data; + int32_t total_written_length = 0; + while (!is_interrupted && total_written_length < num_samples) { + int32_t written_length = 0; status = pv_speaker_write( speaker, - is_last_frame ? num_samples - i : frame_length, - &pcmData[i * bits_per_sample / 8]); + &pcm[total_written_length * bits_per_sample / 8], + num_samples - total_written_length, + &written_length); if (status != PV_SPEAKER_STATUS_SUCCESS) { fprintf(stderr, "Failed to write with %s.\n", pv_speaker_status_to_string(status)); exit(1); } - if (is_interrupted) { - fprintf(stdout, "\nStopped audio...\n"); - break; - } + total_written_length += written_length; } free(pcm); } - status = pv_speaker_stop(speaker); + fprintf(stdout, "Waiting for audio to finish...\n"); + int32_t pcm_length = 0; + int16_t pcm[pcm_length]; + int8_t *pcm_ptr = (int8_t *) pcm; + int32_t written_length = 0; + status = pv_speaker_flush(speaker, pcm_ptr, pcm_length, &written_length); if (status != PV_SPEAKER_STATUS_SUCCESS) { - fprintf(stderr, "Failed to stop device with %s.\n", pv_speaker_status_to_string(status)); + fprintf(stderr, "Failed to flush pcm with %s.\n", pv_speaker_status_to_string(status)); exit(1); } @@ -219,6 +226,12 @@ int main(int argc, char *argv[]) { fprintf(stdout, "Finished playing audio...\n"); } + status = pv_speaker_stop(speaker); + if (status != PV_SPEAKER_STATUS_SUCCESS) { + fprintf(stderr, "Failed to stop device with %s.\n", pv_speaker_status_to_string(status)); + exit(1); + } + fprintf(stdout, "Deleting pv_speaker...\n"); pv_speaker_delete(speaker); diff --git a/demo/python/README.md b/demo/python/README.md index 13f8b31..0fa8b0f 100644 --- a/demo/python/README.md +++ b/demo/python/README.md @@ -33,4 +33,6 @@ To run PvSpeaker run: ```console pv_speaker_demo --audio_device_index {AUDIO_DEVICE_INDEX} --input_wav_path {INPUT_WAV_PATH} -``` \ No newline at end of file +``` + +Hit `Ctrl+C` if you wish to stop playing audio before it completes. If no audio device index (`-d`) is provided, the demo will use the system's default audio player device. \ No newline at end of file diff --git a/demo/python/pv_speaker_demo.py b/demo/python/pv_speaker_demo.py index 7871a52..68ac813 100644 --- a/demo/python/pv_speaker_demo.py +++ b/demo/python/pv_speaker_demo.py @@ -14,28 +14,53 @@ import wave import array +import threading + from pvspeaker import PvSpeaker +def blocking_call(speaker): + speaker.flush() + + +def worker_function(speaker, completion_event): + blocking_call(speaker) + completion_event.set() + + +def split_list(input_list, x): + return [input_list[i:i + x] for i in range(0, len(input_list), x)] + + def main(): parser = argparse.ArgumentParser() parser.add_argument( "--show_audio_devices", + "-s", help="List of audio devices currently available for use.", action="store_true") parser.add_argument( "--audio_device_index", + "-d", help="Index of input audio device.", type=int, default=-1) parser.add_argument( "--input_wav_path", + "-i", help="Path to PCM WAV file to be played.", default=None) + parser.add_argument( + "--buffer_size_secs", + "-b", + help="Size of internal PCM buffer in seconds.", + type=int, + default=20) + args = parser.parse_args() if args.show_audio_devices: @@ -45,6 +70,7 @@ def main(): else: device_index = args.audio_device_index input_path = args.input_wav_path + buffer_size_secs = args.buffer_size_secs wavfile = None speaker = None @@ -71,6 +97,7 @@ def main(): speaker = PvSpeaker( sample_rate=sample_rate, bits_per_sample=bits_per_sample, + buffer_size_secs=buffer_size_secs, device_index=device_index) print("pvspeaker version: %s" % speaker.version) print("Using device: %s" % speaker.selected_device) @@ -90,18 +117,35 @@ def main(): elif bits_per_sample == 32: pcm = list(array.array('i', wav_bytes)) + pcm_list = split_list(pcm, sample_rate) speaker.start() print("Playing audio...") - speaker.write(pcm) + for pcm_sublist in pcm_list: + sublist_length = len(pcm_sublist) + total_written_length = 0 + while total_written_length < sublist_length: + written_length = speaker.write(pcm_sublist[total_written_length:]) + total_written_length += written_length + + print("Waiting for audio to finish...") + + completion_event = threading.Event() + worker_thread = threading.Thread(target=worker_function, args=(speaker, completion_event)) + worker_thread.start() + completion_event.wait() + worker_thread.join() + speaker.stop() print("Finished playing audio...") wavfile.close() except KeyboardInterrupt: - print("Stopping...") + speaker.stop() + print("\nStopped...") finally: + print("Deleting PvSpeaker...") if speaker is not None: speaker.delete() if wavfile is not None: diff --git a/demo/python/requirements.txt b/demo/python/requirements.txt index 3c5aeaa..acf2ad5 100644 --- a/demo/python/requirements.txt +++ b/demo/python/requirements.txt @@ -1 +1 @@ -pvspeaker==1.0.0 \ No newline at end of file +pvspeaker==1.0.1 \ No newline at end of file diff --git a/demo/python/setup.py b/demo/python/setup.py index 9e61692..6090c52 100644 --- a/demo/python/setup.py +++ b/demo/python/setup.py @@ -23,7 +23,7 @@ setuptools.setup( name="pvspeakerdemo", - version="1.0.0", + version="1.0.1", author="Picovoice", author_email="hello@picovoice.ai", description="Speaker library for Picovoice.", @@ -31,7 +31,7 @@ long_description_content_type="text/markdown", url="https://github.com/Picovoice/pvspeaker", packages=["pvspeakerdemo"], - install_requires=["pvspeaker==1.0.0"], + install_requires=["pvspeaker==1.0.1"], include_package_data=True, classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/lib/linux/x86_64/libpv_speaker.so b/lib/linux/x86_64/libpv_speaker.so index 500813e..35224af 100644 Binary files a/lib/linux/x86_64/libpv_speaker.so and b/lib/linux/x86_64/libpv_speaker.so differ diff --git a/lib/mac/arm64/libpv_speaker.dylib b/lib/mac/arm64/libpv_speaker.dylib index 1d31380..8ffa211 100644 Binary files a/lib/mac/arm64/libpv_speaker.dylib and b/lib/mac/arm64/libpv_speaker.dylib differ diff --git a/lib/mac/x86_64/libpv_speaker.dylib b/lib/mac/x86_64/libpv_speaker.dylib index 0ed8d21..35b9dc8 100644 Binary files a/lib/mac/x86_64/libpv_speaker.dylib and b/lib/mac/x86_64/libpv_speaker.dylib differ diff --git a/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so b/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so index 7fecb08..9f75113 100644 Binary files a/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so and b/lib/raspberry-pi/cortex-a53-aarch64/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a53/libpv_speaker.so b/lib/raspberry-pi/cortex-a53/libpv_speaker.so index 6975727..6e1d8f6 100644 Binary files a/lib/raspberry-pi/cortex-a53/libpv_speaker.so and b/lib/raspberry-pi/cortex-a53/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so b/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so index 57df94c..19cd3bc 100644 Binary files a/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so and b/lib/raspberry-pi/cortex-a72-aarch64/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a72/libpv_speaker.so b/lib/raspberry-pi/cortex-a72/libpv_speaker.so index 1163e4c..e408b29 100644 Binary files a/lib/raspberry-pi/cortex-a72/libpv_speaker.so and b/lib/raspberry-pi/cortex-a72/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so b/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so index fabbef3..c21f374 100644 Binary files a/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so and b/lib/raspberry-pi/cortex-a76-aarch64/libpv_speaker.so differ diff --git a/lib/raspberry-pi/cortex-a76/libpv_speaker.so b/lib/raspberry-pi/cortex-a76/libpv_speaker.so index 63c0bf7..2270c7d 100644 Binary files a/lib/raspberry-pi/cortex-a76/libpv_speaker.so and b/lib/raspberry-pi/cortex-a76/libpv_speaker.so differ diff --git a/lib/windows/amd64/libpv_speaker.dll b/lib/windows/amd64/libpv_speaker.dll index 56a66b5..08f99ea 100644 Binary files a/lib/windows/amd64/libpv_speaker.dll and b/lib/windows/amd64/libpv_speaker.dll differ diff --git a/project/README.md b/project/README.md index 864a371..bc3be9f 100644 --- a/project/README.md +++ b/project/README.md @@ -32,22 +32,21 @@ to get a list of possible values. ## Usage 1. Create a PvSpeaker object: + ```c #include "pv_speaker.h" const int32_t sample_rate = 22050; -const int32_t frame_length = 512; const int16_t bits_per_sample = 16; +const int32_t buffer_size_secs = 20; const int32_t device_index = -1; // -1 == default device -const int32_t buffered_frame_count = 10; pv_speaker_t *speaker = NULL; pv_speaker_status_t status = pv_speaker_init( sample_rate, - frame_length, bits_per_sample, + buffer_size_secs, device_index, - buffered_frame_count, &speaker); if (status != PV_SPEAKER_STATUS_SUCCESS) { // handle PvSpeaker init error @@ -63,29 +62,33 @@ if (status != PV_SPEAKER_STATUS_SUCCESS) { } ``` -3. Write frames of audio to the speaker: +3. Write PCM data to the speaker: + ```c -if (pcm) { - for (int i = 0; i < num_samples; i += frame_length) { - // must have length equal to or less than `frame_length` that was given to `pv_speaker_init()` - // be sure to handle the last frame properly (i.e. the last frame will likely not have length `frame_length`) - bool is_last_frame = i + frame_length >= num_samples; - int32_t last_frame_length = num_samples - i; - - status = pv_speaker_write( - speaker, - is_last_frame ? last_frame_length : frame_length, - &pcm[i * bits_per_sample / 8]); - if (status != PV_SPEAKER_STATUS_SUCCESS) { - // handle PvSpeaker write error - } - } - - free(pcm); +int32_3 num_samples; +int8_t *pcm = get_pcm_data(&num_samples); +int32_3 written_length = 0; + +pv_speaker_status_t status = pv_speaker_write(speaker, pcm, num_samples, &written_length); +if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker start error } ``` -4. Stop playing: +4. Wait for buffered audio to finish playing: + +```c +int32_3 num_samples; +int8_t *pcm = get_pcm_data(&num_samples); +int32_3 written_length = 0; + +pv_speaker_status_t status = pv_speaker_flush(speaker, pcm, num_samples, &written_length); +if (status != PV_SPEAKER_STATUS_SUCCESS) { + // handle PvSpeaker flush error +} +``` + +5. Stop the speaker: ```c pv_speaker_status_t status = pv_speaker_stop(speaker); @@ -94,7 +97,8 @@ if (status != PV_SPEAKER_STATUS_SUCCESS) { } ``` -5. Release resources used by PvSpeaker: +6. Release resources used by PvSpeaker: + ```c pv_speaker_delete(speaker); ``` diff --git a/project/include/pv_circular_buffer.h b/project/include/pv_circular_buffer.h index fc48ab1..d262333 100644 --- a/project/include/pv_circular_buffer.h +++ b/project/include/pv_circular_buffer.h @@ -80,6 +80,15 @@ pv_circular_buffer_status_t pv_circular_buffer_write( const void *buffer, int32_t buffer_length); +/** +* Gets the current amount of available space in the object's buffer. +* +* @param object Circular buffer object. +* @param available[out] The current amount of available space in the buffer. +* @return Status Code. Returns PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT on failure. +*/ +pv_circular_buffer_status_t pv_circular_buffer_get_available(pv_circular_buffer_t *object, int32_t *available); + /** * Gets the current size of the object's buffer. * @@ -104,4 +113,4 @@ void pv_circular_buffer_reset(pv_circular_buffer_t *object); */ const char *pv_circular_buffer_status_to_string(pv_circular_buffer_status_t status); -#endif //PV_CIRCULAR_BUFFER_H \ No newline at end of file +#endif //PV_CIRCULAR_BUFFER_H diff --git a/project/include/pv_speaker.h b/project/include/pv_speaker.h index 0b8af32..e09d18c 100644 --- a/project/include/pv_speaker.h +++ b/project/include/pv_speaker.h @@ -17,11 +17,11 @@ #if __PV_PLATFORM_WINDOWS__ -#define PV_API __attribute__ ((dllexport)) +#define PV_API __attribute__((dllexport)) #else -#define PV_API __attribute__((visibility ("default"))) +#define PV_API __attribute__((visibility("default"))) #endif @@ -38,7 +38,6 @@ typedef enum { PV_SPEAKER_STATUS_OUT_OF_MEMORY, PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_INVALID_STATE, - PV_SPEAKER_STATUS_BUFFER_OVERFLOW, PV_SPEAKER_STATUS_BACKEND_ERROR, PV_SPEAKER_STATUS_DEVICE_ALREADY_INITIALIZED, PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED, @@ -51,22 +50,19 @@ typedef enum { * using the `pv_speaker_delete() function. * * @param sample_rate The sample rate of the audio to be played. -* @param frame_length The maximum length of audio frame that will be passed to `pv_speaker_write`. * @param bits_per_sample The number of bits per sample. +* @param buffer_size_secs The size in seconds of the internal buffer used to buffer PCM data +* - i.e. internal circular buffer will be of size `sample_rate` * `buffer_size_secs`. * @param device_index The index of the audio device to use. A value of (-1) will resort to default device. -* @param buffered_frames_count The number of audio frames buffered internally for writing - i.e. internal circular buffer -* will be of size `frame_length` * `buffered_frames_count`. If this value is too low, buffer overflows could occur -* and audio frames could be dropped. A higher value will increase memory usage. * @param[out] object PvSpeaker object to be initialized. * @return Status Code. PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_BACKEND_ERROR, * PV_SPEAKER_STATUS_DEVICE_INITIALIZED or PV_SPEAKER_STATUS_OUT_OF_MEMORY on failure. */ PV_API pv_speaker_status_t pv_speaker_init( int32_t sample_rate, - int32_t frame_length, int16_t bits_per_sample, + int32_t buffer_size_secs, int32_t device_index, - int32_t buffered_frames_count, pv_speaker_t **object); /** @@ -77,7 +73,8 @@ PV_API pv_speaker_status_t pv_speaker_init( PV_API void pv_speaker_delete(pv_speaker_t *object); /** -* Starts the audio output device. After starting, pcm frames can be sent to the audio output device via `pv_speaker_write`. +* Starts the audio output device. After starting, PCM data can be sent to the audio output device via `pv_speaker_write` +* and/or `pv_speaker_flush`. * * @param object PvSpeaker object. * @returnStatus Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED @@ -86,42 +83,46 @@ PV_API void pv_speaker_delete(pv_speaker_t *object); PV_API pv_speaker_status_t pv_speaker_start(pv_speaker_t *object); /** -* Stops playing audio. +* 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. * * @param object PvSpeaker object. -* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED -* or PV_SPEAKER_STATUS_INVALID_STATE on failure. +* @param pcm Pointer to the PCM data that will be written. +* @param pcm_length Length of the PCM data that is passed in. +* @param written_length[out] Length of the PCM data that was successfully written. This value may be less than or equal +* to `pcm_length`, depending on the current state of the internal circular buffer. +* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_INVALID_STATE or PV_SPEAKER_IO_ERROR on +* failure. */ -PV_API pv_speaker_status_t pv_speaker_stop(pv_speaker_t *object); +PV_API pv_speaker_status_t pv_speaker_write(pv_speaker_t *object, int8_t *pcm, int32_t pcm_length, int32_t *written_length); /** -* Synchronous call to write frames. Copies amount of frames to `frame` array provided to input. -* Array size must not be greater than the `frame_length` value that was given to `pv_speaker_init()`. +* Synchronous call to write PCM data to the internal circular buffer for audio playback. +* This call blocks the thread until all PCM data have been successfully written and played. * * @param object PvSpeaker object. -* @param frame_length Size of the array that is passed in. -* @param frame Pointer to the array that will be written. -* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_INVALID_STATE or PV_SPEAKER_IO_ERROR on failure. -* Returns PV_SPEAKER_STATUS_BUFFER_OVERFLOW if audio frames aren't being written fast enough. This means audio frames will be dropped. +* @param pcm Pointer to the PCM data that will be written. +* @param pcm_length Length of the PCM data that is passed in. +* @param written_length[out] Length of the PCM data that was successfully written. This value should always match +* `pcm_length`, unless an error occurred. +* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT, PV_SPEAKER_INVALID_STATE or PV_SPEAKER_IO_ERROR on +* failure. */ -PV_API pv_speaker_status_t pv_speaker_write(pv_speaker_t *object, int32_t frame_length, void *frame); +PV_API pv_speaker_status_t pv_speaker_flush(pv_speaker_t *object, int8_t *pcm, int32_t pcm_length, int32_t *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. +* Stops the device. * * @param object PvSpeaker object. -* @param is_debug_logging_enabled Boolean indicating whether the debug logging is enabled or disabled. +* @return Status Code. Returns PV_SPEAKER_STATUS_INVALID_ARGUMENT or PV_SPEAKER_STATUS_INVALID_STATE on failure. */ -PV_API void pv_speaker_set_debug_logging( - pv_speaker_t *object, - bool is_debug_logging_enabled); +PV_API pv_speaker_status_t pv_speaker_stop(pv_speaker_t *object); /** -* Gets whether the given `pv_speaker_t` instance has started and available to receive pcm frames or not. +* Gets whether the given `pv_speaker_t` instance has started and is available to receive PCM data. * * @param object PvSpeaker object. -* @returns A boolean indicating whether PvSpeaker has started and available to receive pcm frames or not. +* @returns A boolean indicating whether PvSpeaker has started and is available to receive PCM data. */ PV_API bool pv_speaker_get_is_started(pv_speaker_t *object); diff --git a/project/src/pv_circular_buffer.c b/project/src/pv_circular_buffer.c index 0c93d60..abdf56f 100644 --- a/project/src/pv_circular_buffer.c +++ b/project/src/pv_circular_buffer.c @@ -153,6 +153,16 @@ pv_circular_buffer_status_t pv_circular_buffer_write( return PV_CIRCULAR_BUFFER_STATUS_SUCCESS; } +pv_circular_buffer_status_t pv_circular_buffer_get_available(pv_circular_buffer_t *object, int32_t *available) { + if (!object) { + return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; + } + + *available = object->capacity - object->count; + + return PV_CIRCULAR_BUFFER_STATUS_SUCCESS; +} + pv_circular_buffer_status_t pv_circular_buffer_get_count(pv_circular_buffer_t *object, int32_t *count) { if (!object) { return PV_CIRCULAR_BUFFER_STATUS_INVALID_ARGUMENT; @@ -182,4 +192,4 @@ const char *pv_circular_buffer_status_to_string(pv_circular_buffer_status_t stat } return STRINGS[status - PV_CIRCULAR_BUFFER_STATUS_SUCCESS]; -} \ No newline at end of file +} diff --git a/project/src/pv_speaker.c b/project/src/pv_speaker.c index 3cb086c..aeafb98 100644 --- a/project/src/pv_speaker.c +++ b/project/src/pv_speaker.c @@ -26,19 +26,18 @@ #define PV_SPEAKER_VERSION "1.0.0" -static const int32_t WRITE_RETRY_COUNT = 500; -static const int32_t WRITE_SLEEP_MILLI_SECONDS = 2; - -static bool is_stopped_and_empty = false; +static bool is_stopped = false; +static bool is_flushed_and_empty = false; static bool is_data_requested_while_empty = false; +static const int32_t FLUSH_SLEEP_MS = 2; + struct pv_speaker { ma_context context; ma_device device; pv_circular_buffer_t *buffer; - int32_t frame_length; + int32_t bits_per_sample; bool is_started; - bool is_debug_logging_enabled; ma_mutex mutex; }; @@ -49,9 +48,9 @@ static void pv_speaker_ma_callback(ma_device *device, void *output, const void * ma_mutex_lock(&object->mutex); - // this callback being invoked after calling `pv_speaker_stop` and the circular buffer is empty indicates that all + // this callback being invoked after calling `pv_speaker_flush` and the circular buffer is empty indicates that all // frames have been passed to the output buffer, and the device can stop without truncating the last frame of audio - if (is_stopped_and_empty) { + if (is_flushed_and_empty) { is_data_requested_while_empty = true; ma_mutex_unlock(&object->mutex); return; @@ -65,10 +64,9 @@ static void pv_speaker_ma_callback(ma_device *device, void *output, const void * PV_API pv_speaker_status_t pv_speaker_init( int32_t sample_rate, - int32_t frame_length, int16_t bits_per_sample, + int32_t buffer_size_secs, int32_t device_index, - int32_t buffered_frames_count, pv_speaker_t **object) { if (device_index < PV_SPEAKER_DEFAULT_DEVICE_INDEX) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; @@ -76,16 +74,13 @@ PV_API pv_speaker_status_t pv_speaker_init( if (sample_rate <= 0) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } - if (frame_length <= 0) { - return PV_SPEAKER_STATUS_INVALID_ARGUMENT; - } if (bits_per_sample != 8 && bits_per_sample != 16 && bits_per_sample != 24 && bits_per_sample != 32) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } - if (buffered_frames_count < 1) { + if (buffer_size_secs <= 0) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } if (!object) { @@ -188,10 +183,11 @@ PV_API pv_speaker_status_t pv_speaker_init( } } - const int32_t buffer_capacity = frame_length * buffered_frames_count; + const int32_t buffer_capacity = buffer_size_secs * sample_rate; + const int32_t element_size = bits_per_sample / 8; pv_circular_buffer_status_t status = pv_circular_buffer_init( buffer_capacity, - bits_per_sample / 8, + element_size, &(o->buffer)); if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { @@ -199,7 +195,7 @@ PV_API pv_speaker_status_t pv_speaker_init( return PV_SPEAKER_STATUS_OUT_OF_MEMORY; } - o->frame_length = frame_length; + o->bits_per_sample = bits_per_sample; *object = o; @@ -221,7 +217,7 @@ PV_API pv_speaker_status_t pv_speaker_start(pv_speaker_t *object) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } - is_stopped_and_empty = false; + is_flushed_and_empty = false; is_data_requested_while_empty = false; ma_result result = ma_device_start(&(object->device)); @@ -239,88 +235,140 @@ PV_API pv_speaker_status_t pv_speaker_start(pv_speaker_t *object) { return PV_SPEAKER_STATUS_SUCCESS; } -PV_API pv_speaker_status_t pv_speaker_stop(pv_speaker_t *object) { +PV_API pv_speaker_status_t pv_speaker_write(pv_speaker_t *object, int8_t *pcm, int32_t pcm_length, int32_t *written_length) { if (!object) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } + if (!pcm) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (pcm_length <= 0) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!written_length) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!(object->is_started)) { + return PV_SPEAKER_STATUS_INVALID_STATE; + } - // waits for all frames to be copied to output buffer before stopping - while (!is_stopped_and_empty || !is_data_requested_while_empty) { - ma_mutex_lock(&object->mutex); - int32_t count = 0; - pv_circular_buffer_status_t status = pv_circular_buffer_get_count(object->buffer, &count); - if (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && count == 0) { - is_stopped_and_empty = true; - } else if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { - ma_mutex_unlock(&object->mutex); - return PV_SPEAKER_STATUS_RUNTIME_ERROR; - } + is_stopped = false; + + ma_mutex_lock(&object->mutex); + + int32_t available = 0; + pv_circular_buffer_status_t status = pv_circular_buffer_get_available(object->buffer, &available); + if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { ma_mutex_unlock(&object->mutex); - ma_sleep(WRITE_SLEEP_MILLI_SECONDS); + return PV_SPEAKER_STATUS_RUNTIME_ERROR; } - ma_result result = ma_device_stop(&(object->device)); - if (result != MA_SUCCESS) { - if (result == MA_DEVICE_NOT_INITIALIZED) { - return PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED; - } else { - // device already stopped - return PV_SPEAKER_STATUS_INVALID_STATE; + int32_t to_write = pcm_length < available ? pcm_length : available; + if (to_write > 0) { + status = pv_circular_buffer_write(object->buffer, pcm, to_write); + if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { + ma_mutex_unlock(&object->mutex); + return PV_SPEAKER_STATUS_RUNTIME_ERROR; } } - ma_mutex_lock(&object->mutex); - pv_circular_buffer_reset(object->buffer); - object->is_started = false; + *written_length = to_write; + ma_mutex_unlock(&object->mutex); return PV_SPEAKER_STATUS_SUCCESS; } -PV_API pv_speaker_status_t pv_speaker_write(pv_speaker_t *object, int32_t frame_length, void *frame) { +PV_API pv_speaker_status_t pv_speaker_flush(pv_speaker_t *object, int8_t *pcm, int32_t pcm_length, int32_t *written_length) { if (!object) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } - if (!frame) { + if (!pcm) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } - if (frame_length > object->frame_length) { + if (pcm_length < 0) { + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; + } + if (!written_length) { return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } if (!(object->is_started)) { return PV_SPEAKER_STATUS_INVALID_STATE; } - for (int32_t i = 0; i < WRITE_RETRY_COUNT; i++) { + int32_t written = 0; + *written_length = 0; + + while (!is_stopped && written < pcm_length) { ma_mutex_lock(&object->mutex); - pv_circular_buffer_status_t status = pv_circular_buffer_write( - object->buffer, - frame, - frame_length); - if (status == PV_CIRCULAR_BUFFER_STATUS_WRITE_OVERFLOW && (i == (WRITE_RETRY_COUNT - 1))) { + int32_t available = 0; + pv_circular_buffer_status_t status = pv_circular_buffer_get_available(object->buffer, &available); + if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { ma_mutex_unlock(&object->mutex); - return PV_SPEAKER_STATUS_BUFFER_OVERFLOW; - } else if (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { - ma_mutex_unlock(&object->mutex); - return PV_SPEAKER_STATUS_SUCCESS; - } else { + return PV_SPEAKER_STATUS_RUNTIME_ERROR; + } + + int32_t remaining = pcm_length - written; + int32_t to_write = remaining < available ? remaining : available; + if (to_write > 0) { + status = pv_circular_buffer_write( + object->buffer, + &pcm[written * object->bits_per_sample / 8], + to_write); + if (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { + written += to_write; + *written_length = written; + } + } + + ma_mutex_unlock(&object->mutex); + ma_sleep(FLUSH_SLEEP_MS); + } + + // waits for all frames to be copied to output buffer + while (!is_stopped && !is_data_requested_while_empty) { + ma_mutex_lock(&object->mutex); + + int32_t count = 0; + pv_circular_buffer_status_t status = pv_circular_buffer_get_count(object->buffer, &count); + if (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && count == 0) { + is_flushed_and_empty = true; + } else if (status != PV_CIRCULAR_BUFFER_STATUS_SUCCESS) { ma_mutex_unlock(&object->mutex); - ma_sleep(WRITE_SLEEP_MILLI_SECONDS); + return PV_SPEAKER_STATUS_RUNTIME_ERROR; } + + ma_mutex_unlock(&object->mutex); + ma_sleep(FLUSH_SLEEP_MS); } - return PV_SPEAKER_STATUS_IO_ERROR; + return PV_SPEAKER_STATUS_SUCCESS; } -PV_API void pv_speaker_set_debug_logging( - pv_speaker_t *object, - bool is_debug_logging_enabled) { +PV_API pv_speaker_status_t pv_speaker_stop(pv_speaker_t *object) { if (!object) { - return; + return PV_SPEAKER_STATUS_INVALID_ARGUMENT; } - object->is_debug_logging_enabled = is_debug_logging_enabled; + is_stopped = true; + + ma_result result = ma_device_stop(&(object->device)); + if (result != MA_SUCCESS) { + if (result == MA_DEVICE_NOT_INITIALIZED) { + return PV_SPEAKER_STATUS_DEVICE_NOT_INITIALIZED; + } else { + // device already stopped + return PV_SPEAKER_STATUS_INVALID_STATE; + } + } + + ma_mutex_lock(&object->mutex); + pv_circular_buffer_reset(object->buffer); + object->is_started = false; + ma_mutex_unlock(&object->mutex); + + return PV_SPEAKER_STATUS_SUCCESS; } PV_API bool pv_speaker_get_is_started(pv_speaker_t *object) { @@ -419,7 +467,6 @@ PV_API const char *pv_speaker_status_to_string(pv_speaker_status_t status) { "OUT_OF_MEMORY", "INVALID_ARGUMENT", "INVALID_STATE", - "BUFFER_OVERFLOW", "BACKEND_ERROR", "DEVICE_INITIALIZED", "DEVICE_NOT_INITIALIZED", @@ -436,4 +483,4 @@ PV_API const char *pv_speaker_status_to_string(pv_speaker_status_t status) { PV_API const char *pv_speaker_version(void) { return PV_SPEAKER_VERSION; -} \ No newline at end of file +} diff --git a/project/test/test_pv_circular_buffer.c b/project/test/test_pv_circular_buffer.c index 5599248..92e839d 100644 --- a/project/test/test_pv_circular_buffer.c +++ b/project/test/test_pv_circular_buffer.c @@ -13,8 +13,9 @@ #include "test_helper.h" static void test_pv_circular_buffer_once(void) { + int32_t element_count = 128; pv_circular_buffer_t *cb; - pv_circular_buffer_status_t status = pv_circular_buffer_init(128, sizeof(int16_t), &cb); + pv_circular_buffer_status_t status = pv_circular_buffer_init(element_count, sizeof(int16_t), &cb); check_condition( status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, __FUNCTION__, @@ -31,6 +32,14 @@ static void test_pv_circular_buffer_once(void) { status = pv_circular_buffer_write(cb, in_buffer, in_size); check_condition(status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS, __FUNCTION__, __LINE__, "Failed to write buffer."); + int32_t available = 0; + status = pv_circular_buffer_get_available(cb, &available); + check_condition( + (status == PV_CIRCULAR_BUFFER_STATUS_SUCCESS && available == (element_count - in_size)), + __FUNCTION__, + __LINE__, + "Failed to get correct amount of available space before write."); + int32_t count = 0; status = pv_circular_buffer_get_count(cb, &count); check_condition( diff --git a/project/test/test_pv_speaker.c b/project/test/test_pv_speaker.c index 915d06d..75b5f83 100644 --- a/project/test/test_pv_speaker.c +++ b/project/test/test_pv_speaker.c @@ -16,20 +16,18 @@ static void init_test_helper( int32_t sample_rate, - int32_t frame_length, int16_t bits_per_sample, + int32_t buffer_size_secs, int32_t device_index, - int32_t buffered_frames_count, pv_speaker_status_t expected_status) { pv_speaker_t *speaker = NULL; pv_speaker_status_t status; status = pv_speaker_init( sample_rate, - frame_length, bits_per_sample, + buffer_size_secs, device_index, - buffered_frames_count, &speaker); check_condition( @@ -46,34 +44,28 @@ static void init_test_helper( static void test_pv_speaker_init(void) { printf("Initialize with valid parameters\n"); - init_test_helper(16000, 512, 16, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + init_test_helper(16000, 16, 20, 0, PV_SPEAKER_STATUS_SUCCESS); printf("Initialize with valid parameters (different sample rate)\n"); - init_test_helper(22050, 512, 16, 0, 10, PV_SPEAKER_STATUS_SUCCESS); - - printf("Initialize with valid parameters (different frame length)\n"); - init_test_helper(16000, 256, 16, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + init_test_helper(22050, 16, 20, 0, PV_SPEAKER_STATUS_SUCCESS); printf("Initialize with valid parameters (different bits per sample)\n"); - init_test_helper(16000, 512, 8, 0, 10, PV_SPEAKER_STATUS_SUCCESS); + init_test_helper(16000, 8, 20, 0, PV_SPEAKER_STATUS_SUCCESS); printf("Initialize with invalid device index (negative)\n"); - init_test_helper(16000, 512, 16, -2, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + init_test_helper(16000, 16, 20, -2, PV_SPEAKER_STATUS_INVALID_ARGUMENT); printf("Initialize with invalid device index (too high)\n"); - init_test_helper(16000, 512, 16, 500, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); - - printf("Initialize with invalid frame length\n"); - init_test_helper(16000, -1, 16, 0, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + init_test_helper(16000, 16, 20, 500, PV_SPEAKER_STATUS_INVALID_ARGUMENT); printf("Initialize with invalid bits per sample\n"); - init_test_helper(16000, 512, -1, -2, 10, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + init_test_helper(16000, -1, 20, 0, PV_SPEAKER_STATUS_INVALID_ARGUMENT); - printf("Initialize with invalid buffered frames count\n"); - init_test_helper(16000, 512, 16, 0, 0, PV_SPEAKER_STATUS_INVALID_ARGUMENT); + printf("Initialize with invalid buffer size secs\n"); + init_test_helper(16000, 16, 0, 20, PV_SPEAKER_STATUS_INVALID_ARGUMENT); printf("Initialize with null speaker pointer\n"); - pv_speaker_status_t status = pv_speaker_init(16000, 512, 16, 0, 10, NULL); + pv_speaker_status_t status = pv_speaker_init(16000, 16, 20, 0, NULL); check_condition( status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, __FUNCTION__, @@ -86,11 +78,12 @@ static void test_pv_speaker_init(void) { static void test_pv_speaker_start_stop(void) { pv_speaker_t *speaker = NULL; pv_speaker_status_t status; - int32_t frame_length = 512; - int16_t frame[frame_length]; - char *frame_ptr = (char *) frame; + int32_t pcm_length = 512; + int16_t pcm[pcm_length]; + int8_t *pcm_ptr = (int8_t *) pcm; + int32_t written_length = 0; - status = pv_speaker_init(16000, frame_length, 16, 0, 10, &speaker); + status = pv_speaker_init(16000, 16, 20, 0, &speaker); check_condition( status == PV_SPEAKER_STATUS_SUCCESS, __FUNCTION__, @@ -125,8 +118,8 @@ static void test_pv_speaker_start_stop(void) { pv_speaker_status_to_string(status), pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); - printf("Call read before start object\n"); - status = pv_speaker_write(speaker, frame_length, frame_ptr); + printf("Call write before start object\n"); + status = pv_speaker_write(speaker, pcm_ptr, pcm_length, &written_length); check_condition( status == PV_SPEAKER_STATUS_INVALID_STATE, __FUNCTION__, @@ -146,7 +139,17 @@ static void test_pv_speaker_start_stop(void) { pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); printf("Call write on null speaker\n"); - status = pv_speaker_write(NULL, frame_length, frame_ptr); + status = pv_speaker_write(NULL, pcm_ptr, pcm_length, &written_length); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker write returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + printf("Call write with null pcm\n"); + status = pv_speaker_write(speaker, NULL, pcm_length, &written_length); check_condition( status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, __FUNCTION__, @@ -155,8 +158,8 @@ static void test_pv_speaker_start_stop(void) { pv_speaker_status_to_string(status), pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); - printf("Call write with null frame\n"); - status = pv_speaker_write(speaker, frame_length, NULL); + printf("Call write with null written length\n"); + status = pv_speaker_write(speaker, pcm_ptr, pcm_length, NULL); check_condition( status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, __FUNCTION__, @@ -166,7 +169,7 @@ static void test_pv_speaker_start_stop(void) { pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); printf("Call write with valid args\n"); - status = pv_speaker_write(speaker, frame_length, frame_ptr); + status = pv_speaker_write(speaker, pcm_ptr, pcm_length, &written_length); check_condition( status == PV_SPEAKER_STATUS_SUCCESS, __FUNCTION__, @@ -183,16 +186,36 @@ static void test_pv_speaker_start_stop(void) { __LINE__, "get_is_started returned false - expected true."); - printf("Call stop on null speaker object\n"); - status = pv_speaker_stop(NULL); + printf("Call flush on null speaker\n"); + status = pv_speaker_flush(NULL, pcm_ptr, pcm_length, &written_length); check_condition( status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, __FUNCTION__, __LINE__, - "Speaker stop returned %s - expected %s.", + "Speaker flush returned %s - expected %s.", pv_speaker_status_to_string(status), pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + printf("Call flush with null pcm\n"); + status = pv_speaker_flush(speaker, NULL, pcm_length, &written_length); + check_condition( + status == PV_SPEAKER_STATUS_INVALID_ARGUMENT, + __FUNCTION__, + __LINE__, + "Speaker flush returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_INVALID_ARGUMENT)); + + printf("Call flush with valid args\n"); + status = pv_speaker_flush(speaker, pcm_ptr, pcm_length, &written_length); + check_condition( + status == PV_SPEAKER_STATUS_SUCCESS, + __FUNCTION__, + __LINE__, + "Speaker flush returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + printf("Call stop on valid speaker object\n"); status = pv_speaker_stop(speaker); check_condition( @@ -214,9 +237,18 @@ static void test_pv_speaker_start_stop(void) { pv_speaker_delete(speaker); } -static void test_pv_speaker_set_debug_logging(void) { +static void test_pv_speaker_write_flow(void) { pv_speaker_t *speaker = NULL; - pv_speaker_status_t status = pv_speaker_init(16000, 512, 16, 0, 10, &speaker); + pv_speaker_status_t status; + int32_t sample_rate = 16000; + int32_t buffer_size_secs = 1; + int32_t circular_buffer_size = sample_rate * buffer_size_secs; + int32_t pcm_length = circular_buffer_size + 1; + int16_t pcm[pcm_length]; + int8_t *pcm_ptr = (int8_t *) pcm; + int32_t written_length = 0; + + status = pv_speaker_init(sample_rate, 16, buffer_size_secs, 0, &speaker); check_condition( status == PV_SPEAKER_STATUS_SUCCESS, __FUNCTION__, @@ -225,15 +257,33 @@ static void test_pv_speaker_set_debug_logging(void) { pv_speaker_status_to_string(status), pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); - pv_speaker_set_debug_logging(NULL, true); - pv_speaker_set_debug_logging(speaker, true); + printf("Call write with pcm length greater than circular buffer's capacity/available space\n"); + status = pv_speaker_write(speaker, pcm_ptr, pcm_length, &written_length); + check_condition( + (status == PV_SPEAKER_STATUS_SUCCESS && written_length == circular_buffer_size), + __FUNCTION__, + __LINE__, + "Speaker write returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + + printf("Call flush with pcm length greater than circular buffer's capacity/available space\n"); + status = pv_speaker_flush(speaker, pcm_ptr, pcm_length, &written_length); + check_condition( + (status == PV_SPEAKER_STATUS_SUCCESS && written_length == pcm_length), + __FUNCTION__, + __LINE__, + "Speaker flush returned %s - expected %s.", + pv_speaker_status_to_string(status), + pv_speaker_status_to_string(PV_SPEAKER_STATUS_SUCCESS)); + pv_speaker_stop(speaker); pv_speaker_delete(speaker); } static void test_pv_speaker_get_selected_device(void) { pv_speaker_t *speaker = NULL; - pv_speaker_status_t status = pv_speaker_init(16000, 512, 16, 0, 10, &speaker); + pv_speaker_status_t status = pv_speaker_init(16000, 16, 20, 0, &speaker); check_condition( status == PV_SPEAKER_STATUS_SUCCESS, __FUNCTION__, @@ -319,7 +369,6 @@ int main() { test_pv_speaker_version(); test_pv_speaker_init(); test_pv_speaker_start_stop(); - test_pv_speaker_set_debug_logging(); test_pv_speaker_get_selected_device(); return 0;