mirror of https://github.com/McSinyx/palace
Compare commits
97 Commits
Author | SHA1 | Date |
---|---|---|
Nguyễn Gia Phong | 566911efad | |
Ngô Ngọc Đức Huy | 708f23b35a | |
Nguyễn Gia Phong | c5861833ab | |
MostDeadDeveloper | 1ba92b7c50 | |
Nguyễn Gia Phong | 74a9989e53 | |
Nguyễn Gia Phong | fae0848283 | |
Nguyễn Gia Phong | 90528cee2d | |
Nguyễn Gia Phong | 848421353d | |
Nguyễn Gia Phong | e576dc72ab | |
Nguyễn Gia Phong | 02d4889138 | |
Nguyễn Gia Phong | 9a1dbf5d94 | |
Huy-Ngo | 08408c56c7 | |
Nguyễn Gia Phong | a0b4a90f79 | |
Huy Ngo | fa513ea096 | |
Ngô Ngọc Đức Huy | 4caaee5727 | |
Nguyễn Gia Phong | 11cc59fc86 | |
Huy Ngo | 625b166261 | |
Nguyễn Gia Phong | 7fd5d49b3a | |
Nguyễn Gia Phong | f3da7f8e24 | |
Nguyễn Gia Phong | a8e039a9f0 | |
Huy-Ngo | 92f2454b96 | |
Nguyễn Gia Phong | 4e007b4518 | |
Nguyễn Gia Phong | c2a848cf6c | |
Nguyễn Gia Phong | 2e39a68865 | |
Nguyễn Gia Phong | 587179fecf | |
Francesco Caliumi | 3a9e23917f | |
Nguyễn Gia Phong | c78f14fb42 | |
Nguyễn Gia Phong | 6f00f7046a | |
Nguyễn Gia Phong | c216f307ef | |
Nguyễn Gia Phong | c24df3143b | |
Ngô Xuân Minh | 8285eb06c5 | |
Nguyễn Gia Phong | 83315de9c8 | |
Ngô Ngọc Đức Huy | 24192360f8 | |
Nguyễn Gia Phong | 292ef0393d | |
Ngô Xuân Minh | c59d0ec169 | |
Ngô Xuân Minh | b39aaa9903 | |
Nguyễn Gia Phong | 11cda099a3 | |
Nguyễn Gia Phong | a2444f0eab | |
Nguyễn Gia Phong | 5406740517 | |
Nguyễn Gia Phong | 7ff1d8f1d7 | |
Ngô Xuân Minh | e62989fa2b | |
Huy Ngo | 1aa054e8ed | |
Huy Ngo | 4f98c8e343 | |
Huy Ngo | 8016b78167 | |
Huy Ngo | b884a07903 | |
Huy Ngo | fdec565aaf | |
Ngô Ngọc Đức Huy | b2329a1428 | |
Nguyễn Gia Phong | 9de664a750 | |
Ngô Ngọc Đức Huy | 88c5b0b57a | |
Nguyễn Gia Phong | 5ce35416c7 | |
Nguyễn Gia Phong | 2218d192cb | |
Nguyễn Gia Phong | d575e5e15a | |
Nguyễn Gia Phong | e95dad16bf | |
Nguyễn Gia Phong | 944dd067eb | |
Ngô Ngọc Đức Huy | 6148318585 | |
Nguyễn Gia Phong | e674ade7e4 | |
Ngô Ngọc Đức Huy | c9134b31e1 | |
Nguyễn Gia Phong | 8a6ce4bd13 | |
Nguyễn Gia Phong | 30c11b351d | |
Ngô Xuân Minh | 8972112d16 | |
Ngô Xuân Minh | a25c93bbb1 | |
Nguyễn Gia Phong | a9d743ae87 | |
Ngô Ngọc Đức Huy | c992a8a6b0 | |
Nguyễn Gia Phong | 6e95bf491c | |
Huy Ngo | b5ab03eb7e | |
Nguyễn Gia Phong | 09f14bf7f0 | |
Ngô Ngọc Đức Huy | ae913f88de | |
Nguyễn Gia Phong | bdfe30306d | |
Nguyễn Gia Phong | 80a9d88e90 | |
Nguyễn Gia Phong | 38a3a21f43 | |
Ngô Ngọc Đức Huy | aeda09d04e | |
Nguyễn Gia Phong | e747159161 | |
Nguyễn Gia Phong | 47231e9992 | |
Nguyễn Gia Phong | d29d8debe8 | |
Nguyễn Gia Phong | 8fa8f83346 | |
Nguyễn Gia Phong | b08c711dbc | |
Ngô Ngọc Đức Huy | ac3d826b72 | |
Nguyễn Gia Phong | 49072f101e | |
Nguyễn Gia Phong | 2a3bda152f | |
Ngô Xuân Minh | 47c565f25e | |
Nguyễn Gia Phong | 45a284b17a | |
Nguyễn Gia Phong | 40f63f738a | |
Ngô Ngọc Đức Huy | caf43f7b1d | |
Huy Ngo | 12ebb6a7ea | |
Huy Ngo | ad756e692a | |
Huy Ngo | 266cca6c6a | |
Nguyễn Gia Phong | d05d46cbf4 | |
Nguyễn Gia Phong | 3967e0cf1c | |
Nguyễn Gia Phong | 3cc53d56df | |
Nguyễn Gia Phong | 5df009875b | |
Nguyễn Gia Phong | f9dce3e6fd | |
Nguyễn Gia Phong | fc1bbfafeb | |
Nguyễn Gia Phong | 16bdb47c5a | |
Nguyễn Gia Phong | 308671bc40 | |
Nguyễn Gia Phong | 7f2d1cdf7e | |
Nguyễn Gia Phong | 428a436f36 | |
Ngô Xuân Minh | 51a72e94f9 |
|
@ -0,0 +1,24 @@
|
|||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^\d+(\.\d+)+((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$/
|
||||
|
||||
environment:
|
||||
global:
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
|
||||
Alure2_DIR: C:\Program Files (x86)\alure\lib\cmake\Alure2
|
||||
matrix:
|
||||
- CIBW_BUILD: cp36-win_amd64
|
||||
- CIBW_BUILD: cp37-win_amd64
|
||||
- CIBW_BUILD: cp38-win_amd64
|
||||
|
||||
install:
|
||||
- curl "https://openal-soft.org/openal-binaries/openal-soft-1.20.1-bin.zip" -o openal-soft-1.20.1-bin.zip
|
||||
- 7z x -o%APPVEYOR_BUILD_FOLDER%\.. openal-soft-1.20.1-bin.zip
|
||||
- set OPENALDIR=%APPVEYOR_BUILD_FOLDER%\..\openal-soft-1.20.1-bin
|
||||
- git clone https://github.com/kcat/alure %APPVEYOR_BUILD_FOLDER%\..\alure
|
||||
- cmake -A x64 -S %APPVEYOR_BUILD_FOLDER%\..\alure -B %APPVEYOR_BUILD_FOLDER%\..\alure\build
|
||||
- cmake --build %APPVEYOR_BUILD_FOLDER%\..\alure\build --config Release --target install
|
||||
- py -3 -m pip install cibuildwheel
|
||||
|
||||
build_script: echo py -3 -m cibuildwheel --output-dir wheelhouse
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
set -ex
|
||||
git clone --depth 1 https://github.com/kcat/alure /tmp/alure
|
||||
OPENALDIR=$(brew --prefix openal-soft) cmake -DCMAKE_FIND_FRAMEWORK=NEVER \
|
||||
-S /tmp/alure -B /tmp/alure/build
|
||||
sudo cmake --build /tmp/alure/build --parallel $(sysctl -n hw.ncpu) \
|
||||
--config Release --target install
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/sh
|
||||
set -ex
|
||||
yum install -y git cmake pulseaudio \
|
||||
alsa-lib-devel pulseaudio-libs-devel jack-audio-connection-kit-devel \
|
||||
libvorbis-devel opusfile-devel libsndfile-devel
|
||||
pulseaudio --start
|
||||
|
||||
pip install cmake>=3.13
|
||||
git clone --depth 1 https://github.com/kcat/openal-soft
|
||||
cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -S openal-soft -B openal-soft/build
|
||||
cmake --build openal-soft/build --parallel `nproc` --target install
|
||||
|
||||
git clone --depth 1 https://github.com/kcat/alure
|
||||
cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -S alure -B alure/build
|
||||
cmake --build alure/build --parallel `nproc` --target install
|
||||
pip uninstall -y cmake
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
set -ex
|
||||
cd $(mktemp -d)
|
||||
unzip $1
|
||||
DYLD_FALLBACK_LIBRARY_PATH=/usr/local/lib delocate-path -L palace.dylibs .
|
||||
wheel=$(basename $1)
|
||||
zip -r $wheel *
|
||||
mkdir -p $2
|
||||
mv $wheel $2
|
||||
tempdir=$(pwd)
|
||||
cd -
|
||||
rm -rf $tempdir
|
|
@ -0,0 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: __huy_ngo__
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: McSinyx
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@ -0,0 +1,48 @@
|
|||
name: Deploy documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/*
|
||||
- docs/**
|
||||
- src/**
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Checkout palace
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout alure
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: kcat/alure
|
||||
path: alure
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt install \
|
||||
cmake libopenal-dev libvorbis-dev libopusfile-dev libsndfile1-dev
|
||||
cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -S alure -B alure/build
|
||||
sudo cmake --build alure/build --parallel `nproc` --target install
|
||||
rm -fr alure
|
||||
python -m pip install Sphinx sphinx_rtd_theme .
|
||||
|
||||
- name: Build site
|
||||
working-directory: docs
|
||||
run: make html
|
||||
|
||||
- name: Deploy site
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/build/html
|
|
@ -0,0 +1,28 @@
|
|||
name: Quick check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install tox
|
||||
run: python -m pip install tox
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Main check
|
||||
run: python -m tox -e lint
|
|
@ -131,7 +131,13 @@ dmypy.json
|
|||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE & editors
|
||||
# PyCharm
|
||||
.idea/
|
||||
# Emacs backup files patterns
|
||||
|
||||
# Emacs
|
||||
\#*\#
|
||||
.\#*
|
||||
*~
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
|
39
.travis.yml
39
.travis.yml
|
@ -1,30 +1,47 @@
|
|||
language: python
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /\d+\.\d+\.\d+(-\S+)?$/
|
||||
- /^\d+(\.\d+)+((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$/
|
||||
|
||||
language: python
|
||||
|
||||
env:
|
||||
global:
|
||||
- TWINE_USERNAME=__token__
|
||||
- MACOSX_DEPLOYMENT_TARGET=10.9
|
||||
- CIBW_BEFORE_BUILD_MACOS=.ci/before-build-macos
|
||||
- CIBW_BEFORE_BUILD_LINUX=.ci/before-build-manylinux2014
|
||||
- CIBW_MANYLINUX_X86_64_IMAGE=manylinux2014
|
||||
- CIBW_BEFORE_BUILD_LINUX=.travis/cibw-before-build-manylinux2014
|
||||
- CIBW_REPAIR_WHEEL_COMMAND_MACOS=".ci/repair-whl-macos {wheel} {dest_dir}"
|
||||
- CIBW_TEST_REQUIRES=tox
|
||||
- CIBW_TEST_COMMAND_LINUX="tox -c /project"
|
||||
- CIBW_TEST_COMMAND="tox -c {project}"
|
||||
|
||||
addons:
|
||||
homebrew:
|
||||
packages:
|
||||
- openal-soft
|
||||
- libvorbis
|
||||
- opusfile
|
||||
- libsndfile
|
||||
|
||||
install: python3 -m pip install twine cibuildwheel
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode11.3
|
||||
language: shell
|
||||
env: CIBW_BUILD=cp36-macosx_x86_64
|
||||
- services: docker
|
||||
env: CIBW_BUILD=cp36-manylinux_x86_64
|
||||
env: CIBW_BUILD="cp36-manylinux_x86_64 cp36-manylinux_aarch64"
|
||||
- services: docker
|
||||
env: CIBW_BUILD=cp37-manylinux_x86_64
|
||||
env: CIBW_BUILD="cp37-manylinux_x86_64 cp37-manylinux_aarch64"
|
||||
- services: docker
|
||||
env: CIBW_BUILD=cp38-manylinux_x86_64
|
||||
env: CIBW_BUILD="cp38-manylinux_x86_64 cp38-manylinux_aarch64"
|
||||
- services: docker
|
||||
env: CIBW_BUILD="cp39-manylinux_x86_64 cp39-manylinux_aarch64"
|
||||
|
||||
install: pip install twine cibuildwheel
|
||||
|
||||
script: cibuildwheel --output-dir=dist
|
||||
script: python3 -m cibuildwheel --output-dir=dist
|
||||
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
#!/bin/sh
|
||||
if [ ! -d openal-soft ]
|
||||
then
|
||||
yum install -y git cmake pulseaudio \
|
||||
alsa-lib-devel pulseaudio-libs-devel jack-audio-connection-kit-devel \
|
||||
libvorbis-devel opusfile-devel libsndfile-devel
|
||||
pip install cmake>=3
|
||||
git clone https://github.com/kcat/openal-soft
|
||||
cd openal-soft/build
|
||||
cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr ..
|
||||
cmake --build . --parallel `nproc` --target install --config Release
|
||||
git clone https://github.com/kcat/alure
|
||||
mkdir alure/build
|
||||
cd alure/build
|
||||
cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr ..
|
||||
cmake --build . --parallel `nproc` --target install --config Release
|
||||
pip uninstall -y cmake
|
||||
pulseaudio --start
|
||||
fi
|
11
MANIFEST.in
11
MANIFEST.in
|
@ -1 +1,10 @@
|
|||
include src/*.pxd src/*.pyx src/*.h tests/*.py examples/*.py CMakeLists.txt
|
||||
include CMakeLists.txt
|
||||
recursive-include src *.h *.pxd *.pyx
|
||||
|
||||
graft docs
|
||||
prune docs/build
|
||||
|
||||
include tox.ini
|
||||
recursive-include tests *.py
|
||||
recursive-include examples *.py
|
||||
graft tests/data
|
||||
|
|
52
README.md
52
README.md
|
@ -10,12 +10,12 @@ To quote alure's README,
|
|||
In some sense, what palace aimes to be to [OpenAL Soft] is what [ModernGL]
|
||||
is to OpenGL (except that all the heavy-lifting are taken are by alure):
|
||||
|
||||
* 3D sound rendering
|
||||
* Environmental audio effects: reverb, atmospheric air absorption,
|
||||
* 3D positional sound rendering
|
||||
* Environmental effects: reverb, atmospheric air absorption,
|
||||
sound occlusion and obstruction
|
||||
* Binaural (HRTF) rendering
|
||||
* Out-of-the-box audio decoding of FLAC, MP3, Ogg Vorbis, Opus, WAV, AIFF, etc.
|
||||
* Modern Pythonic API: snake_case, `@property`, `with` context manager,
|
||||
* Modern Pythonic API: `snake_case`, `@property`, `with` context manager,
|
||||
type annotation
|
||||
|
||||
## Installation
|
||||
|
@ -27,37 +27,36 @@ Palace can be install from the [Python Package Index][PyPI] via simply
|
|||
|
||||
pip install palace
|
||||
|
||||
Wheel distribution is only built for GNU/Linux on amd64 at the time of writing.
|
||||
If you want to help out, please head to our GitHub issues [#1][GH-1].
|
||||
Wheel distributions are built exclusively for amd64. Currently, only GNU/Linux
|
||||
and macOS are properly supported. If you want to help packaging for Windows,
|
||||
please see [GH-1] on our issues tracker on GitHub.
|
||||
|
||||
### From source
|
||||
Aside from the build dependencies listed in `pyproject.toml`, one will
|
||||
additionally need compatible Python headers, [alure], a C++11 compiler,
|
||||
additionally need compatible Python headers, [alure], a C++14 compiler,
|
||||
[CMake] 2.6+ (and probably `git` for fetching the source).
|
||||
Palace can then be compiled and installed by running
|
||||
```sh
|
||||
git clone https://github.com/McSinyx/palace
|
||||
pip install palace/
|
||||
```
|
||||
|
||||
pip install git+https://github.com/McSinyx/palace
|
||||
|
||||
## Usage
|
||||
One may start with the `examples` for sample usage of palace.
|
||||
For further information, Python's `help` is your friend.
|
||||
For further information, Python's `help` is your friend and
|
||||
the API is also available for [online reference][API].
|
||||
|
||||
## Contributing
|
||||
Our documentation contains [a brief guide][contrib] which may help you
|
||||
get started with the development. We also think that you might find
|
||||
[our design principles][design] appealing as well.
|
||||
|
||||
## License and Credits
|
||||
Palace is released under the [GNU LGPL version 3 or later][LGPLv3+].
|
||||
Palace is free software: you can redistribute it and/or modify it
|
||||
under the terms of the [GNU Lesser General Public License][LGPLv3+]
|
||||
as published by the Free Software Foundation, either version 3
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
To ensure that palace can run without any dependencies outside of the [pip]
|
||||
toolchain, the wheels are bundled with dynamically linked libraries from
|
||||
the build machine, which is similar to static linking:
|
||||
|
||||
| Library | License |
|
||||
| -------------- | ------------ |
|
||||
| [Alure][alure] | ZLib |
|
||||
| [OpenAL Soft] | GNU LGPLv2+ |
|
||||
| [Vorbis] | 3-clause BSD |
|
||||
| [Opus] | 3-clause BSD |
|
||||
| [libsndfile] | GNU LGPL2.1+ |
|
||||
[The full list of works bundled with palace and other credits][copying]
|
||||
can be found in our documentation.
|
||||
|
||||
[alure]: https://github.com/kcat/alure
|
||||
[OpenAL Soft]: https://kcat.strangesoft.net/openal.html
|
||||
|
@ -67,7 +66,8 @@ the build machine, which is similar to static linking:
|
|||
[PyPI]: https://pypi.org/project/palace/
|
||||
[GH-1]: https://github.com/McSinyx/palace/issues/1
|
||||
[CMake]: https://cmake.org/
|
||||
[Vorbis]: https://xiph.org/vorbis/
|
||||
[Opus]: http://opus-codec.org/
|
||||
[libsndfile]: http://www.mega-nerd.com/libsndfile/
|
||||
[API]: https://mcsinyx.github.io/palace/reference.html
|
||||
[contrib]: https://mcsinyx.github.io/palace/contributing.html
|
||||
[design]: https://mcsinyx.github.io/palace/design.html
|
||||
[LGPLv3+]: https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||
[copying]: https://mcsinyx.github.io/palace/copying.html
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
|
||||
# You can set these variables from the command line,
|
||||
# and also from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -0,0 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
|
@ -0,0 +1,36 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options.
|
||||
# For a full list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# Project information
|
||||
project = 'palace'
|
||||
copyright = '2019, 2020 Nguyễn Gia Phong et al'
|
||||
author = 'Nguyễn Gia Phong et al.'
|
||||
release = '0.2.1'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings.
|
||||
# They can be extensions coming with Sphinx (named 'sphinx.ext.*')
|
||||
# or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinx.ext.napoleon']
|
||||
napoleon_google_docstring = False
|
||||
default_role = 'py:obj'
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match
|
||||
# files and directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = []
|
||||
|
||||
# Options for HTML output
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets)
|
||||
# here, relative to this directory. They are copied after the builtin
|
||||
# static files, so a file named "default.css" will overwrite the builtin
|
||||
# "default.css".
|
||||
html_static_path = []
|
|
@ -0,0 +1,196 @@
|
|||
Getting Involved
|
||||
================
|
||||
|
||||
.. note:: The development of palace is carried out on GitHub_.
|
||||
Since GitHub is not free software, we fully understand
|
||||
if one does not want to register an account just to participate
|
||||
in our development. Therefore, we also welcome patches
|
||||
and bug reports sent via email.
|
||||
|
||||
First of all, thank you for using and contributing to palace! We welcome
|
||||
all forms of contribution, and `the mo the merier`_. By saying this, we also
|
||||
mean that we much prefer receiving many small and self-contained bug reports,
|
||||
feature requests and patches than a giant one. There is no limit for
|
||||
the number of contributions one may or should make. While it may seem
|
||||
appealing to be able to dump all thoughts and feelings into one ticket,
|
||||
it would be more difficult for us to keep track of the progress.
|
||||
|
||||
Reporting a Bug
|
||||
---------------
|
||||
|
||||
Before filing a bug report, please make sure that the bug has not been
|
||||
already reported by searching our GitHub Issues_ tracker.
|
||||
|
||||
To facilitate the debugging process, a bug report should at least contain
|
||||
the following information:
|
||||
|
||||
#. The platform, the CPython version and the compiler used to build it.
|
||||
These can be obtained from :py:func:`platform.platform`,
|
||||
:py:func:`platform.python_version` and :py:func:`platform.python_compiler`,
|
||||
respectively.
|
||||
#. The version of palace and how you installed it.
|
||||
The earlier is usually provided by ``pip show palace``.
|
||||
#. Detailed instructions on how to reproduce the bug,
|
||||
for example a short Python script would be appreciated.
|
||||
|
||||
Requesting a Feature
|
||||
--------------------
|
||||
|
||||
Prior to filing a feature request, please make sure that the feature
|
||||
has not been already reported by searching our GitHub Issues_ tracker.
|
||||
|
||||
Please only ask for features that you (or an incapacitated friend
|
||||
you can personally talk to) require. Do not request features because
|
||||
they seem like a good idea. If they are really useful, they will be
|
||||
requested by someone who requires them.
|
||||
|
||||
Submitting a Patch
|
||||
------------------
|
||||
|
||||
We accept all kinds of patches, from documentation and CI/CD setup
|
||||
to bug fixes, feature implementations and tests. These are hosted on GitHub
|
||||
and one may create a local repository by running::
|
||||
|
||||
git clone https://github.com/McSinyx/palace
|
||||
|
||||
While the patch can be submitted via email, it is preferable to file
|
||||
a pull request on GitHub against the ``master`` branch to allow more people
|
||||
to review it, since we do not have any mail list. Either way, contributors
|
||||
must have legal permission to distribute the code and it must be available
|
||||
under `LGPLv3+`_. Furthermore, each contributor retains the copyrights
|
||||
of their patch, to ensure that the licence can never be revoked even if
|
||||
others wants to. It is advisable that the author list per legal name
|
||||
under the copyright header of each source file they modify, like so::
|
||||
|
||||
Copyright (C) 2038 Foo Bar
|
||||
|
||||
Using GitHub
|
||||
^^^^^^^^^^^^
|
||||
|
||||
#. Create a fork_ of our repository on GitHub.
|
||||
#. Checkout the source code and (optionally) add the ``upstream`` remote::
|
||||
|
||||
git clone https://github.com/YOUR_GITHUB_USERNAME/palace
|
||||
cd palace
|
||||
git remote add upstream https://github.com/McSinyx/palace
|
||||
|
||||
#. Start working on your patch and make sure your code complies with
|
||||
the `Style Guidelines`_ and passes the test suit run by tox_.
|
||||
#. Add relevant tests to the patch and work on it until they all pass.
|
||||
In case one is only modifying tests, perse may install palace using
|
||||
``CYTHON_TRACE=1 pip install .`` then run pytest_ directly to avoid
|
||||
having to build the extension module multiple times.
|
||||
#. Update the copyright notices of the files you modified.
|
||||
Palace is collectively licensed under `LGPLv3+`_,
|
||||
and to protect the freedom of the users,
|
||||
copyright holders need to be properly documented.
|
||||
#. Add_, commit_ with `a great message`_ then push_ the result.
|
||||
#. Finally, `create a pull request`_. We will then review and merge it.
|
||||
|
||||
It is recommended to create a new branch in your fork
|
||||
(``git checkout -c what-you-are-working-on``) instead of working directly
|
||||
on ``master``. This way one can still sync per fork with our ``master`` branch
|
||||
and ``git pull --rebase upstream master`` to avoid integration issues.
|
||||
|
||||
Via Email
|
||||
^^^^^^^^^
|
||||
|
||||
#. Checkout the source code::
|
||||
|
||||
git clone https://github.com/McSinyx/palace
|
||||
cd palace
|
||||
|
||||
#. Work on your patch with tests and copyright notice included
|
||||
as described above.
|
||||
#. `git-format-patch`_ and send it to one of the maintainers
|
||||
(our emails addresses are available under ``git log``).
|
||||
We will then review and merge it.
|
||||
|
||||
In any case, thank you very much for your contributions!
|
||||
|
||||
Making a Release
|
||||
----------------
|
||||
|
||||
While this is meant for developers doing a palace release, contributors wishing
|
||||
to improve the CI/CD may find it helpful.
|
||||
|
||||
#. Under the local repository, checkout the ``master`` branch
|
||||
and sync with the one on GitHub using ``git pull``.
|
||||
#. Bump the version in ``setup.cfg`` and push to GitHub.
|
||||
#. Create a source distribution by running ``setup.py sdist``.
|
||||
The distribution generated by this command is now referred to as ``sdist``.
|
||||
#. Using twine_, upload the ``sdist`` to PyPI via ``twine upload $sdist``.
|
||||
#. On GitHub, tag a new release with the ``sdist`` attached.
|
||||
In the release note, make sure to include all user-facing changes
|
||||
since the previous release. This will trigger the CD services
|
||||
to build the wheels and publish them to PyPI.
|
||||
#. Wait for the wheel for your platform to arrive to PyPI and install it.
|
||||
Play around with it for a little to make sure that everything is OK.
|
||||
|
||||
Style Guidelines
|
||||
----------------
|
||||
|
||||
Python and Cython
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Generally, palace follows :pep:`8` and :pep:`257`,
|
||||
with the following preferences and exceptions:
|
||||
|
||||
* Hanging indentation is *always* preferred,
|
||||
where continuation lines are indented by 4 spaces.
|
||||
* Comments and one-line docstrings are limited to column 79
|
||||
instead of 72 like for multi-line docstrings.
|
||||
* Cython extern declarations need not follow the 79-character limit.
|
||||
* Break long lines before a binary operator.
|
||||
* Use form feeds sparingly to break long modules
|
||||
into pages of relating functions and classes.
|
||||
* Prefer single-quoted strings over double-quoted strings,
|
||||
unless the string contains single quote characters.
|
||||
* Avoid trailing commas at all costs.
|
||||
* Line breaks within comments and docstrings should not cut a phrase in half.
|
||||
* Everything deserves a docstring. Palace follows numpydoc_ which support
|
||||
documenting attributes as well as constants and module-level variables.
|
||||
In additional to docstrings, type annotations should be employed
|
||||
for all public names.
|
||||
* Use numpydoc_ markups moderately to keep docstrings readable as plain text.
|
||||
|
||||
C++
|
||||
^^^
|
||||
|
||||
C++ codes should follow GNU style, which is best documented at Octave_.
|
||||
|
||||
reStructuredText
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
In order for reStructuredText to be rendered correctly, the body of
|
||||
constructs beginning with a marker (lists, hyperlink targets, comments, etc.)
|
||||
must be aligned relative to the marker. For this reason, it is convenient
|
||||
to set your editor indentation level to 3 spaces, since most constructs
|
||||
starts with two dots and a space. However, be aware of that bullet items
|
||||
require 2-space alignment and other exceptions.
|
||||
|
||||
Limit all lines to a maximum of 79 characters. Similar to comments
|
||||
and docstrings, phrases should not be broken in the middle.
|
||||
The source code of this guide itself is a good example on how line breaks
|
||||
should be handled. Additionally, two spaces should also be used
|
||||
after a sentence-ending period in multi-sentence paragraph,
|
||||
except after the final sentence.
|
||||
|
||||
.. _GitHub: https://github.com/McSinyx/palace
|
||||
.. _the mo the merier:
|
||||
https://www.phrases.org.uk/meanings/the-more-the-merrier.html
|
||||
.. _Issues: https://github.com/McSinyx/palace/issues
|
||||
.. _LGPLv3+: https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||
.. _fork: https://github.com/McSinyx/palace/fork
|
||||
.. _tox: https://tox.readthedocs.io/en/latest/
|
||||
.. _pytest: https://docs.pytest.org/en/latest/
|
||||
.. _Add: https://git-scm.com/docs/git-add
|
||||
.. _commit: https://git-scm.com/docs/git-commit
|
||||
.. _a great message: https://chris.beams.io/posts/git-commit/#seven-rules
|
||||
.. _push: https://git-scm.com/docs/git-push
|
||||
.. _create a pull request:
|
||||
https://help.github.com/articles/creating-a-pull-request
|
||||
.. _git-format-patch: https://git-scm.com/docs/git-format-patch
|
||||
.. _twine: https://twine.readthedocs.io/en/latest/
|
||||
.. _numpydoc: https://numpydoc.readthedocs.io/en/latest/format.html
|
||||
.. _Octave: https://wiki.octave.org/C%2B%2B_style_guide
|
|
@ -0,0 +1,75 @@
|
|||
Copying
|
||||
=======
|
||||
|
||||
This listing is our best-faith, hard-work effort at accurate attribution,
|
||||
sources, and licenses for everything in palace. If you discover
|
||||
an asset/contribution that is incorrectly attributed or licensed,
|
||||
please contact us immediately. We are happy to do everything we can
|
||||
to fix or remove the issue.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Palace is free software: you can redistribute it and/or modify it
|
||||
under the terms of the `GNU Lesser General Public License`_
|
||||
as published by the Free Software Foundation, either version 3
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
To ensure that palace can run without any dependencies outside of the pip_
|
||||
toolchain, the wheels are bundled with dynamically linked libraries from
|
||||
the build machine, which is similar to static linking:
|
||||
|
||||
============== ============
|
||||
Library License
|
||||
============== ============
|
||||
Alure_ ZLib
|
||||
`OpenAL Soft`_ GNU LGPLv2+
|
||||
Vorbis_ 3-clause BSD
|
||||
Opus_ 3-clause BSD
|
||||
libsndfile_ GNU LGPL2.1+
|
||||
============== ============
|
||||
|
||||
In addition, the following sounds are used for testing:
|
||||
|
||||
=============================================== =========
|
||||
Sound (located in ``tests/data``) License
|
||||
=============================================== =========
|
||||
`164957__zonkmachine__white-noise.ogg`_ CC0 1.0
|
||||
`24741__tim-kahn__b23-c1-raw.aiff`_ CC BY 3.0
|
||||
`261590__kwahmah-02__little-glitch.flac`_ CC BY 3.0
|
||||
`353684__tec-studio__drip2.mp3`_ CC0 1.0
|
||||
`99642__jobro__deconvoluted-20hz-to-20khz.wav`_ CC BY 3.0
|
||||
=============================================== =========
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
Palace would never have seen the light of day without the help from
|
||||
the developers of Alure_ and Cython_ who promptly gave detail answers
|
||||
and made quick fixes to all of our problems.
|
||||
|
||||
The wheels are build using cibuildwheel_, which made building extension modules
|
||||
much less of a painful experience. `Travis CI`_ and AppVeyor_ kindly provides
|
||||
their services free of charge for automated CI/CD.
|
||||
|
||||
This documentation is generated using Sphinx_, whose maintainer responses
|
||||
extreamly quickly to obsolete Cython-related issues.
|
||||
|
||||
.. _GNU Lesser General Public License:
|
||||
https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||
.. _pip: https://pip.pypa.io/en/latest/
|
||||
.. _Alure: https://github.com/kcat/alure
|
||||
.. _OpenAL Soft: https://kcat.strangesoft.net/openal.html
|
||||
.. _Vorbis: https://xiph.org/vorbis/
|
||||
.. _Opus: https://opus-codec.org/
|
||||
.. _libsndfile: http://www.mega-nerd.com/libsndfile/
|
||||
.. _164957__zonkmachine__white-noise.ogg: https://freesound.org/s/164957/
|
||||
.. _24741__tim-kahn__b23-c1-raw.aiff: https://freesound.org/s/24741/
|
||||
.. _261590__kwahmah-02__little-glitch.flac: https://freesound.org/s/261590/
|
||||
.. _353684__tec-studio__drip2.mp3: https://freesound.org/s/353684/
|
||||
.. _99642__jobro__deconvoluted-20hz-to-20khz.wav: https://freesound.org/s/99642/
|
||||
.. _Cython: https://cython.org/
|
||||
.. _cibuildwheel: https://cibuildwheel.readthedocs.io/en/stable/
|
||||
.. _Sphinx: https://www.sphinx-doc.org/en/master/
|
||||
.. _Travis CI: https://travis-ci.com/
|
||||
.. _AppVeyor: https://www.appveyor.com/
|
|
@ -0,0 +1,185 @@
|
|||
Design Principles
|
||||
=================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
In this section, we will discuss a few design principles in order to write
|
||||
a safe, efficient, easy-to-use and extendable 3D audio library for Python,
|
||||
by wrapping existing functionalities from the C++ API alure_.
|
||||
|
||||
This part of the documentation assumes its reader are at least familiar with
|
||||
Cython, Python and C++11.
|
||||
|
||||
.. _impl-idiom:
|
||||
|
||||
The Impl Idiom
|
||||
--------------
|
||||
|
||||
*Not to be confused with* `the pimpl idiom`_.
|
||||
|
||||
For memory-safety, whenever possible, we rely on Cython for allocation and
|
||||
deallocation of C++ objects. To do this, the nullary constructor needs to be
|
||||
(re-)declared in Cython, e.g.
|
||||
|
||||
.. code-block:: cython
|
||||
|
||||
cdef extern from 'foobar.h' namespace 'foobar':
|
||||
cdef cppclass Foo:
|
||||
Foo()
|
||||
float meth(size_t crack) except +
|
||||
...
|
||||
|
||||
The Cython extension type can then be declared as follows
|
||||
|
||||
.. code-block:: cython
|
||||
|
||||
cdef class Bar:
|
||||
cdef Foo impl
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.impl = ...
|
||||
|
||||
@staticmethod
|
||||
def from_baz(baz: Baz) -> Bar:
|
||||
bar = Bar.__new__(Bar)
|
||||
bar.impl = ...
|
||||
return bar
|
||||
|
||||
def meth(self, crack: int) -> float:
|
||||
return self.impl.meth(crack)
|
||||
|
||||
The Modern Python
|
||||
-----------------
|
||||
|
||||
One of the goal of palace is to create a Pythonic, i.e. intuitive and concise,
|
||||
interface. To achieve this, we try to make use of some modern Python features,
|
||||
which not only allow users to adopt palace with ease, but also make their
|
||||
programs more readable and less error-prone.
|
||||
|
||||
.. _getter-setter:
|
||||
|
||||
Property Attributes
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A large proportion of alure API are getters/setter methods. In Python,
|
||||
it is a good practice to use property_ to abstract these calls, and thus make
|
||||
the interface more natural with attribute-like referencing and assignments.
|
||||
|
||||
Due to implementation details, Cython has to hijack the ``@property`` decorator
|
||||
to make it work for read-write properties. Unfortunately, the Cython-generated
|
||||
descriptors do not play very well with other builtin decorators, thus in some
|
||||
cases, it is recommended to alias the call to ``property`` as follows
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
getter = property
|
||||
setter = lambda fset: property(fset=fset, doc=fset.__doc__)
|
||||
|
||||
Then ``@getter`` and ``@setter`` can be used to decorate read-only and
|
||||
write-only properties, respectively, without any trouble even if other
|
||||
decorators are used for the same extension type method.
|
||||
|
||||
Context Managers
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
The alure API defines many objects that need manual tear-down in
|
||||
a particular order. Instead of trying to be clever and perform automatic
|
||||
clean-ups at garbage collection, we should put the user in control.
|
||||
To quote *The Zen of Python*,
|
||||
|
||||
| If the implementation is hard to explain, it's a bad idea.
|
||||
| If the implementation is easy to explain, it may be a good idea.
|
||||
|
||||
With that being said, it does not mean we do not provide any level of
|
||||
abstraction. A simplified case in point would be
|
||||
|
||||
.. code-block:: cython
|
||||
|
||||
cdef class Device:
|
||||
cdef alure.Device impl
|
||||
|
||||
def __init__(self, name: str = '') -> None:
|
||||
self.impl = devmgr.open_playback(name)
|
||||
|
||||
def __enter__(self) -> Device:
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc) -> Optional[bool]:
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
self.impl.close()
|
||||
|
||||
Now if the ``with`` statement is used, it will make sure the device
|
||||
will be closed, regardless of whatever may happen within the inner block
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with Device() as dev:
|
||||
...
|
||||
|
||||
as it is equivalent to
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
dev = Device()
|
||||
try:
|
||||
...
|
||||
finally:
|
||||
dev.close()
|
||||
|
||||
Other than closure/destruction of objects, typical uses of `context managers`__
|
||||
also include saving and restoring various kinds of global state (as seen in
|
||||
:py:class:`Context`), locking and unlocking resources, etc.
|
||||
|
||||
__ https://docs.python.org/3/reference/datamodel.html#context-managers
|
||||
|
||||
The Double Reference
|
||||
--------------------
|
||||
|
||||
While wrapping C++ interfaces, :ref:`the impl idiom <impl-idiom>` might not
|
||||
be adequate, since the derived Python methods need to be callable from C++.
|
||||
Luckily, Cython can handle Python objects within C++ classes just fine,
|
||||
although we'll need to handle the reference count ourselves, e.g.
|
||||
|
||||
.. code-block:: cython
|
||||
|
||||
cdef cppclass CppDecoder(alure.BaseDecoder):
|
||||
Decoder pyo
|
||||
|
||||
__init__(Decoder decoder):
|
||||
this.pyo = decoder
|
||||
Py_INCREF(pyo)
|
||||
|
||||
__dealloc__():
|
||||
Py_DECREF(pyo)
|
||||
|
||||
bool seek(uint64_t pos):
|
||||
return pyo.seek(pos)
|
||||
|
||||
With this being done, we can now write the wrapper as simply as
|
||||
|
||||
.. code-block:: cython
|
||||
|
||||
cdef class BaseDecoder:
|
||||
cdef shared_ptr[alure.Decoder] pimpl
|
||||
|
||||
def __cinit__(self, *args, **kwargs) -> None:
|
||||
self.pimpl = shared_ptr[alure.Decoder](new CppDecoder(self))
|
||||
|
||||
def seek(pos: int) -> bool:
|
||||
...
|
||||
|
||||
Because ``__cinit__`` is called by ``__new__``, any Python class derived
|
||||
from ``BaseDecoder`` will be exposed to C++ as an attribute of ``CppDecoder``.
|
||||
Effectively, this means the users can have the alure API calling their
|
||||
inherited Python object as naturally as if palace is implemented in pure Python.
|
||||
|
||||
In practice, :py:class:`BaseDecoder` will also need to take into account
|
||||
other guarding mechanisms like :py:class:`abc.ABC`. Due to Cython limitations,
|
||||
implementation as a pure Python class and :ref:`aliasing <getter-setter>` of
|
||||
``@getter``/``@setter`` should be considered.
|
||||
|
||||
.. _alure: https://github.com/kcat/alure
|
||||
.. _`the pimpl idiom`: https://wiki.c2.com/?PimplIdiom
|
||||
.. _property: https://docs.python.org/3/library/functions.html#property
|
|
@ -0,0 +1,45 @@
|
|||
Overview
|
||||
========
|
||||
|
||||
Pythonic Audio Library and Codecs Environment provides common higher-level API
|
||||
for audio rendering using OpenAL:
|
||||
|
||||
* 3D positional rendering, with HRTF_ support for stereo systems
|
||||
* Environmental effects: reverb, atmospheric air absorption,
|
||||
sound occlusion and obstruction
|
||||
* Out-of-the-box codec support: FLAC, MP3, Ogg Vorbis, Opus, WAV, AIFF, etc.
|
||||
|
||||
Palace wraps around the C++ interface alure_ using Cython_ for a safe and
|
||||
convenient interface with type hinting, data descriptors and context managers,
|
||||
following :pep:`8#naming-conventions` (``PascalCase.snake_case``).
|
||||
|
||||
.. toctree::
|
||||
:caption: Table of Contents
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
tutorial/index
|
||||
reference/index
|
||||
design
|
||||
contributing
|
||||
copying
|
||||
|
||||
.. toctree::
|
||||
:caption: Quick Navigation
|
||||
:hidden:
|
||||
|
||||
Python Package Index <https://pypi.org/project/palace/>
|
||||
Travis CI Build <https://travis-ci.com/github/McSinyx/palace>
|
||||
AppVeyor Build <https://ci.appveyor.com/project/McSinyx/palace>
|
||||
GitHub Repository <https://github.com/McSinyx/palace>
|
||||
Matrix Chat Room <https://matrix.to/#/#palace-dev:matrix.org>
|
||||
|
||||
Indices and Tables
|
||||
------------------
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
|
||||
.. _HRTF: https://en.wikipedia.org/wiki/Head-related_transfer_function
|
||||
.. _alure: https://github.com/kcat/alure
|
||||
.. _Cython: https://cython.org
|
|
@ -0,0 +1,37 @@
|
|||
Installation
|
||||
============
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
Palace requires CPython_ version 3.6 or above for runtime
|
||||
and pip_ for installation.
|
||||
|
||||
Via PyPI
|
||||
--------
|
||||
|
||||
Palace can be installed from PyPI::
|
||||
|
||||
pip install palace
|
||||
|
||||
Wheel distributions are built exclusively for amd64. Currently, only GNU/Linux
|
||||
and macOS are properly supported. If you want to help packaging for Windows,
|
||||
please see `GH-1`_ on our issues tracker on GitHub.
|
||||
|
||||
From source
|
||||
-----------
|
||||
|
||||
Aside from the build dependencies listed in ``pyproject.toml``,
|
||||
one will additionally need compatible Python headers, alure_,
|
||||
a C++14 compiler, CMake_ 2.6+ (and probably git_ for fetching the source).
|
||||
Palace can then be compiled and installed by running::
|
||||
|
||||
git clone https://github.com/McSinyx/palace.git
|
||||
pip install palace/
|
||||
|
||||
.. _CPython: https://www.python.org/
|
||||
.. _pip: https://pip.pypa.io/en/latest/
|
||||
.. _GH-1: https://github.com/McSinyx/palace/issues/1
|
||||
.. _alure: https://github.com/kcat/alure
|
||||
.. _CMake: https://cmake.org/
|
||||
.. _git: https://git-scm.com/
|
|
@ -0,0 +1,17 @@
|
|||
Resource Caching
|
||||
================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
Audio Buffers
|
||||
-------------
|
||||
|
||||
.. autoclass:: Buffer
|
||||
:members:
|
||||
|
||||
Loading & Freeing in Batch
|
||||
--------------------------
|
||||
|
||||
.. autofunction:: cache
|
||||
|
||||
.. autofunction:: free
|
|
@ -0,0 +1,83 @@
|
|||
Audio Library Contexts
|
||||
======================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
Context and Auxiliary Classes
|
||||
-----------------------------
|
||||
|
||||
.. autoclass:: Context
|
||||
:members:
|
||||
|
||||
.. autoclass:: Listener
|
||||
:members:
|
||||
|
||||
.. autoclass:: MessageHandler
|
||||
:members:
|
||||
|
||||
Using Contexts
|
||||
--------------
|
||||
|
||||
.. autofunction:: use_context
|
||||
|
||||
.. autofunction:: current_context
|
||||
|
||||
.. autofunction:: thread_local
|
||||
|
||||
Context Creation Attributes
|
||||
---------------------------
|
||||
|
||||
.. data:: CHANNEL_CONFIG
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the channel configuration
|
||||
(either ``MONO``, ``STEREO``, ``QUAD``, ``X51``, ``X61`` or ``X71``).
|
||||
|
||||
.. data:: SAMPLE_TYPE
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the sample type
|
||||
(either ``[UNSIGNED_]{BYTE,SHORT,INT}`` or ``FLOAT``).
|
||||
|
||||
.. data:: FREQUENCY
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the frequency in hertz.
|
||||
|
||||
.. data:: MONO_SOURCES
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the number of mono (3D) sources.
|
||||
|
||||
.. data:: STEREO_SOURCES
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the number of stereo sources.
|
||||
|
||||
.. data:: MAX_AUXILIARY_SENDS
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the maximum number of
|
||||
auxiliary source sends.
|
||||
|
||||
.. data:: HRTF
|
||||
:type: int
|
||||
|
||||
Context creation key to specify whether to enable HRTF
|
||||
(either ``FALSE``, ``TRUE`` or ``DONT_CARE``).
|
||||
|
||||
.. data:: HRTF_ID
|
||||
:type: int
|
||||
|
||||
Context creation key to specify the HRTF to be used.
|
||||
|
||||
.. data:: OUTPUT_LIMITER
|
||||
:type: int
|
||||
|
||||
Context creation key to specify whether to use a gain limiter
|
||||
(either ``FALSE``, ``TRUE`` or ``DONT_CARE``).
|
||||
|
||||
.. data:: distance_models
|
||||
:type: Tuple[str, ...]
|
||||
|
||||
Names of available distance models.
|
|
@ -0,0 +1,44 @@
|
|||
Audio Streams
|
||||
=============
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
Builtin Decoders
|
||||
----------------
|
||||
|
||||
.. autoclass:: Decoder
|
||||
:members:
|
||||
|
||||
Decoder Interface
|
||||
-----------------
|
||||
|
||||
.. data:: decoder_factories
|
||||
:type: DecoderNamespace
|
||||
|
||||
Simple object for storing decoder factories.
|
||||
|
||||
User-registered factories are tried one after another
|
||||
if :py:exc:`RuntimeError` is raised, in lexicographical order.
|
||||
Internal decoder factories are always used after registered ones.
|
||||
|
||||
.. autofunction:: decode
|
||||
|
||||
.. autoclass:: BaseDecoder
|
||||
:members:
|
||||
|
||||
Miscellaneous
|
||||
-------------
|
||||
|
||||
.. data:: sample_types
|
||||
:type: Tuple[str, ...]
|
||||
|
||||
Names of available sample types.
|
||||
|
||||
.. data:: channel_configs
|
||||
:type: Tuple[str, ...]
|
||||
|
||||
Names of available channel configurations.
|
||||
|
||||
.. autofunction:: sample_size
|
||||
|
||||
.. autofunction:: sample_length
|
|
@ -0,0 +1,21 @@
|
|||
Audio Devices
|
||||
=============
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
Device-Dependent Utilities
|
||||
--------------------------
|
||||
|
||||
.. autoclass:: Device
|
||||
:members:
|
||||
|
||||
Device-Independent Utilities
|
||||
----------------------------
|
||||
|
||||
.. data:: device_names
|
||||
:type: DeviceNames
|
||||
|
||||
Read-only namespace of device names by category (``basic``, ``full`` and
|
||||
``capture``), as tuples of strings whose first item being the default.
|
||||
|
||||
.. autofunction:: query_extension
|
|
@ -0,0 +1,34 @@
|
|||
Environmental Effects
|
||||
=====================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
For the sake of brevity, we only document the constraints of each effect's
|
||||
properties. Further details can be found at OpenAL's `Effect Extension Guide`_
|
||||
which specifies the purpose and usage of each value.
|
||||
|
||||
Base Effect
|
||||
-----------
|
||||
|
||||
.. autoclass:: BaseEffect
|
||||
:members:
|
||||
|
||||
Chorus Effect
|
||||
-------------
|
||||
|
||||
.. autoclass:: ChorusEffect
|
||||
:members:
|
||||
|
||||
Reverb Effect
|
||||
-------------
|
||||
|
||||
.. data:: reverb_preset_names
|
||||
:type: Tuple[str, ...]
|
||||
|
||||
Names of predefined reverb effect presets in lexicographical order.
|
||||
|
||||
.. autoclass:: ReverbEffect
|
||||
:members:
|
||||
|
||||
.. _Effect Extension Guide:
|
||||
https://kcat.strangesoft.net/misc-downloads/Effects%20Extension%20Guide.pdf
|
|
@ -0,0 +1,11 @@
|
|||
File I/O Interface
|
||||
==================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
.. autofunction:: current_fileio
|
||||
|
||||
.. autofunction:: use_fileio
|
||||
|
||||
.. autoclass:: FileIO
|
||||
:members:
|
|
@ -0,0 +1,14 @@
|
|||
Reference
|
||||
=========
|
||||
|
||||
API reference is divided into the following sections:
|
||||
|
||||
.. toctree::
|
||||
|
||||
device
|
||||
context
|
||||
buffer
|
||||
source
|
||||
effect
|
||||
decoder
|
||||
file-io
|
|
@ -0,0 +1,16 @@
|
|||
Sources & Source Groups
|
||||
=======================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
Sources
|
||||
-------
|
||||
|
||||
.. autoclass:: Source
|
||||
:members:
|
||||
|
||||
Source Groups
|
||||
-------------
|
||||
|
||||
.. autoclass:: SourceGroup
|
||||
:members:
|
|
@ -0,0 +1,31 @@
|
|||
<h3>Quick Navigation</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1">
|
||||
<a class="reference external" href="https://pypi.org/project/palace/">
|
||||
Python Package Index
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a class="reference external"
|
||||
href="https://travis-ci.com/github/McSinyx/palace">
|
||||
Travis CI Build
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a class="reference external"
|
||||
href="https://ci.appveyor.com/project/McSinyx/palace">
|
||||
AppVeyor Build
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a class="reference external" href="https://github.com/McSinyx/palace">
|
||||
GitHub Repository
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a class="reference external"
|
||||
href="https://matrix.to/#/#palace-dev:matrix.org">
|
||||
Matrix Chat Room
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
|
@ -0,0 +1,41 @@
|
|||
Context Creation
|
||||
================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
A context is an object that allows palace to access OpenAL,
|
||||
which is essential when you work with palace. Context maintains
|
||||
the audio environment and contains environment settings and components
|
||||
such as sources, buffers, and effects.
|
||||
|
||||
Creating a Device Object
|
||||
------------------------
|
||||
|
||||
To create a context, we must first create a device,
|
||||
since it's a parameter of the context object.
|
||||
|
||||
To create an object, well, you just have to instantiate
|
||||
the :py:class:`Device` class.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from palace import Device
|
||||
|
||||
with Device() as dev:
|
||||
# Your code goes here
|
||||
|
||||
This is how you declare a :py:class:`Device` object with the default device.
|
||||
There can be several devices available, which can be found
|
||||
in :py:data:`device_names`.
|
||||
|
||||
Creating a Context
|
||||
------------------
|
||||
|
||||
Now that we've created a device, we can create the context:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from palace import Device, Context
|
||||
|
||||
with Device() as dev, Context(dev) as ctx:
|
||||
# Your code goes here
|
|
@ -0,0 +1,65 @@
|
|||
Adding an Effect
|
||||
================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
This section will focus on how to add effects to the audio.
|
||||
|
||||
There are two set of audio effects supported by palace: :py:class:`ReverbEffect`
|
||||
and :py:class:`ChorusEffect`.
|
||||
|
||||
Reverb Effect
|
||||
-------------
|
||||
|
||||
Reverb happens when a sound is reflected and then decay as the sound is absorbed
|
||||
by the objects in the medium. :py:class:`ReverbEffect` facilitates such effect.
|
||||
|
||||
Creating a reverb effect can be as simple as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with ReverbEffect() as effect:
|
||||
source.sends[0].effect = effect
|
||||
|
||||
:py:attr:`Source.sends` is a collection of send path signals, each of which
|
||||
contains `effects` and `filter` that describes it. Here we are only concerned
|
||||
about the former.
|
||||
|
||||
The above code would yield a *generic* reverb effect by default.
|
||||
There are several other presets that you can use, which are listed
|
||||
by :py:data:`reverb_preset_names`. To use these preset, you can simply provide
|
||||
the preset effect name as the first parameter for the constructor. For example,
|
||||
to use `PIPE_LARGE` preset effect, you can initialize the effect like below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with ReverbEffect('PIPE_LARGE') as effect:
|
||||
source.sends[0].effect = effect
|
||||
|
||||
These effects can be modified via their attributes.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
effect.gain = 0.4
|
||||
effect.diffusion = 0.65
|
||||
late_reverb_pan = 0.2, 0.1, 0.3
|
||||
|
||||
The list of these attributes and their constraints can be found
|
||||
in the documentation of :py:class:`ReverbEffect`.
|
||||
|
||||
Chorus Effect
|
||||
-------------
|
||||
|
||||
:py:class:`ChorusEffect` does not have preset effects like
|
||||
:py:class:`ReverbEffect`, so you would have to initialize the effect attributes
|
||||
on creation.
|
||||
|
||||
There are five parameters to initialize the effect, respectively: waveform,
|
||||
phase, depth, feedback, and delay.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with ChorusEffect('sine', 20, 0.4, 0.5, 0.008) as effect:
|
||||
source.sends[0].effect = effect
|
||||
|
||||
For the constraints of these parameters, please refer to the documentation.
|
|
@ -0,0 +1,15 @@
|
|||
Tutorial
|
||||
========
|
||||
|
||||
This tutorial will guide you on:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
context
|
||||
play-audio
|
||||
source
|
||||
effect
|
||||
.. comment these to add later
|
||||
Customize decoder
|
||||
Generate sounds
|
|
@ -0,0 +1,101 @@
|
|||
Play an Audio
|
||||
=============
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
Now that you know how to create a context,
|
||||
let's get into the most essential use case: playing audio.
|
||||
|
||||
Creating a Source
|
||||
-----------------
|
||||
|
||||
To play an audio, you have to create a source. This source
|
||||
is an imaginary sound broadcaster, whose positions and properties
|
||||
can be changed to create desired effects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from palace import Device, Context, Source
|
||||
|
||||
with Device() as dev, Context(dev) as ctx:
|
||||
with Source() as src:
|
||||
# to be written
|
||||
|
||||
Just like for the case of :py:class:`Context`, :py:class:`Source` creation
|
||||
requires a context, but here the context is passed implicitly.
|
||||
|
||||
Decode the Audio File
|
||||
---------------------
|
||||
|
||||
Palace has a module level function :py:func:`decode`, which decodes audio file
|
||||
automatically, and this decoded file is a :py:class:`Decoder` object. This object
|
||||
can be played by a simple :py:meth:`Decoder.play` method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from palace import Device, Context, Source, decode
|
||||
|
||||
filename = 'some_audio.ogg'
|
||||
with Device() as dev, Context(dev) as ctx:
|
||||
with Source() as src:
|
||||
dec = decode(filename)
|
||||
|
||||
We are almost there. Now, let's look at the document for :py:meth:`Decoder.play`.
|
||||
The method takes 3 parameters: ``chunk_len``, ``queue_size``, and ``source``.
|
||||
|
||||
The source object is optional, because if you don't have it, a new source
|
||||
will be generated by default.
|
||||
|
||||
The audio is divided into chunks, each of which is of length ``chunk_len``.
|
||||
Then ``queue_size`` is the number of these chunks that it will play.
|
||||
|
||||
.. TODO: I think it's better to include a diagram here. Add later
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from palace import Device, Context, Source, decode
|
||||
|
||||
filename = 'some_audio.ogg'
|
||||
with Device() as dev, Context(dev) as ctx:
|
||||
with Source() as src:
|
||||
dec = decode(filename)
|
||||
dec.play(12000, 4, src)
|
||||
|
||||
But we don't want it to play only a small part of the audio. We want it to
|
||||
play all of it. How do we do that? The answer is a loop.
|
||||
|
||||
There is a method, :py:meth:`Context.update`, which update the context and the source.
|
||||
When the source is updated, it will be filled with new chunks of data from
|
||||
the decoder.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from palace import Device, Context, Source, decode
|
||||
|
||||
filename = 'some_audio.ogg'
|
||||
with Device() as dev, Context(dev) as ctx:
|
||||
with Source() as src:
|
||||
dec = decode(filename)
|
||||
dec.play(12000, 4, src)
|
||||
while src.playing:
|
||||
ctx.update()
|
||||
|
||||
If you tried this code for a song, you will find that it's a bit rush.
|
||||
That is because the source is renewed too fast. So, a simple solution
|
||||
is to ``sleep`` for a while.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from time import sleep
|
||||
from palace import Device, Context, Source, decode
|
||||
|
||||
filename = 'some_audio.ogg'
|
||||
with Device() as dev, Context(dev) as ctx:
|
||||
with Source() as src:
|
||||
dec = decode(filename)
|
||||
dec.play(12000, 4, src)
|
||||
while src.playing:
|
||||
sleep(0.025)
|
||||
ctx.update()
|
||||
|
||||
Congratulation! Enjoy your music before we get to the next part of this tutorial.
|
|
@ -0,0 +1,82 @@
|
|||
Source Manipulation
|
||||
===================
|
||||
|
||||
.. currentmodule:: palace
|
||||
|
||||
We have created a source in the last section.
|
||||
As said previously, its properties can be manipulated to create wanted effects.
|
||||
|
||||
Moving the Source
|
||||
-----------------
|
||||
|
||||
Changing :py:attr:`Source.position` is one of the most noticeable,
|
||||
but first, we have to enable spatialization via :py:attr:`Source.spatialize`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from time import sleep
|
||||
from palace import Device, Context, Source, decode
|
||||
|
||||
with Device() as device, Context(device) as context, Source() as source:
|
||||
source.spatialize = True
|
||||
decoder = decode('some_audio.ogg')
|
||||
decoder.play(12000, 4, source)
|
||||
while source.playing:
|
||||
sleep(0.025)
|
||||
context.update()
|
||||
|
||||
Now, we can set the position of the source in this virtual 3D space.
|
||||
The position is a 3-tuple indicating the coordinate of the source.
|
||||
The axes are aligned according to the normal coordinate system:
|
||||
|
||||
- The x-axis goes from left to right
|
||||
- The y-axis goes from below to above
|
||||
- The z-axis goes from front to back
|
||||
|
||||
For example, this will set the source above the listener::
|
||||
|
||||
src.position = 0, 1, 0
|
||||
|
||||
.. note::
|
||||
|
||||
For this too work for stereo, you have to have HRTF enabled.
|
||||
You can check that via :py:attr:`Device.current_hrtf`.
|
||||
|
||||
You can as well use a function to move the source automatically by writing
|
||||
a function that generate positions. A simple example is circular motion.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from itertools import takewhile, count
|
||||
...
|
||||
for i in takewhile(src.playing, count(step=0.025)):
|
||||
source.position = sin(i), 0, cos(-i)
|
||||
...
|
||||
|
||||
A more well-written example of this can be found `in our repository`_.
|
||||
|
||||
Speed and Pitch
|
||||
---------------
|
||||
|
||||
Modifying :py:attr:`pitch` changes the playing speed, effectively changing
|
||||
pitch. Pitch can be any positive number.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
src.pitch = 2 # high pitch
|
||||
src.pitch = 0.4 # low pitch
|
||||
|
||||
Air Absorption Factor
|
||||
---------------------
|
||||
|
||||
:py:attr:`Source.air_absorption_factor` simulates atmospheric high-frequency
|
||||
air absorption. Higher values simulate foggy air and lower values simulate
|
||||
drier air.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
src.air_absorption_factor = 9 # very high humidity
|
||||
src.air_absorption_factor = 0 # dry air (default)
|
||||
|
||||
.. _in our repository:
|
||||
https://github.com/McSinyx/palace/blob/master/examples/palace-hrtf.py
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
# A simple example showing how to load and play a sound
|
||||
# Example usage of MessageHandler
|
||||
# Copyright (C) 2019, 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
|
@ -19,14 +19,26 @@
|
|||
|
||||
from argparse import ArgumentParser
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import count, takewhile
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Iterable
|
||||
from typing import Iterable, MutableSequence
|
||||
|
||||
from palace import Device, Context, Buffer
|
||||
from palace import Device, Context, Buffer, Source, MessageHandler
|
||||
|
||||
PERIOD = 0.025
|
||||
PERIOD: float = 0.025
|
||||
|
||||
|
||||
class EventHandler(MessageHandler):
|
||||
"""Message handler of buffer loading events."""
|
||||
def buffer_loading(self, name: str, channel_config: str, sample_type: str,
|
||||
sample_rate: int, data: MutableSequence[int]) -> None:
|
||||
"""Print buffers information on buffer loading events."""
|
||||
print(f'Playing {name} ({sample_type},',
|
||||
f'{channel_config}, {sample_rate} Hz)')
|
||||
|
||||
def source_stopped(self, source: Source) -> None:
|
||||
"""Destroy the source as playback finishes."""
|
||||
source.destroy()
|
||||
|
||||
|
||||
def pretty_time(seconds: float) -> str:
|
||||
|
@ -38,24 +50,24 @@ def pretty_time(seconds: float) -> str:
|
|||
|
||||
def play(files: Iterable[str], device: str) -> None:
|
||||
"""Load and play files on the given device."""
|
||||
with Device(device, fail_safe=True) as dev, Context(dev) as ctx:
|
||||
print('Opened', dev.name['full'])
|
||||
with Device(device) as dev, Context(dev) as ctx:
|
||||
print('Opened', dev.name)
|
||||
ctx.message_handler = EventHandler()
|
||||
for filename in files:
|
||||
try:
|
||||
buffer = Buffer(ctx, filename)
|
||||
buffer = Buffer(filename)
|
||||
except RuntimeError:
|
||||
stderr.write(f'Failed to open file: {filename}\n')
|
||||
continue
|
||||
with buffer, buffer.play() as src:
|
||||
print(f'Playing {filename} ({buffer.sample_type},',
|
||||
f'{buffer.channel_config}, {buffer.frequency} Hz)')
|
||||
|
||||
for i in takewhile(lambda i: src.playing, count()):
|
||||
with buffer:
|
||||
src = buffer.play()
|
||||
while src.playing:
|
||||
print(f' {pretty_time(src.offset_seconds)} /'
|
||||
f' {pretty_time(buffer.length_seconds)}',
|
||||
end='\r', flush=True)
|
||||
sleep(PERIOD)
|
||||
print()
|
||||
ctx.update()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
|
@ -20,12 +20,12 @@
|
|||
from argparse import ArgumentParser
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import count, takewhile
|
||||
from math import cos, sin
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Iterable
|
||||
|
||||
from palace import (ALC_TRUE, ALC_HRTF_SOFT, ALC_HRTF_ID_SOFT,
|
||||
Device, Context, Source, Decoder)
|
||||
from palace import TRUE, HRTF, HRTF_ID, decode, Device, Context, Source
|
||||
|
||||
CHUNK_LEN: int = 12000
|
||||
QUEUE_SIZE: int = 4
|
||||
|
@ -40,46 +40,46 @@ def pretty_time(seconds: float) -> str:
|
|||
|
||||
|
||||
def play(files: Iterable[str], device: str, hrtf_name: str,
|
||||
omega: float, angle: float) -> None:
|
||||
"""HRTF render files with stereo source (angle radians apart)
|
||||
rotating around in omega rad/s using ALC_SOFT_HRTF extension.
|
||||
"""
|
||||
with Device(device, fail_safe=True) as dev:
|
||||
print('Opened', dev.name['full'])
|
||||
omega: float) -> None:
|
||||
"""Render files using HRTF with source rotating in omega rad/s."""
|
||||
with Device(device) as dev:
|
||||
print('Opened', dev.name)
|
||||
hrtf_names = dev.hrtf_names
|
||||
if hrtf_names:
|
||||
print('Available HRTFs:')
|
||||
for name in hrtf_names: print(f' {name}')
|
||||
else:
|
||||
print('No HRTF found!')
|
||||
attrs = {ALC_HRTF_SOFT: ALC_TRUE}
|
||||
attrs = {HRTF: TRUE}
|
||||
if hrtf_name is not None:
|
||||
try:
|
||||
attrs[ALC_HRTF_ID_SOFT] = hrtf_names.index(hrtf_name)
|
||||
attrs[HRTF_ID] = hrtf_names.index(hrtf_name)
|
||||
except ValueError:
|
||||
stderr.write(f'HRTF "{hrtf_name}" not found\n')
|
||||
stderr.write(f'HRTF {hrtf_name!r} not found\n')
|
||||
|
||||
with Context(dev, attrs) as ctx, Source(ctx) as src:
|
||||
with Context(dev, attrs) as ctx, Source() as src:
|
||||
if dev.hrtf_enabled:
|
||||
print(f'Using HRTF "{dev.current_hrtf}"')
|
||||
print(f'Using HRTF {dev.current_hrtf!r}')
|
||||
else:
|
||||
print('HRTF not enabled!')
|
||||
src.spatialize = True
|
||||
|
||||
for filename in files:
|
||||
try:
|
||||
decoder = Decoder(ctx, filename)
|
||||
decoder = decode(filename)
|
||||
except RuntimeError:
|
||||
stderr.write(f'Failed to open file: {filename}\n')
|
||||
continue
|
||||
decoder.play(src, CHUNK_LEN, QUEUE_SIZE)
|
||||
decoder.play(CHUNK_LEN, QUEUE_SIZE, src)
|
||||
print(f'Playing {filename} ({decoder.sample_type},',
|
||||
f'{decoder.channel_config}, {decoder.frequency} Hz)')
|
||||
|
||||
for i in takewhile(lambda i: src.playing,
|
||||
count(step=PERIOD)):
|
||||
src.stereo_angles = i*omega, i*omega+angle
|
||||
print(f' {pretty_time(src.offset_seconds)} /'
|
||||
f' {pretty_time(decoder.length_seconds)}',
|
||||
end='\r', flush=True)
|
||||
src.position = sin(i*omega), 0, -cos(i*omega)
|
||||
sleep(PERIOD)
|
||||
ctx.update()
|
||||
print()
|
||||
|
@ -92,7 +92,5 @@ if __name__ == '__main__':
|
|||
parser.add_argument('-n', '--hrtf', dest='hrtf_name', help='HRTF name')
|
||||
parser.add_argument('-o', '--omega', type=float, default=1.0,
|
||||
help='angular velocity')
|
||||
parser.add_argument('-a', '--angle', type=float, default=1.0,
|
||||
help='relative angle between left and right sources')
|
||||
args = parser.parse_args()
|
||||
play(args.files, args.device, args.hrtf_name, args.omega, args.angle)
|
||||
play(args.files, args.device, args.hrtf_name, args.omega)
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from palace import device_names, device_name_default, Device
|
||||
from palace import device_names, Device, Context
|
||||
|
||||
|
||||
parser = ArgumentParser()
|
||||
|
@ -28,16 +28,22 @@ parser.add_argument('device', type=Device, default='', nargs='?',
|
|||
args = parser.parse_args()
|
||||
|
||||
with args.device:
|
||||
names = device_names.copy()
|
||||
for kind, default in device_name_default.items():
|
||||
i = names[kind].index(default)
|
||||
names[kind][i] += ' [DEFAULT]'
|
||||
print('Available basic devices:', *names['basic'], sep='\n ')
|
||||
print('\nAvailable devices:', *names['full'], sep='\n ')
|
||||
print('\nAvailable capture devices:', *names['capture'], sep='\n ')
|
||||
print('Available basic devices, with the first being default:',
|
||||
*device_names.basic, sep='\n ')
|
||||
print('\nAvailable devices, with the first being default:',
|
||||
*device_names.full, sep='\n ')
|
||||
print('\nAvailable capture devices, with the first being default:',
|
||||
*device_names.capture, sep='\n ')
|
||||
|
||||
print(f'\nInfo of device "{args.device.name["full"]}":')
|
||||
print(f'\nInfo of device {args.device.name!r}:')
|
||||
print('ALC version: {}.{}'.format(*args.device.alc_version))
|
||||
|
||||
with Context(args.device) as ctx:
|
||||
default_idx = ctx.default_resampler_index
|
||||
resamplers = ctx.available_resamplers
|
||||
resamplers[default_idx] += ' (default)'
|
||||
print('Available resamplers:', *resamplers, sep='\n ')
|
||||
|
||||
efx = args.device.efx_version
|
||||
if efx == (0, 0):
|
||||
print('EFX not supported!')
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env python3
|
||||
# Example for latency checking
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Iterable
|
||||
|
||||
from palace import Context, Device, Source, decode
|
||||
|
||||
CHUNK_LEN: int = 12000
|
||||
QUEUE_SIZE: int = 4
|
||||
PERIOD: float = 0.025
|
||||
|
||||
|
||||
def play(files: Iterable[str], device: str) -> None:
|
||||
"""Load and play the file on given device."""
|
||||
with Device(device) as dev, Context(dev) as ctx, Source() as src:
|
||||
print('Opened', dev.name)
|
||||
for filename in files:
|
||||
try:
|
||||
decoder = decode(filename)
|
||||
except RuntimeError:
|
||||
stderr.write(f'Failed to open file: {filename}\n')
|
||||
decoder.play(CHUNK_LEN, QUEUE_SIZE, src)
|
||||
print(f'Playing {filename} ({decoder.sample_type},',
|
||||
f'{decoder.channel_config}, {decoder.frequency} Hz)')
|
||||
while src.playing:
|
||||
print('Offset:', round(src.offset_seconds), 's - Latency:',
|
||||
src.latency//10**6, 'ms', end='\r', flush=True)
|
||||
sleep(PERIOD)
|
||||
ctx.update()
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('files', nargs='+', help='audio files')
|
||||
parser.add_argument('-d', '--device', default='', help='device name')
|
||||
args = parser.parse_args()
|
||||
play(args.files, args.device)
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python3
|
||||
# Apply reverb effect to sound playback
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from argparse import Action, ArgumentParser
|
||||
from datetime import datetime, timedelta
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Iterable
|
||||
|
||||
from palace import (reverb_preset_names, decode,
|
||||
Device, Context, Source, ReverbEffect)
|
||||
|
||||
CHUNK_LEN: int = 12000
|
||||
QUEUE_SIZE: int = 4
|
||||
PERIOD: float = 0.025
|
||||
|
||||
|
||||
class PresetPrinter(Action):
|
||||
"""CLI action to print available preset names and exit."""
|
||||
def __call__(self, parser: ArgumentParser, *args, **kwargs) -> None:
|
||||
print('Available reverb preset names:', *reverb_preset_names, sep='\n')
|
||||
parser.exit()
|
||||
|
||||
|
||||
def pretty_time(seconds: float) -> str:
|
||||
"""Return human-readably formatted time."""
|
||||
time = datetime.min + timedelta(seconds=seconds)
|
||||
if seconds < 3600: return time.strftime('%M:%S')
|
||||
return time.strftime('%H:%M:%S')
|
||||
|
||||
|
||||
def play(files: Iterable[str], device: str, reverb: str) -> None:
|
||||
"""Load and play files on the given device."""
|
||||
with Device(device) as dev, Context(dev) as ctx:
|
||||
print('Opened', dev.name)
|
||||
print('Loading reverb preset', reverb)
|
||||
with Source() as src, ReverbEffect(reverb) as fx:
|
||||
src.sends[0].effect = fx
|
||||
for filename in files:
|
||||
try:
|
||||
decoder = decode(filename)
|
||||
except RuntimeError:
|
||||
stderr.write(f'Failed to open file: {filename}\n')
|
||||
continue
|
||||
decoder.play(CHUNK_LEN, QUEUE_SIZE, src)
|
||||
print(f'Playing {filename} ({decoder.sample_type},',
|
||||
f'{decoder.channel_config}, {decoder.frequency} Hz)')
|
||||
while src.playing:
|
||||
print(f' {pretty_time(src.offset_seconds)} /'
|
||||
f' {pretty_time(decoder.length_seconds)}',
|
||||
end='\r', flush=True)
|
||||
sleep(PERIOD)
|
||||
ctx.update()
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('files', nargs='+', help='audio files')
|
||||
parser.add_argument('-p', '--presets', action=PresetPrinter, nargs=0,
|
||||
help='print available preset names and exit')
|
||||
parser.add_argument('-d', '--device', default='', help='device name')
|
||||
parser.add_argument('-r', '--reverb', default='GENERIC',
|
||||
help='reverb preset')
|
||||
args = parser.parse_args()
|
||||
play(args.files, args.device, args.reverb)
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env python3
|
||||
# Use decoders from Python standard libraries
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import aifc
|
||||
import sunau
|
||||
import wave
|
||||
from argparse import ArgumentParser
|
||||
from datetime import datetime, timedelta
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Iterable, Tuple
|
||||
from types import ModuleType
|
||||
|
||||
from palace import (channel_configs, sample_types, decoder_factories,
|
||||
Device, Context, Buffer, BaseDecoder, FileIO)
|
||||
|
||||
PERIOD: float = 0.025
|
||||
|
||||
|
||||
def pretty_time(seconds: float) -> str:
|
||||
"""Return human-readably formatted time."""
|
||||
time = datetime.min + timedelta(seconds=seconds)
|
||||
if seconds < 3600: return time.strftime('%M:%S')
|
||||
return time.strftime('%H:%M:%S')
|
||||
|
||||
|
||||
def play(files: Iterable[str], device: str) -> None:
|
||||
"""Load and play files on the given device."""
|
||||
with Device(device) as dev, Context(dev):
|
||||
print('Opened', dev.name)
|
||||
for filename in files:
|
||||
try:
|
||||
buffer = Buffer(filename)
|
||||
except RuntimeError:
|
||||
stderr.write(f'Failed to open file: {filename}\n')
|
||||
continue
|
||||
with buffer, buffer.play() as src:
|
||||
print(f'Playing {filename} ({buffer.sample_type},',
|
||||
f'{buffer.channel_config}, {buffer.frequency} Hz)')
|
||||
while src.playing:
|
||||
print(f' {pretty_time(src.offset_seconds)} /'
|
||||
f' {pretty_time(buffer.length_seconds)}',
|
||||
end='\r', flush=True)
|
||||
sleep(PERIOD)
|
||||
print()
|
||||
|
||||
|
||||
class StandardDecoder(BaseDecoder):
|
||||
"""Decoder wrapper for standard libraries aifc, sunau and wave."""
|
||||
def __init__(self, file: FileIO, module: ModuleType, mode: str):
|
||||
self.error = module.Error
|
||||
try:
|
||||
self.impl = module.open(file, mode)
|
||||
except self.error:
|
||||
raise RuntimeError
|
||||
|
||||
@BaseDecoder.frequency.getter
|
||||
def frequency(self) -> int: return self.impl.getframerate()
|
||||
|
||||
@BaseDecoder.channel_config.getter
|
||||
def channel_config(self) -> str:
|
||||
return channel_configs[self.impl.getnchannels()-1]
|
||||
|
||||
@BaseDecoder.sample_type.getter
|
||||
def sample_type(self) -> str:
|
||||
return sample_types[self.impl.getsampwidth()-1]
|
||||
|
||||
@BaseDecoder.length.getter
|
||||
def length(self) -> int: return self.impl.getnframes()
|
||||
|
||||
def seek(self, pos: int) -> bool:
|
||||
try:
|
||||
self.impl.setpos(pos)
|
||||
except self.error:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@BaseDecoder.loop_points.getter
|
||||
def loop_points(self) -> Tuple[int, int]: return 0, 0
|
||||
|
||||
def read(self, count: int) -> bytes: return self.impl.readframes(count)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('files', nargs='+', help='audio files')
|
||||
parser.add_argument('-d', '--device', default='', help='device name')
|
||||
args = parser.parse_args()
|
||||
decoder_factories.aifc = lambda file: StandardDecoder(file, aifc, 'rb')
|
||||
decoder_factories.sunau = lambda file: StandardDecoder(file, sunau, 'r')
|
||||
decoder_factories.wave = lambda file: StandardDecoder(file, wave, 'rb')
|
||||
play(args.files, args.device)
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env python3
|
||||
# Sample for tone generator
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from functools import partial
|
||||
from operator import not_
|
||||
from random import random
|
||||
from time import sleep
|
||||
from typing import Callable, Dict, Tuple
|
||||
|
||||
from numpy import arange, float32, ndarray, pi, sin, vectorize
|
||||
from palace import Buffer, Context, BaseDecoder, Device
|
||||
from scipy.signal import sawtooth, square
|
||||
|
||||
WAVEFORMS: Dict[str, Callable[[ndarray], ndarray]] = {
|
||||
'sine': sin,
|
||||
'square': square,
|
||||
'sawtooth': sawtooth,
|
||||
'triangle': partial(sawtooth, width=0.5),
|
||||
'impulse': vectorize(not_),
|
||||
'white-noise': vectorize(lambda time: random())}
|
||||
|
||||
|
||||
class ToneGenerator(BaseDecoder):
|
||||
"""Generator of elementary signals."""
|
||||
def __init__(self, waveform: str, duration: float, frequency: float):
|
||||
self.func = lambda frames: WAVEFORMS[waveform](
|
||||
frames / self.frequency * pi * 2 * frequency)
|
||||
self.duration = duration
|
||||
self.start = 0
|
||||
|
||||
@BaseDecoder.frequency.getter
|
||||
def frequency(self) -> int: return 44100
|
||||
|
||||
@BaseDecoder.channel_config.getter
|
||||
def channel_config(self) -> str:
|
||||
return 'Mono'
|
||||
|
||||
@BaseDecoder.sample_type.getter
|
||||
def sample_type(self) -> str:
|
||||
return '32-bit float'
|
||||
|
||||
@BaseDecoder.length.getter
|
||||
def length(self) -> int: return int(self.duration * self.frequency)
|
||||
|
||||
def seek(self, pos: int) -> bool: return False
|
||||
|
||||
@BaseDecoder.loop_points.getter
|
||||
def loop_points(self) -> Tuple[int, int]: return 0, 0
|
||||
|
||||
def read(self, count: int) -> bytes:
|
||||
stop = min(self.start + count, self.length)
|
||||
data = self.func(arange(self.start, stop))
|
||||
self.start = stop
|
||||
return data.astype(float32).tobytes()
|
||||
|
||||
|
||||
def play(device: str, waveform: str,
|
||||
duration: float, frequency: float) -> None:
|
||||
"""Play waveform at the given frequency for given duration."""
|
||||
with Device(device) as dev, Context(dev):
|
||||
print('Opened', dev.name)
|
||||
dec = ToneGenerator(waveform, duration, frequency)
|
||||
print(f'Playing {waveform} signal at {frequency} Hz for {duration} s')
|
||||
with Buffer.from_decoder(dec, 'tonegen') as buf, buf.play():
|
||||
sleep(duration)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('-d', '--device', default='', help='device name')
|
||||
parser.add_argument('-w', '--waveform', default='sine', choices=WAVEFORMS,
|
||||
help='waveform to be generated, default to sine')
|
||||
parser.add_argument('-l', '--duration', default=1.0, type=float,
|
||||
help='duration in second, default to 1.0')
|
||||
parser.add_argument('-f', '--frequency', default=440.0, type=float,
|
||||
help='wave frequency in hertz, default to 440.0')
|
||||
args = parser.parse_args()
|
||||
play(args.device, args.waveform, args.duration, args.frequency)
|
|
@ -1,3 +1,3 @@
|
|||
[build-system]
|
||||
requires = ['setuptools>=43', 'wheel>=0.31', 'Cython']
|
||||
build-backend = "setuptools.build_meta"
|
||||
build-backend = 'setuptools.build_meta'
|
||||
|
|
11
setup.cfg
11
setup.cfg
|
@ -1,13 +1,14 @@
|
|||
[metadata]
|
||||
name = palace
|
||||
version = 0.0.10
|
||||
url = https://github.com/McSinyx/palace
|
||||
version = 0.2.2
|
||||
url = https://mcsinyx.github.io/palace
|
||||
author = Nguyễn Gia Phong
|
||||
author_email = vn.mcsinyx@gmail.com
|
||||
author_email = mcsinyx@disroot.org
|
||||
classifiers =
|
||||
Development Status :: 2 - Pre-Alpha
|
||||
Development Status :: 4 - Beta
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
||||
Operating System :: MacOS
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: C++
|
||||
Programming Language :: Cython
|
||||
|
@ -19,7 +20,7 @@ classifiers =
|
|||
Topic :: Software Development :: Libraries
|
||||
Typing :: Typed
|
||||
license = LGPLv3+
|
||||
license_file = LICENSE
|
||||
license_files = LICENSE
|
||||
description = Pythonic Audio Library and Codecs Environment
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
|
|
61
setup.py
61
setup.py
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
# setup script
|
||||
# Copyright (C) 2019, 2020 Nguyễn Gia Phong
|
||||
# Copyright (C) 2020 Francesco Caliumi
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
|
@ -18,16 +19,34 @@
|
|||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
from distutils import log
|
||||
from distutils.command.clean import clean
|
||||
from distutils.dir_util import mkpath
|
||||
from distutils.errors import DistutilsExecError, DistutilsFileError
|
||||
from distutils.file_util import copy_file
|
||||
from operator import methodcaller
|
||||
from os import environ, unlink
|
||||
from os.path import dirname, join
|
||||
from subprocess import DEVNULL, PIPE, run
|
||||
from platform import system
|
||||
from subprocess import DEVNULL, PIPE, run, CalledProcessError
|
||||
|
||||
from Cython.Build import cythonize
|
||||
from setuptools import setup, Extension
|
||||
from setuptools.command.build_ext import build_ext
|
||||
|
||||
CPPSTD = '/std:c++14' if system() == 'Windows' else '-std=c++14'
|
||||
try:
|
||||
TRACE = int(environ['CYTHON_TRACE'])
|
||||
except KeyError:
|
||||
TRACE = 0
|
||||
except ValueError:
|
||||
TRACE = 0
|
||||
|
||||
|
||||
def src(file: str) -> str:
|
||||
"""Return path to the given file in src."""
|
||||
return join(dirname(__file__), 'src', file)
|
||||
|
||||
|
||||
class BuildAlure2Ext(build_ext):
|
||||
"""Builder of extensions linked to alure2."""
|
||||
|
@ -37,10 +56,15 @@ class BuildAlure2Ext(build_ext):
|
|||
"""
|
||||
super().finalize_options()
|
||||
mkpath(self.build_temp)
|
||||
copy_file(join(dirname(__file__), 'CMakeLists.txt'),
|
||||
self.build_temp)
|
||||
cmake = run(['cmake', '.'], check=True, stdout=DEVNULL, stderr=PIPE,
|
||||
cwd=self.build_temp, universal_newlines=True)
|
||||
copy_file(join(dirname(__file__), 'CMakeLists.txt'), self.build_temp)
|
||||
try:
|
||||
cmake = run(
|
||||
['cmake', '.'], check=True, stdout=DEVNULL, stderr=PIPE,
|
||||
cwd=self.build_temp, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
log.error(e.stderr.strip())
|
||||
raise DistutilsExecError(str(e))
|
||||
|
||||
for key, value in map(methodcaller('groups'),
|
||||
re.finditer(r'^alure2_(\w*)=(.*)$',
|
||||
cmake.stderr, re.MULTILINE)):
|
||||
|
@ -48,10 +72,25 @@ class BuildAlure2Ext(build_ext):
|
|||
getattr(ext, key).extend(value.split(';'))
|
||||
|
||||
|
||||
setup(cmdclass={'build_ext': BuildAlure2Ext},
|
||||
class CleanCppToo(clean):
|
||||
"""Clean command that remove Cython C++ outputs."""
|
||||
def run(self) -> None:
|
||||
"""Remove Cython C++ outputs on clean command."""
|
||||
for cpp in [src('palace.cpp')]:
|
||||
log.info(f'removing {cpp!r}')
|
||||
try:
|
||||
unlink(cpp)
|
||||
except OSError as e:
|
||||
raise DistutilsFileError(
|
||||
f'could not delete {cpp!r}: {e.strerror}')
|
||||
super().run()
|
||||
|
||||
|
||||
setup(cmdclass=dict(build_ext=BuildAlure2Ext, clean=CleanCppToo),
|
||||
ext_modules=cythonize(
|
||||
Extension(name='palace', sources=[join('src', 'palace.pyx')],
|
||||
language='c++', define_macros=[('CYTHON_TRACE', 1)]),
|
||||
compiler_directives=dict(language_level='3str', c_string_type='str',
|
||||
c_string_encoding='utf8', linetrace=True,
|
||||
binding=False, embedsignature=True)))
|
||||
Extension(name='palace', sources=[src('palace.pyx')],
|
||||
define_macros=[('CYTHON_TRACE', TRACE)],
|
||||
extra_compile_args=[CPPSTD], language='c++'),
|
||||
compiler_directives=dict(
|
||||
binding=True, linetrace=TRACE, language_level='3str',
|
||||
c_string_type='str', c_string_encoding='utf8')))
|
||||
|
|
288
src/alure.pxd
288
src/alure.pxd
|
@ -20,12 +20,12 @@
|
|||
|
||||
from libc.stdint cimport int64_t, uint64_t
|
||||
from libcpp cimport bool as boolean, nullptr_t
|
||||
from libcpp.memory cimport shared_ptr
|
||||
from libcpp.memory cimport shared_ptr, unique_ptr
|
||||
from libcpp.string cimport string
|
||||
from libcpp.utility cimport pair
|
||||
from libcpp.vector cimport vector
|
||||
|
||||
from std cimport duration, nanoseconds, milliseconds, shared_future
|
||||
from std cimport duration, nanoseconds, milliseconds, streambuf
|
||||
|
||||
|
||||
# OpenAL and Alure auxiliary declarations
|
||||
|
@ -33,17 +33,90 @@ cdef extern from 'alc.h' nogil:
|
|||
cdef int ALC_FALSE
|
||||
cdef int ALC_TRUE
|
||||
|
||||
cdef int ALC_FREQUENCY
|
||||
|
||||
cdef int ALC_MONO_SOURCES
|
||||
cdef int ALC_STEREO_SOURCES
|
||||
|
||||
|
||||
cdef extern from 'efx.h' nogil:
|
||||
cdef int ALC_MAX_AUXILIARY_SENDS
|
||||
|
||||
|
||||
cdef extern from 'alure2-alext.h' nogil:
|
||||
cdef int ALC_FORMAT_CHANNELS_SOFT
|
||||
cdef int ALC_MONO_SOFT
|
||||
cdef int ALC_STEREO_SOFT
|
||||
cdef int ALC_QUAD_SOFT
|
||||
cdef int ALC_5POINT1_SOFT
|
||||
cdef int ALC_6POINT1_SOFT
|
||||
cdef int ALC_7POINT1_SOFT
|
||||
|
||||
cdef int ALC_FORMAT_TYPE_SOFT
|
||||
cdef int ALC_BYTE_SOFT
|
||||
cdef int ALC_UNSIGNED_BYTE_SOFT
|
||||
cdef int ALC_SHORT_SOFT
|
||||
cdef int ALC_UNSIGNED_SHORT_SOFT
|
||||
cdef int ALC_INT_SOFT
|
||||
cdef int ALC_UNSIGNED_INT_SOFT
|
||||
cdef int ALC_FLOAT_SOFT
|
||||
|
||||
cdef int ALC_HRTF_SOFT
|
||||
cdef int ALC_DONT_CARE_SOFT
|
||||
cdef int ALC_HRTF_ID_SOFT
|
||||
|
||||
cdef int ALC_OUTPUT_LIMITER_SOFT
|
||||
|
||||
|
||||
cdef extern from 'alure2-aliases.h' namespace 'alure' nogil:
|
||||
ctypedef duration[double] Seconds
|
||||
|
||||
|
||||
cdef extern from 'alure2-typeviews.h' namespace 'alure' nogil:
|
||||
cdef cppclass ArrayView[T]:
|
||||
const T* begin() except +
|
||||
const T* end() except +
|
||||
|
||||
cdef cppclass StringView:
|
||||
StringView(string) except +
|
||||
|
||||
|
||||
# Alure main module
|
||||
cdef extern from 'alure2.h' nogil:
|
||||
cdef cppclass EFXEAXREVERBPROPERTIES:
|
||||
float density 'flDensity'
|
||||
float diffusion 'flDiffusion'
|
||||
float gain 'flGain'
|
||||
float gain_hf 'flGainHF'
|
||||
float gain_lf 'flGainLF'
|
||||
float decay_time 'flDecayTime'
|
||||
float decay_hf_ratio 'flDecayHFRatio'
|
||||
float decay_lf_ratio 'flDecayLFRatio'
|
||||
float reflections_gain 'flReflectionsGain'
|
||||
float reflections_delay 'flReflectionsDelay'
|
||||
float reflections_pan 'flReflectionsPan'[3]
|
||||
float late_reverb_gain 'flLateReverbGain'
|
||||
float late_reverb_delay 'flLateReverbDelay'
|
||||
float late_reverb_pan 'flLateReverbPan'[3]
|
||||
float echo_time 'flEchoTime'
|
||||
float echo_depth 'flEchoDepth'
|
||||
float modulation_time 'flModulationTime'
|
||||
float modulation_depth 'flModulationDepth'
|
||||
float air_absorption_gain_hf 'flAirAbsorptionGainHF'
|
||||
float hf_reference 'flHFReference'
|
||||
float lf_reference 'flLFReference'
|
||||
float room_rolloff_factor 'flRoomRolloffFactor'
|
||||
bint decay_hf_limit 'iDecayHFLimit'
|
||||
|
||||
cdef cppclass EFXCHORUSPROPERTIES:
|
||||
bint waveform 'iWaveform'
|
||||
int phase 'iPhase'
|
||||
float rate 'flRate'
|
||||
float depth 'flDepth'
|
||||
float feedback 'flFeedback'
|
||||
float delay 'flDelay'
|
||||
|
||||
|
||||
cdef extern from 'alure2.h' namespace 'alure' nogil:
|
||||
# Type aliases:
|
||||
# char*: string
|
||||
|
@ -56,13 +129,10 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
# String: string
|
||||
# StringView: string
|
||||
# SharedPtr: shared_ptr
|
||||
# SharedFuture: shared_future
|
||||
|
||||
# Structs:
|
||||
cdef cppclass AttributePair:
|
||||
int attribute 'mAttribute'
|
||||
int value 'mValue'
|
||||
|
||||
pass
|
||||
cdef cppclass FilterParams:
|
||||
pass
|
||||
|
||||
|
@ -71,22 +141,10 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
unsigned send 'mSend'
|
||||
|
||||
# Enum classes:
|
||||
cdef enum SampleType:
|
||||
UInt8 'alure::SampleType::UInt8' # Unsigned 8-bit
|
||||
Int16 'alure::SampleType::Int16' # Signed 16-bit
|
||||
Float32 'alure::SampleType::Float32' # 32-bit float
|
||||
Mulaw 'alure::SampleType::Mulaw' # Mulaw
|
||||
|
||||
cdef enum ChannelConfig:
|
||||
Mono 'alure::ChannelConfig::Mono' # Mono
|
||||
Stereo 'alure::ChannelConfig::Stereo' # Stereo
|
||||
Rear 'alure::ChannelConfig::Rear' # Rear
|
||||
Quad 'alure::ChannelConfig::Quad' # Quadrophonic
|
||||
X51 'alure::ChannelConfig::X51' # 5.1 Surround
|
||||
X61 'alure::ChannelConfig::X61' # 6.1 Surround
|
||||
X71 'alure::ChannelConfig::X71' # 7.1 Surround
|
||||
BFormat2D 'alure::ChannelConfig::BFormat2D' # B-Format 2D
|
||||
BFormat3D 'alure::ChannelConfig::BFormat3D' # B-Format 3D
|
||||
ctypedef enum SampleType:
|
||||
pass
|
||||
ctypedef enum ChannelConfig:
|
||||
pass
|
||||
|
||||
# The following relies on C++ implicit conversion from char* to string.
|
||||
cdef const string get_sample_type_name 'GetSampleTypeName'(SampleType) except +
|
||||
|
@ -94,24 +152,24 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
cdef unsigned frames_to_bytes 'FramesToBytes'(unsigned, ChannelConfig, SampleType) except +
|
||||
cdef unsigned bytes_to_frames 'BytesToFrames'(unsigned, ChannelConfig, SampleType)
|
||||
|
||||
cdef enum DeviceEnumeration:
|
||||
ctypedef enum DeviceEnumeration:
|
||||
Basic 'alure::DeviceEnumeration::Basic'
|
||||
Full 'alure::DeviceEnumeration::Full'
|
||||
Capture 'alure::DeviceEnumeration::Capture'
|
||||
|
||||
cdef enum DefaultDeviceType:
|
||||
ctypedef enum DefaultDeviceType:
|
||||
Basic 'alure::DefaultDeviceType::Basic'
|
||||
Full 'alure::DefaultDeviceType::Full'
|
||||
Capture 'alure::DefaultDeviceType::Capture'
|
||||
|
||||
cdef enum PlaybackName:
|
||||
ctypedef enum PlaybackName:
|
||||
Basic 'alure::PlaybackName::Basic'
|
||||
Full 'alure::PlaybackName::Full'
|
||||
|
||||
cdef cppclass DistanceModel:
|
||||
ctypedef enum DistanceModel:
|
||||
pass
|
||||
|
||||
cdef enum Spatialize:
|
||||
ctypedef enum Spatialize:
|
||||
Off 'alure::Spatialize::Off'
|
||||
On 'alure::Spatialize::On'
|
||||
Auto 'alure::Spatialize::Auto'
|
||||
|
@ -150,48 +208,28 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
cdef cppclass DeviceManager:
|
||||
@staticmethod
|
||||
DeviceManager get_instance 'getInstance'() except +
|
||||
|
||||
DeviceManager() # nil
|
||||
DeviceManager(const DeviceManager&)
|
||||
DeviceManager(DeviceManager&&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
boolean query_extension 'queryExtension'(const string&) except +
|
||||
|
||||
vector[string] enumerate(DeviceEnumeration) except +
|
||||
string default_device_name 'defaultDeviceName'(DefaultDeviceType) except +
|
||||
|
||||
Device open_playback 'openPlayback'() except +
|
||||
Device open_playback 'openPlayback'(const string&) except +
|
||||
|
||||
cdef cppclass Device:
|
||||
ctypedef DeviceImpl* handle_type
|
||||
|
||||
Device() # nil
|
||||
Device(DeviceImpl*)
|
||||
Device(const Device&)
|
||||
Device(Device&&)
|
||||
|
||||
Device& operator=(const Device&)
|
||||
Device& operator=(Device&&)
|
||||
|
||||
boolean operator==(const Device&)
|
||||
boolean operator!=(const Device&)
|
||||
boolean operator<=(const Device&)
|
||||
boolean operator>=(const Device&)
|
||||
boolean operator<(const Device&)
|
||||
boolean operator>(const Device&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
|
||||
string get_name 'getName'() except +
|
||||
string get_name 'getName'(PlaybackName) except +
|
||||
|
||||
boolean query_extension 'queryExtension'(const string&) except +
|
||||
|
||||
Version get_alc_version 'getALCVersion'() except +
|
||||
Version get_efx_version 'getEFXVersion'() except +
|
||||
|
||||
|
@ -215,34 +253,24 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
void close() except +
|
||||
|
||||
cdef cppclass Context:
|
||||
ctypedef ContextImpl* handle_type
|
||||
|
||||
Context() # nil
|
||||
Context(ContextImpl*)
|
||||
Context(const Context&)
|
||||
Context(Context&&)
|
||||
|
||||
Context& operator=(const Context&)
|
||||
Context& operator=(Context&&)
|
||||
|
||||
boolean operator==(const Context&)
|
||||
boolean operator!=(const Context&)
|
||||
boolean operator<=(const Context&)
|
||||
boolean operator>=(const Context&)
|
||||
boolean operator<(const Context&)
|
||||
boolean operator>(const Context&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
|
||||
@staticmethod
|
||||
void make_current 'MakeCurrent'(Context) except +
|
||||
|
||||
@staticmethod
|
||||
Context get_current 'GetCurrent'() except +
|
||||
|
||||
@staticmethod
|
||||
void make_thread_current 'MakeThreadCurrent'(Context) except +
|
||||
|
||||
@staticmethod
|
||||
Context get_thread_current 'GetThreadCurrent'() except +
|
||||
|
||||
|
@ -256,6 +284,7 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
Listener get_listener 'getListener'() except +
|
||||
|
||||
shared_ptr[MessageHandler] set_message_handler 'setMessageHandler'(shared_ptr[MessageHandler]) except +
|
||||
shared_ptr[MessageHandler] get_message_handler 'getMessageHandler'() except +
|
||||
|
||||
void set_async_wake_interval 'setAsyncWakeInterval'(milliseconds) except +
|
||||
milliseconds get_async_wake_interval 'getAsyncWakeInterval'() except +
|
||||
|
@ -264,20 +293,12 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
|
||||
boolean is_supported 'isSupported'(ChannelConfig, SampleType) except +
|
||||
|
||||
vector[string] get_available_resamplers 'getAvailableResamplers'() except +
|
||||
ArrayView[string] get_available_resamplers 'getAvailableResamplers'() except +
|
||||
int get_default_resampler_index 'getDefaultResamplerIndex'() except +
|
||||
|
||||
Buffer get_buffer 'getBuffer'(string) except +
|
||||
shared_future[Buffer] get_buffer_async 'getBufferAsync'(string) except +
|
||||
|
||||
void precache_buffers_async 'precacheBuffersAsync'(vector[string]) except +
|
||||
|
||||
void precache_buffers_async 'precacheBuffersAsync'(vector[StringView]) except +
|
||||
Buffer create_buffer_from 'createBufferFrom'(string, shared_ptr[Decoder]) except +
|
||||
shared_future[Buffer] create_buffer_async_from 'createBufferAsyncFrom'(string, shared_ptr[Decoder]) except +
|
||||
|
||||
Buffer find_buffer 'findBuffer'(string) except +
|
||||
shared_future[Buffer] find_buffer_async 'findBufferAsync'(string) except +
|
||||
|
||||
void remove_buffer 'removeBuffer'(string) except +
|
||||
void remove_buffer 'removeBuffer'(Buffer) except +
|
||||
|
||||
|
@ -293,63 +314,25 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
void update() except +
|
||||
|
||||
cdef cppclass Listener:
|
||||
ctypedef ListenerImpl* handle_type
|
||||
|
||||
Listener() # nil
|
||||
Listener(ListenerImpl*)
|
||||
Listener(const Listener&)
|
||||
Listener(Listener&&)
|
||||
|
||||
Listener& operator=(const Listener&)
|
||||
Listener& operator=(Listener&&)
|
||||
|
||||
boolean operator==(const Listener&)
|
||||
boolean operator!=(const Listener&)
|
||||
boolean operator<=(const Listener&)
|
||||
boolean operator>=(const Listener&)
|
||||
boolean operator<(const Listener&)
|
||||
boolean operator>(const Listener&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
|
||||
float set_gain 'setGain'(float) except +
|
||||
float set_3d_parameters 'set3DParameters'(const Vector3&, const Vector3&, const Vector3&) except +
|
||||
void set_position 'setPosition'(const Vector3 &) except +
|
||||
void set_position 'setPosition'(const float*) except +
|
||||
|
||||
void set_velocity 'setVelocity'(const Vector3&) except +
|
||||
void set_velocity 'setVelocity'(const float*) except +
|
||||
|
||||
void set_orientation 'setOrientation'(const pair[Vector3, Vector3]&) except +
|
||||
void set_orientation 'setOrientation'(const float*, const float*) except +
|
||||
void set_orientation 'setOrientation'(const float*) except +
|
||||
|
||||
void set_meters_per_unit 'setMetersPerUnit'(float) except +
|
||||
|
||||
cdef cppclass Buffer:
|
||||
ctypedef BufferImpl* handle_type
|
||||
|
||||
Buffer() # nil
|
||||
Buffer(BufferImpl*)
|
||||
Buffer(const Buffer&)
|
||||
Buffer(Buffer&&)
|
||||
|
||||
Buffer& operator=(const Buffer&)
|
||||
Buffer& operator=(Buffer&&)
|
||||
|
||||
boolean operator==(const Buffer&)
|
||||
boolean operator!=(const Buffer&)
|
||||
boolean operator<=(const Buffer&)
|
||||
boolean operator>=(const Buffer&)
|
||||
boolean operator<(const Buffer&)
|
||||
boolean operator>(const Buffer&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
|
||||
unsigned get_length 'getLength'() except +
|
||||
unsigned get_frequency 'getFrequency'() except +
|
||||
ChannelConfig get_channel_config 'getChannelConfig'() except +
|
||||
|
@ -357,45 +340,29 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
unsigned get_size 'getSize'() except +
|
||||
size_t get_source_count 'getSourceCount'() except +
|
||||
vector[Source] get_sources 'getSources'() except +
|
||||
# name is implemented as a read-only attribute in Cython
|
||||
pair[unsigned, unsigned] get_loop_points 'getLoopPoints'() except +
|
||||
void set_loop_points 'setLoopPoints'(unsigned, unsigned) except +
|
||||
|
||||
cdef cppclass Source:
|
||||
ctypedef SourceImpl* handle_type
|
||||
|
||||
Source() # nil
|
||||
Source(SourceImpl*)
|
||||
Source(const Source&)
|
||||
Source(Source&&)
|
||||
|
||||
Source& operator=(const Source&)
|
||||
Source& operator=(Source&&)
|
||||
|
||||
boolean operator==(const Source&)
|
||||
boolean operator!=(const Source&)
|
||||
boolean operator<=(const Source&)
|
||||
boolean operator>=(const Source&)
|
||||
boolean operator<(const Source&)
|
||||
boolean operator>(const Source&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
|
||||
void play(Buffer) except +
|
||||
void play(shared_ptr[Decoder], int, int) except +
|
||||
void play(shared_future[Buffer]) except +
|
||||
|
||||
void stop() except +
|
||||
void fade_out_to_stop 'fadeOutToStop'(float, milliseconds) except +
|
||||
void pause() except +
|
||||
void resume() except +
|
||||
|
||||
boolean is_pending 'isPending'() except +
|
||||
boolean is_playing 'isPlaying'() except +
|
||||
boolean is_paused 'isPaused'() except +
|
||||
boolean is_playing_or_pending 'isPlayingOrPending'() except +
|
||||
|
||||
void set_group 'setGroup'(SourceGroup) except +
|
||||
SourceGroup get_group 'getGroup'() except +
|
||||
|
@ -419,50 +386,30 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
float get_gain 'getGain'() except +
|
||||
void set_gain_range 'setGainRange'(float, float) except +
|
||||
pair[float, float] get_gain_range 'getGainRange'() except +
|
||||
float get_min_gain 'getMinGain'() except +
|
||||
float get_max_gain 'getMaxGain'() except +
|
||||
|
||||
void set_distance_range 'setDistanceRange'(float, float) except +
|
||||
pair[float, float] get_distance_range 'getDistanceRange'() except +
|
||||
float get_reference_distance 'getReferenceDistance'() except +
|
||||
float get_max_distance 'getMaxDistance'() except +
|
||||
|
||||
void set_3d_parameters 'set3DParameters'(const Vector3&, const Vector3&, const Vector3&) except +
|
||||
void set_3d_parameters 'set3DParameters'(const Vector3&, const Vector3&, const pair[Vector3, Vector3]&) except +
|
||||
|
||||
void set_position 'setPosition'(const Vector3&) except +
|
||||
void set_position 'setPosition'(const float*) except +
|
||||
Vector3 get_position 'getPosition'() except +
|
||||
|
||||
void set_velocity 'setVelocity'(const Vector3&) except +
|
||||
void set_velocity 'setVelocity'(const float*) except +
|
||||
Vector3 get_velocity 'getVelocity'() except +
|
||||
|
||||
void set_direction 'setDirection'(const Vector3&) except +
|
||||
void set_direction 'setDirection'(const float*) except +
|
||||
Vector3 get_direction 'getDirection'() except +
|
||||
|
||||
void set_orientation 'setOrientation'(const pair[Vector3, Vector3]&) except +
|
||||
void set_orientation 'setOrientation'(const float*, const float*) except +
|
||||
void set_orientation 'setOrientation'(const float*) except +
|
||||
pair[Vector3, Vector3] get_orientation 'getOrientation'() except +
|
||||
|
||||
void set_cone_angles 'setConeAngles'(float, float) except +
|
||||
pair[float, float] get_cone_angles 'getConeAngles'() except +
|
||||
float get_inner_cone_angle 'getInnerConeAngle'() except +
|
||||
float get_outer_cone_angle 'getOuterConeAngle'() except +
|
||||
|
||||
void set_outer_cone_gains 'setOuterConeGains'(float) except +
|
||||
void set_outer_cone_gains 'setOuterConeGains'(float, float) except +
|
||||
pair[float, float] get_outer_cone_gains 'getOuterConeGains'() except +
|
||||
float get_outer_cone_gain 'getOuterConeGain'() except +
|
||||
float get_outer_cone_gainhf 'getOuterConeGainHF'() except +
|
||||
|
||||
void set_rolloff_factors 'setRolloffFactors'(float) except +
|
||||
void set_rolloff_factors 'setRolloffFactors'(float, float) except +
|
||||
pair[float, float] get_rolloff_factors 'getRolloffFactors'() except +
|
||||
float get_rolloff_factor 'getRolloffFactor'() except +
|
||||
float get_room_rolloff_factor 'getRoomRolloffFactor'() except +
|
||||
|
||||
void set_doppler_factor 'setDopplerFactor'(float) except +
|
||||
float get_doppler_factor 'getDopplerFactor'() except +
|
||||
|
@ -499,26 +446,15 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
void destroy() except +
|
||||
|
||||
cdef cppclass SourceGroup:
|
||||
ctypedef SourceImpl* handle_type
|
||||
|
||||
SourceGroup() # nil
|
||||
SourceGroup(SourceGroupImpl*)
|
||||
SourceGroup(const SourceGroup&)
|
||||
SourceGroup(SourceGroup&&)
|
||||
|
||||
SourceGroup& operator=(const SourceGroup&)
|
||||
SourceGroup& operator=(SourceGroup&&)
|
||||
|
||||
boolean operator==(const SourceGroup&)
|
||||
boolean operator!=(const SourceGroup&)
|
||||
boolean operator<=(const SourceGroup&)
|
||||
boolean operator>=(const SourceGroup&)
|
||||
boolean operator<(const SourceGroup&)
|
||||
boolean operator>(const SourceGroup&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
void set_parent_group 'setParentGroup'(SourceGroup) except +
|
||||
SourceGroup get_parent_group 'getParentGroup'() except +
|
||||
|
||||
|
@ -538,27 +474,15 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
void destroy() except +
|
||||
|
||||
cdef cppclass AuxiliaryEffectSlot:
|
||||
ctypedef AuxiliaryEffectSlotImpl* handle_type
|
||||
|
||||
AuxiliaryEffectSlot() # nil
|
||||
AuxiliaryEffectSlot(AuxiliaryEffectSlotImpl*)
|
||||
AuxiliaryEffectSlot(const AuxiliaryEffectSlot&)
|
||||
AuxiliaryEffectSlot(AuxiliaryEffectSlot&&)
|
||||
|
||||
AuxiliaryEffectSlot& operator=(const AuxiliaryEffectSlot&)
|
||||
AuxiliaryEffectSlot& operator=(AuxiliaryEffectSlot&&)
|
||||
|
||||
boolean operator==(const AuxiliaryEffectSlot&)
|
||||
boolean operator!=(const AuxiliaryEffectSlot&)
|
||||
boolean operator<=(const AuxiliaryEffectSlot&)
|
||||
boolean operator>=(const AuxiliaryEffectSlot&)
|
||||
boolean operator<(const AuxiliaryEffectSlot&)
|
||||
boolean operator>(const AuxiliaryEffectSlot&)
|
||||
|
||||
boolean operator bool()
|
||||
|
||||
handle_type get_handle 'getHandle'()
|
||||
|
||||
void set_gain 'setGain'(float) except +
|
||||
void set_send_auto 'setSendAuto'(bool) except +
|
||||
void apply_effect 'applyEffect'(Effect) except +
|
||||
|
@ -568,33 +492,49 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
|
|||
size_t get_use_count 'getUseCount'() except +
|
||||
|
||||
cdef cppclass Effect:
|
||||
pass
|
||||
Effect() # nil
|
||||
boolean operator==(const Effect&)
|
||||
boolean operator!=(const Effect&)
|
||||
boolean operator<=(const Effect&)
|
||||
boolean operator>=(const Effect&)
|
||||
boolean operator<(const Effect&)
|
||||
boolean operator>(const Effect&)
|
||||
boolean operator bool()
|
||||
|
||||
void set_reverb_properties 'setReverbProperties'(const EFXEAXREVERBPROPERTIES&) except +
|
||||
void set_chorus_properties 'setChorusProperties'(const EFXCHORUSPROPERTIES&) except +
|
||||
void destroy() except +
|
||||
|
||||
cdef cppclass Decoder:
|
||||
int get_frequency 'getFrequency'()
|
||||
ChannelConfig get_channel_config 'getChannelConfig'()
|
||||
SampleType get_sample_type 'getSampleType'()
|
||||
|
||||
uint64_t get_length 'getLength'()
|
||||
boolean seek(uint64_t)
|
||||
|
||||
pair[uint64_t, uint64_t] get_loop_points 'getLoopPoints'()
|
||||
|
||||
int read(void*, int)
|
||||
|
||||
|
||||
cdef cppclass DecoderFactory:
|
||||
pass
|
||||
|
||||
|
||||
cdef cppclass FileIOFactory:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
unique_ptr[FileIOFactory] set(unique_ptr[FileIOFactory])
|
||||
|
||||
@staticmethod
|
||||
FileIOFactory& get()
|
||||
|
||||
cdef cppclass MessageHandler:
|
||||
pass
|
||||
|
||||
|
||||
# GIL is needed for operations with Python objects.
|
||||
cdef extern from 'bases.h' namespace 'palace':
|
||||
cdef cppclass BaseStreamBuf(streambuf):
|
||||
pass
|
||||
cdef cppclass BaseDecoder(Decoder):
|
||||
pass
|
||||
cdef cppclass BaseFileIOFactory(FileIOFactory):
|
||||
pass
|
||||
cdef cppclass BaseMessageHandler(MessageHandler):
|
||||
pass
|
||||
|
|
202
src/bases.h
202
src/bases.h
|
@ -20,106 +20,142 @@
|
|||
#define PALACE_BASES_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <ios>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <streambuf>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "alure2.h"
|
||||
|
||||
// Due to the lack of support for noexcept keyword in Cython, base classes
|
||||
// created to work around the looser throw specifier error in C++.
|
||||
namespace palace {
|
||||
|
||||
class BaseDecoder : public alure::Decoder {
|
||||
public:
|
||||
virtual unsigned get_frequency_() const = 0;
|
||||
inline ALuint
|
||||
getFrequency() const noexcept override
|
||||
namespace palace
|
||||
{
|
||||
// Work around exotic standard type definitions Cython cannot handle
|
||||
class BaseStreamBuf : public std::streambuf
|
||||
{
|
||||
return get_frequency_();
|
||||
}
|
||||
protected:
|
||||
virtual size_t seek (long long offset, int whence = 0) = 0;
|
||||
|
||||
virtual alure::ChannelConfig get_channel_config_() const = 0;
|
||||
inline alure::ChannelConfig
|
||||
getChannelConfig() const noexcept override
|
||||
inline pos_type
|
||||
seekoff (off_type off, std::ios_base::seekdir way,
|
||||
std::ios_base::openmode
|
||||
which = std::ios_base::in|std::ios_base::out) override
|
||||
{
|
||||
switch (way)
|
||||
{
|
||||
case std::ios_base::beg:
|
||||
return seek (off, 0);
|
||||
case std::ios_base::cur:
|
||||
return seek (off, 1);
|
||||
case std::ios_base::end:
|
||||
return seek (off, 2);
|
||||
default:
|
||||
return off_type (-1);
|
||||
}
|
||||
}
|
||||
|
||||
inline pos_type
|
||||
seekpos (pos_type sp,
|
||||
std::ios_base::openmode
|
||||
which = std::ios_base::in|std::ios_base::out) override
|
||||
{ return seek (sp); }
|
||||
|
||||
inline int sync() override
|
||||
{
|
||||
if (gptr() && gptr() < egptr())
|
||||
seek (gptr() - egptr(), 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline std::streamsize showmanyc() override
|
||||
{ return (underflow() == traits_type::eof()) ? -1 : egptr() - gptr(); }
|
||||
};
|
||||
|
||||
// Work around throw specifier (noexcept) and exotic types
|
||||
// that cannot be handled prettily in Cython
|
||||
class BaseDecoder : public alure::Decoder
|
||||
{
|
||||
return get_channel_config_();
|
||||
}
|
||||
public:
|
||||
virtual unsigned get_frequency_() const = 0;
|
||||
inline ALuint
|
||||
getFrequency() const noexcept override
|
||||
{ return get_frequency_(); }
|
||||
|
||||
virtual alure::SampleType get_sample_type_() const = 0;
|
||||
inline alure::SampleType
|
||||
getSampleType() const noexcept override
|
||||
virtual alure::ChannelConfig get_channel_config_() const = 0;
|
||||
inline alure::ChannelConfig
|
||||
getChannelConfig() const noexcept override { return get_channel_config_(); }
|
||||
|
||||
virtual alure::SampleType get_sample_type_() const = 0;
|
||||
inline alure::SampleType
|
||||
getSampleType() const noexcept override { return get_sample_type_(); }
|
||||
|
||||
virtual uint64_t get_length_() const = 0;
|
||||
inline uint64_t
|
||||
getLength() const noexcept override { return get_length_(); }
|
||||
|
||||
virtual bool seek_ (uint64_t pos) = 0;
|
||||
inline bool seek (uint64_t pos) noexcept override { return seek_ (pos); }
|
||||
|
||||
virtual std::pair<uint64_t,uint64_t> get_loop_points_() const = 0;
|
||||
inline std::pair<uint64_t,uint64_t>
|
||||
getLoopPoints() const noexcept override { return get_loop_points_(); }
|
||||
|
||||
virtual unsigned read_ (void* ptr, unsigned count) = 0;
|
||||
inline ALuint
|
||||
read (ALvoid* ptr, ALuint count) noexcept override
|
||||
{ return read_ (ptr, count); }
|
||||
};
|
||||
|
||||
// Work around throw specifier Cython cannot handle (noexcept)
|
||||
class BaseFileIOFactory : public alure::FileIOFactory
|
||||
{
|
||||
return get_sample_type_();
|
||||
}
|
||||
public:
|
||||
virtual std::unique_ptr<std::istream>
|
||||
open_file(const std::string &name) = 0;
|
||||
inline alure::UniquePtr<std::istream>
|
||||
openFile(const alure::String &name) noexcept override
|
||||
{ return open_file (name); }
|
||||
};
|
||||
|
||||
virtual uint64_t get_length_() const = 0;
|
||||
inline uint64_t getLength() const noexcept override { return get_length_(); }
|
||||
|
||||
virtual bool seek_ (uint64_t pos) = 0;
|
||||
inline bool seek (uint64_t pos) noexcept override { return seek_ (pos); }
|
||||
|
||||
virtual std::pair<uint64_t,uint64_t> get_loop_points_() const = 0;
|
||||
inline std::pair<uint64_t,uint64_t>
|
||||
getLoopPoints() const noexcept override
|
||||
// Work around throw specifier Cython cannot handle (noexcept)
|
||||
class BaseMessageHandler : public alure::MessageHandler
|
||||
{
|
||||
return get_loop_points_();
|
||||
}
|
||||
public:
|
||||
virtual void device_disconnected (alure::Device& device) = 0;
|
||||
inline void
|
||||
deviceDisconnected (alure::Device device) noexcept override
|
||||
{ device_disconnected (device); }
|
||||
|
||||
virtual unsigned read_ (void* ptr, unsigned count) = 0;
|
||||
inline ALuint
|
||||
read (ALvoid* ptr, ALuint count) noexcept override
|
||||
{
|
||||
return read_ (ptr, count);
|
||||
}
|
||||
};
|
||||
virtual void source_stopped (alure::Source& source) = 0;
|
||||
inline void
|
||||
sourceStopped (alure::Source source) noexcept override
|
||||
{ source_stopped (source); }
|
||||
|
||||
class BaseMessageHandler : public alure::MessageHandler {
|
||||
public:
|
||||
virtual void device_disconnected (alure::Device device) = 0;
|
||||
inline void
|
||||
deviceDisconnected (alure::Device device) noexcept override
|
||||
{
|
||||
device_disconnected (device);
|
||||
}
|
||||
virtual void source_force_stopped (alure::Source& source) = 0;
|
||||
inline void
|
||||
sourceForceStopped (alure::Source source) noexcept override
|
||||
{ source_force_stopped (source); }
|
||||
|
||||
virtual void source_stopped (alure::Source source) = 0;
|
||||
inline void
|
||||
sourceStopped (alure::Source source) noexcept override
|
||||
{
|
||||
source_stopped (source);
|
||||
}
|
||||
|
||||
virtual void source_force_stopped (alure::Source source) = 0;
|
||||
inline void
|
||||
sourceForceStopped (alure::Source source) noexcept override
|
||||
{
|
||||
source_force_stopped (source);
|
||||
}
|
||||
|
||||
virtual void buffer_loading (std::string name, std::string channel_config,
|
||||
std::string sample_type, unsigned sample_rate,
|
||||
std::vector<signed char> data) = 0;
|
||||
inline void
|
||||
bufferLoading (alure::StringView name, alure::ChannelConfig channels,
|
||||
alure::SampleType type, ALuint samplerate,
|
||||
alure::ArrayView<ALbyte> data) noexcept override
|
||||
{
|
||||
std::vector<signed char> std_data (data.size());
|
||||
// FIXME: This defeats the entire point of alure::ArrayView.
|
||||
std::copy (data.begin(), data.end(), std_data.begin());
|
||||
buffer_loading (name.data(), alure::GetChannelConfigName (channels),
|
||||
alure::GetSampleTypeName (type), samplerate, std_data);
|
||||
}
|
||||
|
||||
virtual std::string resource_not_found (std::string name) = 0;
|
||||
inline alure::String
|
||||
resourceNotFound (alure::StringView name) noexcept override
|
||||
{
|
||||
return resource_not_found (name.data());
|
||||
}
|
||||
};
|
||||
virtual void buffer_loading (std::string name, std::string channel_config,
|
||||
std::string sample_type, unsigned sample_rate,
|
||||
const signed char* data, size_t size) = 0;
|
||||
inline void
|
||||
bufferLoading (alure::StringView name, alure::ChannelConfig channels,
|
||||
alure::SampleType type, ALuint samplerate,
|
||||
alure::ArrayView<ALbyte> data) noexcept override
|
||||
{
|
||||
buffer_loading (name.data(), alure::GetChannelConfigName (channels),
|
||||
alure::GetSampleTypeName (type), samplerate,
|
||||
data.begin(), data.size());
|
||||
}
|
||||
|
||||
virtual std::string resource_not_found (std::string name) = 0;
|
||||
inline alure::String
|
||||
resourceNotFound (alure::StringView name) noexcept override
|
||||
{ return resource_not_found (name.data()); }
|
||||
};
|
||||
} // namespace palace
|
||||
|
||||
#endif // PALACE_BASES_H
|
||||
|
|
2205
src/palace.pyx
2205
src/palace.pyx
File diff suppressed because it is too large
Load Diff
12
src/std.pxd
12
src/std.pxd
|
@ -18,6 +18,7 @@
|
|||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from libc.stdint cimport int64_t
|
||||
from libcpp cimport bool as boolean
|
||||
|
||||
|
||||
cdef extern from '<chrono>' namespace 'std::chrono' nogil:
|
||||
|
@ -31,9 +32,9 @@ cdef extern from '<chrono>' namespace 'std::chrono' nogil:
|
|||
ctypedef duration[int64_t, milli] milliseconds
|
||||
|
||||
|
||||
cdef extern from '<future>' namespace 'std' nogil:
|
||||
cdef cppclass shared_future[R]:
|
||||
pass
|
||||
cdef extern from '<iostream>' namespace 'std' nogil:
|
||||
cdef cppclass istream:
|
||||
istream(streambuf*) except +
|
||||
|
||||
|
||||
cdef extern from '<ratio>' namespace 'std' nogil:
|
||||
|
@ -41,3 +42,8 @@ cdef extern from '<ratio>' namespace 'std' nogil:
|
|||
pass
|
||||
cdef cppclass milli:
|
||||
pass
|
||||
|
||||
|
||||
cdef extern from '<streambuf>' namespace 'std' nogil:
|
||||
cdef cppclass streambuf:
|
||||
void setg(char*, char*, char*) except +
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
// Helper functions and mappings
|
||||
// Copyright (C) 2020 Nguyễn Gia Phong
|
||||
// Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
//
|
||||
// This file is part of palace.
|
||||
//
|
||||
// palace is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Lesser General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License,
|
||||
// or (at your option) any later version.
|
||||
//
|
||||
// palace is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#ifndef PALACE_UTIL_H
|
||||
#define PALACE_UTIL_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "alure2.h"
|
||||
#include "efx-presets.h"
|
||||
|
||||
namespace palace
|
||||
{
|
||||
const std::map<std::string, alure::SampleType> SAMPLE_TYPES {
|
||||
{"Unsigned 8-bit", alure::SampleType::UInt8},
|
||||
{"Signed 16-bit", alure::SampleType::Int16},
|
||||
{"32-bit float", alure::SampleType::Float32},
|
||||
{"Mulaw", alure::SampleType::Mulaw}};
|
||||
|
||||
const std::map<std::string, alure::ChannelConfig> CHANNEL_CONFIGS {
|
||||
{"Mono", alure::ChannelConfig::Mono},
|
||||
{"Stereo", alure::ChannelConfig::Stereo},
|
||||
{"Rear", alure::ChannelConfig::Rear},
|
||||
{"Quadrophonic", alure::ChannelConfig::Quad},
|
||||
{"5.1 Surround", alure::ChannelConfig::X51},
|
||||
{"6.1 Surround", alure::ChannelConfig::X61},
|
||||
{"7.1 Surround", alure::ChannelConfig::X71},
|
||||
{"B-Format 2D", alure::ChannelConfig::BFormat2D},
|
||||
{"B-Format 3D", alure::ChannelConfig::BFormat3D}};
|
||||
|
||||
const std::map<std::string, alure::DistanceModel> DISTANCE_MODELS {
|
||||
{"inverse clamped", alure::DistanceModel::InverseClamped},
|
||||
{"linear clamped", alure::DistanceModel::LinearClamped},
|
||||
{"exponent clamped", alure::DistanceModel::ExponentClamped},
|
||||
{"inverse", alure::DistanceModel::Inverse},
|
||||
{"linear", alure::DistanceModel::Linear},
|
||||
{"exponent", alure::DistanceModel::Exponent},
|
||||
{"none", alure::DistanceModel::None}};
|
||||
|
||||
// This is ported from alure-reverb example.
|
||||
#define DECL(x) { #x, EFX_REVERB_PRESET_##x }
|
||||
const std::map<std::string, EFXEAXREVERBPROPERTIES> REVERB_PRESETS {
|
||||
DECL(GENERIC), DECL(PADDEDCELL), DECL(ROOM), DECL(BATHROOM),
|
||||
DECL(LIVINGROOM), DECL(STONEROOM), DECL(AUDITORIUM), DECL(CONCERTHALL),
|
||||
DECL(CAVE), DECL(ARENA), DECL(HANGAR), DECL(CARPETEDHALLWAY), DECL(HALLWAY),
|
||||
DECL(STONECORRIDOR), DECL(ALLEY), DECL(FOREST), DECL(CITY), DECL(MOUNTAINS),
|
||||
DECL(QUARRY), DECL(PLAIN), DECL(PARKINGLOT), DECL(SEWERPIPE),
|
||||
DECL(UNDERWATER), DECL(DRUGGED), DECL(DIZZY), DECL(PSYCHOTIC),
|
||||
|
||||
DECL(CASTLE_SMALLROOM), DECL(CASTLE_SHORTPASSAGE), DECL(CASTLE_MEDIUMROOM),
|
||||
DECL(CASTLE_LARGEROOM), DECL(CASTLE_LONGPASSAGE), DECL(CASTLE_HALL),
|
||||
DECL(CASTLE_CUPBOARD), DECL(CASTLE_COURTYARD), DECL(CASTLE_ALCOVE),
|
||||
|
||||
DECL(FACTORY_SMALLROOM), DECL(FACTORY_SHORTPASSAGE),
|
||||
DECL(FACTORY_MEDIUMROOM), DECL(FACTORY_LARGEROOM),
|
||||
DECL(FACTORY_LONGPASSAGE), DECL(FACTORY_HALL), DECL(FACTORY_CUPBOARD),
|
||||
DECL(FACTORY_COURTYARD), DECL(FACTORY_ALCOVE),
|
||||
|
||||
DECL(ICEPALACE_SMALLROOM), DECL(ICEPALACE_SHORTPASSAGE),
|
||||
DECL(ICEPALACE_MEDIUMROOM), DECL(ICEPALACE_LARGEROOM),
|
||||
DECL(ICEPALACE_LONGPASSAGE), DECL(ICEPALACE_HALL), DECL(ICEPALACE_CUPBOARD),
|
||||
DECL(ICEPALACE_COURTYARD), DECL(ICEPALACE_ALCOVE),
|
||||
|
||||
DECL(SPACESTATION_SMALLROOM), DECL(SPACESTATION_SHORTPASSAGE),
|
||||
DECL(SPACESTATION_MEDIUMROOM), DECL(SPACESTATION_LARGEROOM),
|
||||
DECL(SPACESTATION_LONGPASSAGE), DECL(SPACESTATION_HALL),
|
||||
DECL(SPACESTATION_CUPBOARD), DECL(SPACESTATION_ALCOVE),
|
||||
|
||||
DECL(WOODEN_SMALLROOM), DECL(WOODEN_SHORTPASSAGE), DECL(WOODEN_MEDIUMROOM),
|
||||
DECL(WOODEN_LARGEROOM), DECL(WOODEN_LONGPASSAGE), DECL(WOODEN_HALL),
|
||||
DECL(WOODEN_CUPBOARD), DECL(WOODEN_COURTYARD), DECL(WOODEN_ALCOVE),
|
||||
|
||||
DECL(SPORT_EMPTYSTADIUM), DECL(SPORT_SQUASHCOURT),
|
||||
DECL(SPORT_SMALLSWIMMINGPOOL), DECL(SPORT_LARGESWIMMINGPOOL),
|
||||
DECL(SPORT_GYMNASIUM), DECL(SPORT_FULLSTADIUM), DECL(SPORT_STADIUMTANNOY),
|
||||
|
||||
DECL(PREFAB_WORKSHOP), DECL(PREFAB_SCHOOLROOM), DECL(PREFAB_PRACTISEROOM),
|
||||
DECL(PREFAB_OUTHOUSE), DECL(PREFAB_CARAVAN),
|
||||
|
||||
DECL(DOME_TOMB), DECL(PIPE_SMALL), DECL(DOME_SAINTPAULS),
|
||||
DECL(PIPE_LONGTHIN), DECL(PIPE_LARGE), DECL(PIPE_RESONANT),
|
||||
|
||||
DECL(OUTDOORS_BACKYARD), DECL(OUTDOORS_ROLLINGPLAINS),
|
||||
DECL(OUTDOORS_DEEPCANYON), DECL(OUTDOORS_CREEK), DECL(OUTDOORS_VALLEY),
|
||||
|
||||
DECL(MOOD_HEAVEN), DECL(MOOD_HELL), DECL(MOOD_MEMORY),
|
||||
|
||||
DECL(DRIVING_COMMENTATOR), DECL(DRIVING_PITGARAGE),
|
||||
DECL(DRIVING_INCAR_RACER), DECL(DRIVING_INCAR_SPORTS),
|
||||
DECL(DRIVING_INCAR_LUXURY), DECL(DRIVING_FULLGRANDSTAND),
|
||||
DECL(DRIVING_EMPTYGRANDSTAND), DECL(DRIVING_TUNNEL),
|
||||
|
||||
DECL(CITY_STREETS), DECL(CITY_SUBWAY), DECL(CITY_MUSEUM),
|
||||
DECL(CITY_LIBRARY), DECL(CITY_UNDERPASS), DECL(CITY_ABANDONED),
|
||||
|
||||
DECL(DUSTYROOM), DECL(CHAPEL), DECL(SMALLWATERROOM)};
|
||||
#undef DECL
|
||||
|
||||
inline std::vector<std::string>
|
||||
reverb_presets() noexcept
|
||||
{
|
||||
std::vector<std::string> presets;
|
||||
for (auto const& preset : REVERB_PRESETS)
|
||||
presets.push_back (preset.first);
|
||||
return presets;
|
||||
}
|
||||
|
||||
inline std::vector<alure::AttributePair>
|
||||
mkattrs (std::vector<std::pair<int, int>> attrs) noexcept
|
||||
{
|
||||
std::vector<alure::AttributePair> attributes;
|
||||
for (auto const& pair : attrs)
|
||||
attributes.push_back ({pair.first, pair.second});
|
||||
attributes.push_back (alure::AttributesEnd());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
inline alure::FilterParams
|
||||
make_filter (float gain, float gain_hf, float gain_lf) noexcept
|
||||
{ return alure::FilterParams {gain, gain_hf, gain_lf}; }
|
||||
|
||||
inline std::vector<float>
|
||||
from_vector3 (alure::Vector3 v) noexcept
|
||||
{ return std::vector<float> {v[0], v[1], v[2]}; }
|
||||
|
||||
inline alure::Vector3
|
||||
to_vector3 (std::vector<float> v) noexcept
|
||||
{ return alure::Vector3 {v[0], v[1], v[2]}; }
|
||||
} // namespace palace
|
||||
|
||||
#endif // PALACE_UTIL_H
|
|
@ -0,0 +1,39 @@
|
|||
# Helper functions and mappings
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from libcpp.map cimport map
|
||||
from libcpp.string cimport string
|
||||
from libcpp.utility cimport pair
|
||||
from libcpp.vector cimport vector
|
||||
|
||||
from alure cimport ( # noqa
|
||||
AttributePair, EFXEAXREVERBPROPERTIES, FilterParams,
|
||||
ChannelConfig, SampleType, DistanceModel, Vector3)
|
||||
|
||||
|
||||
cdef extern from 'util.h' namespace 'palace' nogil:
|
||||
cdef const map[string, EFXEAXREVERBPROPERTIES] REVERB_PRESETS
|
||||
cdef const map[string, SampleType] SAMPLE_TYPES
|
||||
cdef const map[string, ChannelConfig] CHANNEL_CONFIGS
|
||||
cdef const map[string, DistanceModel] DISTANCE_MODELS
|
||||
cdef vector[string] reverb_presets()
|
||||
cdef vector[AttributePair] mkattrs(vector[pair[int, int]])
|
||||
cdef FilterParams make_filter(float gain, float gain_hf, float gain_lf)
|
||||
cdef vector[float] from_vector3(Vector3)
|
||||
cdef Vector3 to_vector3(vector[float])
|
|
@ -1,4 +1,5 @@
|
|||
# test environment
|
||||
# Common test fixtures
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
|
@ -16,37 +17,38 @@
|
|||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This module provide default objects of palace classes as fixtures
|
||||
for convenient testing.
|
||||
"""
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
from pytest import fixture
|
||||
from palace import Device, Context, Source, SourceGroup
|
||||
|
||||
__all__ = ['device', 'context', 'source', 'source_group']
|
||||
DATA_DIR = abspath(join(dirname(__file__), 'data'))
|
||||
|
||||
|
||||
@fixture(scope='session')
|
||||
def device():
|
||||
"""Provide the default device."""
|
||||
with Device() as dev: yield dev
|
||||
@fixture
|
||||
def aiff():
|
||||
"""Provide a sample AIFF file."""
|
||||
return join(DATA_DIR, '24741__tim-kahn__b23-c1-raw.aiff')
|
||||
|
||||
|
||||
@fixture(scope='session')
|
||||
def context(device):
|
||||
"""Provide a context creared from the default device
|
||||
(default context).
|
||||
"""
|
||||
with Context(device) as ctx: yield ctx
|
||||
@fixture
|
||||
def flac():
|
||||
"""Provide a sample FLAC file."""
|
||||
return join(DATA_DIR, '261590__kwahmah-02__little-glitch.flac')
|
||||
|
||||
|
||||
@fixture(scope='session')
|
||||
def source(context):
|
||||
"""Provide a source creared from the default context."""
|
||||
with Source(context) as src: yield src
|
||||
@fixture
|
||||
def mp3():
|
||||
"""Provide a sample MP3 file."""
|
||||
return join(DATA_DIR, '353684__tec-studio__drip2.mp3')
|
||||
|
||||
|
||||
@fixture(scope='session')
|
||||
def source_group(context):
|
||||
"""Provide a source group creared from the default context."""
|
||||
with SourceGroup(context) as group: yield group
|
||||
@fixture
|
||||
def ogg():
|
||||
"""Provide a sample Ogg Vorbis file."""
|
||||
return join(DATA_DIR, '164957__zonkmachine__white-noise.ogg')
|
||||
|
||||
|
||||
@fixture
|
||||
def wav():
|
||||
"""Provide a sample WAVE file."""
|
||||
return join(DATA_DIR, '99642__jobro__deconvoluted-20hz-to-20khz.wav')
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,81 @@
|
|||
# Context managers' functional tests
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from palace import (current_context, cache, free, decode, Device, Context,
|
||||
Buffer, Source, SourceGroup, ReverbEffect, ChorusEffect)
|
||||
from pytest import mark, raises
|
||||
|
||||
|
||||
def test_current_context():
|
||||
"""Test the current context."""
|
||||
with Device() as device, Context(device) as context:
|
||||
assert current_context() == context
|
||||
assert current_context() is None
|
||||
|
||||
|
||||
def test_stream_loading(wav):
|
||||
"""Test implication of context during stream loading."""
|
||||
with Device() as device, Context(device): decode(wav)
|
||||
with raises(RuntimeError): decode(wav)
|
||||
|
||||
|
||||
@mark.skip(reason='deadlock (GH-73)')
|
||||
def test_cache_and_free(aiff, flac, ogg):
|
||||
"""Test cache and free, with and without a current context."""
|
||||
with Device() as device, Context(device):
|
||||
cache([aiff, flac, ogg])
|
||||
free([aiff, flac, ogg])
|
||||
with raises(RuntimeError): cache([aiff, flac, ogg])
|
||||
with raises(RuntimeError): free([aiff, flac, ogg])
|
||||
|
||||
|
||||
def test_buffer_loading(mp3):
|
||||
"""Test implication of context during buffer loading."""
|
||||
with Device() as device, Context(device):
|
||||
with Buffer(mp3): pass
|
||||
with raises(RuntimeError):
|
||||
with Buffer(mp3): pass
|
||||
|
||||
|
||||
@mark.parametrize('cls', [Source, SourceGroup, ReverbEffect, ChorusEffect])
|
||||
def test_init_others(cls):
|
||||
"""Test implication of context during object initialization."""
|
||||
with Device() as device, Context(device):
|
||||
with cls(): pass
|
||||
with raises(RuntimeError):
|
||||
with cls(): pass
|
||||
|
||||
|
||||
def test_nested_context_manager():
|
||||
"""Test if the context manager returns to the previous context."""
|
||||
with Device() as device, Context(device) as context:
|
||||
with Context(device): pass
|
||||
assert current_context() == context
|
||||
|
||||
|
||||
@mark.parametrize('data', [
|
||||
'air_absorption_factor', 'cone_angles', 'distance_range', 'doppler_factor',
|
||||
'gain', 'gain_auto', 'gain_range', 'group', 'looping', 'offset',
|
||||
'orientation', 'outer_cone_gains', 'pitch', 'position', 'radius',
|
||||
'relative', 'rolloff_factors', 'spatialize', 'stereo_angles', 'velocity'])
|
||||
def test_source_setter(data):
|
||||
"""Test setters of a Source when its context is not current."""
|
||||
with Device() as device, Context(device), Source() as source:
|
||||
with raises(RuntimeError), Context(device):
|
||||
setattr(source, data, getattr(source, data))
|
|
@ -0,0 +1,113 @@
|
|||
# Functional tests using examples
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from os import environ
|
||||
from os.path import abspath, dirname, join
|
||||
from platform import system
|
||||
from random import choices
|
||||
from subprocess import PIPE, run, CalledProcessError
|
||||
from sys import executable
|
||||
from uuid import uuid4
|
||||
|
||||
from palace import reverb_preset_names
|
||||
from pytest import mark, raises
|
||||
|
||||
EXAMPLES = abspath(join(dirname(__file__), '..', '..', 'examples'))
|
||||
EVENT = join(EXAMPLES, 'palace-event.py')
|
||||
HRTF = join(EXAMPLES, 'palace-hrtf.py')
|
||||
INFO = join(EXAMPLES, 'palace-info.py')
|
||||
LATENCY = join(EXAMPLES, 'palace-latency.py')
|
||||
REVERB = join(EXAMPLES, 'palace-reverb.py')
|
||||
STDEC = join(EXAMPLES, 'palace-stdec.py')
|
||||
TONEGEN = join(EXAMPLES, 'palace-tonegen.py')
|
||||
|
||||
MADEUP_DEVICE = str(uuid4())
|
||||
REVERB_PRESETS = choices(reverb_preset_names, k=5)
|
||||
WAVEFORMS = ['sine', 'square', 'sawtooth',
|
||||
'triangle', 'impulse', 'white-noise']
|
||||
|
||||
travis_macos = bool(environ.get('TRAVIS')) and system() == 'Darwin'
|
||||
skipif_travis_macos = mark.skipif(travis_macos, reason='Travis CI for macOS')
|
||||
|
||||
|
||||
def capture(*argv):
|
||||
"""Return the captured standard output of given Python script."""
|
||||
return run([executable, *argv], stdout=PIPE).stdout.decode()
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_event(aiff, flac, mp3, ogg, wav):
|
||||
"""Test the event handling example."""
|
||||
event = capture(EVENT, aiff, flac, mp3, ogg, wav)
|
||||
assert 'Opened' in event
|
||||
assert f'Playing {aiff}' in event
|
||||
assert f'Playing {flac}' in event
|
||||
assert f'Playing {mp3}' in event
|
||||
assert f'Playing {ogg}' in event
|
||||
assert f'Playing {wav}' in event
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_hrtf(ogg):
|
||||
"""Test the HRTF example."""
|
||||
hrtf = capture(HRTF, ogg)
|
||||
assert 'Opened' in hrtf
|
||||
assert f'Playing {ogg}' in hrtf
|
||||
|
||||
|
||||
def test_info():
|
||||
"""Test the information query example."""
|
||||
run([executable, INFO], check=True)
|
||||
with raises(CalledProcessError):
|
||||
run([executable, INFO, MADEUP_DEVICE], check=True)
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_latency(mp3):
|
||||
"""Test the latency example."""
|
||||
latency = capture(LATENCY, mp3)
|
||||
assert 'Opened' in latency
|
||||
assert f'Playing {mp3}' in latency
|
||||
assert 'Offset' in latency
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
@mark.parametrize('preset', REVERB_PRESETS)
|
||||
def test_reverb(preset, flac):
|
||||
"""Test the reverb example."""
|
||||
reverb = capture(REVERB, flac, '-r', preset)
|
||||
assert 'Opened' in reverb
|
||||
assert f'Playing {flac}' in reverb
|
||||
assert f'Loading reverb preset {preset}' in reverb
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_stdec(aiff):
|
||||
"""Test the stdec example."""
|
||||
stdec = capture(STDEC, aiff)
|
||||
assert 'Opened' in stdec
|
||||
assert f'Playing {aiff}' in stdec
|
||||
|
||||
|
||||
@mark.parametrize('waveform', WAVEFORMS)
|
||||
def test_tonegen(waveform):
|
||||
"""Test the tonegen example."""
|
||||
tonegen = capture(TONEGEN, '-l', '0.1', '-w', waveform)
|
||||
assert 'Opened' in tonegen
|
||||
assert f'Playing {waveform}' in tonegen
|
|
@ -0,0 +1,97 @@
|
|||
# Message handling functional tests
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import aifc
|
||||
from os import environ
|
||||
from platform import system
|
||||
from unittest.mock import Mock
|
||||
from uuid import uuid4
|
||||
|
||||
from palace import (channel_configs, sample_types, decode,
|
||||
Device, Context, Buffer, SourceGroup, MessageHandler)
|
||||
from pytest import mark
|
||||
|
||||
|
||||
travis_macos = bool(environ.get('TRAVIS')) and system() == 'Darwin'
|
||||
skipif_travis_macos = mark.skipif(travis_macos, reason='Travis CI for macOS')
|
||||
|
||||
|
||||
def mock(message):
|
||||
"""Return the MessageHandler corresponding to the given message."""
|
||||
return type(''.join(map(str.capitalize, message.split('_'))),
|
||||
(MessageHandler,), {message: Mock()})()
|
||||
|
||||
|
||||
@mark.skip(reason='unknown way of disconnecting device to test this')
|
||||
def test_device_diconnected():
|
||||
"""Test the handling of device disconnected message."""
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_source_stopped(wav):
|
||||
"""Test the handling of source stopped message."""
|
||||
with Device() as device, Context(device) as context, Buffer(wav) as buffer:
|
||||
context.message_handler = mock('source_stopped')
|
||||
with buffer.play() as source:
|
||||
while source.playing: pass
|
||||
context.update()
|
||||
context.message_handler.source_stopped.assert_called_with(source)
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_source_force_stopped(ogg):
|
||||
"""Test the handling of source force stopped message."""
|
||||
with Device() as device, Context(device) as context:
|
||||
context.message_handler = mock('source_force_stopped')
|
||||
# TODO: test source preempted by a higher-prioritized one
|
||||
with Buffer(ogg) as buffer: source = buffer.play()
|
||||
context.message_handler.source_force_stopped.assert_called_with(source)
|
||||
with SourceGroup() as group, Buffer(ogg) as buffer:
|
||||
source.group = group
|
||||
buffer.play(source)
|
||||
group.stop_all()
|
||||
context.message_handler.source_force_stopped.assert_called_with(source)
|
||||
source.destroy()
|
||||
|
||||
|
||||
@skipif_travis_macos
|
||||
def test_buffer_loading(aiff):
|
||||
"""Test the handling of buffer loading message."""
|
||||
with Device() as device, Context(device) as context:
|
||||
context.message_handler = mock('buffer_loading')
|
||||
with Buffer(aiff), aifc.open(aiff, 'r') as f:
|
||||
args, kwargs = context.message_handler.buffer_loading.call_args
|
||||
name, channel_config, sample_type, sample_rate, data = args
|
||||
assert name == aiff
|
||||
assert channel_config == channel_configs[f.getnchannels()-1]
|
||||
assert sample_type == sample_types[f.getsampwidth()-1]
|
||||
assert sample_rate == f.getframerate()
|
||||
# TODO: verify data
|
||||
|
||||
|
||||
def test_resource_not_found(flac):
|
||||
"""Test the handling of resource not found message."""
|
||||
with Device() as device, Context(device) as context:
|
||||
context.message_handler = mock('resource_not_found')
|
||||
context.message_handler.resource_not_found.return_value = ''
|
||||
name = str(uuid4())
|
||||
try:
|
||||
decode(name)
|
||||
except RuntimeError:
|
||||
pass
|
||||
context.message_handler.resource_not_found.assert_called_with(name)
|
|
@ -1,252 +0,0 @@
|
|||
# Source pytest module
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This pytest module tries to test the correctness of the class Source."""
|
||||
|
||||
from itertools import product, repeat
|
||||
from math import inf, pi
|
||||
from operator import is_
|
||||
|
||||
from pytest import raises
|
||||
|
||||
from fmath import FLT_MAX, allclose, isclose
|
||||
|
||||
|
||||
def test_group(source, source_group):
|
||||
"""Test read-write property group."""
|
||||
assert source.group is None
|
||||
source.group = source_group
|
||||
assert source.group == source_group
|
||||
source.group = None
|
||||
assert source.group is None
|
||||
|
||||
|
||||
def test_priority(source):
|
||||
"""Test read-write property group."""
|
||||
assert source.priority == 0
|
||||
source.priority = 42
|
||||
assert source.priority == 42
|
||||
source.priority = 0
|
||||
assert source.priority == 0
|
||||
|
||||
|
||||
def test_offset(source):
|
||||
"""Test read-write property offset."""
|
||||
assert source.offset == 0
|
||||
# TODO: give the source a decoder to seek
|
||||
|
||||
|
||||
def test_looping(source):
|
||||
"""Test read-write property looping."""
|
||||
assert source.looping is False
|
||||
source.looping = True
|
||||
assert source.looping is True
|
||||
source.looping = False
|
||||
assert source.looping is False
|
||||
|
||||
|
||||
def test_pitch(source):
|
||||
"""Test read-write property pitch."""
|
||||
assert isclose(source.pitch, 1)
|
||||
with raises(ValueError): source.pitch = -1
|
||||
source.pitch = 5 / 7
|
||||
assert isclose(source.pitch, 5/7)
|
||||
source.pitch = 1
|
||||
assert isclose(source.pitch, 1)
|
||||
|
||||
|
||||
def test_gain(source):
|
||||
"""Test read-write property gain."""
|
||||
assert isclose(source.gain, 1)
|
||||
with raises(ValueError): source.gain = -1
|
||||
source.gain = 5 / 7
|
||||
assert isclose(source.gain, 5/7)
|
||||
source.gain = 1
|
||||
assert isclose(source.gain, 1)
|
||||
|
||||
|
||||
def test_gain_range(source):
|
||||
"""Test read-write property gain_range."""
|
||||
assert allclose(source.gain_range, (0, 1))
|
||||
with raises(ValueError): source.gain_range = 9/11, 5/7
|
||||
with raises(ValueError): source.gain_range = 6/9, 420
|
||||
with raises(ValueError): source.gain_range = -420, 6/9
|
||||
source.gain_range = 5/7, 9/11
|
||||
assert allclose(source.gain_range, (5/7, 9/11))
|
||||
source.gain_range = 0, 1
|
||||
assert allclose(source.gain_range, (0, 1))
|
||||
|
||||
|
||||
def test_distance_range(source):
|
||||
"""Test read-write property distance_range."""
|
||||
assert allclose(source.distance_range, (1, FLT_MAX))
|
||||
with raises(ValueError): source.distance_range = 9/11, 5/7
|
||||
with raises(ValueError): source.distance_range = -420, 6/9
|
||||
with raises(ValueError): source.distance_range = 420, inf
|
||||
source.distance_range = 5/7, 9/11
|
||||
assert allclose(source.distance_range, (5/7, 9/11))
|
||||
source.distance_range = 1, FLT_MAX
|
||||
assert allclose(source.distance_range, (1, FLT_MAX))
|
||||
|
||||
|
||||
def test_position(source):
|
||||
"""Test read-write property position."""
|
||||
assert allclose(source.position, (0, 0, 0))
|
||||
source.position = -1, 0, 1
|
||||
assert allclose(source.position, (-1, 0, 1))
|
||||
source.position = 4, 20, 69
|
||||
assert allclose(source.position, (4, 20, 69))
|
||||
source.position = 0, 0, 0
|
||||
assert allclose(source.position, (0, 0, 0))
|
||||
|
||||
|
||||
def test_velocity(source):
|
||||
"""Test read-write property velocity."""
|
||||
assert allclose(source.velocity, (0, 0, 0))
|
||||
source.velocity = -1, 0, 1
|
||||
assert allclose(source.velocity, (-1, 0, 1))
|
||||
source.velocity = 4, 20, 69
|
||||
assert allclose(source.velocity, (4, 20, 69))
|
||||
source.velocity = 0, 0, 0
|
||||
assert allclose(source.velocity, (0, 0, 0))
|
||||
|
||||
|
||||
def test_orientation(source):
|
||||
"""Test read-write property orientation."""
|
||||
assert all(map(allclose, source.orientation, ((0, 0, -1), (0, 1, 0))))
|
||||
source.orientation = (1, -2, 3), (-4, 5, -6)
|
||||
assert all(map(allclose, source.orientation, ((1, -2, 3), (-4, 5, -6))))
|
||||
source.orientation = (0, 0, -1), (0, 1, 0)
|
||||
assert all(map(allclose, source.orientation, ((0, 0, -1), (0, 1, 0))))
|
||||
|
||||
|
||||
def test_cone_angles(source):
|
||||
"""Test read-write property cone_angles."""
|
||||
assert allclose(source.cone_angles, (360, 360))
|
||||
with raises(ValueError): source.cone_angles = 420, 69
|
||||
with raises(ValueError): source.cone_angles = -4.20, 69
|
||||
with raises(ValueError): source.cone_angles = 4.20, -69
|
||||
source.cone_angles = 4.20, 69
|
||||
assert allclose(source.cone_angles, (4.20, 69))
|
||||
source.cone_angles = 360, 360
|
||||
assert allclose(source.cone_angles, (360, 360))
|
||||
|
||||
|
||||
def test_outer_cone_gains(source):
|
||||
"""Test read-write property outer_cone_gains."""
|
||||
assert allclose(source.outer_cone_gains, (0, 1))
|
||||
with raises(ValueError): source.outer_cone_gains = 6/9, -420
|
||||
with raises(ValueError): source.outer_cone_gains = 6/9, 420
|
||||
with raises(ValueError): source.outer_cone_gains = -420, 6/9
|
||||
with raises(ValueError): source.outer_cone_gains = 420, 6/9
|
||||
source.outer_cone_gains = 5/7, 9/11
|
||||
assert allclose(source.outer_cone_gains, (5/7, 9/11))
|
||||
source.outer_cone_gains = 0, 1
|
||||
assert allclose(source.outer_cone_gains, (0, 1))
|
||||
|
||||
|
||||
def test_rolloff_factors(source):
|
||||
"""Test read-write property rolloff_factors."""
|
||||
assert allclose(source.rolloff_factors, (1, 0))
|
||||
with raises(ValueError): source.rolloff_factors = -6, 9
|
||||
with raises(ValueError): source.rolloff_factors = 6, -9
|
||||
source.rolloff_factors = 6, 9
|
||||
assert allclose(source.rolloff_factors, (6, 9))
|
||||
source.rolloff_factors = 1, 0
|
||||
|
||||
|
||||
def test_doppler_factor(source):
|
||||
"""Test read-write property doppler_factor."""
|
||||
assert isclose(source.doppler_factor, 1)
|
||||
with raises(ValueError): source.doppler_factor = -6.9
|
||||
with raises(ValueError): source.doppler_factor = 4.20
|
||||
source.doppler_factor = 5 / 7
|
||||
assert isclose(source.doppler_factor, 5/7)
|
||||
source.doppler_factor = 1
|
||||
assert isclose(source.doppler_factor, 1)
|
||||
|
||||
|
||||
def test_relative(source):
|
||||
"""Test read-write property relative."""
|
||||
assert source.relative is False
|
||||
source.relative = True
|
||||
assert source.relative is True
|
||||
source.relative = False
|
||||
assert source.relative is False
|
||||
|
||||
|
||||
def test_radius(source):
|
||||
"""Test read-write property radius."""
|
||||
assert isclose(source.radius, 0)
|
||||
with raises(ValueError): source.radius = -1
|
||||
source.radius = 5 / 7
|
||||
assert isclose(source.radius, 5/7)
|
||||
source.radius = 1
|
||||
assert isclose(source.radius, 1)
|
||||
|
||||
|
||||
def test_stereo_angles(source):
|
||||
"""Test read-write property stereo_angles."""
|
||||
assert allclose(source.stereo_angles, (pi/6, -pi/6))
|
||||
source.stereo_angles = 4, 20
|
||||
assert allclose(source.stereo_angles, (4, 20))
|
||||
source.stereo_angles = -6, -9
|
||||
assert allclose(source.stereo_angles, (-6, -9))
|
||||
source.stereo_angles = pi/6, -pi/6
|
||||
assert allclose(source.stereo_angles, (pi/6, -pi/6))
|
||||
|
||||
|
||||
def test_spatialize(source):
|
||||
"""Test read-write property spatialize."""
|
||||
assert source.spatialize is None
|
||||
source.spatialize = False
|
||||
assert source.spatialize is False
|
||||
source.spatialize = True
|
||||
assert source.spatialize is True
|
||||
source.spatialize = None
|
||||
assert source.spatialize is None
|
||||
|
||||
|
||||
def test_resampler_index(source):
|
||||
"""Test read-write property resampler_index."""
|
||||
# TODO: test initial value
|
||||
old_resampler_index = source.resampler_index
|
||||
with raises(ValueError): source.resampler_index = -1
|
||||
source.resampler_index = 69
|
||||
assert source.resampler_index == 69
|
||||
source.resampler_index = old_resampler_index
|
||||
assert source.resampler_index == old_resampler_index
|
||||
|
||||
|
||||
def test_air_absorption_factor(source):
|
||||
"""Test read-write property air_absorption_factor."""
|
||||
assert isclose(source.air_absorption_factor, 0)
|
||||
with raises(ValueError): source.air_absorption_factor = -1
|
||||
with raises(ValueError): source.air_absorption_factor = 11
|
||||
source.air_absorption_factor = 420 / 69
|
||||
assert isclose(source.air_absorption_factor, 420/69)
|
||||
source.air_absorption_factor = 0
|
||||
assert isclose(source.air_absorption_factor, 0)
|
||||
|
||||
|
||||
def test_gain_auto(source):
|
||||
"""Test read-write property gain_auto."""
|
||||
assert all(gain is True for gain in source.gain_auto)
|
||||
for gain_auto in product(*repeat((False, True), 3)):
|
||||
source.gain_auto = gain_auto
|
||||
assert all(map(is_, source.gain_auto, gain_auto))
|
|
@ -1,5 +1,5 @@
|
|||
# Source pytest module
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
# Test fixtures for unit tests
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
|
@ -16,24 +16,23 @@
|
|||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This pytest module tries to test the correctness of the class Context."""
|
||||
"""This module provide default objects of palace classes as fixtures
|
||||
for convenient testing.
|
||||
"""
|
||||
|
||||
from palace import Context, current_context
|
||||
from pytest import fixture
|
||||
from palace import Device, Context
|
||||
|
||||
|
||||
def test_with_context(device):
|
||||
"""Test if `with` can be used to start a context
|
||||
and is destroyed properly.
|
||||
@fixture(scope='session')
|
||||
def device():
|
||||
"""Provide the default device."""
|
||||
with Device() as dev: yield dev
|
||||
|
||||
|
||||
@fixture(scope='session')
|
||||
def context(device):
|
||||
"""Provide a context creared from the default device
|
||||
(default context).
|
||||
"""
|
||||
with Context(device) as context:
|
||||
assert current_context() == context
|
||||
|
||||
|
||||
def test_nested_context_manager(device):
|
||||
"""Test if the context manager returns to the
|
||||
previous context.
|
||||
"""
|
||||
with Context(device) as ctx:
|
||||
with Context(device):
|
||||
pass
|
||||
assert current_context() == ctx
|
||||
with Context(device) as ctx: yield ctx
|
|
@ -1,4 +1,4 @@
|
|||
# single-precision floating-point math
|
||||
# Single-precision floating-point math
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
|
@ -22,7 +22,7 @@ for single-precision floating-point numbers.
|
|||
__all__ = ['FLT_MAX', 'allclose', 'isclose']
|
||||
|
||||
from math import isclose as _isclose
|
||||
from typing import Sequence
|
||||
from typing import Any, Callable, Sequence
|
||||
|
||||
FLT_EPSILON: float = 2.0 ** -23
|
||||
FLT_MAX: float = 2.0**128 - 2.0**104
|
||||
|
@ -42,7 +42,8 @@ def isclose(a: float, b: float) -> bool:
|
|||
return _isclose(a, b, rel_tol=FLT_EPSILON)
|
||||
|
||||
|
||||
def allclose(a: Sequence[float], b: Sequence[float]) -> bool:
|
||||
def allclose(a: Sequence[float], b: Sequence[float],
|
||||
close: Callable[[Any, Any], bool] = isclose) -> bool:
|
||||
"""Determine whether two sequences of single-precision
|
||||
floating-point numbers are close in value.
|
||||
|
||||
|
@ -53,4 +54,4 @@ def allclose(a: Sequence[float], b: Sequence[float]) -> bool:
|
|||
That is, NaN is not close to anything, even itself.
|
||||
inf and -inf are only close to themselves.
|
||||
"""
|
||||
return all(map(isclose, a, b))
|
||||
return type(a) is type(b) and all(map(close, a, b))
|
|
@ -0,0 +1,116 @@
|
|||
# Context pytest module
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
# Copyright (C) 2020 Ngô Xuân Minh
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This pytest module tries to test the correctness of the class Context."""
|
||||
|
||||
from palace import current_context, distance_models, Context, MessageHandler
|
||||
from pytest import raises
|
||||
|
||||
from math import inf
|
||||
|
||||
|
||||
def test_comparison(device):
|
||||
"""Test basic comparisons."""
|
||||
with Context(device) as c0, Context(device) as c1, Context(device) as c2:
|
||||
assert c0 != c1
|
||||
contexts = [c1, c1, c0, c2]
|
||||
contexts.sort()
|
||||
contexts.remove(c2)
|
||||
contexts.remove(c0)
|
||||
assert contexts[0] == contexts[1]
|
||||
|
||||
|
||||
def test_bool(device):
|
||||
"""Test boolean value."""
|
||||
with Context(device) as context: assert context
|
||||
assert not context
|
||||
|
||||
|
||||
def test_batch_control(device):
|
||||
"""Test calls of start_batch and end_batch."""
|
||||
with Context(device) as context:
|
||||
# At the moment these are no-op.
|
||||
context.start_batch()
|
||||
context.end_batch()
|
||||
|
||||
|
||||
def test_message_handler(device):
|
||||
"""Test read-write property MessageHandler."""
|
||||
context = Context(device)
|
||||
assert type(context.message_handler) is MessageHandler
|
||||
message_handler_test = type('MessageHandlerTest', (MessageHandler,), {})()
|
||||
context.message_handler = message_handler_test
|
||||
assert context.message_handler is message_handler_test
|
||||
with context:
|
||||
assert current_context().message_handler is context.message_handler
|
||||
|
||||
|
||||
def test_async_wake_interval(device):
|
||||
"""Test read-write property async_wake_interval."""
|
||||
with Context(device) as context:
|
||||
context.async_wake_interval = 42
|
||||
assert context.async_wake_interval == 42
|
||||
|
||||
|
||||
def test_format_support(device):
|
||||
"""Test method is_supported."""
|
||||
with Context(device) as context:
|
||||
assert isinstance(context.is_supported('Rear', '32-bit float'), bool)
|
||||
with raises(ValueError): context.is_supported('Shu', 'Mulaw')
|
||||
with raises(ValueError): context.is_supported('Stereo', 'Type')
|
||||
|
||||
|
||||
def test_default_resampler_index(device):
|
||||
"""Test read-only property default_resampler_index."""
|
||||
with Context(device) as context:
|
||||
index = context.default_resampler_index
|
||||
assert index >= 0
|
||||
assert len(context.available_resamplers) > index
|
||||
with raises(AttributeError): context.available_resamplers = 0
|
||||
|
||||
|
||||
def test_doppler_factor(device):
|
||||
"""Test write-only property doppler_factor."""
|
||||
with Context(device) as context:
|
||||
context.doppler_factor = 4/9
|
||||
context.doppler_factor = 9/4
|
||||
context.doppler_factor = 0
|
||||
context.doppler_factor = inf
|
||||
with raises(ValueError): context.doppler_factor = -1
|
||||
with raises(AttributeError): context.doppler_factor
|
||||
|
||||
|
||||
def test_speed_of_sound(device):
|
||||
"""Test write-only property speed_of_sound."""
|
||||
with Context(device) as context:
|
||||
context.speed_of_sound = 5/7
|
||||
context.speed_of_sound = 7/5
|
||||
with raises(ValueError): context.speed_of_sound = 0
|
||||
context.speed_of_sound = inf
|
||||
with raises(ValueError): context.speed_of_sound = -1
|
||||
with raises(AttributeError): context.speed_of_sound
|
||||
|
||||
|
||||
def test_distance_model(device):
|
||||
"""Test write-only distance_model."""
|
||||
with Context(device) as context:
|
||||
for model in distance_models: context.distance_model = model
|
||||
with raises(ValueError): context.distance_model = 'EYYYYLMAO'
|
||||
with raises(AttributeError): context.distance_model
|
|
@ -0,0 +1,437 @@
|
|||
# Effect pytest module
|
||||
# Copyright (C) 2020 Ngô Ngọc Đức Huy
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This pytest module verifies environmental effects."""
|
||||
|
||||
from palace import BaseEffect, ChorusEffect, ReverbEffect, Source
|
||||
from pytest import raises
|
||||
|
||||
from fmath import isclose, allclose
|
||||
|
||||
|
||||
def test_slot_gain(context):
|
||||
"""Test write-only property slot_gain."""
|
||||
with BaseEffect() as fx:
|
||||
fx.slot_gain = 0
|
||||
fx.slot_gain = 1
|
||||
fx.slot_gain = 5/7
|
||||
with raises(ValueError): fx.slot_gain = 7/5
|
||||
with raises(ValueError): fx.slot_gain = -1
|
||||
|
||||
|
||||
def test_source_sends(context):
|
||||
"""Test property source_sends by assigning it to a source."""
|
||||
with Source() as src, BaseEffect() as fx:
|
||||
src.sends[0].effect = fx
|
||||
assert fx.source_sends[-1] == (src, 0)
|
||||
|
||||
|
||||
def test_use_count(context):
|
||||
"""Test read-only property use_count."""
|
||||
with BaseEffect() as fx:
|
||||
assert fx.use_count == len(fx.source_sends)
|
||||
|
||||
|
||||
def test_reverb(context):
|
||||
"""Test ReverbEffect initialization."""
|
||||
with ReverbEffect('DRUGGED'): pass
|
||||
with raises(ValueError):
|
||||
with ReverbEffect('NOT_AN_EFFECT'): pass
|
||||
|
||||
|
||||
def test_reverb_send_auto(context):
|
||||
"""Test ReverbEffect's write-only property send_auto."""
|
||||
with ReverbEffect() as fx:
|
||||
fx.send_auto = False
|
||||
fx.send_auto = True
|
||||
|
||||
|
||||
def test_reverb_density(context):
|
||||
"""Test ReverbEffect's property density."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.density == 1
|
||||
fx.density = 5/7
|
||||
assert isclose(fx.density, 5/7)
|
||||
fx.density = 0
|
||||
assert fx.density == 0
|
||||
fx.density = 1
|
||||
assert fx.density == 1
|
||||
with raises(ValueError): fx.density = 7/5
|
||||
with raises(ValueError): fx.density = -1
|
||||
|
||||
|
||||
def test_reverb_diffusion(context):
|
||||
"""Test ReverbEffect's property diffusion."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.diffusion == 1
|
||||
fx.diffusion = 5/7
|
||||
assert isclose(fx.diffusion, 5/7)
|
||||
fx.diffusion = 0
|
||||
assert fx.diffusion == 0
|
||||
fx.diffusion = 1
|
||||
assert fx.diffusion == 1
|
||||
with raises(ValueError): fx.diffusion = 7/5
|
||||
with raises(ValueError): fx.diffusion = -1
|
||||
|
||||
|
||||
def test_reverb_gain(context):
|
||||
"""Test ReverbEffect's property gain."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.gain, 0.3162)
|
||||
fx.gain = 5/7
|
||||
assert isclose(fx.gain, 5/7)
|
||||
fx.gain = 0
|
||||
assert fx.gain == 0
|
||||
fx.gain = 1
|
||||
assert fx.gain == 1
|
||||
with raises(ValueError): fx.gain = 7/5
|
||||
with raises(ValueError): fx.gain = -1
|
||||
|
||||
|
||||
def test_reverb_gain_hf(context):
|
||||
"""Test ReverbEffect's property gain_hf."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.gain_hf, 0.8913)
|
||||
fx.gain_hf = 5/7
|
||||
assert isclose(fx.gain_hf, 5/7)
|
||||
fx.gain_hf = 0
|
||||
assert fx.gain_hf == 0
|
||||
fx.gain_hf = 1
|
||||
assert fx.gain_hf == 1
|
||||
with raises(ValueError): fx.gain_hf = 7/5
|
||||
with raises(ValueError): fx.gain_hf = -1
|
||||
|
||||
|
||||
def test_reverb_gain_lf(context):
|
||||
"""Test ReverbEffect's property gain_lf."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.gain_lf == 1
|
||||
fx.gain_lf = 5/7
|
||||
assert isclose(fx.gain_lf, 5/7)
|
||||
fx.gain_lf = 0
|
||||
assert fx.gain_lf == 0
|
||||
fx.gain_lf = 1
|
||||
assert fx.gain_lf == 1
|
||||
with raises(ValueError): fx.gain_lf = 7/5
|
||||
with raises(ValueError): fx.gain_lf = -1
|
||||
|
||||
|
||||
def test_reverb_decay_time(context):
|
||||
"""Test ReverbEffect's property decay_time."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.decay_time, 1.49)
|
||||
fx.decay_time = 5/7
|
||||
assert isclose(fx.decay_time, 5/7)
|
||||
fx.decay_time = 0.1
|
||||
assert isclose(fx.decay_time, 0.1)
|
||||
fx.decay_time = 20
|
||||
assert fx.decay_time == 20
|
||||
with raises(ValueError): fx.decay_time = 21
|
||||
with raises(ValueError): fx.decay_time = -1
|
||||
|
||||
|
||||
def test_reverb_decay_hf_ratio(context):
|
||||
"""Test ReverbEffect's property decay_hf_ratio."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.decay_hf_ratio, 0.83)
|
||||
fx.decay_hf_ratio = 5/7
|
||||
assert isclose(fx.decay_hf_ratio, 5/7)
|
||||
fx.decay_hf_ratio = 0.1
|
||||
assert isclose(fx.decay_hf_ratio, 0.1)
|
||||
fx.decay_hf_ratio = 2
|
||||
assert fx.decay_hf_ratio == 2
|
||||
with raises(ValueError): fx.decay_hf_ratio = 21
|
||||
with raises(ValueError): fx.decay_hf_ratio = -1
|
||||
|
||||
|
||||
def test_reverb_decay_lf_ratio(context):
|
||||
"""Test ReverbEffect's property decay_lf_ratio."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.decay_lf_ratio == 1
|
||||
fx.decay_lf_ratio = 5/7
|
||||
assert isclose(fx.decay_lf_ratio, 5/7)
|
||||
fx.decay_lf_ratio = 0.1
|
||||
assert isclose(fx.decay_lf_ratio, 0.1)
|
||||
fx.decay_lf_ratio = 2
|
||||
assert fx.decay_lf_ratio == 2
|
||||
with raises(ValueError): fx.decay_lf_ratio = 21
|
||||
with raises(ValueError): fx.decay_lf_ratio = -1
|
||||
|
||||
|
||||
def test_reverb_reflections_gain(context):
|
||||
"""Test ReverbEffect's property reflections_gain."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.reflections_gain, 0.05)
|
||||
fx.reflections_gain = 5/7
|
||||
assert isclose(fx.reflections_gain, 5/7)
|
||||
fx.reflections_gain = 3.16
|
||||
assert isclose(fx.reflections_gain, 3.16)
|
||||
fx.reflections_gain = 0
|
||||
assert fx.reflections_gain == 0
|
||||
with raises(ValueError): fx.reflections_gain = 4
|
||||
with raises(ValueError): fx.reflections_gain = -1
|
||||
|
||||
|
||||
def test_reverb_reflections_delay(context):
|
||||
"""Test ReverbEffect's property reflections_delay."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.reflections_delay, 0.007)
|
||||
fx.reflections_delay = 0.3
|
||||
assert isclose(fx.reflections_delay, 0.3)
|
||||
fx.reflections_delay = 0
|
||||
assert fx.reflections_delay == 0
|
||||
with raises(ValueError): fx.reflections_delay = 1
|
||||
with raises(ValueError): fx.reflections_delay = -1
|
||||
|
||||
|
||||
def test_reverb_reflections_pan(context):
|
||||
"""Test ReverbEffect's property reflections_pan."""
|
||||
with ReverbEffect() as fx:
|
||||
assert allclose(fx.reflections_pan, (0, 0, 0))
|
||||
fx.reflections_pan = 5/7, -69/420, 6/9
|
||||
assert allclose(fx.reflections_pan, (5/7, -69/420, 6/9))
|
||||
with raises(ValueError): fx.reflections_pan = 1, 1, 1
|
||||
with raises(ValueError): fx.reflections_pan = 0, 0, 2
|
||||
with raises(ValueError): fx.reflections_pan = 0, 2, 0
|
||||
with raises(ValueError): fx.reflections_pan = 2, 0, 0
|
||||
with raises(ValueError): fx.reflections_pan = 0, 0, -2
|
||||
with raises(ValueError): fx.reflections_pan = 0, -2, 0
|
||||
with raises(ValueError): fx.reflections_pan = -2, 0, 0
|
||||
|
||||
|
||||
def test_reverb_late_reverb_gain(context):
|
||||
"""Test ReverbEffect's property late_reverb_gain."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.late_reverb_gain, 1.2589)
|
||||
fx.late_reverb_gain = 5/7
|
||||
assert isclose(fx.late_reverb_gain, 5/7)
|
||||
fx.late_reverb_gain = 0
|
||||
assert fx.late_reverb_gain == 0
|
||||
fx.late_reverb_gain = 10
|
||||
assert fx.late_reverb_gain == 10
|
||||
with raises(ValueError): fx.late_reverb_gain = 11
|
||||
with raises(ValueError): fx.late_reverb_gain = -1
|
||||
|
||||
|
||||
def test_reverb_late_reverb_delay(context):
|
||||
"""Test ReverbEffect's property late_reverb_delay."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.late_reverb_delay, 0.011)
|
||||
fx.late_reverb_delay = 0.05
|
||||
assert isclose(fx.late_reverb_delay, 0.05)
|
||||
fx.late_reverb_delay = 0
|
||||
assert fx.late_reverb_delay == 0
|
||||
fx.late_reverb_delay = 0.1
|
||||
assert isclose(fx.late_reverb_delay, 0.1)
|
||||
with raises(ValueError): fx.late_reverb_delay = 1
|
||||
with raises(ValueError): fx.late_reverb_delay = -1
|
||||
|
||||
|
||||
def test_reverb_late_reverb_pan(context):
|
||||
"""Test ReverbEffect's property late_reverb_pan."""
|
||||
with ReverbEffect() as fx:
|
||||
assert allclose(fx.late_reverb_pan, (0, 0, 0))
|
||||
fx.late_reverb_pan = 5/7, -69/420, 6/9
|
||||
assert allclose(fx.late_reverb_pan, (5/7, -69/420, 6/9))
|
||||
with raises(ValueError): fx.late_reverb_pan = 1, 1, 1
|
||||
with raises(ValueError): fx.late_reverb_pan = 0, 0, 2
|
||||
with raises(ValueError): fx.late_reverb_pan = 0, 2, 0
|
||||
with raises(ValueError): fx.late_reverb_pan = 2, 0, 0
|
||||
with raises(ValueError): fx.late_reverb_pan = 0, 0, -2
|
||||
with raises(ValueError): fx.late_reverb_pan = 0, -2, 0
|
||||
with raises(ValueError): fx.late_reverb_pan = -2, 0, 0
|
||||
|
||||
|
||||
def test_reverb_echo_time(context):
|
||||
"""Test ReverbEffect's property echo_time."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.echo_time, 0.25)
|
||||
fx.echo_time = 0.075
|
||||
assert isclose(fx.echo_time, 0.075)
|
||||
fx.echo_time = 0.1
|
||||
assert isclose(fx.echo_time, 0.1)
|
||||
with raises(ValueError): fx.echo_time = 0.05
|
||||
with raises(ValueError): fx.echo_time = 0.5
|
||||
|
||||
|
||||
def test_reverb_echo_depth(context):
|
||||
"""Test ReverbEffect's property echo_depth."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.echo_depth == 0
|
||||
fx.echo_depth = 5/7
|
||||
assert isclose(fx.echo_depth, 5/7)
|
||||
fx.echo_depth = 0
|
||||
assert fx.echo_depth == 0
|
||||
fx.echo_depth = 1
|
||||
assert fx.echo_depth == 1
|
||||
with raises(ValueError): fx.echo_depth = 7/5
|
||||
with raises(ValueError): fx.echo_depth = -1
|
||||
|
||||
|
||||
def test_reverb_modulation_time(context):
|
||||
"""Test ReverbEffect's property modulation_time."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.modulation_time, 0.25)
|
||||
fx.modulation_time = 5/7
|
||||
assert isclose(fx.modulation_time, 5/7)
|
||||
fx.modulation_time = 0.04
|
||||
assert isclose(fx.modulation_time, 0.04)
|
||||
fx.modulation_time = 4
|
||||
assert fx.modulation_time == 4
|
||||
with raises(ValueError): fx.modulation_time = 4.2
|
||||
with raises(ValueError): fx.modulation_time = 0
|
||||
|
||||
|
||||
def test_reverb_modulation_depth(context):
|
||||
"""Test ReverbEffect's property modulation_depth."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.modulation_depth == 0
|
||||
fx.modulation_depth = 5/7
|
||||
assert isclose(fx.modulation_depth, 5/7)
|
||||
fx.modulation_depth = 0
|
||||
assert fx.modulation_depth == 0
|
||||
fx.modulation_depth = 1
|
||||
assert fx.modulation_depth == 1
|
||||
with raises(ValueError): fx.modulation_depth = 7/5
|
||||
with raises(ValueError): fx.modulation_depth = -1
|
||||
|
||||
|
||||
def test_reverb_air_absorption_gain_hf(context):
|
||||
"""Test ReverbEffect's property air_absorption_gain_hf."""
|
||||
with ReverbEffect() as fx:
|
||||
assert isclose(fx.air_absorption_gain_hf, 0.9943)
|
||||
fx.air_absorption_gain_hf = 0.999
|
||||
assert isclose(fx.air_absorption_gain_hf, 0.999)
|
||||
fx.air_absorption_gain_hf = 0.892
|
||||
assert isclose(fx.air_absorption_gain_hf, 0.892)
|
||||
fx.air_absorption_gain_hf = 1
|
||||
assert fx.air_absorption_gain_hf == 1
|
||||
with raises(ValueError): fx.air_absorption_gain_hf = 7/5
|
||||
with raises(ValueError): fx.air_absorption_gain_hf = 0.5
|
||||
|
||||
|
||||
def test_reverb_hf_reference(context):
|
||||
"""Test ReverbEffect's property hf_reference."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.hf_reference == 5000
|
||||
fx.hf_reference = 6969
|
||||
assert fx.hf_reference == 6969
|
||||
fx.hf_reference = 1000
|
||||
assert fx.hf_reference == 1000
|
||||
fx.hf_reference = 20000
|
||||
assert fx.hf_reference == 20000
|
||||
with raises(ValueError): fx.hf_reference = 20000.5
|
||||
with raises(ValueError): fx.hf_reference = 999
|
||||
|
||||
|
||||
def test_reverb_lf_reference(context):
|
||||
"""Test ReverbEffect's property lf_reference."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.lf_reference == 250
|
||||
fx.lf_reference = 666
|
||||
assert fx.lf_reference == 666
|
||||
fx.lf_reference = 1000
|
||||
assert fx.lf_reference == 1000
|
||||
fx.lf_reference = 20
|
||||
assert fx.lf_reference == 20
|
||||
with raises(ValueError): fx.lf_reference = 19.5
|
||||
with raises(ValueError): fx.lf_reference = 1001
|
||||
|
||||
|
||||
def test_reverb_room_rolloff_factor(context):
|
||||
"""Test ReverbEffect's property room_rolloff_factor."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.room_rolloff_factor == 0
|
||||
fx.room_rolloff_factor = 9/6
|
||||
assert fx.room_rolloff_factor == 9/6
|
||||
fx.room_rolloff_factor = 0
|
||||
assert fx.room_rolloff_factor == 0
|
||||
fx.room_rolloff_factor = 10
|
||||
assert fx.room_rolloff_factor == 10
|
||||
with raises(ValueError): fx.room_rolloff_factor = 10.5
|
||||
with raises(ValueError): fx.room_rolloff_factor = -1
|
||||
|
||||
|
||||
def test_reverb_decay_hf_limit(context):
|
||||
"""Test ReverbEffect's property decay_hf_limit."""
|
||||
with ReverbEffect() as fx:
|
||||
assert fx.decay_hf_limit is True
|
||||
fx.decay_hf_limit = False
|
||||
assert fx.decay_hf_limit is False
|
||||
fx.decay_hf_limit = True
|
||||
assert fx.decay_hf_limit is True
|
||||
|
||||
|
||||
def test_chorus_waveform(context):
|
||||
"""Test ChorusEffect's property waveform."""
|
||||
with ChorusEffect() as fx:
|
||||
assert fx.waveform == 'triangle'
|
||||
fx.waveform = 'sine'
|
||||
assert fx.waveform == 'sine'
|
||||
fx.waveform = 'triangle'
|
||||
assert fx.waveform == 'triangle'
|
||||
with raises(ValueError): fx.waveform = 'ABC'
|
||||
|
||||
|
||||
def test_chorus_phase(context):
|
||||
"""Test ChorusEffect's property phase."""
|
||||
with ChorusEffect() as fx:
|
||||
assert fx.phase == 90
|
||||
fx.phase = 180
|
||||
assert fx.phase == 180
|
||||
fx.phase = -180
|
||||
assert fx.phase == -180
|
||||
with raises(ValueError): fx.phase = 181
|
||||
with raises(ValueError): fx.phase = -181
|
||||
|
||||
|
||||
def test_chorus_depth(context):
|
||||
"""Test ChorusEffect's property depth."""
|
||||
with ChorusEffect() as fx:
|
||||
assert isclose(fx.depth, 0.1)
|
||||
fx.depth = 0
|
||||
assert fx.depth == 0
|
||||
fx.depth = 1
|
||||
assert fx.depth == 1
|
||||
with raises(ValueError): fx.depth = 2
|
||||
with raises(ValueError): fx.depth = -1
|
||||
|
||||
|
||||
def test_chorus_feedback(context):
|
||||
"""Test ChorusEffect's property feedback."""
|
||||
with ChorusEffect() as fx:
|
||||
assert isclose(fx.feedback, 0.25)
|
||||
fx.feedback = -1
|
||||
assert fx.feedback == -1
|
||||
fx.feedback = 1
|
||||
assert fx.feedback == 1
|
||||
with raises(ValueError): fx.feedback = 3/2
|
||||
with raises(ValueError): fx.feedback = -7/5
|
||||
|
||||
|
||||
def test_chorus_delay(context):
|
||||
"""Test ChorusEffect's property delay."""
|
||||
with ChorusEffect() as fx:
|
||||
assert isclose(fx.delay, 0.016)
|
||||
fx.delay = 0
|
||||
assert fx.delay == 0
|
||||
fx.delay = 0.016
|
||||
assert isclose(fx.delay, 0.016)
|
||||
with raises(ValueError): fx.delay = 0.017
|
||||
with raises(ValueError): fx.delay = -0.1
|
|
@ -0,0 +1,70 @@
|
|||
# Listener pytest module
|
||||
# Copyright (C) 2020 Ngô Xuân Minh
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This pytest module tries to test the correctness of the class Listener."""
|
||||
|
||||
from pytest import mark, raises
|
||||
|
||||
from math import inf
|
||||
|
||||
|
||||
def test_gain(context):
|
||||
"""Test write-only property gain."""
|
||||
context.listener.gain = 5/7
|
||||
context.listener.gain = 7/5
|
||||
context.listener.gain = 0
|
||||
context.listener.gain = inf
|
||||
with raises(ValueError): context.listener.gain = -1
|
||||
with raises(AttributeError): context.listener.gain
|
||||
|
||||
|
||||
@mark.parametrize('position', [(1, 0, 1), (1, 0, -1), (1, -1, 0),
|
||||
(1, 1, 0), (0, 0, 0), (1, 1, 1)])
|
||||
def test_position(context, position):
|
||||
"""Test write-only property position."""
|
||||
context.listener.position = position
|
||||
with raises(AttributeError): context.listener.position
|
||||
|
||||
|
||||
@mark.parametrize('velocity', [(420, 0, 69), (69, 0, -420), (0, 420, -69),
|
||||
(0, 0, 42), (0, 0, 0), (420, 69, 420)])
|
||||
def test_velocity(context, velocity):
|
||||
"""Test write-only property velocity."""
|
||||
context.listener.velocity = velocity
|
||||
with raises(AttributeError): context.listener.velocity
|
||||
|
||||
|
||||
@mark.parametrize(('at', 'up'), [
|
||||
((420, 0, 69), (0, 42, 0)), ((69, 0, -420), (0, -69, 420)),
|
||||
((0, 420, -69), (420, -69, 69)), ((0, 0, 42), (-420, -420, 0)),
|
||||
((0, 0, 0), (-420, -69, -69)), ((420, 69, 420), (69, -420, 0))])
|
||||
def test_orientaion(context, at, up):
|
||||
"""Test write-only property orientation."""
|
||||
context.listener.orientation = at, up
|
||||
with raises(AttributeError): context.listener.orientation
|
||||
|
||||
|
||||
def test_meters_per_unit(context):
|
||||
"""Test write-only property meters_per_unit."""
|
||||
context.listener.meters_per_unit = 4/9
|
||||
context.listener.meters_per_unit = 9/4
|
||||
with raises(ValueError): context.listener.meters_per_unit = 0
|
||||
context.listener.meters_per_unit = inf
|
||||
with raises(ValueError): context.listener.meters_per_unit = -1
|
||||
with raises(AttributeError): context.listener.meters_per_unit
|
|
@ -0,0 +1,345 @@
|
|||
# Source pytest module
|
||||
# Copyright (C) 2020 Nguyễn Gia Phong
|
||||
#
|
||||
# This file is part of palace.
|
||||
#
|
||||
# palace is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
#
|
||||
# palace is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with palace. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This pytest module tries to test the correctness of the class Source."""
|
||||
|
||||
from itertools import permutations, product, repeat
|
||||
from math import inf, pi
|
||||
from operator import is_
|
||||
from random import random, shuffle
|
||||
|
||||
from palace import Buffer, BaseEffect, Source, SourceGroup
|
||||
from pytest import raises
|
||||
|
||||
from fmath import FLT_MAX, allclose, isclose
|
||||
|
||||
|
||||
def test_comparison(context):
|
||||
"""Test basic comparisons."""
|
||||
with Source() as source0, Source() as source1, Source() as source2:
|
||||
assert source0 != source1
|
||||
sources = [source1, source1, source0, source2]
|
||||
sources.sort()
|
||||
sources.remove(source2)
|
||||
sources.remove(source0)
|
||||
assert sources[0] == sources[1]
|
||||
|
||||
|
||||
def test_bool(context):
|
||||
"""Test boolean value."""
|
||||
with Source() as source: assert source
|
||||
assert not source
|
||||
|
||||
|
||||
def test_control(context, flac):
|
||||
"""Test calling control methods."""
|
||||
with Buffer(flac) as buffer, buffer.play() as source:
|
||||
assert source.playing
|
||||
assert not source.paused
|
||||
source.pause()
|
||||
assert source.paused
|
||||
assert not source.playing
|
||||
source.resume()
|
||||
assert source.playing
|
||||
assert not source.paused
|
||||
source.stop()
|
||||
assert not source.playing
|
||||
assert not source.paused
|
||||
with raises(AttributeError): source.playing = True
|
||||
with raises(AttributeError): source.paused = True
|
||||
|
||||
|
||||
def test_fade_out_to_stop(context, mp3):
|
||||
"""Test calling method fade_out_to_stop."""
|
||||
with Buffer(mp3) as buffer, buffer.play() as source:
|
||||
source.fade_out_to_stop(5/7, buffer.length>>1)
|
||||
with raises(ValueError): source.fade_out_to_stop(0.42, -1)
|
||||
|
||||
|
||||
def test_group(context):
|
||||
"""Test read-write property group."""
|
||||
with Source(context) as source, SourceGroup(context) as source_group:
|
||||
assert source.group is None
|
||||
source.group = source_group
|
||||
assert source.group == source_group
|
||||
assert source in source_group.sources
|
||||
source.group = None
|
||||
assert source.group is None
|
||||
|
||||
|
||||
def test_priority(context):
|
||||
"""Test read-write property priority."""
|
||||
with Source(context) as source:
|
||||
assert source.priority == 0
|
||||
source.priority = 42
|
||||
assert source.priority == 42
|
||||
|
||||
|
||||
def test_offset(context, ogg):
|
||||
"""Test read-write property offset."""
|
||||
with Buffer(ogg) as buffer, buffer.play() as source:
|
||||
assert source.offset == 0
|
||||
length = buffer.length
|
||||
source.offset = length >> 1
|
||||
assert source.offset == length >> 1
|
||||
with raises(RuntimeError): source.offset = length
|
||||
with raises(OverflowError): source.offset = -1
|
||||
|
||||
|
||||
def test_offset_seconds(context, flac):
|
||||
"""Test read-only property offset_seconds."""
|
||||
with Buffer(flac) as buffer, buffer.play() as source:
|
||||
assert isinstance(source.offset_seconds, float)
|
||||
with raises(AttributeError):
|
||||
source.offset_seconds = buffer.length_seconds / 2
|
||||
|
||||
|
||||
def test_latency(context, aiff):
|
||||
"""Test read-only property latency."""
|
||||
with Buffer(aiff) as buffer, buffer.play() as source:
|
||||
assert isinstance(source.latency, int)
|
||||
with raises(AttributeError):
|
||||
source.latency = 42
|
||||
|
||||
|
||||
def test_latency_seconds(context, mp3):
|
||||
"""Test read-only property latency_seconds."""
|
||||
with Buffer(mp3) as buffer, buffer.play() as source:
|
||||
assert isinstance(source.latency_seconds, float)
|
||||
with raises(AttributeError):
|
||||
source.latency_seconds = buffer.length_seconds / 2
|
||||
|
||||
|
||||
def test_looping(context):
|
||||
"""Test read-write property looping."""
|
||||
with Source(context) as source:
|
||||
assert source.looping is False
|
||||
source.looping = True
|
||||
assert source.looping is True
|
||||
source.looping = False
|
||||
assert source.looping is False
|
||||
|
||||
|
||||
def test_pitch(context):
|
||||
"""Test read-write property pitch."""
|
||||
with Source(context) as source:
|
||||
assert isclose(source.pitch, 1)
|
||||
with raises(ValueError): source.pitch = -1
|
||||
source.pitch = 5 / 7
|
||||
assert isclose(source.pitch, 5/7)
|
||||
|
||||
|
||||
def test_gain(context):
|
||||
"""Test read-write property gain."""
|
||||
with Source(context) as source:
|
||||
assert isclose(source.gain, 1)
|
||||
with raises(ValueError): source.gain = -1
|
||||
source.gain = 5 / 7
|
||||
assert isclose(source.gain, 5/7)
|
||||
|
||||
|
||||
def test_gain_range(context):
|
||||
"""Test read-write property gain_range."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.gain_range, (0, 1))
|
||||
with raises(ValueError): source.gain_range = 9/11, 5/7
|
||||
with raises(ValueError): source.gain_range = 6/9, 420
|
||||
with raises(ValueError): source.gain_range = -420, 6/9
|
||||
source.gain_range = 5/7, 9/11
|
||||
assert allclose(source.gain_range, (5/7, 9/11))
|
||||
|
||||
|
||||
def test_distance_range(context):
|
||||
"""Test read-write property distance_range."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.distance_range, (1, FLT_MAX))
|
||||
with raises(ValueError): source.distance_range = 9/11, 5/7
|
||||
with raises(ValueError): source.distance_range = -420, 6/9
|
||||
with raises(ValueError): source.distance_range = 420, inf
|
||||
source.distance_range = 5/7, 9/11
|
||||
assert allclose(source.distance_range, (5/7, 9/11))
|
||||
source.distance_range = 1, FLT_MAX
|
||||
assert allclose(source.distance_range, (1, FLT_MAX))
|
||||
|
||||
|
||||
def test_position(context):
|
||||
"""Test read-write property position."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.position, (0, 0, 0))
|
||||
source.position = -1, 0, 1
|
||||
assert allclose(source.position, (-1, 0, 1))
|
||||
source.position = 4, 20, 69
|
||||
assert allclose(source.position, (4, 20, 69))
|
||||
|
||||
|
||||
def test_velocity(context):
|
||||
"""Test read-write property velocity."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.velocity, (0, 0, 0))
|
||||
source.velocity = -1, 0, 1
|
||||
assert allclose(source.velocity, (-1, 0, 1))
|
||||
source.velocity = 4, 20, 69
|
||||
assert allclose(source.velocity, (4, 20, 69))
|
||||
|
||||
|
||||
def test_orientation(context):
|
||||
"""Test read-write property orientation."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.orientation, ((0, 0, -1), (0, 1, 0)), allclose)
|
||||
source.orientation = (1, 1, -2), (3, -5, 8)
|
||||
assert allclose(source.orientation, ((1, 1, -2), (3, -5, 8)), allclose)
|
||||
|
||||
|
||||
def test_cone_angles(context):
|
||||
"""Test read-write property cone_angles."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.cone_angles, (360, 360))
|
||||
with raises(ValueError): source.cone_angles = 420, 69
|
||||
with raises(ValueError): source.cone_angles = -4.20, 69
|
||||
with raises(ValueError): source.cone_angles = 4.20, -69
|
||||
source.cone_angles = 4.20, 69
|
||||
assert allclose(source.cone_angles, (4.20, 69))
|
||||
|
||||
|
||||
def test_outer_cone_gains(context):
|
||||
"""Test read-write property outer_cone_gains."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.outer_cone_gains, (0, 1))
|
||||
with raises(ValueError): source.outer_cone_gains = 6/9, -420
|
||||
with raises(ValueError): source.outer_cone_gains = 6/9, 420
|
||||
with raises(ValueError): source.outer_cone_gains = -420, 6/9
|
||||
with raises(ValueError): source.outer_cone_gains = 420, 6/9
|
||||
source.outer_cone_gains = 5/7, 9/11
|
||||
assert allclose(source.outer_cone_gains, (5/7, 9/11))
|
||||
|
||||
|
||||
def test_rolloff_factors(context):
|
||||
"""Test read-write property rolloff_factors."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.rolloff_factors, (1, 0))
|
||||
with raises(ValueError): source.rolloff_factors = -6, 9
|
||||
with raises(ValueError): source.rolloff_factors = 6, -9
|
||||
source.rolloff_factors = 6, 9
|
||||
assert allclose(source.rolloff_factors, (6, 9))
|
||||
|
||||
|
||||
def test_doppler_factor(context):
|
||||
"""Test read-write property doppler_factor."""
|
||||
with Source(context) as source:
|
||||
assert isclose(source.doppler_factor, 1)
|
||||
with raises(ValueError): source.doppler_factor = -6.9
|
||||
with raises(ValueError): source.doppler_factor = 4.20
|
||||
source.doppler_factor = 5 / 7
|
||||
assert isclose(source.doppler_factor, 5/7)
|
||||
|
||||
|
||||
def test_relative(context):
|
||||
"""Test read-write property relative."""
|
||||
with Source(context) as source:
|
||||
assert source.relative is False
|
||||
source.relative = True
|
||||
assert source.relative is True
|
||||
source.relative = False
|
||||
assert source.relative is False
|
||||
|
||||
|
||||
def test_radius(context):
|
||||
"""Test read-write property radius."""
|
||||
with Source(context) as source:
|
||||
assert isclose(source.radius, 0)
|
||||
with raises(ValueError): source.radius = -1
|
||||
source.radius = 5 / 7
|
||||
assert isclose(source.radius, 5/7)
|
||||
|
||||
|
||||
def test_stereo_angles(context):
|
||||
"""Test read-write property stereo_angles."""
|
||||
with Source(context) as source:
|
||||
assert allclose(source.stereo_angles, (pi/6, -pi/6))
|
||||
source.stereo_angles = 420, -69
|
||||
assert allclose(source.stereo_angles, (420, -69))
|
||||
source.stereo_angles = -5/7, 9/11
|
||||
assert allclose(source.stereo_angles, (-5/7, 9/11))
|
||||
|
||||
|
||||
def test_spatialize(context):
|
||||
"""Test read-write property spatialize."""
|
||||
with Source(context) as source:
|
||||
assert source.spatialize is None
|
||||
source.spatialize = False
|
||||
assert source.spatialize is False
|
||||
source.spatialize = True
|
||||
assert source.spatialize is True
|
||||
source.spatialize = None
|
||||
assert source.spatialize is None
|
||||
|
||||
|
||||
def test_resampler_index(context):
|
||||
"""Test read-write property resampler_index."""
|
||||
with Source() as source:
|
||||
assert source.resampler_index == context.default_resampler_index
|
||||
with raises(ValueError): source.resampler_index = -1
|
||||
source.resampler_index = 69
|
||||
assert source.resampler_index == 69
|
||||
|
||||
|
||||
def test_air_absorption_factor(context):
|
||||
"""Test read-write property air_absorption_factor."""
|
||||
with Source(context) as source:
|
||||
assert isclose(source.air_absorption_factor, 0)
|
||||
with raises(ValueError): source.air_absorption_factor = -1
|
||||
with raises(ValueError): source.air_absorption_factor = 11
|
||||
source.air_absorption_factor = 420 / 69
|
||||
assert isclose(source.air_absorption_factor, 420/69)
|
||||
|
||||
|
||||
def test_gain_auto(context):
|
||||
"""Test read-write property gain_auto."""
|
||||
with Source(context) as source:
|
||||
assert all(gain is True for gain in source.gain_auto)
|
||||
for gain_auto in product(*repeat((False, True), 3)):
|
||||
source.gain_auto = gain_auto
|
||||
assert all(map(is_, source.gain_auto, gain_auto))
|
||||
|
||||
|
||||
def tests_sends(device, context):
|
||||
"""Test send paths assignment."""
|
||||
with Source() as source, BaseEffect() as effect:
|
||||
invalid_filter = [-1, 0, 1]
|
||||
for i in range(device.max_auxiliary_sends):
|
||||
source.sends[i].effect = effect
|
||||
source.sends[i].filter = random(), random(), random()
|
||||
shuffle(invalid_filter)
|
||||
with raises(ValueError): source.sends[i].filter = invalid_filter
|
||||
with raises(AttributeError): source.sends[i].effect
|
||||
with raises(AttributeError): source.sends[i].filter
|
||||
with raises(IndexError): source.sends[-1]
|
||||
with raises(TypeError): source.sends[4.2]
|
||||
with raises(TypeError): source.sends['0']
|
||||
with raises(TypeError): source.sends[6:9]
|
||||
with raises(AttributeError): source.sends = ...
|
||||
|
||||
|
||||
def test_filter(context):
|
||||
"""Test write-only property filter."""
|
||||
with Source() as source:
|
||||
with raises(AttributeError): source.filter
|
||||
source.filter = 1, 6.9, 5/7
|
||||
source.filter = 0, 0, 0
|
||||
for gain, gain_hf, gain_lf in permutations([4, -2, 0]):
|
||||
with raises(ValueError): source.filter = gain, gain_hf, gain_lf
|
17
tox.ini
17
tox.ini
|
@ -1,21 +1,26 @@
|
|||
[tox]
|
||||
envlist = py
|
||||
minversion = 3.3
|
||||
isolated_build = true
|
||||
isolated_build = True
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
flake8
|
||||
Cython
|
||||
scipy
|
||||
pytest-cov
|
||||
commands =
|
||||
flake8
|
||||
pytest
|
||||
commands = pytest
|
||||
setenv = CYTHON_TRACE = 1
|
||||
passenv = TRAVIS
|
||||
|
||||
[testenv:lint]
|
||||
skip_install = true
|
||||
deps = flake8
|
||||
commands = flake8
|
||||
|
||||
[flake8]
|
||||
filename = *.pxd, *.pyx, *.py
|
||||
hang-closing = True
|
||||
ignore = E225, E226, E227, E701, E704
|
||||
ignore = W503, E125, E225, E226, E227, E701, E704
|
||||
per-file-ignores = *.pxd:E501,E999
|
||||
; See https://github.com/PyCQA/pycodestyle/issues/906
|
||||
;max-doc-length = 72
|
||||
|
|
Loading…
Reference in New Issue