-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathspectrogram-component.js
More file actions
176 lines (161 loc) · 7.75 KB
/
spectrogram-component.js
File metadata and controls
176 lines (161 loc) · 7.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
An A-Frame component that uses an audio analyser component connected to an audio source to create audio-reactive elements arranged in a matrix that represent binned audio frequency levels over time (bins x time)
*/
AFRAME.registerComponent('spectrogram', {
schema: {
// The number of bins in which to divide the frequency spectrum (i.e., the number of rows of shapes)
bins: {type: 'int', default: 8},
// The number of times to draw a frequency spectrum sample (i.e., the number of columns of shapes)
samples: {type: 'int', default: 8},
// Three values representing the size scale factor in the x,y,z axes
sizeScale: {type: 'vec3'},
// Three values representing the position scale factor in the x,y,z axes
positionScale: {type: 'vec3'},
// The amount of space between bins (i.e., shapes along a row)
binSpacing: {type: 'float', default: 0},
// The amount of space between samples (i.e., shapes along a column)
sampleSpacing: {type: 'float', default: 0},
// The axis along which the bins are arranged
binAlong: {type: 'string', default: 'x-axis'},
// The axis along which the samples are arranged
sampleAlong: {type: 'string', default: 'z-axis'},
// Only affect x scaling in one direction
offsetX: {type: 'boolean'},
// Only affect y scaling in one direction
offsetY: {type: 'boolean'},
// Only affect z scaling in one direction
offsetZ: {type: 'boolean'},
// How the frequency values are scaled to determine average bin frequency
frequencyScale: {type: 'string', default: 'log'},
// The type of shape to draw
shape: {type: 'string', default: 'box'},
// The color of the shape
color: {type: 'string', default: 'grey'}
},
init: function () {
// Create empty array that will hold samples at time 'x'
this.audioAnalyser = this.el.components.audioanalyser;
this.sampleT = []
// Clone the initial values of the element's scale and position
this.initialScale = this.el.object3D.scale.clone()
this.initialPos = {x:0,y:0,z:0}//this.el.object3D.position.clone()
this.childrenInitialPos = []
// Set up the positioning scheme as a line along an axis using the
// scale factor of the 'binAlong' axis
const xMultiplier = this.data.binAlong === 'x-axis' ?
this.initialScale.x + this.data.binSpacing : 0
const yMultiplier = this.data.binAlong === 'y-axis' ?
this.initialScale.y + this.data.binSpacing : 0
const zMultiplier = this.data.binAlong === 'z-axis' ?
this.initialScale.z + this.data.binSpacing : 0
// Set up the positioning scheme as a line along an axis using the
// scale factor of the 'sampleAlong' axis
const xMultiplier2 = this.data.sampleAlong === 'x-axis' ?
this.initialScale.x + this.data.sampleSpacing : 0
const yMultiplier2 = this.data.sampleAlong === 'y-axis' ?
this.initialScale.y + this.data.sampleSpacing : 0
const zMultiplier2 = this.data.sampleAlong === 'z-axis' ?
this.initialScale.z + this.data.sampleSpacing : 0
// Function that returns a one object array containing the x, y, z
// posiiton of each child element
const setChildPosition = (band, sample) => [{
x: this.initialPos.x + band * xMultiplier +
sample * xMultiplier2,
y: this.initialPos.y + band * yMultiplier +
sample * yMultiplier2,
z: this.initialPos.z + band * zMultiplier +
sample * zMultiplier2
}
]
// Create a child entity for each band and set some parameters
for (let j = 0; j < this.data.samples; j++) {
for (let i = 0; i < this.data.bins; i++) {
const childEntity = document.createElement('a-entity')
this.el.appendChild(childEntity)
childEntity.setAttribute('geometry', { primitive: this.data.shape })
childEntity.setAttribute('material', { color: this.data.color })
// add particle to each box
// childEntity.setAttribute('particle-system', { color: this.data.color })
childEntity.setAttribute('position', ...setChildPosition(i, j))
this.childrenInitialPos.push(...setChildPosition(i, j))
}
}
},
tick: function () {
if (!this.audioAnalyser.levels) return console.log(`No audio analyser component yet`);
// console.log(this.audioAnalyser)
// Get average levels of bins
const levels = this.audioAnalyser.levels
let levelsSum = 0;
let numFrequencies = 0;
let bandMax = 0;
const bandedLevels = [];
// Default to log scale if linear isn't explicitly declared
if (this.data.frequencyScale === 'linear') {
// Determine the bandwidth based on the levels sample size and number
// of bins
const bandWidth = Math.floor(levels.length / this.data.bins)
calculateLinearAverageBins(bandWidth)
} else {
const logBandWidth = this.data.bins + 1 / Math.log10(levels.length)
calculateLogAverageBins(logBandWidth)
}
// Add frequency bins to time sample array
if (this.sampleT.length === this.data.samples) { this.sampleT.pop() }
this.sampleT.unshift(bandedLevels)
// Function that determines the average levels within a specified
// bandwidth over a linear scale of frequency levels and pushes to an
// array of length this.data.bins
function calculateLinearAverageBins(bandWidth) {
for (let i = 0; i < levels.length; i++) {
levelsSum += levels[i]
numFrequencies++
if ((i + 1) % bandWidth === 0) {
bandedLevels.push(levelsSum / numFrequencies / 255)
levelsSum = 0
numFrequencies = 0
}
}
}
// Function that determines the average levels within a specified
// bandwidth over a log10 scale of frequency levels and pushes to an
// array of length this.data.bins
function calculateLogAverageBins(bandWidth) {
for (let i = 0; i < levels.length; i++) {
levelsSum += levels[i]
numFrequencies++
if (Math.floor(Math.log10(i + 1) * bandWidth) > bandMax) {
bandedLevels.push(levelsSum / numFrequencies / 255)
levelsSum = 0
numFrequencies = 0
bandMax = Math.floor(Math.log10(i + 1) * bandWidth)
}
}
}
// Set the scale values of each child based on the average volume of each
// band and scale factor
const children = this.el.children
for (let j = 0; j < this.sampleT.length; j++) {
for (let i = 0; i < this.data.bins; i++) {
const child = children[i + j * this.data.bins].object3D
const k = i + j * this.data.bins
child.scale.x = this.initialScale.x *
(1 + this.data.sizeScale.x * this.sampleT[j][i])
child.scale.y = this.initialScale.y *
(1 + this.data.sizeScale.y * this.sampleT[j][i])
child.scale.z = this.initialScale.z *
(1 + this.data.sizeScale.z * this.sampleT[j][i])
child.position.x = this.childrenInitialPos[k].x + (1 + this.data.positionScale.x * this.sampleT[j][i])
child.position.y = this.childrenInitialPos[k].y + (1 + this.data.positionScale.y * this.sampleT[j][i])
child.position.z = this.childrenInitialPos[k].z + (1 + this.data.positionScale.z * this.sampleT[j][i])
// Set positive offset of element along axis if specified
if (this.data.offsetX)
{ child.position.x = this.initialPos.x + child.scale.x / 2 }
if (this.data.offsetY)
{ child.position.y = this.initialPos.y + child.scale.y / 2 }
if (this.data.offsetZ)
{ child.position.z = this.initialPos.z + child.scale.z / 2 }
}
}
}
})