Compare commits

...

55 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
57 changed files with 3147 additions and 698 deletions

View File

@ -1,7 +1,7 @@
#!/bin/sh
set -ev
set -ex
git clone --depth 1 https://github.com/kcat/alure /tmp/alure
cd /tmp/alure/build
OPENALDIR=$(brew --prefix openal-soft) cmake -DCMAKE_FIND_FRAMEWORK=NEVER ..
cmake --build . --config Release
sudo cmake --install .
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

View File

@ -1,5 +1,5 @@
#!/bin/sh
set -ev
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

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

3
.gitignore vendored
View File

@ -138,3 +138,6 @@ dmypy.json
\#*\#
.\#*
*~
# VS Code
.vscode/

View File

@ -1,20 +1,20 @@
language: python
branches:
only:
- master
- /^\d+(\.\d+)+((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?$/
language: python
env:
global:
- TWINE_USERNAME=__token__
- MACOSX_DEPLOYMENT_TARGET=10.9
- CIBW_MANYLINUX_X86_64_IMAGE=manylinux2014
- CIBW_BEFORE_BUILD_LINUX=.ci/before-build-manylinux2014
- CIBW_BEFORE_BUILD_MACOS=.ci/before-build-macos
- CIBW_BEFORE_BUILD_LINUX=.ci/before-build-manylinux2014
- CIBW_MANYLINUX_X86_64_IMAGE=manylinux2014
- CIBW_REPAIR_WHEEL_COMMAND_MACOS=".ci/repair-whl-macos {wheel} {dest_dir}"
- CIBW_TEST_REQUIRES=tox
- CIBW_TEST_COMMAND_MACOS="tox -c /Users/travis/build/McSinyx/palace"
- CIBW_TEST_COMMAND_LINUX="tox -c /project"
- CIBW_TEST_COMMAND="tox -c {project}"
addons:
homebrew:
@ -32,16 +32,14 @@ jobs:
osx_image: xcode11.3
language: shell
env: CIBW_BUILD=cp36-macosx_x86_64
- os: osx
osx_image: xcode11.3
language: shell
env: CIBW_BUILD=cp37-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"
script: python3 -m cibuildwheel --output-dir=dist

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
@ -28,8 +28,8 @@ Palace can be install from the [Python Package Index][PyPI] via simply
pip install palace
Wheel distributions are built exclusively for amd64. Currently, only GNU/Linux
is properly supported. If you want to help packaging for Windows and macOS,
see [GH-1] and [GH-63] respectively on our issues tracker on GitHub.
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
@ -44,23 +44,19 @@ One may start with the `examples` for sample usage of palace.
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 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
@ -69,10 +65,9 @@ the build machine, which is similar to static linking:
[pip]: https://pip.pypa.io/en/latest/
[PyPI]: https://pypi.org/project/palace/
[GH-1]: https://github.com/McSinyx/palace/issues/1
[GH-63]: https://github.com/McSinyx/palace/issues/63
[CMake]: https://cmake.org/
[API]: https://mcsinyx.github.io/palace/html/reference.html
[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
[Vorbis]: https://xiph.org/vorbis/
[Opus]: http://opus-codec.org/
[libsndfile]: http://www.mega-nerd.com/libsndfile/
[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

@ -26,7 +26,7 @@ from palace import Context, Device, Source, decode
CHUNK_LEN: int = 12000
QUEUE_SIZE: int = 4
PERIOD: float = 0.01
PERIOD: float = 0.025
def play(files: Iterable[str], device: str) -> None:

View File

@ -23,7 +23,8 @@ from sys import stderr
from time import sleep
from typing import Iterable
from palace import reverb_preset_names, decode, Device, Context, Source, Effect
from palace import (reverb_preset_names, decode,
Device, Context, Source, ReverbEffect)
CHUNK_LEN: int = 12000
QUEUE_SIZE: int = 4
@ -48,11 +49,9 @@ 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)
with Source() as src, Effect() as fx:
print('Loading reverb preset', reverb)
fx.reverb_preset = reverb
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)

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.1.3
url = https://github.com/McSinyx/palace
version = 0.2.2
url = https://mcsinyx.github.io/palace
author = Nguyễn Gia Phong
author_email = mcsinyx@disroot.org
classifiers =
Development Status :: 3 - 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.
#
@ -21,13 +22,13 @@ import re
from distutils import log
from distutils.command.clean import clean
from distutils.dir_util import mkpath
from distutils.errors import DistutilsFileError
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 platform import system
from subprocess import DEVNULL, PIPE, run
from subprocess import DEVNULL, PIPE, run, CalledProcessError
from Cython.Build import cythonize
from setuptools import setup, Extension
@ -55,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)):

View File

@ -25,7 +25,7 @@ from libcpp.string cimport string
from libcpp.utility cimport pair
from libcpp.vector cimport vector
from std cimport duration, nanoseconds, milliseconds, shared_future, streambuf
from std cimport duration, nanoseconds, milliseconds, streambuf
# OpenAL and Alure auxiliary declarations
@ -84,37 +84,37 @@ cdef extern from 'alure2-typeviews.h' namespace 'alure' nogil:
# Alure main module
cdef extern from 'alure2.h' nogil:
cdef cppclass EFXEAXREVERBPROPERTIES:
float flDensity
float flDiffusion
float flGain
float flGainHF
float flGainLF
float flDecayTime
float flDecayHFRatio
float flDecayLFRatio
float flReflectionsGain
float flReflectionsDelay
float flReflectionsPan[3]
float flLateReverbGain
float flLateReverbDelay
float flLateReverbPan[3]
float flEchoTime
float flEchoDepth
float flModulationTime
float flModulationDepth
float flAirAbsorptionGainHF
float flHFReference
float flLFReference
float flRoomRolloffFactor
int iDecayHFLimit
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:
int iWaveform
int iPhase
float flRate
float flDepth
float flFeedback
float flDelay
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:
@ -129,7 +129,6 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
# String: string
# StringView: string
# SharedPtr: shared_ptr
# SharedFuture: shared_future
# Structs:
cdef cppclass AttributePair:
@ -142,9 +141,9 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
unsigned send 'mSend'
# Enum classes:
cdef enum SampleType:
ctypedef enum SampleType:
pass
cdef enum ChannelConfig:
ctypedef enum ChannelConfig:
pass
# The following relies on C++ implicit conversion from char* to string.
@ -153,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 enum DistanceModel:
ctypedef enum DistanceModel:
pass
cdef enum Spatialize:
ctypedef enum Spatialize:
Off 'alure::Spatialize::Off'
On 'alure::Spatialize::On'
Auto 'alure::Spatialize::Auto'
@ -209,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 +
@ -274,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 +
@ -328,13 +297,8 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
int get_default_resampler_index 'getDefaultResamplerIndex'() 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 +
@ -350,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 +
@ -414,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 +
@ -476,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 +
@ -556,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 +
@ -595,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 +
@ -625,42 +492,26 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
size_t get_use_count 'getUseCount'() except +
cdef cppclass Effect:
ctypedef EffectImpl* handle_type
Effect() # nil
Effect(EffectImpl*)
Effect(const Effect&)
Effect(Effect&&)
Effect& operator=(const Effect&)
Effect& operator=(Effect&&)
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()
handle_type get_handle 'getHandle'()
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:
@ -669,6 +520,7 @@ cdef extern from 'alure2.h' namespace 'alure' nogil:
cdef cppclass FileIOFactory:
@staticmethod
unique_ptr[FileIOFactory] set(unique_ptr[FileIOFactory])
@staticmethod
FileIOFactory& get()

File diff suppressed because it is too large Load Diff

View File

@ -37,12 +37,6 @@ cdef extern from '<iostream>' namespace 'std' nogil:
istream(streambuf*) except +
cdef extern from '<future>' namespace 'std' nogil:
cdef cppclass shared_future[R]:
R& get() except +
boolean valid() const
cdef extern from '<ratio>' namespace 'std' nogil:
cdef cppclass nano:
pass

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,23 +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
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
def mp3():
"""Provide a sample MP3 file."""
return join(DATA_DIR, '353684__tec-studio__drip2.mp3')
@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,71 +0,0 @@
# Listener pytest module
# 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 Listener."""
from pytest import raises
from math import inf
def test_gain(context):
"""Test write 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
def test_position(context):
"""Test write property position."""
context.listener.position = 1, 0, 1
context.listener.position = 1, 0, -1
context.listener.position = 1, -1, 0
context.listener.position = 1, 1, 0
context.listener.position = 0, 0, 0
context.listener.position = 1, 1, 1
def test_velocity(context):
"""Test write property velocity."""
context.listener.velocity = 420, 0, 69
context.listener.velocity = 69, 0, -420
context.listener.velocity = 0, 420, -69
context.listener.velocity = 0, 0, 42
context.listener.velocity = 0, 0, 0
context.listener.velocity = 420, 69, 420
def test_orientaion(context):
"""Test write property orientation."""
context.listener.orientation = (420, 0, 69), (0, 42, 0)
context.listener.orientation = (69, 0, -420), (0, -69, 420)
context.listener.orientation = (0, 420, -69), (420, -69, 69)
context.listener.orientation = (0, 0, 42), (-420, -420, 0)
context.listener.orientation = (0, 0, 0), (-420, -69, -69)
context.listener.orientation = (420, 69, 420), (69, -420, 0)
def test_meters_per_unit(context):
"""Test write property meter_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

38
tests/unit/conftest.py Normal file
View File

@ -0,0 +1,38 @@
# Test fixtures for unit 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/>.
"""This module provide default objects of palace classes as fixtures
for convenient testing.
"""
from pytest import fixture
from palace import Device, Context
@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 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))

View File

@ -1,4 +1,4 @@
# Source pytest module
# 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
@ -26,21 +26,29 @@ from pytest import raises
from math import inf
def test_with_context(device):
"""Test if `with` can be used to start a context
and is destroyed properly.
"""
with Context(device) as context:
assert current_context() == context
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_nested_context_manager(device):
"""Test if the context manager returns to the
previous context.
"""
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:
with Context(device): pass
assert current_context() == context
# At the moment these are no-op.
context.start_batch()
context.end_batch()
def test_message_handler(device):
@ -61,36 +69,48 @@ def test_async_wake_interval(device):
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 return values default_resampler_index."""
"""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 property doppler_factor."""
"""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 property speed_of_sound."""
"""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 preset values distance_model."""
"""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

View File

@ -18,16 +18,59 @@
"""This pytest module tries to test the correctness of the class Source."""
from itertools import product, repeat
from itertools import permutations, product, repeat
from math import inf, pi
from operator import is_
from random import random, shuffle
from palace import Source, SourceGroup
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:
@ -47,11 +90,39 @@ def test_priority(context):
assert source.priority == 42
def test_offset(context):
def test_offset(context, ogg):
"""Test read-write property offset."""
with Source(context) as source:
with Buffer(ogg) as buffer, buffer.play() as source:
assert source.offset == 0
# TODO: give the source a decoder to seek
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):
@ -129,9 +200,9 @@ def test_velocity(context):
def test_orientation(context):
"""Test read-write property orientation."""
with Source(context) as source:
assert all(map(allclose, source.orientation, ((0, 0, -1), (0, 1, 0))))
assert allclose(source.orientation, ((0, 0, -1), (0, 1, 0)), allclose)
source.orientation = (1, 1, -2), (3, -5, 8)
assert all(map(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):
@ -220,8 +291,8 @@ def test_spatialize(context):
def test_resampler_index(context):
"""Test read-write property resampler_index."""
with Source(context) as source:
# TODO: test initial value
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
@ -244,3 +315,31 @@ def test_gain_auto(context):
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,18 +1,21 @@
[tox]
envlist = py
minversion = 3.3
isolated_build = true
isolated_build = True
[testenv]
setenv =
CYTHON_TRACE = 1
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