diff --git a/examples/reference/widgets/Audio.ipynb b/examples/reference/widgets/Audio.ipynb index c622feb4af..48cb3485c4 100644 --- a/examples/reference/widgets/Audio.ipynb +++ b/examples/reference/widgets/Audio.ipynb @@ -6,7 +6,9 @@ "metadata": {}, "outputs": [], "source": [ + "import numpy as np\n", "import panel as pn\n", + "\n", "pn.extension()" ] }, @@ -33,13 +35,51 @@ "___" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Audio` widget can be constructed with a URL pointing to a remote audio file or a local audio file (in which case the data is embedded)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pn.widgets.Audio(name='Audio', value='http://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, if SciPy is available, the widget may also be constructed from a NumPy array containing int16 values and a `sample_rate`, e.g. in this example we plot a frequency modulated signal:" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "audio = pn.widgets.Audio(name='Audio', value='http://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3')\n", + "sps = 44100 # Samples per second\n", + "duration = 10 # Duration in seconds\n", + "\n", + "modulator_frequency = 2.0\n", + "carrier_frequency = 120.0\n", + "modulation_index = 2.0\n", + "\n", + "time = np.arange(sps*duration) / sps\n", + "modulator = np.sin(2.0 * np.pi * modulator_frequency * time) * modulation_index\n", + "carrier = np.sin(2.0 * np.pi * carrier_frequency * time)\n", + "waveform = np.sin(2. * np.pi * (carrier_frequency * time + modulator))\n", + " \n", + "waveform_quiet = waveform * 0.3\n", + "waveform_int = np.int16(waveform_quiet * 32767)\n", + "\n", + "audio = pn.widgets.Audio(value=waveform_int, sample_rate=sps)\n", "audio" ] }, @@ -99,5 +139,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/panel/tests/widgets/test_misc.py b/panel/tests/widgets/test_misc.py new file mode 100644 index 0000000000..19c06cb7da --- /dev/null +++ b/panel/tests/widgets/test_misc.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, division, unicode_literals + +from io import BytesIO +from base64 import b64encode + +import numpy as np +from scipy.io import wavfile + +from panel.widgets import Audio + +def test_audio_array(document, comm): + data = np.random.randint(-100,100, 100).astype('int16') + sample_rate = 10 + buffer = BytesIO() + wavfile.write(buffer, sample_rate, data) + b64_encoded = b64encode(buffer.getvalue()).decode('utf-8') + + audio = Audio(name='Button', value=data, sample_rate=sample_rate) + widget = audio.get_root(document, comm=comm) + widget_value = widget.value + + assert widget_value.split(',')[1] == b64_encoded + assert widget.value.startswith('data:audio/wav;base64') + + +def test_audio_url(document, comm): + audio_url = 'http://ccrma.stanford.edu/~jos/mp3/pno-cs.mp3' + audio2 = Audio(name='Audio', value=audio_url) + url_widget = audio2.get_root(document, comm=comm) + + assert audio_url == url_widget.value + diff --git a/panel/widgets/misc.py b/panel/widgets/misc.py index 3d1c308c89..cb0c709034 100644 --- a/panel/widgets/misc.py +++ b/panel/widgets/misc.py @@ -5,9 +5,13 @@ import os +from io import BytesIO from base64 import b64encode +from six import string_types import param +import numpy as np +from scipy.io import wavfile from ..io.notebook import push from ..io.state import state @@ -30,7 +34,10 @@ class Audio(Widget): paused = param.Boolean(default=True, doc=""" Whether the audio is currently paused""") - value = param.String(default='', doc=""" + sample_rate = param.Integer(default=44100, doc=""" + The sample_rate of the audio when given a NumPy array.""") + + value = param.ClassSelector(default='', class_=(string_types + (np.ndarray,)), doc=""" The audio file either local or remote.""") volume = param.Number(default=None, bounds=(0, 100), doc=""" @@ -38,21 +45,40 @@ class Audio(Widget): _widget_type = _BkAudio - _rename = {'name': None} + _rename = {'name': None, 'sample_rate': None} + + def _from_numpy(self, data): + buffer = BytesIO() + wavfile.write(buffer, self.sample_rate, data) + return buffer def _process_param_change(self, msg): msg = super(Audio, self)._process_param_change(msg) - if 'value' in msg and os.path.isfile(msg['value']): - fmt = msg['value'].split('.')[-1] - with open(msg['value'], 'rb') as f: - data = f.read() + + if 'value' in msg: + value = msg['value'] + if isinstance(value, np.ndarray): + fmt = 'wav' + buffer = self._from_numpy(value) + data = b64encode(buffer.getvalue()) + elif os.path.isfile(value): + fmt = value.split('.')[-1] + with open(value, 'rb') as f: + data = f.read() + data = b64encode(data) + elif value.lower().startswith('http'): + return msg + elif not value: + data, fmt = b'', 'wav' + else: + raise ValueError('Value should be either path to a sound file or numpy array') template = 'data:audio/{mime};base64,{data}' - data = b64encode(data) msg['value'] = template.format(data=data.decode('utf-8'), mime=fmt) return msg + class VideoStream(Widget): format = param.ObjectSelector(default='png', objects=['png', 'jpeg'],