Compare commits

...

97 Commits

Author SHA1 Message Date
Nguyễn Gia Phong 566911efad
Build wheels for CPython 3.9 and manylinux aarch64 2020-11-08 20:05:20 +07:00
Ngô Ngọc Đức Huy 708f23b35a
Write tutorial for source effect
Continue stabbing at GH-93
2020-10-06 20:39:56 +07:00
Nguyễn Gia Phong c5861833ab s/Notes/Note/ 2020-10-06 15:55:25 +07:00
MostDeadDeveloper 1ba92b7c50
Use RTD theme for HTML documentation (fix GH-119) 2020-10-04 14:26:34 +07:00
Nguyễn Gia Phong 74a9989e53 Use peaceiris/actions-gh-pages for docs deploy 2020-10-02 21:28:34 +07:00
Nguyễn Gia Phong fae0848283 Follow up on GH-116
* Include docs in source distributions
* Prevent docs deploy job from failing when there's nothing to commit
2020-09-11 15:12:38 +07:00
Nguyễn Gia Phong 90528cee2d Interlink backquoted objects in docstring 2020-09-09 17:38:23 +07:00
Nguyễn Gia Phong 848421353d Create FUNDING.yml 2020-09-09 17:38:19 +07:00
Nguyễn Gia Phong e576dc72ab Update pointers to documentation 2020-08-24 21:57:53 +07:00
Nguyễn Gia Phong 02d4889138 Deploy documentation using GitHub Action 2020-08-24 21:49:20 +07:00
Nguyễn Gia Phong 9a1dbf5d94 Move documentation to docs/ 2020-08-24 21:41:22 +07:00
Huy-Ngo 08408c56c7 Write source manipulation tutorial 2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong a0b4a90f79 Split the reference into multiple pages 2020-08-24 21:41:22 +07:00
Huy Ngo fa513ea096 Writing first two sections of tutorial 2020-08-24 21:41:22 +07:00
Ngô Ngọc Đức Huy 4caaee5727 Changing layout for for palace
Make the sidebar fixed and add prev/next navigation
2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong 11cc59fc86 Add contributing guidelines and project overview
Co-Authored-By: Ngô Ngọc Đức Huy <duchuy29092000@gmail.com>
2020-08-24 21:41:22 +07:00
Huy Ngo 625b166261 Change structure due to change in design 2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong 7fd5d49b3a Add Copying section 2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong f3da7f8e24 Restructure the reference section 2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong a8e039a9f0 Add design principles 2020-08-24 21:41:22 +07:00
Huy-Ngo 92f2454b96 Add installation instruction to the doc 2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong 4e007b4518 Build the docs the first time
This is actually my thrid try,
but on this branch, it is the first time, OK?
2020-08-24 21:41:22 +07:00
Nguyễn Gia Phong c2a848cf6c Set up GitHub Action for linting
This will give faster linting feedback since we can skip compilation.
2020-08-24 15:32:42 +07:00
Nguyễn Gia Phong 2e39a68865 Check manifest 2020-08-08 17:05:32 +07:00
Nguyễn Gia Phong 587179fecf Nitpick packaging
This follows up GH-112.
2020-07-31 16:32:22 +07:00
Francesco Caliumi 3a9e23917f
Enhance setup.py cmake errors reporting 2020-07-31 14:51:12 +07:00
Nguyễn Gia Phong c78f14fb42 Nitpick numpydoc section names
/cc GH-102 and GH-109
2020-07-23 21:39:33 +07:00
Nguyễn Gia Phong 6f00f7046a Housekeep and partially tackle GH-109 2020-06-19 14:34:28 +07:00
Nguyễn Gia Phong c216f307ef Fix incorrect multivalue return type
This fixes GH-107 and supersedes and closes GH-108.
2020-06-02 10:42:53 +07:00
Nguyễn Gia Phong c24df3143b Opt out CI/CD for Python 3.7 and 3.8 on macOS
As per GH-63, wheels built on these platforms
are not properly repaired.
2020-05-16 17:35:53 +07:00
Ngô Xuân Minh 8285eb06c5
PEP 257 compliance
Rework docstrings following PEP 257
2020-05-12 22:38:18 +07:00
Nguyễn Gia Phong 83315de9c8 Retouch docstrings for multi-value properties
This fixes GH-64.  Moreover switch back to flake8 since pytest-flake8
is not as configurable.
2020-05-09 21:52:20 +07:00
Ngô Ngọc Đức Huy 24192360f8
Change cdef enum to ctypedef enum to avoid redeclaration
Apparently this redeclaration error (C3431) only happens on Windows.
2020-05-08 12:31:25 +07:00
Nguyễn Gia Phong 292ef0393d Bump version to 0.2.0 2020-05-04 22:50:08 +07:00
Ngô Xuân Minh c59d0ec169
Fix ReverbEffect
Resolve GH-88
2020-05-04 22:32:03 +07:00
Ngô Xuân Minh b39aaa9903
Fix ChorusEffect (#91)
To make properties properly raise ValueError
2020-05-03 20:53:57 +07:00
Nguyễn Gia Phong 11cda099a3
Opt in Travis CI testing for Python 3.6 on macOS
/r/oddlyspecific
2020-05-03 20:00:44 +07:00
Nguyễn Gia Phong a2444f0eab Prepare for 0.2 release 2020-05-02 21:08:37 +07:00
Nguyễn Gia Phong 5406740517
Repair wheels on macOS correctly 2020-05-02 18:03:57 +07:00
Nguyễn Gia Phong 7ff1d8f1d7 Complete tests for context and fix discovered bugs
Namely the impliit use without checking of current_context
which can cause segfault if it is None.
2020-04-30 17:01:00 +07:00
Ngô Xuân Minh e62989fa2b
Intuitively implement properties of ChorusEffect (#90)
Intuitively implement properties of ChorusEffect and add docstrings.
2020-04-28 23:15:22 +07:00
Huy Ngo 1aa054e8ed Update version number for new release 2020-04-28 22:46:24 +07:00
Huy Ngo 4f98c8e343 Add notice for credits 2020-04-28 22:43:42 +07:00
Huy Ngo 8016b78167 Remove redundant credit information. 2020-04-28 22:41:11 +07:00
Huy Ngo b884a07903 Add tests for ChorusEffect 2020-04-27 13:52:35 +07:00
Huy Ngo fdec565aaf Add tests for ReverbEffect 2020-04-27 13:52:01 +07:00
Ngô Ngọc Đức Huy b2329a1428
Fixing some small mistakes made in 9de664a (#86) 2020-04-26 09:49:30 +07:00
Nguyễn Gia Phong 9de664a750 Make effects properties more intuitive (fix GH-85)
Additionally run flake8 within pytest by default for better CI experience.
2020-04-25 21:33:17 +07:00
Ngô Ngọc Đức Huy 88c5b0b57a
Add some tests for Effect (#84)
Only reverb_properties and chorus_properties are not tested
2020-04-25 17:15:16 +07:00
Nguyễn Gia Phong 5ce35416c7 Opt macOS out of automated testing entirely
I spent too much time to day fixing up macOS related issue,
when the wheel for it is not even built properly, so I decide
to solve it all together next time.
2020-04-24 20:14:24 +07:00
Nguyễn Gia Phong 2218d192cb Test message handling 2020-04-24 17:46:54 +07:00
Nguyễn Gia Phong d575e5e15a Complete unit tests for Source and clean up 2020-04-24 09:15:07 +07:00
Nguyễn Gia Phong e95dad16bf
Allow examples to fail on macOS CI 2020-04-23 13:29:17 +07:00
Nguyễn Gia Phong 944dd067eb Make functional tests run faster
This resolves GH-81.
2020-04-22 22:34:53 +07:00
Ngô Ngọc Đức Huy 6148318585
Add functional tests (#79)
Add functional tests
2020-04-22 01:25:09 +07:00
Nguyễn Gia Phong e674ade7e4 Update CI/CD
This optimizes the execution time of macOS builds and adds initial
(no-op) support for Windows.  Documentation now admits the issues with
macOS wheels.

Because of GH-63, CI/CD for Python 3.8 on macOS is temporarily dropped.

Build reference for upcoming v0.1.3
2020-04-22 00:29:53 +07:00
Ngô Ngọc Đức Huy c9134b31e1
Add resamplers' info for palace-info (#75) 2020-04-18 08:06:36 +07:00
Nguyễn Gia Phong 8a6ce4bd13 Make send paths easier to use (fix GH-66)
Also nit exceptions and one-liners.
2020-04-17 20:40:39 +07:00
Nguyễn Gia Phong 30c11b351d Abstract away AuxiliaryEffectSlot
Address the first part of GH-66.  Also clean up comparison code.
2020-04-17 11:07:39 +07:00
Ngô Xuân Minh 8972112d16
Implement test listener (#76)
Implement test for listener class
2020-04-17 08:53:10 +07:00
Ngô Xuân Minh a25c93bbb1
Nitpick test_source, implement test_context. (#70)
Implement test_context

Tests that have implemented:
* async wake interval
* default resampler index
* doppler factor
* speed of sound
* distance model
2020-04-13 21:53:30 +07:00
Nguyễn Gia Phong a9d743ae87 Nitpick examples and documentation 2020-04-13 21:33:46 +07:00
Ngô Ngọc Đức Huy c992a8a6b0
Add example for tone generating (#69)
Waveforms include:
- sine
- square
- triangle
- sawtooth
- impulse
- white noise
2020-04-13 20:28:32 +07:00
Nguyễn Gia Phong 6e95bf491c Uniform enum handling (resolve GH-47) 2020-04-09 15:05:06 +07:00
Huy Ngo b5ab03eb7e Change from for loop to while loop 2020-04-07 11:56:54 +07:00
Nguyễn Gia Phong 09f14bf7f0 Add cache free function and nit strings 2020-04-07 11:52:06 +07:00
Ngô Ngọc Đức Huy ae913f88de
Writing example for latency (#67) 2020-04-06 17:34:58 +07:00
Nguyễn Gia Phong bdfe30306d Revisit a few design desisions 2020-04-05 12:36:41 +07:00
Nguyễn Gia Phong 80a9d88e90 Revise MessageHandler
As a side effect, Context.precache_buffers_async no longer causes segfault.
2020-04-04 22:58:01 +07:00
Nguyễn Gia Phong 38a3a21f43 Clean up for release 0.1 2020-04-01 15:17:23 +07:00
Ngô Ngọc Đức Huy aeda09d04e
Finishing the Source class (#65)
* Define make_param_filters() for converting params to struct

* Write `direct_filter`, `send_filter`, and `auxiliary_send_filter`.

* Update copyright information
2020-04-01 14:41:48 +07:00
Nguyễn Gia Phong e747159161 Move utilities to native C++ 2020-03-31 11:35:09 +07:00
Nguyễn Gia Phong 47231e9992 Fix forward declaration regression with binding on 2020-03-30 20:53:28 +07:00
Nguyễn Gia Phong d29d8debe8 Compile Cython with binding and allow cleaning C++ output 2020-03-30 18:07:31 +07:00
Nguyễn Gia Phong 8fa8f83346 Add reverb example 2020-03-30 17:32:13 +07:00
Nguyễn Gia Phong b08c711dbc Declare predefined reverb presets (close GH-62)
Declare predefined reverb presets
2020-03-30 17:31:02 +07:00
Ngô Ngọc Đức Huy ac3d826b72 Finishing the functions in Context
* Implement DistanceModel with Enum
* Implement precache_buffers_async
2020-03-28 21:24:05 +07:00
Nguyễn Gia Phong 49072f101e Allow falling back on current context 2020-03-26 22:27:14 +07:00
Nguyễn Gia Phong 2a3bda152f Add context manager for thread-local context preference 2020-03-26 15:26:12 +07:00
Ngô Xuân Minh 47c565f25e
Merging `new-eff` to `master` and finished relevant methods (#58)
Implement Effect and relevant methods
2020-03-23 20:38:21 +07:00
Nguyễn Gia Phong 45a284b17a Revise read-only properties, warning and debug strings 2020-03-22 16:06:19 +07:00
Nguyễn Gia Phong 40f63f738a Revisit some design decisions
Namely buffer creation and device names (fix GH-7)
2020-03-21 22:45:02 +07:00
Ngô Ngọc Đức Huy caf43f7b1d
Merge pull request #53 from McSinyx/ctx-fin
Adding context functionalities
2020-03-20 19:31:16 +07:00
Huy Ngo 12ebb6a7ea Implement ArrayView 2020-03-20 16:41:51 +07:00
Huy Ngo ad756e692a Declaring DistanceModel enum class 2020-03-20 16:41:51 +07:00
Huy Ngo 266cca6c6a Enable threading in use_context and current_context 2020-03-20 16:41:40 +07:00
Nguyễn Gia Phong d05d46cbf4 Nitpick
* Destroy buffer in stdec example after use
* Prefer pass-by-reference in C++
* Prefer Python-style type annotation
* Avoid iterator of C++ object wrappers
* Make Source comparable
* Fix GH-22
2020-03-19 11:59:28 +07:00
Nguyễn Gia Phong 3967e0cf1c Add custom decoder factory example 2020-03-16 20:26:44 +07:00
Nguyễn Gia Phong 3cc53d56df Allow intergration of user-defined decoders
The type system is also revised.
2020-03-16 20:26:44 +07:00
Nguyễn Gia Phong 5df009875b Implement FileIOFactory bridge
While the factory indeed works, Python file I/O called from C++
causes similar GIL deadlock as seen before with BaseDecoder.
2020-03-16 20:26:44 +07:00
Nguyễn Gia Phong f9dce3e6fd Prepare C++ bases for FileIOFactory
C++ code is reformatted according to Octave style guide.
Additionally, C++14 is now required to be future-proof with alure.
2020-03-16 20:26:44 +07:00
Nguyễn Gia Phong fc1bbfafeb Expose enumerants necessary for context creation (resolve #16)
Additionally in-source docs are revised.
2020-03-09 17:54:49 +07:00
Nguyễn Gia Phong 16bdb47c5a Improve tests
Namely avoiding sharing testing object and completing test for #41.
2020-03-05 22:19:23 +07:00
Nguyễn Gia Phong 308671bc40 Fix reference count and reference of message handler
This should close #41.
2020-03-05 15:17:22 +07:00
Nguyễn Gia Phong 7f2d1cdf7e Finish Buffer and document Context missing methods 2020-03-01 22:25:51 +07:00
Nguyễn Gia Phong 428a436f36 [Travis] Build for macOS
Also enable Cython trace explicitly for tests
2020-03-01 16:20:44 +07:00
Ngô Xuân Minh 51a72e94f9 Get rid of __init__(None) (#43) 2020-03-01 16:20:40 +07:00
67 changed files with 5286 additions and 1230 deletions

24
.appveyor.yml Normal file
View File

@ -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

7
.ci/before-build-macos Executable file
View File

@ -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

16
.ci/before-build-manylinux2014 Executable file
View File

@ -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

12
.ci/repair-whl-macos Executable file
View File

@ -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

12
.github/FUNDING.yml vendored Normal file
View File

@ -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']

48
.github/workflows/docs.yml vendored Normal file
View File

@ -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

28
.github/workflows/lint.yml vendored Normal file
View File

@ -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

10
.gitignore vendored
View File

@ -131,7 +131,13 @@ dmypy.json
# Pyre type checker
.pyre/
# IDE & editors
# PyCharm
.idea/
# Emacs backup files patterns
# Emacs
\#*\#
.\#*
*~
# VS Code
.vscode/

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

19
docs/Makefile Normal file
View File

@ -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)

35
docs/make.bat Normal file
View File

@ -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

36
docs/source/conf.py Normal file
View File

@ -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 = []

View File

@ -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

75
docs/source/copying.rst Normal file
View File

@ -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/

185
docs/source/design.rst Normal file
View File

@ -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

45
docs/source/index.rst Normal file
View File

@ -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

View File

@ -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/

View File

@ -0,0 +1,17 @@
Resource Caching
================
.. currentmodule:: palace
Audio Buffers
-------------
.. autoclass:: Buffer
:members:
Loading & Freeing in Batch
--------------------------
.. autofunction:: cache
.. autofunction:: free

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
File I/O Interface
==================
.. currentmodule:: palace
.. autofunction:: current_fileio
.. autofunction:: use_fileio
.. autoclass:: FileIO
:members:

View File

@ -0,0 +1,14 @@
Reference
=========
API reference is divided into the following sections:
.. toctree::
device
context
buffer
source
effect
decoder
file-io

View File

@ -0,0 +1,16 @@
Sources & Source Groups
=======================
.. currentmodule:: palace
Sources
-------
.. autoclass:: Source
:members:
Source Groups
-------------
.. autoclass:: SourceGroup
:members:

View File

@ -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>

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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__':

View File

@ -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)

View File

@ -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!')

57
examples/palace-latency.py Executable file
View File

@ -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)

82
examples/palace-reverb.py Executable file
View File

@ -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)

109
examples/palace-stdec.py Executable file
View File

@ -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)

95
examples/palace-tonegen.py Executable file
View File

@ -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)

View File

@ -1,3 +1,3 @@
[build-system]
requires = ['setuptools>=43', 'wheel>=0.31', 'Cython']
build-backend = "setuptools.build_meta"
build-backend = 'setuptools.build_meta'

View File

@ -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

View File

@ -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')))

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 +

150
src/util.h Normal file
View File

@ -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

39
src/util.pxd Normal file
View File

@ -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])

View File

@ -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.

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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

View File

@ -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))

116
tests/unit/test_context.py Normal file
View File

@ -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

437
tests/unit/test_effect.py Normal file
View File

@ -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

View File

@ -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

345
tests/unit/test_source.py Normal file
View File

@ -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
View File

@ -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