Skip to content

Commit b95e82a

Browse files
committed
Updated tests, CLI WIP, now started on interactive mode.
1 parent 2cc0d8d commit b95e82a

10 files changed

Lines changed: 318 additions & 38 deletions

File tree

bluebox/__init__.py

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

bluebox/backends/__init__.py

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

bluebox/backends/backend_dummy.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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

bluebox/box.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ def __init__(
3535
channels: int = 1,
3636
stop_on_error: bool = False,
3737
logger: t.Optional[logging.Logger] = None,
38-
backend: t.Optional[BlueboxBackend] = None) -> None:
38+
backend: t.Optional[
39+
t.Union[BlueboxBackend, t.Type[BlueboxBackend]]
40+
] = None) -> None:
3941
"""Initialize the Sequencer object.
4042
4143
Args:
@@ -54,7 +56,7 @@ def __init__(
5456
"""
5557

5658
self._mf = mf
57-
self._wave = SineWave(sr=sample_rate, ch=channels)
59+
self._wave = SineWave(sample_rate=sample_rate, channels=channels)
5860
self._length = length
5961
self._amplitude = amplitude
6062
self._pause = pause
@@ -75,7 +77,7 @@ def __init__(
7577
channels=channels,
7678
amplitude=1.0,
7779
logger=self._logger)
78-
self._backend = backend # type: ignore
80+
self._backend = backend # type: ignore
7981

8082
def sequence(self, codes: str) -> t.Iterator[float]:
8183
"""Generate a sequence of waveforms."""
@@ -101,7 +103,6 @@ def sequence(self, codes: str) -> t.Iterator[float]:
101103
t2 = next(tone2)
102104
yield t1 + t2
103105
except StopIteration:
104-
print('stop')
105106
break
106107

107108
i += 1

bluebox/cli.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
This file can be used to interactively generate tone sequences.
44
"""
55

6+
from pathlib import Path
67
import argparse
78
import logging
89
import sys
910
from .box import Sequencer
10-
from .freqs import DTMF, MF
11+
from . import get_mf, list_mf
1112

1213

1314
def parse_args() -> argparse.Namespace:
@@ -39,27 +40,58 @@ def parse_args() -> argparse.Namespace:
3940
type=float,
4041
default=44100.0,
4142
help='The sample rate of the waveforms.')
42-
parser.add_argument(
43-
'-c', '--channels',
44-
type=int,
45-
default=1,
46-
help='The number of channels in the waveforms.')
4743
parser.add_argument(
4844
'-m', '--mf',
4945
type=str,
50-
default='DTMF',
51-
help='The MF to use e.g. DTMF.')
46+
default='dtmf',
47+
help='The MF to use e.g. dtmf, mf.')
5248
parser.add_argument(
5349
'-d', '--debug',
5450
action='store_true',
5551
help='Enable debug logging.')
56-
parser.add_argument(
52+
# we can have sequence or file,pipe,stdin OR interactive
53+
group = parser.add_mutually_exclusive_group(required=True)
54+
group.add_argument(
55+
'-f', '--file',
56+
type=Path,
57+
help='The file to read the sequence from.')
58+
group.add_argument(
59+
'-P', '--pipe',
60+
type=Path,
61+
help='Read the sequence from a pipe.')
62+
group.add_argument(
63+
'-S', '--stdin',
64+
action='store_true',
65+
help='Read the sequence from stdin.')
66+
group.add_argument(
67+
'-i', '--interactive',
68+
action='store_true',
69+
help='Enter interactive mode.')
70+
group.add_argument(
5771
'sequence',
5872
type=str,
59-
help='The sequence to generate.')
73+
nargs='?',
74+
help='The sequence of tones to generate.')
75+
6076
return parser.parse_args()
6177

6278

79+
def bluebox_interactive(seq: Sequencer) -> None:
80+
"""Enter interactive mode."""
81+
82+
print('Entering interactive mode. Type "exit" to quit.')
83+
while True:
84+
try:
85+
seq(input('Sequence: '))
86+
except KeyboardInterrupt:
87+
print('Exiting...')
88+
break
89+
except Exception as e:
90+
logging.error(e)
91+
if seq._stop_on_error:
92+
break
93+
94+
6395
def bluebox() -> None:
6496
"""Generate a tone sequence.
6597
@@ -76,12 +108,11 @@ def bluebox() -> None:
76108
stop_on_error = False
77109
logging.basicConfig(level=logging.INFO)
78110

79-
if args.mf == 'DTMF':
80-
mf = DTMF()
81-
elif args.mf == 'MF':
82-
mf = MF()
111+
if args.mf in list_mf():
112+
mf = get_mf(args.mf)()
83113
else:
84114
logging.error('Invalid MF: %s', args.mf)
115+
logging.error('Valid MFs: %s', ', '.join(list_mf()))
85116
sys.exit(1)
86117

87118
seq = Sequencer(
@@ -90,9 +121,11 @@ def bluebox() -> None:
90121
length=args.length,
91122
pause=args.pause,
92123
sample_rate=args.sample_rate,
93-
channels=args.channels,
124+
channels=1,
94125
stop_on_error=stop_on_error)
95-
126+
if args.interactive:
127+
bluebox_interactive(seq)
128+
return
96129
try:
97130
seq(args.sequence)
98131
except Exception as e:

bluebox/freqs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from abc import ABC, abstractmethod
99
import math
1010

11+
1112
class BaseMF(ABC):
1213
"""BaseMF class for defining MF frequencies."""
1314

bluebox/wave.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@
1313
class SineWave:
1414
"""SineWave class for generating waveform arrays."""
1515

16-
_sample_rate: float = 44100.0
17-
_channels: int = 1
16+
_sr: float = 44100.0
17+
_ch: int = 1
1818

1919
def __init__(
2020
self,
21-
sr: t.Optional[float] = None,
22-
ch: t.Optional[int] = None) -> None:
21+
sample_rate: t.Optional[float] = None,
22+
channels: t.Optional[int] = None) -> None:
2323
"""Initialize the Wave object."""
24-
if sr is not None:
25-
self._sample_rate = sr
26-
if ch is not None:
27-
self._channels = ch
24+
if sample_rate is not None:
25+
self._sr = sample_rate
26+
if channels is not None:
27+
self._ch = channels
2828

2929
def sine(
3030
self,
@@ -47,14 +47,14 @@ def sine(
4747

4848
# silence / pauses
4949
if freq == 0.0 or amplitude == 0.0:
50-
for i in range(math.ceil(length * self._sample_rate / 1000)):
50+
for i in range(math.ceil(length * self._sr / 1000)):
5151
yield 0.0
5252
return
5353

5454
# sine wave
55-
for i in range(math.ceil(length * self._sample_rate / 1000)):
55+
for i in range(math.ceil(length * self._sr / 1000)):
5656
yield amplitude * math.sin(
57-
2 * math.pi * freq * (i / self._sample_rate) + phase)
57+
2 * math.pi * freq * (i / self._sr) + phase)
5858

5959
def __call__(
6060
self,
@@ -81,4 +81,4 @@ def __call__(
8181

8282
def __repr__(self) -> str:
8383
"""Get the representation of the Wave."""
84-
return f'{self.__class__.__name__}(Sample Rate: {self._sample_rate}, Channels: {self._channels})' # noqa: E501
84+
return f'{self.__class__.__name__}(Sample Rate: {self._sr}, Channels: {self._ch})' # noqa: E501

0 commit comments

Comments
 (0)