Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7044ff6

Browse files
committed
Add normalize parameter to Audio.
1 parent 80013f4 commit 7044ff6

2 files changed

Lines changed: 90 additions & 29 deletions

File tree

IPython/lib/display.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ class Audio(DisplayObject):
5454
autoplay : bool
5555
Set to True if the audio should immediately start playing.
5656
Default is `False`.
57+
normalize : bool
58+
Whether audio should be normalized (rescaled) to the maximum possible
59+
range. Default is `True`. When set to `False`, `data` must be between
60+
-1 and 1 (inclusive), otherwise an error is raised.
61+
Applies only when `data` is a list or array of samples; other types of
62+
audio are never normalized.
5763
5864
Examples
5965
--------
@@ -83,7 +89,7 @@ class Audio(DisplayObject):
8389
"""
8490
_read_flags = 'rb'
8591

86-
def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False):
92+
def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True):
8793
if filename is None and url is None and data is None:
8894
raise ValueError("No audio data found. Expecting filename, url, or data.")
8995
if embed is False and url is None:
@@ -99,7 +105,7 @@ def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, au
99105
if self.data is not None and not isinstance(self.data, bytes):
100106
if rate is None:
101107
raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
102-
self.data = Audio._make_wav(data, rate)
108+
self.data = Audio._make_wav(data, rate, normalize)
103109

104110
def reload(self):
105111
"""Reload the raw data from file or URL."""
@@ -115,16 +121,16 @@ def reload(self):
115121
self.mimetype = "audio/wav"
116122

117123
@staticmethod
118-
def _make_wav(data, rate):
124+
def _make_wav(data, rate, normalize):
119125
""" Transform a numpy array to a PCM bytestring """
120126
import struct
121127
from io import BytesIO
122128
import wave
123129

124130
try:
125-
scaled, nchan = Audio._validate_and_normalize_with_numpy(data)
131+
scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
126132
except ImportError:
127-
scaled, nchan = Audio._validate_and_normalize_without_numpy(data)
133+
scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
128134

129135
fp = BytesIO()
130136
waveobj = wave.open(fp,mode='wb')
@@ -139,7 +145,7 @@ def _make_wav(data, rate):
139145
return val
140146

141147
@staticmethod
142-
def _validate_and_normalize_with_numpy(data):
148+
def _validate_and_normalize_with_numpy(data, normalize):
143149
import numpy as np
144150

145151
data = np.array(data, dtype=float)
@@ -154,21 +160,32 @@ def _validate_and_normalize_with_numpy(data):
154160
data = data.T.ravel()
155161
else:
156162
raise ValueError('Array audio input must be a 1D or 2D array')
157-
scaled = np.int16(data/np.max(np.abs(data))*32767).tolist()
163+
164+
max_abs_value = np.max(np.abs(data))
165+
normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
166+
scaled = np.int16(data / normalization_factor * 32767).tolist()
158167
return scaled, nchan
159168

169+
160170
@staticmethod
161-
def _validate_and_normalize_without_numpy(data):
171+
def _validate_and_normalize_without_numpy(data, normalize):
162172
try:
163-
maxabsvalue = float(max([abs(x) for x in data]))
173+
max_abs_value = float(max([abs(x) for x in data]))
164174
except TypeError:
165175
raise TypeError('Only lists of mono audio are '
166176
'supported if numpy is not installed')
167177

168-
scaled = [int(x/maxabsvalue*32767) for x in data]
178+
normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
179+
scaled = [int(x / normalization_factor * 32767) for x in data]
169180
nchan = 1
170181
return scaled, nchan
171182

183+
@staticmethod
184+
def _get_normalization_factor(max_abs_value, normalize):
185+
if not normalize and max_abs_value > 1:
186+
raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
187+
return max_abs_value if normalize else 1
188+
172189
def _data_and_metadata(self):
173190
"""shortcut for returning metadata with url information, if defined"""
174191
md = {}

IPython/lib/tests/test_display.py

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import pathlib
2020
except ImportError:
2121
pass
22-
from unittest import mock
22+
from unittest import TestCase, mock
23+
import struct
24+
import wave
25+
from io import BytesIO
2326

2427
# Third-party imports
2528
import nose.tools as nt
@@ -184,25 +187,66 @@ def test_audio_from_file():
184187
path = pjoin(dirname(__file__), 'test.wav')
185188
display.Audio(filename=path)
186189

187-
def test_audio_from_numpy_array():
188-
display.Audio(get_test_tone(), rate=44100)
189-
190-
def test_audio_from_list_without_numpy():
191-
# Simulate numpy not installed.
192-
with mock.patch('numpy.array', side_effect=ImportError):
193-
display.Audio(list(get_test_tone()), rate=44100)
194-
195-
def test_audio_from_list_without_numpy_raises_for_nested_list():
196-
# Simulate numpy not installed.
197-
with mock.patch('numpy.array', side_effect=ImportError):
190+
class TestAudioDataWithNumpy(TestCase):
191+
def test_audio_from_numpy_array(self):
192+
test_tone = get_test_tone()
193+
audio = display.Audio(test_tone, rate=44100)
194+
nt.assert_equal(len(read_wav(audio.data)), len(test_tone))
195+
196+
def test_audio_from_list(self):
197+
test_tone = get_test_tone()
198+
audio = display.Audio(list(test_tone), rate=44100)
199+
nt.assert_equal(len(read_wav(audio.data)), len(test_tone))
200+
201+
def test_audio_from_numpy_array_without_rate_raises(self):
202+
nt.assert_raises(ValueError, display.Audio, get_test_tone())
203+
204+
def test_audio_data_normalization(self):
205+
expected_max_value = numpy.iinfo(numpy.int16).max
206+
for scale in [1, 0.5, 2]:
207+
audio = display.Audio(get_test_tone(scale), rate=44100)
208+
actual_max_value = numpy.max(numpy.abs(read_wav(audio.data)))
209+
nt.assert_equal(actual_max_value, expected_max_value)
210+
211+
def test_audio_data_without_normalization(self):
212+
max_int16 = numpy.iinfo(numpy.int16).max
213+
for scale in [1, 0.5, 0.2]:
214+
test_tone = get_test_tone(scale)
215+
test_tone_max_abs = numpy.max(numpy.abs(test_tone))
216+
expected_max_value = int(max_int16 * test_tone_max_abs)
217+
audio = display.Audio(test_tone, rate=44100, normalize=False)
218+
actual_max_value = numpy.max(numpy.abs(read_wav(audio.data)))
219+
nt.assert_equal(actual_max_value, expected_max_value)
220+
221+
def test_audio_data_without_normalization_raises_for_invalid_data(self):
222+
nt.assert_raises(
223+
ValueError,
224+
lambda: display.Audio([1.001], rate=44100, normalize=False))
225+
nt.assert_raises(
226+
ValueError,
227+
lambda: display.Audio([-1.001], rate=44100, normalize=False))
228+
229+
def simulate_numpy_not_installed():
230+
return mock.patch('numpy.array', mock.MagicMock(side_effect=ImportError))
231+
232+
@simulate_numpy_not_installed()
233+
class TestAudioDataWithoutNumpy(TestAudioDataWithNumpy):
234+
# All tests from `TestAudioDataWithNumpy` are inherited.
235+
236+
def test_audio_raises_for_nested_list(self):
198237
stereo_signal = [list(get_test_tone())] * 2
199-
nt.assert_raises(TypeError, lambda: display.Audio(stereo_signal, rate=44100))
200-
201-
def test_audio_from_numpy_array_without_rate_raises():
202-
nt.assert_raises(ValueError, display.Audio, get_test_tone())
203-
204-
def get_test_tone():
205-
return numpy.sin(2 * numpy.pi * 440 * numpy.linspace(0, 1, 44100))
238+
nt.assert_raises(
239+
TypeError,
240+
lambda: display.Audio(stereo_signal, rate=44100))
241+
242+
def get_test_tone(scale=1):
243+
return numpy.sin(2 * numpy.pi * 440 * numpy.linspace(0, 1, 44100)) * scale
244+
245+
def read_wav(data):
246+
with wave.open(BytesIO(data)) as wave_file:
247+
wave_data = wave_file.readframes(wave_file.getnframes())
248+
num_samples = wave_file.getnframes() * wave_file.getnchannels()
249+
return struct.unpack('<%sh' % num_samples, wave_data)
206250

207251
def test_code_from_file():
208252
c = display.Code(filename=__file__)

0 commit comments

Comments
 (0)