Compare commits

...

29 Commits

Author SHA1 Message Date
Roman Zeyde
451551d37e Bump version: 1.15.5 → 1.15.6 2024-10-25 14:06:58 +03:00
Roman Zeyde
1d5fb11481 Add amodem.tests to source distribution
Also, fix a few pylint issues.
2024-10-24 21:48:10 +03:00
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
30 changed files with 116 additions and 132 deletions

View File

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

View File

@@ -12,18 +12,18 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install pytest mock pycodestyle coverage pylint six
python -m pip install --upgrade pip setuptools
pip install pytest pytest-cov mock pycodestyle coverage pylint
pip install -e .
- name: Lint
@@ -33,5 +33,4 @@ jobs:
- name: Test with pytest
run: |
coverage run --source amodem/ --omit="*/__main__.py" -m py.test -v
coverage report
pytest -v --cov=amodem

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, redefined-outer-name

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(
@@ -201,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:

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,

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):
@@ -135,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,16 +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. """
iterables = [iter(iterable) for iterable in iterables]
try:
while True:
yield tuple([next(iterable) for iterable in iterables])
except StopIteration:
pass
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:

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

0
amodem/tests/__init__.py Normal file
View File

View File

@@ -1,7 +1,7 @@
from amodem import alsa, config
import mock
from .. import alsa, config
def test_alsa():
interface = alsa.Interface(config=config.fastest())

View File

@@ -1,8 +1,11 @@
import mock
import time
import pytest
from amodem import async_reader
import logging
import time
import mock
import pytest
from .. import async_reader
logging.basicConfig(format='%(message)s')

View File

@@ -1,8 +1,8 @@
from amodem import audio, config
import mock
import pytest
from .. import audio, config
def test():
length = 1024
@@ -31,4 +31,4 @@ def test():
s.close()
with pytest.raises(Exception):
interface._error_check(1)
interface._error_check(1) # pylint: disable=protected-access

View File

@@ -1,13 +1,12 @@
from amodem import calib
from amodem import common
from amodem import config
from io import BytesIO
import numpy as np
import random
import pytest
import mock
import numpy as np
import pytest
from .. import calib, common, config
config = config.fastest()

View File

@@ -1,7 +1,7 @@
from amodem import common
from amodem import config
import numpy as np
from .. import common, config
def iterlist(x, *args, **kwargs):
x = np.array(x)
@@ -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,4 +1,4 @@
from amodem import config
from .. import config
def test_bitrates():

View File

@@ -1,13 +1,8 @@
import numpy as np
import pytest
from amodem import dsp
from amodem import recv
from amodem import detect
from amodem import equalizer
from amodem import sampling
from amodem import config
from amodem import common
from .. import common, config, detect, dsp, equalizer, recv, sampling
config = config.fastest()
@@ -17,7 +12,7 @@ def test_detect():
x = np.cos(2 * np.pi * config.Fc * t)
detector = detect.Detector(config, pylab=common.Dummy())
samples, amp, freq_err = detector.run(x)
_samples, amp, freq_err = detector.run(x)
assert abs(1 - amp) < 1e-12
assert abs(freq_err) < 1e-12
@@ -39,11 +34,11 @@ def test_prefix():
sampler = sampling.Sampler(signal)
return dsp.Demux(sampler=sampler, omegas=[omega], Nsym=config.Nsym)
r = recv.Receiver(config, pylab=common.Dummy())
r._prefix(symbols_stream(signal))
r._prefix(symbols_stream(signal)) # pylint: disable=protected-access
with pytest.raises(ValueError):
silence = 0 * signal
r._prefix(symbols_stream(silence))
r._prefix(symbols_stream(silence)) # pylint: disable=protected-access
def test_find_start():

View File

@@ -1,12 +1,11 @@
from amodem import dsp
from amodem import sampling
from amodem import config
import utils
import numpy as np
import random
import itertools
import numpy as np
from .. import dsp, sampling, config
from . import utils
config = config.fastest()
@@ -69,7 +68,7 @@ def quantize(q, s):
def test_overflow():
q = dsp.MODEM(config.symbols)
r = np.random.RandomState(seed=0)
for i in range(10000):
for _ in range(10000):
s = 10*(r.normal() + 1j * r.normal())
quantize(q, s)

View File

@@ -1,10 +1,9 @@
from numpy.random import RandomState
import numpy as np
import utils
from amodem import equalizer
from amodem import dsp
from amodem import config
from . import utils
from .. import config, dsp, equalizer
config = config.fastest()

View File

@@ -1,9 +1,10 @@
from amodem import framing
import random
import itertools
import random
import pytest
from .. import framing
def concat(iterable):
return bytearray(itertools.chain.from_iterable(iterable))

View File

@@ -1,8 +1,8 @@
from amodem import sampling
from amodem import common
from io import BytesIO
import numpy as np
from io import BytesIO
from .. import common, sampling
def test_resample():

View File

@@ -1,7 +1,8 @@
from amodem import stream
import subprocess as sp
import sys
from .. import stream
script = br"""
import sys
import time

View File

@@ -1,15 +1,13 @@
from amodem import main
from amodem import common
from amodem import sampling
from amodem import config
import utils
from io import BytesIO
import logging
import os
import numpy as np
import os
from io import BytesIO
import pytest
import logging
from .. import common, config, main, sampling
from . import utils
logging.basicConfig(level=logging.DEBUG, # useful for debugging
format='%(asctime)s %(levelname)-12s %(message)s')
@@ -49,13 +47,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

@@ -1,5 +1,5 @@
#!/usr/bin/env python
from setuptools import setup
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
class PyTest(TestCommand):
@@ -15,16 +15,16 @@ class PyTest(TestCommand):
setup(
name='amodem',
version='1.15.1',
version='1.15.6',
description='Audio Modem Communication Library',
author='Roman Zeyde',
author_email='dev@romanzey.de',
license='MIT',
url='http://github.com/romanz/amodem',
packages=['amodem'],
packages=find_packages(),
tests_require=['pytest'],
cmdclass={'test': PyTest},
install_requires=['numpy', 'six'],
install_requires=['numpy'],
platforms=['POSIX'],
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -32,10 +32,13 @@ setup(
'Intended Audience :: Information Technology',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX',
'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