mirror of
https://github.com/romanz/amodem.git
synced 2026-03-17 07:05:59 +08:00
switch to PyAudio package for portability
This commit is contained in:
@@ -8,8 +8,7 @@ python:
|
||||
|
||||
install:
|
||||
- pip install .
|
||||
- pip install coveralls
|
||||
- pip install pep8
|
||||
- pip install coveralls pep8 mock
|
||||
|
||||
script:
|
||||
- pep8 amodem/ scripts/ tests/ amodem-cli
|
||||
|
||||
61
amodem-cli
61
amodem-cli
@@ -19,7 +19,7 @@ import logging
|
||||
|
||||
log = logging.getLogger('__name__')
|
||||
|
||||
from amodem import recv, send, audio, calib
|
||||
from amodem import recv, send, calib, audio
|
||||
from amodem.config import bitrates
|
||||
|
||||
null = open('/dev/null', 'wb')
|
||||
@@ -29,18 +29,18 @@ bitrate = os.environ.get('BITRATE', 1)
|
||||
config = bitrates.get(int(bitrate))
|
||||
|
||||
|
||||
def FileType(mode, process=None):
|
||||
def FileType(mode, audio_interface=None):
|
||||
def opener(fname):
|
||||
assert 'r' in mode or 'w' in mode
|
||||
if process is None and fname is None:
|
||||
if audio_interface is None and fname is None:
|
||||
fname = '-'
|
||||
|
||||
if fname is None:
|
||||
assert process is not None
|
||||
assert audio_interface is not None
|
||||
if 'r' in mode:
|
||||
return process.launch(stdout=audio.sp.PIPE, stderr=null).stdout
|
||||
return audio_interface.recorder()
|
||||
if 'w' in mode:
|
||||
return process.launch(stdin=audio.sp.PIPE, stderr=null).stdin
|
||||
return audio_interface.player()
|
||||
|
||||
if fname == '-':
|
||||
if 'r' in mode:
|
||||
@@ -58,6 +58,7 @@ def main():
|
||||
'Fs={3:.1f} kHz')
|
||||
description = fmt.format(config.modem_bps / 1e3, len(config.symbols),
|
||||
config.Nfreq, config.Fs / 1e3)
|
||||
interface = audio.Interface(config)
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
subparsers = p.add_subparsers()
|
||||
|
||||
@@ -71,13 +72,11 @@ def main():
|
||||
' if not specified, `aplay` tool will be used.')
|
||||
sender.add_argument(
|
||||
'-c', '--calibrate', default=False, action='store_true')
|
||||
sender.add_argument(
|
||||
'-w', '--wave', default=False, action='store_true')
|
||||
|
||||
sender.set_defaults(
|
||||
main=run_send,
|
||||
main=send.main, calibration=calib.send,
|
||||
input_type=FileType('rb'),
|
||||
output_type=FileType('wb', audio.play(Fs=config.Fs))
|
||||
output_type=FileType('wb', interface)
|
||||
)
|
||||
|
||||
# Demodulator
|
||||
@@ -90,14 +89,12 @@ def main():
|
||||
'-o', '--output', help='output file (use "-" for stdout).')
|
||||
receiver.add_argument(
|
||||
'-c', '--calibrate', default=False, action='store_true')
|
||||
receiver.add_argument(
|
||||
'-w', '--wave', default=False, action='store_true')
|
||||
receiver.add_argument(
|
||||
'--plot', action='store_true', default=False,
|
||||
help='plot results using pylab module')
|
||||
receiver.set_defaults(
|
||||
main=run_recv,
|
||||
input_type=FileType('rb', audio.record(Fs=config.Fs)),
|
||||
main=recv.main, calibration=calib.recv,
|
||||
input_type=FileType('rb', interface),
|
||||
output_type=FileType('wb')
|
||||
)
|
||||
|
||||
@@ -125,43 +122,15 @@ def main():
|
||||
log.debug(description)
|
||||
if getattr(args, 'plot', False):
|
||||
import pylab
|
||||
args.plot = pylab
|
||||
args.config = config
|
||||
args.main(args)
|
||||
else:
|
||||
pylab = None
|
||||
|
||||
|
||||
def join_process(process):
|
||||
exitcode = 0
|
||||
try:
|
||||
exitcode = process.wait()
|
||||
except KeyboardInterrupt:
|
||||
process.kill()
|
||||
exitcode = process.wait()
|
||||
sys.exit(exitcode)
|
||||
|
||||
|
||||
def run_modem(args, main_func, **kwargs):
|
||||
src = args.input_type(args.input)
|
||||
dst = args.output_type(args.output)
|
||||
main_func(args.config, src, dst, **kwargs)
|
||||
|
||||
|
||||
def run_send(args):
|
||||
if args.calibrate:
|
||||
calib.send(config=config, verbose=args.verbose)
|
||||
elif args.wave:
|
||||
join_process(audio.play(Fs=config.Fs).launch(fname=args.input))
|
||||
args.calibration(config=config, src=src, dst=dst, verbose=args.verbose)
|
||||
else:
|
||||
run_modem(args, send.main)
|
||||
|
||||
|
||||
def run_recv(args):
|
||||
if args.calibrate:
|
||||
calib.recv(config=config, verbose=args.verbose)
|
||||
elif args.wave:
|
||||
join_process(audio.record(Fs=config.Fs).launch(fname=args.output))
|
||||
else:
|
||||
run_modem(args, recv.main, plt=args.plot)
|
||||
args.main(config=config, src=src, dst=dst, pylab=pylab)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import send, recv, config, audio
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import subprocess as sp
|
||||
import pyaudio
|
||||
import logging
|
||||
import functools
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ALSA(object):
|
||||
def __init__(self, tool, Fs):
|
||||
self.Fs = int(Fs) # sampling rate
|
||||
self.bits_per_sample = 16
|
||||
self.bytes_per_sample = self.bits_per_sample / 8.0
|
||||
self.bytes_per_second = self.bytes_per_sample * self.Fs
|
||||
# PCM signed little endian
|
||||
self.audio_format = 'S{0}_LE'.format(self.bits_per_sample)
|
||||
self.audio_tool = tool
|
||||
class Interface(object):
|
||||
def __init__(self, config, library=pyaudio):
|
||||
self.p = library.PyAudio()
|
||||
format = getattr(library, 'paInt{0}'.format(config.bits_per_sample))
|
||||
self.sample_size = config.sample_size
|
||||
self.kwargs = dict(
|
||||
channels=1, rate=int(config.Fs), format=format,
|
||||
frames_per_buffer=config.samples_per_buffer
|
||||
)
|
||||
|
||||
def launch(self, fname=None, **kwargs):
|
||||
if fname is None:
|
||||
fname = '-' # use stdin/stdout if filename not specified
|
||||
args = [self.audio_tool, fname, '-q',
|
||||
'-f', self.audio_format,
|
||||
'-c', '1',
|
||||
'-r', str(self.Fs)]
|
||||
log.debug('Running: %r', ' '.join(args))
|
||||
p = sp.Popen(args=args, **kwargs)
|
||||
return p
|
||||
def __del__(self):
|
||||
self.p.terminate()
|
||||
|
||||
# Use ALSA tools for audio playing/recording
|
||||
play = functools.partial(ALSA, tool='aplay')
|
||||
record = functools.partial(ALSA, tool='arecord')
|
||||
def player(self):
|
||||
return self.p.open(output=True, **self.kwargs)
|
||||
|
||||
def recorder(self):
|
||||
stream = self.p.open(input=True, **self.kwargs)
|
||||
return _Recorder(stream, sample_size=self.sample_size)
|
||||
|
||||
|
||||
class _Recorder(object):
|
||||
def __init__(self, stream, sample_size):
|
||||
self.stream = stream
|
||||
self.sample_size = sample_size
|
||||
|
||||
def read(self, size):
|
||||
assert size % self.sample_size == 0
|
||||
return self.stream.read(size // self.sample_size)
|
||||
|
||||
@@ -3,49 +3,40 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from subprocess import PIPE
|
||||
from . import common
|
||||
from . import audio
|
||||
|
||||
ALLOWED_EXCEPTIONS = (IOError, KeyboardInterrupt)
|
||||
|
||||
|
||||
def send(config, audio_play=audio.play, verbose=False):
|
||||
def send(config, dst, src=None, verbose=False):
|
||||
calibration_symbols = int(1.0 * config.Fs)
|
||||
t = np.arange(0, calibration_symbols) * config.Ts
|
||||
signal = [np.sin(2 * np.pi * f * t) for f in config.frequencies]
|
||||
signal = common.dumps(np.concatenate(signal))
|
||||
|
||||
player = audio_play(Fs=config.Fs)
|
||||
p = player.launch(stdin=PIPE)
|
||||
fd = p.stdin
|
||||
try:
|
||||
while True:
|
||||
fd.write(signal)
|
||||
dst.write(signal)
|
||||
except ALLOWED_EXCEPTIONS:
|
||||
pass
|
||||
finally:
|
||||
p.kill()
|
||||
|
||||
|
||||
def run_recorder(config, recorder):
|
||||
|
||||
FRAME_LENGTH = 200 * config.Nsym
|
||||
process = recorder.launch(stdout=PIPE)
|
||||
frame_size = int(recorder.bytes_per_sample * FRAME_LENGTH)
|
||||
frame_length = 200 * config.Nsym
|
||||
frame_size = frame_length * config.sample_size
|
||||
|
||||
t = np.arange(0, frame_length) * config.Ts
|
||||
|
||||
t = np.arange(0, FRAME_LENGTH) * config.Ts
|
||||
scaling_factor = 0.5 * len(t)
|
||||
carriers = [np.exp(2j * np.pi * f * t) for f in config.frequencies]
|
||||
carriers = np.array(carriers) / scaling_factor
|
||||
|
||||
fd = process.stdout
|
||||
|
||||
states = [True]
|
||||
errors = ['weak', 'strong', 'noisy']
|
||||
try:
|
||||
while True:
|
||||
data = fd.read(frame_size)
|
||||
data = recorder.read(frame_size)
|
||||
if len(data) < frame_size:
|
||||
return
|
||||
data = common.loads(data)
|
||||
@@ -76,20 +67,17 @@ def run_recorder(config, recorder):
|
||||
)
|
||||
except ALLOWED_EXCEPTIONS:
|
||||
pass
|
||||
finally:
|
||||
process.kill()
|
||||
|
||||
fmt = '{freq:6.0f} Hz: {message:s}{extra:s}'
|
||||
fields = ['peak', 'total', 'rms', 'coherency']
|
||||
|
||||
|
||||
def recv(config, audio_record=audio.record, verbose=False):
|
||||
def recv(config, src, dst=None, verbose=False):
|
||||
extra = ''
|
||||
if verbose:
|
||||
extra = ''.join(', {0}={{{0}:.4f}}'.format(f) for f in fields)
|
||||
|
||||
recorder = audio_record(Fs=config.Fs)
|
||||
for result in run_recorder(config=config, recorder=recorder):
|
||||
for result in run_recorder(config=config, recorder=src):
|
||||
msg = fmt.format(extra=extra.format(**result), **result)
|
||||
if not result['error']:
|
||||
log.info(msg)
|
||||
|
||||
@@ -8,6 +8,11 @@ class Configuration(object):
|
||||
Npoints = 64
|
||||
F0 = 1e3
|
||||
|
||||
# audio config
|
||||
bits_per_sample = 16
|
||||
sample_size = bits_per_sample // 8
|
||||
samples_per_buffer = 1024
|
||||
|
||||
# sender config
|
||||
silence_start = 1.0
|
||||
silence_stop = 1.0
|
||||
|
||||
@@ -18,9 +18,9 @@ from . import detect
|
||||
|
||||
class Receiver(object):
|
||||
|
||||
def __init__(self, config, plt=None):
|
||||
def __init__(self, config, pylab=None):
|
||||
self.stats = {}
|
||||
self.plt = plt or common.Dummy()
|
||||
self.plt = pylab or common.Dummy()
|
||||
self.modem = dsp.MODEM(config.symbols)
|
||||
self.frequencies = np.array(config.frequencies)
|
||||
self.omegas = 2 * np.pi * self.frequencies / config.Fs
|
||||
@@ -209,7 +209,7 @@ class Receiver(object):
|
||||
self.plt.title(title)
|
||||
|
||||
|
||||
def main(config, src, dst, plt=None):
|
||||
def main(config, src, dst, pylab=None):
|
||||
reader = stream.Reader(src, data_type=common.loads)
|
||||
signal = itertools.chain.from_iterable(reader)
|
||||
|
||||
@@ -220,7 +220,7 @@ def main(config, src, dst, plt=None):
|
||||
reader.check = common.check_saturation
|
||||
|
||||
detector = detect.Detector(config=config)
|
||||
receiver = Receiver(config=config, plt=plt)
|
||||
receiver = Receiver(config=config, pylab=pylab)
|
||||
success = False
|
||||
try:
|
||||
log.info('Waiting for carrier tone: %.1f kHz', config.Fc / 1e3)
|
||||
|
||||
@@ -52,7 +52,7 @@ class Sender(object):
|
||||
log.debug('Sent %8.1f kB', total_bits / 8e3)
|
||||
|
||||
|
||||
def main(config, src, dst):
|
||||
def main(config, src, dst, pylab=None):
|
||||
sender = Sender(dst, config=config)
|
||||
Fs = config.Fs
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
numpy
|
||||
bitarray
|
||||
argcomplete
|
||||
pyaudio
|
||||
|
||||
2
setup.py
2
setup.py
@@ -25,7 +25,7 @@ setup(
|
||||
packages=['amodem'],
|
||||
tests_require=['pytest'],
|
||||
cmdclass={'test': PyTest},
|
||||
install_requires=['numpy', 'bitarray', 'argcomplete'],
|
||||
install_requires=['numpy', 'bitarray', 'argcomplete', 'pyaudio'],
|
||||
platforms=['POSIX'],
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
from amodem import audio
|
||||
import subprocess as sp
|
||||
import signal
|
||||
from amodem import config
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
def test_launch():
|
||||
p = audio.ALSA(tool='true', Fs=32000).launch()
|
||||
assert p.wait() == 0
|
||||
def test_pyaudio_mock():
|
||||
m = mock.Mock()
|
||||
m.paInt16 = 8
|
||||
m.PyAudio.return_value = m
|
||||
m.open.return_value = m
|
||||
|
||||
cfg = config.fastest()
|
||||
interface = audio.Interface(config=cfg, library=m)
|
||||
recorder = interface.recorder()
|
||||
n = 1024
|
||||
data = recorder.read(n)
|
||||
|
||||
def test_exit():
|
||||
p = audio.ALSA(tool='python', Fs=32000).launch(fname='-', stdin=sp.PIPE)
|
||||
s = b'import sys; sys.exit(42)'
|
||||
p.stdin.write(s)
|
||||
p.stdin.close()
|
||||
assert p.wait() == 42
|
||||
data = '\x00' * n
|
||||
player = interface.player()
|
||||
player.write(data)
|
||||
|
||||
|
||||
def test_io():
|
||||
p = audio.ALSA(tool='python', Fs=32000)
|
||||
p = p.launch(fname='-', stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
s = b'Hello World!'
|
||||
p.stdin.write(b'print("' + s + b'")\n')
|
||||
p.stdin.close()
|
||||
assert p.stdout.read(len(s)) == s
|
||||
|
||||
|
||||
def test_kill():
|
||||
p = audio.ALSA(tool='python', Fs=32000)
|
||||
p = p.launch(fname='-', stdin=sp.PIPE, stdout=sp.PIPE)
|
||||
p.kill()
|
||||
assert p.wait() == -signal.SIGKILL
|
||||
kwargs = dict(
|
||||
channels=1, frames_per_buffer=cfg.samples_per_buffer,
|
||||
rate=cfg.Fs, format=m.paInt16
|
||||
)
|
||||
assert m.mock_calls == [
|
||||
mock.call.PyAudio(),
|
||||
mock.call.open(input=True, **kwargs),
|
||||
mock.call.read(n // cfg.sample_size),
|
||||
mock.call.open(output=True, **kwargs),
|
||||
mock.call.write(data)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user