##// END OF EJS Templates
Add normalize parameter to Audio.
Matan Gover -
Show More
@@ -54,6 +54,12 b' class Audio(DisplayObject):'
54 54 autoplay : bool
55 55 Set to True if the audio should immediately start playing.
56 56 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.
57 63
58 64 Examples
59 65 --------
@@ -83,7 +89,7 b' class Audio(DisplayObject):'
83 89 """
84 90 _read_flags = 'rb'
85 91
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):
87 93 if filename is None and url is None and data is None:
88 94 raise ValueError("No audio data found. Expecting filename, url, or data.")
89 95 if embed is False and url is None:
@@ -99,7 +105,7 b' class Audio(DisplayObject):'
99 105 if self.data is not None and not isinstance(self.data, bytes):
100 106 if rate is None:
101 107 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)
103 109
104 110 def reload(self):
105 111 """Reload the raw data from file or URL."""
@@ -115,16 +121,16 b' class Audio(DisplayObject):'
115 121 self.mimetype = "audio/wav"
116 122
117 123 @staticmethod
118 def _make_wav(data, rate):
124 def _make_wav(data, rate, normalize):
119 125 """ Transform a numpy array to a PCM bytestring """
120 126 import struct
121 127 from io import BytesIO
122 128 import wave
123 129
124 130 try:
125 scaled, nchan = Audio._validate_and_normalize_with_numpy(data)
131 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
126 132 except ImportError:
127 scaled, nchan = Audio._validate_and_normalize_without_numpy(data)
133 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
128 134
129 135 fp = BytesIO()
130 136 waveobj = wave.open(fp,mode='wb')
@@ -139,7 +145,7 b' class Audio(DisplayObject):'
139 145 return val
140 146
141 147 @staticmethod
142 def _validate_and_normalize_with_numpy(data):
148 def _validate_and_normalize_with_numpy(data, normalize):
143 149 import numpy as np
144 150
145 151 data = np.array(data, dtype=float)
@@ -154,21 +160,32 b' class Audio(DisplayObject):'
154 160 data = data.T.ravel()
155 161 else:
156 162 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()
158 167 return scaled, nchan
159 168
169
160 170 @staticmethod
161 def _validate_and_normalize_without_numpy(data):
171 def _validate_and_normalize_without_numpy(data, normalize):
162 172 try:
163 maxabsvalue = float(max([abs(x) for x in data]))
173 max_abs_value = float(max([abs(x) for x in data]))
164 174 except TypeError:
165 175 raise TypeError('Only lists of mono audio are '
166 176 'supported if numpy is not installed')
167 177
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]
169 180 nchan = 1
170 181 return scaled, nchan
171 182
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
172 189 def _data_and_metadata(self):
173 190 """shortcut for returning metadata with url information, if defined"""
174 191 md = {}
@@ -19,7 +19,10 b' try:'
19 19 import pathlib
20 20 except ImportError:
21 21 pass
22 from unittest import mock
22 from unittest import TestCase, mock
23 import struct
24 import wave
25 from io import BytesIO
23 26
24 27 # Third-party imports
25 28 import nose.tools as nt
@@ -184,25 +187,66 b' def test_audio_from_file():'
184 187 path = pjoin(dirname(__file__), 'test.wav')
185 188 display.Audio(filename=path)
186 189
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):
198 237 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)
206 250
207 251 def test_code_from_file():
208 252 c = display.Code(filename=__file__)
General Comments 0
You need to be logged in to leave comments. Login now