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