Skip to content

Commit 7744c7b

Browse files
authored
Merge pull request #4 from zeyus/feature/modernize-bluebox
Started making a modularized version with easily swapable backends, e…
2 parents 2a5688e + 7a330f6 commit 7744c7b

23 files changed

+1352
-138
lines changed

.github/workflows/codetest.yml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Lint, test and notify
2+
3+
on: [push]
4+
5+
# env:
6+
# PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/nlp4all:$PYTHONPATH
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python-version: ["3.9", "3.10", "3.11"]
14+
steps:
15+
- uses: actions/checkout@v3
16+
- name: Set up Python ${{ matrix.python-version }}
17+
uses: actions/setup-python@v4
18+
id: cpmat
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
cache: 'pip'
22+
- name: Install dependencies
23+
run: |
24+
sudo apt-get update
25+
sudo apt-get install -y libasound-dev portaudio19-dev libportaudio2 libportaudiocpp0
26+
echo "PYTHONPATH=${{ github.workspace }}" >> $GITHUB_ENV
27+
python -m pip install --progress-bar off --upgrade pip
28+
pip install --progress-bar off wheel
29+
pip install --progress-bar off -r requirements-dev.txt
30+
- name: Print python os env for debugging, and pip freeze
31+
run: |
32+
printenv
33+
pip freeze
34+
- name: Analysing the code with Flake8
35+
run: |
36+
flake8
37+
# figure out why mypy doesn't work on github later
38+
- name: Type checking with mypy
39+
run: |
40+
mypy
41+
- name: Run tests with pytest
42+
run: |
43+
pytest

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
__pycache__/
2+
.pytest_cache/
3+
.vscode/
4+
*.pyc
5+
*.pyo
6+
*.pyd

LICENSE.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) 2023 zeyus
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

PythonBlueBox.py

-82
This file was deleted.

README.md

+26-56
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,45 @@
11
Python 3 Blue Box DTMF Tone Generator
22
===================
33

4-
Really simple, functional tone generator
4+
Modernized python blue box
55

66
**How it works**
77

8-
Just type on a sequence of numbers/commands and press enter, your computer will play it back for you!
8+
You can run this in interactive mode, or from a file, pipe, stdin, etc.
99

1010
**Requirements**
1111

12-
- PyAudio https://people.csail.mit.edu/hubert/pyaudio/
12+
- Python 3 (tested on 3.9 - 3.11)
1313

14-
```pip3 install pyaudio```
14+
**Installation**
1515

16-
For OSX you need port audio, which you can install from homebrew:
17-
18-
```brew install portaudio```
16+
```
17+
git clone https://github.com/zeyus/Python3BlueBox.git
18+
cd PythonBlueBox
19+
pip install -r requirements.txt
20+
```
1921

20-
For linux or windows, see your package manager, google it or visit here:
22+
**Usage**
2123

22-
http://www.portaudio.com/download.html
24+
```
25+
./bluebox.py -h
26+
```
2327

24-
**Usage**
28+
**Examples**
2529

2630
```
27-
./PythonBlueBox.py
31+
./bluebox.py -i
2832
```
2933

30-
You'll see the '>>>' prompt.
31-
32-
```>>> U12345O12345```
33-
34-
You can use the 'U' to switch to user tones or 'O' to switch to operator tones
35-
36-
The current tone mapping works as follows:
37-
38-
```python
39-
user_tones = {
40-
'1',
41-
'2',
42-
'3',
43-
'A',
44-
'4',
45-
'5',
46-
'6',
47-
'B',
48-
'7',
49-
'8',
50-
'9',
51-
'C',
52-
'*',
53-
'0',
54-
'#',
55-
'D',
56-
}
57-
op_tones = {
58-
'1',
59-
'2',
60-
'3',
61-
'4',
62-
'5',
63-
'6',
64-
'7',
65-
'8',
66-
'9',
67-
'0', # 0 or "10"
68-
'A', # 0 or "10"
69-
'B', # 11 or ST3
70-
'C', # 12 or ST2
71-
'D', # KP
72-
'E', # KP2
73-
'F', # ST
74-
}
7534
```
35+
./bluebox.py 123456789
36+
```
37+
38+
39+
40+
**Development**
41+
42+
Development of different MF implementations and audio backends is extremely easy now. Just create a new class that inherits from the MF class, and register it.
43+
Same thing for audio backends.
44+
45+
Currently there are two MF implementations (DTMF and MF), and two audio backends (PyAudio and Dummy).

bluebox.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python
2+
from bluebox import cli
3+
4+
if __name__ == '__main__':
5+
cli.bluebox()

bluebox/__init__.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import typing as t
2+
from .freqs import BaseMF, DTMF, MF
3+
4+
__version__ = '0.1.0'
5+
6+
_MF: t.Dict[str, t.Type[BaseMF]] = {}
7+
8+
9+
def register_mf(name: str, backend: t.Type[BaseMF]) -> None:
10+
"""Register a MF set."""
11+
_MF[name] = backend
12+
13+
14+
def get_mf(name: str) -> t.Type[BaseMF]:
15+
"""Get a MF set."""
16+
return _MF[name]
17+
18+
19+
def list_mf() -> t.List[str]:
20+
"""List the available MF sets."""
21+
return list(_MF.keys())
22+
23+
24+
register_mf('dtmf', DTMF)
25+
register_mf('mf', MF)

bluebox/backends/__init__.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import typing as t
2+
from .base import BlueboxBackend as BlueboxBackend # noqa: F401
3+
from .backend_pyaudio import PyAudioBackend as PyAudioBackend # noqa: F401
4+
from .backend_dummy import DummyBackend as DummyBackend # noqa: F401
5+
6+
_BACKENDS: t.Dict[str, t.Type[BlueboxBackend]] = {}
7+
8+
9+
def register_backend(name: str, backend: t.Type[BlueboxBackend]) -> None:
10+
"""Register a backend."""
11+
_BACKENDS[name] = backend
12+
13+
14+
def get_backend(name: str) -> t.Type[BlueboxBackend]:
15+
"""Get a backend."""
16+
return _BACKENDS[name]
17+
18+
19+
def list_backends() -> t.List[str]:
20+
"""List the available backends."""
21+
return list(_BACKENDS.keys())
22+
23+
24+
register_backend('pyaudio', PyAudioBackend)
25+
register_backend('dummy', DummyBackend)

bluebox/backends/backend_dummy.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""backend_dummy.py
2+
3+
This file contains the dummy backend for bluebox.
4+
This is used for testing and instead of generating
5+
sound, it can print the data to the console, or return
6+
it as a list.
7+
"""
8+
9+
import typing as t
10+
import logging
11+
from .base import BlueboxBackend
12+
13+
14+
class DummyBackend(BlueboxBackend):
15+
"""DummyBackend class for the dummy backend."""
16+
17+
_data: t.List[float]
18+
19+
def __init__(
20+
self,
21+
sample_rate: float = 44100.0,
22+
channels: int = 1,
23+
amplitude: float = 1.0,
24+
logger: t.Optional[logging.Logger] = None,
25+
mode: str = 'print') -> None:
26+
"""Initialize the dummy backend."""
27+
super().__init__(sample_rate, channels, amplitude, logger)
28+
self._mode = mode
29+
self._data = []
30+
31+
def _to_bytes(self, data: t.Iterator[float]) -> t.List[float]:
32+
"""Wrap the data in a buffer."""
33+
_data = []
34+
while True:
35+
try:
36+
d = next(data)
37+
_data.append(d)
38+
except StopIteration:
39+
break
40+
41+
return _data
42+
43+
def play(self, data: t.Iterator[float], close=True) -> None:
44+
"""Play the given data."""
45+
d = self._to_bytes(data)
46+
if self._mode == 'print':
47+
print(d)
48+
elif self._mode == 'list':
49+
self._data += d
50+
else:
51+
raise ValueError(f'Invalid mode: {self._mode}')
52+
53+
def play_all(self, queue: t.Iterator[t.Iterator[float]]) -> None:
54+
"""Play the given data and then stop."""
55+
for data in queue:
56+
self.play(data, close=False)
57+
58+
def stop(self) -> None:
59+
"""Stop playing the data."""
60+
pass
61+
62+
def close(self) -> None:
63+
"""Close the backend."""
64+
pass
65+
66+
def __del__(self) -> None:
67+
"""Delete the backend."""
68+
self.close()
69+
70+
def get_data(self) -> t.List[float]:
71+
"""Get the data."""
72+
return self._data
73+
74+
def clear_data(self) -> None:
75+
"""Clear the data."""
76+
self._data = []

0 commit comments

Comments
 (0)