Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@jessuni
Last active July 3, 2023 05:58
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jessuni/94120edb2b6d8442388b84fa5946c915 to your computer and use it in GitHub Desktop.
Save jessuni/94120edb2b6d8442388b84fa5946c915 to your computer and use it in GitHub Desktop.
Audio Oscilloscope (Frequency Byte) - w/ web audio API and canvas
const audioCtx = new (window.AudioContext || window.webkitAudioContext)()
// route audio element to analyser, analyser to destination
const source = audioCtx.createMediaElementSource(player.audio)
const analyser = audioCtx.createAnalyser()
source.connect(analyser)
analyser.connect(audioCtx.destination)
const bufferLength = analyser.frequencyBinCount
const dataArray = new Uint8Array(bufferLength)
// init canvas
const canvas = document.createElement('canvas')
// generate linear waveform with LinearWave
const linearWave = new LinearWave({
data: dataArray,
canvas: canvas,
gap: 2,
sliceCount: 100,
heightScale: 0.4,
color: 'salmon',
})
// or generate circle waveform with CircleWave
const circleWave = new CircleWave({
data: dataArray,
canvas: canvas,
gap: 1,
sliceCount: 50,
heightScale: 0.4,
color: 'rainbow',
})
//or generate dot waveform with DotWave
const dotWave = new DotWave({
data: dataArray,
canvas: canvas,
gap: 1,
sliceCount: 50,
radiusScale: 0.005,
centerRadius: 32,
radius: 1,
distance: 3,
rows: 8,
color: 'rainbow',
})
document.body.append(canvas)
// generate linear wave
linearWave.draw(analyser)
//generate circle wave
circleWave.draw(analyser)
const _config = {
width: 200,
height: 200,
gap: 1,
sliceCount: 50,
heightScale: 0.4,
centerRadius: 32,
color: 'rainbow',
}
/**
* analyser: Object,
* data: Array,
* canvas: Object,
* gap: Number,
* sliceCount: Number,
* heightScale: Number,
* centerRadius: Number,
* width: Number,
* height: Number,
* color: String,
*/
class CircleWave {
constructor(options) {
// handle options
this.analyser = options.analyser
this.data = options.data
this.canvas = options.canvas
this.gap = options.gap || _config.gap
this.sliceCount = options.sliceCount || _config.sliceCount
this.heightScale = options.heightScale || _config.heightScale
this.color = options.color || _config.color
this.centerRadius = options.centerRadius || _config.centerRadius
this.canvas.width = options.width || _config.width
this.canvas.height = options.height || _config.height
this.ctx = this.canvas.getContext('2d')
// fill as much bars as canvas width
this.widthPerSlice = this.canvas.width / this.sliceCount
}
draw() {
requestAnimationFrame(() => this.draw())
this.analyser.getByteFrequencyData(this.data)
// clear context on every redrawing
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.beginPath()
let startAngle = 0
const sectionAngle = 2 * Math.PI / this.sliceCount
const gap = 2 * Math.PI / 360 * this.gap
const colorProportion = Math.floor(360 / this.sliceCount)
for (var i = 0; i < this.sliceCount; i++) {
const radius = this.data[i] * this.heightScale
const color = this.color === 'rainbow' ? 'hsl(' + i * colorProportion + ',80%,60%)' : this.color
// sectionAngle - gap = how much angle each portion actually occupies
this.drawPieSlice(this.ctx, this.canvas.width / 2, this.canvas.height / 2, radius, startAngle, startAngle + sectionAngle - gap, color)
startAngle += sectionAngle
}
// using composite to subtract a circle from center
// note that this is not adding a white circle to center
if (this.centerRadius) {
this.ctx.globalCompositeOperation = 'destination-out'
this.ctx.beginPath()
this.ctx.arc(this.canvas.width / 2, this.canvas.height / 2, this.centerRadius, 0, 2 * Math.PI, '#fff')
this.ctx.fill()
// reset composite
this.ctx.globalCompositeOperation = 'source-over'
}
}
drawPieSlice(ctx, centerX, centerY, radius, startAngle, endAngle, color) {
ctx.fillStyle = color
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius, startAngle, endAngle)
ctx.closePath()
ctx.fill()
ctx.stroke()
}
}
export default CircleWave
const _config = {
width: 300,
height: 300,
gap: 1,
sliceCount: 50,
radiusScale: 0.005,
centerRadius: 32,
radius: 1,
distance: 3,
rows: 8,
color: '#fff',
}
/**
* analyser: Object,
* data: Array,
* canvas: Object,
* gap: Number,
* sliceCount: Number,
* radiusScale: Number,
* centerRadius: Number,
* radius: Number,
* distance: Number,
* rows: number
* width: Number,
* height: Number,
* color: String,
*/
class DotWave {
constructor(options) {
this.analyser = options.analyser
this.data = options.data
this.canvas = options.canvas
this.gap = options.gap || _config.gap
this.sliceCount = options.sliceCount || _config.sliceCount
this.radiusScale = options.radiusScale || _config.radiusScale
this.color = options.color || _config.color
this.centerRadius = options.centerRadius || _config.centerRadius
this.radius = options.radius || _config.radius
this.distance = options.distance || _config.distance
this.rows = options.rows || _config.rows
this.canvas.width = options.width || _config.width
this.canvas.height = options.height || _config.height
this.ctx = this.canvas.getContext('2d')
// evenly distribute dots to each slice
this.proportion = 360 / this.sliceCount
this.circles = []
for (let slice = 0; slice < this.sliceCount; slice++) {
const sectionAngle = 2 * Math.PI / this.sliceCount
const radians = sectionAngle * (slice + 1)
this.circles[slice] = []
let distance = 0
for (let row = 0; row < this.rows; row++) {
let radius
if (row === 0) {
radius = this.centerRadius
} else {
// increase radius as row increases
radius = distance + this.distance*(0.8+ 0.8 * row)
}
const offsetX = this.canvas.width / 2
const offsetY = this.canvas.height / 2
const x = offsetX + radius * Math.cos(radians)
const y = offsetY + radius * Math.sin(radians)
this.circles[slice].push({ x, y })
distance = radius
}
}
}
draw() {
setTimeout(() => {
requestAnimationFrame(() => this.draw())
}, 30)
this.analyser.getByteFrequencyData(this.data)
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.beginPath()
for (let i = 0; i < this.sliceCount; i++) {
const dotRaidus = this.data[i] * this.radiusScale
for (let j = 0; j < this.rows; j++) {
const hue = i * this.proportion
const color = this.color === 'rainbow' ? 'hsl(' + hue + ',80%,60%)' : this.color
// increase radius as row increases
this.drawPieSlice(this.ctx, this.circles[i][j].x, this.circles[i][j].y, dotRaidus * (1.2 + 120 * this.radiusScale * j), 0, 2 * Math.PI, color)
}
}
}
drawPieSlice(ctx, centerX, centerY, radius, startAngle, endAngle, color) {
ctx.fillStyle = color
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius, startAngle, endAngle)
ctx.closePath()
ctx.fill()
}
}
export default DotWave
const _config = {
width: 800,
height: 200,
gap: 2,
sliceCount: 100,
heightScale: 0.4,
}
/**
* analyser: Object,
* data: Array,
* canvas: Object,
* gap: Number,
* sliceCount: Number,
* heightScale: Number,
* width: Number,
* height: Number,
* color: String,
*/
class LinearWave {
constructor(options) {
// handle options
this.analyser = options.analyser
this.data = options.data
this.canvas = options.canvas
this.gap = options.gap || _config.gap
this.sliceCount = options.sliceCount || _config.sliceCount
this.heightScale = options.heightScale || _config.heightScale
this.color = options.color || '#555'
this.canvas.width = options.width || _config.width
this.canvas.height = options.height || _config.height
this.ctx = this.canvas.getContext('2d')
// fill as much bars as canvas width
this.widthPerSlice = this.canvas.width / this.sliceCount
}
draw() {
// auto repaint canvas on change
requestAnimationFrame(() => this.draw())
// populate the data array with the frequency data
this.analyser.getByteFrequencyData(this.data)
// white as canvas background color
this.ctx.fillStyle = 'rgb(255,255,255)'
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.beginPath()
for (var i = 0; i < this.sliceCount; i++) {
const barHeight = this.data[i] * this.heightScale
this.ctx.fillStyle = this.color
this.ctx.fillRect(this.widthPerSlice * i, this.canvas.height - barHeight, this.widthPerSlice - this.gap, barHeight)
}
}
}
export default LinearWave
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment