Skip to content

Instantly share code, notes, and snippets.

@loretoparisi
Last active December 20, 2023 00:08
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loretoparisi/c5c2b878ba553de05d5a928c85f89a5f to your computer and use it in GitHub Desktop.
Save loretoparisi/c5c2b878ba553de05d5a928c85f89a5f to your computer and use it in GitHub Desktop.
Wavesurfer js setup with timing labels and time formating callback
/**
* Wavesurfer.js minimalistic example with
* regions loading, save, plugins, waveform loading from json, waveform zoom, player events, keyboard codes, auto play
* @author: Loreto Parisi (loretoparisi at gmail dot com) - mainly, some code and functions adpated from Wavesurfer examples
* @disclaimer: code adapted from online examples
*/
// JavaScript
// Wrap the native DOM audio element play function and handle any autoplay errors
Audio.prototype.play = (function(play) {
return function() {
var audio = this,
args = arguments,
promise = play.apply(audio, args);
if (promise !== undefined) {
promise.catch(_ => {
// Autoplay was prevented. This is optional, but add a button to start playing.
});
}
};
})(Audio.prototype.play);
/**
* Save annotations to localStorage.
*/
function saveRegions() {
localStorage.regions = JSON.stringify(
Object.keys(wavesurfer.regions.list).map(function(id) {
var region = wavesurfer.regions.list[id];
return {
start: region.start,
end: region.end,
attributes: region.attributes,
data: region.data
};
})
);
} //saveRegions
/**
* Load regions from localStorage.
*/
function loadRegions(regions) {
regions.forEach(function(region) {
// number of colors, transparency
var randomColor = Math.floor(Math.random()*16777215).toString(16);
region.color = randomColor;
wavesurfer.addRegion(region);
});
} //loadRegions
/**
* Use formatTimeCallback to style the notch labels as you wish, such
* as with more detail as the number of pixels per second increases.
*
* Here we format as M:SS.frac, with M suppressed for times < 1 minute,
* and frac having 0, 1, or 2 digits as the zoom increases.
*
* Note that if you override the default function, you'll almost
* certainly want to override timeInterval, primaryLabelInterval and/or
* secondaryLabelInterval so they all work together.
*
* @param: seconds
* @param: pxPerSec
*/
function formatTimeCallback(seconds, pxPerSec) {
seconds = Number(seconds);
var minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
// fill up seconds with zeroes
var secondsStr = Math.round(seconds).toString();
if (pxPerSec >= 25 * 10) {
secondsStr = seconds.toFixed(2);
} else if (pxPerSec >= 25 * 1) {
secondsStr = seconds.toFixed(1);
}
if (minutes > 0) {
if (seconds < 10) {
secondsStr = '0' + secondsStr;
}
return `${minutes}:${secondsStr}`;
}
return secondsStr;
}
/**
* Use timeInterval to set the period between notches, in seconds,
* adding notches as the number of pixels per second increases.
*
* Note that if you override the default function, you'll almost
* certainly want to override formatTimeCallback, primaryLabelInterval
* and/or secondaryLabelInterval so they all work together.
*
* @param: pxPerSec
*/
function timeInterval(pxPerSec) {
var retval = 1;
if (pxPerSec >= 25 * 100) {
retval = 0.01;
} else if (pxPerSec >= 25 * 40) {
retval = 0.025;
} else if (pxPerSec >= 25 * 10) {
retval = 0.1;
} else if (pxPerSec >= 25 * 4) {
retval = 0.25;
} else if (pxPerSec >= 25) {
retval = 1;
} else if (pxPerSec * 5 >= 25) {
retval = 5;
} else if (pxPerSec * 15 >= 25) {
retval = 15;
} else {
retval = Math.ceil(0.5 / pxPerSec) * 60;
}
return retval;
}
/**
* Return the cadence of notches that get labels in the primary color.
* EG, return 2 if every 2nd notch should be labeled,
* return 10 if every 10th notch should be labeled, etc.
*
* Note that if you override the default function, you'll almost
* certainly want to override formatTimeCallback, primaryLabelInterval
* and/or secondaryLabelInterval so they all work together.
*
* @param pxPerSec
*/
function primaryLabelInterval(pxPerSec) {
var retval = 1;
if (pxPerSec >= 25 * 100) {
retval = 10;
} else if (pxPerSec >= 25 * 40) {
retval = 4;
} else if (pxPerSec >= 25 * 10) {
retval = 10;
} else if (pxPerSec >= 25 * 4) {
retval = 4;
} else if (pxPerSec >= 25) {
retval = 1;
} else if (pxPerSec * 5 >= 25) {
retval = 5;
} else if (pxPerSec * 15 >= 25) {
retval = 15;
} else {
retval = Math.ceil(0.5 / pxPerSec) * 60;
}
return retval;
}
/**
* Return the cadence of notches to get labels in the secondary color.
* EG, return 2 if every 2nd notch should be labeled,
* return 10 if every 10th notch should be labeled, etc.
*
* Secondary labels are drawn after primary labels, so if
* you want to have labels every 10 seconds and another color labels
* every 60 seconds, the 60 second labels should be the secondaries.
*
* Note that if you override the default function, you'll almost
* certainly want to override formatTimeCallback, primaryLabelInterval
* and/or secondaryLabelInterval so they all work together.
*
* @param pxPerSec
*/
function secondaryLabelInterval(pxPerSec) {
// draw one every 10s as an example
return Math.floor(10 / timeInterval(pxPerSec));
}
// true to activate colormap plugin
const colorMap = false;
// true to use a waveform from json data points
const loadWaveformFromJSON = true;
const minPxPerSec = 200;
// the audio file uri to be loaded
const myAudioFilePath = ""
// the uri of the json waveform generator
// please note that minPxPerSec must be coherent with dots returned from this waveform
const myWaveformGeneratorPath = ""
var plugins = [
WaveSurfer.regions.create({
/*regions: [
{ "id": 1, "color": "red", "start": 2.96, "end": 3.5, "data": {}, "attributes": {"label": "stars", "highlight":true}}
]*/
}),
WaveSurfer.minimap.create({
height: 100,
waveColor: '#ddd',
progressColor: '#999',
cursorColor: '#999'
}),
WaveSurfer.timeline.create({
container: '#wave-timeline',
formatTimeCallback: formatTimeCallback,
timeInterval: timeInterval,
primaryLabelInterval: primaryLabelInterval,
secondaryLabelInterval: secondaryLabelInterval,
primaryColor: 'blue',
secondaryColor: 'red',
primaryFontColor: 'blue',
secondaryFontColor: 'red'
})
]
if (colorMap) { // add spectrogram plugin, running on the main thread it freezes the UI
plugins.push(WaveSurfer.spectrogram.create({
container: '#wave-spectrogram',
labels: true,
colorMap: colorMap
}))
};
// create waveform
wavesurfer = WaveSurfer.create({
container: document.querySelector('#waveform'),
waveColor: '#D9DCFF',
progressColor: '#4353FF',
cursorColor: '#4353FF',
normalize: true,
//LP: danger higher values will crash the computer
pixelRatio: 1,
minPxPerSec: minPxPerSec,
scrollParent: true,
// media playback
//backend: 'MediaElement',
//// bar visualization
barWidth: 3,
barRadius: 3,
barGap: 1,
barHeight: 1,
//// bar visualization
hideScrollbar: false,
cursorWidth: 1,
height: 300,
plugins: plugins
});
// set initial zoom to match slider value
wavesurfer.zoom(minPxPerSec);
// Zoom slider
var slider = document.querySelector('[data-action="zoom"]');
slider.value = wavesurfer.params.minPxPerSec;
//slider.min = wavesurfer.params.minPxPerSec;
slider.addEventListener('input', function() {
console.log("pxPerSec", this.value);
//zoom(pxPerSec) – Horizontally zooms the waveform in and out. The parameter is a number of horizontal pixels per second of audio.
// It also changes the parameter minPxPerSec and enables the scrollParent option.
wavesurfer.zoom(Number(this.value));
});
// events
wavesurfer.on('error', function(e) {
});
// bind wavesurfer events
wavesurfer.on('ready', function() {
// load here regions data
var data = [];
// just adding an example region
var elem = {
start: 0,
end: 5,
data: {},
attributes: {
label: "my label"
},
drag: false,
resize: false
};
data.push(elem);
loadRegions(data);
// now save loaded regions to local storage
saveRegions();
}); // ready
// load waveform from json
if (loadWaveformFromJSON) {
wavesurfer.util
.fetchFile({
responseType: 'json',
url: `${myWaveformGeneratorPath}`
})
.on('success', function(data) {
var points = data.data || data;
wavesurfer.load(
`${myAudioFilePath}`,
points
);
if (autoPlay) { // autoplay
// not working: The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page
wavesurfer.seekTo(0);
setTimeout(() => wavesurfer.play(), 1000);
} else { // seek to load annotations data
wavesurfer.seekTo(0);
}
});
} else {
wavesurfer.load(`${myAudioFilePath}`);
}
// player actions
var GLOBAL_ACTIONS = {
'play': function() {
wavesurfer.playPause();
},
'back': function() {
wavesurfer.skipBackward();
},
'forth': function() {
wavesurfer.skipForward();
},
'toggle-mute': function() {
wavesurfer.toggleMute();
}
};
// handle keyboard events
document.addEventListener('keydown', function(e) {
var map = {
32: 'play', // space
37: 'back', // left
39: 'forth' // right
};
var action = map[e.keyCode];
if (action in GLOBAL_ACTIONS) {
if (document == e.target || document.body == e.target) {
e.preventDefault();
}
GLOBAL_ACTIONS[action](e);
}
});
// bind actions to events
[].forEach.call(document.querySelectorAll('[data-action]'), function(el) {
el.addEventListener('click', function(e) {
var action = e.currentTarget.dataset.action;
if (action in GLOBAL_ACTIONS) {
e.preventDefault();
GLOBAL_ACTIONS[action](e);
}
});
});
@SebastianBetz
Copy link

Hey there :)
Just stumbled upon you snippet here.
Do you have a gist or a pen somewhere, where you can play with your extension?
I'm currently doing the same (adding labels, improving region draggin and maybe I can also add my 2 cents.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment