mirror of
https://github.com/markqvist/LXST.git
synced 2025-12-22 10:57:08 +00:00
Sync upstream
This commit is contained in:
4
LXST/Filters.def
Normal file
4
LXST/Filters.def
Normal file
@@ -0,0 +1,4 @@
|
||||
EXPORTS
|
||||
highpass_filter
|
||||
lowpass_filter
|
||||
agc_process
|
||||
@@ -16,16 +16,16 @@ else:
|
||||
ffi = FFI()
|
||||
|
||||
try:
|
||||
# Disable native filterlib loading on Windows
|
||||
# for now due to strange linking behaviour,
|
||||
# but allow local compilation if the user has
|
||||
# a C compiler installed.
|
||||
if not RNS.vendor.platformutils.is_windows():
|
||||
filterlib_spec = find_spec("LXST.filterlib")
|
||||
if not filterlib_spec or filterlib_spec.origin == None: raise ImportError("Could not locate pre-compiled LXST.filterlib module")
|
||||
with open(os.path.join(c_src_path, "Filters.h"), "r") as f: ffi.cdef(f.read())
|
||||
native_functions = ffi.dlopen(filterlib_spec.origin)
|
||||
USE_NATIVE_FILTERS = True
|
||||
else:
|
||||
with open(os.path.join(c_src_path, "Filters.h"), "r") as f: ffi.cdef(f.read())
|
||||
native_functions = ffi.dlopen(os.path.join(c_src_path, "filterlib.dll"))
|
||||
USE_NATIVE_FILTERS = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not load pre-compiled LXST filters library. The contained exception was: {e}", RNS.LOG_WARNING)
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import atexit
|
||||
import collections.abc
|
||||
import time
|
||||
|
||||
0
LXST/Platforms/darwin/__init__.py
Normal file
0
LXST/Platforms/darwin/__init__.py
Normal file
261
LXST/Platforms/darwin/coreaudio.h
Normal file
261
LXST/Platforms/darwin/coreaudio.h
Normal file
@@ -0,0 +1,261 @@
|
||||
// All files are found in /System/Library/Frameworks
|
||||
|
||||
// CoreFoundation/CFBase.h:
|
||||
typedef unsigned char Boolean;
|
||||
typedef unsigned char UInt8;
|
||||
typedef signed char SInt8;
|
||||
typedef unsigned short UInt16;
|
||||
typedef signed short SInt16;
|
||||
typedef unsigned int UInt32;
|
||||
typedef signed int SInt32;
|
||||
typedef uint64_t UInt64;
|
||||
typedef int64_t SInt64;
|
||||
typedef SInt32 OSStatus;
|
||||
typedef float Float32;
|
||||
typedef double Float64;
|
||||
typedef unsigned short UniChar;
|
||||
typedef unsigned long UniCharCount;
|
||||
typedef unsigned char * StringPtr;
|
||||
typedef const unsigned char * ConstStringPtr;
|
||||
typedef unsigned char Str255[256];
|
||||
typedef const unsigned char * ConstStr255Param;
|
||||
typedef SInt16 OSErr;
|
||||
typedef SInt16 RegionCode;
|
||||
typedef SInt16 LangCode;
|
||||
typedef SInt16 ScriptCode;
|
||||
typedef UInt32 FourCharCode;
|
||||
typedef FourCharCode OSType;
|
||||
typedef UInt8 Byte;
|
||||
typedef SInt8 SignedByte;
|
||||
typedef UInt32 UTF32Char;
|
||||
typedef UInt16 UTF16Char;
|
||||
typedef UInt8 UTF8Char;
|
||||
typedef signed long long CFIndex;
|
||||
typedef const void * CFStringRef;
|
||||
|
||||
// CoreFoundation/CFString.h
|
||||
typedef UInt32 CFStringEncoding;
|
||||
CFIndex CFStringGetLength(CFStringRef theString);
|
||||
Boolean CFStringGetCString(CFStringRef theString, char *buffer, CFIndex bufferSize, CFStringEncoding encoding);
|
||||
|
||||
// CoreFoundation/CFRunLoop.h
|
||||
typedef struct __CFRunLoop * CFRunLoopRef;
|
||||
|
||||
// CoreAudio/AudioHardwareBase.h
|
||||
typedef UInt32 AudioObjectID;
|
||||
typedef UInt32 AudioObjectPropertySelector;
|
||||
typedef UInt32 AudioObjectPropertyScope;
|
||||
typedef UInt32 AudioObjectPropertyElement;
|
||||
struct AudioObjectPropertyAddress
|
||||
{
|
||||
AudioObjectPropertySelector mSelector;
|
||||
AudioObjectPropertyScope mScope;
|
||||
AudioObjectPropertyElement mElement;
|
||||
};
|
||||
typedef struct AudioObjectPropertyAddress AudioObjectPropertyAddress;
|
||||
|
||||
// CoreAudio/AudioHardware.h
|
||||
Boolean AudioObjectHasProperty(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress);
|
||||
OSStatus AudioObjectGetPropertyDataSize(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32* outDataSize);
|
||||
OSStatus AudioObjectGetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32* ioDataSize,
|
||||
void* outData);
|
||||
OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID,
|
||||
const AudioObjectPropertyAddress* inAddress,
|
||||
UInt32 inQualifierDataSize,
|
||||
const void* inQualifierData,
|
||||
UInt32 inDataSize,
|
||||
const void* inData);
|
||||
|
||||
|
||||
// CoreAudioTypes.h
|
||||
typedef UInt32 AudioFormatID;
|
||||
typedef UInt32 AudioFormatFlags;
|
||||
struct AudioStreamBasicDescription
|
||||
{
|
||||
Float64 mSampleRate;
|
||||
AudioFormatID mFormatID;
|
||||
AudioFormatFlags mFormatFlags;
|
||||
UInt32 mBytesPerPacket;
|
||||
UInt32 mFramesPerPacket;
|
||||
UInt32 mBytesPerFrame;
|
||||
UInt32 mChannelsPerFrame;
|
||||
UInt32 mBitsPerChannel;
|
||||
UInt32 mReserved;
|
||||
};
|
||||
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
|
||||
struct AudioStreamPacketDescription
|
||||
{
|
||||
SInt64 mStartOffset;
|
||||
UInt32 mVariableFramesInPacket;
|
||||
UInt32 mDataByteSize;
|
||||
};
|
||||
typedef struct AudioStreamPacketDescription AudioStreamPacketDescription;
|
||||
|
||||
// AudioToolbox/AudioQueue.h
|
||||
|
||||
// data structures:
|
||||
|
||||
struct SMPTETime
|
||||
{
|
||||
SInt16 mSubframes;
|
||||
SInt16 mSubframeDivisor;
|
||||
UInt32 mCounter;
|
||||
UInt32 mType;
|
||||
UInt32 mFlags;
|
||||
SInt16 mHours;
|
||||
SInt16 mMinutes;
|
||||
SInt16 mSeconds;
|
||||
SInt16 mFrames;
|
||||
};
|
||||
typedef struct SMPTETime SMPTETime;
|
||||
struct AudioTimeStamp
|
||||
{
|
||||
Float64 mSampleTime;
|
||||
UInt64 mHostTime;
|
||||
Float64 mRateScalar;
|
||||
UInt64 mWordClockTime;
|
||||
SMPTETime mSMPTETime;
|
||||
UInt32 mFlags;
|
||||
UInt32 mReserved;
|
||||
};
|
||||
typedef struct AudioTimeStamp AudioTimeStamp;
|
||||
|
||||
// AudioComponent.h
|
||||
|
||||
typedef struct AudioComponentDescription {
|
||||
OSType componentType;
|
||||
OSType componentSubType;
|
||||
OSType componentManufacturer;
|
||||
UInt32 componentFlags;
|
||||
UInt32 componentFlagsMask;
|
||||
} AudioComponentDescription;
|
||||
typedef struct OpaqueAudioComponent * AudioComponent;
|
||||
typedef struct ComponentInstanceRecord * AudioComponentInstance;
|
||||
AudioComponent AudioComponentFindNext(AudioComponent inComponent,
|
||||
const AudioComponentDescription *inDesc);
|
||||
OSStatus AudioComponentInstanceNew(AudioComponent inComponent,
|
||||
AudioComponentInstance *outInstance);
|
||||
OSStatus AudioComponentInstanceDispose(AudioComponentInstance inInstance);
|
||||
OSStatus AudioComponentCopyName(AudioComponent inComponent,
|
||||
CFStringRef *outName);
|
||||
OSStatus AudioComponentGetDescription(AudioComponent inComponent,
|
||||
AudioComponentDescription *outDesc);
|
||||
|
||||
// AUComponent.h
|
||||
|
||||
typedef AudioComponentInstance AudioUnit;
|
||||
typedef UInt32 AudioUnitPropertyID;
|
||||
typedef UInt32 AudioUnitScope;
|
||||
typedef UInt32 AudioUnitElement;
|
||||
|
||||
OSStatus AudioUnitInitialize(AudioUnit inUnit);
|
||||
OSStatus AudioUnitGetPropertyInfo(AudioUnit inUnit,
|
||||
AudioUnitPropertyID inID,
|
||||
AudioUnitScope inScope,
|
||||
AudioUnitElement inElement,
|
||||
UInt32 *outDataSize,
|
||||
Boolean *outWritable);
|
||||
OSStatus AudioUnitGetProperty(AudioUnit inUnit,
|
||||
AudioUnitPropertyID inID,
|
||||
AudioUnitScope inScope,
|
||||
AudioUnitElement inElement,
|
||||
void *outData,
|
||||
UInt32 *ioDataSize);
|
||||
OSStatus AudioUnitSetProperty(AudioUnit inUnit,
|
||||
AudioUnitPropertyID inID,
|
||||
AudioUnitScope inScope,
|
||||
AudioUnitElement inElement,
|
||||
const void *inData,
|
||||
UInt32 inDataSize);
|
||||
|
||||
OSStatus AudioOutputUnitStart(AudioUnit ci);
|
||||
OSStatus AudioOutputUnitStop(AudioUnit ci);
|
||||
|
||||
typedef UInt32 AudioUnitRenderActionFlags;
|
||||
|
||||
struct AudioBuffer
|
||||
{
|
||||
UInt32 mNumberChannels;
|
||||
UInt32 mDataByteSize;
|
||||
void* mData;
|
||||
};
|
||||
typedef struct AudioBuffer AudioBuffer;
|
||||
|
||||
struct AudioBufferList
|
||||
{
|
||||
UInt32 mNumberBuffers;
|
||||
AudioBuffer mBuffers[]; // this is a variable length array of mNumberBuffers elements
|
||||
};
|
||||
typedef struct AudioBufferList AudioBufferList;
|
||||
|
||||
OSStatus AudioUnitProcess(AudioUnit inUnit,
|
||||
AudioUnitRenderActionFlags * ioActionFlags,
|
||||
const AudioTimeStamp *inTimeStamp,
|
||||
UInt32 inNumberFrames,
|
||||
AudioBufferList *ioData);
|
||||
OSStatus AudioUnitRender(AudioUnit inUnit,
|
||||
AudioUnitRenderActionFlags * ioActionFlags,
|
||||
const AudioTimeStamp * inTimeStamp,
|
||||
UInt32 inOutputBusNumber,
|
||||
UInt32 inNumberFrames,
|
||||
AudioBufferList *ioData);
|
||||
|
||||
typedef OSStatus (*AURenderCallback)(void * inRefCon,
|
||||
AudioUnitRenderActionFlags *ioActionFlags,
|
||||
const AudioTimeStamp *inTimeStamp,
|
||||
UInt32 inBusNumber,
|
||||
UInt32 inNumberFrames,
|
||||
AudioBufferList *ioData);
|
||||
|
||||
typedef struct AURenderCallbackStruct {
|
||||
AURenderCallback inputProc;
|
||||
void *inputProcRefCon;
|
||||
} AURenderCallbackStruct;
|
||||
|
||||
struct AudioValueRange
|
||||
{
|
||||
Float64 mMinimum;
|
||||
Float64 mMaximum;
|
||||
};
|
||||
typedef struct AudioValueRange AudioValueRange;
|
||||
|
||||
|
||||
// AudioConverter.h
|
||||
typedef struct OpaqueAudioConverter * AudioConverterRef;
|
||||
typedef UInt32 AudioConverterPropertyID;
|
||||
|
||||
OSStatus AudioConverterNew(const AudioStreamBasicDescription *inSourceFormat,
|
||||
const AudioStreamBasicDescription *inDestinationFormat,
|
||||
AudioConverterRef *outAudioConverter);
|
||||
OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter);
|
||||
typedef OSStatus (*AudioConverterComplexInputDataProc)(
|
||||
AudioConverterRef inAudioConverter,
|
||||
UInt32 *ioNumberDataPackets,
|
||||
AudioBufferList *ioData,
|
||||
AudioStreamPacketDescription **outDataPacketDescription,
|
||||
void *inUserData);
|
||||
extern OSStatus AudioConverterFillComplexBuffer(
|
||||
AudioConverterRef inAudioConverter,
|
||||
AudioConverterComplexInputDataProc inInputDataProc,
|
||||
void *inInputDataProcUserData,
|
||||
UInt32 *ioOutputDataPacketSize,
|
||||
AudioBufferList *outOutputData,
|
||||
AudioStreamPacketDescription *outPacketDescription);
|
||||
extern OSStatus AudioConverterSetProperty(
|
||||
AudioConverterRef inAudioConverter,
|
||||
AudioConverterPropertyID inPropertyID,
|
||||
UInt32 inPropertyDataSize,
|
||||
const void *inPropertyData);
|
||||
extern OSStatus AudioConverterGetProperty(
|
||||
AudioConverterRef inAudioConverter,
|
||||
AudioConverterPropertyID inPropertyID,
|
||||
UInt32 *ioPropertyDataSize,
|
||||
void *outPropertyData);
|
||||
945
LXST/Platforms/darwin/soundcard.py
Normal file
945
LXST/Platforms/darwin/soundcard.py
Normal file
@@ -0,0 +1,945 @@
|
||||
# Adapted from Bastian Bechtold's soundcard library, originally released
|
||||
# under the BSD 3-Clause License
|
||||
#
|
||||
# https://github.com/bastibe/SoundCard
|
||||
#
|
||||
# Copyright (c) 2016 Bastian Bechtold
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
# Modifications and improvements Copyright 2025 Mark Qvist, and released
|
||||
# under the same BSD 3-Clause License.
|
||||
|
||||
import os
|
||||
import cffi
|
||||
import numpy
|
||||
import collections
|
||||
import time
|
||||
import re
|
||||
import math
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
_ffi = cffi.FFI()
|
||||
_package_dir, _ = os.path.split(__file__)
|
||||
with open(os.path.join(_package_dir, 'coreaudio.h'), 'rt') as f:
|
||||
_ffi.cdef(f.read())
|
||||
|
||||
_ca = _ffi.dlopen('CoreAudio')
|
||||
_au = _ffi.dlopen('AudioUnit')
|
||||
|
||||
from soundcard import coreaudioconstants as _cac
|
||||
|
||||
|
||||
def all_speakers():
|
||||
"""A list of all connected speakers."""
|
||||
device_ids = _CoreAudio.get_property(
|
||||
_cac.kAudioObjectSystemObject,
|
||||
_cac.kAudioHardwarePropertyDevices,
|
||||
"AudioObjectID")
|
||||
return [_Speaker(id=d) for d in device_ids
|
||||
if _Speaker(id=d).channels > 0]
|
||||
|
||||
|
||||
def all_microphones(include_loopback=False):
|
||||
"""A list of all connected microphones."""
|
||||
|
||||
# macOS does not support loopback recording functionality
|
||||
if include_loopback:
|
||||
warnings.warn("macOS does not support loopback recording functionality", Warning)
|
||||
|
||||
device_ids = _CoreAudio.get_property(
|
||||
_cac.kAudioObjectSystemObject,
|
||||
_cac.kAudioHardwarePropertyDevices,
|
||||
"AudioObjectID")
|
||||
return [_Microphone(id=d) for d in device_ids
|
||||
if _Microphone(id=d).channels > 0]
|
||||
|
||||
|
||||
def default_speaker():
|
||||
"""The default speaker of the system."""
|
||||
device_id, = _CoreAudio.get_property(
|
||||
_cac.kAudioObjectSystemObject,
|
||||
_cac.kAudioHardwarePropertyDefaultOutputDevice,
|
||||
"AudioObjectID")
|
||||
return _Speaker(id=device_id)
|
||||
|
||||
|
||||
def get_speaker(id):
|
||||
"""Get a specific speaker by a variety of means.
|
||||
|
||||
id can be an a CoreAudio id, a substring of the speaker name, or a
|
||||
fuzzy-matched pattern for the speaker name.
|
||||
|
||||
"""
|
||||
return _match_device(id, all_speakers())
|
||||
|
||||
|
||||
def default_microphone():
|
||||
"""The default microphone of the system."""
|
||||
device_id, = _CoreAudio.get_property(
|
||||
_cac.kAudioObjectSystemObject,
|
||||
_cac.kAudioHardwarePropertyDefaultInputDevice,
|
||||
"AudioObjectID")
|
||||
return _Microphone(id=device_id)
|
||||
|
||||
|
||||
def get_microphone(id, include_loopback=False):
|
||||
"""Get a specific microphone by a variety of means.
|
||||
|
||||
id can be a CoreAudio id, a substring of the microphone name, or a
|
||||
fuzzy-matched pattern for the microphone name.
|
||||
|
||||
"""
|
||||
return _match_device(id, all_microphones(include_loopback))
|
||||
|
||||
|
||||
def _match_device(id, devices):
|
||||
"""Find id in a list of devices.
|
||||
|
||||
id can be a CoreAudio id, a substring of the device name, or a
|
||||
fuzzy-matched pattern for the microphone name.
|
||||
|
||||
"""
|
||||
devices_by_id = {device.id: device for device in devices}
|
||||
devices_by_name = {device.name: device for device in devices}
|
||||
if id in devices_by_id:
|
||||
return devices_by_id[id]
|
||||
# try substring match:
|
||||
for name, device in devices_by_name.items():
|
||||
if id in name:
|
||||
return device
|
||||
# try fuzzy match:
|
||||
pattern = '.*'.join(id)
|
||||
for name, device in devices_by_name.items():
|
||||
if re.match(pattern, name):
|
||||
return device
|
||||
raise IndexError('no device with id {}'.format(id))
|
||||
|
||||
|
||||
def get_name():
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def set_name(name):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class _Soundcard:
|
||||
"""A soundcard. This is meant to be subclassed.
|
||||
|
||||
Properties:
|
||||
- `name`: the name of the soundcard
|
||||
|
||||
"""
|
||||
def __init__(self, *, id):
|
||||
self._id = id
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
name = _CoreAudio.get_property(
|
||||
self._id, _cac.kAudioObjectPropertyName, 'CFStringRef')
|
||||
return _CoreAudio.CFString_to_str(name)
|
||||
|
||||
|
||||
class _Speaker(_Soundcard):
|
||||
"""A soundcard output. Can be used to play audio.
|
||||
|
||||
Use the `play` method to play one piece of audio, or use the
|
||||
`player` method to get a context manager for playing continuous
|
||||
audio.
|
||||
|
||||
Properties:
|
||||
- `channels`: either the number of channels to play, or a list
|
||||
of channel indices. Index -1 is silence, and subsequent numbers
|
||||
are channel numbers (left, right, center, ...)
|
||||
- `name`: the name of the soundcard
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
bufferlist = _CoreAudio.get_property(
|
||||
self._id,
|
||||
_cac.kAudioDevicePropertyStreamConfiguration,
|
||||
'AudioBufferList', scope=_cac.kAudioObjectPropertyScopeOutput)
|
||||
if bufferlist and bufferlist[0].mNumberBuffers > 0:
|
||||
return bufferlist[0].mBuffers[0].mNumberChannels
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __repr__(self):
|
||||
return '<Speaker {} ({} channels)>'.format(self.name, self.channels)
|
||||
|
||||
def player(self, samplerate, channels=None, blocksize=None):
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
return _Player(self._id, samplerate, channels, blocksize)
|
||||
|
||||
def play(self, data, samplerate, channels=None, blocksize=None):
|
||||
if channels is None and len(data.shape) == 2:
|
||||
channels = data.shape[1]
|
||||
elif channels is None:
|
||||
channels = self.channels
|
||||
with self.player(samplerate, channels, blocksize) as p:
|
||||
p.play(data)
|
||||
|
||||
|
||||
class _Microphone(_Soundcard):
|
||||
"""A soundcard input. Can be used to record audio.
|
||||
|
||||
Use the `record` method to record a piece of audio, or use the
|
||||
`recorder` method to get a context manager for recording
|
||||
continuous audio.
|
||||
|
||||
Properties:
|
||||
- `channels`: either the number of channels to record, or a list
|
||||
of channel indices. Index -1 is silence, and subsequent numbers
|
||||
are channel numbers (left, right, center, ...)
|
||||
- `name`: the name of the soundcard
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def isloopback(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
bufferlist = _CoreAudio.get_property(
|
||||
self._id,
|
||||
_cac.kAudioDevicePropertyStreamConfiguration,
|
||||
'AudioBufferList', scope=_cac.kAudioObjectPropertyScopeInput)
|
||||
if bufferlist and bufferlist[0].mNumberBuffers > 0:
|
||||
return bufferlist[0].mBuffers[0].mNumberChannels
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __repr__(self):
|
||||
return '<Microphone {} ({} channels)>'.format(self.name, self.channels)
|
||||
|
||||
def recorder(self, samplerate, channels=None, blocksize=None):
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
return _Recorder(self._id, samplerate, channels, blocksize)
|
||||
|
||||
def record(self, numframes, samplerate, channels=None, blocksize=None):
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
with self.recorder(samplerate, channels, blocksize) as p:
|
||||
return p.record(numframes)
|
||||
|
||||
|
||||
class _CoreAudio:
|
||||
"""A helper class for interacting with CoreAudio."""
|
||||
|
||||
@staticmethod
|
||||
def get_property(target, selector, ctype, scope=_cac.kAudioObjectPropertyScopeGlobal):
|
||||
"""Get a CoreAudio property.
|
||||
|
||||
This might include things like a list of available sound
|
||||
cards, or various meta data about those sound cards.
|
||||
|
||||
Arguments:
|
||||
- `target`: The AudioObject that the property belongs to
|
||||
- `selector`: The Selector for this property
|
||||
- `scope`: The Scope for this property
|
||||
- `ctype`: The type of the property
|
||||
|
||||
Returns:
|
||||
A list of objects of type `ctype`
|
||||
|
||||
"""
|
||||
|
||||
prop = _ffi.new("AudioObjectPropertyAddress*",
|
||||
{'mSelector': selector,
|
||||
'mScope': scope,
|
||||
'mElement': _cac.kAudioObjectPropertyElementMaster})
|
||||
|
||||
has_prop = _ca.AudioObjectHasProperty(target, prop)
|
||||
assert has_prop == 1, 'Core Audio does not have the requested property'
|
||||
|
||||
size = _ffi.new("UInt32*")
|
||||
err = _ca.AudioObjectGetPropertyDataSize(target, prop, 0, _ffi.NULL, size)
|
||||
assert err == 0, "Can't get Core Audio property size"
|
||||
num_values = int(size[0]//_ffi.sizeof(ctype))
|
||||
|
||||
prop_data = _ffi.new(ctype+'[]', num_values)
|
||||
err = _ca.AudioObjectGetPropertyData(target, prop, 0, _ffi.NULL,
|
||||
size, prop_data)
|
||||
assert err == 0, "Can't get Core Audio property data"
|
||||
return prop_data
|
||||
|
||||
@staticmethod
|
||||
def set_property(target, selector, prop_data, scope=_cac.kAudioObjectPropertyScopeGlobal):
|
||||
"""Set a CoreAudio property.
|
||||
|
||||
This is typically a piece of meta data about a sound card.
|
||||
|
||||
Arguments:
|
||||
- `target`: The AudioObject that the property belongs to
|
||||
- `selector`: The Selector for this property
|
||||
- `scope`: The Scope for this property
|
||||
- `prop_data`: The new property value
|
||||
|
||||
"""
|
||||
|
||||
prop = _ffi.new("AudioObjectPropertyAddress*",
|
||||
{'mSelector': selector,
|
||||
'mScope': scope,
|
||||
'mElement': _cac.kAudioObjectPropertyElementMaster})
|
||||
|
||||
err = _ca.AudioObjectSetPropertyData(target, prop, 0, _ffi.NULL,
|
||||
_ffi.sizeof(_ffi.typeof(prop_data).item.cname), prop_data)
|
||||
assert err == 0, "Can't set Core Audio property data"
|
||||
|
||||
@staticmethod
|
||||
def CFString_to_str(cfstrptr):
|
||||
"""Converts a CFStringRef to a Python str."""
|
||||
|
||||
# Multiply by 4, the maximum number of bytes used per character in UTF-8.
|
||||
str_length = _ca.CFStringGetLength(cfstrptr[0]) * 4
|
||||
str_buffer = _ffi.new('char[]', str_length+1)
|
||||
|
||||
err = _ca.CFStringGetCString(cfstrptr[0], str_buffer, str_length+1, _cac.kCFStringEncodingUTF8)
|
||||
assert err == 1, "Could not decode string"
|
||||
|
||||
return _ffi.string(str_buffer).decode()
|
||||
|
||||
|
||||
class _Player:
|
||||
"""A context manager for an active output stream.
|
||||
|
||||
Audio playback is available as soon as the context manager is
|
||||
entered. Audio data can be played using the `play` method.
|
||||
Successive calls to `play` will queue up the audio one piece after
|
||||
another. If no audio is queued up, this will play silence.
|
||||
|
||||
This context manager can only be entered once, and can not be used
|
||||
after it is closed.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, id, samplerate, channels, blocksize=None):
|
||||
self._au = _AudioUnit("output", id, samplerate, channels, blocksize)
|
||||
|
||||
def __enter__(self):
|
||||
self._queue = collections.deque()
|
||||
|
||||
@_ffi.callback("AURenderCallback")
|
||||
def render_callback(userdata, actionflags, timestamp,
|
||||
busnumber, numframes, bufferlist):
|
||||
for bufferidx in range(bufferlist.mNumberBuffers):
|
||||
dest = bufferlist.mBuffers[bufferidx]
|
||||
channels = dest.mNumberChannels
|
||||
bytes_written = 0
|
||||
to_write = dest.mDataByteSize
|
||||
while bytes_written < to_write:
|
||||
if self._queue:
|
||||
data = self._queue.popleft()
|
||||
srcbuffer = _ffi.from_buffer(data)
|
||||
numbytes = min(len(srcbuffer), to_write-bytes_written)
|
||||
_ffi.memmove(dest.mData+bytes_written, srcbuffer, numbytes)
|
||||
if numbytes < len(srcbuffer):
|
||||
leftover = data[numbytes//4//channels:]
|
||||
self._queue.appendleft(leftover)
|
||||
bytes_written += numbytes
|
||||
else:
|
||||
src = bytearray(to_write-bytes_written)
|
||||
_ffi.memmove(dest.mData+bytes_written, src, len(src))
|
||||
bytes_written += len(src)
|
||||
return 0
|
||||
|
||||
self._au.set_callback(render_callback)
|
||||
|
||||
self._au.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self._au.close()
|
||||
|
||||
def play(self, data, wait=True):
|
||||
"""Play some audio data.
|
||||
|
||||
Internally, all data is handled as float32 and with the
|
||||
appropriate number of channels. For maximum performance,
|
||||
provide data as a `frames × channels` float32 numpy array.
|
||||
|
||||
If single-channel or one-dimensional data is given, this data
|
||||
will be played on all available channels.
|
||||
|
||||
This function will return *before* all data has been played,
|
||||
so that additional data can be provided for gapless playback.
|
||||
The amount of buffering can be controlled through the
|
||||
blocksize of the player object.
|
||||
|
||||
If data is provided faster than it is played, later pieces
|
||||
will be queued up and played one after another.
|
||||
|
||||
"""
|
||||
|
||||
data = numpy.asarray(data, dtype="float32", order='C')
|
||||
data[data>1] = 1
|
||||
data[data<-1] = -1
|
||||
if data.ndim == 1:
|
||||
data = data[:, None] # force 2d
|
||||
if data.ndim != 2:
|
||||
raise TypeError('data must be 1d or 2d, not {}d'.format(data.ndim))
|
||||
if data.shape[1] == 1 and self._au.channels != 1:
|
||||
data = numpy.tile(data, [1, self._au.channels])
|
||||
if data.shape[1] != self._au.channels:
|
||||
raise TypeError('second dimension of data must be equal to the number of channels, not {}'.format(data.shape[1]))
|
||||
idx = 0
|
||||
while idx < len(data)-self._au.blocksize:
|
||||
self._queue.append(data[idx:idx+self._au.blocksize])
|
||||
idx += self._au.blocksize
|
||||
self._queue.append(data[idx:])
|
||||
while self._queue and wait:
|
||||
time.sleep(0.001)
|
||||
|
||||
class _AudioUnit:
|
||||
"""Communication helper with AudioUnits.
|
||||
|
||||
This provides an abstraction over a single AudioUnit. Can be used
|
||||
as soon as it instatiated.
|
||||
|
||||
Properties:
|
||||
- `enableinput`, `enableoutput`: set up the AudioUnit for playback
|
||||
or recording. It is not possible to record and play at the same
|
||||
time.
|
||||
- `device`: The numeric ID of the underlying CoreAudio device.
|
||||
- `blocksize`: The amount of buffering in the AudioUnit. Values
|
||||
outside of `blocksizerange` will be silently clamped to that
|
||||
range.
|
||||
- `blocksizerange`: The minimum and maximum possible block size.
|
||||
- `samplerate`: The sampling rate of the CoreAudio device. This
|
||||
will lead to errors if changed in a recording AudioUnit.
|
||||
- `channels`: The number of channels of the AudioUnit.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, iotype, device, samplerate, channels, blocksize):
|
||||
self._iotype = iotype
|
||||
|
||||
desc = _ffi.new(
|
||||
"AudioComponentDescription*",
|
||||
dict(componentType=_cac.kAudioUnitType_Output,
|
||||
componentSubType=_cac.kAudioUnitSubType_HALOutput,
|
||||
componentFlags=0,
|
||||
componentFlagsMask=0,
|
||||
componentManufacturer=_cac.kAudioUnitManufacturer_Apple))
|
||||
|
||||
audiocomponent = _au.AudioComponentFindNext(_ffi.NULL, desc)
|
||||
if not audiocomponent:
|
||||
raise RuntimeError("could not find audio component")
|
||||
self.ptr = _ffi.new("AudioComponentInstance*")
|
||||
status = _au.AudioComponentInstanceNew(audiocomponent, self.ptr)
|
||||
if status:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
|
||||
if iotype == 'input':
|
||||
self.enableinput = True
|
||||
self.enableoutput = False
|
||||
self._au_scope = _cac.kAudioUnitScope_Output
|
||||
self._au_element = 1
|
||||
elif iotype == 'output':
|
||||
self.enableinput = False
|
||||
self.enableoutput = True
|
||||
self._au_scope = _cac.kAudioUnitScope_Input
|
||||
self._au_element = 0
|
||||
|
||||
self.device = device
|
||||
|
||||
blocksize = blocksize or self.blocksize
|
||||
|
||||
# Input AudioUnits can't use non-native sample rates.
|
||||
# Therefore, if a non-native sample rate is requested, use a
|
||||
# resampled block size and resample later, manually:
|
||||
if iotype == 'input':
|
||||
# Get the input device format
|
||||
curr_device_format = self._get_property(_cac.kAudioUnitProperty_StreamFormat,
|
||||
_cac.kAudioUnitScope_Input,
|
||||
1,
|
||||
"AudioStreamBasicDescription")
|
||||
|
||||
self.samplerate = curr_device_format[0].mSampleRate
|
||||
self.resample = self.samplerate/samplerate
|
||||
else:
|
||||
self.resample = 1
|
||||
self.samplerate = samplerate
|
||||
|
||||
# there are two maximum block sizes for some reason:
|
||||
maxblocksize = min(self.blocksizerange[1],
|
||||
self.maxblocksize)
|
||||
if self.blocksizerange[0] <= blocksize <= maxblocksize:
|
||||
self.blocksize = blocksize
|
||||
else:
|
||||
raise TypeError("blocksize must be between {} and {}"
|
||||
.format(self.blocksizerange[0],
|
||||
maxblocksize))
|
||||
|
||||
if isinstance(channels, collections.abc.Iterable):
|
||||
if iotype == 'output':
|
||||
# invert channel map and fill with -1 ([2, 0] -> [1, -1, 0]):
|
||||
self.channels = len([c for c in channels if c >= 0])
|
||||
channelmap = [-1]*(max(channels)+1)
|
||||
for idx, c in enumerate(channels):
|
||||
channelmap[c] = idx
|
||||
self.channelmap = channelmap
|
||||
else:
|
||||
self.channels = len(channels)
|
||||
self.channelmap = channels
|
||||
elif isinstance(channels, int):
|
||||
self.channels = channels
|
||||
else:
|
||||
raise TypeError('channels must be iterable or integer')
|
||||
self._set_channels(self.channels)
|
||||
|
||||
def _set_property(self, property, scope, element, data):
|
||||
if '[]' in _ffi.typeof(data).cname:
|
||||
num_values = len(data)
|
||||
else:
|
||||
num_values = 1
|
||||
status = _au.AudioUnitSetProperty(self.ptr[0],
|
||||
property, scope, element,
|
||||
data, _ffi.sizeof(_ffi.typeof(data).item.cname)*num_values)
|
||||
if status != 0:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
|
||||
def _get_property(self, property, scope, element, type):
|
||||
datasize = _ffi.new("UInt32*")
|
||||
status = _au.AudioUnitGetPropertyInfo(self.ptr[0],
|
||||
property, scope, element,
|
||||
datasize, _ffi.NULL)
|
||||
num_values = datasize[0]//_ffi.sizeof(type)
|
||||
data = _ffi.new(type + '[{}]'.format(num_values))
|
||||
status = _au.AudioUnitGetProperty(self.ptr[0],
|
||||
property, scope, element,
|
||||
data, datasize)
|
||||
if status != 0:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
# return trivial data trivially
|
||||
if num_values == 1 and (type == "UInt32" or type == "Float64"):
|
||||
return data[0]
|
||||
else: # everything else, return the cdata, to keep it alive
|
||||
return data
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self._get_property(
|
||||
_cac.kAudioOutputUnitProperty_CurrentDevice,
|
||||
_cac.kAudioUnitScope_Global, 0, "UInt32")
|
||||
|
||||
@device.setter
|
||||
def device(self, dev):
|
||||
data = _ffi.new("UInt32*", dev)
|
||||
self._set_property(
|
||||
_cac.kAudioOutputUnitProperty_CurrentDevice,
|
||||
_cac.kAudioUnitScope_Global, 0, data)
|
||||
|
||||
@property
|
||||
def enableinput(self):
|
||||
return self._get_property(
|
||||
_cac.kAudioOutputUnitProperty_EnableIO,
|
||||
_cac.kAudioUnitScope_Input, 1, "UInt32")
|
||||
|
||||
@enableinput.setter
|
||||
def enableinput(self, yesno):
|
||||
data = _ffi.new("UInt32*", yesno)
|
||||
self._set_property(
|
||||
_cac.kAudioOutputUnitProperty_EnableIO,
|
||||
_cac.kAudioUnitScope_Input, 1, data)
|
||||
|
||||
@property
|
||||
def enableoutput(self):
|
||||
return self._get_property(
|
||||
_cac.kAudioOutputUnitProperty_EnableIO,
|
||||
_cac.kAudioUnitScope_Output, 0, "UInt32")
|
||||
|
||||
@enableoutput.setter
|
||||
def enableoutput(self, yesno):
|
||||
data = _ffi.new("UInt32*", yesno)
|
||||
self._set_property(
|
||||
_cac.kAudioOutputUnitProperty_EnableIO,
|
||||
_cac.kAudioUnitScope_Output, 0, data)
|
||||
|
||||
@property
|
||||
def samplerate(self):
|
||||
return self._get_property(
|
||||
_cac.kAudioUnitProperty_SampleRate,
|
||||
self._au_scope, self._au_element, "Float64")
|
||||
|
||||
@samplerate.setter
|
||||
def samplerate(self, samplerate):
|
||||
data = _ffi.new("Float64*", samplerate)
|
||||
self._set_property(
|
||||
_cac.kAudioUnitProperty_SampleRate,
|
||||
self._au_scope, self._au_element, data)
|
||||
|
||||
def _set_channels(self, channels):
|
||||
streamformat = _ffi.new(
|
||||
"AudioStreamBasicDescription*",
|
||||
dict(mSampleRate=self.samplerate,
|
||||
mFormatID=_cac.kAudioFormatLinearPCM,
|
||||
mFormatFlags=_cac.kAudioFormatFlagIsFloat,
|
||||
mFramesPerPacket=1,
|
||||
mChannelsPerFrame=channels,
|
||||
mBitsPerChannel=32,
|
||||
mBytesPerPacket=channels * 4,
|
||||
mBytesPerFrame=channels * 4))
|
||||
self._set_property(
|
||||
_cac.kAudioUnitProperty_StreamFormat,
|
||||
self._au_scope, self._au_element, streamformat)
|
||||
|
||||
@property
|
||||
def maxblocksize(self):
|
||||
maxblocksize = self._get_property(
|
||||
_cac.kAudioUnitProperty_MaximumFramesPerSlice,
|
||||
_cac.kAudioUnitScope_Global, 0, "UInt32")
|
||||
assert maxblocksize
|
||||
return maxblocksize
|
||||
|
||||
@property
|
||||
def channelmap(self):
|
||||
scope = {2: 1, 1: 2}[self._au_scope]
|
||||
map = self._get_property(
|
||||
_cac.kAudioOutputUnitProperty_ChannelMap,
|
||||
scope, self._au_element,
|
||||
"SInt32")
|
||||
last_meaningful = max(idx for idx, c in enumerate(map) if c != -1)
|
||||
return list(map[0:last_meaningful+1])
|
||||
|
||||
@channelmap.setter
|
||||
def channelmap(self, map):
|
||||
scope = {2: 1, 1: 2}[self._au_scope]
|
||||
cmap = _ffi.new("SInt32[]", map)
|
||||
self._set_property(
|
||||
_cac.kAudioOutputUnitProperty_ChannelMap,
|
||||
scope, self._au_element,
|
||||
cmap)
|
||||
|
||||
@property
|
||||
def blocksizerange(self):
|
||||
framesizerange = _CoreAudio.get_property(
|
||||
self.device,
|
||||
_cac.kAudioDevicePropertyBufferFrameSizeRange,
|
||||
'AudioValueRange', scope=_cac.kAudioObjectPropertyScopeOutput)
|
||||
assert framesizerange
|
||||
return framesizerange[0].mMinimum, framesizerange[0].mMaximum
|
||||
|
||||
@property
|
||||
def blocksize(self):
|
||||
framesize = _CoreAudio.get_property(
|
||||
self.device,
|
||||
_cac.kAudioDevicePropertyBufferFrameSize,
|
||||
'UInt32', scope=_cac.kAudioObjectPropertyScopeOutput)
|
||||
assert framesize
|
||||
return framesize[0]
|
||||
|
||||
@blocksize.setter
|
||||
def blocksize(self, blocksize):
|
||||
framesize = _ffi.new("UInt32*", blocksize)
|
||||
status = _CoreAudio.set_property(
|
||||
self.device,
|
||||
_cac.kAudioDevicePropertyBufferFrameSize,
|
||||
framesize, scope=_cac.kAudioObjectPropertyScopeOutput)
|
||||
|
||||
def set_callback(self, callback):
|
||||
"""Set a callback function for the AudioUnit. """
|
||||
|
||||
if self._iotype == 'input':
|
||||
callbacktype = _cac.kAudioOutputUnitProperty_SetInputCallback
|
||||
elif self._iotype == 'output':
|
||||
callbacktype = _cac.kAudioUnitProperty_SetRenderCallback
|
||||
|
||||
self._callback = callback
|
||||
callbackstruct = _ffi.new(
|
||||
"AURenderCallbackStruct*",
|
||||
dict(inputProc=callback,
|
||||
inputProcRefCon=_ffi.NULL))
|
||||
self._set_property(
|
||||
callbacktype,
|
||||
_cac.kAudioUnitScope_Global, 0, callbackstruct)
|
||||
|
||||
def start(self):
|
||||
"""Start processing audio, and start calling the callback."""
|
||||
|
||||
status = _au.AudioUnitInitialize(self.ptr[0])
|
||||
if status:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
status = _au.AudioOutputUnitStart(self.ptr[0])
|
||||
if status:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
|
||||
def close(self):
|
||||
"""Stop processing audio, and stop calling the callback."""
|
||||
|
||||
status = _au.AudioOutputUnitStop(self.ptr[0])
|
||||
if status:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
status = _au.AudioComponentInstanceDispose(self.ptr[0])
|
||||
if status:
|
||||
raise RuntimeError(_cac.error_number_to_string(status))
|
||||
del self.ptr
|
||||
|
||||
|
||||
# Here's how to do it: http://atastypixel.com/blog/using-remoteio-audio-unit/
|
||||
# https://developer.apple.com/library/content/technotes/tn2091/_index.html
|
||||
|
||||
|
||||
class _Resampler:
|
||||
def __init__(self, fromsamplerate, tosamplerate, channels):
|
||||
self.fromsamplerate = fromsamplerate
|
||||
self.tosamplerate = tosamplerate
|
||||
self.channels = channels
|
||||
|
||||
fromstreamformat = _ffi.new(
|
||||
"AudioStreamBasicDescription*",
|
||||
dict(mSampleRate=self.fromsamplerate,
|
||||
mFormatID=_cac.kAudioFormatLinearPCM,
|
||||
mFormatFlags=_cac.kAudioFormatFlagIsFloat,
|
||||
mFramesPerPacket=1,
|
||||
mChannelsPerFrame=self.channels,
|
||||
mBitsPerChannel=32,
|
||||
mBytesPerPacket=self.channels * 4,
|
||||
mBytesPerFrame=self.channels * 4))
|
||||
|
||||
tostreamformat = _ffi.new(
|
||||
"AudioStreamBasicDescription*",
|
||||
dict(mSampleRate=self.tosamplerate,
|
||||
mFormatID=_cac.kAudioFormatLinearPCM,
|
||||
mFormatFlags=_cac.kAudioFormatFlagIsFloat,
|
||||
mFramesPerPacket=1,
|
||||
mChannelsPerFrame=self.channels,
|
||||
mBitsPerChannel=32,
|
||||
mBytesPerPacket=self.channels * 4,
|
||||
mBytesPerFrame=self.channels * 4))
|
||||
|
||||
self.audioconverter = _ffi.new("AudioConverterRef*")
|
||||
_au.AudioConverterNew(fromstreamformat, tostreamformat, self.audioconverter)
|
||||
|
||||
@_ffi.callback("AudioConverterComplexInputDataProc")
|
||||
def converter_callback(converter, numberpackets, bufferlist, desc, userdata):
|
||||
return self.converter_callback(converter, numberpackets, bufferlist, desc, userdata)
|
||||
self._converter_callback = converter_callback
|
||||
|
||||
self.queue = []
|
||||
|
||||
self.blocksize = 512
|
||||
self.outbuffer = _ffi.new("AudioBufferList*", [1, 1])
|
||||
self.outbuffer.mNumberBuffers = 1
|
||||
self.outbuffer.mBuffers[0].mNumberChannels = self.channels
|
||||
self.outbuffer.mBuffers[0].mDataByteSize = self.blocksize*4*self.channels
|
||||
self.outdata = _ffi.new("Float32[]", self.blocksize*self.channels)
|
||||
self.outbuffer.mBuffers[0].mData = self.outdata
|
||||
self.outsize = _ffi.new("UInt32*")
|
||||
|
||||
def converter_callback(self, converter, numberpackets, bufferlist, desc, userdata):
|
||||
numframes = min(numberpackets[0], len(self.todo), self.blocksize)
|
||||
raw_data = self.todo[:numframes].tobytes()
|
||||
_ffi.memmove(self.outdata, raw_data, len(raw_data))
|
||||
bufferlist[0].mBuffers[0].mDataByteSize = len(raw_data)
|
||||
bufferlist[0].mBuffers[0].mData = self.outdata
|
||||
numberpackets[0] = numframes
|
||||
self.todo = self.todo[numframes:]
|
||||
|
||||
if len(self.todo) == 0 and numframes == 0:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def resample(self, data):
|
||||
self.todo = numpy.array(data, dtype='float32')
|
||||
while len(self.todo) > 0:
|
||||
self.outsize[0] = self.blocksize
|
||||
# Set outbuffer each iteration to avoid mDataByteSize decreasing over time
|
||||
self.outbuffer.mNumberBuffers = 1
|
||||
self.outbuffer.mBuffers[0].mNumberChannels = self.channels
|
||||
self.outbuffer.mBuffers[0].mDataByteSize = self.blocksize*4*self.channels
|
||||
self.outbuffer.mBuffers[0].mData = self.outdata
|
||||
|
||||
status = _au.AudioConverterFillComplexBuffer(self.audioconverter[0],
|
||||
self._converter_callback,
|
||||
_ffi.NULL,
|
||||
self.outsize,
|
||||
self.outbuffer,
|
||||
_ffi.NULL)
|
||||
|
||||
if status != 0 and status != -1:
|
||||
raise RuntimeError('error during sample rate conversion:', status)
|
||||
|
||||
array = numpy.frombuffer(_ffi.buffer(self.outdata), dtype='float32').copy()
|
||||
|
||||
self.queue.append(array[:self.outsize[0]*self.channels])
|
||||
|
||||
converted_data = numpy.concatenate(self.queue)
|
||||
self.queue.clear()
|
||||
|
||||
return converted_data.reshape([-1, self.channels])
|
||||
|
||||
def __del__(self):
|
||||
_au.AudioConverterDispose(self.audioconverter[0])
|
||||
|
||||
|
||||
class _Recorder:
|
||||
"""A context manager for an active input stream.
|
||||
|
||||
Audio recording is available as soon as the context manager is
|
||||
entered. Recorded audio data can be read using the `record`
|
||||
method. If no audio data is available, `record` will block until
|
||||
the requested amount of audio data has been recorded.
|
||||
|
||||
This context manager can only be entered once, and can not be used
|
||||
after it is closed.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, id, samplerate, channels, blocksize=None):
|
||||
self._au = _AudioUnit("input", id, samplerate, channels, blocksize)
|
||||
self._resampler = _Resampler(self._au.samplerate, samplerate, self._au.channels)
|
||||
self._record_event = threading.Event()
|
||||
|
||||
def __enter__(self):
|
||||
self._queue = collections.deque()
|
||||
self._pending_chunk = numpy.zeros([0, self._au.channels], dtype='float32')
|
||||
|
||||
channels = self._au.channels
|
||||
au = self._au.ptr[0]
|
||||
|
||||
@_ffi.callback("AURenderCallback")
|
||||
def input_callback(userdata, actionflags, timestamp,
|
||||
busnumber, numframes, bufferlist):
|
||||
bufferlist = _ffi.new("AudioBufferList*", [1, 1])
|
||||
bufferlist.mNumberBuffers = 1
|
||||
bufferlist.mBuffers[0].mNumberChannels = channels
|
||||
bufferlist.mBuffers[0].mDataByteSize = numframes * 4 * channels
|
||||
bufferlist.mBuffers[0].mData = _ffi.NULL
|
||||
|
||||
status = _au.AudioUnitRender(au,
|
||||
actionflags,
|
||||
timestamp,
|
||||
busnumber,
|
||||
numframes,
|
||||
bufferlist)
|
||||
|
||||
# special case if output is silence:
|
||||
if (actionflags[0] == _cac.kAudioUnitRenderAction_OutputIsSilence
|
||||
and status == _cac.kAudioUnitErr_CannotDoInCurrentContext):
|
||||
actionflags[0] = 0 # reset actionflags
|
||||
status = 0 # reset error code
|
||||
data = numpy.zeros([numframes, channels], 'float32')
|
||||
else:
|
||||
data = numpy.frombuffer(_ffi.buffer(bufferlist.mBuffers[0].mData,
|
||||
bufferlist.mBuffers[0].mDataByteSize),
|
||||
dtype='float32')
|
||||
data = data.reshape([-1, bufferlist.mBuffers[0].mNumberChannels]).copy()
|
||||
|
||||
if status != 0:
|
||||
print('error during recording:', status)
|
||||
|
||||
self._queue.append(data)
|
||||
self._record_event.set()
|
||||
return status
|
||||
|
||||
self._au.set_callback(input_callback)
|
||||
self._au.start()
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self._au.close()
|
||||
|
||||
def _record_chunk(self):
|
||||
"""Record one chunk of audio data, as returned by core audio
|
||||
|
||||
The data will be returned as a 1D numpy array, which will be used by
|
||||
the `record` method. This function is the interface of the `_Recorder`
|
||||
object with core audio.
|
||||
"""
|
||||
while not self._queue:
|
||||
self._record_event.wait()
|
||||
self._record_event.clear()
|
||||
block = self._queue.popleft()
|
||||
|
||||
# perform sample rate conversion:
|
||||
if self._au.resample != 1:
|
||||
block = self._resampler.resample(block)
|
||||
return block
|
||||
|
||||
def record(self, numframes=None):
|
||||
"""Record a block of audio data.
|
||||
|
||||
The data will be returned as a frames × channels float32 numpy array.
|
||||
This function will wait until numframes frames have been recorded.
|
||||
If numframes is given, it will return exactly `numframes` frames,
|
||||
and buffer the rest for later.
|
||||
|
||||
If numframes is None, it will return whatever the audio backend
|
||||
has available right now.
|
||||
Use this if latency must be kept to a minimum, but be aware that
|
||||
block sizes can change at the whims of the audio backend.
|
||||
|
||||
If using `record` with `numframes=None` after using `record` with a
|
||||
required `numframes`, the last buffered frame will be returned along
|
||||
with the new recorded block.
|
||||
(If you want to empty the last buffered frame instead, use `flush`)
|
||||
|
||||
"""
|
||||
|
||||
if numframes is None:
|
||||
blocks = [self._pending_chunk, self._record_chunk()]
|
||||
self._pending_chunk = numpy.zeros([0, self._au.channels], dtype='float32')
|
||||
else:
|
||||
blocks = [self._pending_chunk]
|
||||
self._pending_chunk = numpy.zeros([0, self._au.channels], dtype='float32')
|
||||
recorded_frames = len(blocks[0])
|
||||
while recorded_frames < numframes:
|
||||
block = self._record_chunk()
|
||||
blocks.append(block)
|
||||
recorded_frames += len(block)
|
||||
if recorded_frames > numframes:
|
||||
to_split = -(recorded_frames-numframes)
|
||||
blocks[-1], self._pending_chunk = numpy.split(blocks[-1], [to_split])
|
||||
data = numpy.concatenate(blocks, axis=0)
|
||||
|
||||
return data
|
||||
|
||||
def flush(self):
|
||||
"""Return the last pending chunk
|
||||
After using the record method, this will return the last incomplete
|
||||
chunk and delete it.
|
||||
|
||||
"""
|
||||
last_chunk = numpy.reshape(self._pending_chunk, [-1, self._au.channels])
|
||||
self._pending_chunk = numpy.zeros([0, self._au.channels], dtype='float32')
|
||||
return last_chunk
|
||||
0
LXST/Platforms/linux/__init__.py
Normal file
0
LXST/Platforms/linux/__init__.py
Normal file
419
LXST/Platforms/linux/pulseaudio.h
Normal file
419
LXST/Platforms/linux/pulseaudio.h
Normal file
@@ -0,0 +1,419 @@
|
||||
|
||||
typedef enum pa_stream_direction {
|
||||
PA_STREAM_NODIRECTION,
|
||||
PA_STREAM_PLAYBACK,
|
||||
PA_STREAM_RECORD,
|
||||
PA_STREAM_UPLOAD
|
||||
} pa_stream_direction_t;
|
||||
|
||||
typedef enum pa_sample_format {
|
||||
PA_SAMPLE_U8,
|
||||
PA_SAMPLE_ALAW,
|
||||
PA_SAMPLE_ULAW,
|
||||
PA_SAMPLE_S16LE,
|
||||
PA_SAMPLE_S16BE,
|
||||
PA_SAMPLE_FLOAT32LE,
|
||||
PA_SAMPLE_FLOAT32BE,
|
||||
PA_SAMPLE_S32LE,
|
||||
PA_SAMPLE_S32BE,
|
||||
PA_SAMPLE_S24LE,
|
||||
PA_SAMPLE_S24BE,
|
||||
PA_SAMPLE_S24_32LE,
|
||||
PA_SAMPLE_S24_32BE,
|
||||
PA_SAMPLE_MAX,
|
||||
PA_SAMPLE_INVALID = -1
|
||||
} pa_sample_format_t;
|
||||
|
||||
typedef struct pa_sample_spec {
|
||||
pa_sample_format_t format;
|
||||
uint32_t rate;
|
||||
uint8_t channels;
|
||||
} pa_sample_spec;
|
||||
|
||||
typedef enum pa_channel_position {
|
||||
PA_CHANNEL_POSITION_INVALID = -1,
|
||||
PA_CHANNEL_POSITION_MONO = 0,
|
||||
|
||||
PA_CHANNEL_POSITION_FRONT_LEFT,
|
||||
PA_CHANNEL_POSITION_FRONT_RIGHT,
|
||||
PA_CHANNEL_POSITION_FRONT_CENTER,
|
||||
PA_CHANNEL_POSITION_LEFT = PA_CHANNEL_POSITION_FRONT_LEFT,
|
||||
PA_CHANNEL_POSITION_RIGHT = PA_CHANNEL_POSITION_FRONT_RIGHT,
|
||||
PA_CHANNEL_POSITION_CENTER = PA_CHANNEL_POSITION_FRONT_CENTER,
|
||||
PA_CHANNEL_POSITION_REAR_CENTER,
|
||||
PA_CHANNEL_POSITION_REAR_LEFT,
|
||||
PA_CHANNEL_POSITION_REAR_RIGHT,
|
||||
PA_CHANNEL_POSITION_LFE,
|
||||
PA_CHANNEL_POSITION_SUBWOOFER = PA_CHANNEL_POSITION_LFE,
|
||||
PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER,
|
||||
PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER,
|
||||
PA_CHANNEL_POSITION_SIDE_LEFT,
|
||||
PA_CHANNEL_POSITION_SIDE_RIGHT,
|
||||
PA_CHANNEL_POSITION_AUX0,
|
||||
PA_CHANNEL_POSITION_AUX1,
|
||||
PA_CHANNEL_POSITION_AUX2,
|
||||
PA_CHANNEL_POSITION_AUX3,
|
||||
PA_CHANNEL_POSITION_AUX4,
|
||||
PA_CHANNEL_POSITION_AUX5,
|
||||
PA_CHANNEL_POSITION_AUX6,
|
||||
PA_CHANNEL_POSITION_AUX7,
|
||||
PA_CHANNEL_POSITION_AUX8,
|
||||
PA_CHANNEL_POSITION_AUX9,
|
||||
PA_CHANNEL_POSITION_AUX10,
|
||||
PA_CHANNEL_POSITION_AUX11,
|
||||
PA_CHANNEL_POSITION_AUX12,
|
||||
PA_CHANNEL_POSITION_AUX13,
|
||||
PA_CHANNEL_POSITION_AUX14,
|
||||
PA_CHANNEL_POSITION_AUX15,
|
||||
PA_CHANNEL_POSITION_AUX16,
|
||||
PA_CHANNEL_POSITION_AUX17,
|
||||
PA_CHANNEL_POSITION_AUX18,
|
||||
PA_CHANNEL_POSITION_AUX19,
|
||||
PA_CHANNEL_POSITION_AUX20,
|
||||
PA_CHANNEL_POSITION_AUX21,
|
||||
PA_CHANNEL_POSITION_AUX22,
|
||||
PA_CHANNEL_POSITION_AUX23,
|
||||
PA_CHANNEL_POSITION_AUX24,
|
||||
PA_CHANNEL_POSITION_AUX25,
|
||||
PA_CHANNEL_POSITION_AUX26,
|
||||
PA_CHANNEL_POSITION_AUX27,
|
||||
PA_CHANNEL_POSITION_AUX28,
|
||||
PA_CHANNEL_POSITION_AUX29,
|
||||
PA_CHANNEL_POSITION_AUX30,
|
||||
PA_CHANNEL_POSITION_AUX31,
|
||||
|
||||
PA_CHANNEL_POSITION_TOP_CENTER,
|
||||
PA_CHANNEL_POSITION_TOP_FRONT_LEFT,
|
||||
PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,
|
||||
PA_CHANNEL_POSITION_TOP_FRONT_CENTER,
|
||||
PA_CHANNEL_POSITION_TOP_REAR_LEFT,
|
||||
PA_CHANNEL_POSITION_TOP_REAR_RIGHT,
|
||||
PA_CHANNEL_POSITION_TOP_REAR_CENTER,
|
||||
PA_CHANNEL_POSITION_MAX
|
||||
} pa_channel_position_t;
|
||||
|
||||
#define PA_CHANNELS_MAX 32U
|
||||
|
||||
typedef struct pa_channel_map {
|
||||
uint8_t channels;
|
||||
pa_channel_position_t map[PA_CHANNELS_MAX];
|
||||
} pa_channel_map;
|
||||
|
||||
typedef enum pa_channel_map_def {
|
||||
PA_CHANNEL_MAP_AIFF,
|
||||
PA_CHANNEL_MAP_ALSA,
|
||||
PA_CHANNEL_MAP_AUX,
|
||||
PA_CHANNEL_MAP_WAVEEX,
|
||||
PA_CHANNEL_MAP_OSS,
|
||||
PA_CHANNEL_MAP_DEF_MAX,
|
||||
PA_CHANNEL_MAP_DEFAULT = PA_CHANNEL_MAP_AIFF
|
||||
} pa_channel_map_def_t;
|
||||
|
||||
pa_channel_map* pa_channel_map_init_extend(pa_channel_map *m, unsigned channels, pa_channel_map_def_t def);
|
||||
int pa_channel_map_valid(const pa_channel_map *map);
|
||||
const char* pa_channel_position_to_string(pa_channel_position_t pos);
|
||||
|
||||
typedef struct pa_buffer_attr {
|
||||
uint32_t maxlength;
|
||||
uint32_t tlength;
|
||||
uint32_t prebuf;
|
||||
uint32_t minreq;
|
||||
uint32_t fragsize;
|
||||
} pa_buffer_attr;
|
||||
|
||||
typedef struct pa_simple pa_simple;
|
||||
|
||||
pa_simple* pa_simple_new(
|
||||
const char *server,
|
||||
const char *name,
|
||||
pa_stream_direction_t dir,
|
||||
const char *dev,
|
||||
const char *stream_name,
|
||||
const pa_sample_spec *ss,
|
||||
const pa_channel_map *map,
|
||||
const pa_buffer_attr *attr,
|
||||
int *error
|
||||
);
|
||||
|
||||
typedef struct pa_mainloop pa_mainloop;
|
||||
pa_mainloop *pa_mainloop_new(void);
|
||||
void pa_mainloop_free(pa_mainloop* m);
|
||||
int pa_mainloop_run(pa_mainloop *m, int *retval);
|
||||
void pa_mainloop_quit(pa_mainloop *m, int retval);
|
||||
|
||||
typedef struct pa_threaded_mainloop pa_threaded_mainloop;
|
||||
pa_threaded_mainloop *pa_threaded_mainloop_new(void);
|
||||
int pa_threaded_mainloop_start(pa_threaded_mainloop *m);
|
||||
void pa_threaded_mainloop_stop(pa_threaded_mainloop *m);
|
||||
void pa_threaded_mainloop_free(pa_threaded_mainloop *m);
|
||||
void pa_threaded_mainloop_lock(pa_threaded_mainloop *m);
|
||||
void pa_threaded_mainloop_unlock(pa_threaded_mainloop *m);
|
||||
|
||||
typedef struct pa_mainloop_api pa_mainloop_api;
|
||||
pa_mainloop_api* pa_mainloop_get_api(pa_mainloop*m);
|
||||
pa_mainloop_api *pa_threaded_mainloop_get_api(pa_threaded_mainloop *m);
|
||||
|
||||
typedef struct pa_context pa_context;
|
||||
pa_context *pa_context_new(pa_mainloop_api *mainloop, const char *name);
|
||||
void pa_context_unref(pa_context *c);
|
||||
typedef enum pa_context_flags {PA_CONTEXT_NOFLAGS = 0} pa_context_flags_t;
|
||||
typedef struct pa_spawn_api pa_spawn_api;
|
||||
int pa_context_connect(pa_context *c, const char *server, pa_context_flags_t flags, const pa_spawn_api *api);
|
||||
void pa_context_disconnect(pa_context *c);
|
||||
int pa_context_errno(const pa_context *c);
|
||||
typedef enum pa_context_state {
|
||||
PA_CONTEXT_UNCONNECTED,
|
||||
PA_CONTEXT_CONNECTING,
|
||||
PA_CONTEXT_AUTHORIZING,
|
||||
PA_CONTEXT_SETTING_NAME,
|
||||
PA_CONTEXT_READY,
|
||||
PA_CONTEXT_FAILED,
|
||||
PA_CONTEXT_TERMINATED
|
||||
} pa_context_state_t;
|
||||
pa_context_state_t pa_context_get_state(pa_context *c);
|
||||
|
||||
typedef struct pa_operation pa_operation;
|
||||
pa_operation *pa_operation_ref(pa_operation *o);
|
||||
void pa_operation_unref(pa_operation *o);
|
||||
typedef enum pa_operation_state {
|
||||
PA_OPERATION_RUNNING,
|
||||
PA_OPERATION_DONE,
|
||||
PA_OPERATION_CANCELLED
|
||||
} pa_operation_state_t;
|
||||
pa_operation_state_t pa_operation_get_state(pa_operation *o);
|
||||
|
||||
typedef enum pa_sink_state { /* enum serialized in u8 */
|
||||
PA_SINK_INVALID_STATE = -1,
|
||||
PA_SINK_RUNNING = 0,
|
||||
PA_SINK_IDLE = 1,
|
||||
PA_SINK_SUSPENDED = 2
|
||||
} pa_sink_state_t;
|
||||
|
||||
typedef struct pa_proplist pa_proplist;
|
||||
const char *pa_proplist_gets(pa_proplist *p, const char *key);
|
||||
|
||||
typedef enum pa_encoding {
|
||||
PA_ENCODING_ANY,
|
||||
PA_ENCODING_PCM,
|
||||
PA_ENCODING_AC3_IEC61937,
|
||||
PA_ENCODING_EAC3_IEC61937,
|
||||
PA_ENCODING_MPEG_IEC61937,
|
||||
PA_ENCODING_DTS_IEC61937,
|
||||
PA_ENCODING_MPEG2_AAC_IEC61937,
|
||||
PA_ENCODING_MAX,
|
||||
PA_ENCODING_INVALID = -1,
|
||||
} pa_encoding_t;
|
||||
|
||||
typedef struct pa_format_info {
|
||||
pa_encoding_t encoding;
|
||||
pa_proplist *plist;
|
||||
} pa_format_info;
|
||||
|
||||
typedef struct pa_sink_port_info {
|
||||
const char *name;
|
||||
const char *description;
|
||||
uint32_t priority;
|
||||
int available;
|
||||
} pa_sink_port_info;
|
||||
|
||||
typedef uint32_t pa_volume_t;
|
||||
typedef struct pa_cvolume {
|
||||
uint8_t channels;
|
||||
pa_volume_t values[PA_CHANNELS_MAX];
|
||||
} pa_cvolume;
|
||||
|
||||
typedef uint64_t pa_usec_t;
|
||||
|
||||
typedef enum pa_sink_flags {
|
||||
PA_SINK_NOFLAGS = 0x0000,
|
||||
PA_SINK_HW_VOLUME_CTRL = 0x0001,
|
||||
PA_SINK_LATENCY = 0x0002,
|
||||
PA_SINK_HARDWARE = 0x0004,
|
||||
PA_SINK_NETWORK = 0x0008,
|
||||
PA_SINK_HW_MUTE_CTRL = 0x0010,
|
||||
PA_SINK_DECIBEL_VOLUME = 0x0020,
|
||||
PA_SINK_FLAT_VOLUME = 0x0040,
|
||||
PA_SINK_DYNAMIC_LATENCY = 0x0080,
|
||||
PA_SINK_SET_FORMATS = 0x0100
|
||||
} pa_sink_flags_t;
|
||||
|
||||
typedef struct pa_sink_info {
|
||||
const char *name;
|
||||
uint32_t index;
|
||||
const char *description;
|
||||
pa_sample_spec sample_spec;
|
||||
pa_channel_map channel_map;
|
||||
uint32_t owner_module;
|
||||
pa_cvolume volume;
|
||||
int mute;
|
||||
uint32_t monitor_source;
|
||||
const char *monitor_source_name;
|
||||
pa_usec_t latency;
|
||||
const char *driver;
|
||||
pa_sink_flags_t flags;
|
||||
pa_proplist *proplist;
|
||||
pa_usec_t configured_latency;
|
||||
pa_volume_t base_volume;
|
||||
pa_sink_state_t state;
|
||||
uint32_t n_volume_steps;
|
||||
uint32_t card;
|
||||
uint32_t n_ports;
|
||||
pa_sink_port_info** ports;
|
||||
pa_sink_port_info* active_port;
|
||||
uint8_t n_formats;
|
||||
pa_format_info **formats;
|
||||
} pa_sink_info;
|
||||
|
||||
typedef struct pa_source_port_info {
|
||||
const char *name;
|
||||
const char *description;
|
||||
uint32_t priority;
|
||||
int available;
|
||||
} pa_source_port_info;
|
||||
|
||||
typedef enum pa_source_flags {
|
||||
PA_SOURCE_NOFLAGS = 0x0000,
|
||||
PA_SOURCE_HW_VOLUME_CTRL = 0x0001,
|
||||
PA_SOURCE_LATENCY = 0x0002,
|
||||
PA_SOURCE_HARDWARE = 0x0004,
|
||||
PA_SOURCE_NETWORK = 0x0008,
|
||||
PA_SOURCE_HW_MUTE_CTRL = 0x0010,
|
||||
PA_SOURCE_DECIBEL_VOLUME = 0x0020,
|
||||
PA_SOURCE_DYNAMIC_LATENCY = 0x0040,
|
||||
PA_SOURCE_FLAT_VOLUME = 0x0080
|
||||
} pa_source_flags_t;
|
||||
|
||||
typedef enum pa_source_state {
|
||||
PA_SOURCE_INVALID_STATE = -1,
|
||||
PA_SOURCE_RUNNING = 0,
|
||||
PA_SOURCE_IDLE = 1,
|
||||
PA_SOURCE_SUSPENDED = 2
|
||||
} pa_source_state_t;
|
||||
|
||||
typedef struct pa_source_info {
|
||||
const char *name;
|
||||
uint32_t index;
|
||||
const char *description;
|
||||
pa_sample_spec sample_spec;
|
||||
pa_channel_map channel_map;
|
||||
uint32_t owner_module;
|
||||
pa_cvolume volume;
|
||||
int mute;
|
||||
uint32_t monitor_of_sink;
|
||||
const char *monitor_of_sink_name;
|
||||
pa_usec_t latency;
|
||||
const char *driver;
|
||||
pa_source_flags_t flags; //
|
||||
pa_proplist *proplist;
|
||||
pa_usec_t configured_latency;
|
||||
pa_volume_t base_volume;
|
||||
pa_source_state_t state; //
|
||||
uint32_t n_volume_steps;
|
||||
uint32_t card;
|
||||
uint32_t n_ports;
|
||||
pa_source_port_info** ports;
|
||||
pa_source_port_info* active_port;
|
||||
uint8_t n_formats;
|
||||
pa_format_info **formats;
|
||||
} pa_source_info;
|
||||
|
||||
typedef void (*pa_sink_info_cb_t)(pa_context *c, const pa_sink_info *i, int eol, void *userdata);
|
||||
pa_operation* pa_context_get_sink_info_list(pa_context *c, pa_sink_info_cb_t cb, void *userdata);
|
||||
pa_operation* pa_context_get_sink_info_by_name(pa_context *c, const char *name, pa_sink_info_cb_t cb, void *userdata);
|
||||
typedef void (*pa_source_info_cb_t)(pa_context *c, const pa_source_info *i, int eol, void *userdata);
|
||||
pa_operation* pa_context_get_source_info_list(pa_context *c, pa_source_info_cb_t cb, void *userdata);
|
||||
pa_operation* pa_context_get_source_info_by_name(pa_context *c, const char *name, pa_source_info_cb_t cb, void *userdata);
|
||||
typedef void (*pa_context_notify_cb)(pa_context *c, void *userdata);
|
||||
pa_operation* pa_context_drain(pa_context *c, pa_context_notify_cb cb, void *userdata);
|
||||
typedef void (*pa_context_success_cb_t)(pa_context *c, int success, void *userdata);
|
||||
pa_operation* pa_context_set_name(pa_context *c, const char *name, pa_context_success_cb_t cb, void *userdata);
|
||||
uint32_t pa_context_get_index(const pa_context *s);
|
||||
|
||||
typedef struct pa_client_info {
|
||||
uint32_t index;
|
||||
const char *name;
|
||||
uint32_t owner_module;
|
||||
const char *driver;
|
||||
pa_proplist *proplist;
|
||||
} pa_client_info;
|
||||
typedef void (*pa_client_info_cb_t) (pa_context *c, const pa_client_info*i, int eol, void *userdata);
|
||||
pa_operation* pa_context_get_client_info(pa_context *c, uint32_t idx, pa_client_info_cb_t cb, void *userdata);
|
||||
|
||||
typedef struct pa_server_info {
|
||||
const char *user_name;
|
||||
const char *host_name;
|
||||
const char *server_version;
|
||||
const char *server_name;
|
||||
pa_sample_spec sample_spec;
|
||||
const char *default_sink_name;
|
||||
const char *default_source_name;
|
||||
uint32_t cookie;
|
||||
pa_channel_map channel_map;
|
||||
} pa_server_info;
|
||||
typedef void (*pa_server_info_cb_t) (pa_context *c, const pa_server_info*i, void *userdata);
|
||||
pa_operation* pa_context_get_server_info(pa_context *c, pa_server_info_cb_t cb, void *userdata);
|
||||
|
||||
int pa_sample_spec_valid(const pa_sample_spec *spec);
|
||||
|
||||
typedef struct pa_stream pa_stream;
|
||||
pa_stream* pa_stream_new(pa_context *c, const char *name, const pa_sample_spec *ss, const pa_channel_map *map);
|
||||
void pa_stream_unref(pa_stream *s);
|
||||
|
||||
typedef enum pa_stream_flags {
|
||||
PA_STREAM_NOFLAGS = 0x0000,
|
||||
PA_STREAM_START_CORKED = 0x0001,
|
||||
PA_STREAM_INTERPOLATE_TIMING = 0x0002,
|
||||
PA_STREAM_NOT_MONOTONIC = 0x0004,
|
||||
PA_STREAM_AUTO_TIMING_UPDATE = 0x0008,
|
||||
PA_STREAM_NO_REMAP_CHANNELS = 0x0010,
|
||||
PA_STREAM_NO_REMIX_CHANNELS = 0x0020,
|
||||
PA_STREAM_FIX_FORMAT = 0x0040,
|
||||
PA_STREAM_FIX_RATE = 0x0080,
|
||||
PA_STREAM_FIX_CHANNELS = 0x0100,
|
||||
PA_STREAM_DONT_MOVE = 0x0200,
|
||||
PA_STREAM_VARIABLE_RATE = 0x0400,
|
||||
PA_STREAM_PEAK_DETECT = 0x0800,
|
||||
PA_STREAM_START_MUTED = 0x1000,
|
||||
PA_STREAM_ADJUST_LATENCY = 0x2000,
|
||||
PA_STREAM_EARLY_REQUESTS = 0x4000,
|
||||
PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND = 0x8000,
|
||||
PA_STREAM_START_UNMUTED = 0x10000,
|
||||
PA_STREAM_FAIL_ON_SUSPEND = 0x20000,
|
||||
PA_STREAM_RELATIVE_VOLUME = 0x40000,
|
||||
PA_STREAM_PASSTHROUGH = 0x80000
|
||||
} pa_stream_flags_t;
|
||||
int pa_stream_connect_playback(pa_stream *s, const char *dev, const pa_buffer_attr *attr, pa_stream_flags_t flags, const pa_cvolume *volume, pa_stream *sync_stream);
|
||||
int pa_stream_connect_record(pa_stream *s, const char *dev, const pa_buffer_attr *attr, pa_stream_flags_t flags);
|
||||
int pa_stream_disconnect(pa_stream *s);
|
||||
typedef void (*pa_stream_success_cb_t) (pa_stream*s, int success, void *userdata);
|
||||
pa_operation* pa_stream_cork(pa_stream *s, int b, pa_stream_success_cb_t cb, void *userdata);
|
||||
pa_operation* pa_stream_drain(pa_stream *s, pa_stream_success_cb_t cb, void *userdata);
|
||||
size_t pa_stream_writable_size(pa_stream *p);
|
||||
size_t pa_stream_readable_size(pa_stream *p);
|
||||
typedef void (*pa_free_cb_t)(void *p);
|
||||
typedef enum pa_seek_mode {
|
||||
PA_SEEK_RELATIVE = 0,
|
||||
PA_SEEK_ABSOLUTE = 1,
|
||||
PA_SEEK_RELATIVE_ON_READ = 2,
|
||||
PA_SEEK_RELATIVE_END = 3
|
||||
} pa_seek_mode_t;
|
||||
int pa_stream_write(pa_stream *p, const void *data, size_t nbytes, pa_free_cb_t free_cb, int64_t offset, pa_seek_mode_t seek);
|
||||
int pa_stream_peek(pa_stream *p, const void **data, size_t *nbytes);
|
||||
int pa_stream_drop(pa_stream *p);
|
||||
int pa_stream_get_latency(pa_stream *s, pa_usec_t *r_usec, int *negative);
|
||||
const pa_channel_map* pa_stream_get_channel_map(pa_stream *s);
|
||||
const pa_buffer_attr* pa_stream_get_buffer_attr(pa_stream *s);
|
||||
|
||||
typedef enum pa_stream_state {
|
||||
PA_STREAM_UNCONNECTED,
|
||||
PA_STREAM_CREATING,
|
||||
PA_STREAM_READY,
|
||||
PA_STREAM_FAILED,
|
||||
PA_STREAM_TERMINATED
|
||||
} pa_stream_state_t;
|
||||
pa_stream_state_t pa_stream_get_state(pa_stream *p);
|
||||
|
||||
typedef void(*pa_stream_request_cb_t)(pa_stream *p, size_t nbytes, void *userdata);
|
||||
void pa_stream_set_read_callback(pa_stream *p, pa_stream_request_cb_t cb, void *userdata);
|
||||
|
||||
pa_operation* pa_stream_update_timing_info(pa_stream *s, pa_stream_success_cb_t cb, void *userdata);
|
||||
944
LXST/Platforms/linux/soundcard.py
Normal file
944
LXST/Platforms/linux/soundcard.py
Normal file
@@ -0,0 +1,944 @@
|
||||
# Adapted from Bastian Bechtold's soundcard library, originally released
|
||||
# under the BSD 3-Clause License
|
||||
#
|
||||
# https://github.com/bastibe/SoundCard
|
||||
#
|
||||
# Copyright (c) 2016 Bastian Bechtold
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
# Modifications and improvements Copyright 2025 Mark Qvist, and released
|
||||
# under the same BSD 3-Clause License.
|
||||
|
||||
import os
|
||||
import atexit
|
||||
import collections.abc
|
||||
import time
|
||||
import re
|
||||
import threading
|
||||
import warnings
|
||||
import numpy
|
||||
import cffi
|
||||
|
||||
_ffi = cffi.FFI()
|
||||
_package_dir, _ = os.path.split(__file__)
|
||||
with open(os.path.join(_package_dir, "pulseaudio.h"), "rt") as f: _ffi.cdef(f.read())
|
||||
|
||||
# Try explicit file name, if the general does not work (e.g. on nixos)
|
||||
try: _pa = _ffi.dlopen("pulse")
|
||||
except OSError: _pa = _ffi.dlopen("libpulse.so")
|
||||
|
||||
# First, we need to define a global _PulseAudio proxy for interacting
|
||||
# with the C API:
|
||||
|
||||
def _lock(func):
|
||||
"""Call a pulseaudio function while holding the mainloop lock."""
|
||||
def func_with_lock(*args, **kwargs):
|
||||
self = args[0]
|
||||
with self._lock_mainloop():
|
||||
return func(*args[1:], **kwargs)
|
||||
return func_with_lock
|
||||
|
||||
|
||||
def _lock_and_block(func):
|
||||
"""Call a pulseaudio function while holding the mainloop lock, and
|
||||
block until the operation has finished.
|
||||
|
||||
Use this for pulseaudio functions that return a `pa_operation *`.
|
||||
|
||||
"""
|
||||
def func_with_lock(*args, **kwargs):
|
||||
self = args[0]
|
||||
with self._lock_mainloop():
|
||||
operation = func(*args[1:], **kwargs)
|
||||
self._block_operation(operation)
|
||||
self._pa_operation_unref(operation)
|
||||
return func_with_lock
|
||||
|
||||
|
||||
def channel_name_map():
|
||||
"""
|
||||
Return a dict containing the channel position index for every channel position name string.
|
||||
"""
|
||||
|
||||
channel_indices = {
|
||||
_ffi.string(_pa.pa_channel_position_to_string(idx)).decode("utf-8"): idx for idx in
|
||||
range(_pa.PA_CHANNEL_POSITION_MAX)
|
||||
}
|
||||
|
||||
# Append alternative names for front-left, front-right, front-center and lfe according to
|
||||
# the PulseAudio definitions.
|
||||
channel_indices.update({"left": _pa.PA_CHANNEL_POSITION_LEFT,
|
||||
"right": _pa.PA_CHANNEL_POSITION_RIGHT,
|
||||
"center": _pa.PA_CHANNEL_POSITION_CENTER,
|
||||
"subwoofer": _pa.PA_CHANNEL_POSITION_SUBWOOFER})
|
||||
|
||||
# The values returned from Pulseaudio contain 1 for "left", 2 for "right" and so on.
|
||||
# SoundCard"s channel indices for "left" start at 0. Therefore, we have to decrement all values.
|
||||
channel_indices = {key: value - 1 for (key, value) in channel_indices.items()}
|
||||
|
||||
return channel_indices
|
||||
|
||||
|
||||
class _PulseAudio:
|
||||
"""Proxy for communcation with Pulseaudio.
|
||||
|
||||
This holds the pulseaudio main loop, and a pulseaudio context.
|
||||
Together, these provide the building blocks for interacting with
|
||||
pulseaudio.
|
||||
|
||||
This can be used to query the pulseaudio server for sources,
|
||||
sinks, and server information, and provides thread-safe access to
|
||||
the main pulseaudio functions.
|
||||
|
||||
Any function that would return a `pa_operation *` in pulseaudio
|
||||
will block until the operation has finished.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# these functions are called before the mainloop starts, so we
|
||||
# don't need to hold the lock:
|
||||
self.mainloop = _pa.pa_threaded_mainloop_new()
|
||||
self.mainloop_api = _pa.pa_threaded_mainloop_get_api(self.mainloop)
|
||||
self.context = _pa.pa_context_new(self.mainloop_api, self._infer_program_name().encode())
|
||||
_pa.pa_context_connect(self.context, _ffi.NULL, _pa.PA_CONTEXT_NOFLAGS, _ffi.NULL)
|
||||
_pa.pa_threaded_mainloop_start(self.mainloop)
|
||||
|
||||
while self._pa_context_get_state(self.context) in (_pa.PA_CONTEXT_UNCONNECTED, _pa.PA_CONTEXT_CONNECTING, _pa.PA_CONTEXT_AUTHORIZING, _pa.PA_CONTEXT_SETTING_NAME):
|
||||
time.sleep(0.001)
|
||||
assert self._pa_context_get_state(self.context)==_pa.PA_CONTEXT_READY
|
||||
|
||||
@staticmethod
|
||||
def _infer_program_name():
|
||||
"""Get current progam name.
|
||||
|
||||
Will handle `./script.py`, `python path/to/script.py`,
|
||||
`python -m module.submodule` and `python -c "code(x=y)"`.
|
||||
See https://docs.python.org/3/using/cmdline.html#interface-options
|
||||
"""
|
||||
import sys
|
||||
prog_name = sys.argv[0]
|
||||
if prog_name == "-c":
|
||||
return sys.argv[1][:30] + "..."
|
||||
if prog_name == "-m":
|
||||
prog_name = sys.argv[1]
|
||||
# Usually even with -m, sys.argv[0] will already be a path,
|
||||
# so do the following outside the above check
|
||||
main_str = "/__main__.py"
|
||||
if prog_name.endswith(main_str):
|
||||
prog_name = prog_name[:-len(main_str)]
|
||||
# Not handled: sys.argv[0] == "-"
|
||||
return os.path.basename(prog_name)
|
||||
|
||||
def _shutdown(self):
|
||||
operation = self._pa_context_drain(self.context, _ffi.NULL, _ffi.NULL)
|
||||
self._block_operation(operation)
|
||||
self._pa_context_disconnect(self.context)
|
||||
self._pa_context_unref(self.context)
|
||||
# no more mainloop locking necessary from here on:
|
||||
_pa.pa_threaded_mainloop_stop(self.mainloop)
|
||||
_pa.pa_threaded_mainloop_free(self.mainloop)
|
||||
|
||||
def _block_operation(self, operation):
|
||||
"""Wait until the operation has finished."""
|
||||
if operation == _ffi.NULL:
|
||||
return
|
||||
while self._pa_operation_get_state(operation) == _pa.PA_OPERATION_RUNNING:
|
||||
time.sleep(0.001)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return application name stored in client proplist"""
|
||||
idx = self._pa_context_get_index(self.context)
|
||||
if idx < 0: # PA_INVALID_INDEX == -1
|
||||
raise RuntimeError("Could not get client index of PulseAudio context.")
|
||||
name = None
|
||||
@_ffi.callback("pa_client_info_cb_t")
|
||||
def callback(context, client_info, eol, userdata):
|
||||
nonlocal name
|
||||
if not eol:
|
||||
name = _ffi.string(client_info.name).decode("utf-8")
|
||||
self._pa_context_get_client_info(self.context, idx, callback, _ffi.NULL)
|
||||
assert name is not None
|
||||
return name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
rv = None
|
||||
@_ffi.callback("pa_context_success_cb_t")
|
||||
def callback(context, success, userdata):
|
||||
nonlocal rv
|
||||
rv = success
|
||||
self._pa_context_set_name(self.context, name.encode(), callback, _ffi.NULL)
|
||||
assert rv is not None
|
||||
if rv == 0:
|
||||
raise RuntimeError("Setting PulseAudio context name failed")
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""Return a list of dicts of information about available sources."""
|
||||
info = []
|
||||
@_ffi.callback("pa_source_info_cb_t")
|
||||
def callback(context, source_info, eol, userdata):
|
||||
if not eol:
|
||||
info.append(dict(name=_ffi.string(source_info.description).decode("utf-8"),
|
||||
id=_ffi.string(source_info.name).decode("utf-8")))
|
||||
self._pa_context_get_source_info_list(self.context, callback, _ffi.NULL)
|
||||
return info
|
||||
|
||||
def source_info(self, id):
|
||||
"""Return a dictionary of information about a specific source."""
|
||||
info = []
|
||||
@_ffi.callback("pa_source_info_cb_t")
|
||||
def callback(context, source_info, eol, userdata):
|
||||
if not eol:
|
||||
info_dict = dict(latency=source_info.latency,
|
||||
configured_latency=source_info.configured_latency,
|
||||
channels=source_info.sample_spec.channels,
|
||||
name=_ffi.string(source_info.description).decode("utf-8"))
|
||||
for prop in ["device.class", "device.api", "device.bus"]:
|
||||
data = _pa.pa_proplist_gets(source_info.proplist, prop.encode())
|
||||
info_dict[prop] = _ffi.string(data).decode("utf-8") if data else None
|
||||
info.append(info_dict)
|
||||
|
||||
self._pa_context_get_source_info_by_name(self.context, id.encode(), callback, _ffi.NULL)
|
||||
return info[0]
|
||||
|
||||
@property
|
||||
def sink_list(self):
|
||||
"""Return a list of dicts of information about available sinks."""
|
||||
info = []
|
||||
@_ffi.callback("pa_sink_info_cb_t")
|
||||
def callback(context, sink_info, eol, userdata):
|
||||
if not eol:
|
||||
info.append((dict(name=_ffi.string(sink_info.description).decode("utf-8"),
|
||||
id=_ffi.string(sink_info.name).decode("utf-8"))))
|
||||
self._pa_context_get_sink_info_list(self.context, callback, _ffi.NULL)
|
||||
return info
|
||||
|
||||
def sink_info(self, id):
|
||||
"""Return a dictionary of information about a specific sink."""
|
||||
info = []
|
||||
@_ffi.callback("pa_sink_info_cb_t")
|
||||
def callback(context, sink_info, eol, userdata):
|
||||
if not eol:
|
||||
info_dict = dict(latency=sink_info.latency,
|
||||
configured_latency=sink_info.configured_latency,
|
||||
channels=sink_info.sample_spec.channels,
|
||||
name=_ffi.string(sink_info.description).decode("utf-8"))
|
||||
for prop in ["device.class", "device.api", "device.bus"]:
|
||||
data = _pa.pa_proplist_gets(sink_info.proplist, prop.encode())
|
||||
info_dict[prop] = _ffi.string(data).decode("utf-8") if data else None
|
||||
info.append(info_dict)
|
||||
self._pa_context_get_sink_info_by_name(self.context, id.encode(), callback, _ffi.NULL)
|
||||
return info[0]
|
||||
|
||||
@property
|
||||
def server_info(self):
|
||||
"""Return a dictionary of information about the server."""
|
||||
info = {}
|
||||
@_ffi.callback("pa_server_info_cb_t")
|
||||
def callback(context, server_info, userdata):
|
||||
info["server version"] = _ffi.string(server_info.server_version).decode("utf-8")
|
||||
info["server name"] = _ffi.string(server_info.server_name).decode("utf-8")
|
||||
info["default sink id"] = _ffi.string(server_info.default_sink_name).decode("utf-8")
|
||||
info["default source id"] = _ffi.string(server_info.default_source_name).decode("utf-8")
|
||||
self._pa_context_get_server_info(self.context, callback, _ffi.NULL)
|
||||
return info
|
||||
|
||||
def _lock_mainloop(self):
|
||||
"""Context manager for locking the mainloop.
|
||||
|
||||
Hold this lock before calling any pulseaudio function while
|
||||
the mainloop is running.
|
||||
|
||||
"""
|
||||
|
||||
class Lock():
|
||||
def __enter__(self_):
|
||||
_pa.pa_threaded_mainloop_lock(self.mainloop)
|
||||
def __exit__(self_, exc_type, exc_value, traceback):
|
||||
_pa.pa_threaded_mainloop_unlock(self.mainloop)
|
||||
return Lock()
|
||||
|
||||
# create thread-safe versions of all used pulseaudio functions:
|
||||
_pa_context_get_source_info_list = _lock_and_block(_pa.pa_context_get_source_info_list)
|
||||
_pa_context_get_source_info_by_name = _lock_and_block(_pa.pa_context_get_source_info_by_name)
|
||||
_pa_context_get_sink_info_list = _lock_and_block(_pa.pa_context_get_sink_info_list)
|
||||
_pa_context_get_sink_info_by_name = _lock_and_block(_pa.pa_context_get_sink_info_by_name)
|
||||
_pa_context_get_client_info = _lock_and_block(_pa.pa_context_get_client_info)
|
||||
_pa_context_get_server_info = _lock_and_block(_pa.pa_context_get_server_info)
|
||||
_pa_context_get_index = _lock(_pa.pa_context_get_index)
|
||||
_pa_context_get_state = _lock(_pa.pa_context_get_state)
|
||||
_pa_context_set_name = _lock_and_block(_pa.pa_context_set_name)
|
||||
_pa_context_drain = _lock(_pa.pa_context_drain)
|
||||
_pa_context_disconnect = _lock(_pa.pa_context_disconnect)
|
||||
_pa_context_unref = _lock(_pa.pa_context_unref)
|
||||
_pa_context_errno = _lock(_pa.pa_context_errno)
|
||||
_pa_operation_get_state = _lock(_pa.pa_operation_get_state)
|
||||
_pa_operation_unref = _lock(_pa.pa_operation_unref)
|
||||
_pa_stream_get_state = _lock(_pa.pa_stream_get_state)
|
||||
_pa_sample_spec_valid = _lock(_pa.pa_sample_spec_valid)
|
||||
_pa_stream_new = _lock(_pa.pa_stream_new)
|
||||
_pa_stream_get_channel_map = _lock(_pa.pa_stream_get_channel_map)
|
||||
_pa_stream_drain = _lock_and_block(_pa.pa_stream_drain)
|
||||
_pa_stream_disconnect = _lock(_pa.pa_stream_disconnect)
|
||||
_pa_stream_unref = _lock(_pa.pa_stream_unref)
|
||||
_pa_stream_connect_record = _lock(_pa.pa_stream_connect_record)
|
||||
_pa_stream_readable_size = _lock(_pa.pa_stream_readable_size)
|
||||
_pa_stream_peek = _lock(_pa.pa_stream_peek)
|
||||
_pa_stream_drop = _lock(_pa.pa_stream_drop)
|
||||
_pa_stream_connect_playback = _lock(_pa.pa_stream_connect_playback)
|
||||
_pa_stream_update_timing_info = _lock_and_block(_pa.pa_stream_update_timing_info)
|
||||
_pa_stream_get_latency = _lock(_pa.pa_stream_get_latency)
|
||||
_pa_stream_writable_size = _lock(_pa.pa_stream_writable_size)
|
||||
_pa_stream_write = _lock(_pa.pa_stream_write)
|
||||
_pa_stream_set_read_callback = _pa.pa_stream_set_read_callback
|
||||
|
||||
_pulse = _PulseAudio()
|
||||
atexit.register(_pulse._shutdown)
|
||||
|
||||
def all_speakers():
|
||||
"""A list of all connected speakers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
speakers : list(_Speaker)
|
||||
|
||||
"""
|
||||
return [_Speaker(id=s["id"]) for s in _pulse.sink_list]
|
||||
|
||||
|
||||
def default_speaker():
|
||||
"""The default speaker of the system.
|
||||
|
||||
Returns
|
||||
-------
|
||||
speaker : _Speaker
|
||||
|
||||
"""
|
||||
name = _pulse.server_info["default sink id"]
|
||||
return get_speaker(name)
|
||||
|
||||
|
||||
def get_speaker(id):
|
||||
"""Get a specific speaker by a variety of means.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int or str
|
||||
can be a backend id string (Windows, Linux) or a device id int (MacOS), a substring of the
|
||||
speaker name, or a fuzzy-matched pattern for the speaker name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
speaker : _Speaker
|
||||
|
||||
"""
|
||||
speakers = _pulse.sink_list
|
||||
return _Speaker(id=_match_soundcard(id, speakers)["id"])
|
||||
|
||||
|
||||
def all_microphones(include_loopback=False, exclude_monitors=True):
|
||||
"""A list of all connected microphones.
|
||||
|
||||
By default, this does not include loopbacks (virtual microphones
|
||||
that record the output of a speaker).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
include_loopback : bool
|
||||
allow recording of speaker outputs
|
||||
exclude_monitors : bool
|
||||
deprecated version of ``include_loopback``
|
||||
|
||||
Returns
|
||||
-------
|
||||
microphones : list(_Microphone)
|
||||
|
||||
"""
|
||||
|
||||
if not exclude_monitors:
|
||||
warnings.warn("The exclude_monitors flag is being replaced by the include_loopback flag", DeprecationWarning)
|
||||
include_loopback = not exclude_monitors
|
||||
|
||||
mics = [_Microphone(id=m["id"]) for m in _pulse.source_list]
|
||||
if not include_loopback:
|
||||
return [m for m in mics if m._get_info()["device.class"] != "monitor"]
|
||||
else:
|
||||
return mics
|
||||
|
||||
|
||||
def default_microphone():
|
||||
"""The default microphone of the system.
|
||||
|
||||
Returns
|
||||
-------
|
||||
microphone : _Microphone
|
||||
"""
|
||||
name = _pulse.server_info["default source id"]
|
||||
return get_microphone(name, include_loopback=True)
|
||||
|
||||
|
||||
def get_microphone(id, include_loopback=False, exclude_monitors=True):
|
||||
"""Get a specific microphone by a variety of means.
|
||||
|
||||
By default, this does not include loopbacks (virtual microphones
|
||||
that record the output of a speaker).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id : int or str
|
||||
can be a backend id string (Windows, Linux) or a device id int (MacOS), a substring of the
|
||||
speaker name, or a fuzzy-matched pattern for the speaker name.
|
||||
include_loopback : bool
|
||||
allow recording of speaker outputs
|
||||
exclude_monitors : bool
|
||||
deprecated version of ``include_loopback``
|
||||
|
||||
Returns
|
||||
-------
|
||||
microphone : _Microphone
|
||||
"""
|
||||
|
||||
if not exclude_monitors:
|
||||
warnings.warn("The exclude_monitors flag is being replaced by the include_loopback flag", DeprecationWarning)
|
||||
include_loopback = not exclude_monitors
|
||||
|
||||
microphones = _pulse.source_list
|
||||
return _Microphone(id=_match_soundcard(id, microphones, include_loopback)["id"])
|
||||
|
||||
|
||||
def _match_soundcard(id, soundcards, include_loopback=False):
|
||||
"""Find id in a list of soundcards.
|
||||
|
||||
id can be a pulseaudio id, a substring of the microphone name, or
|
||||
a fuzzy-matched pattern for the microphone name.
|
||||
"""
|
||||
if not include_loopback:
|
||||
soundcards_by_id = {soundcard["id"]: soundcard for soundcard in soundcards
|
||||
if not "monitor" in soundcard["id"]}
|
||||
soundcards_by_name = {soundcard["name"]: soundcard for soundcard in soundcards
|
||||
if not "monitor" in soundcard["id"]}
|
||||
else:
|
||||
soundcards_by_id = {soundcard["id"]: soundcard for soundcard in soundcards}
|
||||
soundcards_by_name = {soundcard["name"]: soundcard for soundcard in soundcards}
|
||||
if id in soundcards_by_id:
|
||||
return soundcards_by_id[id]
|
||||
# try substring match:
|
||||
for name, soundcard in soundcards_by_name.items():
|
||||
if id in name:
|
||||
return soundcard
|
||||
# try fuzzy match:
|
||||
pattern = ".*".join(id)
|
||||
for name, soundcard in soundcards_by_name.items():
|
||||
if re.match(pattern, name):
|
||||
return soundcard
|
||||
raise IndexError("no soundcard with id {}".format(id))
|
||||
|
||||
|
||||
def get_name():
|
||||
"""Get application name.
|
||||
|
||||
.. note::
|
||||
Currently only works on Linux.
|
||||
|
||||
Returns
|
||||
-------
|
||||
name : str
|
||||
"""
|
||||
return _pulse.name
|
||||
|
||||
|
||||
def set_name(name):
|
||||
"""Set application name.
|
||||
|
||||
.. note::
|
||||
Currently only works on Linux.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The application using the soundcard
|
||||
will be identified by the OS using this name.
|
||||
"""
|
||||
_pulse.name = name
|
||||
|
||||
|
||||
class _SoundCard:
|
||||
def __init__(self, *, id):
|
||||
self._id = id
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
"""int or list(int): Either the number of channels, or a list of
|
||||
channel indices. Index -1 is the mono mixture of all channels,
|
||||
and subsequent numbers are channel numbers (left, right,
|
||||
center, ...)
|
||||
|
||||
"""
|
||||
return self._get_info()["channels"]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""object: A backend-dependent unique ID."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: The human-readable name of the soundcard."""
|
||||
return self._get_info()["name"]
|
||||
|
||||
def _get_info(self):
|
||||
return _pulse.source_info(self._id)
|
||||
|
||||
|
||||
class _Speaker(_SoundCard):
|
||||
"""A soundcard output. Can be used to play audio.
|
||||
|
||||
Use the :func:`play` method to play one piece of audio, or use the
|
||||
:func:`player` method to get a context manager for playing continuous
|
||||
audio.
|
||||
|
||||
Multiple calls to :func:`play` play immediately and concurrently,
|
||||
while the :func:`player` schedules multiple pieces of audio one
|
||||
after another.
|
||||
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return "<Speaker {} ({} channels)>".format(self.name, self.channels)
|
||||
|
||||
def player(self, samplerate, channels=None, blocksize=None):
|
||||
"""Create Player for playing audio.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
samplerate : int
|
||||
The desired sampling rate in Hz
|
||||
channels : {int, list(int)}, optional
|
||||
Play on these channels. For example, ``[0, 3]`` will play
|
||||
stereo data on the physical channels one and four.
|
||||
Defaults to use all available channels.
|
||||
On Linux, channel ``-1`` is the mono mix of all channels.
|
||||
On macOS, channel ``-1`` is silence.
|
||||
blocksize : int
|
||||
Will play this many samples at a time. Choose a lower
|
||||
block size for lower latency and more CPU usage.
|
||||
exclusive_mode : bool, optional
|
||||
Windows only: open sound card in exclusive mode, which
|
||||
might be necessary for short block lengths or high
|
||||
sample rates or optimal performance. Default is ``False``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
player : _Player
|
||||
"""
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
return _Player(self._id, samplerate, channels, blocksize)
|
||||
|
||||
def play(self, data, samplerate, channels=None, blocksize=None):
|
||||
"""Play some audio data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : numpy array
|
||||
The audio data to play. Must be a *frames x channels* Numpy array.
|
||||
samplerate : int
|
||||
The desired sampling rate in Hz
|
||||
channels : {int, list(int)}, optional
|
||||
Play on these channels. For example, ``[0, 3]`` will play
|
||||
stereo data on the physical channels one and four.
|
||||
Defaults to use all available channels.
|
||||
On Linux, channel ``-1`` is the mono mix of all channels.
|
||||
On macOS, channel ``-1`` is silence.
|
||||
blocksize : int
|
||||
Will play this many samples at a time. Choose a lower
|
||||
block size for lower latency and more CPU usage.
|
||||
"""
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
with _Player(self._id, samplerate, channels, blocksize) as s:
|
||||
s.play(data)
|
||||
|
||||
def _get_info(self):
|
||||
return _pulse.sink_info(self._id)
|
||||
|
||||
|
||||
class _Microphone(_SoundCard):
|
||||
"""A soundcard input. Can be used to record audio.
|
||||
|
||||
Use the :func:`record` method to record one piece of audio, or use
|
||||
the :func:`recorder` method to get a context manager for recording
|
||||
continuous audio.
|
||||
|
||||
Multiple calls to :func:`record` record immediately and
|
||||
concurrently, while the :func:`recorder` schedules multiple pieces
|
||||
of audio to be recorded one after another.
|
||||
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
if self.isloopback:
|
||||
return "<Loopback {} ({} channels)>".format(self.name, self.channels)
|
||||
else:
|
||||
return "<Microphone {} ({} channels)>".format(self.name, self.channels)
|
||||
|
||||
@property
|
||||
def isloopback(self):
|
||||
"""bool : Whether this microphone is recording a speaker."""
|
||||
return self._get_info()["device.class"] == "monitor"
|
||||
|
||||
def recorder(self, samplerate, channels=None, blocksize=None):
|
||||
"""Create Recorder for recording audio.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
samplerate : int
|
||||
The desired sampling rate in Hz
|
||||
channels : {int, list(int)}, optional
|
||||
Record on these channels. For example, ``[0, 3]`` will record
|
||||
stereo data from the physical channels one and four.
|
||||
Defaults to use all available channels.
|
||||
On Linux, channel ``-1`` is the mono mix of all channels.
|
||||
On macOS, channel ``-1`` is silence.
|
||||
blocksize : int
|
||||
Will record this many samples at a time. Choose a lower
|
||||
block size for lower latency and more CPU usage.
|
||||
exclusive_mode : bool, optional
|
||||
Windows only: open sound card in exclusive mode, which
|
||||
might be necessary for short block lengths or high
|
||||
sample rates or optimal performance. Default is ``False``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
recorder : _Recorder
|
||||
"""
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
return _Recorder(self._id, samplerate, channels, blocksize)
|
||||
|
||||
def record(self, numframes, samplerate, channels=None, blocksize=None):
|
||||
"""Record some audio data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
numframes: int
|
||||
The number of frames to record.
|
||||
samplerate : int
|
||||
The desired sampling rate in Hz
|
||||
channels : {int, list(int)}, optional
|
||||
Record on these channels. For example, ``[0, 3]`` will record
|
||||
stereo data from the physical channels one and four.
|
||||
Defaults to use all available channels.
|
||||
On Linux, channel ``-1`` is the mono mix of all channels.
|
||||
On macOS, channel ``-1`` is silence.
|
||||
blocksize : int
|
||||
Will record this many samples at a time. Choose a lower
|
||||
block size for lower latency and more CPU usage.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data : numpy array
|
||||
The recorded audio data. Will be a *frames x channels* Numpy array.
|
||||
"""
|
||||
if channels is None:
|
||||
channels = self.channels
|
||||
with _Recorder(self._id, samplerate, channels, blocksize) as r:
|
||||
return r.record(numframes)
|
||||
|
||||
|
||||
class _Stream:
|
||||
"""A context manager for an active audio stream.
|
||||
|
||||
This class is meant to be subclassed. Children must implement the
|
||||
`_connect_stream` method which takes a `pa_buffer_attr*` struct,
|
||||
and connects an appropriate stream.
|
||||
|
||||
This context manager can only be entered once, and can not be used
|
||||
after it is closed.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, id, samplerate, channels, blocksize=None, name="outputstream"):
|
||||
self._id = id
|
||||
self._samplerate = samplerate
|
||||
self._name = name
|
||||
self._blocksize = blocksize
|
||||
self.channels = channels
|
||||
|
||||
def __enter__(self):
|
||||
samplespec = _ffi.new("pa_sample_spec*")
|
||||
samplespec.format = _pa.PA_SAMPLE_FLOAT32LE
|
||||
samplespec.rate = self._samplerate
|
||||
if isinstance(self.channels, collections.abc.Iterable):
|
||||
samplespec.channels = len(self.channels)
|
||||
elif isinstance(self.channels, int):
|
||||
samplespec.channels = self.channels
|
||||
else:
|
||||
raise TypeError("channels must be iterable or integer")
|
||||
if not _pulse._pa_sample_spec_valid(samplespec):
|
||||
raise RuntimeError("invalid sample spec")
|
||||
|
||||
# pam and channelmap refer to the same object, but need different
|
||||
# names to avoid garbage collection trouble on the Python/C boundary
|
||||
pam = _ffi.new("pa_channel_map*")
|
||||
channelmap = _pa.pa_channel_map_init_extend(pam, samplespec.channels, _pa.PA_CHANNEL_MAP_DEFAULT)
|
||||
if isinstance(self.channels, collections.abc.Iterable):
|
||||
for idx, ch in enumerate(self.channels):
|
||||
if isinstance(ch, int):
|
||||
channelmap.map[idx] = ch + 1
|
||||
else:
|
||||
channel_name_to_index = channel_name_map()
|
||||
channelmap.map[idx] = channel_name_to_index[ch] + 1
|
||||
|
||||
if not _pa.pa_channel_map_valid(channelmap):
|
||||
raise RuntimeError("invalid channel map")
|
||||
|
||||
self.stream = _pulse._pa_stream_new(_pulse.context, self._name.encode(), samplespec, channelmap)
|
||||
if not self.stream:
|
||||
errno = _pulse._pa_context_errno(_pulse.context)
|
||||
raise RuntimeError("stream creation failed with error ", errno)
|
||||
bufattr = _ffi.new("pa_buffer_attr*")
|
||||
bufattr.maxlength = 2**32-1 # max buffer length
|
||||
numchannels = self.channels if isinstance(self.channels, int) else len(self.channels)
|
||||
bufattr.fragsize = self._blocksize*numchannels*4 if self._blocksize else 2**32-1 # recording block sys.getsizeof()
|
||||
bufattr.minreq = 2**32-1 # start requesting more data at this bytes
|
||||
bufattr.prebuf = 2**32-1 # start playback after this bytes are available
|
||||
bufattr.tlength = self._blocksize*numchannels*4 if self._blocksize else 2**32-1 # buffer length in bytes on server
|
||||
self._connect_stream(bufattr)
|
||||
while _pulse._pa_stream_get_state(self.stream) not in [_pa.PA_STREAM_READY, _pa.PA_STREAM_FAILED]:
|
||||
time.sleep(0.01)
|
||||
if _pulse._pa_stream_get_state(self.stream) == _pa.PA_STREAM_FAILED:
|
||||
raise RuntimeError("Stream creation failed. Stream is in status {}"
|
||||
.format(_pulse._pa_stream_get_state(self.stream)))
|
||||
channel_map = _pulse._pa_stream_get_channel_map(self.stream)
|
||||
self.channels = int(channel_map.channels)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if isinstance(self, _Player): # only playback streams need to drain
|
||||
_pulse._pa_stream_drain(self.stream, _ffi.NULL, _ffi.NULL)
|
||||
_pulse._pa_stream_disconnect(self.stream)
|
||||
while _pulse._pa_stream_get_state(self.stream) not in (_pa.PA_STREAM_TERMINATED, _pa.PA_STREAM_FAILED):
|
||||
time.sleep(0.01)
|
||||
_pulse._pa_stream_unref(self.stream)
|
||||
|
||||
@property
|
||||
def latency(self):
|
||||
"""float : Latency of the stream in seconds (only available on Linux)"""
|
||||
_pulse._pa_stream_update_timing_info(self.stream, _ffi.NULL, _ffi.NULL)
|
||||
microseconds = _ffi.new("pa_usec_t*")
|
||||
_pulse._pa_stream_get_latency(self.stream, microseconds, _ffi.NULL)
|
||||
return microseconds[0] / 1000000 # 1_000_000 (3.5 compat)
|
||||
|
||||
|
||||
class _Player(_Stream):
|
||||
"""A context manager for an active output stream.
|
||||
|
||||
Audio playback is available as soon as the context manager is
|
||||
entered. Audio data can be played using the :func:`play` method.
|
||||
Successive calls to :func:`play` will queue up the audio one piece
|
||||
after another. If no audio is queued up, this will play silence.
|
||||
|
||||
This context manager can only be entered once, and can not be used
|
||||
after it is closed.
|
||||
|
||||
"""
|
||||
|
||||
def _connect_stream(self, bufattr):
|
||||
_pulse._pa_stream_connect_playback(self.stream, self._id.encode(), bufattr, _pa.PA_STREAM_ADJUST_LATENCY,
|
||||
_ffi.NULL, _ffi.NULL)
|
||||
|
||||
def play(self, data):
|
||||
"""Play some audio data.
|
||||
|
||||
Internally, all data is handled as ``float32`` and with the
|
||||
appropriate number of channels. For maximum performance,
|
||||
provide data as a *frames × channels* float32 numpy array.
|
||||
|
||||
If single-channel or one-dimensional data is given, this data
|
||||
will be played on all available channels.
|
||||
|
||||
This function will return *before* all data has been played,
|
||||
so that additional data can be provided for gapless playback.
|
||||
The amount of buffering can be controlled through the
|
||||
blocksize of the player object.
|
||||
|
||||
If data is provided faster than it is played, later pieces
|
||||
will be queued up and played one after another.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : numpy array
|
||||
The audio data to play. Must be a *frames x channels* Numpy array.
|
||||
|
||||
"""
|
||||
|
||||
data = numpy.array(data, dtype="float32", order="C")
|
||||
if data.ndim == 1:
|
||||
data = data[:, None] # force 2d
|
||||
if data.ndim != 2:
|
||||
raise TypeError("data must be 1d or 2d, not {}d".format(data.ndim))
|
||||
if data.shape[1] == 1 and self.channels != 1:
|
||||
data = numpy.tile(data, [1, self.channels])
|
||||
if data.shape[1] != self.channels:
|
||||
raise TypeError("second dimension of data must be equal to the number of channels, not {}".format(data.shape[1]))
|
||||
while data.nbytes > 0:
|
||||
nwrite = _pulse._pa_stream_writable_size(self.stream) // (4 * self.channels) # 4 bytes per sample
|
||||
|
||||
if nwrite == 0:
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
bytes = data[:nwrite].ravel().tobytes()
|
||||
_pulse._pa_stream_write(self.stream, bytes, len(bytes), _ffi.NULL, 0, _pa.PA_SEEK_RELATIVE)
|
||||
data = data[nwrite:]
|
||||
|
||||
class _Recorder(_Stream):
|
||||
"""A context manager for an active input stream.
|
||||
|
||||
Audio recording is available as soon as the context manager is
|
||||
entered. Recorded audio data can be read using the :func:`record`
|
||||
method. If no audio data is available, :func:`record` will block until
|
||||
the requested amount of audio data has been recorded.
|
||||
|
||||
This context manager can only be entered once, and can not be used
|
||||
after it is closed.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_Recorder, self).__init__(*args, **kwargs)
|
||||
self._pending_chunk = numpy.zeros((0, ), dtype="float32")
|
||||
self._record_event = threading.Event()
|
||||
|
||||
def _connect_stream(self, bufattr):
|
||||
_pulse._pa_stream_connect_record(self.stream, self._id.encode(), bufattr, _pa.PA_STREAM_ADJUST_LATENCY)
|
||||
@_ffi.callback("pa_stream_request_cb_t")
|
||||
def read_callback(stream, nbytes, userdata):
|
||||
self._record_event.set()
|
||||
self._callback = read_callback
|
||||
_pulse._pa_stream_set_read_callback(self.stream, read_callback, _ffi.NULL)
|
||||
|
||||
def _record_chunk(self):
|
||||
'''Record one chunk of audio data, as returned by pulseaudio
|
||||
|
||||
The data will be returned as a 1D numpy array, which will be used by
|
||||
the `record` method. This function is the interface of the `_Recorder`
|
||||
object with pulseaudio
|
||||
'''
|
||||
data_ptr = _ffi.new("void**")
|
||||
nbytes_ptr = _ffi.new("size_t*")
|
||||
readable_bytes = _pulse._pa_stream_readable_size(self.stream)
|
||||
while not readable_bytes:
|
||||
if not self._record_event.wait(timeout=1):
|
||||
if _pulse._pa_stream_get_state(self.stream) == _pa.PA_STREAM_FAILED:
|
||||
raise RuntimeError("Recording failed, stream is in status FAILED")
|
||||
self._record_event.clear()
|
||||
readable_bytes = _pulse._pa_stream_readable_size(self.stream)
|
||||
data_ptr[0] = _ffi.NULL
|
||||
nbytes_ptr[0] = 0
|
||||
_pulse._pa_stream_peek(self.stream, data_ptr, nbytes_ptr)
|
||||
if data_ptr[0] != _ffi.NULL:
|
||||
buffer = _ffi.buffer(data_ptr[0], nbytes_ptr[0])
|
||||
chunk = numpy.frombuffer(buffer, dtype="float32").copy()
|
||||
if data_ptr[0] == _ffi.NULL and nbytes_ptr[0] != 0:
|
||||
chunk = numpy.zeros(nbytes_ptr[0]//4, dtype="float32")
|
||||
if nbytes_ptr[0] > 0:
|
||||
_pulse._pa_stream_drop(self.stream)
|
||||
return chunk
|
||||
|
||||
def record(self, numframes=None):
|
||||
"""Record a block of audio data.
|
||||
|
||||
The data will be returned as a *frames × channels* float32
|
||||
numpy array. This function will wait until ``numframes``
|
||||
frames have been recorded. If numframes is given, it will
|
||||
return exactly ``numframes`` frames, and buffer the rest for
|
||||
later.
|
||||
|
||||
If ``numframes`` is None, it will return whatever the audio
|
||||
backend has available right now. Use this if latency must be
|
||||
kept to a minimum, but be aware that block sizes can change at
|
||||
the whims of the audio backend.
|
||||
|
||||
If using :func:`record` with ``numframes=None`` after using
|
||||
:func:`record` with a required ``numframes``, the last
|
||||
buffered frame will be returned along with the new recorded
|
||||
block. (If you want to empty the last buffered frame instead,
|
||||
use :func:`flush`)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
numframes : int, optional
|
||||
The number of frames to record.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data : numpy array
|
||||
The recorded audio data. Will be a *frames x channels* Numpy array.
|
||||
|
||||
"""
|
||||
if numframes is None:
|
||||
return numpy.reshape(numpy.concatenate([self.flush().ravel(), self._record_chunk()]),
|
||||
[-1, self.channels])
|
||||
else:
|
||||
captured_data = [self._pending_chunk]
|
||||
captured_frames = self._pending_chunk.shape[0] / self.channels
|
||||
if captured_frames >= numframes:
|
||||
keep, self._pending_chunk = numpy.split(self._pending_chunk,
|
||||
[int(numframes * self.channels)])
|
||||
return numpy.reshape(keep, [-1, self.channels])
|
||||
else:
|
||||
while captured_frames < numframes:
|
||||
chunk = self._record_chunk()
|
||||
captured_data.append(chunk)
|
||||
captured_frames += len(chunk)/self.channels
|
||||
to_split = int(len(chunk) - (captured_frames - numframes) * self.channels)
|
||||
captured_data[-1], self._pending_chunk = numpy.split(captured_data[-1], [to_split])
|
||||
return numpy.reshape(numpy.concatenate(captured_data), [-1, self.channels])
|
||||
|
||||
def flush(self):
|
||||
"""Return the last pending chunk.
|
||||
|
||||
After using the :func:`record` method, this will return the
|
||||
last incomplete chunk and delete it.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data : numpy array
|
||||
The recorded audio data. Will be a *frames x channels* Numpy array.
|
||||
|
||||
"""
|
||||
last_chunk = numpy.reshape(self._pending_chunk, [-1, self.channels])
|
||||
self._pending_chunk = numpy.zeros((0, ), dtype="float32")
|
||||
return last_chunk
|
||||
0
LXST/Platforms/windows/__init__.py
Normal file
0
LXST/Platforms/windows/__init__.py
Normal file
256
LXST/Platforms/windows/mediafoundation.h
Normal file
256
LXST/Platforms/windows/mediafoundation.h
Normal file
@@ -0,0 +1,256 @@
|
||||
// see um/winnt.h:
|
||||
typedef long HRESULT;
|
||||
typedef wchar_t *LPWSTR;
|
||||
typedef long long LONGLONG;
|
||||
|
||||
// originally, struct=interface, see um/combaseapi.h
|
||||
|
||||
// see shared/rpcndr.h:
|
||||
typedef unsigned char byte;
|
||||
|
||||
// see shared/guiddef.h:
|
||||
typedef struct {
|
||||
unsigned long Data1;
|
||||
unsigned short Data2;
|
||||
unsigned short Data3;
|
||||
byte Data4[ 8 ];
|
||||
} GUID;
|
||||
typedef GUID IID;
|
||||
typedef IID *LPIID;
|
||||
|
||||
// see um/mmdeviceapi.h:
|
||||
typedef struct IMMDeviceEnumerator IMMDeviceEnumerator;
|
||||
typedef struct IMMDeviceCollection IMMDeviceCollection;
|
||||
typedef struct IMMDevice IMMDevice;
|
||||
typedef struct IMMNotificationClient IMMNotificationClient;
|
||||
|
||||
// see um/mfidl.h:
|
||||
typedef struct IMFMediaSink IMFMediaSink;
|
||||
|
||||
// see um/mfobjects.h:
|
||||
typedef struct IMFAttributes IMFAttributes;
|
||||
|
||||
// see um/Unknwn.h:
|
||||
typedef struct IUnknown IUnknown;
|
||||
typedef IUnknown *LPUNKNOWN;
|
||||
|
||||
// see shared/wtypes.h:
|
||||
typedef unsigned long DWORD;
|
||||
typedef const char *LPCSTR;
|
||||
|
||||
// see shared/WTypesbase.h:
|
||||
typedef void *LPVOID;
|
||||
typedef LPCSTR LPCOLESTR;
|
||||
typedef IID *REFIID;
|
||||
|
||||
// see um/combaseapi.h:
|
||||
HRESULT CoCreateInstance(const GUID* rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, const GUID* riid, LPVOID * ppv);
|
||||
HRESULT IIDFromString(LPCOLESTR lpsz, LPIID lpiid);
|
||||
HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit);
|
||||
void CoTaskMemFree(LPVOID pv);
|
||||
LPVOID CoTaskMemAlloc(size_t cb);
|
||||
void CoUninitialize(void);
|
||||
|
||||
// see um/mmdeviceapi.h:
|
||||
typedef enum EDataFlow {eRender, eCapture, eAll, EDataFlow_enum_count} EDataFlow;
|
||||
|
||||
typedef enum ERole {eConsole, eMultimedia, eCommunications, ERole_enum_count} ERole;
|
||||
|
||||
typedef struct IMMDeviceEnumeratorVtbl
|
||||
{
|
||||
HRESULT ( __stdcall *QueryInterface )(IMMDeviceEnumerator * This, const GUID *riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IMMDeviceEnumerator * This);
|
||||
ULONG ( __stdcall *Release )(IMMDeviceEnumerator * This);
|
||||
HRESULT ( __stdcall *EnumAudioEndpoints )(IMMDeviceEnumerator * This, EDataFlow dataFlow, DWORD dwStateMask, IMMDeviceCollection **ppDevices);
|
||||
HRESULT ( __stdcall *GetDefaultAudioEndpoint )(IMMDeviceEnumerator * This, EDataFlow dataFlow, ERole role, IMMDevice **ppEndpoint);
|
||||
HRESULT ( __stdcall *GetDevice )(IMMDeviceEnumerator * This, LPCWSTR pwstrId, IMMDevice **ppDevice);
|
||||
/* I hope I won't need these
|
||||
HRESULT ( __stdcall *RegisterEndpointNotificationCallback )(IMMDeviceEnumerator * This, IMMNotificationClient *pClient);
|
||||
HRESULT ( __stdcall *UnregisterEndpointNotificationCallback )(IMMDeviceEnumerator * This, IMMNotificationClient *pClient);
|
||||
*/
|
||||
} IMMDeviceEnumeratorVtbl;
|
||||
|
||||
struct IMMDeviceEnumerator
|
||||
{
|
||||
const struct IMMDeviceEnumeratorVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
typedef struct IMMDeviceCollectionVtbl
|
||||
{
|
||||
HRESULT ( __stdcall *QueryInterface )(IMMDeviceCollection * This, REFIID riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IMMDeviceCollection * This);
|
||||
ULONG ( __stdcall *Release )(IMMDeviceCollection * This);
|
||||
HRESULT ( __stdcall *GetCount )(IMMDeviceCollection * This, UINT *pcDevices);
|
||||
HRESULT ( __stdcall *Item )(IMMDeviceCollection * This, UINT nDevice, IMMDevice **ppDevice);
|
||||
} IMMDeviceCollectionVtbl;
|
||||
|
||||
struct IMMDeviceCollection
|
||||
{
|
||||
const struct IMMDeviceCollectionVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
// um/propsys.h
|
||||
typedef struct IPropertyStore IPropertyStore;
|
||||
// um/combaseapi.h
|
||||
typedef struct tag_inner_PROPVARIANT PROPVARIANT;
|
||||
// shared/wtypes.h
|
||||
typedef unsigned short VARTYPE;
|
||||
// um/propidl.h
|
||||
struct tag_inner_PROPVARIANT {
|
||||
VARTYPE vt;
|
||||
WORD wReserved1;
|
||||
WORD wReserved2;
|
||||
WORD wReserved3;
|
||||
void * data;
|
||||
};
|
||||
void PropVariantInit(PROPVARIANT *p);
|
||||
HRESULT PropVariantClear(PROPVARIANT *p);
|
||||
|
||||
typedef struct IMMDeviceVtbl {
|
||||
HRESULT ( __stdcall *QueryInterface )(IMMDevice * This, REFIID riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IMMDevice * This);
|
||||
ULONG ( __stdcall *Release )(IMMDevice * This);
|
||||
HRESULT ( __stdcall *Activate )(IMMDevice * This, REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface);
|
||||
HRESULT ( __stdcall *OpenPropertyStore )(IMMDevice * This, DWORD stgmAccess, IPropertyStore **ppProperties);
|
||||
HRESULT ( __stdcall *GetId )(IMMDevice * This, LPWSTR *ppstrId);
|
||||
HRESULT ( __stdcall *GetState )(IMMDevice * This, DWORD *pdwState);
|
||||
} IMMDeviceVtbl;
|
||||
|
||||
struct IMMDevice {
|
||||
const struct IMMDeviceVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
// um/propkeydef.h
|
||||
typedef struct {
|
||||
GUID fmtid;
|
||||
DWORD pid;
|
||||
} PROPERTYKEY;
|
||||
|
||||
const PROPERTYKEY PKEY_Device_FriendlyName = {{0xa45c254e, 0xdf1c, 0x4efd, {0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0}}, 14};
|
||||
const PROPERTYKEY PKEY_AudioEngine_DeviceFormat = {{0xf19f064d, 0x82c, 0x4e27, {0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c}}, 0};
|
||||
|
||||
typedef struct IPropertyStoreVtbl {
|
||||
HRESULT ( __stdcall *QueryInterface )(IPropertyStore * This, REFIID riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IPropertyStore * This);
|
||||
ULONG ( __stdcall *Release )(IPropertyStore * This);
|
||||
HRESULT ( __stdcall *GetCount )(IPropertyStore * This, DWORD *cProps);
|
||||
HRESULT ( __stdcall *GetAt )(IPropertyStore * This, DWORD iProp, PROPERTYKEY *pkey);
|
||||
HRESULT ( __stdcall *GetValue )(IPropertyStore * This, const PROPERTYKEY *key, PROPVARIANT *pv);
|
||||
HRESULT ( __stdcall *SetValue )(IPropertyStore * This, const PROPERTYKEY *key, const PROPVARIANT *propvar);
|
||||
HRESULT ( __stdcall *Commit )(IPropertyStore * This);
|
||||
} IPropertyStoreVtbl;
|
||||
|
||||
struct IPropertyStore {
|
||||
const struct IPropertyStoreVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
// shared/WTypesbase.h
|
||||
typedef struct tagBLOB {
|
||||
ULONG cbSize;
|
||||
BYTE *pBlobData;
|
||||
} BLOB;
|
||||
|
||||
|
||||
typedef struct tag_inner_BLOB_PROPVARIANT BLOB_PROPVARIANT;
|
||||
struct tag_inner_BLOB_PROPVARIANT {
|
||||
VARTYPE vt;
|
||||
WORD wReserved1;
|
||||
WORD wReserved2;
|
||||
WORD wReserved3;
|
||||
BLOB blob;
|
||||
};
|
||||
|
||||
typedef struct WAVEFORMATEX {
|
||||
WORD wFormatTag; /* format type */
|
||||
WORD nChannels; /* number of channels (i.e. mono, stereo...) */
|
||||
DWORD nSamplesPerSec; /* sample rate */
|
||||
DWORD nAvgBytesPerSec; /* for buffer estimation */
|
||||
WORD nBlockAlign; /* block size of data */
|
||||
WORD wBitsPerSample; /* Number of bits per sample of mono data */
|
||||
WORD cbSize; /* The count in bytes of the size of
|
||||
extra information (after cbSize) */
|
||||
} WAVEFORMATEX;
|
||||
|
||||
typedef struct {
|
||||
WAVEFORMATEX Format;
|
||||
union {
|
||||
WORD wValidBitsPerSample; /* bits of precision */
|
||||
WORD wSamplesPerBlock; /* valid if wBitsPerSample==0 */
|
||||
WORD wReserved; /* If neither applies, set to zero. */
|
||||
} Samples;
|
||||
DWORD dwChannelMask; /* which channels are */
|
||||
/* present in stream */
|
||||
GUID SubFormat;
|
||||
} WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE;
|
||||
|
||||
// um/AudioSessionTypes.h
|
||||
typedef enum _AUDCLNT_SHAREMODE
|
||||
{
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
AUDCLNT_SHAREMODE_EXCLUSIVE
|
||||
} AUDCLNT_SHAREMODE;
|
||||
|
||||
// um/dsound.h
|
||||
typedef const GUID *LPCGUID;
|
||||
|
||||
// um/Audioclient.h
|
||||
typedef LONGLONG REFERENCE_TIME;
|
||||
|
||||
typedef struct IAudioClient IAudioClient;
|
||||
|
||||
typedef struct IAudioClientVtbl {
|
||||
HRESULT ( __stdcall *QueryInterface )(IAudioClient * This, REFIID riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IAudioClient * This);
|
||||
ULONG ( __stdcall *Release )(IAudioClient * This);
|
||||
HRESULT ( __stdcall *Initialize )(IAudioClient * This, AUDCLNT_SHAREMODE ShareMode, DWORD StreamFlags, REFERENCE_TIME hnsBufferDuration, REFERENCE_TIME hnsPeriodicity, const WAVEFORMATEXTENSIBLE *pFormat, LPCGUID AudioSessionGuid);
|
||||
HRESULT ( __stdcall *GetBufferSize )(IAudioClient * This, UINT32 *pNumBufferFrames);
|
||||
HRESULT ( __stdcall *GetStreamLatency )(IAudioClient * This, REFERENCE_TIME *phnsLatency);
|
||||
HRESULT ( __stdcall *GetCurrentPadding )(IAudioClient * This, UINT32 *pNumPaddingFrames);
|
||||
HRESULT ( __stdcall *IsFormatSupported )(IAudioClient * This, AUDCLNT_SHAREMODE ShareMode, const WAVEFORMATEXTENSIBLE *pFormat, WAVEFORMATEXTENSIBLE **ppClosestMatch);
|
||||
HRESULT ( __stdcall *GetMixFormat )(IAudioClient * This, WAVEFORMATEXTENSIBLE **ppDeviceFormat);
|
||||
HRESULT ( __stdcall *GetDevicePeriod )(IAudioClient * This, REFERENCE_TIME *phnsDefaultDevicePeriod, REFERENCE_TIME *phnsMinimumDevicePeriod);
|
||||
HRESULT ( __stdcall *Start )(IAudioClient * This);
|
||||
HRESULT ( __stdcall *Stop )(IAudioClient * This);
|
||||
HRESULT ( __stdcall *Reset )(IAudioClient * This);
|
||||
HRESULT ( __stdcall *SetEventHandle )(IAudioClient * This, HANDLE eventHandle);
|
||||
HRESULT ( __stdcall *GetService )(IAudioClient * This, REFIID riid, void **ppv);
|
||||
} IAudioClientVtbl;
|
||||
|
||||
struct IAudioClient {
|
||||
const struct IAudioClientVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
typedef struct IAudioRenderClient IAudioRenderClient;
|
||||
|
||||
typedef struct IAudioRenderClientVtbl {
|
||||
HRESULT ( __stdcall *QueryInterface )(IAudioRenderClient * This, REFIID riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IAudioRenderClient * This);
|
||||
ULONG ( __stdcall *Release )(IAudioRenderClient * This);
|
||||
HRESULT ( __stdcall *GetBuffer )(IAudioRenderClient * This, UINT32 NumFramesRequested, BYTE **ppData);
|
||||
HRESULT ( __stdcall *ReleaseBuffer )(IAudioRenderClient * This, UINT32 NumFramesWritten, DWORD dwFlags);
|
||||
} IAudioRenderClientVtbl;
|
||||
|
||||
struct IAudioRenderClient {
|
||||
const struct IAudioRenderClientVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
typedef enum _AUDCLNT_BUFFERFLAGS {
|
||||
AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY = 0x1,
|
||||
AUDCLNT_BUFFERFLAGS_SILENT = 0x2,
|
||||
AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR = 0x4
|
||||
};
|
||||
|
||||
typedef struct IAudioCaptureClient IAudioCaptureClient;
|
||||
|
||||
typedef struct IAudioCaptureClientVtbl {
|
||||
HRESULT ( __stdcall *QueryInterface )(IAudioCaptureClient * This, REFIID riid, void **ppvObject);
|
||||
ULONG ( __stdcall *AddRef )(IAudioCaptureClient * This);
|
||||
ULONG ( __stdcall *Release )(IAudioCaptureClient * This);
|
||||
HRESULT ( __stdcall *GetBuffer )(IAudioCaptureClient * This, BYTE **ppData, UINT32 *pNumFramesToRead, DWORD *pdwFlags, UINT64 *pu64DevicePosition, UINT64 *pu64QPCPosition);
|
||||
HRESULT ( __stdcall *ReleaseBuffer )(IAudioCaptureClient * This, UINT32 NumFramesRead);
|
||||
HRESULT ( __stdcall *GetNextPacketSize )(IAudioCaptureClient * This, UINT32 *pNumFramesInNextPacket);
|
||||
} IAudioCaptureClientVtbl;
|
||||
|
||||
struct IAudioCaptureClient {
|
||||
const struct IAudioCaptureClientVtbl *lpVtbl;
|
||||
};
|
||||
641
LXST/Platforms/windows/soundcard.py
Normal file
641
LXST/Platforms/windows/soundcard.py
Normal file
@@ -0,0 +1,641 @@
|
||||
# Adapted from Bastian Bechtold's soundcard library, originally released
|
||||
# under the BSD 3-Clause License
|
||||
#
|
||||
# https://github.com/bastibe/SoundCard
|
||||
#
|
||||
# Copyright (c) 2016 Bastian Bechtold
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
# Modifications and improvements Copyright 2025 Mark Qvist, and released
|
||||
# under the same BSD 3-Clause License.
|
||||
|
||||
import os
|
||||
import cffi
|
||||
import re
|
||||
import time
|
||||
import struct
|
||||
import collections
|
||||
import platform
|
||||
import warnings
|
||||
import threading
|
||||
import numpy
|
||||
import RNS
|
||||
|
||||
_ffi = cffi.FFI()
|
||||
_package_dir, _ = os.path.split(__file__)
|
||||
with open(os.path.join(_package_dir, 'mediafoundation.h'), 'rt') as f: _ffi.cdef(f.read())
|
||||
try: _ole32 = _ffi.dlopen('ole32')
|
||||
except OSError:
|
||||
try: _ole32 = _ffi.dlopen('ole32.dll')
|
||||
except: raise SystemError("LXST Could not load OLE32 DLL for WASAPI integration")
|
||||
|
||||
def tid(): return threading.get_native_id()
|
||||
com_thread_ids = []
|
||||
class _COMLibrary:
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self.init_com()
|
||||
|
||||
def init_com(self):
|
||||
with self._lock:
|
||||
if tid() in com_thread_ids: return
|
||||
else:
|
||||
com_thread_ids.append(tid())
|
||||
COINIT_MULTITHREADED = 0x0
|
||||
RNS.log(f"COM init from thread {tid()}", RNS.LOG_EXTREME)
|
||||
if platform.win32_ver()[0] == "8": raise OSError("Unsupported Windows version")
|
||||
else: hr = _ole32.CoInitializeEx(_ffi.NULL, COINIT_MULTITHREADED)
|
||||
|
||||
try:
|
||||
self.check_error(hr)
|
||||
self.com_loaded = True
|
||||
except RuntimeError as e:
|
||||
# Error 0x80010106 - COM already initialized
|
||||
RPC_E_CHANGED_MODE = 0x80010106
|
||||
if hr + 2 ** 32 == RPC_E_CHANGED_MODE: self.com_loaded = False
|
||||
else: raise e
|
||||
|
||||
def release_com(self):
|
||||
with self._lock:
|
||||
if tid() in com_thread_ids:
|
||||
com_thread_ids.remove(tid())
|
||||
RNS.log(f"COM release from thread {tid()}", RNS.LOG_EXTREME)
|
||||
if _ole32 != None: _ole32.CoUninitialize()
|
||||
else: RNS.log(f"OLE32 instance was None at de-init for thread {tid()}", RNS.LOG_DEBUG)
|
||||
|
||||
def __del__(self): self.release_com()
|
||||
|
||||
@staticmethod
|
||||
def check_error(hresult):
|
||||
S_OK = 0
|
||||
E_NOINTERFACE = 0x80004002
|
||||
E_POINTER = 0x80004003
|
||||
E_OUTOFMEMORY = 0x8007000e
|
||||
E_INVALIDARG = 0x80070057
|
||||
CO_E_NOTINITIALIZED = 0x800401f0
|
||||
AUDCLNT_E_UNSUPPORTED_FORMAT = 0x88890008
|
||||
if hresult == S_OK: return
|
||||
elif hresult+2**32 == E_NOINTERFACE: raise RuntimeError("The specified class does not implement the requested interface, or the controlling IUnknown does not expose the requested interface.")
|
||||
elif hresult+2**32 == E_POINTER: raise RuntimeError("An argument is NULL")
|
||||
elif hresult+2**32 == E_INVALIDARG: raise RuntimeError("Invalid argument")
|
||||
elif hresult+2**32 == E_OUTOFMEMORY: raise RuntimeError("Out of memory")
|
||||
elif hresult+2**32 == AUDCLNT_E_UNSUPPORTED_FORMAT: raise RuntimeError("Unsupported format")
|
||||
elif hresult+2**32 == CO_E_NOTINITIALIZED: raise RuntimeError(f"Windows COM context not initialized in {tid()}")
|
||||
else: raise RuntimeError("Error {}".format(hex(hresult+2**32)))
|
||||
|
||||
@staticmethod
|
||||
def release(ppObject):
|
||||
if ppObject[0] != _ffi.NULL:
|
||||
ppObject[0][0].lpVtbl.Release(ppObject[0])
|
||||
ppObject[0] = _ffi.NULL
|
||||
|
||||
_com = _COMLibrary()
|
||||
|
||||
def all_speakers():
|
||||
with _DeviceEnumerator() as enum:
|
||||
return [_Speaker(dev) for dev in enum.all_devices('speaker')]
|
||||
|
||||
def default_speaker():
|
||||
with _DeviceEnumerator() as enum:
|
||||
return _Speaker(enum.default_device('speaker'))
|
||||
|
||||
def get_speaker(id):
|
||||
return _match_device(id, all_speakers())
|
||||
|
||||
def all_microphones(include_loopback=False):
|
||||
with _DeviceEnumerator() as enum:
|
||||
if include_loopback:
|
||||
return [_Microphone(dev, isloopback=True) for dev in enum.all_devices('speaker')] + [_Microphone(dev) for dev in enum.all_devices('microphone')]
|
||||
else:
|
||||
return [_Microphone(dev) for dev in enum.all_devices('microphone')]
|
||||
|
||||
def default_microphone():
|
||||
with _DeviceEnumerator() as enum:
|
||||
return _Microphone(enum.default_device('microphone'))
|
||||
|
||||
def get_microphone(id, include_loopback=False):
|
||||
return _match_device(id, all_microphones(include_loopback))
|
||||
|
||||
def _match_device(id, devices):
|
||||
devices_by_id = {device.id: device for device in devices}
|
||||
devices_by_name = {device.name: device for device in devices}
|
||||
if id in devices_by_id: return devices_by_id[id]
|
||||
|
||||
# Try substring match:
|
||||
for name, device in devices_by_name.items():
|
||||
if id in name: return device
|
||||
|
||||
# Try fuzzy match:
|
||||
pattern = '.*'.join(id)
|
||||
for name, device in devices_by_name.items():
|
||||
if re.match(pattern, name): return device
|
||||
|
||||
raise IndexError('No device with id {}'.format(id))
|
||||
|
||||
def _str2wstr(string):
|
||||
return _ffi.new('int16_t[]', [ord(s) for s in string]+[0])
|
||||
|
||||
def _guidof(uuid_str):
|
||||
IID = _ffi.new('LPIID')
|
||||
uuid = _str2wstr(uuid_str)
|
||||
hr = _ole32.IIDFromString(_ffi.cast("char*", uuid), IID)
|
||||
_com.check_error(hr)
|
||||
return IID
|
||||
|
||||
def get_name(): raise NotImplementedError()
|
||||
def set_name(name): raise NotImplementedError()
|
||||
|
||||
class _DeviceEnumerator:
|
||||
# See shared/WTypesbase.h and um/combaseapi.h:
|
||||
def __init__(self):
|
||||
_com.init_com()
|
||||
self._ptr = _ffi.new('IMMDeviceEnumerator **')
|
||||
IID_MMDeviceEnumerator = _guidof("{BCDE0395-E52F-467C-8E3D-C4579291692E}")
|
||||
IID_IMMDeviceEnumerator = _guidof("{A95664D2-9614-4F35-A746-DE8DB63617E6}")
|
||||
CLSCTX_ALL = 23
|
||||
hr = _ole32.CoCreateInstance(IID_MMDeviceEnumerator, _ffi.NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, _ffi.cast("void **", self._ptr))
|
||||
_com.check_error(hr)
|
||||
|
||||
def __enter__(self):
|
||||
_com.init_com()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): _com.release(self._ptr)
|
||||
def __del__(self): _com.release(self._ptr)
|
||||
|
||||
def _device_id(self, device_ptr):
|
||||
ppId = _ffi.new('LPWSTR *')
|
||||
hr = device_ptr[0][0].lpVtbl.GetId(device_ptr[0], ppId)
|
||||
_com.check_error(hr)
|
||||
return _ffi.string(ppId[0])
|
||||
|
||||
def all_devices(self, kind):
|
||||
if kind == 'speaker': data_flow = 0 # render
|
||||
elif kind == 'microphone': data_flow = 1 # capture
|
||||
else: raise TypeError('Invalid kind: {}'.format(kind))
|
||||
|
||||
DEVICE_STATE_ACTIVE = 0x1
|
||||
ppDevices = _ffi.new('IMMDeviceCollection **')
|
||||
hr = self._ptr[0][0].lpVtbl.EnumAudioEndpoints(self._ptr[0], data_flow, DEVICE_STATE_ACTIVE, ppDevices);
|
||||
_com.check_error(hr)
|
||||
|
||||
for ppDevice in _DeviceCollection(ppDevices):
|
||||
device = _Device(self._device_id(ppDevice))
|
||||
_com.release(ppDevice)
|
||||
yield device
|
||||
|
||||
def default_device(self, kind):
|
||||
if kind == 'speaker': data_flow = 0 # render
|
||||
elif kind == 'microphone': data_flow = 1 # capture
|
||||
else: raise TypeError('Invalid kind: {}'.format(kind))
|
||||
|
||||
ppDevice = _ffi.new('IMMDevice **')
|
||||
eConsole = 0
|
||||
hr = self._ptr[0][0].lpVtbl.GetDefaultAudioEndpoint(self._ptr[0], data_flow, eConsole, ppDevice);
|
||||
_com.check_error(hr)
|
||||
device = _Device(self._device_id(ppDevice))
|
||||
_com.release(ppDevice)
|
||||
return device
|
||||
|
||||
def device_ptr(self, devid):
|
||||
ppDevice = _ffi.new('IMMDevice **')
|
||||
devid = _str2wstr(devid)
|
||||
hr = self._ptr[0][0].lpVtbl.GetDevice(self._ptr[0], _ffi.cast('wchar_t *', devid), ppDevice);
|
||||
_com.check_error(hr)
|
||||
return ppDevice
|
||||
|
||||
class _DeviceCollection:
|
||||
def __init__(self, ptr):
|
||||
_com.init_com()
|
||||
self._ptr = ptr
|
||||
|
||||
def __del__(self): _com.release(self._ptr)
|
||||
|
||||
def __len__(self):
|
||||
pCount = _ffi.new('UINT *')
|
||||
hr = self._ptr[0][0].lpVtbl.GetCount(self._ptr[0], pCount)
|
||||
_com.check_error(hr)
|
||||
return pCount[0]
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if idx >= len(self):
|
||||
raise StopIteration()
|
||||
ppDevice = _ffi.new('IMMDevice **')
|
||||
hr = self._ptr[0][0].lpVtbl.Item(self._ptr[0], idx, ppDevice)
|
||||
_com.check_error(hr)
|
||||
return ppDevice
|
||||
|
||||
class _PropVariant:
|
||||
def __init__(self):
|
||||
_com.init_com()
|
||||
self.ptr = _ole32.CoTaskMemAlloc(_ffi.sizeof('PROPVARIANT'))
|
||||
self.ptr = _ffi.cast("PROPVARIANT *", self.ptr)
|
||||
|
||||
def __del__(self):
|
||||
hr = _ole32.PropVariantClear(self.ptr)
|
||||
_com.check_error(hr)
|
||||
|
||||
class _Device:
|
||||
def __init__(self, id):
|
||||
_com.init_com()
|
||||
self._id = id
|
||||
|
||||
def _device_ptr(self):
|
||||
with _DeviceEnumerator() as enum:
|
||||
return enum.device_ptr(self._id)
|
||||
|
||||
@property
|
||||
def id(self): return self._id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# um/coml2api.h:
|
||||
ppPropertyStore = _ffi.new('IPropertyStore **')
|
||||
ptr = self._device_ptr()
|
||||
hr = ptr[0][0].lpVtbl.OpenPropertyStore(ptr[0], 0, ppPropertyStore)
|
||||
_com.release(ptr)
|
||||
_com.check_error(hr)
|
||||
propvariant = _PropVariant()
|
||||
# um/functiondiscoverykeys_devpkey.h and https://msdn.microsoft.com/en-us/library/windows/desktop/dd370812(v=vs.85).aspx
|
||||
PKEY_Device_FriendlyName = _ffi.new("PROPERTYKEY *",
|
||||
[[0xa45c254e, 0xdf1c, 0x4efd, [0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0]],
|
||||
14])
|
||||
hr = ppPropertyStore[0][0].lpVtbl.GetValue(ppPropertyStore[0], PKEY_Device_FriendlyName, propvariant.ptr)
|
||||
_com.check_error(hr)
|
||||
if propvariant.ptr[0].vt != 31:
|
||||
raise RuntimeError('Property was expected to be a string, but is not a string')
|
||||
data = _ffi.cast("short*", propvariant.ptr[0].data)
|
||||
for idx in range(256):
|
||||
if data[idx] == 0: break
|
||||
devicename = struct.pack('h' * idx, *data[0:idx]).decode('utf-16')
|
||||
_com.release(ppPropertyStore)
|
||||
return devicename
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
# um/coml2api.h:
|
||||
ppPropertyStore = _ffi.new('IPropertyStore **')
|
||||
ptr = self._device_ptr()
|
||||
hr = ptr[0][0].lpVtbl.OpenPropertyStore(ptr[0], 0, ppPropertyStore)
|
||||
_com.release(ptr)
|
||||
_com.check_error(hr)
|
||||
propvariant = _PropVariant()
|
||||
# um/functiondiscoverykeys_devpkey.h and https://msdn.microsoft.com/en-us/library/windows/desktop/dd370812(v=vs.85).aspx
|
||||
PKEY_AudioEngine_DeviceFormat = _ffi.new("PROPERTYKEY *",
|
||||
[[0xf19f064d, 0x82c, 0x4e27, [0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c]],
|
||||
0])
|
||||
hr = ppPropertyStore[0][0].lpVtbl.GetValue(ppPropertyStore[0], PKEY_AudioEngine_DeviceFormat, propvariant.ptr)
|
||||
_com.release(ppPropertyStore)
|
||||
_com.check_error(hr)
|
||||
if propvariant.ptr[0].vt != 65:
|
||||
raise RuntimeError('Property was expected to be a blob, but is not a blob')
|
||||
pPropVariantBlob = _ffi.cast("BLOB_PROPVARIANT *", propvariant.ptr)
|
||||
assert pPropVariantBlob[0].blob.cbSize == 40
|
||||
waveformat = _ffi.cast("WAVEFORMATEX *", pPropVariantBlob[0].blob.pBlobData)
|
||||
channels = waveformat[0].nChannels
|
||||
return channels
|
||||
|
||||
def _audio_client(self):
|
||||
CLSCTX_ALL = 23
|
||||
ppAudioClient = _ffi.new("IAudioClient **")
|
||||
IID_IAudioClient = _guidof("{1CB9AD4C-DBFA-4C32-B178-C2F568A703B2}")
|
||||
ptr = self._device_ptr()
|
||||
hr = ptr[0][0].lpVtbl.Activate(ptr[0], IID_IAudioClient, CLSCTX_ALL, _ffi.NULL, _ffi.cast("void**", ppAudioClient))
|
||||
_com.release(ptr)
|
||||
_com.check_error(hr)
|
||||
return ppAudioClient
|
||||
|
||||
class _Speaker(_Device):
|
||||
def __init__(self, device): self._id = device._id
|
||||
|
||||
def __repr__(self): return '<Speaker {} ({} channels)>'.format(self.name,self.channels)
|
||||
|
||||
def player(self, samplerate, channels=None, blocksize=None, exclusive_mode=False):
|
||||
if channels is None: channels = self.channels
|
||||
return _Player(self._audio_client(), samplerate, channels, blocksize, False, exclusive_mode)
|
||||
|
||||
def play(self, data, samplerate, channels=None, blocksize=None):
|
||||
with self.player(samplerate, channels, blocksize) as p: p.play(data)
|
||||
|
||||
|
||||
class _Microphone(_Device):
|
||||
def __init__(self, device, isloopback=False):
|
||||
self._id = device._id
|
||||
self.isloopback = isloopback
|
||||
|
||||
def __repr__(self):
|
||||
if self.isloopback: return '<Loopback {} ({} channels)>'.format(self.name,self.channels)
|
||||
else: return '<Microphone {} ({} channels)>'.format(self.name,self.channels)
|
||||
|
||||
def recorder(self, samplerate, channels=None, blocksize=None, exclusive_mode=False):
|
||||
if channels is None: channels = self.channels
|
||||
return _Recorder(self._audio_client(), samplerate, channels, blocksize, self.isloopback, exclusive_mode)
|
||||
|
||||
def record(self, numframes, samplerate, channels=None, blocksize=None):
|
||||
with self.recorder(samplerate, channels, blocksize) as r: return r.record(numframes)
|
||||
|
||||
class _AudioClient:
|
||||
def __init__(self, ptr, samplerate, channels, blocksize, isloopback, exclusive_mode=False):
|
||||
self._ptr = ptr
|
||||
|
||||
if isinstance(channels, int): self.channelmap = list(range(channels))
|
||||
elif isinstance(channels, collections.abc.Iterable): self.channelmap = channels
|
||||
else: raise TypeError('Channels must be iterable or integer')
|
||||
|
||||
if list(range(len(set(self.channelmap)))) != sorted(list(set(self.channelmap))):
|
||||
raise TypeError('Due to limitations of WASAPI, channel maps on Windows must be a combination of `range(0, x)`.')
|
||||
|
||||
if blocksize is None: blocksize = self.deviceperiod[0]*samplerate
|
||||
|
||||
ppMixFormat = _ffi.new('WAVEFORMATEXTENSIBLE**') # See: https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible
|
||||
hr = self._ptr[0][0].lpVtbl.GetMixFormat(self._ptr[0], ppMixFormat)
|
||||
_com.check_error(hr)
|
||||
|
||||
# It's a WAVEFORMATEXTENSIBLE with room for KSDATAFORMAT_SUBTYPE_IEEE_FLOAT:
|
||||
# Note: Some devices may not return 0xFFFE format, but WASAPI should handle conversion
|
||||
if ppMixFormat[0][0].Format.wFormatTag == 0xFFFE:
|
||||
assert ppMixFormat[0][0].Format.cbSize == 22
|
||||
|
||||
# The data format is float32:
|
||||
# These values were found empirically, and I don't know why they work.
|
||||
# The program crashes if these values are different
|
||||
assert ppMixFormat[0][0].SubFormat.Data1 == 0x100000
|
||||
assert ppMixFormat[0][0].SubFormat.Data2 == 0x0080
|
||||
assert ppMixFormat[0][0].SubFormat.Data3 == 0xaa00
|
||||
assert [int(x) for x in ppMixFormat[0][0].SubFormat.Data4[0:4]] == [0, 56, 155, 113]
|
||||
# the last four bytes seem to vary randomly
|
||||
else:
|
||||
# Device doesn't return WAVEFORMATEXTENSIBLE, but WASAPI will handle conversion
|
||||
# Just skip the assertions and let WASAPI convert
|
||||
pass
|
||||
|
||||
channels = len(set(self.channelmap))
|
||||
channelmask = 0
|
||||
for ch in self.channelmap: channelmask |= 1<<ch
|
||||
ppMixFormat[0][0].Format.nChannels=channels
|
||||
ppMixFormat[0][0].Format.nSamplesPerSec=int(samplerate)
|
||||
ppMixFormat[0][0].Format.nAvgBytesPerSec=int(samplerate) * channels * 4
|
||||
ppMixFormat[0][0].Format.nBlockAlign=channels * 4
|
||||
ppMixFormat[0][0].Format.wBitsPerSample=32
|
||||
ppMixFormat[0][0].Samples=dict(wValidBitsPerSample=32)
|
||||
# does not work:
|
||||
# ppMixFormat[0][0].dwChannelMask=channelmask
|
||||
|
||||
# See: https://docs.microsoft.com/en-us/windows/win32/coreaudio/exclusive-mode-streams
|
||||
# nopersist, see: https://docs.microsoft.com/en-us/windows/win32/coreaudio/audclnt-streamflags-xxx-constants
|
||||
streamflags = 0x00080000
|
||||
if exclusive_mode:
|
||||
sharemode = _ole32.AUDCLNT_SHAREMODE_EXCLUSIVE
|
||||
periodicity = 0 # 0 uses default, must set value if using AUDCLNT_STREAMFLAGS_EVENTCALLBACK (0x00040000)
|
||||
if isloopback: raise RuntimeError("Loopback mode and exclusive mode are incompatible.")
|
||||
else:
|
||||
sharemode = _ole32.AUDCLNT_SHAREMODE_SHARED
|
||||
# resample | remix | better-SRC
|
||||
# rateadjust | autoconvPCM | SRC default quality
|
||||
streamflags |= 0x00100000 | 0x80000000 | 0x08000000 # These flags are only relevant/permitted for shared mode
|
||||
periodicity = 0 # Always 0 for shared mode
|
||||
if isloopback: streamflags |= 0x00020000 # Loopback only allowed for shared mode
|
||||
|
||||
bufferduration = int(blocksize/samplerate * 10000000) # in hecto-nanoseconds (1000_000_0)
|
||||
hr = self._ptr[0][0].lpVtbl.Initialize(self._ptr[0], sharemode, streamflags, bufferduration, periodicity, ppMixFormat[0], _ffi.NULL)
|
||||
_com.check_error(hr)
|
||||
_ole32.CoTaskMemFree(ppMixFormat[0])
|
||||
|
||||
# save samplerate for later
|
||||
self.samplerate = samplerate
|
||||
# placeholder for the last time we had audio input available
|
||||
self._idle_start_time = None
|
||||
|
||||
|
||||
@property
|
||||
def buffersize(self):
|
||||
pBufferSize = _ffi.new("UINT32*")
|
||||
hr = self._ptr[0][0].lpVtbl.GetBufferSize(self._ptr[0], pBufferSize)
|
||||
_com.check_error(hr)
|
||||
return pBufferSize[0]
|
||||
|
||||
@property
|
||||
def deviceperiod(self):
|
||||
pDefaultPeriod = _ffi.new("REFERENCE_TIME*")
|
||||
pMinimumPeriod = _ffi.new("REFERENCE_TIME*")
|
||||
hr = self._ptr[0][0].lpVtbl.GetDevicePeriod(self._ptr[0], pDefaultPeriod, pMinimumPeriod)
|
||||
_com.check_error(hr)
|
||||
return pDefaultPeriod[0]/10_000_000, pMinimumPeriod[0]/10_000_000
|
||||
|
||||
@property
|
||||
def currentpadding(self):
|
||||
pPadding = _ffi.new("UINT32*")
|
||||
hr = self._ptr[0][0].lpVtbl.GetCurrentPadding(self._ptr[0], pPadding)
|
||||
_com.check_error(hr)
|
||||
return pPadding[0]
|
||||
|
||||
class _Player(_AudioClient):
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd316756(v=vs.85).aspx
|
||||
def _render_client(self):
|
||||
iid = _guidof("{F294ACFC-3146-4483-A7BF-ADDCA7C260E2}")
|
||||
ppRenderClient = _ffi.new("IAudioRenderClient**")
|
||||
hr = self._ptr[0][0].lpVtbl.GetService(self._ptr[0], iid, _ffi.cast("void**", ppRenderClient))
|
||||
_com.check_error(hr)
|
||||
return ppRenderClient
|
||||
|
||||
def _render_buffer(self, numframes):
|
||||
data = _ffi.new("BYTE**")
|
||||
hr = self._ppRenderClient[0][0].lpVtbl.GetBuffer(self._ppRenderClient[0], numframes, data)
|
||||
_com.check_error(hr)
|
||||
return data
|
||||
|
||||
def _render_release(self, numframes):
|
||||
hr = self._ppRenderClient[0][0].lpVtbl.ReleaseBuffer(self._ppRenderClient[0], numframes, 0)
|
||||
_com.check_error(hr)
|
||||
|
||||
def _render_available_frames(self):
|
||||
return self.buffersize-self.currentpadding
|
||||
|
||||
def __enter__(self):
|
||||
_com.init_com()
|
||||
self._ppRenderClient = self._render_client()
|
||||
hr = self._ptr[0][0].lpVtbl.Start(self._ptr[0])
|
||||
_com.check_error(hr)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
hr = self._ptr[0][0].lpVtbl.Stop(self._ptr[0])
|
||||
_com.check_error(hr)
|
||||
_com.release(self._ppRenderClient)
|
||||
_com.release(self._ptr)
|
||||
_com.release_com()
|
||||
|
||||
def play(self, data):
|
||||
data = numpy.array(data, dtype='float32', order='C')
|
||||
if data.ndim == 1: data = data[:, None] # force 2d
|
||||
if data.ndim != 2: raise TypeError('Data must be 1d or 2d, not {}d'.format(data.ndim))
|
||||
if data.shape[1] == 1 and len(set(self.channelmap)) != 1: data = numpy.tile(data, [1, len(set(self.channelmap))])
|
||||
|
||||
# Internally, channel numbers are always ascending:
|
||||
sortidx = sorted(range(len(self.channelmap)), key=lambda k: self.channelmap[k])
|
||||
data = data[:, sortidx]
|
||||
|
||||
if data.shape[1] != len(set(self.channelmap)):
|
||||
raise TypeError('second dimension of data must be equal to the number of channels, not {}'.format(data.shape[1]))
|
||||
|
||||
while data.nbytes > 0:
|
||||
towrite = self._render_available_frames()
|
||||
if towrite == 0:
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
|
||||
bytes = data[:towrite].ravel().tobytes()
|
||||
buffer = self._render_buffer(towrite)
|
||||
_ffi.memmove(buffer[0], bytes, len(bytes))
|
||||
self._render_release(towrite)
|
||||
data = data[towrite:]
|
||||
|
||||
class _Recorder(_AudioClient):
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd370800(v=vs.85).aspx
|
||||
def _capture_client(self):
|
||||
iid = _guidof("{C8ADBD64-E71E-48a0-A4DE-185C395CD317}")
|
||||
ppCaptureClient = _ffi.new("IAudioCaptureClient**")
|
||||
hr = self._ptr[0][0].lpVtbl.GetService(self._ptr[0], iid, _ffi.cast("void**", ppCaptureClient))
|
||||
_com.check_error(hr)
|
||||
return ppCaptureClient
|
||||
|
||||
def _capture_buffer(self):
|
||||
data = _ffi.new("BYTE**")
|
||||
toread = _ffi.new('UINT32*')
|
||||
flags = _ffi.new('DWORD*')
|
||||
hr = self._ppCaptureClient[0][0].lpVtbl.GetBuffer(self._ppCaptureClient[0], data, toread, flags, _ffi.NULL, _ffi.NULL)
|
||||
_com.check_error(hr)
|
||||
return data[0], toread[0], flags[0]
|
||||
|
||||
def _capture_release(self, numframes):
|
||||
hr = self._ppCaptureClient[0][0].lpVtbl.ReleaseBuffer(self._ppCaptureClient[0], numframes)
|
||||
_com.check_error(hr)
|
||||
|
||||
def _capture_available_frames(self):
|
||||
pSize = _ffi.new("UINT32*")
|
||||
hr = self._ppCaptureClient[0][0].lpVtbl.GetNextPacketSize(self._ppCaptureClient[0], pSize)
|
||||
_com.check_error(hr)
|
||||
return pSize[0]
|
||||
|
||||
def __enter__(self):
|
||||
_com.init_com()
|
||||
self._ppCaptureClient = self._capture_client()
|
||||
hr = self._ptr[0][0].lpVtbl.Start(self._ptr[0])
|
||||
_com.check_error(hr)
|
||||
self._pending_chunk = numpy.zeros([0], dtype='float32')
|
||||
self._is_first_frame = True
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
hr = self._ptr[0][0].lpVtbl.Stop(self._ptr[0])
|
||||
_com.check_error(hr)
|
||||
_com.release(self._ppCaptureClient)
|
||||
_com.release(self._ptr)
|
||||
_com.release_com()
|
||||
|
||||
def _record_chunk(self):
|
||||
while self._capture_available_frames() == 0:
|
||||
# Some sound cards indicate silence by not making any
|
||||
# frames available. If that is the case, we need to
|
||||
# estimate the number of zeros to return, by measuring the
|
||||
# silent time:
|
||||
if self._idle_start_time is None: self._idle_start_time = time.perf_counter_ns()
|
||||
|
||||
default_block_length, minimum_block_length = self.deviceperiod
|
||||
time.sleep(minimum_block_length/4)
|
||||
elapsed_time_ns = time.perf_counter_ns() - self._idle_start_time
|
||||
|
||||
# Waiting times shorter than a block length or so are
|
||||
# normal, and not indicative of a silent sound card. If
|
||||
# the waiting times get longer however, we must assume
|
||||
# that there is no audio data forthcoming, and return
|
||||
# zeros instead:
|
||||
if elapsed_time_ns / 1_000_000_000 > default_block_length * 4:
|
||||
num_frames = int(self.samplerate * elapsed_time_ns / 1_000_000_000)
|
||||
num_channels = len(set(self.channelmap))
|
||||
self._idle_start_time += elapsed_time_ns
|
||||
return numpy.zeros([num_frames * num_channels], dtype='float32')
|
||||
|
||||
self._idle_start_time = None
|
||||
data_ptr, nframes, flags = self._capture_buffer()
|
||||
if data_ptr != _ffi.NULL:
|
||||
# Convert the raw CFFI buffer into a standard bytes object to ensure compatibility
|
||||
# with modern NumPy versions (fromstring binary mode was removed). Using frombuffer
|
||||
# on bytes plus .copy() guarantees a writable float32 array for downstream processing.
|
||||
buf = bytes(_ffi.buffer(data_ptr, nframes * 4 * len(set(self.channelmap))))
|
||||
chunk = numpy.frombuffer(buf, dtype=numpy.float32).copy()
|
||||
else: raise RuntimeError('Could not create capture buffer')
|
||||
|
||||
# See https://learn.microsoft.com/en-us/windows/win32/api/audioclient/ne-audioclient-_audclnt_bufferflags
|
||||
if flags & _ole32.AUDCLNT_BUFFERFLAGS_SILENT: chunk[:] = 0
|
||||
if self._is_first_frame:
|
||||
# On first run, clear data discontinuity error, as it will always be set:
|
||||
flags &= ~_ole32.AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY
|
||||
self._is_first_frame = False
|
||||
if flags & _ole32.AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY: pass
|
||||
|
||||
# Ignore _ole32.AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR, since we don't use time stamps.
|
||||
if nframes > 0:
|
||||
self._capture_release(nframes)
|
||||
return chunk
|
||||
else:
|
||||
return numpy.zeros([0], dtype='float32')
|
||||
|
||||
def record(self, numframes=None):
|
||||
if numframes is None:
|
||||
recorded_data = [self._pending_chunk, self._record_chunk()]
|
||||
self._pending_chunk = numpy.zeros([0], dtype='float32')
|
||||
|
||||
else:
|
||||
recorded_frames = len(self._pending_chunk)
|
||||
recorded_data = [self._pending_chunk]
|
||||
self._pending_chunk = numpy.zeros([0], dtype='float32')
|
||||
required_frames = numframes*len(set(self.channelmap))
|
||||
|
||||
while recorded_frames < required_frames:
|
||||
chunk = self._record_chunk()
|
||||
if len(chunk) == 0:
|
||||
# No data forthcoming: return zeros
|
||||
chunk = numpy.zeros(required_frames-recorded_frames, dtype='float32')
|
||||
recorded_data.append(chunk)
|
||||
recorded_frames += len(chunk)
|
||||
|
||||
if recorded_frames > required_frames:
|
||||
to_split = -int(recorded_frames-required_frames)
|
||||
recorded_data[-1], self._pending_chunk = numpy.split(recorded_data[-1], [to_split])
|
||||
|
||||
data = numpy.reshape(numpy.concatenate(recorded_data), [-1, len(set(self.channelmap))])
|
||||
return data[:, self.channelmap]
|
||||
|
||||
def flush(self):
|
||||
last_chunk = numpy.reshape(self._pending_chunk, [-1, len(set(self.channelmap))])
|
||||
self._pending_chunk = numpy.zeros([0], dtype='float32')
|
||||
return last_chunk
|
||||
@@ -124,10 +124,16 @@ class Telephone(SignallingReceiver):
|
||||
ALLOW_NONE = 0xFE
|
||||
|
||||
@staticmethod
|
||||
def available_outputs(): return LXST.Sources.Backend().soundcard.all_speakers()
|
||||
def available_outputs(): return LXST.Sinks.Backend().all_speakers()
|
||||
|
||||
@staticmethod
|
||||
def available_inputs(): return LXST.Sinks.Backend().soundcard.all_microphones()
|
||||
def available_inputs(): return LXST.Sources.Backend().all_microphones()
|
||||
|
||||
@staticmethod
|
||||
def default_output(): return LXST.Sinks.Backend().default_speaker()
|
||||
|
||||
@staticmethod
|
||||
def default_input(): return LXST.Sources.Backend().default_microphone()
|
||||
|
||||
def __init__(self, identity, ring_time=RING_TIME, wait_time=WAIT_TIME, auto_answer=None, allowed=ALLOW_ALL, receive_gain=0.0, transmit_gain=0.0):
|
||||
super().__init__()
|
||||
@@ -163,6 +169,7 @@ class Telephone(SignallingReceiver):
|
||||
self.dial_tone = None
|
||||
self.dial_tone_frequency = self.DIAL_TONE_FREQUENCY
|
||||
self.dial_tone_ease_ms = self.DIAL_TONE_EASE_MS
|
||||
self.busy_tone_seconds = 4.25
|
||||
self.transmit_codec = None
|
||||
self.receive_codec = None
|
||||
self.receive_mixer = None
|
||||
@@ -245,6 +252,9 @@ class Telephone(SignallingReceiver):
|
||||
self.ringtone_gain = gain
|
||||
RNS.log(f"{self} ringtone set to {self.ringtone_path}", RNS.LOG_DEBUG)
|
||||
|
||||
def set_busy_tone_time(self, seconds=4.25):
|
||||
self.busy_tone_seconds = seconds
|
||||
|
||||
def enable_agc(self, enable=True):
|
||||
if enable == True: self.use_agc = True
|
||||
else: self.use_agc = False
|
||||
@@ -521,10 +531,11 @@ class Telephone(SignallingReceiver):
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
|
||||
def __play_busy_tone(self):
|
||||
if self.busy_tone_seconds > 0:
|
||||
if self.audio_output == None or self.receive_mixer == None or self.dial_tone == None: self.__reset_dialling_pipelines()
|
||||
with self.pipeline_lock:
|
||||
window = 0.5; started = time.time()
|
||||
while time.time()-started < 4.25:
|
||||
while time.time()-started < self.busy_tone_seconds:
|
||||
elapsed = (time.time()-started)%window
|
||||
if elapsed > 0.25: self.__enable_dial_tone()
|
||||
else: self.__mute_dial_tone()
|
||||
|
||||
@@ -8,7 +8,7 @@ class LinuxBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
||||
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
|
||||
import soundcard
|
||||
from .Platforms.linux import soundcard
|
||||
self.samplerate = samplerate
|
||||
self.soundcard = soundcard
|
||||
if preferred_device:
|
||||
@@ -17,7 +17,10 @@ class LinuxBackend():
|
||||
else: self.device = soundcard.default_speaker()
|
||||
RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_speakers(self): return self.soundcard.all_speakers()
|
||||
def default_speaker(self): return self.soundcard.default_speaker()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_player(self, samples_per_frame=None, low_latency=None):
|
||||
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame)
|
||||
@@ -37,7 +40,10 @@ class AndroidBackend():
|
||||
else: self.device = soundcard.default_speaker()
|
||||
RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_speakers(self): return self.soundcard.all_speakers()
|
||||
def default_speaker(self): return self.soundcard.default_speaker()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_player(self, samples_per_frame=None, low_latency=None):
|
||||
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame, low_latency=low_latency)
|
||||
@@ -48,7 +54,7 @@ class DarwinBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
||||
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
|
||||
import soundcard
|
||||
from .Platforms.darwin import soundcard
|
||||
self.samplerate = samplerate
|
||||
self.soundcard = soundcard
|
||||
if preferred_device:
|
||||
@@ -57,7 +63,10 @@ class DarwinBackend():
|
||||
else: self.device = soundcard.default_speaker()
|
||||
RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_speakers(self): return self.soundcard.all_speakers()
|
||||
def default_speaker(self): return self.soundcard.default_speaker()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_player(self, samples_per_frame=None, low_latency=None):
|
||||
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame)
|
||||
@@ -68,10 +77,7 @@ class WindowsBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
||||
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
|
||||
import soundcard
|
||||
from pythoncom import CoInitializeEx, CoUninitialize
|
||||
self.com_init = CoInitializeEx
|
||||
self.com_release = CoUninitialize
|
||||
from .Platforms.windows import soundcard
|
||||
self.samplerate = samplerate
|
||||
self.soundcard = soundcard
|
||||
if preferred_device:
|
||||
@@ -80,13 +86,15 @@ class WindowsBackend():
|
||||
else: self.device = soundcard.default_speaker()
|
||||
RNS.log(f"Using output device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_speakers(self): return self.soundcard.all_speakers()
|
||||
def default_speaker(self): return self.soundcard.default_speaker()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_player(self, samples_per_frame=None, low_latency=None):
|
||||
self.com_init(0)
|
||||
return self.device.player(samplerate=self.samplerate, blocksize=samples_per_frame)
|
||||
|
||||
def release_player(self): self.com_release()
|
||||
def release_player(self): pass
|
||||
|
||||
def get_backend():
|
||||
if RNS.vendor.platformutils.is_linux(): return LinuxBackend
|
||||
|
||||
@@ -13,7 +13,7 @@ class LinuxBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
||||
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
|
||||
import soundcard
|
||||
from .Platforms.linux import soundcard
|
||||
self.samplerate = samplerate
|
||||
self.soundcard = soundcard
|
||||
if preferred_device:
|
||||
@@ -24,7 +24,10 @@ class LinuxBackend():
|
||||
self.bitdepth = 32
|
||||
RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_microphones(self): return self.soundcard.all_microphones()
|
||||
def default_microphone(self): return self.soundcard.default_microphone()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_recorder(self, samples_per_frame):
|
||||
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
|
||||
@@ -46,7 +49,10 @@ class AndroidBackend():
|
||||
self.bitdepth = 32
|
||||
RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_microphones(self): return self.soundcard.all_microphones()
|
||||
def default_microphone(self): return self.soundcard.default_microphone()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_recorder(self, samples_per_frame):
|
||||
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
|
||||
@@ -57,7 +63,7 @@ class DarwinBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
||||
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
|
||||
import soundcard
|
||||
from .Platforms.darwin import soundcard
|
||||
self.samplerate = samplerate
|
||||
self.soundcard = soundcard
|
||||
if preferred_device:
|
||||
@@ -68,7 +74,10 @@ class DarwinBackend():
|
||||
self.bitdepth = 32
|
||||
RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_microphones(self): return self.soundcard.all_microphones()
|
||||
def default_microphone(self): return self.soundcard.default_microphone()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_recorder(self, samples_per_frame):
|
||||
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
|
||||
@@ -79,10 +88,7 @@ class WindowsBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
||||
def __init__(self, preferred_device=None, samplerate=SAMPLERATE):
|
||||
import soundcard
|
||||
from pythoncom import CoInitializeEx, CoUninitialize
|
||||
self.com_init = CoInitializeEx
|
||||
self.com_release = CoUninitialize
|
||||
from .Platforms.windows import soundcard
|
||||
self.samplerate = samplerate
|
||||
self.soundcard = soundcard
|
||||
if preferred_device:
|
||||
@@ -93,13 +99,15 @@ class WindowsBackend():
|
||||
self.bitdepth = 32
|
||||
RNS.log(f"Using input device {self.device}", RNS.LOG_DEBUG)
|
||||
|
||||
def flush(self): self.recorder.flush()
|
||||
def all_microphones(self): return self.soundcard.all_microphones()
|
||||
def default_microphone(self): return self.soundcard.default_microphone()
|
||||
|
||||
def flush(self): self.device.flush()
|
||||
|
||||
def get_recorder(self, samples_per_frame):
|
||||
self.com_init(0)
|
||||
return self.device.recorder(samplerate=self.SAMPLERATE, blocksize=samples_per_frame)
|
||||
|
||||
def release_recorder(self): self.com_release()
|
||||
def release_recorder(self): pass
|
||||
|
||||
def get_backend():
|
||||
if RNS.vendor.platformutils.is_linux(): return LinuxBackend
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.4.2"
|
||||
__version__ = "0.4.3"
|
||||
|
||||
10
Makefile
10
Makefile
@@ -24,7 +24,14 @@ build_wheel:
|
||||
python3 setup.py sdist bdist_wheel
|
||||
rm ./skip_extensions
|
||||
-@(rm ./LXST/*.so)
|
||||
-@(rm ./LXST/*.pyd)
|
||||
-@(rm ./LXST/*.dll)
|
||||
|
||||
windll:
|
||||
cl /LD LXST/Filters.c LXST/Filters.def /Fefilterlib.dll
|
||||
mv ./filterlib.dll ./lib/dev/
|
||||
rm ./filterlib.exp
|
||||
rm ./filterlib.lib
|
||||
rm ./filterlib.obj
|
||||
|
||||
native_libs:
|
||||
./march_build.sh
|
||||
@@ -32,7 +39,6 @@ native_libs:
|
||||
persist_libs:
|
||||
-cp ./lib/dev/*.so ./lib/static/
|
||||
-cp ./lib/dev/*.dll ./lib/static/
|
||||
-cp ./lib/dev/*.dylib ./lib/static/
|
||||
|
||||
release: remove_symlinks build_wheel create_symlinks
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ LXST uses encryption provided by [Reticulum](https://reticulum.network), and thu
|
||||
|
||||
This software is in a very early alpha state, and will change rapidly with ongoing development. Consider no APIs stable. Consider everything explosive. Not all features are implemented. Nothing is documented. For a fully functional LXST program, take a look at [Sideband](https://github.com/markqvist/Sideband) or the included `rnphone` program, which provides telephony service over Reticulum. Everything else will currently be a voyage of your own making.
|
||||
|
||||
While under early development, the project is kept under a `CC BY-NC-ND 4.0` license.
|
||||
While under early development, and unless otherwise noted, the project is kept under a `CC BY-NC-ND 4.0` license.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
BIN
lib/static/filterlib.dll
Normal file
BIN
lib/static/filterlib.dll
Normal file
Binary file not shown.
10
setup.py
10
setup.py
@@ -6,6 +6,7 @@ import platform
|
||||
|
||||
if os.path.isfile("./skip_extensions"): BUILD_EXTENSIONS = False
|
||||
else: BUILD_EXTENSIONS = True
|
||||
if os.name == "nt": BUILD_EXTENSIONS = False
|
||||
|
||||
if BUILD_EXTENSIONS: print(f"Building LXST with native extensions...")
|
||||
else: print(f"Building LXST without native extensions...")
|
||||
@@ -14,7 +15,6 @@ with open("README.md", "r") as fh: long_description = fh.read()
|
||||
exec(open("LXST/_version.py", "r").read())
|
||||
|
||||
c_sources = ["LXST/Filters.c"]
|
||||
if os.name == "nt": c_sources.append("LXST/Platforms/windows.c")
|
||||
|
||||
if BUILD_EXTENSIONS: extensions = [ Extension("LXST.filterlib", sources=c_sources, include_dirs=["LXST"], language="c"), ]
|
||||
else: extensions = []
|
||||
@@ -35,7 +35,10 @@ package_data = {
|
||||
"Filters.h",
|
||||
"Filters.c",
|
||||
"filterlib*.so",
|
||||
"filterlib*.pyd",
|
||||
"filterlib*.dll",
|
||||
"Platforms/linux/pulseaudio.h",
|
||||
"Platforms/darwin/coreaudio.h",
|
||||
"Platforms/windows/mediafoundation.h",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -64,10 +67,9 @@ setuptools.setup(
|
||||
},
|
||||
install_requires=["rns>=1.0.4",
|
||||
"lxmf>=0.9.3",
|
||||
"soundcard>=0.4.5",
|
||||
"numpy>=2.3.4",
|
||||
"pycodec2>=4.1.0",
|
||||
"audioop-lts>=0.2.1;python_version>='3.13'",
|
||||
"cffi>=1.17.1"],
|
||||
"cffi>=2.0.0"],
|
||||
python_requires=">=3.11",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user