moar refactoring, fix some minor bugs with loading, update readme
This commit is contained in:
parent
53efb64c84
commit
154ca4f8f2
|
@ -1,6 +1,6 @@
|
||||||
## ThreeJS Audio Visualizer
|
## ThreeJS Audio Visualizer
|
||||||
|
|
||||||
Currently a work in progress. It will be able to take in an MP3 file, and distort the imported 3D hand model along to the highs and lows.
|
A virtual spoken word exhibit. Objects animate along to the frequencies of the background music while a hand model distorts to the volume of the spoken word.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; background: black">
|
<body style="margin: 0; background: black">
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="loader">Loading model: 0%</div>
|
<div id="loader"></div>
|
||||||
<div id="startButton" style="display: none">BEGIN</div>
|
<div id="startButton" style="display: none">BEGIN</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
import { AudioAnalyser, AudioListener } from "three";
|
import { AudioAnalyser, AudioListener } from "three";
|
||||||
import { LoadAudio } from "../audio";
|
import { LoadAudio } from "../utils/loaders";
|
||||||
|
|
||||||
|
type SRTParser = {
|
||||||
|
(srt: string): SRT[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SRT = {
|
||||||
|
id: number;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClickCallback = () => void;
|
||||||
export class Jukebox {
|
export class Jukebox {
|
||||||
parser: SRTParser;
|
parser: SRTParser;
|
||||||
listener: AudioListener;
|
listener: AudioListener;
|
||||||
|
@ -26,14 +38,14 @@ export class Jukebox {
|
||||||
const music = await LoadAudio(
|
const music = await LoadAudio(
|
||||||
this.listener,
|
this.listener,
|
||||||
"/static/music/space_chillout.mp3",
|
"/static/music/space_chillout.mp3",
|
||||||
1,
|
0.5,
|
||||||
"music",
|
"music",
|
||||||
true
|
false
|
||||||
);
|
);
|
||||||
const poem = await LoadAudio(
|
const poem = await LoadAudio(
|
||||||
this.listener,
|
this.listener,
|
||||||
"/static/music/who_am_i.mp3",
|
"/static/music/who_am_i.mp3",
|
||||||
1,
|
0.7,
|
||||||
"poem",
|
"poem",
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -43,13 +55,11 @@ export class Jukebox {
|
||||||
const poemAnalyser = new AudioAnalyser(poem, 512);
|
const poemAnalyser = new AudioAnalyser(poem, 512);
|
||||||
|
|
||||||
const startButton = document.getElementById("startButton");
|
const startButton = document.getElementById("startButton");
|
||||||
startButton.style.display = "block";
|
|
||||||
startButton.addEventListener("click", () => {
|
startButton.addEventListener("click", () => {
|
||||||
silence.play();
|
silence.play();
|
||||||
|
|
||||||
music.play();
|
music.play();
|
||||||
poem.play();
|
poem.play();
|
||||||
// CORE.playing = true;
|
|
||||||
startButton.remove();
|
startButton.remove();
|
||||||
clickCallback();
|
clickCallback();
|
||||||
});
|
});
|
||||||
|
@ -79,16 +89,3 @@ export class Jukebox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SRTParser = {
|
|
||||||
(srt: string): SRT[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type SRT = {
|
|
||||||
id: number;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClickCallback = () => void;
|
|
||||||
|
|
|
@ -6,8 +6,16 @@ import {
|
||||||
SphereBufferGeometry,
|
SphereBufferGeometry,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from "three";
|
} from "three";
|
||||||
import { calculateSoundAverages } from "../audio";
|
import { calculateSoundAverages } from "../utils/audioUtils";
|
||||||
|
|
||||||
|
export interface Planet extends Mesh {
|
||||||
|
geometry: SphereBufferGeometry;
|
||||||
|
material: MeshToonMaterial;
|
||||||
|
originalPosition?: Vector3;
|
||||||
|
offsetY?: number;
|
||||||
|
angleSpeed?: number;
|
||||||
|
angle?: number;
|
||||||
|
}
|
||||||
export class Planets {
|
export class Planets {
|
||||||
meshes: Array<Planet>;
|
meshes: Array<Planet>;
|
||||||
analyser: AudioAnalyser;
|
analyser: AudioAnalyser;
|
||||||
|
@ -56,12 +64,3 @@ export class Planets {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Planet extends Mesh {
|
|
||||||
geometry: SphereBufferGeometry;
|
|
||||||
material: MeshToonMaterial;
|
|
||||||
originalPosition?: Vector3;
|
|
||||||
offsetY?: number;
|
|
||||||
angleSpeed?: number;
|
|
||||||
angle?: number;
|
|
||||||
}
|
|
||||||
|
|
56
src/audio.ts
56
src/audio.ts
|
@ -1,56 +0,0 @@
|
||||||
import {
|
|
||||||
Audio,
|
|
||||||
AudioAnalyser,
|
|
||||||
AudioListener,
|
|
||||||
AudioLoader,
|
|
||||||
PositionalAudio,
|
|
||||||
} from "three";
|
|
||||||
import { avg, chunk } from "./utils";
|
|
||||||
|
|
||||||
const loader = new AudioLoader();
|
|
||||||
|
|
||||||
export function LoadAudio(
|
|
||||||
listener: AudioListener,
|
|
||||||
url: string,
|
|
||||||
volume: number,
|
|
||||||
type: string,
|
|
||||||
positional?: boolean
|
|
||||||
): Promise<PositionalAudio | Audio> {
|
|
||||||
const loadingDiv = document.getElementById("loader");
|
|
||||||
loadingDiv.innerHTML = `Loading ${type}: 0%`;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
loader.load(
|
|
||||||
url,
|
|
||||||
(audio) => {
|
|
||||||
const sound = positional
|
|
||||||
? new PositionalAudio(listener)
|
|
||||||
: new Audio(listener);
|
|
||||||
sound.setBuffer(audio);
|
|
||||||
sound.setLoop(false);
|
|
||||||
sound.setVolume(volume);
|
|
||||||
loadingDiv.innerHTML = "";
|
|
||||||
return resolve(sound);
|
|
||||||
},
|
|
||||||
(progress) =>
|
|
||||||
(loadingDiv.innerHTML = `Loading audio: ${
|
|
||||||
(progress.loaded / progress.total) * 100
|
|
||||||
}%`),
|
|
||||||
(error: ErrorEvent) => {
|
|
||||||
console.log(error.target);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateSoundAverages(
|
|
||||||
audioAnalyser: AudioAnalyser,
|
|
||||||
chunkAmount: number
|
|
||||||
): number[] {
|
|
||||||
const soundArray = audioAnalyser.getFrequencyData();
|
|
||||||
const chunkedArray = chunk(soundArray, chunkAmount);
|
|
||||||
const averages = chunkedArray.map((arr) => avg(arr) / 255);
|
|
||||||
|
|
||||||
return averages;
|
|
||||||
}
|
|
36
src/index.ts
36
src/index.ts
|
@ -12,7 +12,6 @@ import {
|
||||||
PerspectiveCamera,
|
PerspectiveCamera,
|
||||||
} from "three";
|
} from "three";
|
||||||
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
import { LoadModel } from "./model";
|
|
||||||
import { prng_alea } from "esm-seedrandom";
|
import { prng_alea } from "esm-seedrandom";
|
||||||
import parseSRT from "parse-srt";
|
import parseSRT from "parse-srt";
|
||||||
import { Planets } from "./actors/Planets";
|
import { Planets } from "./actors/Planets";
|
||||||
|
@ -20,6 +19,20 @@ import { OrbitCamera } from "./actors/OrbitCamera";
|
||||||
import { Hand } from "./actors/Hand";
|
import { Hand } from "./actors/Hand";
|
||||||
import { OrbitLight } from "./actors/OrbitLight";
|
import { OrbitLight } from "./actors/OrbitLight";
|
||||||
import { Jukebox } from "./actors/Jukebox";
|
import { Jukebox } from "./actors/Jukebox";
|
||||||
|
import { LoadModel } from "./utils/loaders";
|
||||||
|
|
||||||
|
interface CORE {
|
||||||
|
renderer: WebGLRenderer;
|
||||||
|
scene: Scene;
|
||||||
|
clock: Clock;
|
||||||
|
rng: RandomNumberGenerator;
|
||||||
|
playing: boolean;
|
||||||
|
audioListener: AudioListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RandomNumberGenerator = {
|
||||||
|
(): number;
|
||||||
|
};
|
||||||
|
|
||||||
const CORE: CORE = {
|
const CORE: CORE = {
|
||||||
renderer: null,
|
renderer: null,
|
||||||
|
@ -38,7 +51,9 @@ let HAND: Hand;
|
||||||
let PLANETS: Planets;
|
let PLANETS: Planets;
|
||||||
let JUKEBOX: Jukebox;
|
let JUKEBOX: Jukebox;
|
||||||
|
|
||||||
init().then(() => animate());
|
init()
|
||||||
|
.then(() => animate())
|
||||||
|
.catch((err) => console.warn(err));
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const container = document.getElementById("container");
|
const container = document.getElementById("container");
|
||||||
|
@ -70,13 +85,15 @@ async function init() {
|
||||||
CORE.playing = true;
|
CORE.playing = true;
|
||||||
});
|
});
|
||||||
CORE.scene.add(poem);
|
CORE.scene.add(poem);
|
||||||
ORBIT_LIGHT.light.add(music);
|
CORE.scene.add(music);
|
||||||
const model = await LoadModel();
|
const model = await LoadModel();
|
||||||
initializeModel(model, poemAnalyser);
|
initializeModel(model, poemAnalyser);
|
||||||
initializeStars();
|
initializeStars();
|
||||||
ORBIT_CAMERA.update(0, HAND.mesh.position);
|
ORBIT_CAMERA.update(0, HAND.mesh.position);
|
||||||
PLANETS = new Planets(musicAnalyser);
|
PLANETS = new Planets(musicAnalyser);
|
||||||
PLANETS.meshes.forEach((planet) => CORE.scene.add(planet));
|
PLANETS.meshes.forEach((planet) => CORE.scene.add(planet));
|
||||||
|
document.getElementById("loader").innerHTML = "";
|
||||||
|
document.getElementById("startButton").style.display = "block";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
|
@ -133,16 +150,3 @@ function onWindowResize() {
|
||||||
|
|
||||||
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
CORE.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CORE {
|
|
||||||
renderer: WebGLRenderer;
|
|
||||||
scene: Scene;
|
|
||||||
clock: Clock;
|
|
||||||
rng: RandomNumberGenerator;
|
|
||||||
playing: boolean;
|
|
||||||
audioListener: AudioListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RandomNumberGenerator = {
|
|
||||||
(): number;
|
|
||||||
};
|
|
||||||
|
|
44
src/model.ts
44
src/model.ts
|
@ -1,44 +0,0 @@
|
||||||
import { Texture, TextureLoader } from "three";
|
|
||||||
import { GLTFLoader, GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
||||||
|
|
||||||
const gltfLoader = new GLTFLoader();
|
|
||||||
const textureLoader = new TextureLoader();
|
|
||||||
|
|
||||||
export function LoadModel(): Promise<GLTF> {
|
|
||||||
const loadingDiv = document.getElementById("loader");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
gltfLoader.load(
|
|
||||||
"/static/models/scene.gltf",
|
|
||||||
(gltf: GLTF) => {
|
|
||||||
loadingDiv.innerHTML = "";
|
|
||||||
resolve(gltf);
|
|
||||||
},
|
|
||||||
(progress) =>
|
|
||||||
(loadingDiv.innerHTML = `Loading model: ${
|
|
||||||
(progress.loaded / progress.total) * 100
|
|
||||||
}%`),
|
|
||||||
(error: ErrorEvent) => {
|
|
||||||
console.log(error.target);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoadTexture(url): Promise<Texture> {
|
|
||||||
const loadingDiv = document.getElementById("loader");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
textureLoader.load(
|
|
||||||
url,
|
|
||||||
(texture: Texture) => resolve(texture),
|
|
||||||
(progress) =>
|
|
||||||
(loadingDiv.innerHTML = `Loading texture: ${
|
|
||||||
(progress.loaded / progress.total) * 100
|
|
||||||
}%`),
|
|
||||||
(error: ErrorEvent) => {
|
|
||||||
console.log(error.target);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { AudioAnalyser } from "three";
|
||||||
|
import { avg, chunk } from "./arrayUtils";
|
||||||
|
|
||||||
|
export function calculateSoundAverages(
|
||||||
|
audioAnalyser: AudioAnalyser,
|
||||||
|
chunkAmount: number
|
||||||
|
): number[] {
|
||||||
|
const soundArray = audioAnalyser.getFrequencyData();
|
||||||
|
const chunkedArray = chunk(soundArray, chunkAmount);
|
||||||
|
const averages = chunkedArray.map((arr) => avg(arr) / 255);
|
||||||
|
|
||||||
|
return averages;
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Audio, AudioListener, AudioLoader, PositionalAudio } from "three";
|
||||||
|
import { GLTFLoader, GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
|
||||||
|
const loader = new AudioLoader();
|
||||||
|
const gltfLoader = new GLTFLoader();
|
||||||
|
|
||||||
|
export function LoadAudio(
|
||||||
|
listener: AudioListener,
|
||||||
|
url: string,
|
||||||
|
volume: number,
|
||||||
|
type: string,
|
||||||
|
positional?: boolean
|
||||||
|
): Promise<PositionalAudio | Audio> {
|
||||||
|
const loadingDiv = document.getElementById("loader");
|
||||||
|
loadingDiv.innerHTML = `Loading ${type}: 0.00%`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
loader.load(
|
||||||
|
url,
|
||||||
|
(audio) => {
|
||||||
|
const sound = positional
|
||||||
|
? new PositionalAudio(listener)
|
||||||
|
: new Audio(listener);
|
||||||
|
sound.setBuffer(audio);
|
||||||
|
sound.setLoop(false);
|
||||||
|
sound.setVolume(volume);
|
||||||
|
return resolve(sound);
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
const percent = (progress.loaded / progress.total) * 100;
|
||||||
|
loadingDiv.innerHTML = `Loading ${type}: ${percent.toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
(error: ErrorEvent) => {
|
||||||
|
console.log(error.target);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadModel(): Promise<GLTF> {
|
||||||
|
const loadingDiv = document.getElementById("loader");
|
||||||
|
loadingDiv.innerHTML = `Loading model: 0.00%`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
gltfLoader.load(
|
||||||
|
"/static/models/scene.gltf",
|
||||||
|
(gltf: GLTF) => {
|
||||||
|
resolve(gltf);
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
const percent = (progress.loaded / progress.total) * 100;
|
||||||
|
loadingDiv.innerHTML = `Loading model: ${percent.toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
(error: ErrorEvent) => {
|
||||||
|
console.log(error.target);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
BIN
static/og.png
BIN
static/og.png
Binary file not shown.
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 226 KiB |
Loading…
Reference in New Issue