Forked from nathanielatom/bokeh_audio_player.py
Last active
February 18, 2023 17:39
-
-
Save david-macleod/6d8fb75f4d0003b0908b7ef2dca9c5b1 to your computer and use it in GitHub Desktop.
Custom Bokeh Model for audio playback (not so useful on its own; meant to be integrated with plot interactions).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Forked from nathanielatom, updated to work with typescript and bokeh==1.3.4 | |
from bokeh.layouts import column, layout | |
from bokeh.models import CustomJS, Model, LayoutDOM | |
from bokeh.document import Document | |
from bokeh.models.widgets import Button, Slider, Toggle | |
from bokeh.core.properties import Instance, String | |
from bokeh.io import save, output_file, show | |
from bokeh.util.compiler import TypeScript | |
from fontawesome.fontawesome_icon import FontAwesomeIcon as Icon | |
# Requires https://github.com/bokeh/bokeh/tree/master/examples/custom/font-awesome | |
TS_CODE = """ | |
import {LayoutDOM, LayoutDOMView} from "models/layouts/layout_dom" | |
import {LayoutItem} from "core/layout" | |
import * as p from "core/properties" | |
declare class Howl { | |
constructor(src: object) | |
on(event: String, fn: Function, id?: Number): void | |
duration(id?: Number): Number | |
seek(seek?: Number, id?: Number): Number | |
stop(id?: Number): void | |
pause(id?: Number): void | |
playing(id?: Number): Boolean | |
play(id?: String | Number): void | |
volume(volume?: Number, id?: Number): Number | |
} | |
export class AudioPlayerView extends LayoutDOMView { | |
model: AudioPlayer | |
private _audio: Howl | |
initialize(): void { | |
super.initialize() | |
// TODO replace this with ___javascript___ = ["https://cdnjs.cloudflare.com/ajax/libs/howler/2.0.1/howler.core.min.js"] | |
const url = "https://cdnjs.cloudflare.com/ajax/libs/howler/2.0.1/howler.core.min.js" | |
const script = document.createElement("script") | |
script.onload = () => this._init() | |
script.async = false | |
script.src = url | |
document.head.appendChild(script) | |
} | |
private _init(): void { | |
this._audio = new Howl({src: [this.model.audio_source]}) | |
this._audio.on('play', () => this.model.play_pause_button.icon = this.model.icon_pause) | |
this._audio.on('pause', () => this.model.play_pause_button.icon = this.model.icon_play) | |
this._audio.on('stop', this.stop_button) | |
this._audio.on('load', () => this.model.seek_bar.end = this._audio.duration()) | |
this._audio.on('end', this.stop) | |
this.connect(this.model.play_pause_button.properties.active.change, this.play_pause_press) | |
this.connect(this.model.stop_button.properties.clicks.change, this.stop) | |
this.connect(this.model.seek_bar.properties.value.change, this.connect_seek) | |
this.connect(this.model.volume_bar.properties.value.change, () => { | |
this._audio.volume(this.model.volume_bar.value) | |
}) | |
} | |
stop_button(): void { | |
this.model.play_pause_button.icon = this.model.icon_play | |
this.model.play_pause_button.active = false | |
} | |
connect_seek(): void { | |
if (!this._audio.playing()) | |
this._audio.seek(this.model.seek_bar.value) | |
} | |
play(): void { | |
this._audio.play() | |
this.step() | |
} | |
pause(): void { | |
this._audio.pause() | |
} | |
stop(): void { | |
this._audio.stop() | |
this.step() | |
} | |
play_pause_press(): void { | |
if (this.model.play_pause_button.active) | |
this.play() | |
else | |
this.pause() | |
} | |
update_seek_bar(): void { | |
this.model.seek_bar.value = this._audio.seek() | |
} | |
step(): void { | |
this.update_seek_bar() | |
if (this._audio.playing()) | |
requestAnimationFrame(() => {this.step()}) | |
} | |
get child_models(): LayoutDOM[] { | |
return [] | |
} | |
_update_layout(): void { | |
this.layout = new LayoutItem() | |
this.layout.set_sizing(this.box_sizing()) | |
} | |
} | |
export namespace AudioPlayer { | |
export type Attrs = p.AttrsOf<Props> | |
export type Props = LayoutDOM.Props & { | |
audio_source: p.Property<string> | |
icon_play: p.Property<any> | |
icon_pause: p.Property<any> | |
icon_stop: p.Property<any> | |
play_pause_button: p.Property<any> | |
stop_button: p.Property<any> | |
seek_bar: p.Property<any> | |
volume_bar: p.Property<any> | |
} | |
} | |
export interface AudioPlayer extends AudioPlayer.Attrs {} | |
export class AudioPlayer extends LayoutDOM { | |
properties: AudioPlayer.Props | |
constructor(attrs?: Partial<AudioPlayer.Attrs>) { | |
super(attrs) | |
} | |
static __name__ = "AudioPlayer" | |
static initClass() { | |
this.prototype.default_view = AudioPlayerView | |
this.define<AudioPlayer.Props>({ | |
audio_source: [ p.String ], | |
icon_play: [ p.Any ], | |
icon_pause: [ p.Any ], | |
icon_stop: [ p.Any ], | |
play_pause_button: [ p.Any ], | |
stop_button: [ p.Any ], | |
seek_bar: [ p.Any ], | |
volume_bar: [ p.Any ], | |
}) | |
} | |
} | |
AudioPlayer.initClass() | |
""" | |
class AudioPlayer(LayoutDOM): | |
""" | |
Custom Bokeh Model for audio playback (not so useful on its own; meant to be integrated with plot interactions). | |
""" | |
__implementation__ = TypeScript(TS_CODE) | |
audio_source = String(help="Audio file or base64 encoded file with header.") | |
icon_play = Instance(Icon, default=Icon(icon_name='play', size=1.5), help="Icon used for play.") | |
icon_pause = Instance(Icon, default=Icon(icon_name='pause', size=1.5), help="Icon used for pause.") | |
icon_stop = Instance(Icon, default=Icon(icon_name='stop', size=1.5), help="Icon used for stop.") | |
play_pause_button = Instance(Toggle, default=Toggle(label="", width=50, button_type="success", icon=icon_play._default)) | |
stop_button = Instance(Button, default=Button(label="", width=50, button_type="success", icon=icon_stop._default)) | |
seek_bar = Instance(Slider, default=Slider(start=0, end=100, step=0.1, value=0, title="Time [s]", width=350), help="Seek bar to control playback.") | |
volume_bar = Instance(Slider, default=Slider(start=0, end=1, step=0.01, value=0.5, title="Gain", width=200), help="Volume bar to control playback gain.") | |
doc = Document() | |
playa = AudioPlayer(audio_source="http://sampleswap.org/samples-ghost/PUBLIC%20DOMAIN%20MUSIC/20839[kb]Wagner-die_walkure_fantasie.mp3.mp3") | |
doc.add_root(playa) | |
doc.add_root(layout([[playa.play_pause_button, playa.stop_button, playa.volume_bar], | |
[playa.seek_bar]])) | |
output_file("audio_player.html") | |
save(doc) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment