Skip to content

Commit 9901156

Browse files
committed
Added pypi/pip package.
1 parent 5f967eb commit 9901156

23 files changed

+1131
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
__pycache__/
22
.pytest_cache/
3+
.mypy_cache/
34
.vscode/
45
*.pyc
56
*.pyo

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ Well, initially this was a fun little python 3 project from 2015, but I thought
1818

1919
## Installation
2020

21+
### PyPI
22+
23+
```
24+
pip install mfbluebox
25+
```
26+
27+
### Development
28+
2129
```
2230
git clone https://github.com/zeyus/Python3BlueBox.git
2331
cd PythonBlueBox
24-
pip install -r requirements.txt
32+
pip install .
2533
```
2634

2735
### Requirements
@@ -33,13 +41,13 @@ pip install -r requirements.txt
3341
### CLI
3442

3543
```
36-
python ./bluebox.py -h
44+
python -m bluebox -h
3745
```
3846

3947
Output:
4048

4149
```
42-
usage: bluebox.py [-h] [-l LENGTH] [-p PAUSE] [-a AMPLITUDE] [-s SAMPLE_RATE] [-m MF] [-d] [-b BACKEND] [-r PAD_PAUSE_DURATION] [-f FILE] [-P PIPE] [-S] [-i] [-v] [sequence]
50+
usage: __main__.py [-h] [-l LENGTH] [-p PAUSE] [-a AMPLITUDE] [-s SAMPLE_RATE] [-m MF] [-d] [-b BACKEND] [-r PAD_PAUSE_DURATION] [-f FILE] [-P PIPE] [-S] [-i] [-v] [sequence]
4351
4452
Generate tone sequences.
4553
@@ -72,11 +80,11 @@ options:
7280
**Examples**
7381

7482
```
75-
python ./bluebox.py -i
83+
python -m bluebox -i
7684
```
7785

7886
```
79-
python ./bluebox.py 123456789
87+
python -m bluebox 123456789
8088
```
8189

8290
### API

bluebox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing as t
22
from .freqs import BaseMF, DTMF, MF
33

4-
__version__ = '0.1.0'
4+
__version__ = '0.1.2'
55

66
_MF: t.Dict[str, t.Type[BaseMF]] = {}
77

bluebox/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""CLI for bluebox"""
2+
3+
from . import cli
4+
5+
if __name__ == '__main__':
6+
cli.bluebox()

build/lib/bluebox/__init__.py

Lines changed: 25 additions & 0 deletions
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.1'
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)

build/lib/bluebox/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""CLI for bluebox"""
2+
3+
from . import cli
4+
5+
if __name__ == '__main__':
6+
cli.bluebox()
Lines changed: 25 additions & 0 deletions
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)
Lines changed: 76 additions & 0 deletions
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 = []
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""backend_pyaudio.py
2+
3+
This file contains the PyAudio backend for bluebox.
4+
"""
5+
6+
import typing as t
7+
import logging
8+
import struct
9+
import pyaudio # type: ignore
10+
from .base import BlueboxBackend
11+
12+
13+
class PyAudioBackend(BlueboxBackend):
14+
"""PyAudioBackend class for the PyAudio backend."""
15+
16+
_stream: pyaudio.Stream
17+
_stream_open: bool = False
18+
_device: t.Union[int, None]
19+
20+
def __init__(
21+
self,
22+
sample_rate: float = 44100.0,
23+
channels: int = 1,
24+
amplitude: float = 1.0,
25+
logger: t.Optional[logging.Logger] = None,
26+
device: t.Union[int, None] = None) -> None:
27+
"""Initialize the PyAudio backend."""
28+
super().__init__(sample_rate, channels, amplitude, logger)
29+
self._device = device
30+
31+
def _get_stream(
32+
self,
33+
callback: t.Optional[t.Callable] = None) -> pyaudio.Stream:
34+
"""Get the PyAudio stream."""
35+
if not self._stream_open:
36+
self._stream = pyaudio.PyAudio().open(
37+
format=pyaudio.paFloat32,
38+
channels=self._ch,
39+
rate=int(self._sr),
40+
output=True,
41+
output_device_index=self._device,
42+
stream_callback=callback)
43+
self._stream_open = True
44+
return self._stream
45+
46+
def _to_bytes(self, data: t.Iterator[float]) -> bytes:
47+
"""Wrap the data in a buffer."""
48+
_data = []
49+
while True:
50+
try:
51+
d = next(data)
52+
_data.append(d)
53+
except StopIteration:
54+
break
55+
56+
return struct.pack(f'{len(_data)}f', *_data)
57+
58+
def play(self, data: t.Iterator[float], close=True) -> None:
59+
"""Play the given data."""
60+
d = self._to_bytes(data)
61+
self._get_stream().write(d)
62+
if close:
63+
self.close()
64+
65+
def play_all(self, queue: t.Iterator[t.Iterator[float]]) -> None:
66+
"""Play all the items until the end."""
67+
while True:
68+
try:
69+
data = next(queue)
70+
self.play(data, False)
71+
except StopIteration:
72+
break
73+
self.close()
74+
75+
def stop(self) -> None:
76+
"""Stop playing the data."""
77+
if self._stream_open:
78+
self._stream.stop_stream()
79+
80+
def close(self) -> None:
81+
"""Close the backend."""
82+
if self._stream_open:
83+
self._stream.close()
84+
self._stream_open = False
85+
86+
def __del__(self) -> None:
87+
"""Close the backend."""
88+
self.close()
89+
90+
91+
# class PyAudioBackendNonBlocking(PyAudioBackend):
92+
# """PyAudioBackendNonBlocking class for the PyAudio backend."""
93+
94+
# def _get_stream(
95+
# self,
96+
# callback:
97+
# t.Optional[t.Callable] = None) -> pyaudio.Stream:
98+
# """Get the PyAudio stream."""
99+
# if not self._stream_open:
100+
# self._stream = pyaudio.PyAudio().open(
101+
# format=pyaudio.paFloat32,
102+
# channels=self._ch,
103+
# rate=int(self._sr),
104+
# output=True,
105+
# stream_callback=callback,
106+
# output_device_index=self._device)
107+
# self._stream_open = True
108+
# return self._stream
109+
110+
# @property
111+
# def is_playing(self) -> bool:
112+
# """Return whether the stream is playing."""
113+
# if not self._stream_open:
114+
# return False
115+
# return self._stream.is_active()
116+
117+
# def play(self, data: t.Iterator[float], close=True) -> None:
118+
# """Play the given data."""
119+
# total_frames = len(data)
120+
# frame_index = 0
121+
122+
# def stream_callback(
123+
# in_data: t.Optional[t.MutableSequence[bytes]],
124+
# frame_count: int,
125+
# time_info: t.Optional[dict],
126+
# status: t.Optional[int]) -> t.Tuple[
127+
# t.Optional[t.MutableSequence[float]],
128+
# int]:
129+
# nonlocal frame_index
130+
# if frame_index >= total_frames:
131+
# return (None, pyaudio.paComplete)
132+
# frame_index += min(frame_count, total_frames - frame_index)
133+
# return (
134+
# data[frame_index - frame_count:frame_index],
135+
# pyaudio.paContinue)
136+
137+
# self._get_stream(stream_callback)
138+
139+
# def play_all(self, queue: t.Iterator[t.MutableSequence[float]]) -> None:
140+
# """Not implemented. Probably should use asyncio."""
141+
# raise NotImplementedError()

0 commit comments

Comments
 (0)