Skip to content

Instantly share code, notes, and snippets.

@david-macleod
Forked from nathanielatom/bokeh_audio_player.py
Last active February 18, 2023 17:39
Show Gist options
  • Save david-macleod/6d8fb75f4d0003b0908b7ef2dca9c5b1 to your computer and use it in GitHub Desktop.
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).
# 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