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