mirror of
https://github.com/FMS-Cat/condition.git
synced 2025-08-07 06:26:40 +02:00
help
This commit is contained in:
305
src/Music.ts
305
src/Music.ts
@@ -1,305 +0,0 @@
|
||||
import { Channel } from '@fms-cat/automaton';
|
||||
import { GLCat, GLCatBuffer, GLCatProgram, GLCatTexture, GLCatTransformFeedback } from '@fms-cat/glcat-ts';
|
||||
import { MUSIC_AUTOMATON_TEXTURE_HEIGHT, MUSIC_BPM, MUSIC_BUFFER_LENGTH } from './config';
|
||||
import { Pool } from './utils/Pool';
|
||||
import { automaton } from './globals/automaton';
|
||||
import { gl, glCat } from './globals/canvas';
|
||||
import { injectCodeToShader } from './utils/injectCodeToShader';
|
||||
import { randomTextureStatic } from './globals/randomTexture';
|
||||
import musicVert from './shaders/music.vert';
|
||||
import samplesOpus from './samples.opus';
|
||||
import type { AutomatonWithGUI } from '@fms-cat/automaton-with-gui';
|
||||
|
||||
const discardFrag = '#version 300 es\nvoid main(){discard;}';
|
||||
|
||||
export class Music {
|
||||
public isPlaying: boolean;
|
||||
public time: number;
|
||||
public deltaTime: number;
|
||||
public audio: AudioContext;
|
||||
|
||||
private __program: GLCatProgram;
|
||||
private __bufferOff: GLCatBuffer;
|
||||
private __bufferTransformFeedbacks: [
|
||||
GLCatBuffer,
|
||||
GLCatBuffer
|
||||
];
|
||||
private __transformFeedback: GLCatTransformFeedback;
|
||||
private __prevAudioTime: number;
|
||||
private __bufferPool: Pool<AudioBuffer>;
|
||||
private __prevBufferSource: AudioBufferSourceNode | null = null;
|
||||
private __samples?: GLCatTexture;
|
||||
private __automatonChannelList: Channel[];
|
||||
private __automatonDefineString: string;
|
||||
private __arrayAutomaton: Float32Array;
|
||||
private __textureAutomaton: GLCatTexture;
|
||||
|
||||
public constructor( glCat: GLCat, audio: AudioContext ) {
|
||||
this.audio = audio;
|
||||
|
||||
// == yoinked from wavenerd-deck ===============================================================
|
||||
const bufferOffArray = new Array( MUSIC_BUFFER_LENGTH )
|
||||
.fill( 0 )
|
||||
.map( ( _, i ) => i );
|
||||
this.__bufferOff = glCat.createBuffer();
|
||||
this.__bufferOff.setVertexbuffer( new Float32Array( bufferOffArray ) );
|
||||
|
||||
this.__bufferTransformFeedbacks = [
|
||||
glCat.createBuffer(),
|
||||
glCat.createBuffer(),
|
||||
];
|
||||
this.__transformFeedback = glCat.createTransformFeedback();
|
||||
|
||||
this.__bufferTransformFeedbacks[ 0 ].setVertexbuffer(
|
||||
MUSIC_BUFFER_LENGTH * 4,
|
||||
gl.STREAM_READ
|
||||
);
|
||||
|
||||
this.__bufferTransformFeedbacks[ 1 ].setVertexbuffer(
|
||||
MUSIC_BUFFER_LENGTH * 4,
|
||||
gl.STREAM_READ
|
||||
);
|
||||
|
||||
this.__transformFeedback.bindBuffer( 0, this.__bufferTransformFeedbacks[ 0 ] );
|
||||
this.__transformFeedback.bindBuffer( 1, this.__bufferTransformFeedbacks[ 1 ] );
|
||||
|
||||
// == automaton? ===============================================================================
|
||||
this.__automatonChannelList = [];
|
||||
this.__automatonDefineString = '';
|
||||
|
||||
this.__updateAutomatonChannelList();
|
||||
|
||||
this.__arrayAutomaton = new Float32Array(
|
||||
MUSIC_BUFFER_LENGTH * MUSIC_AUTOMATON_TEXTURE_HEIGHT
|
||||
);
|
||||
|
||||
this.__textureAutomaton = glCat.createTexture();
|
||||
this.__textureAutomaton.textureFilter( gl.NEAREST );
|
||||
|
||||
// == program ==================================================================================
|
||||
this.__program = glCat.lazyProgram(
|
||||
injectCodeToShader( musicVert, this.__automatonDefineString ),
|
||||
discardFrag,
|
||||
{ transformFeedbackVaryings: [ 'outL', 'outR' ] },
|
||||
);
|
||||
|
||||
// == audio ====================================================================================
|
||||
this.__prevAudioTime = 0;
|
||||
|
||||
this.__bufferPool = new Pool( [
|
||||
audio.createBuffer( 2, MUSIC_BUFFER_LENGTH, audio.sampleRate ),
|
||||
audio.createBuffer( 2, MUSIC_BUFFER_LENGTH, audio.sampleRate ),
|
||||
] );
|
||||
|
||||
this.__loadSamples();
|
||||
|
||||
// == I think these are gonna be required ======================================================
|
||||
this.isPlaying = false;
|
||||
this.time = 0.0;
|
||||
this.deltaTime = 0.0;
|
||||
|
||||
// == hot hot hot hot hot ======================================================================
|
||||
if ( process.env.DEV && module.hot ) {
|
||||
const recompileShader = async (): Promise<void> => {
|
||||
const program = await glCat.lazyProgramAsync(
|
||||
injectCodeToShader( musicVert, this.__automatonDefineString ),
|
||||
discardFrag,
|
||||
{ transformFeedbackVaryings: [ 'outL', 'outR' ] },
|
||||
).catch( ( error: any ) => {
|
||||
console.error( error );
|
||||
return null;
|
||||
} );
|
||||
|
||||
if ( program ) {
|
||||
this.__program.dispose( true );
|
||||
this.__program = program;
|
||||
}
|
||||
};
|
||||
|
||||
module.hot.accept(
|
||||
[ './shaders/music.vert' ],
|
||||
() => {
|
||||
recompileShader();
|
||||
}
|
||||
);
|
||||
|
||||
module.hot.accept(
|
||||
[ './automaton.json' ],
|
||||
() => {
|
||||
this.__updateAutomatonChannelList();
|
||||
recompileShader();
|
||||
}
|
||||
);
|
||||
|
||||
( automaton as AutomatonWithGUI ).on( 'createChannel', ( { name } ) => {
|
||||
if ( name.startsWith( 'Music/' ) ) {
|
||||
this.__updateAutomatonChannelList();
|
||||
recompileShader();
|
||||
}
|
||||
} );
|
||||
|
||||
( automaton as AutomatonWithGUI ).on( 'removeChannel', ( { name } ) => {
|
||||
if ( name.startsWith( 'Music/' ) ) {
|
||||
this.__updateAutomatonChannelList();
|
||||
recompileShader();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
const { audio, isPlaying } = this;
|
||||
|
||||
const genTime = audio.currentTime;
|
||||
|
||||
if ( isPlaying ) {
|
||||
this.deltaTime = genTime - this.__prevAudioTime;
|
||||
this.time += this.deltaTime;
|
||||
|
||||
const buffer = this.__bufferPool.next();
|
||||
|
||||
if ( this.__program ) {
|
||||
this.__updateAutomatonTexture();
|
||||
this.__prepareBuffer( buffer );
|
||||
}
|
||||
|
||||
const bufferSource = audio.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
bufferSource.connect( audio.destination );
|
||||
|
||||
this.__prevBufferSource?.stop( audio.currentTime );
|
||||
bufferSource.start( audio.currentTime, audio.currentTime - genTime );
|
||||
this.__prevBufferSource = bufferSource;
|
||||
} else {
|
||||
this.deltaTime = 0.0;
|
||||
|
||||
this.__prevBufferSource?.stop( audio.currentTime );
|
||||
this.__prevBufferSource = null;
|
||||
}
|
||||
|
||||
this.__prevAudioTime = genTime;
|
||||
}
|
||||
|
||||
private async __loadSamples(): Promise<void> {
|
||||
const inputBuffer = await fetch( samplesOpus ).then( ( res ) => res.arrayBuffer() );
|
||||
const audioBuffer = await this.audio.decodeAudioData( inputBuffer );
|
||||
|
||||
const buffer = new Float32Array( 96000 );
|
||||
|
||||
const data = audioBuffer.getChannelData( 0 );
|
||||
for ( let i = 0; i < audioBuffer.length; i ++ ) {
|
||||
buffer[ i ] = data[ i ];
|
||||
}
|
||||
|
||||
const texture = glCat.createTexture()!;
|
||||
texture.setTextureFromArray(
|
||||
6000,
|
||||
16,
|
||||
buffer,
|
||||
{
|
||||
internalformat: gl.R32F,
|
||||
format: gl.RED,
|
||||
type: gl.FLOAT,
|
||||
}
|
||||
);
|
||||
texture.textureFilter( gl.LINEAR );
|
||||
|
||||
this.__samples = texture;
|
||||
}
|
||||
|
||||
private __updateAutomatonChannelList(): void {
|
||||
this.__automatonChannelList = [];
|
||||
this.__automatonDefineString = '';
|
||||
|
||||
for ( const [ channelName, channel ] of automaton.mapNameToChannel.entries() ) {
|
||||
if ( channelName.startsWith( 'Music/' ) ) {
|
||||
const key = channelName.substring( 6 );
|
||||
const index = this.__automatonChannelList.length;
|
||||
const y = ( index + 0.5 ) / MUSIC_AUTOMATON_TEXTURE_HEIGHT;
|
||||
this.__automatonDefineString += `const float AUTO_${key}=${y};`;
|
||||
this.__automatonChannelList.push( channel );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private __updateAutomatonTexture(): void {
|
||||
for ( const [ iChannel, channel ] of this.__automatonChannelList.entries() ) {
|
||||
for ( let iSample = 0; iSample < MUSIC_BUFFER_LENGTH; iSample ++ ) {
|
||||
const t = this.time + iSample / this.audio.sampleRate;
|
||||
this.__arrayAutomaton[ MUSIC_BUFFER_LENGTH * iChannel + iSample ] = channel.getValue( t );
|
||||
}
|
||||
}
|
||||
|
||||
this.__textureAutomaton.setTextureFromArray(
|
||||
MUSIC_BUFFER_LENGTH,
|
||||
MUSIC_AUTOMATON_TEXTURE_HEIGHT,
|
||||
this.__arrayAutomaton,
|
||||
{
|
||||
internalformat: gl.R32F,
|
||||
format: gl.RED,
|
||||
type: gl.FLOAT,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private __prepareBuffer( buffer: AudioBuffer ): void {
|
||||
const { time } = this;
|
||||
const program = this.__program;
|
||||
|
||||
const beatLength = 60.0 / MUSIC_BPM;
|
||||
const barLength = 240.0 / MUSIC_BPM;
|
||||
const sixteenBarLength = 3840.0 / MUSIC_BPM;
|
||||
|
||||
program.attribute( 'off', this.__bufferOff, 1 );
|
||||
program.uniform( 'bpm', '1f', MUSIC_BPM );
|
||||
program.uniform( 'bufferLength', '1f', MUSIC_BUFFER_LENGTH );
|
||||
program.uniform( '_deltaSample', '1f', 1.0 / this.audio.sampleRate );
|
||||
program.uniform(
|
||||
'timeLength',
|
||||
'4f',
|
||||
beatLength,
|
||||
barLength,
|
||||
sixteenBarLength,
|
||||
1E16
|
||||
);
|
||||
program.uniform(
|
||||
'_timeHead',
|
||||
'4f',
|
||||
time % beatLength,
|
||||
time % barLength,
|
||||
time % sixteenBarLength,
|
||||
time,
|
||||
);
|
||||
|
||||
program.uniformTexture( 'samplerRandom', randomTextureStatic.texture );
|
||||
program.uniformTexture( 'samplerAutomaton', this.__textureAutomaton );
|
||||
|
||||
if ( this.__samples ) {
|
||||
program.uniformTexture( 'samplerSamples', this.__samples );
|
||||
}
|
||||
|
||||
glCat.useProgram( program, () => {
|
||||
glCat.bindTransformFeedback( this.__transformFeedback, () => {
|
||||
gl.enable( gl.RASTERIZER_DISCARD );
|
||||
gl.beginTransformFeedback( gl.POINTS );
|
||||
gl.drawArrays( gl.POINTS, 0, MUSIC_BUFFER_LENGTH );
|
||||
gl.endTransformFeedback();
|
||||
gl.disable( gl.RASTERIZER_DISCARD );
|
||||
} );
|
||||
} );
|
||||
|
||||
gl.flush();
|
||||
|
||||
[ 0, 1 ].map( ( i ) => {
|
||||
glCat.bindVertexBuffer( this.__bufferTransformFeedbacks[ i ], () => {
|
||||
gl.getBufferSubData(
|
||||
gl.ARRAY_BUFFER,
|
||||
0,
|
||||
buffer.getChannelData( i ),
|
||||
0,
|
||||
MUSIC_BUFFER_LENGTH
|
||||
);
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@@ -6,5 +6,7 @@ export const
|
||||
RESOLUTION = [ 1280, 720 ],
|
||||
MUSIC_BPM = 180,
|
||||
START_POSITION = 36,
|
||||
MUSIC_BUFFER_LENGTH = 4096,
|
||||
MUSIC_AUTOMATON_TEXTURE_HEIGHT = 16;
|
||||
MUSIC_LENGTH = 213,
|
||||
MUSIC_AUTOMATON_TEXTURE_HEIGHT = 16,
|
||||
IBLLUT_ITER = 400,
|
||||
IBLLUT_SIZE = 256;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { BufferRenderTarget } from '../heck/BufferRenderTarget';
|
||||
import { Entity } from '../heck/Entity';
|
||||
import { GLCatTexture } from '@fms-cat/glcat-ts';
|
||||
import { IBLLUT_ITER, IBLLUT_SIZE } from '../config';
|
||||
import { Lambda } from '../heck/components/Lambda';
|
||||
import { Material } from '../heck/Material';
|
||||
import { Quad } from '../heck/components/Quad';
|
||||
@@ -11,8 +12,6 @@ import { vdc } from '../utils/vdc';
|
||||
import iblLutFrag from '../shaders/ibl-lut.frag';
|
||||
import quadVert from '../shaders/quad.vert';
|
||||
|
||||
const IBL_SIZE = 256;
|
||||
|
||||
export class IBLLUT {
|
||||
public entity: Entity;
|
||||
|
||||
@@ -29,13 +28,13 @@ export class IBLLUT {
|
||||
// -- swap -------------------------------------------------------------------------------------
|
||||
this.swap = new Swap(
|
||||
new BufferRenderTarget( {
|
||||
width: IBL_SIZE,
|
||||
height: IBL_SIZE,
|
||||
width: IBLLUT_SIZE,
|
||||
height: IBLLUT_SIZE,
|
||||
name: process.env.DEV && 'IBLLUT/swap0',
|
||||
} ),
|
||||
new BufferRenderTarget( {
|
||||
width: IBL_SIZE,
|
||||
height: IBL_SIZE,
|
||||
width: IBLLUT_SIZE,
|
||||
height: IBLLUT_SIZE,
|
||||
name: process.env.DEV && 'IBLLUT/swap1',
|
||||
} ),
|
||||
);
|
||||
@@ -64,7 +63,7 @@ export class IBLLUT {
|
||||
samples ++;
|
||||
this.swap.swap();
|
||||
|
||||
if ( samples > 1024 ) {
|
||||
if ( samples > IBLLUT_ITER ) {
|
||||
this.entity.active = false;
|
||||
} else {
|
||||
material.addUniform( 'samples', '1f', samples );
|
||||
|
@@ -63,12 +63,6 @@ export class LightEntity extends Entity {
|
||||
this.camera.clear = [ 1.0, 1.0, 1.0, 1.0 ];
|
||||
this.components.push( this.camera );
|
||||
|
||||
this.shadowMap = new BufferRenderTarget( {
|
||||
width: options.shadowMapWidth ?? 1024,
|
||||
height: options.shadowMapHeight ?? 1024,
|
||||
name: process.env.DEV && `${ options.namePrefix }/shadowMap`,
|
||||
} );
|
||||
|
||||
swap.swap();
|
||||
|
||||
// -- blur -------------------------------------------------------------------------------------
|
||||
@@ -82,12 +76,15 @@ export class LightEntity extends Entity {
|
||||
material.addUniformTexture( 'sampler0', swap.i.texture );
|
||||
|
||||
this.components.push( new Quad( {
|
||||
target: i === 0 ? swap.o : this.shadowMap,
|
||||
target: swap.o,
|
||||
material,
|
||||
name: process.env.DEV && `${ options.namePrefix }/quadShadowBlur${ i }`,
|
||||
} ) );
|
||||
|
||||
swap.swap();
|
||||
}
|
||||
|
||||
// -- this is the shadow map -------------------------------------------------------------------
|
||||
this.shadowMap = swap.i;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Automaton } from '@fms-cat/automaton';
|
||||
import { AutomatonWithGUI } from '@fms-cat/automaton-with-gui';
|
||||
import { Music } from '../music/Music';
|
||||
import { fxDefinitions } from '../automaton-fxs/fxDefinitions';
|
||||
import { getDivAutomaton } from './dom';
|
||||
import automatonData from '../automaton.json';
|
||||
import type { Music } from '../Music';
|
||||
|
||||
export const automaton = ( () => {
|
||||
if ( process.env.DEV ) {
|
||||
@@ -13,7 +13,7 @@ export const automaton = ( () => {
|
||||
automatonData,
|
||||
{
|
||||
gui: getDivAutomaton(),
|
||||
isPlaying: true,
|
||||
isPlaying: false,
|
||||
fxDefinitions,
|
||||
},
|
||||
);
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Music } from '../Music';
|
||||
import { Music } from '../music/Music';
|
||||
import { MusicOffline } from '../music/MusicOffline';
|
||||
import { MusicRealtime } from '../music/MusicRealtime';
|
||||
import { automatonSetupMusic } from './automaton';
|
||||
import { glCat } from './canvas';
|
||||
|
||||
export const audio = new AudioContext();
|
||||
export const music = new Music( glCat, audio );
|
||||
// export const music: Music = new MusicRealtime();
|
||||
export const music: Music = new MusicOffline();
|
||||
automatonSetupMusic( music );
|
||||
|
35
src/main.ts
35
src/main.ts
@@ -23,26 +23,12 @@ if ( process.env.DEV ) {
|
||||
canvas.style.margin = 'auto';
|
||||
canvas.style.maxWidth = '100%';
|
||||
canvas.style.maxHeight = '100%';
|
||||
|
||||
music.isPlaying = true;
|
||||
music.time = START_POSITION;
|
||||
} else {
|
||||
canvas.style.position = 'fixed';
|
||||
canvas.style.left = '0';
|
||||
canvas.style.top = '0';
|
||||
document.body.style.width = canvas.style.width = '100%';
|
||||
document.body.style.height = canvas.style.height = '100%';
|
||||
|
||||
const button = document.createElement( 'a' );
|
||||
document.body.appendChild( button );
|
||||
button.innerHTML = 'click me!';
|
||||
|
||||
button.onclick = () => {
|
||||
document.body.appendChild( canvas );
|
||||
music.isPlaying = true;
|
||||
music.time = START_POSITION;
|
||||
document.body.requestFullscreen();
|
||||
};
|
||||
}
|
||||
|
||||
// -- keyboards ------------------------------------------------------------------------------------
|
||||
@@ -85,3 +71,24 @@ if ( process.env.DEV ) {
|
||||
console.info( Component.nameMap );
|
||||
console.info( BufferRenderTarget.nameMap );
|
||||
}
|
||||
|
||||
// -- let's gooooo ---------------------------------------------------------------------------------
|
||||
async function load(): Promise<void> {
|
||||
await music.prepare();
|
||||
|
||||
if ( process.env.DEV ) {
|
||||
music.time = START_POSITION;
|
||||
( automaton as AutomatonWithGUI ).play();
|
||||
} else {
|
||||
const button = document.createElement( 'a' );
|
||||
document.body.appendChild( button );
|
||||
button.innerHTML = 'click me!';
|
||||
|
||||
button.onclick = () => {
|
||||
document.body.appendChild( canvas );
|
||||
music.isPlaying = true;
|
||||
document.body.requestFullscreen();
|
||||
};
|
||||
}
|
||||
}
|
||||
load();
|
||||
|
99
src/music/AutomatonManager.ts
Normal file
99
src/music/AutomatonManager.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Channel } from '@fms-cat/automaton';
|
||||
import { GLCatTexture } from '@fms-cat/glcat-ts';
|
||||
import { MUSIC_AUTOMATON_TEXTURE_HEIGHT } from '../config';
|
||||
import { Music } from './Music';
|
||||
import { audio } from '../globals/music';
|
||||
import { automaton } from '../globals/automaton';
|
||||
import { gl, glCat } from '../globals/canvas';
|
||||
import type { AutomatonWithGUI } from '@fms-cat/automaton-with-gui';
|
||||
|
||||
/**
|
||||
* Inject automaton params into music...
|
||||
*/
|
||||
export class AutomatonManager {
|
||||
public texture: GLCatTexture;
|
||||
public defineString: string;
|
||||
private __music: Music;
|
||||
private __ChannelList: Channel[];
|
||||
private __array: Float32Array;
|
||||
|
||||
public constructor( music: Music ) {
|
||||
this.__music = music;
|
||||
|
||||
this.defineString = '';
|
||||
this.__ChannelList = [];
|
||||
|
||||
this.__updateAutomatonChannelList();
|
||||
|
||||
this.__array = new Float32Array(
|
||||
music.bufferLength * MUSIC_AUTOMATON_TEXTURE_HEIGHT
|
||||
);
|
||||
|
||||
this.texture = glCat.createTexture();
|
||||
this.texture.textureFilter( gl.NEAREST );
|
||||
|
||||
// == hot hot hot hot hot ======================================================================
|
||||
if ( process.env.DEV ) {
|
||||
if ( module.hot ) {
|
||||
module.hot.accept(
|
||||
[ '../automaton.json' ],
|
||||
() => {
|
||||
this.__updateAutomatonChannelList();
|
||||
music.recompile?.();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
( automaton as AutomatonWithGUI ).on( 'createChannel', ( { name } ) => {
|
||||
if ( name.startsWith( 'Music/' ) ) {
|
||||
this.__updateAutomatonChannelList();
|
||||
music.recompile?.();
|
||||
}
|
||||
} );
|
||||
|
||||
( automaton as AutomatonWithGUI ).on( 'removeChannel', ( { name } ) => {
|
||||
if ( name.startsWith( 'Music/' ) ) {
|
||||
this.__updateAutomatonChannelList();
|
||||
music.recompile?.();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
public update( time: number ): void {
|
||||
const bufferLength = this.__music.bufferLength;
|
||||
|
||||
for ( const [ iChannel, channel ] of this.__ChannelList.entries() ) {
|
||||
for ( let iSample = 0; iSample < bufferLength; iSample ++ ) {
|
||||
const t = time + iSample / audio.sampleRate;
|
||||
this.__array[ bufferLength * iChannel + iSample ] = channel.getValue( t );
|
||||
}
|
||||
}
|
||||
|
||||
this.texture.setTextureFromArray(
|
||||
bufferLength,
|
||||
MUSIC_AUTOMATON_TEXTURE_HEIGHT,
|
||||
this.__array,
|
||||
{
|
||||
internalformat: gl.R32F,
|
||||
format: gl.RED,
|
||||
type: gl.FLOAT,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private __updateAutomatonChannelList(): void {
|
||||
this.__ChannelList = [];
|
||||
this.defineString = '';
|
||||
|
||||
for ( const [ channelName, channel ] of automaton.mapNameToChannel.entries() ) {
|
||||
if ( channelName.startsWith( 'Music/' ) ) {
|
||||
const key = channelName.substring( 6 );
|
||||
const index = this.__ChannelList.length;
|
||||
const y = ( index + 0.5 ) / MUSIC_AUTOMATON_TEXTURE_HEIGHT;
|
||||
this.defineString += `const float AUTO_${key}=${y};`;
|
||||
this.__ChannelList.push( channel );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
178
src/music/Music.ts
Normal file
178
src/music/Music.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { AutomatonManager } from './AutomatonManager';
|
||||
import { GLCatBuffer, GLCatProgram, GLCatTransformFeedback } from '@fms-cat/glcat-ts';
|
||||
import { MUSIC_BPM } from '../config';
|
||||
import { SamplesManager } from './SamplesManager';
|
||||
import { audio } from '../globals/music';
|
||||
import { gl, glCat } from '../globals/canvas';
|
||||
import { injectCodeToShader } from '../utils/injectCodeToShader';
|
||||
import { randomTextureStatic } from '../globals/randomTexture';
|
||||
import musicVert from './music.vert';
|
||||
|
||||
const discardFrag = '#version 300 es\nvoid main(){discard;}';
|
||||
|
||||
export abstract class Music {
|
||||
public isPlaying: boolean;
|
||||
public time: number;
|
||||
public deltaTime: number;
|
||||
public readonly bufferLength: number;
|
||||
|
||||
protected __program: GLCatProgram;
|
||||
protected __samplesManager: SamplesManager;
|
||||
protected __automatonManager: AutomatonManager;
|
||||
|
||||
private __bufferOff: GLCatBuffer;
|
||||
private __bufferTransformFeedbacks: [
|
||||
GLCatBuffer,
|
||||
GLCatBuffer,
|
||||
];
|
||||
private __transformFeedback: GLCatTransformFeedback;
|
||||
private __prevAudioTime: number;
|
||||
|
||||
public constructor( bufferLength: number ) {
|
||||
this.isPlaying = false;
|
||||
this.time = 0.0;
|
||||
this.deltaTime = 0.0;
|
||||
this.__prevAudioTime = 0.0;
|
||||
this.bufferLength = bufferLength;
|
||||
|
||||
// == spicy boys ===============================================================================
|
||||
this.__samplesManager = new SamplesManager( this );
|
||||
this.__automatonManager = new AutomatonManager( this );
|
||||
|
||||
// == gl =======================================================================================
|
||||
const bufferOffArray = new Array( bufferLength )
|
||||
.fill( 0 )
|
||||
.map( ( _, i ) => i );
|
||||
this.__bufferOff = glCat.createBuffer();
|
||||
this.__bufferOff.setVertexbuffer( new Float32Array( bufferOffArray ) );
|
||||
|
||||
this.__bufferTransformFeedbacks = [
|
||||
glCat.createBuffer(),
|
||||
glCat.createBuffer(),
|
||||
];
|
||||
this.__transformFeedback = glCat.createTransformFeedback();
|
||||
|
||||
this.__bufferTransformFeedbacks[ 0 ].setVertexbuffer(
|
||||
bufferLength * 4,
|
||||
gl.STREAM_READ
|
||||
);
|
||||
|
||||
this.__bufferTransformFeedbacks[ 1 ].setVertexbuffer(
|
||||
bufferLength * 4,
|
||||
gl.STREAM_READ
|
||||
);
|
||||
|
||||
this.__transformFeedback.bindBuffer( 0, this.__bufferTransformFeedbacks[ 0 ] );
|
||||
this.__transformFeedback.bindBuffer( 1, this.__bufferTransformFeedbacks[ 1 ] );
|
||||
|
||||
this.__program = glCat.lazyProgram(
|
||||
injectCodeToShader( musicVert, this.__automatonManager.defineString ),
|
||||
discardFrag,
|
||||
{ transformFeedbackVaryings: [ 'outL', 'outR' ] },
|
||||
);
|
||||
|
||||
// == hot hot hot hot hot ======================================================================
|
||||
if ( process.env.DEV && module.hot ) {
|
||||
module.hot.accept(
|
||||
[ './music.vert' ],
|
||||
() => {
|
||||
this.recompile();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async prepare(): Promise<void> {
|
||||
await this.__samplesManager.loadSamples();
|
||||
}
|
||||
|
||||
public async recompile(): Promise<void> {
|
||||
if ( process.env.DEV ) {
|
||||
const program = await glCat.lazyProgramAsync(
|
||||
injectCodeToShader( musicVert, this.__automatonManager.defineString ),
|
||||
discardFrag,
|
||||
{ transformFeedbackVaryings: [ 'outL', 'outR' ] },
|
||||
).catch( ( error: any ) => {
|
||||
console.error( error );
|
||||
return null;
|
||||
} );
|
||||
|
||||
if ( program ) {
|
||||
this.__program.dispose( true );
|
||||
this.__program = program;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
const now = audio.currentTime;
|
||||
|
||||
if ( this.isPlaying ) {
|
||||
this.deltaTime = now - this.__prevAudioTime;
|
||||
this.time += this.deltaTime;
|
||||
}
|
||||
|
||||
this.__updateImpl();
|
||||
|
||||
this.__prevAudioTime = now;
|
||||
}
|
||||
|
||||
protected abstract __updateImpl(): void;
|
||||
|
||||
protected __render( time: number, callback: ( channel: number ) => void ): void {
|
||||
this.__automatonManager.update( time );
|
||||
|
||||
const program = this.__program;
|
||||
|
||||
const beatLength = 60.0 / MUSIC_BPM;
|
||||
const barLength = 240.0 / MUSIC_BPM;
|
||||
const sixteenBarLength = 3840.0 / MUSIC_BPM;
|
||||
|
||||
program.attribute( 'off', this.__bufferOff, 1 );
|
||||
program.uniform( 'bpm', '1f', MUSIC_BPM );
|
||||
program.uniform( 'bufferLength', '1f', this.bufferLength );
|
||||
program.uniform( '_deltaSample', '1f', 1.0 / audio.sampleRate );
|
||||
program.uniform(
|
||||
'timeLength',
|
||||
'4f',
|
||||
beatLength,
|
||||
barLength,
|
||||
sixteenBarLength,
|
||||
1E16
|
||||
);
|
||||
program.uniform(
|
||||
'_timeHead',
|
||||
'4f',
|
||||
time % beatLength,
|
||||
time % barLength,
|
||||
time % sixteenBarLength,
|
||||
time,
|
||||
);
|
||||
|
||||
program.uniformTexture( 'samplerRandom', randomTextureStatic.texture );
|
||||
program.uniformTexture( 'samplerAutomaton', this.__automatonManager.texture );
|
||||
|
||||
if ( this.__samplesManager.texture ) {
|
||||
program.uniformTexture( 'samplerSamples', this.__samplesManager.texture );
|
||||
}
|
||||
|
||||
glCat.useProgram( program, () => {
|
||||
glCat.bindTransformFeedback( this.__transformFeedback, () => {
|
||||
gl.enable( gl.RASTERIZER_DISCARD );
|
||||
gl.beginTransformFeedback( gl.POINTS );
|
||||
gl.drawArrays( gl.POINTS, 0, this.bufferLength );
|
||||
gl.endTransformFeedback();
|
||||
gl.disable( gl.RASTERIZER_DISCARD );
|
||||
} );
|
||||
} );
|
||||
|
||||
gl.finish(); // fenceよくわからん
|
||||
|
||||
[ 0, 1 ].map( ( i ) => {
|
||||
glCat.bindVertexBuffer(
|
||||
this.__bufferTransformFeedbacks[ i ],
|
||||
() => callback( i ),
|
||||
);
|
||||
} );
|
||||
}
|
||||
}
|
67
src/music/MusicOffline.ts
Normal file
67
src/music/MusicOffline.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { MUSIC_LENGTH } from '../config';
|
||||
import { Music } from './Music';
|
||||
import { audio } from '../globals/music';
|
||||
import { gl } from '../globals/canvas';
|
||||
|
||||
const BUFFER_LENGTH = 16384;
|
||||
|
||||
export class MusicOffline extends Music {
|
||||
private __buffer: AudioBuffer;
|
||||
private __currentBufferSource?: AudioBufferSourceNode | null;
|
||||
|
||||
public constructor() {
|
||||
super( BUFFER_LENGTH );
|
||||
|
||||
this.__buffer = audio.createBuffer(
|
||||
2,
|
||||
MUSIC_LENGTH * audio.sampleRate,
|
||||
audio.sampleRate
|
||||
);
|
||||
}
|
||||
|
||||
public async prepare(): Promise<void> {
|
||||
await super.prepare();
|
||||
|
||||
let head = 0;
|
||||
|
||||
return new Promise( ( resolve ) => {
|
||||
const render = (): void => {
|
||||
const remain = ( MUSIC_LENGTH * audio.sampleRate ) - head;
|
||||
if ( remain <= 0 ) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.__render( head / audio.sampleRate, ( i ) => {
|
||||
gl.getBufferSubData(
|
||||
gl.ARRAY_BUFFER,
|
||||
0,
|
||||
this.__buffer.getChannelData( i ),
|
||||
head,
|
||||
Math.min( remain, BUFFER_LENGTH )
|
||||
);
|
||||
} );
|
||||
|
||||
head += BUFFER_LENGTH;
|
||||
|
||||
setTimeout( render, 1 );
|
||||
};
|
||||
render();
|
||||
} );
|
||||
}
|
||||
|
||||
public __updateImpl(): void {
|
||||
if ( this.isPlaying && this.__currentBufferSource == null ) {
|
||||
this.__currentBufferSource = audio.createBufferSource();
|
||||
this.__currentBufferSource.buffer = this.__buffer;
|
||||
this.__currentBufferSource.connect( audio.destination );
|
||||
|
||||
this.__currentBufferSource.start( audio.currentTime, this.time );
|
||||
}
|
||||
|
||||
if ( !this.isPlaying && this.__currentBufferSource != null ) {
|
||||
this.__currentBufferSource.stop( audio.currentTime );
|
||||
this.__currentBufferSource = null;
|
||||
}
|
||||
}
|
||||
}
|
55
src/music/MusicRealtime.ts
Normal file
55
src/music/MusicRealtime.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Music } from './Music';
|
||||
import { Pool } from '../utils/Pool';
|
||||
import { audio } from '../globals/music';
|
||||
import { gl } from '../globals/canvas';
|
||||
|
||||
const BUFFER_LENGTH = 4096;
|
||||
|
||||
export class MusicRealtime extends Music {
|
||||
private __bufferPool: Pool<AudioBuffer>;
|
||||
private __prevBufferSource: AudioBufferSourceNode | null = null;
|
||||
|
||||
public constructor() {
|
||||
super( BUFFER_LENGTH );
|
||||
|
||||
this.__bufferPool = new Pool( [
|
||||
audio.createBuffer( 2, BUFFER_LENGTH, audio.sampleRate ),
|
||||
audio.createBuffer( 2, BUFFER_LENGTH, audio.sampleRate ),
|
||||
] );
|
||||
|
||||
// casually calling async function
|
||||
this.__samplesManager.loadSamples();
|
||||
}
|
||||
|
||||
public __updateImpl(): void {
|
||||
if ( this.isPlaying ) {
|
||||
const buffer = this.__bufferPool.next();
|
||||
const genTime = audio.currentTime;
|
||||
|
||||
if ( this.__program ) {
|
||||
this.__render( this.time, ( i ) => {
|
||||
gl.getBufferSubData(
|
||||
gl.ARRAY_BUFFER,
|
||||
0,
|
||||
buffer.getChannelData( i ),
|
||||
0,
|
||||
BUFFER_LENGTH
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
const bufferSource = audio.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
bufferSource.connect( audio.destination );
|
||||
|
||||
this.__prevBufferSource?.stop( audio.currentTime );
|
||||
bufferSource.start( audio.currentTime, audio.currentTime - genTime );
|
||||
this.__prevBufferSource = bufferSource;
|
||||
} else {
|
||||
this.deltaTime = 0.0;
|
||||
|
||||
this.__prevBufferSource?.stop( audio.currentTime );
|
||||
this.__prevBufferSource = null;
|
||||
}
|
||||
}
|
||||
}
|
62
src/music/SamplesManager.ts
Normal file
62
src/music/SamplesManager.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { GLCatTexture } from '@fms-cat/glcat-ts';
|
||||
import { Music } from './Music';
|
||||
import { audio } from '../globals/music';
|
||||
import { gl, glCat } from '../globals/canvas';
|
||||
import samplesOpus from './samples.opus';
|
||||
|
||||
export class SamplesManager {
|
||||
public __music: Music;
|
||||
|
||||
/**
|
||||
* it can be undefined since it's loaded asynchronously
|
||||
*/
|
||||
public texture?: GLCatTexture;
|
||||
|
||||
public constructor( music: Music ) {
|
||||
this.__music = music;
|
||||
|
||||
// == hot hot hot hot hot ======================================================================
|
||||
if ( process.env.DEV ) {
|
||||
if ( module.hot ) {
|
||||
module.hot.accept(
|
||||
[ './samples.opus' ],
|
||||
async () => {
|
||||
await this.loadSamples();
|
||||
music.recompile?.();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async loadSamples(): Promise<void> {
|
||||
const inputBuffer = await fetch( samplesOpus ).then( ( res ) => res.arrayBuffer() );
|
||||
const audioBuffer = await audio.decodeAudioData( inputBuffer );
|
||||
|
||||
const buffer = new Float32Array( 96000 );
|
||||
|
||||
const data = audioBuffer.getChannelData( 0 );
|
||||
for ( let i = 0; i < audioBuffer.length; i ++ ) {
|
||||
buffer[ i ] = data[ i ];
|
||||
}
|
||||
|
||||
const texture = glCat.createTexture()!;
|
||||
texture.setTextureFromArray(
|
||||
6000,
|
||||
16,
|
||||
buffer,
|
||||
{
|
||||
internalformat: gl.R32F,
|
||||
format: gl.RED,
|
||||
type: gl.FLOAT,
|
||||
}
|
||||
);
|
||||
texture.textureFilter( gl.LINEAR );
|
||||
|
||||
if ( process.env.DEV && this.texture != null ) {
|
||||
this.texture.dispose();
|
||||
}
|
||||
|
||||
this.texture = texture;
|
||||
}
|
||||
}
|
12
src/scene.ts
12
src/scene.ts
@@ -14,6 +14,7 @@ import { FlashyTerrain } from './entities/FlashyTerrain';
|
||||
import { FlickyParticles } from './entities/FlickyParticles';
|
||||
import { Glitch } from './entities/Glitch';
|
||||
import { IBLLUT } from './entities/IBLLUT';
|
||||
import { IFSPistons } from './entities/IFSPistons';
|
||||
import { Lambda } from './heck/components/Lambda';
|
||||
import { LightEntity } from './entities/LightEntity';
|
||||
import { PixelSorter } from './entities/PixelSorter';
|
||||
@@ -162,6 +163,13 @@ if ( process.env.DEV && module.hot ) {
|
||||
} );
|
||||
}
|
||||
|
||||
const replacerIFSPistons = new EntityReplacer( () => new IFSPistons(), 'IFSPistons' );
|
||||
if ( process.env.DEV && module.hot ) {
|
||||
module.hot.accept( './entities/IFSPistons', () => {
|
||||
replacerIFSPistons.replace();
|
||||
} );
|
||||
}
|
||||
|
||||
// -- things that is not an "object" ---------------------------------------------------------------
|
||||
const swapOptions = {
|
||||
width: canvasRenderTarget.width,
|
||||
@@ -187,7 +195,7 @@ const replacerLightFirst = new EntityReplacer( () => {
|
||||
shadowMapFar: 20.0,
|
||||
namePrefix: process.env.DEV && 'lightFirst',
|
||||
} );
|
||||
light.color = [ 1.0, 1.0, 1.0 ];
|
||||
light.color = [ 40.0, 40.0, 40.0 ];
|
||||
light.transform.lookAt( new Vector3( [ -1.0, 2.0, 8.0 ] ) );
|
||||
return light;
|
||||
}, 'LightFirst' );
|
||||
@@ -201,7 +209,7 @@ const replacerLightPink = new EntityReplacer( () => {
|
||||
shadowMapFar: 20.0,
|
||||
namePrefix: process.env.DEV && 'lightPink',
|
||||
} );
|
||||
light.color = [ 60.0, 1.0, 5.0 ];
|
||||
light.color = [ 120.0, 2.0, 10.0 ];
|
||||
light.transform.lookAt( new Vector3( [ -1.0, 4.0, 4.0 ] ) );
|
||||
return light;
|
||||
}, 'LightPink' );
|
||||
|
Reference in New Issue
Block a user