diff --git a/README.rst b/README.rst index 6207d27..78e8df5 100644 --- a/README.rst +++ b/README.rst @@ -73,19 +73,20 @@ For validation, run:: $ export BITRATE=48 # explicitly select high MODEM bit rate (assuming good SNR). $ amodem -h - usage: amodem [-h] {send,recv} ... + usage: amodem [-h] {send,recv,lsdev} ... - Audio OFDM MODEM: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz + Audio OFDM MODEM v1.15.4: 48.0 kb/s (64-QAM x 8 carriers) Fs=32.0 kHz positional arguments: - {send,recv} - send modulate binary data into audio signal. - recv demodulate audio signal into binary data. + {send,recv,lsdev} + send modulate binary data into audio signal. + recv demodulate audio signal into binary data. + lsdev list all devices. (portaudio only) optional arguments: - -h, --help show this help message and exit + -h, --help show this help message and exit -On, Windows you may download the `portaudio` library from `MinGW `_. +On Windows you may download the `portaudio` library from `MinGW `_. Then, you should specify the DLL using the following command-line flag:: -l AUDIO_LIBRARY, --audio-library AUDIO_LIBRARY @@ -136,6 +137,21 @@ and send me the resulting ``audio.raw`` file for debugging:: You can see a screencast of the `calibration process `_. +Devices +------- +To specify an input/output device other than default, set the following environment variables:: + + ~/sender $ export OUTAUDIODEVICE=2 # Set output device using device id. + ~/sender $ export INAUDIODEVICE=2 # Set input device using device id. + +To list input/output devices, use the following command:: + + ~/sender $ amodem lsdev + +To see details of a specific i/o device, specify the device id using ``-d`` as shown below:: + + ~/sender $ amodem lsdev -d 2 + Usage ----- diff --git a/amodem/__main__.py b/amodem/__main__.py index dc946a9..70e71e8 100644 --- a/amodem/__main__.py +++ b/amodem/__main__.py @@ -12,6 +12,7 @@ from . import audio from . import calib from . import main +from . import lsdev from .config import bitrates @@ -25,6 +26,9 @@ bitrate = os.environ.get('BITRATE', 1) config = bitrates.get(int(bitrate)) +input_device = os.environ.get('INAUDIODEVICE') +output_device = os.environ.get('OUTAUDIODEVICE') + class Compressor: def __init__(self, stream): @@ -71,10 +75,10 @@ def opener(fname): if fname is None: assert audio_interface is not None if 'r' in mode: - s = audio_interface.recorder() + s = audio_interface.recorder(int(input_device)) return async_reader.AsyncReader(stream=s, bufsize=s.bufsize) if 'w' in mode: - return audio_interface.player() + return audio_interface.player(int(output_device)) if fname == '-': if 'r' in mode: @@ -177,6 +181,24 @@ def create_parser(description, interface_factory): g.add_argument('-v', '--verbose', default=0, action='count') g.add_argument('-q', '--quiet', default=False, action='store_true') + device_lister = subparsers.add_parser( + 'lsdev', help='list all devices. (portaudio only)') + device_lister.add_argument( + '-d', '--device', help='get details of a single device') + device_lister.add_argument( + '-l', '--audio-library', default='libportaudio.so', + help='File name of PortAudio shared library.') + device_lister.set_defaults( + input_type=FileType('rb'), + output_type=FileType('wb'), + input=None, + output=None, + verbose=0, + quiet=False, + calibrate=False, + command='lsdev' + ) + if argcomplete: argcomplete.autocomplete(p) @@ -251,7 +273,12 @@ def interface_factory(): args.src = args.input_type(args.input) args.dst = args.output_type(args.output) try: - if args.calibrate is False: + if args.command == 'lsdev': + if args.device: + lsdev.getParticularDevice(interface, int(args.device)) + else: + lsdev.getDevices(interface) + elif args.calibrate is False: args.main(config=config, args=args) else: args.calib(config=config, args=args) diff --git a/amodem/audio.py b/amodem/audio.py index 3b70ff8..330dd95 100644 --- a/amodem/audio.py +++ b/amodem/audio.py @@ -46,11 +46,15 @@ def __exit__(self, *args): s.close() self.call('Terminate') - def recorder(self): - return Stream(self, config=self.config, read=True) + def recorder(self, preferred_device=None): + return Stream( + self, config=self.config, read=True, + preferred_device=preferred_device) - def player(self): - return Stream(self, config=self.config, write=True) + def player(self, preferred_device=None): + return Stream( + self, config=self.config, write=True, + preferred_device=preferred_device) class Stream: @@ -66,7 +70,9 @@ class Parameters(ctypes.Structure): ('hostApiSpecificStreamInfo', ctypes.POINTER(None)) ] - def __init__(self, interface, config, read=False, write=False): + def __init__( + self, interface, config, + read=False, write=False, preferred_device=None): self.interface = interface self.stream = ctypes.POINTER(ctypes.c_void_p)() self.user_data = ctypes.c_void_p(None) @@ -80,11 +86,14 @@ def __init__(self, interface, config, read=False, write=False): write = bool(write) assert read != write # don't support full duplex - direction = 'Input' if read else 'Output' - api_name = f'GetDefault{direction}Device' - index = interface.call(api_name, restype=ctypes.c_int) + # use default device if preferred_device is undefined + if preferred_device is None: + direction = 'Input' if read else 'Output' + api_name = f'GetDefault{direction}Device' + preferred_device = interface.call(api_name, restype=ctypes.c_int) + self.params = Stream.Parameters( - device=index, # choose default device + device=preferred_device, channelCount=1, # mono audio sampleFormat=0x00000008, # 16-bit samples (paInt16) suggestedLatency=self.latency, diff --git a/amodem/lsdev.py b/amodem/lsdev.py new file mode 100644 index 0000000..d2757b1 --- /dev/null +++ b/amodem/lsdev.py @@ -0,0 +1,39 @@ +from ctypes import * + + +class PADeviceInfo(Structure): + _fields_ = [ + ("structVersion", c_int), + ("name", c_char_p), + ("hostApi", c_int), + ("maxInputChannels", c_int), + ("maxOutputChannels", c_int), + ("defaultLowInputLatency", c_double), + ("defaultLowOutputLatency", c_double), + ("defaultHighInputLatency", c_double), + ("defaultHighOutputLatency", c_double), + ("defaultSampleRate", c_double) + ] + + +def printDeviceDetails(device): + contents = device.contents + print(str(contents.name, 'ascii')) + print(f"Input Channels: {contents.maxInputChannels}") + print(f"Output Channels: {contents.maxOutputChannels}") + print(f"Default Sample Rate: {contents.defaultSampleRate}") + + +def getDevices(interface): + num_devices = interface.call('GetDeviceCount', restype=c_int) + for i in range(num_devices): + device_i = interface.call( + 'GetDeviceInfo', i, restype=POINTER(PADeviceInfo)) + device_str = str(device_i.contents.name, 'ascii') + print(f"Device #{i}: {device_str}") + + +def getParticularDevice(interface, device_id): + device = interface.call( + 'GetDeviceInfo', device_id, restype=POINTER(PADeviceInfo)) + printDeviceDetails(device) diff --git a/setup.py b/setup.py index 4b9cf05..b8eb62a 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ from setuptools import setup from setuptools.command.test import test as TestCommand + class PyTest(TestCommand): def finalize_options(self): @@ -13,6 +14,7 @@ def run_tests(self): import pytest sys.exit(pytest.main(['.'])) + setup( name='amodem', version='1.15.4',