Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make pyaudio optional #82

Merged
merged 10 commits into from
Sep 10, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.5.2 (??? 2023)
* `pyaudio` is now optional: If you plan to use `PyAudioBackend`, install `pya` with `pip install pya[pyaudio]`

## 0.5.1 (Dec 2022)
* Now support Python3.10
* Bugfix #67: When the channels argument of Aserver and Arecorder has not been set it was determined by the default device instead of the actual device.
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ At this time pya is more suitable for offline rendering than realtime.

## Installation

`pya` requires `portaudio` and its Python wrapper `PyAudio` to play and record audio.
Install using
```
pip install pya
```

However to play and record audio you need a backend.

- `pip install pya[remote]` for a web based Jupyter backend
- `pip install pya[pyaudio]` for `portaudio` and its Python wrapper `PyAudio`

### Using Conda

Expand Down Expand Up @@ -101,7 +109,7 @@ For Apple ARM Chip, if you failed to install the PyAudio dependency, you can fol
Try `sudo apt-get install portaudio19-dev` or equivalent to your distro, then

```
pip isntall pya
pip install pya
```

### Using PIP (Windows)
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ install:
- conda config --append channels conda-forge
- "conda create -q -n test-environment python=%PYTHON_VERSION% ffmpeg coverage --file=requirements_remote.txt --file=requirements_test.txt"
- activate test-environment
- "pip install -r requirements.txt"
- "pip install -r requirements.txt -r requirements_pyaudio.txt"

test_script:
- pytest
3 changes: 1 addition & 2 deletions pya/arecorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import numbers
from warnings import warn
import numpy as np
import pyaudio
from . import Asig
from . import Aserver
from pyamapping import db_to_amp
Expand Down Expand Up @@ -108,7 +107,7 @@ def _recorder_callback(self, in_data, frame_count, time_info, flag):
self.record_buffer.append(data_float)
# E = 10 * np.log10(np.mean(data_float ** 2)) # energy in dB
# os.write(1, f"\r{E} | {self.block_cnt}".encode())
return None, pyaudio.paContinue
return self.backend.process_buffer(None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, maybe add a comment that process_buffer is called both for input and output streams


def record(self):
"""Activate recording"""
Expand Down
18 changes: 15 additions & 3 deletions pya/aserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,22 @@ def __init__(self, sr=44100, bs=None, device=None,
"""
# TODO check if channels is overwritten by the device.
self.sr = sr
self.stream = None

if backend is None:
from .backend.PyAudio import PyAudioBackend
self.backend = PyAudioBackend(**kwargs)
try:
from .backend.PyAudio import PyAudioBackend
self.backend = PyAudioBackend(**kwargs)
except ImportError:
from .helper.backend import determine_backend
jupyter_backend = determine_backend()
if jupyter_backend is not None:
self.backend = jupyter_backend
else:
raise RuntimeError(
"Could not find a backend."
"To use the Aserver install the 'pyaudio' or 'remote' extra."
)
else:
self.backend = backend
self.bs = bs or self.backend.bs
Expand All @@ -96,7 +109,6 @@ def __init__(self, sr=44100, bs=None, device=None,
self.srv_curpos = [] # start of next frame to deliver
self.srv_asigs = []
self.srv_outs = [] # output channel offset for that asig
self.stream = None
self.boot_time = 0 # time.time() when stream starts
self.block_cnt = 0 # nr. of callback invocations
self.block_duration = self.bs / self.sr # nominal time increment per callback
Expand Down
9 changes: 8 additions & 1 deletion pya/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .Dummy import DummyBackend
from .PyAudio import PyAudioBackend
import logging

_LOGGER = logging.getLogger(__name__)
Expand All @@ -11,4 +10,12 @@
except ImportError: # pragma: no cover
_LOGGER.warning("Jupyter backend not found.")
pass

try:
from .PyAudio import PyAudioBackend
except ImportError: # pragma: no cover
_LOGGER.warning("PyAudio backend not found.")
pass


from ..helper.backend import determine_backend
17 changes: 14 additions & 3 deletions pya/helper/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Collection of small helper functions
import numpy as np
import pyaudio
from scipy.fftpack import fft
from .codec import audio_read
import logging
Expand Down Expand Up @@ -111,9 +110,21 @@ def buf_to_float(x, n_bytes=2, dtype=np.float32):
return scale * np.frombuffer(x, fmt).astype(dtype)


def _try_importing_pyaudio(fun_name):
try:
import pyaudio
except ImportError as e:
msg = (
f"Function '{fun_name}' requires pyaudio"
)
raise RuntimeError(msg) from e
else:
return pyaudio.PyAudio()


def device_info():
"""Return a formatted string about available audio devices and their info"""
pa = pyaudio.PyAudio()
pa = _try_importing_pyaudio("device_info")
dreinsch marked this conversation as resolved.
Show resolved Hide resolved
line1 = (f"idx {'Device Name':25}{'INP':4}{'OUT':4} SR INP-(Lo|Hi) OUT-(Lo/Hi) (Latency in ms)")
devs = [pa.get_device_info_by_index(i) for i in range(pa.get_device_count())]
lines = [line1]
Expand All @@ -128,7 +139,7 @@ def device_info():


def find_device(min_input=0, min_output=0):
pa = pyaudio.PyAudio()
pa = _try_importing_pyaudio("find_device")
res = []
for idx in range(pa.get_device_count()):
dev = pa.get_device_info_by_index(idx)
Expand Down
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
pyamapping
scipy>=1.7.3
matplotlib>=3.5.3
pyaudio>=0.2.12; python_version >= '3.10'
pyaudio>=0.2.11; python_version < '3.10'
2 changes: 2 additions & 0 deletions requirements_pyaudio.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pyaudio>=0.2.12; python_version >= '3.10'
pyaudio>=0.2.11; python_version < '3.10'
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
with open(join(project_root, 'requirements_remote.txt')) as read_file:
REQUIRED_EXTRAS['remote'] = read_file.read().splitlines()

with open(join(project_root, 'requirements_pyaudio.txt')) as read_file:
REQUIRED_EXTRAS['pyaudio'] = read_file.read().splitlines()

with open(join(project_root, 'requirements_test.txt')) as read_file:
REQUIRED_TEST = read_file.read().splitlines()

Expand Down
17 changes: 17 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import time
from typing import Callable

Expand All @@ -9,3 +10,19 @@ def wait(condition: Callable[[], bool], seconds: float = 10, interval: float = 0
return True
time.sleep(interval)
return False


def check_for_input() -> bool:
with contextlib.suppress(ImportError, OSError):
import pyaudio
pyaudio.PyAudio().get_default_input_device_info()
return True
return False


def check_for_output() -> bool:
with contextlib.suppress(ImportError, OSError):
import pyaudio
pyaudio.PyAudio().get_default_input_device_info()
wiccy46 marked this conversation as resolved.
Show resolved Hide resolved
return True
return False
12 changes: 5 additions & 7 deletions tests/test_arecorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
from pya import Arecorder, Aserver, find_device
from unittest import TestCase, mock
import pytest
import pyaudio

# check if we have an output device
has_input = False
try:
pyaudio.PyAudio().get_default_input_device_info()
has_input = True
except OSError:
pass
import pyaudio
has_pyaudio = True
except ImportError:
has_pyaudio = False


FAKE_INPUT = {'index': 0,
Expand Down Expand Up @@ -164,6 +161,7 @@ class TestArecorder(TestArecorderBase):

class TestMockArecorder(TestCase):

@pytest.mark.skipif(not has_pyaudio, reason="requires pyaudio to be installed")
def test_mock_arecorder(self):
mock_recorder = MockRecorder()
with mock.patch('pyaudio.PyAudio', return_value=mock_recorder):
Expand Down
9 changes: 0 additions & 9 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@
from pya.helper import signal_to_frame, magspec, powspec

import numpy as np
import pyaudio


has_input = False
try:
pyaudio.PyAudio().get_default_input_device_info()
has_input = True
except OSError:
pass


class TestHelpers(TestCase):
Expand Down
9 changes: 2 additions & 7 deletions tests/test_play.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from .helpers import check_for_output
import time
from unittest import TestCase, skipUnless, mock
from pya import *
import numpy as np
import pyaudio
import warnings
import pytest


# check if we have an output device
has_output = False
try:
pyaudio.PyAudio().get_default_output_device_info()
has_output = True
except OSError:
pass
has_output = check_for_output()


class MockAudio(mock.MagicMock):
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ envlist = py3, check-manifest
passenv = PULSE_SERVER
deps=
-rrequirements.txt
-rrequirements_pyaudio.txt
-rrequirements_remote.txt
-rrequirements_test.txt
coverage
Expand Down