From a548e270e8e3b25a12bbb27d2629e44eb12a7859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= Date: Sun, 22 Dec 2019 16:26:51 +0700 Subject: [PATCH] Prove the concept by the first few examples --- .gitignore | 3 + README.md | 11 + alure.pxd | 488 ++++++++++++++++++++++++++++++++++++++ archaicy.pyx | 336 ++++++++++++++++++++++++++ examples/archaicy-hrtf.py | 103 ++++++++ examples/archaicy-play.py | 82 +++++++ pyproject.toml | 3 + setup.py | 40 ++++ 8 files changed, 1066 insertions(+) create mode 100644 alure.pxd create mode 100644 archaicy.pyx create mode 100755 examples/archaicy-hrtf.py create mode 100755 examples/archaicy-play.py create mode 100644 pyproject.toml create mode 100755 setup.py diff --git a/.gitignore b/.gitignore index b6e4761..dc46139 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# C++ files compiled by Cython +*.cpp + # Distribution / packaging .Python build/ diff --git a/README.md b/README.md index cd2ff40..af3290c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # archaicy Pythonic wrapper for Audio Library Utilities REtooled in Cython + +## Prerequisites +Archaicy requires Python 3.5+ and [alure](https://github.com/kcat/alure). +Currently only GNU/Linux is supported. If you want to help package for +other operating systems, head to issue #1. + +## Installation +```sh +python setup.py build +python setup.py install +``` diff --git a/alure.pxd b/alure.pxd new file mode 100644 index 0000000..7dadeda --- /dev/null +++ b/alure.pxd @@ -0,0 +1,488 @@ +# Cython declarations of alure +# Copyright (C) 2019 Nguyễn Gia Phong +# +# This file is part of archaicy. +# +# archaicy 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. +# +# archaicy 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 archaicy. If not, see . + +from libc.stdint cimport uint64_t +from libcpp cimport bool, nullptr_t +from libcpp.memory cimport shared_ptr +from libcpp.pair cimport pair +from libcpp.string cimport string +from libcpp.vector cimport vector + + +cdef extern from '' namespace 'std' nogil: + cdef cppclass shared_future[R]: + pass + + +cdef extern from '' nogil: + cdef int ALC_TRUE + + +cdef extern from '' nogil: + cdef int ALC_HRTF_SOFT + cdef int ALC_HRTF_ID_SOFT + + +cdef extern from '' namespace 'alure::PlaybackName' nogil: + cdef enum PlaybackName 'alure::PlaybackName': + Basic + Full + + +cdef extern from '' namespace 'alure' nogil: + # Type aliases: + # char*: string + # ALfloat: float + # ALsizei: int + # ALuint: unsigned + # Vector: vector + # ArrayView: vector + # String: string + # StringView: string + # SharedPtr: shared_ptr + # SharedFuture: shared_future + + # Structs: + cdef cppclass AttributePair: + int mAttribute + int mValue + + cdef cppclass FilterParams: + pass + cdef cppclass SourceSend: + pass + + + # Enum classes: + cdef cppclass SampleType: + pass + # The following relies on C++ implicit conversion from char* to string. + cdef const string get_sample_type_name 'GetSampleTypeName'(SampleType) except + + + cdef cppclass ChannelConfig: + pass + cdef const string get_channel_config_name 'GetChannelConfigName'(ChannelConfig) except + + cdef unsigned frames_to_bytes 'FramesToBytes'(unsigned, ChannelConfig, SampleType) except + + cdef unsigned bytes_to_frames 'BytesToFrames'(unsigned, ChannelConfig, SampleType) + + cdef cppclass DeviceEnumeration: + pass + + cdef cppclass DefaultDeviceType: + pass + + cdef cppclass DistanceModel: + pass + + cdef cppclass Spatialize: + pass + + + # Helper classes + cdef cppclass Vector3: + pass + cdef cppclass Version: + pass + + + # Opaque class implementations: + cdef cppclass DeviceManagerImpl: + pass + cdef cppclass DeviceImpl: + pass + cdef cppclass ContextImpl: + pass + cdef cppclass ListenerImpl: + pass + cdef cppclass BufferImpl: + pass + cdef cppclass SourceImpl: + pass + cdef cppclass SourceGroupImpl: + pass + cdef cppclass AuxiliaryEffectSlotImpl: + pass + cdef cppclass EffectImpl: + pass + + + # Available class interfaces: + cdef cppclass DeviceManager: + @staticmethod + DeviceManager get_instance 'getInstance'() except + + + DeviceManager() + DeviceManager(const DeviceManager&) + DeviceManager(DeviceManager&&) + + DeviceManager& operator=(const DeviceManager&) + DeviceManager& operator=(DeviceManager&&) + DeviceManager& operator=(nullptr_t) + + bool operator bool() + + bool query_extension 'queryExtension'(const string&) except + + + vector[string] enumerate(DeviceEnumeration) except + + string 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&&) + + bool operator==(const Device&) + bool operator!=(const Device&) + bool operator<=(const Device&) + bool operator>=(const Device&) + bool operator<(const Device&) + bool operator>(const Device&) + + bool operator bool() + + handle_type get_handle 'getHandle'() + + string get_name 'getName'() except + + string get_name 'getName'(PlaybackName) except + + + bool query_extension 'queryExtension'(const string&) except + + + Version get_alc_version 'getALCVersion'() except + + Version get_efx_version 'getEFXVersion'() except + + + unsigned get_frequency 'getFrequency'() except + + unsigned get_max_auxiliary_sends 'getMaxAuxiliarySends'() except + + + vector[string] enumerate_hrtf_names 'enumerateHRTFNames'() except + + bool is_hrtf_enabled 'isHRTFEnabled'() except + + string get_current_hrtf 'getCurrentHRTF'() except + + + void reset(vector[AttributePair]) except + + + Context create_context 'createContext'() except + + Context create_context 'createContext'(vector[AttributePair]) except + + + void pause_dsp 'pauseDSP'() except + + void resume_dsp 'resumeDSP'() except + + + # get_clock_time + + 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&&) + + bool operator==(const Context&) + bool operator!=(const Context&) + bool operator<=(const Context&) + bool operator>=(const Context&) + bool operator<(const Context&) + bool operator>(const Context&) + + bool 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 + + + void destroy() except + + + Device get_device 'getDevice'() except + + + void start_batch 'startBatch'() except + + void end_batch 'endBatch'() except + + + Listener get_listener 'getListener'() except + + + shared_ptr[MessageHandler] set_message_handler 'setMessageHandler'(shared_ptr[MessageHandler]) except + + shared_ptr[MessageHandler] get_message_handler 'getMessageHandler'() except + + + # set_async_wake_interval + # get_async_wake_interval + + shared_ptr[Decoder] create_decoder 'createDecoder'(string) except + + + bool is_supported 'isSupported'(ChannelConfig, SampleType) except + + + vector[string] get_available_resamplers 'getAvailableResamplers'() except + + int get_default_resampler_index 'getDefaultResamplerIndex'() except + + + Buffer get_buffer 'getBuffer'(string) except + + shared_future[Buffer] get_buffer_async 'getBufferAsync'(string) except + + + void precache_buffers_async 'precacheBuffersAsync'(vector[string]) except + + + 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 + + + Source create_source 'createSource'() except + + AuxiliaryEffectSlot create_auxiliary_effect_slot 'createAuxiliaryEffectSlot'() except + + Effect create_effect 'createEffect'() except + + SourceGroup create_source_group 'createSourceGroup'() except + + + void set_doppler_factor 'setDopplerFactor'(float) except + + void set_speed_of_sound 'setSpeedOfSound'(float) except + + void set_distance_model 'setDistanceModel'(DistanceModel) except + + + void update() except + + + + cdef cppclass Listener: + pass + + + cdef cppclass Buffer: + ctypedef BufferImpl* handle_type + + Buffer() + Buffer(BufferImpl*) + Buffer(const Buffer&) + Buffer(Buffer&&) + + Buffer& operator=(const Buffer&) + Buffer& operator=(Buffer&&) + + bool operator==(const Buffer&) + bool operator!=(const Buffer&) + bool operator<=(const Buffer&) + bool operator>=(const Buffer&) + bool operator<(const Buffer&) + bool operator>(const Buffer&) + + bool operator bool() + + handle_type get_handle 'getHandle'() + + unsigned get_length 'getLength'() except + + unsigned get_frequency 'getFrequency'() except + + ChannelConfig get_channel_config 'getChannelConfig'() except + + SampleType get_sample_type 'getSampleType'() except + + unsigned get_size 'getSize'() except + + string get_name 'getName'() except + + size_t get_source_count 'getSourceCount'() except + + vector[Source] get_sources 'getSources'() except + + pair[unsigned, unsigned] get_loop_points 'getLoopPoints'() except + + void set_loop_points 'setLoopPoints'(unsigned, unsigned) except + + + + cdef cppclass Source: + ctypedef SourceImpl* handle_type + + Source() + Source(SourceImpl*) + Source(const Source&) + Source(Source&&) + + Source& operator=(const Source&) + Source& operator=(Source&&) + + bool operator==(const Source&) + bool operator!=(const Source&) + bool operator<=(const Source&) + bool operator>=(const Source&) + bool operator<(const Source&) + bool operator>(const Source&) + + bool 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 + + # fade_out_to_stop + void pause() except + + void resume() except + + + bool is_pending 'isPending'() except + + bool is_playing 'isPlaying'() except + + bool is_paused 'isPaused'() except + + bool is_playing_or_pending 'isPlayingOrPending'() except + + + void set_group 'setGroup'(SourceGroup) except + + SourceGroup get_group 'getGroup'() except + + + void set_priority 'setPriority'(unsigned) except + + unsigned get_priority 'getPriority'() except + + + void set_offset 'setOffset'(uint64_t) except + + # get_sample_offset_latency + uint64_t get_sample_offset 'getSampleOffset'() except + + # get_sec_offset_latency + # get_sec_offset + + void set_looping 'setLooping'(bool) except + + bool get_looping 'getLooping'() except + + + void set_pitch 'setPitch'(float) except + + float get_pitch 'getPitch'() except + + + void set_gain 'setGain'(float) except + + 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 set_doppler_factor 'getDopplerFactor'() except + + + void set_relative 'setRelative'(bool) except + + bool get_relative 'getRelative'() except + + + void set_radius 'setRadius'(float) except + + float get_radius 'getRadius'() except + + + void set_stereo_angles 'setStereoAngles'(float, float) except + + pair[float, float] get_stereo_angles 'getStereoAngles'() except + + + void set_3d_spatialize 'set3DSpatialize'(Spatialize) except + + Spatialize get_3d_spatialize 'get3DSpatialize'() except + + + void set_resampler_index 'setResamplerIndex'(int) except + + int get_resampler_index 'getResamplerIndex'() except + + + void set_air_absorption_factor 'setAirAbsorptionFactor'(float) except + + float get_air_absorption_factor 'getAirAbsorptionFactor'() except + + + void set_gain_auto 'setGainAuto'(bool, bool, bool) except + + # get_gain_auto + bool get_direct_gain_hf_auto 'getDirectGainHFAuto'() except + + bool get_send_gain_auto 'getSendGainAuto'() except + + bool get_send_gain_hf_auto 'getSendGainHFAuto'() except + + + void set_direct_filter 'setDirectFilter'(const FilterParams&) except + + void set_send_filter 'setSendFilter'(unsigned, const FilterParams&) except + + void set_auxiliary_send 'setAuxiliarySend'(AuxiliaryEffectSlot, int) except + + void set_auxiliary_send_filter 'setAuxiliarySendFilter'(AuxiliaryEffectSlot, int, const FilterParams&) except + + + void destroy() except + + + + cdef cppclass SourceGroup: + pass + + + cdef cppclass AuxiliaryEffectSlot: + pass + + + cdef cppclass Effect: + pass + + + cdef cppclass Decoder: + int get_frequency 'getFrequency'() + ChannelConfig get_channel_config 'getChannelConfig'() + SampleType get_sample_type 'getSampleType'() + + uint64_t get_length 'getLength'() + bool seek(uint64_t) + + pair[uint64_t, uint64_t] get_loop_points 'getLoopPoints'() + + int read(void*, int) + + + cdef cppclass DecoderFactory: + pass + + + cdef cppclass FileIOFactory: + pass + + + cdef cppclass MessageHandler: + pass diff --git a/archaicy.pyx b/archaicy.pyx new file mode 100644 index 0000000..07d9263 --- /dev/null +++ b/archaicy.pyx @@ -0,0 +1,336 @@ +# cython: binding=True +# Python object wrappers for alure +# Copyright (C) 2019 Nguyễn Gia Phong +# +# This file is part of archaicy. +# +# archaicy 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. +# +# archaicy 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 archaicy. If not, see . + +__doc__ = 'Wrapper for Audio Library Utilities REtooled in Cython' +__all__ = ['ALC_TRUE', 'ALC_HRTF_SOFT', 'ALC_HRTF_ID_SOFT', + 'DeviceManager', 'Device', 'Context', 'Buffer', 'Source', 'Decoder'] + +from typing import Dict, List, Tuple + +from libcpp cimport nullptr +from libcpp.memory cimport shared_ptr +from libcpp.pair cimport pair +from libcpp.vector cimport vector + +cimport alure + +# Cast to Python objects +ALC_TRUE = alure.ALC_TRUE +ALC_HRTF_SOFT = alure.ALC_HRTF_SOFT +ALC_HRTF_ID_SOFT = alure.ALC_HRTF_ID_SOFT + + +cdef vector[alure.AttributePair] mkattrs(vector[pair[int, int]] attrs): + """Convert attribute pairs from Python object to alure format.""" + cdef vector[alure.AttributePair] attributes + cdef alure.AttributePair pair + for attribute, value in attrs: + pair.mAttribute = attribute + pair.mValue = value + attributes.push_back(pair) # insert a copy + pair.mAttribute = pair.mValue = 0 + attributes.push_back(pair) # insert a copy + return attributes + + +cdef class DeviceManager: + """Manager of Device objects and other related functionality. + This class is a singleton, only one instance will exist in a process + at a time. + """ + cdef alure.DeviceManager impl + + def __init__(self): + """Multiple calls will give the same instance as long as + there is still a pre-existing reference to the instance, + or else a new instance will be created. + """ + self.impl = alure.DeviceManager.get_instance() + + def open_playback(self, name: str = None) -> Device: + """Return the playback device given by name. + + Raise RuntimeError on failure. + """ + device = Device() + if name is None: + device.impl = self.impl.open_playback() + else: + device.impl = self.impl.open_playback(name.encode()) + return device + + +cdef class Device: + """Playback device.""" + cdef alure.Device impl + + @property + def basic_name(self) -> str: + """Basic name of the device.""" + return self.impl.get_name(alure.PlaybackName.Basic).decode() + + @property + def full_name(self) -> str: + """Full name of the device.""" + return self.impl.get_name(alure.PlaybackName.Full).decode() + + @property + def hrtf_names(self) -> List[str]: + """List of available HRTF names, sorted as OpenAL gives them, + such that the index of a given name is the ID to use with + ALC_HRTF_ID_SOFT. + + If the ALC_SOFT_HRTF extension is unavailable, + this will be an empty list. + """ + return [name.decode() for name in self.impl.enumerate_hrtf_names()] + + @property + def hrtf_enabled(self) -> bool: + """Whether HRTF is enabled on the device. + + If the ALC_SOFT_HRTF extension is unavailable, + this will return False although there could still be + HRTF applied at a lower hardware level. + """ + return self.impl.is_hrtf_enabled() + + @property + def current_hrtf(self) -> str: + """Name of the HRTF currently being used by this device. + + If HRTF is not currently enabled, this will be None. + """ + name = self.impl.get_current_hrtf().decode() + return name or None + + def create_context(self, attrs: Dict[int, int] = {}) -> Context: + """Return a newly created Context on this device, + using the specified attributes. + + Raise RuntimeError on failure. + """ + context = Context() + if attrs: + context.impl = self.impl.create_context(mkattrs(attrs.items())) + else: + context.impl = self.impl.create_context() + return context + + def close(self) -> None: + """Close and free the device. All previously-created contexts + must first be destroyed. + """ + self.impl.close() + + +cdef class Context: + """With statement is supported, for example + + with context: + ... + + is equivalent to + + Context.make_current(context) + ... + Context.make_current() + context.destroy() + """ + cdef alure.Context impl + + def __enter__(self): + Context.make_current(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + Context.make_current() + self.destroy() + + @staticmethod + def make_current(context: Context = None) -> None: + """Make the specified context current for OpenAL operations.""" + if context is None: + alure.Context.make_current( nullptr) + else: + alure.Context.make_current(context.impl) + + def destroy(self) -> None: + """Destroy the context. The context must not be current + when this is called. + """ + self.impl.destroy() + + def create_decoder(self, name: str) -> Decoder: + """Return a Decoder instance for the given audio file + or resource name. + """ + decoder = Decoder() + decoder.pimpl = self.impl.create_decoder(name.encode()) + return decoder + + def get_buffer(self, name: str) -> Buffer: + """Create and cache a Buffer for the given audio file + or resource name. Multiple calls with the same name will + return the same Buffer object. Cached buffers must be + freed using remove_buffer before destroying the context. + + If the buffer can't be loaded RuntimeError will be raised. + """ + buffer = Buffer() + buffer.impl = self.impl.get_buffer(name.encode()) + return buffer + + def remove_buffer(self, buffer: Buffer) -> None: + """Delete the given cached buffer, invalidating all other + Buffer objects with the same name. + """ + self.impl.remove_buffer(buffer.impl) + + def create_source(self) -> Source: + """Return a newly created Source for playing audio. + There is no practical limit to the number of sources you may create. + You must call Source.destroy when the source is no longer needed. + """ + source = Source() + source.impl = self.impl.create_source() + return source + + def update(self) -> None: + """Update the context and all sources belonging to this context.""" + self.impl.update() + + +cdef class Buffer: + cdef alure.Buffer impl + + @property + def length(self) -> int: + """The length of the buffer in sample frames.""" + return self.impl.get_length() + + @property + def frequency(self) -> int: + """The buffer's frequency in hertz.""" + return self.impl.get_frequency() + + @property + def channel_config_name(self) -> str: + """The buffer's sample configuration name.""" + return alure.get_channel_config_name( + self.impl.get_channel_config()).decode() + + @property + def sample_type_name(self) -> str: + """The buffer's sample type name.""" + return alure.get_sample_type_name( + self.impl.get_sample_type()).decode() + + +cdef class Source: + cdef alure.Source impl + + def play_from_buffer(self, buffer: Buffer) -> None: + """Play the source using a buffer. The same buffer + may be played from multiple sources simultaneously. + """ + self.impl.play(buffer.impl); + + def play_from_decoder(self, decoder: Decoder, + chunk_len: int, queue_size: int) -> None: + """Plays the source by asynchronously streaming audio from + a decoder. The given decoder must NOT have its read or seek + methods called from elsewhere while in use. + + Parameters + ---------- + decoder : Decoder + The decoder object to play audio from. + chunk_len : int + The number of sample frames to read for each chunk update. + Smaller values will require more frequent updates and + larger values will handle more data with each chunk. + queue_size : int + The number of chunks to keep queued during playback. + Smaller values use less memory while larger values + improve protection against underruns. + """ + self.impl.play(decoder.pimpl, chunk_len, queue_size) + + @property + def playing(self) -> bool: + """Whether the source is currently playing.""" + return self.impl.is_playing() + + @property + def sample_offset(self) -> int: + """The source offset in sample frames. For streaming sources + this will be the offset based on the decoder's read position. + """ + return self.impl.get_sample_offset() + + @property + def stereo_angles(self) -> Tuple[float, float]: + """The left and right channel angles, in radians, when playing + a stereo buffer or stream. The angles go counter-clockwise, + with 0 being in front and positive values going left. + + Has no effect without the AL_EXT_STEREO_ANGLES extension. + """ + return self.impl.get_stereo_angles() + + @stereo_angles.setter + def stereo_angles(self, angles: Tuple[float, float]): + left, right = angles + self.impl.set_stereo_angles(left, right) + + + def destroy(self) -> None: + """Destroy the source, stop playback and release resources.""" + self.impl.destroy() + + +cdef class Decoder: + """Audio decoder interface.""" + cdef shared_ptr[alure.Decoder] pimpl + + @property + def frequency(self) -> int: + """The sample frequency, in hertz, of the audio being decoded.""" + return self.pimpl.get()[0].get_frequency() + + @property + def channel_config_name(self) -> str: + """Name of the channel configuration of the audio being decoded.""" + return alure.get_channel_config_name( + self.pimpl.get()[0].get_channel_config()).decode() + + @property + def sample_type_name(self) -> str: + """Name of the sample type of the audio being decoded.""" + return alure.get_sample_type_name( + self.pimpl.get()[0].get_sample_type()).decode() + + @property + def length(self) -> int: + """The total length of the audio, in sample frames, + falling-back to 0. Note that if the length is 0, + the decoder may not be used to load a Buffer. + """ + return self.pimpl.get()[0].get_length() diff --git a/examples/archaicy-hrtf.py b/examples/archaicy-hrtf.py new file mode 100755 index 0000000..0af4d77 --- /dev/null +++ b/examples/archaicy-hrtf.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# HRTF rendering example using ALC_SOFT_HRTF extension +# Copyright (C) 2019 Nguyễn Gia Phong +# +# This file is part of archaicy. +# +# archaicy 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. +# +# archaicy 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 archaicy. If not, see . + +from argparse import ArgumentParser +from datetime import datetime, timedelta +from itertools import count, takewhile +from sys import stderr +from time import sleep +from typing import Iterable + +from archaicy import ALC_TRUE, ALC_HRTF_SOFT, ALC_HRTF_ID_SOFT, DeviceManager + +PERIOD = 0.025 + + +def pretty_time(seconds: float) -> str: + """Return human-readably formatted time.""" + time = datetime.min + timedelta(seconds=seconds) + if seconds < 3600: return time.strftime('%M:%S') + return time.strftime('%H:%M:%S') + + +def hrtf(files: Iterable[str], device: str, hrtf_name: str, + omega: float, angle: float): + devmrg = DeviceManager() + try: + dev = devmrg.open_playback(device) + except RuntimeError: + stderr.write(f'Failed to open "{device}" - trying default\n') + dev = devmrg.open_playback() + print('Opened', dev.full_name) + + hrtf_names = dev.hrtf_names + if hrtf_names: + print('Available HRTFs:') + for name in hrtf_names: print(f' {name}') + else: + print('No HRTF found!') + attrs = {ALC_HRTF_SOFT: ALC_TRUE} + if hrtf_name is not None: + try: + attrs[ALC_HRTF_ID_SOFT] = hrtf_names.index(hrtf_name) + except ValueError: + stderr.write(f'HRTF "{hrtf_name}" not found\n') + + with dev.create_context(attrs) as ctx: + if dev.hrtf_enabled: + print(f'Using HRTF "{dev.current_hrtf}"') + else: + print('HRTF not enabled!') + + for filename in files: + try: + decoder = ctx.create_decoder(filename) + except RuntimeError: + stderr.write(f'Failed to open file: {filename}\n') + continue + source = ctx.create_source() + + source.play_from_decoder(decoder, 12000, 4) + print(f'Playing {filename} ({decoder.sample_type_name},', + f'{decoder.channel_config_name}, {decoder.frequency} Hz)') + + invfreq = 1 / decoder.frequency + for i in takewhile(lambda i: source.playing, count(step=PERIOD)): + source.stereo_angles = i*omega, i*omega+angle + print(f' {pretty_time(source.sample_offset*invfreq)} /' + f' {pretty_time(decoder.length*invfreq)}', + end='\r', flush=True) + sleep(PERIOD) + ctx.update() + print() + source.destroy() + dev.close() + + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument('files', nargs='+', help='audio files') + parser.add_argument('-d', '--device', help='device name') + parser.add_argument('-n', '--hrtf', dest='hrtf_name', help='HRTF name') + parser.add_argument('-o', '--omega', type=float, default=1.0, + help='angular velocity') + parser.add_argument('-a', '--angle', type=float, default=1.0, + help='relative angle between left and right sources') + args = parser.parse_args() + hrtf(args.files, args.device, args.hrtf_name, args.omega, args.angle) diff --git a/examples/archaicy-play.py b/examples/archaicy-play.py new file mode 100755 index 0000000..cbca024 --- /dev/null +++ b/examples/archaicy-play.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# A simple example showing how to load and play a sound. +# Copyright (C) 2019 Nguyễn Gia Phong +# +# This file is part of archaicy. +# +# archaicy 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. +# +# archaicy 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 archaicy. If not, see . + +from argparse import ArgumentParser +from datetime import datetime, timedelta +from itertools import count, takewhile +from sys import stderr +from time import sleep +from typing import Iterable + +from archaicy import DeviceManager, Context + +PERIOD = 0.025 + + +def pretty_time(seconds: float) -> str: + """Return human-readably formatted time.""" + time = datetime.min + timedelta(seconds=seconds) + if seconds < 3600: return time.strftime('%M:%S') + return time.strftime('%H:%M:%S') + + +def play(files: Iterable[str], device: str): + """Load and play files on the given device.""" + devmrg = DeviceManager() + try: + dev = devmrg.open_playback(device) + except RuntimeError: + stderr.write(f'Failed to open "{device}" - trying default\n') + dev = devmrg.open_playback() + print('Opened', dev.full_name) + + ctx = dev.create_context() + Context.make_current(ctx) + for filename in files: + try: + buffer = ctx.get_buffer(filename) + except RuntimeError: + stderr.write(f'Failed to open file: {filename}\n') + continue + source = ctx.create_source() + + source.play_from_buffer(buffer) + print(f'Playing {filename} ({buffer.sample_type_name},', + f'{buffer.channel_config_name}, {buffer.frequency} Hz)') + + invfreq = 1 / buffer.frequency + for i in takewhile(lambda i: source.playing, count()): + print(f' {pretty_time(source.sample_offset*invfreq)} /' + f' {pretty_time(buffer.length*invfreq)}', + end='\r', flush=True) + sleep(PERIOD) + print() + source.destroy() + ctx.remove_buffer(buffer) + Context.make_current() + ctx.destroy() + dev.close() + + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument('files', nargs='+', help='audio files') + parser.add_argument('-d', '--device', help='device name') + args = parser.parse_args() + play(args.files, args.device) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e8dff76 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +# Minimum requirements for the build system to execute. +requires= ['setuptools', 'cython'] diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..242dd32 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +from Cython.Build import cythonize +from setuptools import setup, Extension + +with open('README.md') as f: + long_description = f.read() + +setup( + name='archaicy', + version='0.0.1', + description='Wrapper for Audio Library Utilities REtooled in Cython', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/McSinyx/archaicy', + author='Nguyễn Gia Phong', + author_email='vn.mcsinyx@gmail.com', + license='LGPLv3+', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: C++', + 'Programming Language :: Cython', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Multimedia :: Sound/Audio', + 'Topic :: Software Development :: Libraries', + 'Typing :: Typed'], + keywords='openal alure hrtf', + ext_modules=cythonize( + Extension('archaicy', ['archaicy.pyx'], + include_dirs=['/usr/include/AL/'], + libraries=['alure2'], language='c++'), + compiler_directives={'language_level': 3}), + zip_safe=False)