Last active
July 3, 2023 05:58
-
-
Save jessuni/94120edb2b6d8442388b84fa5946c915 to your computer and use it in GitHub Desktop.
Audio Oscilloscope (Frequency Byte) - w/ web audio API and canvas
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
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) |
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
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 |
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
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 |
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
// rename gist |
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
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