Created
September 4, 2020 07:51
-
-
Save jamesliu96/2007825feae7c8354a029eaa4097245f to your computer and use it in GitHub Desktop.
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 { ccclass, property } = cc._decorator; | |
const floor = (n: number) => ~~n; | |
const round = (n: number) => floor(n + 0.5); | |
function HSVtoRGB(h: number, s: number, v: number) { | |
let r: number, | |
g: number, | |
b: number, | |
i: number, | |
f: number, | |
p: number, | |
q: number, | |
t: number; | |
i = floor(h * 6); | |
f = h * 6 - i; | |
p = v * (1 - s); | |
q = v * (1 - f * s); | |
t = v * (1 - (1 - f) * s); | |
switch (i % 6) { | |
case 0: { | |
(r = v), (g = t), (b = p); | |
break; | |
} | |
case 1: { | |
(r = q), (g = v), (b = p); | |
break; | |
} | |
case 2: { | |
(r = p), (g = v), (b = t); | |
break; | |
} | |
case 3: { | |
(r = p), (g = q), (b = v); | |
break; | |
} | |
case 4: { | |
(r = t), (g = p), (b = v); | |
break; | |
} | |
case 5: { | |
(r = v), (g = p), (b = q); | |
break; | |
} | |
} | |
return { | |
r: round(r * 255), | |
g: round(g * 255), | |
b: round(b * 255), | |
}; | |
} | |
type Point = { | |
x: number; | |
y: number; | |
}; | |
type Size = { | |
w: number; | |
h: number; | |
}; | |
type Rect = Point & Size; | |
enum Mode { | |
Bar, | |
Sine, | |
Spect, | |
} | |
@ccclass | |
export default class Muse extends cc.Component { | |
@property({ type: cc.Enum(Mode) }) | |
public mode = Mode.Bar; | |
@property(cc.Boolean) | |
public autoColor = false; | |
@property(cc.Color) | |
public color = cc.Color.CYAN; | |
@property({ | |
type: cc.Integer, | |
min: 1 << 5, | |
max: 1 << 15, | |
displayName: 'FFT Size', | |
tooltip: 'Fast Fourier Transform (FFT) Size', | |
}) | |
public fftSize = 2048; | |
public static Mode = Mode; | |
private audioMedia?: MediaStream; | |
private audioContext?: AudioContext; | |
private audioSource?: MediaStreamAudioSourceNode; | |
private audioAnalyser?: AnalyserNode; | |
private graphics: cc.Graphics; | |
public get volume() { | |
return this._volume; | |
} | |
private _volume = 0; | |
public get offsetX() { | |
return -this.node.anchorX * this.node.width; | |
} | |
public get offsetY() { | |
return -this.node.anchorY * this.node.height; | |
} | |
public onLoad() { | |
this.graphics = this.node.addComponent(cc.Graphics); | |
} | |
public async start() { | |
this.audioMedia = await navigator.mediaDevices.getUserMedia({ | |
audio: true, | |
}); | |
this.audioContext = new AudioContext(); | |
this.audioSource = this.audioContext.createMediaStreamSource( | |
this.audioMedia | |
); | |
this.audioAnalyser = this.audioContext.createAnalyser(); | |
this.audioSource.connect(this.audioAnalyser); | |
} | |
public update(dt: number) { | |
this.graphics.clear(); | |
switch (this.mode) { | |
case Mode.Bar: { | |
this.drawBar(); | |
break; | |
} | |
case Mode.Sine: { | |
this.drawSine(); | |
break; | |
} | |
case Mode.Spect: { | |
this.drawSpect(); | |
break; | |
} | |
} | |
this._volume = this.getVolume(); | |
} | |
private isReady() { | |
return Boolean(this.audioAnalyser); | |
} | |
private drawBar() { | |
this.graphics.fillColor = this.autoColor | |
? this.getAutoColor(this._volume) | |
: this.color; | |
const rects = this.getBarRects(); | |
rects.forEach(({ x, y, w, h }) => this.graphics.fillRect(x, y, w, h)); | |
} | |
private getBarRects() { | |
const rects: Rect[] = []; | |
if (this.isReady()) { | |
const data = this.getByteFrequencyData(); | |
for (let i = 0; i < this.node.width; i++) { | |
const d = data[floor((i / this.node.width) * data.length)]; | |
const dRate = d / 255; | |
const [x, y, w, h] = [ | |
i + this.offsetX, | |
0 + this.offsetY, | |
1, | |
dRate * this.node.height, | |
]; | |
rects.push({ x, y, w, h }); | |
} | |
} | |
return rects; | |
} | |
private drawSine() { | |
this.graphics.strokeColor = this.autoColor | |
? this.getAutoColor(this._volume) | |
: this.color; | |
this.graphics.lineWidth = 1; | |
this.getSinePoints().forEach(({ x, y }, i) => | |
i === 0 ? this.graphics.moveTo(x, y) : this.graphics.lineTo(x, y) | |
); | |
this.graphics.stroke(); | |
} | |
private getSinePoints() { | |
const rects: Point[] = []; | |
if (this.isReady()) { | |
const data = this.getByteTimeDomainData(); | |
for (let i = 0; i < this.node.width; i++) { | |
const d = data[floor((i / this.node.width) * data.length)]; | |
const dRate = d / 255; | |
const [x, y] = [ | |
i + this.offsetX, | |
dRate * this.node.height + this.offsetY, | |
]; | |
rects.push({ x, y }); | |
} | |
} | |
return rects; | |
} | |
private spectData: Uint8Array[] = []; | |
private drawSpect() { | |
if (this.isReady()) { | |
const data = this.getByteFrequencyData(); | |
this.spectData.push(data); | |
while (this.spectData.length > this.node.height) { | |
this.spectData.shift(); | |
} | |
for (let y = 0; y < this.spectData.length; y++) { | |
const spect = this.spectData[y]; | |
for (let x = 0; x < this.node.width; x++) { | |
const d = spect[floor((x / this.node.width) * spect.length)]; | |
const dRate = d / 255; | |
this.graphics.fillColor = this.getHeatColor(dRate); | |
this.graphics.fillRect(x + this.offsetX, y + this.offsetY, 1, 1); | |
} | |
} | |
} | |
} | |
private getByteFrequencyData() { | |
this.audioAnalyser.fftSize = this.fftSize; | |
const data = new Uint8Array(this.audioAnalyser.frequencyBinCount); | |
this.audioAnalyser.getByteFrequencyData(data); | |
return data; | |
} | |
private getByteTimeDomainData() { | |
this.audioAnalyser.fftSize = this.fftSize; | |
const data = new Uint8Array(this.audioAnalyser.fftSize); | |
this.audioAnalyser.getByteTimeDomainData(data); | |
return data; | |
} | |
private getVolume() { | |
if (this.isReady()) { | |
const data = this.getByteTimeDomainData(); | |
let i = 0, | |
total = 0; | |
while (i < this.audioAnalyser.fftSize) { | |
const float = data[i++] / 0x80 - 1; | |
total += float * float; | |
} | |
const rms = Math.sqrt(total / this.audioAnalyser.fftSize); | |
let db = Math.max(-48, Math.min(20 * (Math.log(rms) / Math.log(10)), 0)); | |
return 1 + db * 0.02083; | |
} | |
return 0; | |
} | |
private getAutoColor(n: number) { | |
const { r, g, b } = HSVtoRGB(n, 1, 1); | |
return cc.color(r, g, b); | |
} | |
private getHeatColor(n: number) { | |
const m = floor(n * 4); | |
let colorA: cc.Color, colorB: cc.Color; | |
switch (m) { | |
case 0: { | |
colorA = cc.Color.TRANSPARENT; | |
colorB = cc.Color.BLUE; | |
break; | |
} | |
case 1: { | |
colorA = cc.Color.BLUE; | |
colorB = cc.Color.RED; | |
break; | |
} | |
case 2: { | |
colorA = cc.Color.RED; | |
colorB = cc.Color.YELLOW; | |
break; | |
} | |
case 3: { | |
colorA = cc.Color.YELLOW; | |
colorB = cc.Color.WHITE; | |
break; | |
} | |
default: { | |
return cc.Color.TRANSPARENT; | |
} | |
} | |
return colorA.lerp(colorB, n); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment