Skip to content

Instantly share code, notes, and snippets.

@jamesliu96
Created September 4, 2020 07:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesliu96/2007825feae7c8354a029eaa4097245f to your computer and use it in GitHub Desktop.
Save jamesliu96/2007825feae7c8354a029eaa4097245f to your computer and use it in GitHub Desktop.
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