mirror of
https://github.com/larsimmisch/pyalsaaudio.git
synced 2026-06-01 19:07:02 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3d11582e0 | |||
| 2a2fa8f742 |
@@ -46,14 +46,9 @@ First, get the sources and change to the source directory:
|
|||||||
$ cd pyalsaaudio
|
$ cd pyalsaaudio
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, build:
|
Then, build and install:
|
||||||
```
|
```
|
||||||
$ python setup.py build
|
$ pip install .
|
||||||
```
|
|
||||||
|
|
||||||
And install:
|
|
||||||
```
|
|
||||||
$ sudo python setup.py install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Using the API
|
# Using the API
|
||||||
|
|||||||
+8
-18
@@ -1,28 +1,18 @@
|
|||||||
# Make a new release
|
# Make a new release
|
||||||
|
|
||||||
Update the version in setup.py
|
Create and push a tag naming the version (i.e. 0.11.1):
|
||||||
|
|
||||||
pyalsa_version = '0.9.0'
|
git tag 0.11.1
|
||||||
|
git push origin 0.11.1
|
||||||
|
|
||||||
Commit and push the update.
|
This should trigger a build via a github actions and publish pre-built binaries to pypi.org
|
||||||
|
|
||||||
Create and push a tag naming the version (i.e. 0.9.0):
|
|
||||||
|
|
||||||
git tag 0.9.0
|
|
||||||
git push origin 0.9.0
|
|
||||||
|
|
||||||
Create the package:
|
|
||||||
|
|
||||||
python3 setup.py sdist
|
|
||||||
|
|
||||||
Upload the package
|
|
||||||
|
|
||||||
twine upload dist/*
|
|
||||||
|
|
||||||
Don't forget to update the documentation.
|
|
||||||
|
|
||||||
# Publish the documentation
|
# Publish the documentation
|
||||||
|
|
||||||
|
All commits to main should trigger a rebuild of the documentation.
|
||||||
|
|
||||||
|
## Historical background
|
||||||
|
|
||||||
The documentation is published through the `gh-pages` branch.
|
The documentation is published through the `gh-pages` branch.
|
||||||
|
|
||||||
To publish the documentation, you need to clone the `gh-pages` branch of this repository into
|
To publish the documentation, you need to clone the `gh-pages` branch of this repository into
|
||||||
|
|||||||
+1
-5
@@ -86,11 +86,7 @@ ship with ALSA kernels.
|
|||||||
|
|
||||||
To install, execute the following: --- ::
|
To install, execute the following: --- ::
|
||||||
|
|
||||||
$ python setup.py build
|
$ pip install .
|
||||||
|
|
||||||
And then as root: --- ::
|
|
||||||
|
|
||||||
# python setup.py install
|
|
||||||
|
|
||||||
|
|
||||||
*******
|
*******
|
||||||
|
|||||||
+95
-143
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4; python-indent: 4 -*-
|
# -*- mode: python; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import select
|
import select
|
||||||
@@ -7,21 +7,12 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import errno
|
|
||||||
from enum import Enum, auto
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from alsaaudio import (PCM, pcms, PCM_PLAYBACK, PCM_CAPTURE, PCM_NONBLOCK, Mixer,
|
from alsaaudio import (PCM, pcms, PCM_PLAYBACK, PCM_CAPTURE, PCM_NONBLOCK, Mixer,
|
||||||
PCM_STATE_OPEN, PCM_STATE_SETUP, PCM_STATE_PREPARED, PCM_STATE_RUNNING, PCM_STATE_XRUN, PCM_STATE_DRAINING,
|
PCM_STATE_OPEN, PCM_STATE_SETUP, PCM_STATE_PREPARED, PCM_STATE_RUNNING, PCM_STATE_XRUN, PCM_STATE_DRAINING,
|
||||||
PCM_STATE_PAUSED, PCM_STATE_SUSPENDED, ALSAAudioError)
|
PCM_STATE_PAUSED, PCM_STATE_SUSPENDED, ALSAAudioError)
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
# wake up at least after *idle_timer_period* seconds
|
|
||||||
idle_timer_period = 0.25
|
|
||||||
# grace period for opening the device in seconds
|
|
||||||
open_grace_period = 0.5
|
|
||||||
# close the device after *idle_close_timeout* seconds silence
|
|
||||||
idle_close_timeout = 2.0
|
|
||||||
|
|
||||||
poll_names = {
|
poll_names = {
|
||||||
select.POLLIN: 'POLLIN',
|
select.POLLIN: 'POLLIN',
|
||||||
select.POLLPRI: 'POLLPRI',
|
select.POLLPRI: 'POLLPRI',
|
||||||
@@ -43,10 +34,6 @@ state_names = {
|
|||||||
PCM_STATE_SUSPENDED: 'PCM_STATE_SUSPENDED'
|
PCM_STATE_SUSPENDED: 'PCM_STATE_SUSPENDED'
|
||||||
}
|
}
|
||||||
|
|
||||||
type NamedProcess = tuple[str, subprocess.Popen]
|
|
||||||
|
|
||||||
cmd_process : NamedProcess = None
|
|
||||||
|
|
||||||
def poll_desc(mask):
|
def poll_desc(mask):
|
||||||
return '|'.join([poll_names[bit] for bit, name in poll_names.items() if mask & bit])
|
return '|'.join([poll_names[bit] for bit, name in poll_names.items() if mask & bit])
|
||||||
|
|
||||||
@@ -70,19 +57,14 @@ class PollDescriptor(object):
|
|||||||
|
|
||||||
return cls(name, fd, mask)
|
return cls(name, fd, mask)
|
||||||
|
|
||||||
class LoopbackState(Enum):
|
|
||||||
LISTENING = auto()
|
|
||||||
PLAYING = auto()
|
|
||||||
DEVICE_BUSY = auto()
|
|
||||||
|
|
||||||
class Loopback(object):
|
class Loopback(object):
|
||||||
'''Loopback state and event handling'''
|
'''Loopback state and event handling'''
|
||||||
|
|
||||||
def __init__(self, capture, playback_args, volume_handler, run_after_stop=None, run_before_start=None):
|
def __init__(self, capture, playback_args, volume_handler, run_after_stop=None, run_before_start=None):
|
||||||
self.playback_args = playback_args
|
self.playback_args = playback_args
|
||||||
self.playback = None
|
self.playback = None
|
||||||
|
|
||||||
self.volume_handler = volume_handler
|
self.volume_handler = volume_handler
|
||||||
|
self.capture_started = None
|
||||||
self.last_capture_event = None
|
self.last_capture_event = None
|
||||||
|
|
||||||
self.capture = capture
|
self.capture = capture
|
||||||
@@ -95,10 +77,13 @@ class Loopback(object):
|
|||||||
self.run_before_start = None
|
self.run_before_start = None
|
||||||
if run_before_start:
|
if run_before_start:
|
||||||
self.run_before_start = run_before_start.split(' ')
|
self.run_before_start = run_before_start.split(' ')
|
||||||
|
self.run_after_stop_did_run = False
|
||||||
|
|
||||||
self.state = None
|
self.waitBeforeOpen = False
|
||||||
self.last_state_change = None
|
self.queue = []
|
||||||
self.silence_start = None
|
|
||||||
|
self.period_size = 0
|
||||||
|
self.silent_periods = 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_energy(data):
|
def compute_energy(data):
|
||||||
@@ -112,84 +97,43 @@ class Loopback(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def run_command(cmd):
|
def run_command(cmd):
|
||||||
if cmd:
|
if cmd:
|
||||||
global cmd_process
|
rc = subprocess.run(cmd)
|
||||||
cmd_process = (cmd, subprocess.Popen(cmd))
|
if rc.returncode:
|
||||||
|
logging.warning(f'run {cmd}, return code {rc.returncode}')
|
||||||
@staticmethod
|
else:
|
||||||
def check_command_idle_handler():
|
logging.info(f'run {cmd}, return code {rc.returncode}')
|
||||||
# an idle handler to watch the process created above
|
|
||||||
if cmd_process:
|
|
||||||
rc = cmd_process[1].poll()
|
|
||||||
if rc:
|
|
||||||
if rc.returncode:
|
|
||||||
logging.warning(f'run {cmd_process[0]}, return code {rc.returncode}')
|
|
||||||
else:
|
|
||||||
logging.info(f'run {cmd_process[0]}, return code {rc.returncode}')
|
|
||||||
cmd_process = None
|
|
||||||
|
|
||||||
def register(self, reactor):
|
def register(self, reactor):
|
||||||
reactor.register_idle_handler(self.check_command_idle_handler)
|
reactor.register_timeout_handler(self.timeout_handler)
|
||||||
reactor.register_idle_handler(self.idle_handler)
|
|
||||||
reactor.register(self.capture_pd, self)
|
reactor.register(self.capture_pd, self)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
assert self.state == None, "start must only be called once"
|
|
||||||
# start reading data
|
# start reading data
|
||||||
size, _ = self.capture.read()
|
size, data = self.capture.read()
|
||||||
if size:
|
if size:
|
||||||
logging.warning(f'initial data discarded ({size} bytes)')
|
self.queue.append(data)
|
||||||
|
|
||||||
self.state = LoopbackState.LISTENING
|
def timeout_handler(self):
|
||||||
|
if self.playback and self.capture_started:
|
||||||
def set_state(self, new_state: LoopbackState) -> LoopbackState:
|
if self.last_capture_event:
|
||||||
'''Implement the Loopback state as a state machine'''
|
if datetime.now() - self.last_capture_event > timedelta(seconds=2):
|
||||||
|
logging.info('timeout - closing playback device')
|
||||||
if self.state == new_state:
|
self.playback.close()
|
||||||
return self.state
|
self.playback = None
|
||||||
|
self.capture_started = None
|
||||||
logging.info(f'{self.state} -> {new_state}')
|
|
||||||
self.last_state_change = datetime.now()
|
|
||||||
|
|
||||||
if new_state == LoopbackState.LISTENING:
|
|
||||||
if self.state == LoopbackState.PLAYING:
|
|
||||||
self.playback.close()
|
|
||||||
self.playback = None
|
|
||||||
self.last_capture_event = None
|
|
||||||
if self.volume_handler:
|
|
||||||
self.volume_handler.stop()
|
|
||||||
self.run_command(self.run_after_stop)
|
|
||||||
elif self.state == LoopbackState.DEVICE_BUSY:
|
|
||||||
pass
|
|
||||||
elif new_state == LoopbackState.PLAYING:
|
|
||||||
if self.state == LoopbackState.LISTENING:
|
|
||||||
try:
|
|
||||||
self.run_command(self.run_before_start)
|
|
||||||
self.playback = PCM(**self.playback_args)
|
|
||||||
period_size = self.playback.info()['period_size']
|
|
||||||
logging.info(f'opened playback device with period_size {period_size}')
|
|
||||||
if self.volume_handler:
|
if self.volume_handler:
|
||||||
self.volume_handler.start()
|
self.volume_handler.stop()
|
||||||
except ALSAAudioError as e:
|
self.run_command(self.run_after_stop)
|
||||||
logging.warning('opening PCM playback device failed: %s', e)
|
|
||||||
self.state = LoopbackState.DEVICE_BUSY
|
|
||||||
return self.state
|
|
||||||
elif self.state == LoopbackState.DEVICE_BUSY:
|
|
||||||
# Only try to reopen the device after the grace period
|
|
||||||
if datetime.now() - self.last_state_change > timedelta(seconds=open_grace_period):
|
|
||||||
return self.set_state(LoopbackState.PLAYING)
|
|
||||||
elif new_state == LoopbackState.DEVICE_BUSY:
|
|
||||||
logging.error(f'{new_state} is internal and cannot be set directly')
|
|
||||||
|
|
||||||
self.state = new_state
|
|
||||||
return self.state
|
|
||||||
|
|
||||||
def idle_handler(self):
|
|
||||||
if self.state == LoopbackState.PLAYING:
|
|
||||||
if datetime.now() - self.last_capture_event > timedelta(seconds=idle_close_timeout):
|
|
||||||
logging.info('timeout - closing playback device')
|
|
||||||
self.set_state(LoopbackState.LISTENING)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.waitBeforeOpen = False
|
||||||
|
|
||||||
|
if not self.run_after_stop_did_run and not self.playback:
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.stop()
|
||||||
|
self.run_command(self.run_after_stop)
|
||||||
|
self.run_after_stop_did_run = True
|
||||||
|
|
||||||
def pop(self):
|
def pop(self):
|
||||||
if len(self.queue):
|
if len(self.queue):
|
||||||
return self.queue.pop()
|
return self.queue.pop()
|
||||||
@@ -197,15 +141,6 @@ class Loopback(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def handle_capture_event(self, eventmask, name):
|
def handle_capture_event(self, eventmask, name):
|
||||||
|
|
||||||
if eventmask & select.POLLERR == select.POLLERR:
|
|
||||||
# This is typically an underrun caused by the external command being run synchronously
|
|
||||||
# (on the same thread)
|
|
||||||
state = self.capture.state()
|
|
||||||
if state == PCM_STATE_XRUN:
|
|
||||||
self.capture.drop()
|
|
||||||
logging.warning(f'POLLERR for capture device: {state_names[state]}')
|
|
||||||
|
|
||||||
'''called when data is available for reading'''
|
'''called when data is available for reading'''
|
||||||
self.last_capture_event = datetime.now()
|
self.last_capture_event = datetime.now()
|
||||||
size, data = self.capture.read()
|
size, data = self.capture.read()
|
||||||
@@ -213,37 +148,65 @@ class Loopback(object):
|
|||||||
logging.warning(f'capture event but no data')
|
logging.warning(f'capture event but no data')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# the usecase is a USB capture device where we get perfect silence when it's idle
|
|
||||||
# compute the energy and go back to LISTENING if nothing is captured
|
|
||||||
energy = self.compute_energy(data)
|
energy = self.compute_energy(data)
|
||||||
|
logging.debug(f'energy: {energy}')
|
||||||
|
|
||||||
|
# the usecase is a USB capture device where we get perfect silence when it's idle
|
||||||
if energy == 0:
|
if energy == 0:
|
||||||
if self.silence_start is None:
|
self.silent_periods = self.silent_periods + 1
|
||||||
self.silence_start = datetime.now()
|
|
||||||
|
|
||||||
# turn off playback after idle_close_timeout when there was only silence
|
# turn off playback after two seconds of silence
|
||||||
if datetime.now() - self.silence_start > timedelta(seconds=idle_close_timeout):
|
# 2 channels * 2 seconds * 2 bytes per sample
|
||||||
logging.debug('silence')
|
fps = self.playback_args['rate'] * 8 // (self.playback_args['periodsize'] * self.playback_args['periods'])
|
||||||
self.set_state(LoopbackState.LISTENING)
|
|
||||||
return False
|
logging.debug(f'{self.silent_periods} of {fps} silent periods: {self.playback}')
|
||||||
|
|
||||||
|
if self.silent_periods > fps and self.playback:
|
||||||
|
logging.info(f'closing playback due to silence')
|
||||||
|
self.playback.close()
|
||||||
|
self.playback = None
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.stop()
|
||||||
|
self.run_command(self.run_after_stop)
|
||||||
|
self.run_after_stop_did_run = True
|
||||||
|
|
||||||
|
if not self.playback:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
self.silence_start = None
|
self.silent_periods = 0
|
||||||
|
|
||||||
loop_state = self.set_state(LoopbackState.PLAYING)
|
if not self.playback:
|
||||||
if loop_state != LoopbackState.PLAYING:
|
if self.waitBeforeOpen:
|
||||||
logging.warning(f'setting state PLAYING failed: {str(loop_state)}')
|
return False
|
||||||
|
try:
|
||||||
|
if self.volume_handler:
|
||||||
|
self.volume_handler.start()
|
||||||
|
self.run_command(self.run_before_start)
|
||||||
|
self.playback = PCM(**self.playback_args)
|
||||||
|
self.period_size = self.playback.info()['period_size']
|
||||||
|
logging.info(f'opened playback device with period_size {self.period_size}')
|
||||||
|
except ALSAAudioError as e:
|
||||||
|
logging.info('opening PCM playback device failed: %s', e)
|
||||||
|
self.waitBeforeOpen = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.capture_started = datetime.now()
|
||||||
|
logging.info(f'{self.playback} capture started: {self.capture_started}')
|
||||||
|
|
||||||
|
self.queue.append(data)
|
||||||
|
|
||||||
|
if len(self.queue) <= 2:
|
||||||
|
logging.info(f'buffering: {len(self.queue)}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if data:
|
try:
|
||||||
space = self.playback.avail()
|
data = self.pop()
|
||||||
if space > 0:
|
if data:
|
||||||
|
space = self.playback.avail()
|
||||||
written = self.playback.write(data)
|
written = self.playback.write(data)
|
||||||
if written == -errno.EPIPE:
|
logging.debug(f'wrote {written} bytes while space was {space}')
|
||||||
logging.warning('playback underrun')
|
except ALSAAudioError:
|
||||||
self.playback.write(data)
|
logging.error('underrun', exc_info=1)
|
||||||
silence = ''
|
|
||||||
if energy == 0:
|
|
||||||
silence = '(silence)'
|
|
||||||
logging.debug(f'wrote {written} bytes while space was {space} {silence}')
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -276,13 +239,11 @@ class VolumeForwarder(object):
|
|||||||
def start(self):
|
def start(self):
|
||||||
self.active = True
|
self.active = True
|
||||||
if self.volume:
|
if self.volume:
|
||||||
logging.info(f'start volume is {self.volume}')
|
|
||||||
self.volume = playback_control.setvolume(self.volume)
|
self.volume = playback_control.setvolume(self.volume)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.active = False
|
self.active = False
|
||||||
self.volume = self.playback_control.getvolume(pcmtype=PCM_CAPTURE)[0]
|
self.volume = self.playback_control.getvolume(pcmtype=PCM_CAPTURE)[0]
|
||||||
logging.info(f'stop volume is {self.volume}')
|
|
||||||
|
|
||||||
def __call__(self, fd, eventmask, name):
|
def __call__(self, fd, eventmask, name):
|
||||||
if not self.active:
|
if not self.active:
|
||||||
@@ -302,7 +263,7 @@ class Reactor(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.poll = select.poll()
|
self.poll = select.poll()
|
||||||
self.descriptors = {}
|
self.descriptors = {}
|
||||||
self.idle_handlers = set()
|
self.timeout_handlers = set()
|
||||||
|
|
||||||
def register(self, polldescriptor, callable):
|
def register(self, polldescriptor, callable):
|
||||||
logging.debug(f'registered {polldescriptor.name}: {poll_desc(polldescriptor.mask)}')
|
logging.debug(f'registered {polldescriptor.name}: {poll_desc(polldescriptor.mask)}')
|
||||||
@@ -313,17 +274,17 @@ class Reactor(object):
|
|||||||
self.poll.unregister(polldescriptor.fd)
|
self.poll.unregister(polldescriptor.fd)
|
||||||
del self.descriptors[polldescriptor.fd]
|
del self.descriptors[polldescriptor.fd]
|
||||||
|
|
||||||
def register_idle_handler(self, callable):
|
def register_timeout_handler(self, callable):
|
||||||
self.idle_handlers.add(callable)
|
self.timeout_handlers.add(callable)
|
||||||
|
|
||||||
def unregister_idle_handler(self, callable):
|
def unregister_timeout_handler(self, callable):
|
||||||
self.idle_handlers.remove(callable)
|
self.timeout_handlers.remove(callable)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
last_timeout_ev = datetime.now()
|
last_timeout_ev = datetime.now()
|
||||||
while True:
|
while True:
|
||||||
# poll for a bit, then send a timeout to registered handlers
|
# poll for a bit, then send a timeout to registered handlers
|
||||||
events = self.poll.poll(idle_timer_period)
|
events = self.poll.poll(0.25)
|
||||||
for fd, ev in events:
|
for fd, ev in events:
|
||||||
polldescriptor, handler = self.descriptors[fd]
|
polldescriptor, handler = self.descriptors[fd]
|
||||||
|
|
||||||
@@ -332,8 +293,8 @@ class Reactor(object):
|
|||||||
|
|
||||||
handler(fd, ev, polldescriptor.name)
|
handler(fd, ev, polldescriptor.name)
|
||||||
|
|
||||||
if datetime.now() - last_timeout_ev > timedelta(seconds=idle_timer_period):
|
if datetime.now() - last_timeout_ev > timedelta(seconds=0.25):
|
||||||
for t in self.idle_handlers:
|
for t in self.timeout_handlers:
|
||||||
t()
|
t()
|
||||||
last_timeout_ev = datetime.now()
|
last_timeout_ev = datetime.now()
|
||||||
|
|
||||||
@@ -383,16 +344,6 @@ if __name__ == '__main__':
|
|||||||
'periods': args.periods
|
'periods': args.periods
|
||||||
}
|
}
|
||||||
|
|
||||||
capture_args = {
|
|
||||||
'type': PCM_CAPTURE,
|
|
||||||
'mode': PCM_NONBLOCK,
|
|
||||||
'device': args.input,
|
|
||||||
'rate': args.rate,
|
|
||||||
'channels': args.channels,
|
|
||||||
'periodsize': args.periodsize,
|
|
||||||
'periods': args.periods
|
|
||||||
}
|
|
||||||
|
|
||||||
reactor = Reactor()
|
reactor = Reactor()
|
||||||
|
|
||||||
# If args.input_mixer and args.output_mixer are set, forward the capture volume to the playback volume.
|
# If args.input_mixer and args.output_mixer are set, forward the capture volume to the playback volume.
|
||||||
@@ -427,7 +378,8 @@ if __name__ == '__main__':
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if input_mixer_card is None:
|
if input_mixer_card is None:
|
||||||
capture = PCM(**capture_args)
|
capture = PCM(type=PCM_CAPTURE, mode=PCM_NONBLOCK, device=args.input, rate=args.rate,
|
||||||
|
channels=args.channels, periodsize=args.periodsize, periods=args.periods)
|
||||||
input_mixer_card = capture.info()['card_no']
|
input_mixer_card = capture.info()['card_no']
|
||||||
|
|
||||||
if output_mixer_card is None:
|
if output_mixer_card is None:
|
||||||
|
|||||||
+111
-57
@@ -106,9 +106,6 @@ typedef struct {
|
|||||||
|
|
||||||
snd_pcm_t *handle;
|
snd_pcm_t *handle;
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
int state;
|
|
||||||
|
|
||||||
// Configurable parameters
|
// Configurable parameters
|
||||||
unsigned int channels;
|
unsigned int channels;
|
||||||
unsigned int rate;
|
unsigned int rate;
|
||||||
@@ -363,40 +360,6 @@ alsapcm_list(PyObject *self, PyObject *args, PyObject *kwds)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* state_name(int state) {
|
|
||||||
switch (state) {
|
|
||||||
case SND_PCM_STATE_OPEN:
|
|
||||||
return "SND_PCM_STATE_OPEN";
|
|
||||||
case SND_PCM_STATE_SETUP:
|
|
||||||
return "SND_PCM_STATE_SETUP";
|
|
||||||
case SND_PCM_STATE_PREPARED:
|
|
||||||
return "SND_PCM_STATE_PREPARED";
|
|
||||||
case SND_PCM_STATE_RUNNING:
|
|
||||||
return "SND_PCM_STATE_RUNNING";
|
|
||||||
case SND_PCM_STATE_XRUN:
|
|
||||||
return "SND_PCM_STATE_XRUN";
|
|
||||||
case SND_PCM_STATE_DISCONNECTED:
|
|
||||||
return "SND_PCM_STATE_DISCONNECTED";
|
|
||||||
case SND_PCM_STATE_DRAINING:
|
|
||||||
return "SND_PCM_STATE_DRAINING";
|
|
||||||
case SND_PCM_STATE_PAUSED:
|
|
||||||
return "SND_PCM_STATE_PAUSED";
|
|
||||||
case SND_PCM_STATE_SUSPENDED:
|
|
||||||
return "SND_PCM_STATE_SUSPENDED";
|
|
||||||
default:
|
|
||||||
return "invalid PCM state";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void print_state(alsapcm_t *self)
|
|
||||||
{
|
|
||||||
int state = snd_pcm_state(self->handle);
|
|
||||||
if (state != self->state) {
|
|
||||||
printf("[%s %s] %s->%s\n", self->cardname, self->pcmtype == SND_PCM_STREAM_CAPTURE ? "capture" : "playback", state_name(self->state), state_name(state));
|
|
||||||
self->state = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int alsapcm_setup(alsapcm_t *self)
|
static int alsapcm_setup(alsapcm_t *self)
|
||||||
{
|
{
|
||||||
int res,dir;
|
int res,dir;
|
||||||
@@ -442,8 +405,6 @@ static int alsapcm_setup(alsapcm_t *self)
|
|||||||
|
|
||||||
self->framesize = self->channels * snd_pcm_format_physical_width(self->format)/8;
|
self->framesize = self->channels * snd_pcm_format_physical_width(self->format)/8;
|
||||||
|
|
||||||
self->state = snd_pcm_state(self->handle);
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1386,6 +1347,7 @@ static PyObject *
|
|||||||
alsapcm_read(alsapcm_t *self, PyObject *args)
|
alsapcm_read(alsapcm_t *self, PyObject *args)
|
||||||
{
|
{
|
||||||
snd_pcm_state_t state;
|
snd_pcm_state_t state;
|
||||||
|
int res;
|
||||||
int size = self->framesize * self->periodsize;
|
int size = self->framesize * self->periodsize;
|
||||||
int sizeout = 0;
|
int sizeout = 0;
|
||||||
PyObject *buffer_obj, *tuple_obj, *res_obj;
|
PyObject *buffer_obj, *tuple_obj, *res_obj;
|
||||||
@@ -1418,22 +1380,122 @@ alsapcm_read(alsapcm_t *self, PyObject *args)
|
|||||||
buffer = PyBytes_AS_STRING(buffer_obj);
|
buffer = PyBytes_AS_STRING(buffer_obj);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int res = 0;
|
// After drop() and drain(), we need to prepare the stream again.
|
||||||
|
// Note that fresh streams are already prepared by snd_pcm_hw_params().
|
||||||
|
state = snd_pcm_state(self->handle);
|
||||||
|
if ((state != SND_PCM_STATE_SETUP) ||
|
||||||
|
!(res = snd_pcm_prepare(self->handle))) {
|
||||||
|
|
||||||
print_state(self);
|
Py_BEGIN_ALLOW_THREADS
|
||||||
|
res = snd_pcm_readi(self->handle, buffer, self->periodsize);
|
||||||
|
Py_END_ALLOW_THREADS
|
||||||
|
|
||||||
|
if (res == -EPIPE) {
|
||||||
|
// This means buffer overrun, which we need to report.
|
||||||
|
// However, we recover the stream, so the next PCM.read() will work
|
||||||
|
// again. If recovery fails (very unlikely), report that instead.
|
||||||
|
if (!(res = snd_pcm_prepare(self->handle)))
|
||||||
|
res = -EPIPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res != -EPIPE)
|
||||||
|
{
|
||||||
|
if (res == -EAGAIN)
|
||||||
|
{
|
||||||
|
res = 0;
|
||||||
|
}
|
||||||
|
else if (res < 0) {
|
||||||
|
PyErr_Format(ALSAAudioError, "%s [%s]", snd_strerror(res),
|
||||||
|
self->cardname);
|
||||||
|
|
||||||
|
Py_DECREF(buffer_obj);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sizeout = res * self->framesize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size != sizeout) {
|
||||||
|
#if PY_MAJOR_VERSION < 3
|
||||||
|
/* If the following fails, it will free the object */
|
||||||
|
if (_PyString_Resize(&buffer_obj, sizeout))
|
||||||
|
return NULL;
|
||||||
|
#else
|
||||||
|
/* If the following fails, it will free the object */
|
||||||
|
if (_PyBytes_Resize(&buffer_obj, sizeout))
|
||||||
|
return NULL;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
res_obj = PyLong_FromLong(res);
|
||||||
|
if (!res_obj) {
|
||||||
|
Py_DECREF(buffer_obj);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
tuple_obj = PyTuple_New(2);
|
||||||
|
if (!tuple_obj) {
|
||||||
|
Py_DECREF(buffer_obj);
|
||||||
|
Py_DECREF(res_obj);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/* Steal reference counts */
|
||||||
|
PyTuple_SET_ITEM(tuple_obj, 0, res_obj);
|
||||||
|
PyTuple_SET_ITEM(tuple_obj, 1, buffer_obj);
|
||||||
|
|
||||||
|
return tuple_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
alsapcm_read_sw(alsapcm_t *self, PyObject *args)
|
||||||
|
{
|
||||||
|
snd_pcm_state_t state;
|
||||||
|
int res;
|
||||||
|
int max_frames_to_read;
|
||||||
|
int size;
|
||||||
|
int sizeout = 0;
|
||||||
|
PyObject *buffer_obj, *tuple_obj, *res_obj;
|
||||||
|
char *buffer;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args,"i:read_sw", &max_frames_to_read))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (!self->handle) {
|
||||||
|
PyErr_SetString(ALSAAudioError, "PCM device is closed");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->pcmtype != SND_PCM_STREAM_CAPTURE)
|
||||||
|
{
|
||||||
|
PyErr_Format(ALSAAudioError, "Cannot read from playback PCM [%s]",
|
||||||
|
self->cardname);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = self->framesize * max_frames_to_read;
|
||||||
|
|
||||||
|
#if PY_MAJOR_VERSION < 3
|
||||||
|
buffer_obj = PyString_FromStringAndSize(NULL, size);
|
||||||
|
if (!buffer_obj)
|
||||||
|
return NULL;
|
||||||
|
buffer = PyString_AS_STRING(buffer_obj);
|
||||||
|
#else
|
||||||
|
buffer_obj = PyBytes_FromStringAndSize(NULL, size);
|
||||||
|
if (!buffer_obj)
|
||||||
|
return NULL;
|
||||||
|
buffer = PyBytes_AS_STRING(buffer_obj);
|
||||||
|
#endif
|
||||||
|
|
||||||
// After drop() and drain(), we need to prepare the stream again.
|
// After drop() and drain(), we need to prepare the stream again.
|
||||||
// Note that fresh streams are already prepared by snd_pcm_hw_params().
|
// Note that fresh streams are already prepared by snd_pcm_hw_params().
|
||||||
state = snd_pcm_state(self->handle);
|
state = snd_pcm_state(self->handle);
|
||||||
if (state == SND_PCM_STATE_SETUP) {
|
if ((state != SND_PCM_STATE_SETUP) ||
|
||||||
res = snd_pcm_prepare(self->handle);
|
!(res = snd_pcm_prepare(self->handle))) {
|
||||||
printf("[%s] called snd_pcm_prepare: %d\n", self->cardname, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == 0) {
|
|
||||||
|
|
||||||
Py_BEGIN_ALLOW_THREADS
|
Py_BEGIN_ALLOW_THREADS
|
||||||
res = snd_pcm_readi(self->handle, buffer, self->periodsize);
|
res = snd_pcm_readi(self->handle, buffer, max_frames_to_read);
|
||||||
Py_END_ALLOW_THREADS
|
Py_END_ALLOW_THREADS
|
||||||
|
|
||||||
if (res == -EPIPE) {
|
if (res == -EPIPE) {
|
||||||
@@ -1534,8 +1596,6 @@ static PyObject *alsapcm_write(alsapcm_t *self, PyObject *args)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_state(self);
|
|
||||||
|
|
||||||
int res;
|
int res;
|
||||||
// After drop() and drain(), we need to prepare the stream again.
|
// After drop() and drain(), we need to prepare the stream again.
|
||||||
// Note that fresh streams are already prepared by snd_pcm_hw_params().
|
// Note that fresh streams are already prepared by snd_pcm_hw_params().
|
||||||
@@ -1625,9 +1685,6 @@ static PyObject *alsapcm_pause(alsapcm_t *self, PyObject *args)
|
|||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_state(self);
|
|
||||||
|
|
||||||
return PyLong_FromLong(res);
|
return PyLong_FromLong(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1650,8 +1707,6 @@ static PyObject *alsapcm_drop(alsapcm_t *self)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_state(self);
|
|
||||||
|
|
||||||
return PyLong_FromLong(res);
|
return PyLong_FromLong(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1676,8 +1731,6 @@ static PyObject *alsapcm_drain(alsapcm_t *self)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_state(self);
|
|
||||||
|
|
||||||
return PyLong_FromLong(res);
|
return PyLong_FromLong(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1822,6 +1875,7 @@ static PyMethodDef alsapcm_methods[] = {
|
|||||||
{"getratebounds", (PyCFunction)alsapcm_getratemaxmin, METH_VARARGS},
|
{"getratebounds", (PyCFunction)alsapcm_getratemaxmin, METH_VARARGS},
|
||||||
{"getrates", (PyCFunction)alsapcm_getrates, METH_VARARGS},
|
{"getrates", (PyCFunction)alsapcm_getrates, METH_VARARGS},
|
||||||
{"read", (PyCFunction)alsapcm_read, METH_VARARGS},
|
{"read", (PyCFunction)alsapcm_read, METH_VARARGS},
|
||||||
|
{"read_sw", (PyCFunction)alsapcm_read_sw, METH_VARARGS},
|
||||||
{"write", (PyCFunction)alsapcm_write, METH_VARARGS},
|
{"write", (PyCFunction)alsapcm_write, METH_VARARGS},
|
||||||
{"avail", (PyCFunction)alsapcm_avail, METH_VARARGS},
|
{"avail", (PyCFunction)alsapcm_avail, METH_VARARGS},
|
||||||
{"pause", (PyCFunction)alsapcm_pause, METH_VARARGS},
|
{"pause", (PyCFunction)alsapcm_pause, METH_VARARGS},
|
||||||
|
|||||||
Reference in New Issue
Block a user