2023-05-24 13:23:58 +00:00
export enum SmoothingAlgorythm {
Linear ,
BezierPerpendicular ,
CatmullRom ,
BezierWeighted ,
}
export enum ShapeType {
Circle ,
Line ,
/ * T o b e I m p l m e n e t e d
2023-05-24 15:42:05 +00:00
Custom ,
2023-05-24 13:23:58 +00:00
Waveform ,
FullSongWaveForm
* /
}
type Point = {
x : number ,
y : number ,
}
type Shape = {
shape_type : ShapeType ,
//Algo-rythm, because this is about music. Get it? xd
smoothing_algorythm : SmoothingAlgorythm
points : Point [ ]
}
2023-05-24 15:42:05 +00:00
export class AudioVisual {
# analyzer_node
# svg_injecting_element
# shape
# buffer_length
# fft_multiplier
# fft_offset
# from_fft_range
# to_fft_range
# point_count
# canvas_height
# canvas_width
# fft_data
2023-06-12 15:50:33 +00:00
# subscriber_fns = new Array < ( data : Float32Array ) = > void > ( )
2023-05-24 15:42:05 +00:00
constructor (
analyzer_node : AnalyserNode ,
svg_injecting_element : SVGSVGElement ,
shape : Shape ,
buffer_length : number ,
fft_multiplier : number ,
fft_offset : number ,
from_fft_range : number ,
to_fft_range : number ,
point_count : number ,
) {
this . # analyzer_node = analyzer_node
this . # svg_injecting_element = svg_injecting_element
this . # shape = shape
this . # buffer_length = buffer_length
this . # fft_multiplier = fft_multiplier
this . # fft_offset = fft_offset
this . # from_fft_range = from_fft_range
this . # to_fft_range = to_fft_range
this . # point_count = point_count
2023-05-24 13:23:58 +00:00
2023-05-24 15:42:05 +00:00
this . # fft_data = new Float32Array ( )
this . # canvas_width = svg_injecting_element . viewBox . baseVal . width
this . # canvas_height = svg_injecting_element . viewBox . baseVal . height
}
# get_cured_frequency_data() {
this . # fft_data = new Float32Array ( this . # buffer_length )
this . # analyzer_node . getFloatFrequencyData ( this . # fft_data )
const from = Math . round ( ( this . # point_count / 100 ) * this . # from_fft_range )
const to = Math . round ( this . # buffer_length - ( this . # buffer_length / 100 ) * this . # to_fft_range )
const squeeze_factor = Math . round ( ( this . # buffer_length - to ) / this . # point_count )
2023-05-24 13:23:58 +00:00
2023-05-24 15:42:05 +00:00
const return_array = new Array ( this . # point_count )
for ( let i = 0 ; i < this . # point_count + 1 ; i ++ ) {
return_array [ i ] = this . # fft_data [ from + i * squeeze_factor ]
2023-05-24 13:23:58 +00:00
}
return return_array
}
2023-05-24 15:42:05 +00:00
# normalise_perpendicular_anchors ( x : number , y : number ) {
2023-05-24 13:23:58 +00:00
const magnitude = Math . sqrt ( x * x + y * y )
return [ x / magnitude , y / magnitude ]
}
2023-05-24 15:42:05 +00:00
# create_perpendicular_anchors ( arr : { x : number , y : number } [ ] ) {
2023-05-24 13:23:58 +00:00
const anchors = [ ]
2023-05-24 15:42:05 +00:00
switch ( this . # shape . shape_type ) {
2023-05-24 13:23:58 +00:00
case ShapeType . Circle : {
const pointDistance = 7
for ( let curPoint = 0 ; curPoint < arr . length ; curPoint ++ ) {
2023-05-24 15:42:05 +00:00
const [ dx , dy ] = this . # normalise_perpendicular_anchors ( arr [ curPoint ] . x , arr [ curPoint ] . y )
2023-05-24 13:23:58 +00:00
const perpendicular = [ - dy , dx ]
anchors . push ( {
leftAnchor : {
x : arr [ curPoint ] . x + pointDistance * perpendicular [ 0 ] ,
y : arr [ curPoint ] . y + pointDistance * perpendicular [ 1 ] ,
} ,
rightAnchor : {
x : arr [ curPoint ] . x - pointDistance * perpendicular [ 0 ] ,
y : arr [ curPoint ] . y - pointDistance * perpendicular [ 1 ] ,
} ,
} )
}
break
}
case ShapeType . Line : {
2023-05-24 15:42:05 +00:00
const pointDistance = this . # canvas_width / arr . length
2023-05-24 13:23:58 +00:00
for ( let curPoint = 0 ; curPoint < arr . length ; curPoint ++ ) {
anchors . push ( {
leftAnchor : {
x : pointDistance * curPoint - pointDistance / 3 ,
y : arr [ curPoint ] . y ,
} ,
rightAnchor : {
x : pointDistance * curPoint + pointDistance / 3 ,
y : arr [ curPoint ] . y ,
} ,
} )
}
}
}
return anchors
}
2023-05-24 15:42:05 +00:00
# catmull_rom_smooth ( arr : { x : number , y : number } [ ] , k : number ) {
2023-05-24 13:23:58 +00:00
if ( k == null ) k = 1
const last = arr . length - 2
let path = "M" + [ arr [ 0 ] . x , arr [ 0 ] . y ]
for ( let i = 0 ; i < arr . length - 1 ; i ++ ) {
const x0 = i ? arr [ i - 1 ] . x : arr [ 0 ] . x
const y0 = i ? arr [ i - 1 ] . y : arr [ 0 ] . y
const x1 = arr [ i ] . x
const y1 = arr [ i ] . y
const x2 = arr [ i + 1 ] . x
const y2 = arr [ i + 1 ] . y
let subx = y2
let suby = y2
//Makes the last line before Z a bit less jarring
2023-05-24 15:42:05 +00:00
if ( this . # shape . shape_type == ShapeType . Circle ) {
2023-05-24 13:23:58 +00:00
subx = arr [ 0 ] . x
suby = arr [ 0 ] . y
}
const x3 = i !== last ? arr [ i + 2 ] . x : subx
const y3 = i !== last ? arr [ i + 2 ] . y : suby
const cp1x = x1 + ( x2 - x0 ) / 6 * k
const cp1y = y1 + ( y2 - y0 ) / 6 * k
const cp2x = x2 - ( x3 - x1 ) / 6 * k
const cp2y = y2 - ( y3 - y1 ) / 6 * k
path += "C" + [ cp1x , cp1y , cp2x , cp2y , x2 , y2 ]
}
path += " Z"
return path
}
2023-05-24 15:42:05 +00:00
# mutate_points() {
2023-05-24 13:23:58 +00:00
const mutated_points = [ ]
2023-05-24 15:42:05 +00:00
const frequency_data = this . # get_cured_frequency_data ( )
const out_range = [ 0 , this . # canvas_height ]
2023-05-24 13:23:58 +00:00
const in_range = [ - 165 , - 30 ]
2023-05-24 15:42:05 +00:00
switch ( this . # shape . shape_type ) {
2023-05-24 13:23:58 +00:00
case ShapeType . Line : {
for ( let i = 0 ; i < frequency_data . length - 1 ; i ++ ) {
mutated_points . push ( {
2023-05-24 15:42:05 +00:00
x : this. # shape . points [ i ] . x /** ((Math.max(FFTDataArray[i] + 100)) * 4)*/ ,
y : this. # shape . points [ i ] . y - this . # convert_range ( frequency_data [ i ] * this . # fft_multiplier + this . # fft_offset , in_range , out_range ) ,
2023-05-24 13:23:58 +00:00
} )
}
break
}
case ShapeType . Circle : {
for ( let i = 0 ; i < frequency_data . length - 1 ; i ++ ) {
const new_i = i > ( frequency_data . length - 1 ) / 2 ? frequency_data . length - 1 - i : i
mutated_points . push ( {
2023-05-24 15:42:05 +00:00
x : this. # shape . points [ i ] . x * Math . max ( ( frequency_data [ new_i ] * this . # fft_multiplier + this . # fft_offset ) / 50 , 1 ) + this . # canvas_width / 2 ,
y : this. # shape . points [ i ] . y * Math . max ( ( frequency_data [ new_i ] * this . # fft_multiplier + this . # fft_offset ) / 50 , 1 ) + this . # canvas_height / 2 ,
2023-05-24 13:23:58 +00:00
} )
/ * T O D O : I M P L E M E N T S C A L I N G T O B E A T
this . injectingHTMLElement . parentElement . style . transform = ` scale( ${ ( 100 + Math . max ( ( frequencyData [ 2 ] * 2 + 130 ) / 5 , 1 ) ) / 100 } ) `
* /
}
break
}
}
return mutated_points
}
2023-05-24 15:42:05 +00:00
# convert_range ( value : number , r1 : number [ ] , r2 : number [ ] ) {
2023-05-24 13:23:58 +00:00
if ( ! isFinite ( value ) ) return 0
return ( ( value - r1 [ 0 ] ) * ( r2 [ 1 ] - r2 [ 0 ] ) ) / ( r1 [ 1 ] - r1 [ 0 ] ) + r2 [ 0 ]
}
2023-05-24 15:42:05 +00:00
# create_svg_element() {
2023-05-24 13:23:58 +00:00
let path
2023-05-24 15:42:05 +00:00
const arr = this . # mutate_points ( )
switch ( this . # shape . shape_type ) {
2023-05-24 13:23:58 +00:00
case ShapeType . Line : {
2023-05-24 15:42:05 +00:00
path = ` M ${ 0 } ${ this . # canvas_height } `
2023-05-24 13:23:58 +00:00
break
}
case ShapeType . Circle : {
path = ` M ${ arr [ 0 ] . x } ${ arr [ 0 ] . y } `
}
}
2023-05-24 15:42:05 +00:00
switch ( this . # shape . smoothing_algorythm ) {
2023-05-24 13:23:58 +00:00
case SmoothingAlgorythm . Linear : {
for ( let i = 0 ; i < arr . length ; i ++ ) {
path += ` L ${ arr [ i ] . x } , ${ arr [ i ] . y } `
}
2023-05-24 15:42:05 +00:00
if ( this . # shape . shape_type == ShapeType . Line ) {
path += ` L ${ this . # canvas_width } ${ this . # canvas_height / 2 } `
2023-05-24 13:23:58 +00:00
//path += `L ${canvas_width} ${canvas_height} `
}
path += ` Z `
break
}
case SmoothingAlgorythm . BezierPerpendicular : {
2023-05-24 15:42:05 +00:00
const anchors = this . # create_perpendicular_anchors ( arr )
2023-05-24 13:23:58 +00:00
for ( let i = 1 ; i < arr . length ; i ++ ) {
path += ` C ${ anchors [ i - 1 ] . rightAnchor . x } ${ anchors [ i - 1 ] . rightAnchor . y } ${ anchors [ i ] . leftAnchor . x } ${ anchors [ i ] . leftAnchor . y } ${ arr [ i ] . x } ${ arr [ i ] . y } `
}
2023-05-24 15:42:05 +00:00
if ( this . # shape . shape_type == ShapeType . Line ) {
2023-05-24 13:23:58 +00:00
//path += `L ${this.canvasWidth} ${this.canvasHeight / 2} `
2023-05-24 15:42:05 +00:00
path += ` L ${ this . # canvas_width } ${ this . # canvas_height } `
2023-05-24 13:23:58 +00:00
}
path += ` Z `
break
}
case SmoothingAlgorythm . BezierWeighted : {
/ * T H I S I S V E R Y M U C H B R O K E N A T M : (
for ( let i = 2 ; i < arr . length ; i ++ ) {
const end = [ arr . x [ i ] , arr . y [ i ] ] // the current point is the end of this segment of the curve
path += ` C ${ startControl [ 0 ] } ${ startControl [ 1 ] } ${ endControl [ 0 ] } ${ endControl [ 1 ] } ${ end [ 0 ] } ${ end [ 1 ] } `
} * /
console . error ( "BezierWeighted not implemented yet..." )
break
}
case SmoothingAlgorythm . CatmullRom : {
2023-05-24 15:42:05 +00:00
path = this . # catmull_rom_smooth ( arr , 1 )
2023-05-24 13:23:58 +00:00
break
}
}
return ` <path width="100%" height="100%" d=" ${ path } "/> `
}
2023-06-12 15:50:33 +00:00
on_data ( fn : ( ( data : Float32Array ) = > void ) ) {
this . # subscriber_fns . push ( fn )
}
2023-05-24 15:42:05 +00:00
draw() {
this . # analyzer_node . getFloatFrequencyData ( this . # fft_data )
this . # svg_injecting_element . innerHTML = this . # create_svg_element ( )
2023-06-12 15:50:33 +00:00
this . # subscriber_fns . forEach ( ( fn ) = > fn ( this . # fft_data ) )
2023-05-24 15:42:05 +00:00
requestAnimationFrame ( this . draw . bind ( this ) )
2023-05-24 13:23:58 +00:00
}
}
2023-05-24 15:42:05 +00:00
export class AudioVisualBuilder {
# analyzer_node
# svg_injecting_element
# canvas_height
# canvas_width
# buffer_length
# smoothing_algorythm
# fft_size
# fft_multipier
# fft_offset
# from_fft_range
# to_fft_range
# point_count : number
constructor ( analyzer_node : AnalyserNode , svg_injecting_element : SVGSVGElement ) {
this . # analyzer_node = analyzer_node
this . # svg_injecting_element = svg_injecting_element
this . # canvas_width = svg_injecting_element . viewBox . baseVal . width
this . # canvas_height = svg_injecting_element . viewBox . baseVal . height
this . # buffer_length = analyzer_node . frequencyBinCount
this . # smoothing_algorythm = SmoothingAlgorythm . Linear
this . # fft_size = 2048
this . # fft_multipier = 1.5
this . # fft_offset = 150
this . # from_fft_range = 0
this . # to_fft_range = 100
this . # point_count = Math . round ( ( this . # buffer_length / 100 ) * ( this . # from_fft_range - this . # to_fft_range ) )
2023-05-24 13:23:58 +00:00
}
2023-05-24 15:42:05 +00:00
/ * *
* The smoothingTimeConstant property of the AnalyserNode interface is a double value representing the averaging constant with the last analysis frame . It ' s basically an average between the current buffer and the last buffer the AnalyserNode processed , and results in a much smoother set of value changes over time .
* @param fft_time_smoothing_i A double within the range 0 to 1 ( 0 meaning no time averaging ) . The default value is 0.8 .
* @returns this
* /
set_fft_time_smoothing ( fft_time_smoothing_i : number ) {
this . # analyzer_node . smoothingTimeConstant = fft_time_smoothing_i
2023-05-24 13:23:58 +00:00
return this
}
2023-05-24 15:42:05 +00:00
/ * *
* The fftSize property of the AnalyserNode interface is an unsigned long value and represents the window size in samples that is used when performing a Fast Fourier Transform ( FFT ) to get frequency domain data .
*
* If the point count set for the analyzer is less than the fft_from > FFT_DATA > fft_to , points might go missing or something will break . In that case increase , otherwise keep to as low as possible due to performance reasons
* @param fft_size Must be a power of 2 between 2 ^ 5 and 2 ^ 15 , so one of : ` 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, and 32768. ` Defaults to ` 2048 ` .
* @returns this
* /
set_fft_size ( fft_size : number ) {
if ( ! ( this . # fft_size && ! ( this . # fft_size & ( this . # fft_size - 1 ) ) ) ) throw Error ( "fft_size not power of two" )
this . # analyzer_node . fftSize = this . # fft_size = fft_size
this . # buffer_length = this . # analyzer_node . frequencyBinCount
2023-05-24 13:23:58 +00:00
return this
}
2023-05-24 15:42:05 +00:00
/ * *
* Changes how the FFT data is parsed .
* Limits the range like :
*
* ` from_fft_range < FFT_DATA < to_fft_range ` ,
*
* then splits the remaining data evently between point_count .
* ` fft_multiplier ` and ` fft_offset ` define how the data then affects the point mutation . FFT data goes from ` -Infinity ` to ` 0 ` ( Noise floor usually around ` -160 ` db from experience ) Multiplying the number exaggerates the higher decibel changes , making the result more dynamic , whilst the offset keeps the visualizer from clipping .
*
* Reccomended values are around :
*
* ` shape_type == line `
*
* - multiplier : 3
*
* - offset : - 30
*
* ` shape_type == circle `
*
* - multiplier : 1.5
*
* - offset : 150
* @param param0
* @returns
* /
set_fft_data_tresholds ( { from_fft_range_i = 0 , to_fft_range_i = 100 , point_count_i = Math . round ( ( this . # buffer_length / 100 ) * ( from_fft_range_i - to_fft_range_i ) ) , fft_multiplier_i = 2 , fft_offset_i = - 50 } ) {
this . # from_fft_range = from_fft_range_i
this . # to_fft_range = to_fft_range_i
this . # point_count = point_count_i
this . # fft_multipier = fft_multiplier_i
this . # fft_offset = fft_offset_i
2023-05-24 13:23:58 +00:00
return this
}
2023-05-24 15:42:05 +00:00
/ * *
* Defines what the svg d attribute path command will use .
*
* ` Linear ` - Uses no interpolation between points , jagged but very fast
*
* ` BezierPerpendicular ` - Sets the Cubic Bézier anchors perpendicular to the point . Great for Line shapes with no curves .
*
* ` CatmullRom ` - Uses Centripetal Catmull – Rom spline under the hood , then translates them to Cubic Bézier points . Best quality , worst performance .
*
* ` BezierWeighted ` - DO NOT USE ! It 's broken at the moment :/. Sets the Cubic Bézier anchors halfway between the next and previous point. Better than Linear on Circular shapes and doesn' t have the "invards bulding" side effect of Catmull Rom
* @param algorythm Linear = 0 ; BezierPerpendicular = 1 ; CatmullRom = 2 ; BezierWeighted = 3 ;
* @returns
* /
set_smoothing_algorythm ( algorythm : SmoothingAlgorythm ) {
this . # smoothing_algorythm = algorythm
2023-05-24 13:23:58 +00:00
return this
}
2023-05-24 15:42:05 +00:00
/ * *
* Creates the Audio Visualizer . To start drawing , use . draw ( ) . To further modify it ' s looks use CSS , eg :
* ` ` ` css
* svg {
* fill : grey ;
* stroke : black ;
* transform : rotate ( 90 deg ) ;
* }
* ` ` `
* @param shape_type Circle = 0 ; Line = 1 ;
* @returns ` new AudioVisual `
* /
build ( shape_type : ShapeType ) {
const shape = this . # create_shape ( shape_type )
return new AudioVisual ( this . # analyzer_node , this . # svg_injecting_element , shape , this . # buffer_length , this . # fft_multipier , this . # fft_offset , this . # from_fft_range , this . # to_fft_range , this . # point_count )
2023-05-24 13:23:58 +00:00
}
2023-05-24 15:42:05 +00:00
# create_shape ( shape_type : ShapeType ) : Shape {
const point_amount = this . # get_cured_frequency_data ( ) . length
2023-05-24 13:23:58 +00:00
let new_shape : Shape
switch ( shape_type ) {
case ShapeType . Line : {
const points = [ ]
for ( let i = 0 ; i < point_amount ; i ++ ) {
points . push ( {
2023-05-24 15:42:05 +00:00
x : ( this . # canvas_width / point_amount ) * i ,
y : this. # canvas_height / 2 - ( 0 / point_amount ) * i ,
2023-05-24 13:23:58 +00:00
} )
}
2023-05-24 15:42:05 +00:00
new_shape = { shape_type , points , smoothing_algorythm : this. # smoothing_algorythm }
2023-05-24 13:23:58 +00:00
break
}
case ShapeType . Circle : {
const points = [ ]
2023-05-24 15:42:05 +00:00
const radius = this . # canvas_height > this . # canvas_width ? this . # canvas_height / 5 : this. # canvas_width / 5
2023-05-24 13:23:58 +00:00
for ( let i = 0 ; i < point_amount ; i ++ ) {
points . push ( {
x : Math.cos ( ( ( 2 * Math . PI ) / point_amount ) * i - Math . PI / 2 ) * radius ,
y : Math.sin ( ( ( 2 * Math . PI ) / point_amount ) * i - Math . PI / 2 ) * radius ,
} )
}
2023-05-24 15:42:05 +00:00
new_shape = { shape_type , points , smoothing_algorythm : this. # smoothing_algorythm }
2023-05-24 13:23:58 +00:00
break
}
}
2023-05-24 15:42:05 +00:00
return new_shape
2023-05-24 13:23:58 +00:00
}
2023-05-24 15:42:05 +00:00
# get_cured_frequency_data() {
const fft_data_array = new Float32Array ( this . # buffer_length )
this . # analyzer_node . getFloatFrequencyData ( fft_data_array )
const from = Math . round ( ( this . # point_count / 100 ) * this . # from_fft_range )
const to = Math . round ( this . # buffer_length - ( this . # buffer_length / 100 ) * this . # to_fft_range )
const squeezeFactor = Math . round ( ( this . # buffer_length - to ) / this . # point_count )
2023-05-24 13:23:58 +00:00
2023-05-24 15:42:05 +00:00
const return_array = new Array ( this . # point_count )
for ( let i = 0 ; i < this . # point_count ; i ++ ) {
2023-05-24 13:23:58 +00:00
return_array [ i ] = fft_data_array [ from + i * squeezeFactor ]
}
return return_array
}
2023-05-24 15:42:05 +00:00
2023-05-24 13:23:58 +00:00
}