forked from auracaster/pyalsaaudio
Refactor. SCNR.
The Reactor now takes a callable, and the loopback and volume forwarder are now implemented as callable instances, which seemed the most Pythonic solution.
This commit is contained in:
102
loopback.py
102
loopback.py
@@ -4,6 +4,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import select
|
import select
|
||||||
import logging
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
from alsaaudio import PCM, pcms, PCM_PLAYBACK, PCM_CAPTURE, PCM_FORMAT_S16_LE, PCM_NONBLOCK, Mixer
|
from alsaaudio import PCM, pcms, PCM_PLAYBACK, PCM_CAPTURE, PCM_FORMAT_S16_LE, PCM_NONBLOCK, Mixer
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
@@ -20,28 +21,44 @@ poll_names = {
|
|||||||
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])
|
||||||
|
|
||||||
class AlsaEvent(object):
|
class PollDescriptor(object):
|
||||||
def __init__(self, name, alsaobject):
|
'''File Descriptor, event mask and a name for logging'''
|
||||||
|
def __init__(self, name, fd, mask):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.fd, self.mask = alsaobject.polldescriptors()[0]
|
self.fd = fd
|
||||||
|
self.mask = mask
|
||||||
|
|
||||||
def handle_event(self, mask):
|
@classmethod
|
||||||
'''Override this if you need to handle events'''
|
def fromAlsaObject(cls, name, alsaobject, mask=None):
|
||||||
pass
|
fd, alsamask = alsaobject.polldescriptors()[0]
|
||||||
|
|
||||||
|
if mask is None:
|
||||||
|
mask = alsamask
|
||||||
|
|
||||||
class CaptureEvent(AlsaEvent):
|
return cls(name, fd, mask)
|
||||||
|
|
||||||
def __init__(self, name, capture, playback):
|
class Loopback(object):
|
||||||
super().__init__(name, capture)
|
'''Loopback state and event handling'''
|
||||||
|
|
||||||
|
def __init__(self, capture, playback):
|
||||||
self.playback = playback
|
self.playback = playback
|
||||||
|
self.playback_pd = PollDescriptor.fromAlsaObject('playback', playback)
|
||||||
|
|
||||||
self.capture = capture
|
self.capture = capture
|
||||||
|
self.capture_pd = PollDescriptor.fromAlsaObject('capture', capture)
|
||||||
|
|
||||||
|
def register(self, reactor):
|
||||||
|
reactor.register(self.capture_pd, self)
|
||||||
|
reactor.register(self.playback_pd, self)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# start reading data
|
# start reading data
|
||||||
self.capture.read()
|
self.capture.read()
|
||||||
|
|
||||||
def handle_event(self, mask):
|
def handle_playback_event(self, eventmask, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_capture_event(self, eventmask, name):
|
||||||
size, data = self.capture.read()
|
size, data = self.capture.read()
|
||||||
if not size:
|
if not size:
|
||||||
logging.warning(f'underrun')
|
logging.warning(f'underrun')
|
||||||
@@ -49,23 +66,29 @@ class CaptureEvent(AlsaEvent):
|
|||||||
|
|
||||||
written = self.playback.write(data)
|
written = self.playback.write(data)
|
||||||
if not written:
|
if not written:
|
||||||
# if this happened, we might push the data to a queue
|
|
||||||
logging.warning('overrun')
|
logging.warning('overrun')
|
||||||
else:
|
else:
|
||||||
logging.debug(f'{self.name} wrote {size}: {written}')
|
logging.debug(f'wrote {size}: {written}')
|
||||||
|
|
||||||
|
def __call__(self, fd, eventmask, name):
|
||||||
|
if fd == self.capture_pd.fd:
|
||||||
|
self.handle_capture_event(eventmask, name)
|
||||||
|
else:
|
||||||
|
self.handle_playback_event(eventmask, name)
|
||||||
|
|
||||||
|
|
||||||
class VolumeEvent(AlsaEvent):
|
class VolumeForwarder(object):
|
||||||
def __init__(self, name, capture_control, playback_control):
|
'''Volume control event handling'''
|
||||||
super().__init__(name, capture_control)
|
|
||||||
|
def __init__(self, capture_control, playback_control):
|
||||||
self.playback_control = playback_control
|
self.playback_control = playback_control
|
||||||
self.capture_control = capture_control
|
self.capture_control = capture_control
|
||||||
|
|
||||||
def handle_event(self, mask):
|
def __call__(self, fd, eventmask, name):
|
||||||
volume = self.capture_control.getvolume(pcmtype=PCM_CAPTURE)
|
volume = self.capture_control.getvolume(pcmtype=PCM_CAPTURE)
|
||||||
# it looks as if we need to get the playback volume to reset the event
|
# it looks as if we need to get the playback volume to reset the event
|
||||||
self.capture_control.getvolume()
|
self.capture_control.getvolume()
|
||||||
logging.info(f'{self.name} adjusting volume to {volume}')
|
logging.info(f'{name} adjusting volume to {volume}')
|
||||||
if volume:
|
if volume:
|
||||||
self.playback_control.setvolume(volume[0])
|
self.playback_control.setvolume(volume[0])
|
||||||
|
|
||||||
@@ -75,35 +98,32 @@ class Reactor(object):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.poll = select.poll()
|
self.poll = select.poll()
|
||||||
self.events = {}
|
self.descriptors = {}
|
||||||
|
|
||||||
def register(self, event, mask=None):
|
def register(self, polldescriptor, callable):
|
||||||
logging.debug(f'registered {event.name}: {poll_desc(event.mask)}')
|
logging.debug(f'registered {polldescriptor.name}: {poll_desc(polldescriptor.mask)}')
|
||||||
self.events[event.fd] = event
|
self.descriptors[polldescriptor.fd] = (polldescriptor, callable)
|
||||||
|
|
||||||
# allow overriding of mask
|
self.poll.register(polldescriptor.fd, polldescriptor.mask)
|
||||||
if mask is None:
|
|
||||||
mask = event.mask
|
|
||||||
|
|
||||||
self.poll.register(event.fd, mask)
|
def unregister(self, polldescriptor):
|
||||||
|
self.poll.unregister(polldescriptor.fd)
|
||||||
def unregister(self, event):
|
del self.descriptors[polldescriptor.fd]
|
||||||
self.poll.unregister(event.fd)
|
|
||||||
del self.events[event.fd]
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while True:
|
||||||
events = self.poll.poll()
|
events = self.poll.poll()
|
||||||
for fd, ev in events:
|
for fd, ev in events:
|
||||||
handler = self.events[fd]
|
polldescriptor, handler = self.descriptors[fd]
|
||||||
|
|
||||||
# warn about unexpected/unhandled events
|
# warn about unexpected/unhandled events
|
||||||
if ev & (select.POLLERR | select.POLLHUP | select.POLLNVAL | select.POLLRDHUP):
|
if ev & (select.POLLERR | select.POLLHUP | select.POLLNVAL | select.POLLRDHUP):
|
||||||
logging.warning(f'{handler.name}: {poll_desc(ev)} ({ev})')
|
logging.warning(f'{polldescriptor.name}: {poll_desc(ev)} ({ev})')
|
||||||
else:
|
else:
|
||||||
logging.debug(f'{handler.name}: {poll_desc(ev)} ({ev})')
|
logging.debug(f'{polldescriptor.name}: {poll_desc(ev)} ({ev})')
|
||||||
|
|
||||||
|
handler(fd, ev, polldescriptor.name)
|
||||||
|
|
||||||
handler.handle_event(ev)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
@@ -143,16 +163,10 @@ if __name__ == '__main__':
|
|||||||
capture = PCM(type=PCM_CAPTURE, mode=PCM_NONBLOCK, device=args.input, rate=args.rate,
|
capture = PCM(type=PCM_CAPTURE, mode=PCM_NONBLOCK, device=args.input, rate=args.rate,
|
||||||
channels=args.channels, periodsize=args.periodsize, periods=args.periods)
|
channels=args.channels, periodsize=args.periodsize, periods=args.periods)
|
||||||
|
|
||||||
capture_handler = CaptureEvent('capture', capture, playback)
|
loopback = Loopback(capture, playback)
|
||||||
playback_handler = AlsaEvent('playback', playback)
|
|
||||||
|
|
||||||
reactor = Reactor()
|
reactor = Reactor()
|
||||||
|
|
||||||
capture_handler.start()
|
|
||||||
|
|
||||||
reactor.register(capture_handler)
|
|
||||||
reactor.register(playback_handler)
|
|
||||||
|
|
||||||
# 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.
|
||||||
# The usecase is a capture device that is implemented using g_audio, i.e. the Linux USB gadget driver.
|
# The usecase is a capture device that is implemented using g_audio, i.e. the Linux USB gadget driver.
|
||||||
# When a USB device (eg. an iPad) is connected to this machine, its volume events will to the volume control
|
# When a USB device (eg. an iPad) is connected to this machine, its volume events will to the volume control
|
||||||
@@ -161,7 +175,11 @@ if __name__ == '__main__':
|
|||||||
playback_control = Mixer(control=args.output_mixer, cardindex=playback.info()['card_no'])
|
playback_control = Mixer(control=args.output_mixer, cardindex=playback.info()['card_no'])
|
||||||
capture_control = Mixer(control=args.input_mixer, cardindex=capture.info()['card_no'])
|
capture_control = Mixer(control=args.input_mixer, cardindex=capture.info()['card_no'])
|
||||||
|
|
||||||
volume_handler = VolumeEvent('volume', capture_control, playback_control)
|
volume_handler = VolumeForwarder(capture_control, playback_control)
|
||||||
reactor.register(volume_handler, select.POLLIN)
|
reactor.register(PollDescriptor.fromAlsaObject('capture_control', capture_control, select.POLLIN), volume_handler)
|
||||||
|
|
||||||
|
loopback.register(reactor)
|
||||||
|
|
||||||
|
loopback.start()
|
||||||
|
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user