I have enough of this working to provide a starting point for audio applications.
I still can’t figure out how to include scripts in JSComponent, so:
%%html
<!-- script src="https://cdn.jsdelivr.net/npm/wavesurfer.js@7"></script -->
<!-- script src="https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.plugin/wavesurfer.envelope.min.js"></script-->
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/wavesurfer.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/envelope.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/hover.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/minimap.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/record.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/regions.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/spectrogram.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/timeline.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7.8.14/dist/plugins/zoom.min.js"></script>
import panel as pn; pn.extension();
import param
from IPython.display import Audio
import soundfile as sf
import base64
class WaveSurferForPanel(pn.custom.JSComponent):
width = param.Integer(default=500, doc="Width of the canvas")
height = param.Integer(default=320, doc="Height of the canvas")
audio_rate = param.Integer(default=0, doc="sample rate of audio")
data = param.String( default="");
zoom_level = param.Integer(default= 100, bounds=(10,1000), doc='time series zoom level')
options = param.ListSelector(default=['timeline', 'spectrogram', 'zoom', 'hover'], objects=['spectrogram', 'timeline', 'minimap', 'envelope', 'hover', 'record', 'zoom'])
# 'regions',
@staticmethod
def encode(data, rate=None, with_header=True):
"""Transform a wave file or a numpy array to a PCM bytestring"""
if with_header:
return Audio(data, rate=rate).src_attr()
data = np.clip(data, -1, 1)
scaled, nchan = Audio._validate_and_normalize_with_numpy(data, False)
return base64.b64encode(scaled).decode("ascii")
def load_audio(self, url):
rate = sf.info(url).samplerate if self.audio_rate <= 0 else self.audio_rate
print( "LOAD ", url, "sample rate ", rate);
self.data = self.encode(url, rate)
_esm = """
export function render({ model, el }) {
// Create a container div for the waveform
const id = Math.random().toString(36).substr(2, 9);
const container = document.createElement('div');
container.id = 'waveform_' + id;
container.style.width = model.width + 'px';
container.style.height = model.height + 'px';
container.style.border = '1px solid black';
el.appendChild(container);
function mk_div(nm) {
const d = document.createElement('div');
d.id = nm + id;
container.appendChild(d);
return d;
}
const tl = mk_div('timeline_');
// =================================================================================== WaveSurfer
// Initialize WaveSurfer
const wavesurfer = WaveSurfer.create({
container: container,
waveColor: '#4F4A85',
progressColor: '#383351',
backend: 'WebAudio',
responsive: true,
minPxPerSec: 100,
dragToSeek: true,
});
wavesurfer.on('interaction', () => { wavesurfer.playPause() });
// =================================================================================== Timeline
// add a time axis
function create_timeline() {
return WaveSurfer.Timeline.create({
height: 15,
timeInterval: 0.1,
primaryLabelInterval: 1,
insertPosition: 'beforebegin',
style: {
fontSize: '11px',
color: 'black',
},
});
}
if (model.options.includes('timeline')) {
wavesurfer.registerPlugin(create_timeline());
}
// =================================================================================== Minimap
// add a small view of the whole file for easy reference and navigation
function create_minimap() {
return WaveSurfer.Minimap.create({
height: 30,
waveColor: '#DDDD',
progressColor: '#9999',
normalize: true,
plugins: [WaveSurfer.Hover.create({
lineColor: '#DDDD',
lineWidth: 1,
labelBackground: '#5555',
labelColor: '#FFFF',
labelSize: '11px',
})],
});
}
if (model.options.includes('minimap')) {
wavesurfer.registerPlugin(create_minimap());
}
// =================================================================================== Spectrogram
function create_spectrogram() {
return WaveSurfer.Spectrogram.create({
labels: true,
colorMap: 'igray', // 'gray', 'roseus'
fftSamples: 2048,
});
}
if (model.options.includes('spectrogram')) {
wavesurfer.registerPlugin(create_spectrogram());
}
// =================================================================================== Zoom
// Zoom in or out on the waveform. If spectrogram is displayed, this takes a while!
function create_zoom() {
return WaveSurfer.Zoom.create({
scale: 0.5,
maxZoom: 1000,
});
}
if (model.options.includes('zoom')) {
wavesurfer.registerPlugin(create_zoom());
model.on('change:zoom_level', () => {
wavesurfer.zoom(model.zoom_level);
});
}
// ===================================================================================
function create_hover() {
return WaveSurfer.Hover.create({
lineColor: 'red',
lineWidth: 1,
labelBackground: '#5555',
labelColor: '#FFFF',
labelSize: '11px',
});
}
if (model.options.includes('hover')) {
wavesurfer.registerPlugin(create_hover());
}
// ===================================================================================
wavesurfer.on('error', function(e) {
console.error('Wavesurfer error:', e);
// Display user-friendly error message
});
// ===================================================================================
model.on('change:data', () => {
console.log("got data");
wavesurfer.load( model.data );
});
return container;
// ===================================================================================
wavesurfer.on('error', function(e) {
console.error('Wavesurfer error:', e);
// Display user-friendly error message
});
// ===================================================================================
model.on('change:data', () => {
console.log("got data");
wavesurfer.load( model.data );
});
return container;
}
"""
# ---------------------------------------------------------------------------------------
ws = WaveSurferForPanel()
pn.Column( ws, height=ws.height+10 ).servable()