Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/python-demos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ jobs:
- name: Pre-build dependencies
run: python -m pip install --upgrade pip

# TODO: remove after release
- name: Build dependencies
run: |
python -m pip install -U pip setuptools
pip install wheel && cd ../../binding/python && python3 setup.py sdist bdist_wheel && pip install dist/pvspeaker-1.0.1-py3-none-any.whl

- name: Install dependencies
run: pip install -r requirements.txt

Expand All @@ -55,6 +61,13 @@ jobs:
steps:
- uses: actions/checkout@v3

# TODO: remove after release
- name: Build dependencies
run: |
python3 -m venv .venv
source .venv/bin/activate
pip install wheel && cd ../../binding/python && python3 setup.py sdist bdist_wheel && pip install dist/pvspeaker-1.0.1-py3-none-any.whl

- name: Install dependencies
run: |
python3 -m venv .venv
Expand Down
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()
```

Once you are done playing audio, run `stop()` on the instance:

```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. 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()
```

Once you are done playing audio, run `stop()` on the instance:

```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. 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
Loading