Sync upstream

This commit is contained in:
Mark Qvist
2025-11-25 21:20:05 +01:00
parent dd0cced5b0
commit 3fd611ecea
20 changed files with 3610 additions and 75 deletions

4
LXST/Filters.def Normal file
View File

@@ -0,0 +1,4 @@
EXPORTS
highpass_filter
lowpass_filter
agc_process

View File

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

View File

@@ -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
@@ -81,28 +111,28 @@ class _AndroidAudio:
# Populate device type descriptions from JNI
self.device_type_descriptions = {
adi.TYPE_AUX_LINE: "Aux Line", # 0x13 - API level 23
adi.TYPE_BLUETOOTH_A2DP: "Bluetooth A2DP", # 0x08 - API level 23
adi.TYPE_BLUETOOTH_SCO: "Bluetooth SCO", # 0x07 - API level 23
adi.TYPE_AUX_LINE: "Aux Line", # 0x13 - API level 23
adi.TYPE_BLUETOOTH_A2DP: "Bluetooth A2DP", # 0x08 - API level 23
adi.TYPE_BLUETOOTH_SCO: "Bluetooth SCO", # 0x07 - API level 23
adi.TYPE_BUILTIN_EARPIECE: "Internal Earpiece", # 0x01 - API level 23
adi.TYPE_BUILTIN_MIC: "Internal Microphone", # 0x0f - API level 23
adi.TYPE_BUILTIN_SPEAKER: "Internal Speaker", # 0x02 - API level 23
adi.TYPE_DOCK: "Dock", # 0x0d - API level 23
adi.TYPE_FM: "FM", # 0x0e - API level 23
adi.TYPE_FM_TUNER: "FM Tuner", # 0x10 - API level 23
adi.TYPE_HDMI: "HDMI", # 0x09 - API level 23
adi.TYPE_HDMI_ARC: "HDMI ARC", # 0x0a - API level 23
adi.TYPE_IP: "IP", # 0x14 - API level 23
adi.TYPE_LINE_ANALOG: "Analog Line", # 0x05 - API level 23
adi.TYPE_LINE_DIGITAL: "Digital Line", # 0x06 - API level 23
adi.TYPE_TELEPHONY: "Telephony", # 0x12 - API level 23
adi.TYPE_TV_TUNER: "TV Tuner", # 0x11 - API level 23
adi.TYPE_UNKNOWN: "Unknown", # 0x00 - API level 23
adi.TYPE_USB_ACCESSORY: "USB Accessory", # 0x0c - API level 23
adi.TYPE_USB_DEVICE: "USB Device", # 0x0b - API level 23
adi.TYPE_WIRED_HEADPHONES: "Wired Headphones", # 0x04 - API level 23
adi.TYPE_WIRED_HEADSET: "Wired Headset", # 0x03 - API level 23
adi.TYPE_BUS: "Bus", # 0x15 - API level 24
adi.TYPE_BUILTIN_MIC: "Internal Microphone", # 0x0f - API level 23
adi.TYPE_BUILTIN_SPEAKER: "Internal Speaker", # 0x02 - API level 23
adi.TYPE_DOCK: "Dock", # 0x0d - API level 23
adi.TYPE_FM: "FM", # 0x0e - API level 23
adi.TYPE_FM_TUNER: "FM Tuner", # 0x10 - API level 23
adi.TYPE_HDMI: "HDMI", # 0x09 - API level 23
adi.TYPE_HDMI_ARC: "HDMI ARC", # 0x0a - API level 23
adi.TYPE_IP: "IP", # 0x14 - API level 23
adi.TYPE_LINE_ANALOG: "Analog Line", # 0x05 - API level 23
adi.TYPE_LINE_DIGITAL: "Digital Line", # 0x06 - API level 23
adi.TYPE_TELEPHONY: "Telephony", # 0x12 - API level 23
adi.TYPE_TV_TUNER: "TV Tuner", # 0x11 - API level 23
adi.TYPE_UNKNOWN: "Unknown", # 0x00 - API level 23
adi.TYPE_USB_ACCESSORY: "USB Accessory", # 0x0c - API level 23
adi.TYPE_USB_DEVICE: "USB Device", # 0x0b - API level 23
adi.TYPE_WIRED_HEADPHONES: "Wired Headphones", # 0x04 - API level 23
adi.TYPE_WIRED_HEADSET: "Wired Headset", # 0x03 - API level 23
adi.TYPE_BUS: "Bus", # 0x15 - API level 24
}
if self.android_api_version >= 26:
@@ -115,9 +145,9 @@ class _AndroidAudio:
self.device_type_descriptions[adi.TYPE_BUILTIN_SPEAKER_SAFE] = "Ringer Speaker" # 0x18 - API level 30
if self.android_api_version >= 31:
self.device_type_descriptions[adi.TYPE_BLE_HEADSET] = "BLE Headset" # 0x1a - API level 31
self.device_type_descriptions[adi.TYPE_BLE_SPEAKER] = "BLE Speaker" # 0x1b - API level 31
self.device_type_descriptions[adi.TYPE_HDMI_EARC] = "HDMI EARC" # 0x1d - API level 31
self.device_type_descriptions[adi.TYPE_BLE_HEADSET] = "BLE Headset" # 0x1a - API level 31
self.device_type_descriptions[adi.TYPE_BLE_SPEAKER] = "BLE Speaker" # 0x1b - API level 31
self.device_type_descriptions[adi.TYPE_HDMI_EARC] = "HDMI EARC" # 0x1d - API level 31
self.device_type_descriptions[adi.TYPE_REMOTE_SUBMIX] = "Remote Submix" # 0x19 - API level 31
if self.android_api_version >= 33:

View File

View 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);

View 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

View File

View 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);

View 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

View File

View 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;
};

View 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

View File

@@ -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,15 +531,16 @@ class Telephone(SignallingReceiver):
threading.Thread(target=job, daemon=True).start()
def __play_busy_tone(self):
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:
elapsed = (time.time()-started)%window
if elapsed > 0.25: self.__enable_dial_tone()
else: self.__mute_dial_tone()
time.sleep(0.005)
time.sleep(0.5)
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 < self.busy_tone_seconds:
elapsed = (time.time()-started)%window
if elapsed > 0.25: self.__enable_dial_tone()
else: self.__mute_dial_tone()
time.sleep(0.005)
time.sleep(0.5)
def __activate_dial_tone(self):
def job():

View File

@@ -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,25 +77,24 @@ 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
self.samplerate = samplerate
self.soundcard = soundcard
from .Platforms.windows import soundcard
self.samplerate = samplerate
self.soundcard = soundcard
if preferred_device:
try: self.device = self.soundcard.get_speaker(preferred_device)
except: self.device = soundcard.default_speaker()
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

View File

@@ -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,12 +88,9 @@ 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
self.samplerate = samplerate
self.soundcard = soundcard
from .Platforms.windows import soundcard
self.samplerate = samplerate
self.soundcard = soundcard
if preferred_device:
try: self.device = self.soundcard.get_microphone(preferred_device)
except: self.device = self.soundcard.default_microphone()
@@ -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

View File

@@ -1 +1 @@
__version__ = "0.4.2"
__version__ = "0.4.3"

View File

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

View File

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

Binary file not shown.

View File

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