Skip to content

Commit

Permalink
Merge pull request #70 from interactive-sonification/develop
Browse files Browse the repository at this point in the history
0.5.1
  • Loading branch information
wiccy46 authored Dec 22, 2022
2 parents 07ad7e0 + 1f202fb commit e6357ea
Show file tree
Hide file tree
Showing 20 changed files with 227 additions and 90 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/pya-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest"]
python-version: [3.7, 3.8, 3.9]
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install portaudio Ubuntu
if: matrix.os == 'ubuntu-latest'
shell: bash -l {0}
Expand All @@ -38,7 +38,9 @@ jobs:
run: |
conda init bash
conda activate test-env
conda install ffmpeg coverage python-coveralls --file=requirements.txt --file=requirements_remote.txt --file=requirements_test.txt
conda install ffmpeg coverage python-coveralls --file=requirements_remote.txt --file=requirements_test.txt
# pyaudio is not yet available on conda
pip install -r requirements.txt
- name: Run tests
shell: bash -l {0}
run: |
Expand Down
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.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.

## 0.5.0 (Oct 2022)
* Make Aserver a context that can wrap play inside. This context will handle boot() and quit() automatically.
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ recursive-include tests *.py
recursive-include pya *.py
recursive-include dev *.md
recursive-include dev *.py
recursive-include ci *.yml
93 changes: 79 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,80 @@ At this time pya is more suitable for offline rendering than realtime.

## Authors and Contributors

* Thomas Hermann, Ambient Intelligence Group, Faculty of Technology, Bielefeld University (author and maintainer)
* Jiajun Yang, Ambient Intelligence Group, Faculty of Technology, Bielefeld University (co-author)
* Alexander Neumann, Neurocognitions and Action - Biomechanics, Bielefeld University
* [Thomas](https://github.com/thomas-hermann) (author, maintainer)
* [Jiajun](https://github.com/wiccy46) (co-author, maintainer)
* [Alexander](https://github.com/aleneum) (maintainer)
* Contributors will be acknowledged here, contributions are welcome.

## Installation

<!-- **Disclaimer**: We are currently making sure that pya can be uploaded to PyPI, until then clone the master branch and from inside the pya directory install via `pip install -e .` -->
`pya` requires `portaudio` and its Python wrapper `PyAudio` to play and record audio.

**Note**: pya can be installed using **pip**. But pya uses PyAudio for audio playback and record, and PyAudio 0.2.11 has yet to fully support Python 3.7. So using pip install with Python 3.7 may encounter issues such as portaudio. Solutions are:
### Using Conda

1. Anaconda can install non-python packages, so that the easiest way (if applicable) would be to
Pyaudio can be installed via [conda](https://docs.conda.io):

conda install pyaudio
```
conda install pyaudio
```

Disclaimer: Python 3.10+ requires PyAudio 0.2.12 which is not available on Conda as of December 2022. [Conda-forge](https://conda-forge.org/) provides a version only for Linux at the moment. Users of Python 3.10 should for now use other installation options.

### Using Homebrew and PIP (MacOS only)


```
brew install portaudio
```

2. For Mac users, you can `brew install portaudio` beforehand.
Then

3. For Linux users, try `sudo apt-get install portaudio19-dev` or equivalent to your distro.
```
pip install pya
```

4. For Windows users, you can install PyAudio wheel at:
https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio
For Apple ARM Chip, if you failed to install the PyAudio dependency, you can follow this guide: [Installation on ARM chip](https://stackoverflow.com/a/73166852/4930109)
- Option 1: Create .pydistutils.cfg in your home directory, `~/.pydistutils.cfg`, add:

Then pya can be installed using pip:
```
echo "[build_ext]
include_dirs=$(brew --prefix portaudio)/include/
library_dirs=$(brew --prefix portaudio)/lib/" > ~/.pydistutils.cfg
```
Use pip:

```
pip install pya
```

You can remove the `.pydistutils.cfg` file after installation.

- Option 2: Use `CFLAGS`:

```
CFLAGS="-I/opt/homebrew/include -L/opt/homebrew/lib" pip install pya
```



### Using PIP (Linux)

Try `sudo apt-get install portaudio19-dev` or equivalent to your distro, then

```
pip isntall pya
```

### Using PIP (Windows)

[PyPI](https://pypi.org/) provides [PyAudio wheels](https://pypi.org/project/PyAudio/#files) for Windows including portaudio:

```
pip install pyaudio
```

should be sufficient.

See pyaudio installation http://people.csail.mit.edu/hubert/pyaudio/#downloads

## A simple example

Expand Down Expand Up @@ -140,8 +186,9 @@ The benefit of this is that it will handle server bootup and shutdown for you. B

```Python
from pya import find_device
from pya import Aserver
devices = find_device() # This will return a dictionary of all devices, with their index, name, channels.
s = pya.Aserver(sr=48000, bs=256, device=devices['name_of_your_device']['index'])
s = Aserver(sr=48000, bs=256, device=devices['name_of_your_device']['index'])
```


Expand All @@ -168,6 +215,24 @@ to plot the spectrogram via the Astft class
* `a1[:100, :3]` would select the first 100 samples and the first 3 channels,
* `a1[{1.2:2}, ['left']]` would select the channel named 'left' using a time slice from 1

### Recording from Device

`Arecorder` allows recording from input device

```Python
import time

from pya import find_device
from pya import Arecorder
devices = find_device() # Find the index of the input device
arecorder = Arecorder(device=some_index, sr=48000, bs=512) # Or not set device to let pya find the default device
arecorder.boot()
arecorder.record()
time.sleep(2) # Recording is non-blocking
arecorder.stop()
last_recording = arecorder.recordings[-1] # Each time a recorder stop, a new recording is appended to recordings
```

### Method chaining
Asig methods usually return an Asig, so methods can be chained, e.g

Expand Down
3 changes: 2 additions & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ install:
- conda update -q conda
- conda info -a
- conda config --append channels conda-forge
- "conda create -q -n test-environment python=%PYTHON_VERSION% ffmpeg coverage --file=requirements.txt --file=requirements_remote.txt --file=requirements_test.txt"
- "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"

test_script:
- pytest
2 changes: 1 addition & 1 deletion binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ dependencies:
- ipywidgets
- requests
- notebook
- sanic
- websockets
13 changes: 1 addition & 12 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
#
import os
import sys
from m2r import MdInclude
from recommonmark.transform import AutoStructify
sys.path.insert(0, os.path.abspath('..'))


Expand All @@ -33,7 +31,7 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.napoleon',
'recommonmark',
'sphinx_mdinclude',
'autoapi.extension']

source_suffix = ['.rst', '.md']
Expand Down Expand Up @@ -71,12 +69,3 @@ def setup(app):
'auto_toc_tree_section': 'Contents',
'enable_eval_rst': True,
}
app.add_config_value('recommonmark_config', config, True)
app.add_transform(AutoStructify)

# from m2r to make `mdinclude` work
app.add_config_value('no_underscore_emphasis', False, 'env')
app.add_config_value('m2r_parse_relative_links', False, 'env')
app.add_config_value('m2r_anonymous_references', False, 'env')
app.add_config_value('m2r_disable_inline_math', False, 'env')
app.add_directive('mdinclude', MdInclude)
19 changes: 10 additions & 9 deletions examples/pya-examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
"# This part only makes sure that the repository version of pya is used for this notebook ...\n",
"import os, sys, inspect, io\n",
"\n",
"cmd_folder = os.path.realpath(\n",
" os.path.dirname(\n",
" os.path.abspath(os.path.split(inspect.getfile( inspect.currentframe() ))[0])))\n",
"cmd_folder = os.path.abspath(sys.path[0] + \"/../\")\n",
"\n",
"if cmd_folder not in sys.path:\n",
" sys.path.insert(0, cmd_folder)\n",
Expand Down Expand Up @@ -48,10 +46,13 @@
"outputs": [],
"source": [
"# Boot up the audio server\n",
"# Aserver(sr=44100, bs=256, device=None, channels=2, backend=None, format=pyaudio.paFloat32)\n",
"# By default pya will try to use PyAudio as the audio backend\n",
"# If buffer size is not passed it will be determined by the backend\n",
"s = Aserver(backend=auto_backend) \n",
"# e.g. with Aserver(sr=44100, bs=256, device=None, channels=2, backend=None, format=pyaudio.paFloat32)\n",
"# By default pya will try to use PyAudio as the audio backend.\n",
"# If buffer size is not passed it will be determined by the backend.\n",
"# Note that a larger buffer size (bs) might cause lag when playing audio.\n",
"# However, a small buffer might cause stutter, especially when the WebAudio/Jupyter backend is used.\n",
"# The default values should suffice for most use cases.\n",
"s = Aserver(backend=auto_backend)\n",
"Aserver.default = s # set default Aserver to use play() w/o explicit arg\n",
"s.boot()"
]
Expand Down Expand Up @@ -2108,7 +2109,7 @@
"metadata": {
"hide_input": false,
"kernelspec": {
"display_name": "Python 3",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
Expand All @@ -2122,7 +2123,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.5"
"version": "3.9.14"
},
"toc": {
"base_numbering": 1,
Expand Down
4 changes: 2 additions & 2 deletions pya/arecorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def __init__(self, sr=44100, bs=256, device=None, channels=None, backend=None, *
self._record_all = True
self.gains = np.ones(self.channels)
self.tracks = slice(None)
self.device = device or self.backend.get_default_input_device_info()['index']
self.channels = channels or self.backend.get_default_input_device_info()['maxInputChannels']
self._device = device or self.backend.get_default_input_device_info()['index']
self.channels = channels or self.backend.get_device_info_by_index(self._device)['maxInputChannels']

def set_tracks(self, tracks, gains):
"""Define the number of track to be recorded and their gains.
Expand Down
27 changes: 22 additions & 5 deletions pya/aserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def shutdown_default_server():
else:
warn("Aserver:shutdown_default_server: no default_server to shutdown")

def __init__(self, sr=44100, bs=512, device=None,
channels=2, backend=None, **kwargs):
def __init__(self, sr=44100, bs=None, device=None,
channels=None, backend=None, **kwargs):
"""Aserver manages an pyaudio stream, using its aserver callback
to feed dispatched signals to output at the right time.
Expand All @@ -58,7 +58,7 @@ def __init__(self, sr=44100, bs=512, device=None,
sr : int
Sampling rate (Default value = 44100)
bs : int
block size or buffer size (Default value = 256)
Override block size or buffer size set by chosen backend
device : int
The device index based on pya.device_info(), default is None which will set
the default device from PyAudio
Expand All @@ -73,13 +73,12 @@ def __init__(self, sr=44100, bs=512, device=None,
"""
# TODO check if channels is overwritten by the device.
self.sr = sr
self.bs = bs
if backend is None:
from .backend.PyAudio import PyAudioBackend
self.backend = PyAudioBackend(**kwargs)
else:
self.backend = backend
self.channels = channels
self.bs = bs or self.backend.bs
# Get audio devices to input_device and output_device
self.input_devices = []
self.output_devices = []
Expand All @@ -90,6 +89,7 @@ def __init__(self, sr=44100, bs=512, device=None,
self.output_devices.append(self.backend.get_device_info_by_index(i))

self._device = device or self.backend.get_default_output_device_info()['index']
self.channels = channels or self.backend.get_device_info_by_index(self.device)['maxOutputChannels']

self.gain = 1.0
self.srv_onsets = []
Expand All @@ -104,6 +104,19 @@ def __init__(self, sr=44100, bs=512, device=None,
self._stop = True
self.empty_buffer = np.zeros((self.bs, self.channels),
dtype=self.backend.dtype)
self._is_active = False

@property
def device_dict(self):
return self.backend.get_device_info_by_index(self._device)

@property
def max_out_chn(self):
return int(self.device_dict['maxOutputChannels'])

@property
def max_in_chn(self):
return int(self.device_dict['maxInputChannels'])

@property
def device_dict(self):
Expand Down Expand Up @@ -183,6 +196,7 @@ def boot(self):
frames_per_buffer=self.bs,
output_device_index=self.device,
stream_callback=self._play_callback)
self._is_active = self.stream.is_active()
_LOGGER.info("Server Booted")
return self

Expand Down Expand Up @@ -301,3 +315,6 @@ def __exit__(self, exc_type, exc_value, traceback):
def __del__(self):
self.quit()
self.backend.terminate()

def __enter__(self):
return self.boot()
8 changes: 5 additions & 3 deletions pya/backend/Dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ class DummyBackend(BackendBase):
range = 1
bs = 256

def __init__(self, dummy_devices=None):
if dummy_devices is None:
self.dummy_devices = [dict(maxInputChannels=10, maxOutputChannels=10, index=0, name="DummyDevice")]
def __init__(self):
self.dummy_devices = [dict(maxInputChannels=10, maxOutputChannels=10, index=0, name="DummyDevice")]

def get_device_count(self):
return len(self.dummy_devices)
Expand All @@ -27,6 +26,9 @@ def get_default_output_device_info(self):
return self.dummy_devices[0]

def open(self, *args, input_flag, output_flag, rate, frames_per_buffer, channels, stream_callback=None, **kwargs):
checker = 'maxInputChannels' if input_flag else 'maxOutputChannels'
if channels > self.dummy_devices[0][checker]:
raise OSError("[Errno -9998] Invalid number of channels")
stream = DummyStream(input_flag=input_flag, output_flag=output_flag,
rate=rate, frames_per_buffer=frames_per_buffer, channels=channels,
stream_callback=stream_callback)
Expand Down
Loading

0 comments on commit e6357ea

Please sign in to comment.