Skip to content

Commit

Permalink
Change api (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
albho authored Jul 24, 2024
1 parent de9c3d7 commit ca76602
Show file tree
Hide file tree
Showing 29 changed files with 520 additions and 284 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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' }}
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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).

Expand Down Expand Up @@ -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()
Expand Down
14 changes: 12 additions & 2 deletions binding/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand Down
109 changes: 54 additions & 55 deletions binding/python/_pvspeaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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.")

Expand All @@ -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
Expand All @@ -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('<i', sample)[0:3] for sample in frame)
elif self._bits_per_sample == 32:
byte_data = pack('i' * len(frame), *frame)
def _pcm_to_bytes(self, pcm) -> 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('<i', sample)[0:3] for sample in pcm)
elif self._bits_per_sample == 32:
byte_data = pack('i' * len(pcm), *pcm)
return byte_data

def write(self, pcm) -> 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:
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion binding/python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@

setuptools.setup(
name="pvspeaker",
version="1.0.0",
version="1.0.1",
author="Picovoice",
author_email="[email protected]",
description="Speaker library for Picovoice.",
Expand Down
Loading

0 comments on commit ca76602

Please sign in to comment.