Compare commits

...

34 Commits

Author SHA1 Message Date
Roman Zeyde
a8e8bbbf02 Bump version: 1.15.4 → 1.15.5 2024-10-21 21:59:40 +03:00
Roman Zeyde
d015118d76 Update supported Python versions 2024-10-21 21:59:26 +03:00
Roman Zeyde
9410a988b1 Test on Python 3.13 2024-10-21 21:53:39 +03:00
Roman Zeyde
2a03d9f72c Fix a small pylint issue 2024-10-21 21:53:39 +03:00
Roman Zeyde
2925becfac Test on Python 3.12 (#67) 2023-11-11 21:26:03 +02:00
Roman Zeyde
30d6af680a Bump CI actions (#68) 2023-11-11 21:09:28 +02:00
Roman Zeyde
3284447d1c Fix division by 0 during calibration 2023-08-19 14:58:01 +03:00
Roman Zeyde
8557faba6e Fix lint nits 2023-07-15 21:48:13 +03:00
Roman Zeyde
d8059b5ff0 Require the user to specify send/recv subcommand 2023-07-15 21:40:01 +03:00
Roman Zeyde
250713a853 Bump version: 1.15.3 → 1.15.4 2022-12-25 21:45:15 +02:00
Roman Zeyde
f4473145c3 Merge pull request #60 from romanz/fix-test
Fix pytest usage
2022-12-24 21:54:47 +02:00
Roman Zeyde
bb68779d75 Update supported Python versions 2022-12-24 21:25:13 +02:00
Roman Zeyde
2452ed56e8 Fix pytest usage
0c5ba9480b (r93932858)
2022-12-24 21:23:05 +02:00
Roman Zeyde
55b446e362 Merge pull request #59 from mgorny/np-bool
Replace deprecated `np.bool` to fix NumPy 1.24 compatibility
2022-12-24 21:20:55 +02:00
Michał Górny
a99fb6d083 Replace deprecated np.bool to fix NumPy 1.24 compatibility
Replace the deprecated `np.bool` type with `bool`.  According
to the documentation, the former "has been an alias of the builtin"
"for a long time".  Using these aliases has been deprecated
in NumPy 1.20, and they were removed entirely in 1.24.

See:
https://numpy.org/doc/stable/release/1.20.0-notes.html#using-the-aliases-of-builtin-types-like-np-int-is-deprecated
https://numpy.org/doc/stable/release/1.24.0-notes.html#expired-deprecations
2022-12-22 15:55:07 +01:00
Roman Zeyde
0c5ba9480b Fix py.test in CI 2022-10-28 10:11:59 +03:00
Roman Zeyde
034a2c9ebe No need to specify encoding in binary I/O 2022-10-27 21:51:07 +03:00
Roman Zeyde
8553f74f88 Use f-string interpolation 2022-10-27 21:47:02 +03:00
Roman Zeyde
ffeda35d49 Support Python 3.10 2022-10-27 21:18:37 +03:00
Roman Zeyde
883ecb4334 Bump version: 1.15.2 → 1.15.3 2021-07-20 22:11:58 +03:00
Roman Zeyde
96b87ec5be A few more fixes after dropping Python 2 support 2021-06-19 21:45:54 +03:00
Roman Zeyde
ceffe7fac0 Drop six dependency 2021-06-19 21:41:38 +03:00
Roman Zeyde
e8cf356248 Drop Python 3.5 and test on 3.9 2021-06-19 21:38:14 +03:00
Roman Zeyde
ee5c543737 Bump version: 1.15.1 → 1.15.2 2021-06-19 19:38:26 +03:00
Roman Zeyde
1311c58005 Ignore 'consider-using-with' pylint warning 2021-06-19 14:31:38 +03:00
Roman Zeyde
9c6ef2884e Drop izip compatibility helper 2021-06-19 14:12:26 +03:00
Roman Zeyde
21fe42e68d Increase leading and trailing silence defaults
Leading silence should help ignore initial A/D artifacts.
Trailing silence fixes buffering issues (e.g. #49).
2021-06-19 14:07:01 +03:00
Roman Zeyde
9352088389 Bump version: 1.15.0 → 1.15.1 2020-07-03 14:56:12 +03:00
Roman Zeyde
db47cda390 Remove travis, coveralls, landscape & waffle badges 2020-07-03 14:55:12 +03:00
Roman Zeyde
4cd3def507 Fix lint errors and adapt to latest Python 2020-07-03 14:50:59 +03:00
Roman Zeyde
9192a8ff67 Switch to GitHub actions 2020-07-03 14:23:12 +03:00
Roman Zeyde
3b88a23dfb Document PortAudio DLL usage on Windows 2020-07-03 13:16:10 +03:00
Roman Zeyde
10c06f7646 Ignore .pytest_cache/ 2019-03-08 15:11:47 +02:00
Roman Zeyde
df9cbfdf13 Allow more silence before transmission 2019-03-08 15:10:02 +02:00
23 changed files with 117 additions and 112 deletions

View File

@@ -1,7 +1,6 @@
[bumpversion]
commit = True
tag = True
current_version = 1.15.0
current_version = 1.15.5
[bumpversion:file:setup.py]

36
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip setuptools
pip install pytest pytest-cov mock pycodestyle coverage pylint
pip install -e .
- name: Lint
run: |
pycodestyle amodem/ scripts/
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
- name: Test with pytest
run: |
pytest -v --cov=amodem

1
.gitignore vendored
View File

@@ -26,3 +26,4 @@ htmlcov
/dist
/deb_dist
*.html
.pytest_cache/

View File

@@ -1,2 +1,2 @@
[MESSAGES CONTROL]
disable=invalid-name, missing-docstring, too-many-instance-attributes, too-few-public-methods, logging-format-interpolation
disable=invalid-name, missing-docstring, too-many-instance-attributes, too-few-public-methods, logging-format-interpolation, consider-using-with

View File

@@ -1,20 +0,0 @@
sudo: false
language: python
python:
- "3.5"
- "3.6"
- "3.7-dev"
install:
- pip install .
- pip install pytest>=2.7.3 --upgrade
- pip install coveralls pycodestyle mock
script:
- pycodestyle amodem/ scripts/
- echo "Hello World!" | amodem send -vv -l- -o- | amodem recv -vv -l- -i-
- coverage run --source=amodem --omit="*/__main__.py" -m py.test -vvs
after_success:
- coverage report
- coveralls

View File

@@ -1,16 +1,6 @@
Audio Modem Communication Library
=================================
.. image:: https://travis-ci.org/romanz/amodem.svg?branch=master
:target: https://travis-ci.org/romanz/amodem
:alt: Build Status
.. image:: https://coveralls.io/repos/romanz/amodem/badge.svg?branch=master
:target: https://coveralls.io/r/romanz/amodem?branch=master
:alt: Code Coverage
.. image:: https://landscape.io/github/romanz/amodem/master/landscape.svg?style=flat
:target: https://landscape.io/github/romanz/amodem/master
:alt: Code Health
.. image:: https://img.shields.io/pypi/pyversions/amodem.svg
:target: https://pypi.python.org/pypi/amodem/
:alt: Python Versions
@@ -24,9 +14,6 @@ Audio Modem Communication Library
:target: https://pypi.python.org/pypi/amodem/
:alt: Development Status
.. image:: https://badge.waffle.io/romanz/amodem.svg?label=ready&title=ready
:target: https://waffle.io/romanz/amodem
:alt: 'Ready'
Description
-----------
@@ -98,6 +85,12 @@ For validation, run::
optional arguments:
-h, --help show this help message and exit
On, Windows you may download the `portaudio` library from `MinGW <https://packages.msys2.org/base/mingw-w64-portaudio>`_.
Then, you should specify the DLL using the following command-line flag::
-l AUDIO_LIBRARY, --audio-library AUDIO_LIBRARY
File name of PortAudio shared library.
Calibration
-----------

View File

@@ -15,11 +15,6 @@ from . import main
from .config import bitrates
# Python 3 has `buffer` attribute for byte-based I/O
_stdin = getattr(sys.stdin, 'buffer', sys.stdin)
_stdout = getattr(sys.stdout, 'buffer', sys.stdout)
try:
import argcomplete
except ImportError:
@@ -83,21 +78,21 @@ def FileType(mode, interface_factory=None):
if fname == '-':
if 'r' in mode:
return _stdin
return sys.stdin.buffer
if 'w' in mode:
return _stdout
return sys.stdout.buffer
return open(fname, mode)
return open(fname, mode) # pylint: disable=unspecified-encoding
return opener
def get_volume_cmd(args):
volume_controllers = [
dict(test='pactl --version',
send='pactl set-sink-volume @DEFAULT_SINK@',
recv='pactl set-source-volume @DEFAULT_SOURCE@')
]
volume_controllers = [{
'test': 'pactl --version',
'send': 'pactl set-sink-volume @DEFAULT_SINK@',
'recv': 'pactl set-source-volume @DEFAULT_SOURCE@'
}]
if args.calibrate == 'auto':
for c in volume_controllers:
if os.system(c['test']) == 0:
@@ -111,7 +106,7 @@ def wrap(cls, stream, enable):
def create_parser(description, interface_factory):
p = argparse.ArgumentParser(description=description)
subparsers = p.add_subparsers()
subparsers = p.add_subparsers(required=True)
# Modulator
sender = subparsers.add_parser(
@@ -124,10 +119,13 @@ def create_parser(description, interface_factory):
sender.add_argument(
'-g', '--gain', type=float, default=1.0,
help='Modulator gain (defaults to 1)')
sender.add_argument(
'--silence', type=float, default=0.0,
help='Extra silence before sending the data (in seconds)')
sender.set_defaults(
main=lambda config, args: main.send(
config, src=wrap(Compressor, args.src, args.zlib), dst=args.dst,
gain=args.gain
gain=args.gain, extra_silence=args.silence
),
calib=lambda config, args: calib.send(
config=config, dst=args.dst,
@@ -198,6 +196,8 @@ def _version():
def _config_log(args):
level = fmt = None
if args.verbose == 0:
level, fmt = 'INFO', '%(message)s'
elif args.verbose == 1:
@@ -233,11 +233,11 @@ def _main():
args.pylab = None
if getattr(args, 'plot', False):
import pylab # pylint: disable=import-error
import pylab # pylint: disable=import-error,import-outside-toplevel
args.pylab = pylab
if args.audio_library == 'ALSA':
from . import alsa
from . import alsa # pylint: disable=import-outside-toplevel
interface = alsa.Interface(config)
elif args.audio_library == '-':
interface = _Dummy() # manually disable PortAudio

View File

@@ -3,7 +3,7 @@
import logging
import threading
import six # since `Queue` module was renamed to `queue` (in Python 3)
from queue import Queue
log = logging.getLogger()
@@ -11,7 +11,7 @@ log = logging.getLogger()
class AsyncReader:
def __init__(self, stream, bufsize):
self.stream = stream
self.queue = six.moves.queue.Queue()
self.queue = Queue()
self.stop = threading.Event()
args = (stream, bufsize, self.queue, self.stop)
self.thread = threading.Thread(target=AsyncReader._thread,
@@ -29,7 +29,7 @@ class AsyncReader:
queue.put(buf)
total += len(buf)
log.debug('AsyncReader thread stopped (read %d bytes)', total)
except BaseException:
except BaseException: # pylint: disable=broad-except
log.exception('AsyncReader thread failed')
queue.put(None)

View File

@@ -7,6 +7,10 @@ import time
log = logging.getLogger(__name__)
class AudioError(Exception):
pass
class Interface:
def __init__(self, config, debug=False):
self.debug = bool(debug)
@@ -26,7 +30,7 @@ class Interface:
def call(self, name, *args, **kwargs):
assert self.lib is not None
func_name = 'Pa_{0}'.format(name)
func_name = f'Pa_{name}'
if self.debug:
log.debug('API: %s%s', name, args)
func = getattr(self.lib, func_name)
@@ -35,7 +39,7 @@ class Interface:
def _error_check(self, res):
if res != 0:
raise Exception(res, self._error_string(res))
raise AudioError(res, self._error_string(res))
def __enter__(self):
self.call('Initialize')
@@ -81,7 +85,7 @@ class Stream:
assert read != write # don't support full duplex
direction = 'Input' if read else 'Output'
api_name = 'GetDefault{0}Device'.format(direction)
api_name = f'GetDefault{direction}Device'
index = interface.call(api_name, restype=ctypes.c_int)
self.params = Stream.Parameters(
device=index, # choose default device

View File

@@ -18,7 +18,7 @@ def volume_controller(cmd):
def controller(level):
assert 0 < level <= 1
percent = 100 * level
args = '{0} {1:.0f}%'.format(cmd, percent)
args = f'{cmd} {percent:.0f}%'
log.debug('Setting volume to %7.3f%% -> "%s"', percent, args)
subprocess.check_call(args=args, shell=True)
return controller if cmd else (lambda level: None)
@@ -66,19 +66,19 @@ def detector(config, src, frame_length=200):
max_index = np.argmax(coeffs)
freq = config.frequencies[max_index]
rms = abs(coeffs[max_index])
coherency = rms / total
coherency = rms / total if total > 0 else 0.0
flags = [total > 0.1, peak < 1.0, coherency > 0.99]
success = all(flags)
if success:
msg = 'good signal'
else:
msg = 'too {0} signal'.format(errors[flags.index(False)])
msg = f'too {errors[flags.index(False)]} signal'
yield dict(
freq=freq, rms=rms, peak=peak, coherency=coherency,
total=total, success=success, msg=msg
)
yield {
'freq': freq, 'rms': rms, 'peak': peak, 'coherency': coherency,
'total': total, 'success': success, 'msg': msg
}
def volume_calibration(result_iterator, volume_ctl):
@@ -107,8 +107,7 @@ def volume_calibration(result_iterator, volume_ctl):
def iter_window(iterable, size):
# pylint: disable=stop-iteration-return
block = []
while True:
item = next(iterable)
for item in iterable:
block.append(item)
block = block[-size:]
if len(block) == size:
@@ -136,7 +135,7 @@ def recv(config, src, verbose=False, volume_cmd=None, dump_audio=None):
log.info('verbose: %s', verbose)
if verbose:
fields = ['total', 'rms', 'coherency', 'peak']
fmt += ', '.join('{0}={{{0}:.4f}}'.format(f) for f in fields)
fmt += ', '.join(f'{f}={{{f}:.4f}}' for f in fields)
for state in recv_iter(config, src, volume_cmd, dump_audio):
log.info(fmt.format(**state))

View File

@@ -73,14 +73,6 @@ def take(iterable, n):
return np.array(list(itertools.islice(iterable, n)))
def izip(iterables):
""" "Python 3" zip re-implementation for Python 2. """
# pylint: disable=stop-iteration-return
iterables = [iter(iterable) for iterable in iterables]
while True:
yield tuple([next(iterable) for iterable in iterables])
class Dummy:
""" Dummy placeholder object for testing and mocking. """

View File

@@ -14,8 +14,8 @@ class Configuration:
latency = 0.1
# sender config
silence_start = 0.25
silence_stop = 0.25
silence_start = 0.5
silence_stop = 0.5
# receiver config
skip_start = 0.1

View File

@@ -113,5 +113,5 @@ class Detector:
freq_err = a / (self.Tsym * self.freq)
log.info('Frequency error: %.3f ppm', freq_err * 1e6)
self.plt.title('Frequency drift: {0:.3f} ppm'.format(freq_err * 1e6))
self.plt.title(f'Frequency drift: {freq_err * 1e6:.3f} ppm')
return amplitude, freq_err

View File

@@ -79,7 +79,7 @@ class MODEM:
symbols = np.array(list(symbols))
bits_per_symbol = np.log2(len(symbols))
bits_per_symbol = np.round(bits_per_symbol)
N = (2 ** bits_per_symbol)
N = 2 ** bits_per_symbol
assert N == len(symbols)
bits_per_symbol = int(bits_per_symbol)

View File

@@ -10,8 +10,7 @@ log = logging.getLogger(__name__)
def _checksum_func(x):
''' The result will be unsigned on Python 2/3. '''
return binascii.crc32(bytes(x)) & 0xFFFFFFFF
return binascii.crc32(bytes(x))
class Checksum:
@@ -29,8 +28,7 @@ class Checksum:
if received != expected:
log.warning('Invalid checksum: %08x != %08x', received, expected)
raise ValueError('Invalid checksum')
else:
log.debug('Good checksum: %08x', received)
log.debug('Good checksum: %08x', received)
return payload

View File

@@ -10,12 +10,12 @@ from . import framing, common, stream, detect, sampling
log = logging.getLogger(__name__)
def send(config, src, dst, gain=1.0):
def send(config, src, dst, gain=1.0, extra_silence=0.0):
sender = _send.Sender(dst, config=config, gain=gain)
Fs = config.Fs
# pre-padding audio with silence (priming the audio sending queue)
sender.write(np.zeros(int(Fs * config.silence_start)))
sender.write(np.zeros(int(Fs * (config.silence_start + extra_silence))))
sender.start()
@@ -63,7 +63,7 @@ def recv(config, src, dst, dump_audio=None, pylab=None):
freq=freq)
receiver.run(sampler, gain=1.0/amplitude, output=dst)
return True
except BaseException:
except BaseException: # pylint: disable=broad-except
log.exception('Decoding failed')
return False
finally:

View File

@@ -43,9 +43,9 @@ class Receiver:
self.plt.subplot(1, 2, 2)
self.plt.plot(np.abs(S))
self.plt.plot(equalizer.prefix)
errors = (bits != equalizer.prefix)
errors = bits != equalizer.prefix
if any(errors):
msg = 'Incorrect prefix: {0} errors'.format(sum(errors))
msg = f'Incorrect prefix: {sum(errors)} errors'
raise ValueError(msg)
log.debug('Prefix OK')
@@ -81,7 +81,7 @@ class Receiver:
equalizer_length = equalizer.equalizer_length
symbols = self.equalizer.demodulator(equalized, equalizer_length)
sliced = np.array(symbols).round()
errors = np.array(sliced - train_symbols, dtype=np.bool)
errors = np.array(sliced - train_symbols, dtype=bool)
error_rate = errors.sum() / errors.size
errors = np.array(symbols - train_symbols)
@@ -94,7 +94,7 @@ class Receiver:
for (i, freq), snr in zip(enumerate(self.frequencies), SNRs):
log.debug('%5.1f kHz: SNR = %5.2f dB', freq / 1e3, snr)
self._constellation(symbols[:, i], train_symbols[:, i],
'$F_c = {0} Hz$'.format(freq), index=i)
f'$F_c = {freq} Hz$', index=i)
assert error_rate == 0, error_rate
log.debug('Training verified')
@@ -111,7 +111,7 @@ class Receiver:
bits = self.modem.decode(S, freq_handler) # list of bit tuples
streams.append(bits) # bit stream per frequency
return common.izip(streams), symbol_list
return zip(*streams), symbol_list
def _demodulate(self, sampler, symbols):
symbol_list = []
@@ -188,7 +188,7 @@ class Receiver:
symbol_list = np.array(self.stats['symbol_list'])
for i, freq in enumerate(self.frequencies):
self._constellation(symbol_list[i], self.modem.symbols,
'$F_c = {0} Hz$'.format(freq), index=i)
f'$F_c = {freq} Hz$', index=i)
self.plt.show()
def _constellation(self, y, symbols, title, index=None):

View File

@@ -5,7 +5,7 @@ class Reader:
wait = 0.2
timeout = 2.0
bufsize = (8 << 10)
bufsize = 8 << 10
def __init__(self, fd, data_type=None, eof=False):
self.fd = fd

View File

@@ -48,12 +48,6 @@ def test_dumps_loads():
assert all(x == y)
def test_izip():
x = range(10)
y = range(-10, 0)
assert list(common.izip([x, y])) == list(zip(x, y))
def test_configs():
default = config.Configuration()
fastest = config.fastest()

View File

@@ -1,5 +1,6 @@
from amodem import stream
import subprocess as sp
import sys
script = br"""
import sys
@@ -14,7 +15,7 @@ while True:
def test_read():
p = sp.Popen(args=['python', '-'], stdin=sp.PIPE, stdout=sp.PIPE)
p = sp.Popen(args=[sys.executable, '-'], stdin=sp.PIPE, stdout=sp.PIPE)
p.stdin.write(script)
p.stdin.close()
f = stream.Reader(p.stdout)

View File

@@ -49,13 +49,18 @@ def run(size, chan=None, df=0, success=True, cfg=None):
assert rx_data == tx_data
@pytest.fixture(params=[0, 1, 3, 10, 42, 123])
@pytest.fixture(params=[0, 1, 3, 10, 16, 17, 42, 123])
def small_size(request):
return request.param
def test_small(small_size):
run(small_size, chan=lambda x: x)
@pytest.fixture(params=list(config.bitrates.values()))
def all_configs(request):
return request.param
def test_small(small_size, all_configs):
run(small_size, chan=lambda x: x, cfg=all_configs)
def test_flip():

View File

@@ -15,7 +15,7 @@ class PyTest(TestCommand):
setup(
name='amodem',
version='1.15.0',
version='1.15.5',
description='Audio Modem Communication Library',
author='Roman Zeyde',
author_email='dev@romanzey.de',
@@ -24,7 +24,7 @@ setup(
packages=['amodem'],
tests_require=['pytest'],
cmdclass={'test': PyTest},
install_requires=['numpy', 'six'],
install_requires=['numpy'],
platforms=['POSIX'],
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -32,9 +32,13 @@ setup(
'Intended Audience :: Information Technology',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',

View File

@@ -7,9 +7,8 @@ deps=
pycodestyle
coverage
pylint
six
commands=
pycodestyle amodem/ scripts/
pylint --extension-pkg-whitelist=numpy --reports=no amodem --rcfile .pylintrc
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
coverage run --source amodem/ --omit="*/__main__.py" -m pytest -v
coverage report