Skip to content

Commit 376153b

Browse files
committed
Update release processes, add Microphone.list_working_microphones() to test every microphone to ensure they can actually record audio
1 parent b24d057 commit 376153b

File tree

5 files changed

+70
-14
lines changed

5 files changed

+70
-14
lines changed

.github/ISSUE_TEMPLATE.md

+2
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ My **PyAudio library version** is <INSERT VERSION HERE> / I don't have PyAudio i
3636

3737
My **microphones** are: (You can check this by running `python -c "import speech_recognition as sr;print(sr.Microphone.list_microphone_names())"`.)
3838

39+
My **working microphones** are: (You can check this by running `python -c "import speech_recognition as sr;print(sr.Microphone.list_working_microphones())"`.)
40+
3941
I **installed PocketSphinx from** <INSERT SOURCE HERE>. (For example, from the Debian repositories, from Homebrew, or from the source code.)

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ To install/reinstall the library locally, run ``python setup.py install`` in the
278278

279279
Before a release, the version number is bumped in ``README.rst`` and ``speech_recognition/__init__.py``. Version tags are then created using ``git config gpg.program gpg2 && git config user.signingkey DB45F6C431DE7C2DCD99FF7904882258A4063489 && git tag -s VERSION_GOES_HERE -m "Version VERSION_GOES_HERE"``.
280280

281-
Releases are done by running ``make-release.sh`` to build the Python source packages, sign them, and upload them to PyPI.
281+
Releases are done by running ``make-release.sh VERSION_GOES_HERE`` to build the Python source packages, sign them, and upload them to PyPI.
282282

283283
Testing
284284
~~~~~~~

make-release.sh

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ set -e # give an error if any command finishes with a non-zero exit code
55
set -u # give an error if we reference unset variables
66
set -o pipefail # for a pipeline, if any of the commands fail with a non-zero exit code, fail the entire pipeline with that exit code
77

8-
echo "if the following doesn't work, make sure you have your account set up properly with 'python3 setup.py register'"
8+
echo "Making release for SpeechRecognition-$1"
99

10-
# make sure we use GnuPG 2 rather than GnuPG 1
11-
sudo ln --force "$(which gpg2)" dist/gpg
12-
PATH=./dist:$PATH python3 setup.py bdist_wheel upload --sign
10+
python setup.py bdist_wheel
11+
gpg2 --detach-sign -a dist/SpeechRecognition-$1-*.whl
12+
twine upload dist/SpeechRecognition-$1-*.whl dist/SpeechRecognition-$1-*.whl.asc

reference/library-reference.rst

+18-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Instances of this class are context managers, and are designed to be used with `
3131

3232
Returns a list of the names of all available microphones. For microphones where the name can't be retrieved, the list entry contains ``None`` instead.
3333

34-
The index of each microphone's name is the same as its device index when creating a ``Microphone`` instance - indices in this list can be used as values of ``device_index``.
34+
The index of each microphone's name in the returned list is the same as its device index when creating a ``Microphone`` instance - if you want to use the microphone at index 3 in the returned list, use ``Microphone(device_index=3)``.
3535

3636
To create a ``Microphone`` instance by name:
3737

@@ -42,6 +42,23 @@ To create a ``Microphone`` instance by name:
4242
if microphone_name == "HDA Intel HDMI: 0 (hw:0,3)":
4343
m = Microphone(device_index=i)
4444
45+
``Microphone.list_working_microphones() -> Dict[int, str]``
46+
-----------------------------------------------------------
47+
48+
Returns a dictionary mapping device indices to microphone names, for microphones that are currently hearing sounds. When using this function, ensure that your microphone is unmuted and make some noise at it to ensure it will be detected as working.
49+
50+
Each key in the returned dictionary can be passed to the ``Microphone`` constructor to use that microphone. For example, if the return value is ``{3: "HDA Intel PCH: ALC3232 Analog (hw:1,0)"}``, you can do ``Microphone(device_index=3)`` to use that microphone.
51+
52+
To create a ``Microphone`` instance for the first working microphone:
53+
54+
.. code:: python
55+
56+
for device_index in Microphone.list_working_microphones():
57+
m = Microphone(device_index=device_index)
58+
break
59+
else:
60+
print("No working microphones found!")
61+
4562
``AudioFile(filename_or_fileobject: Union[str, io.IOBase]) -> AudioFile``
4663
-------------------------------------------------------------------------
4764

speech_recognition/__init__.py

+45-8
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ def __init__(self, device_index=None, sample_rate=None, chunk_size=1024):
8686
device_info = audio.get_device_info_by_index(device_index) if device_index is not None else audio.get_default_input_device_info()
8787
assert isinstance(device_info.get("defaultSampleRate"), (float, int)) and device_info["defaultSampleRate"] > 0, "Invalid device info returned from PyAudio: {}".format(device_info)
8888
sample_rate = int(device_info["defaultSampleRate"])
89-
except Exception:
89+
finally:
9090
audio.terminate()
91-
raise
9291

9392
self.device_index = device_index
9493
self.format = self.pyaudio_module.paInt16 # 16-bit int sampling
@@ -118,7 +117,7 @@ def list_microphone_names():
118117
"""
119118
Returns a list of the names of all available microphones. For microphones where the name can't be retrieved, the list entry contains ``None`` instead.
120119
121-
The index of each microphone's name is the same as its device index when creating a ``Microphone`` instance - indices in this list can be used as values of ``device_index``.
120+
The index of each microphone's name in the returned list is the same as its device index when creating a ``Microphone`` instance - if you want to use the microphone at index 3 in the returned list, use ``Microphone(device_index=3)``.
122121
"""
123122
audio = Microphone.get_pyaudio().PyAudio()
124123
try:
@@ -130,20 +129,58 @@ def list_microphone_names():
130129
audio.terminate()
131130
return result
132131

132+
@staticmethod
133+
def list_working_microphones():
134+
"""
135+
Returns a dictionary mapping device indices to microphone names, for microphones that are currently hearing sounds. When using this function, ensure that your microphone is unmuted and make some noise at it to ensure it will be detected as working.
136+
137+
Each key in the returned dictionary can be passed to the ``Microphone`` constructor to use that microphone. For example, if the return value is ``{3: "HDA Intel PCH: ALC3232 Analog (hw:1,0)"}``, you can do ``Microphone(device_index=3)`` to use that microphone.
138+
"""
139+
pyaudio_module = Microphone.get_pyaudio()
140+
audio = pyaudio_module.PyAudio()
141+
try:
142+
result = {}
143+
for device_index in range(audio.get_device_count()):
144+
device_info = audio.get_device_info_by_index(device_index)
145+
device_name = device_info.get("name")
146+
assert isinstance(device_info.get("defaultSampleRate"), (float, int)) and device_info["defaultSampleRate"] > 0, "Invalid device info returned from PyAudio: {}".format(device_info)
147+
try:
148+
# read audio
149+
pyaudio_stream = audio.open(
150+
input_device_index=device_index, channels=1, format=pyaudio_module.paInt16,
151+
rate=int(device_info["defaultSampleRate"]), input=True
152+
)
153+
try:
154+
buffer = pyaudio_stream.read(1024)
155+
if not pyaudio_stream.is_stopped(): pyaudio_stream.stop_stream()
156+
finally:
157+
pyaudio_stream.close()
158+
except Exception:
159+
continue
160+
161+
# compute RMS of debiased audio
162+
energy = -audioop.rms(buffer, 2)
163+
energy_bytes = chr(energy & 0xFF) + chr((energy >> 8) & 0xFF) if bytes is str else bytes([energy & 0xFF, (energy >> 8) & 0xFF]) # Python 2 compatibility
164+
debiased_energy = audioop.rms(audioop.add(buffer, energy_bytes * (len(buffer) // 2), 2), 2)
165+
166+
if debiased_energy > 30: # probably actually audio
167+
result[device_index] = device_name
168+
finally:
169+
audio.terminate()
170+
return result
171+
133172
def __enter__(self):
134173
assert self.stream is None, "This audio source is already inside a context manager"
135174
self.audio = self.pyaudio_module.PyAudio()
136175
try:
137176
self.stream = Microphone.MicrophoneStream(
138177
self.audio.open(
139-
input_device_index=self.device_index, channels=1,
140-
format=self.format, rate=self.SAMPLE_RATE, frames_per_buffer=self.CHUNK,
141-
input=True, # stream is an input stream
178+
input_device_index=self.device_index, channels=1, format=self.format,
179+
rate=self.SAMPLE_RATE, frames_per_buffer=self.CHUNK, input=True,
142180
)
143181
)
144-
except Exception:
182+
finally:
145183
self.audio.terminate()
146-
raise
147184
return self
148185

149186
def __exit__(self, exc_type, exc_value, traceback):

0 commit comments

Comments
 (0)