##// END OF EJS Templates
Mention the ipywidget's Audio widget.
Matthias Bussonnier -
Show More
@@ -1,643 +1,648 b''
1 1 """Various display related classes.
2 2
3 3 Authors : MinRK, gregcaporaso, dannystaple
4 4 """
5 5 from html import escape as html_escape
6 6 from os.path import exists, isfile, splitext, abspath, join, isdir
7 7 from os import walk, sep, fsdecode
8 8
9 9 from IPython.core.display import DisplayObject, TextDisplayObject
10 10
11 11 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
12 12 'FileLink', 'FileLinks', 'Code']
13 13
14 14
15 15 class Audio(DisplayObject):
16 16 """Create an audio object.
17 17
18 18 When this object is returned by an input cell or passed to the
19 19 display function, it will result in Audio controls being displayed
20 20 in the frontend (only works in the notebook).
21 21
22 22 Parameters
23 23 ----------
24 24 data : numpy array, list, unicode, str or bytes
25 25 Can be one of
26 26
27 27 * Numpy 1d array containing the desired waveform (mono)
28 28 * Numpy 2d array containing waveforms for each channel.
29 29 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
30 30 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
31 31 * List of float or integer representing the waveform (mono)
32 32 * String containing the filename
33 33 * Bytestring containing raw PCM data or
34 34 * URL pointing to a file on the web.
35 35
36 36 If the array option is used, the waveform will be normalized.
37 37
38 38 If a filename or url is used, the format support will be browser
39 39 dependent.
40 40 url : unicode
41 41 A URL to download the data from.
42 42 filename : unicode
43 43 Path to a local file to load the data from.
44 44 embed : boolean
45 45 Should the audio data be embedded using a data URI (True) or should
46 46 the original source be referenced. Set this to True if you want the
47 47 audio to playable later with no internet connection in the notebook.
48 48
49 49 Default is `True`, unless the keyword argument `url` is set, then
50 50 default value is `False`.
51 51 rate : integer
52 52 The sampling rate of the raw data.
53 53 Only required when data parameter is being used as an array
54 54 autoplay : bool
55 55 Set to True if the audio should immediately start playing.
56 56 Default is `False`.
57 57 normalize : bool
58 58 Whether audio should be normalized (rescaled) to the maximum possible
59 59 range. Default is `True`. When set to `False`, `data` must be between
60 60 -1 and 1 (inclusive), otherwise an error is raised.
61 61 Applies only when `data` is a list or array of samples; other types of
62 62 audio are never normalized.
63 63
64 64 Examples
65 65 --------
66 66 ::
67 67
68 68 # Generate a sound
69 69 import numpy as np
70 70 framerate = 44100
71 71 t = np.linspace(0,5,framerate*5)
72 72 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
73 73 Audio(data,rate=framerate)
74 74
75 75 # Can also do stereo or more channels
76 76 dataleft = np.sin(2*np.pi*220*t)
77 77 dataright = np.sin(2*np.pi*224*t)
78 78 Audio([dataleft, dataright],rate=framerate)
79 79
80 80 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
81 81 Audio(url="http://www.w3schools.com/html/horse.ogg")
82 82
83 83 Audio('/path/to/sound.wav') # From file
84 84 Audio(filename='/path/to/sound.ogg')
85 85
86 86 Audio(b'RAW_WAV_DATA..) # From bytes
87 87 Audio(data=b'RAW_WAV_DATA..)
88 88
89 See Also
90 --------
91
92 See also the ``Audio`` widgets form the ``ipywidget`` package for more flexibility and options.
93
89 94 """
90 95 _read_flags = 'rb'
91 96
92 97 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
93 98 element_id=None):
94 99 if filename is None and url is None and data is None:
95 100 raise ValueError("No audio data found. Expecting filename, url, or data.")
96 101 if embed is False and url is None:
97 102 raise ValueError("No url found. Expecting url when embed=False")
98 103
99 104 if url is not None and embed is not True:
100 105 self.embed = False
101 106 else:
102 107 self.embed = True
103 108 self.autoplay = autoplay
104 109 self.element_id = element_id
105 110 super(Audio, self).__init__(data=data, url=url, filename=filename)
106 111
107 112 if self.data is not None and not isinstance(self.data, bytes):
108 113 if rate is None:
109 114 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
110 115 self.data = Audio._make_wav(data, rate, normalize)
111 116
112 117 def reload(self):
113 118 """Reload the raw data from file or URL."""
114 119 import mimetypes
115 120 if self.embed:
116 121 super(Audio, self).reload()
117 122
118 123 if self.filename is not None:
119 124 self.mimetype = mimetypes.guess_type(self.filename)[0]
120 125 elif self.url is not None:
121 126 self.mimetype = mimetypes.guess_type(self.url)[0]
122 127 else:
123 128 self.mimetype = "audio/wav"
124 129
125 130 @staticmethod
126 131 def _make_wav(data, rate, normalize):
127 132 """ Transform a numpy array to a PCM bytestring """
128 133 import struct
129 134 from io import BytesIO
130 135 import wave
131 136
132 137 try:
133 138 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
134 139 except ImportError:
135 140 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
136 141
137 142 fp = BytesIO()
138 143 waveobj = wave.open(fp,mode='wb')
139 144 waveobj.setnchannels(nchan)
140 145 waveobj.setframerate(rate)
141 146 waveobj.setsampwidth(2)
142 147 waveobj.setcomptype('NONE','NONE')
143 148 waveobj.writeframes(b''.join([struct.pack('<h',x) for x in scaled]))
144 149 val = fp.getvalue()
145 150 waveobj.close()
146 151
147 152 return val
148 153
149 154 @staticmethod
150 155 def _validate_and_normalize_with_numpy(data, normalize):
151 156 import numpy as np
152 157
153 158 data = np.array(data, dtype=float)
154 159 if len(data.shape) == 1:
155 160 nchan = 1
156 161 elif len(data.shape) == 2:
157 162 # In wave files,channels are interleaved. E.g.,
158 163 # "L1R1L2R2..." for stereo. See
159 164 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
160 165 # for channel ordering
161 166 nchan = data.shape[0]
162 167 data = data.T.ravel()
163 168 else:
164 169 raise ValueError('Array audio input must be a 1D or 2D array')
165 170
166 171 max_abs_value = np.max(np.abs(data))
167 172 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
168 173 scaled = np.int16(data / normalization_factor * 32767).tolist()
169 174 return scaled, nchan
170 175
171 176
172 177 @staticmethod
173 178 def _validate_and_normalize_without_numpy(data, normalize):
174 179 try:
175 180 max_abs_value = float(max([abs(x) for x in data]))
176 181 except TypeError:
177 182 raise TypeError('Only lists of mono audio are '
178 183 'supported if numpy is not installed')
179 184
180 185 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
181 186 scaled = [int(x / normalization_factor * 32767) for x in data]
182 187 nchan = 1
183 188 return scaled, nchan
184 189
185 190 @staticmethod
186 191 def _get_normalization_factor(max_abs_value, normalize):
187 192 if not normalize and max_abs_value > 1:
188 193 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
189 194 return max_abs_value if normalize else 1
190 195
191 196 def _data_and_metadata(self):
192 197 """shortcut for returning metadata with url information, if defined"""
193 198 md = {}
194 199 if self.url:
195 200 md['url'] = self.url
196 201 if md:
197 202 return self.data, md
198 203 else:
199 204 return self.data
200 205
201 206 def _repr_html_(self):
202 207 src = """
203 208 <audio {element_id} controls="controls" {autoplay}>
204 209 <source src="{src}" type="{type}" />
205 210 Your browser does not support the audio element.
206 211 </audio>
207 212 """
208 213 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
209 214 element_id=self.element_id_attr())
210 215
211 216 def src_attr(self):
212 217 import base64
213 218 if self.embed and (self.data is not None):
214 219 data = base64=base64.b64encode(self.data).decode('ascii')
215 220 return """data:{type};base64,{base64}""".format(type=self.mimetype,
216 221 base64=data)
217 222 elif self.url is not None:
218 223 return self.url
219 224 else:
220 225 return ""
221 226
222 227 def autoplay_attr(self):
223 228 if(self.autoplay):
224 229 return 'autoplay="autoplay"'
225 230 else:
226 231 return ''
227 232
228 233 def element_id_attr(self):
229 234 if (self.element_id):
230 235 return 'id="{element_id}"'.format(element_id=self.element_id)
231 236 else:
232 237 return ''
233 238
234 239 class IFrame(object):
235 240 """
236 241 Generic class to embed an iframe in an IPython notebook
237 242 """
238 243
239 244 iframe = """
240 245 <iframe
241 246 width="{width}"
242 247 height="{height}"
243 248 src="{src}{params}"
244 249 frameborder="0"
245 250 allowfullscreen
246 251 ></iframe>
247 252 """
248 253
249 254 def __init__(self, src, width, height, **kwargs):
250 255 self.src = src
251 256 self.width = width
252 257 self.height = height
253 258 self.params = kwargs
254 259
255 260 def _repr_html_(self):
256 261 """return the embed iframe"""
257 262 if self.params:
258 263 try:
259 264 from urllib.parse import urlencode # Py 3
260 265 except ImportError:
261 266 from urllib import urlencode
262 267 params = "?" + urlencode(self.params)
263 268 else:
264 269 params = ""
265 270 return self.iframe.format(src=self.src,
266 271 width=self.width,
267 272 height=self.height,
268 273 params=params)
269 274
270 275 class YouTubeVideo(IFrame):
271 276 """Class for embedding a YouTube Video in an IPython session, based on its video id.
272 277
273 278 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
274 279 do::
275 280
276 281 vid = YouTubeVideo("foo")
277 282 display(vid)
278 283
279 284 To start from 30 seconds::
280 285
281 286 vid = YouTubeVideo("abc", start=30)
282 287 display(vid)
283 288
284 289 To calculate seconds from time as hours, minutes, seconds use
285 290 :class:`datetime.timedelta`::
286 291
287 292 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
288 293
289 294 Other parameters can be provided as documented at
290 295 https://developers.google.com/youtube/player_parameters#Parameters
291 296
292 297 When converting the notebook using nbconvert, a jpeg representation of the video
293 298 will be inserted in the document.
294 299 """
295 300
296 301 def __init__(self, id, width=400, height=300, **kwargs):
297 302 self.id=id
298 303 src = "https://www.youtube.com/embed/{0}".format(id)
299 304 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
300 305
301 306 def _repr_jpeg_(self):
302 307 # Deferred import
303 308 from urllib.request import urlopen
304 309
305 310 try:
306 311 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
307 312 except IOError:
308 313 return None
309 314
310 315 class VimeoVideo(IFrame):
311 316 """
312 317 Class for embedding a Vimeo video in an IPython session, based on its video id.
313 318 """
314 319
315 320 def __init__(self, id, width=400, height=300, **kwargs):
316 321 src="https://player.vimeo.com/video/{0}".format(id)
317 322 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
318 323
319 324 class ScribdDocument(IFrame):
320 325 """
321 326 Class for embedding a Scribd document in an IPython session
322 327
323 328 Use the start_page params to specify a starting point in the document
324 329 Use the view_mode params to specify display type one off scroll | slideshow | book
325 330
326 331 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
327 332
328 333 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
329 334 """
330 335
331 336 def __init__(self, id, width=400, height=300, **kwargs):
332 337 src="https://www.scribd.com/embeds/{0}/content".format(id)
333 338 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
334 339
335 340 class FileLink(object):
336 341 """Class for embedding a local file link in an IPython session, based on path
337 342
338 343 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
339 344
340 345 you would do::
341 346
342 347 local_file = FileLink("my/data.txt")
343 348 display(local_file)
344 349
345 350 or in the HTML notebook, just::
346 351
347 352 FileLink("my/data.txt")
348 353 """
349 354
350 355 html_link_str = "<a href='%s' target='_blank'>%s</a>"
351 356
352 357 def __init__(self,
353 358 path,
354 359 url_prefix='',
355 360 result_html_prefix='',
356 361 result_html_suffix='<br>'):
357 362 """
358 363 Parameters
359 364 ----------
360 365 path : str
361 366 path to the file or directory that should be formatted
362 367 url_prefix : str
363 368 prefix to be prepended to all files to form a working link [default:
364 369 '']
365 370 result_html_prefix : str
366 371 text to append to beginning to link [default: '']
367 372 result_html_suffix : str
368 373 text to append at the end of link [default: '<br>']
369 374 """
370 375 if isdir(path):
371 376 raise ValueError("Cannot display a directory using FileLink. "
372 377 "Use FileLinks to display '%s'." % path)
373 378 self.path = fsdecode(path)
374 379 self.url_prefix = url_prefix
375 380 self.result_html_prefix = result_html_prefix
376 381 self.result_html_suffix = result_html_suffix
377 382
378 383 def _format_path(self):
379 384 fp = ''.join([self.url_prefix, html_escape(self.path)])
380 385 return ''.join([self.result_html_prefix,
381 386 self.html_link_str % \
382 387 (fp, html_escape(self.path, quote=False)),
383 388 self.result_html_suffix])
384 389
385 390 def _repr_html_(self):
386 391 """return html link to file
387 392 """
388 393 if not exists(self.path):
389 394 return ("Path (<tt>%s</tt>) doesn't exist. "
390 395 "It may still be in the process of "
391 396 "being generated, or you may have the "
392 397 "incorrect path." % self.path)
393 398
394 399 return self._format_path()
395 400
396 401 def __repr__(self):
397 402 """return absolute path to file
398 403 """
399 404 return abspath(self.path)
400 405
401 406 class FileLinks(FileLink):
402 407 """Class for embedding local file links in an IPython session, based on path
403 408
404 409 e.g. to embed links to files that were generated in the IPython notebook
405 410 under ``my/data``, you would do::
406 411
407 412 local_files = FileLinks("my/data")
408 413 display(local_files)
409 414
410 415 or in the HTML notebook, just::
411 416
412 417 FileLinks("my/data")
413 418 """
414 419 def __init__(self,
415 420 path,
416 421 url_prefix='',
417 422 included_suffixes=None,
418 423 result_html_prefix='',
419 424 result_html_suffix='<br>',
420 425 notebook_display_formatter=None,
421 426 terminal_display_formatter=None,
422 427 recursive=True):
423 428 """
424 429 See :class:`FileLink` for the ``path``, ``url_prefix``,
425 430 ``result_html_prefix`` and ``result_html_suffix`` parameters.
426 431
427 432 included_suffixes : list
428 433 Filename suffixes to include when formatting output [default: include
429 434 all files]
430 435
431 436 notebook_display_formatter : function
432 437 Used to format links for display in the notebook. See discussion of
433 438 formatter functions below.
434 439
435 440 terminal_display_formatter : function
436 441 Used to format links for display in the terminal. See discussion of
437 442 formatter functions below.
438 443
439 444 Formatter functions must be of the form::
440 445
441 446 f(dirname, fnames, included_suffixes)
442 447
443 448 dirname : str
444 449 The name of a directory
445 450 fnames : list
446 451 The files in that directory
447 452 included_suffixes : list
448 453 The file suffixes that should be included in the output (passing None
449 454 meansto include all suffixes in the output in the built-in formatters)
450 455 recursive : boolean
451 456 Whether to recurse into subdirectories. Default is True.
452 457
453 458 The function should return a list of lines that will be printed in the
454 459 notebook (if passing notebook_display_formatter) or the terminal (if
455 460 passing terminal_display_formatter). This function is iterated over for
456 461 each directory in self.path. Default formatters are in place, can be
457 462 passed here to support alternative formatting.
458 463
459 464 """
460 465 if isfile(path):
461 466 raise ValueError("Cannot display a file using FileLinks. "
462 467 "Use FileLink to display '%s'." % path)
463 468 self.included_suffixes = included_suffixes
464 469 # remove trailing slashes for more consistent output formatting
465 470 path = path.rstrip('/')
466 471
467 472 self.path = path
468 473 self.url_prefix = url_prefix
469 474 self.result_html_prefix = result_html_prefix
470 475 self.result_html_suffix = result_html_suffix
471 476
472 477 self.notebook_display_formatter = \
473 478 notebook_display_formatter or self._get_notebook_display_formatter()
474 479 self.terminal_display_formatter = \
475 480 terminal_display_formatter or self._get_terminal_display_formatter()
476 481
477 482 self.recursive = recursive
478 483
479 484 def _get_display_formatter(self,
480 485 dirname_output_format,
481 486 fname_output_format,
482 487 fp_format,
483 488 fp_cleaner=None):
484 489 """ generate built-in formatter function
485 490
486 491 this is used to define both the notebook and terminal built-in
487 492 formatters as they only differ by some wrapper text for each entry
488 493
489 494 dirname_output_format: string to use for formatting directory
490 495 names, dirname will be substituted for a single "%s" which
491 496 must appear in this string
492 497 fname_output_format: string to use for formatting file names,
493 498 if a single "%s" appears in the string, fname will be substituted
494 499 if two "%s" appear in the string, the path to fname will be
495 500 substituted for the first and fname will be substituted for the
496 501 second
497 502 fp_format: string to use for formatting filepaths, must contain
498 503 exactly two "%s" and the dirname will be subsituted for the first
499 504 and fname will be substituted for the second
500 505 """
501 506 def f(dirname, fnames, included_suffixes=None):
502 507 result = []
503 508 # begin by figuring out which filenames, if any,
504 509 # are going to be displayed
505 510 display_fnames = []
506 511 for fname in fnames:
507 512 if (isfile(join(dirname,fname)) and
508 513 (included_suffixes is None or
509 514 splitext(fname)[1] in included_suffixes)):
510 515 display_fnames.append(fname)
511 516
512 517 if len(display_fnames) == 0:
513 518 # if there are no filenames to display, don't print anything
514 519 # (not even the directory name)
515 520 pass
516 521 else:
517 522 # otherwise print the formatted directory name followed by
518 523 # the formatted filenames
519 524 dirname_output_line = dirname_output_format % dirname
520 525 result.append(dirname_output_line)
521 526 for fname in display_fnames:
522 527 fp = fp_format % (dirname,fname)
523 528 if fp_cleaner is not None:
524 529 fp = fp_cleaner(fp)
525 530 try:
526 531 # output can include both a filepath and a filename...
527 532 fname_output_line = fname_output_format % (fp, fname)
528 533 except TypeError:
529 534 # ... or just a single filepath
530 535 fname_output_line = fname_output_format % fname
531 536 result.append(fname_output_line)
532 537 return result
533 538 return f
534 539
535 540 def _get_notebook_display_formatter(self,
536 541 spacer="&nbsp;&nbsp;"):
537 542 """ generate function to use for notebook formatting
538 543 """
539 544 dirname_output_format = \
540 545 self.result_html_prefix + "%s/" + self.result_html_suffix
541 546 fname_output_format = \
542 547 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
543 548 fp_format = self.url_prefix + '%s/%s'
544 549 if sep == "\\":
545 550 # Working on a platform where the path separator is "\", so
546 551 # must convert these to "/" for generating a URI
547 552 def fp_cleaner(fp):
548 553 # Replace all occurrences of backslash ("\") with a forward
549 554 # slash ("/") - this is necessary on windows when a path is
550 555 # provided as input, but we must link to a URI
551 556 return fp.replace('\\','/')
552 557 else:
553 558 fp_cleaner = None
554 559
555 560 return self._get_display_formatter(dirname_output_format,
556 561 fname_output_format,
557 562 fp_format,
558 563 fp_cleaner)
559 564
560 565 def _get_terminal_display_formatter(self,
561 566 spacer=" "):
562 567 """ generate function to use for terminal formatting
563 568 """
564 569 dirname_output_format = "%s/"
565 570 fname_output_format = spacer + "%s"
566 571 fp_format = '%s/%s'
567 572
568 573 return self._get_display_formatter(dirname_output_format,
569 574 fname_output_format,
570 575 fp_format)
571 576
572 577 def _format_path(self):
573 578 result_lines = []
574 579 if self.recursive:
575 580 walked_dir = list(walk(self.path))
576 581 else:
577 582 walked_dir = [next(walk(self.path))]
578 583 walked_dir.sort()
579 584 for dirname, subdirs, fnames in walked_dir:
580 585 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
581 586 return '\n'.join(result_lines)
582 587
583 588 def __repr__(self):
584 589 """return newline-separated absolute paths
585 590 """
586 591 result_lines = []
587 592 if self.recursive:
588 593 walked_dir = list(walk(self.path))
589 594 else:
590 595 walked_dir = [next(walk(self.path))]
591 596 walked_dir.sort()
592 597 for dirname, subdirs, fnames in walked_dir:
593 598 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
594 599 return '\n'.join(result_lines)
595 600
596 601
597 602 class Code(TextDisplayObject):
598 603 """Display syntax-highlighted source code.
599 604
600 605 This uses Pygments to highlight the code for HTML and Latex output.
601 606
602 607 Parameters
603 608 ----------
604 609 data : str
605 610 The code as a string
606 611 url : str
607 612 A URL to fetch the code from
608 613 filename : str
609 614 A local filename to load the code from
610 615 language : str
611 616 The short name of a Pygments lexer to use for highlighting.
612 617 If not specified, it will guess the lexer based on the filename
613 618 or the code. Available lexers: http://pygments.org/docs/lexers/
614 619 """
615 620 def __init__(self, data=None, url=None, filename=None, language=None):
616 621 self.language = language
617 622 super().__init__(data=data, url=url, filename=filename)
618 623
619 624 def _get_lexer(self):
620 625 if self.language:
621 626 from pygments.lexers import get_lexer_by_name
622 627 return get_lexer_by_name(self.language)
623 628 elif self.filename:
624 629 from pygments.lexers import get_lexer_for_filename
625 630 return get_lexer_for_filename(self.filename)
626 631 else:
627 632 from pygments.lexers import guess_lexer
628 633 return guess_lexer(self.data)
629 634
630 635 def __repr__(self):
631 636 return self.data
632 637
633 638 def _repr_html_(self):
634 639 from pygments import highlight
635 640 from pygments.formatters import HtmlFormatter
636 641 fmt = HtmlFormatter()
637 642 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
638 643 return style + highlight(self.data, self._get_lexer(), fmt)
639 644
640 645 def _repr_latex_(self):
641 646 from pygments import highlight
642 647 from pygments.formatters import LatexFormatter
643 648 return highlight(self.data, self._get_lexer(), LatexFormatter())
General Comments 0
You need to be logged in to leave comments. Login now