Reputation: 30416
I'm working on an animated logo that will be revealed by a spectrum analyzer going from zero to eleven. I'm looking for something that will work on a broad variety of browsers so wiring it up to an HTML5-audio element is likely not an option as the only libraries I've found that can do this work on only the newest WebKit and Firefox releases. So far I've be playing with just generating a random value at an interval. Here is an example of where I am currently stuck (using jQuery's animate function()):
<div id='Logo'>
<div id='channelA' class='channel'></div>
<div id='channelB' class='channel'></div>
<div id='channelC' class='channel'></div>
<div id='channelD' class='channel'></div>
<div id='channelE' class='channel'></div>
<div id='channelF' class='channel'></div>
<div id='channelG' class='channel'></div>
</div>
<script>
setInterval(function () {
$('.channel').each(function () {
$(this).animate({
height: (Math.round(Math.random() * 185)) + 'px'
});
});
}, 100);
</script>
<style>
#Logo {
width: 245px;
height: 245px;
background: red;
}
div.channel {
float: left;
z-index: 9;
background: white;
}
#channelA {
width: 35px;
height: 45px;
}
#channelB {
width: 35px;
height: 85px;
}
#channelC {
width: 35px;
height: 85px;
}
#channelD {
width: 35px;
height: 50px;
}
#channelE {
width: 35px;
height: 150px;
}
#channelF {
width: 35px;
height: 30px;
}
#channelG {
width: 35px;
height: 85px;
}
</style>
This doesn't look "right". Is there a function that can generate data that "feels" more like an audio signal? I'm also interested in other approaches to this problem (maybe I just need to capture spectrum analyzer data in a browser that supports HTML5 audio and then "play it back" in older browsers.)
This is an example of the kind of look I am going for:
After a little searching for a implementation of Bézier curves in JavaScript I've started mixing generated singles to produce something. Though my work in unfinished, in case this gives anyone else any ideas here is a demo.
Upvotes: 10
Views: 3428
Reputation:
The key to make a spectrum look realistic (virtual data or not) is to have a fallback mechanism for the band bar.
A band is only set if the new value is higher than the current. If not the current value is decreased by a value (linear or logarithmic). The speed of the fallback influence the perception as well.
As the data in a spectrum analyzer does not represent the actual wave form but the FFT (fast-fourier transform), or value of each frequency band, it can work fine with random data. You will course not get the "rhythmic" fingerprint as for music, but due to having fallback it will still look realistic to a certain degree (as if anyone wanted to listen to noise that is :-) ).
An example follows -
Demo here:
http://jsfiddle.net/AbdiasSoftware/VXxwt/
Initial HTML, a simple div:
<div id="logo"></div>
Inital CSS:
.band {
background-color: #3897e0;
border-radius:3px 3px 0 0;
}
And the main call to create a virtual spectrum of of that:
makeSpectrum('logo', 300, 120, 7);
Full code:
/**
* Turn an element into a virtual spectrum,
* Ken Fyrstenberg Nilsen, Public domain.
*
* USAGE:
* makeSpectrum(id, width, height)
* makeSpectrum(id, width, height, bands)
* makeSpectrum(id, width, height, bands, volume)
*
* id id of the element to be converted into spectrum
* width width in pixels of spectrum
* height height in pixels of spectrum
* bands (optional) number of "bands"
* volume initial volume (0-1)
*
* METHODS:
*
* setVolume() returns current volume
* setVolume(vol) sets new volume (0-1 float)
*/
function makeSpectrum(id, width, height, bands, volume) {
bands = bands ? bands : 12;
volume = volume ? volume : 1;
if (bands < 1) bands = 1;
if (bands > 128) bands = 128;
// init parent element
var parent = document.getElementById(id),
bandElements = [];
if (typeof parent === 'undefined')
alert('Element ' +id + ' not found!');
parent.style.display = 'block';
parent.style.width = width + 'px';
parent.style.height = height + 'px';
parent.style.position = 'relative';
var bandValues = [],
oldBandValues = [],
bw = (((width)/ bands) |0),
me = this;
function calcBand(bandNum) {
var bv = bandValues[bandNum],
obv = oldBandValues[bandNum];
if (bv >= obv) obv = bv;
obv -= 0.1;
if (obv < 0 ) obv = 0;
obv *= volume;
oldBandValues[bandNum] = obv;
return obv;
}
function getFFT(band) {
band = band ? band : bandValues;
for(var i = 0; i < bands; i++) {
band[i] = Math.random();
}
}
function createBands() {
var i, html = '';
for(i = 0; i < bands; i++) {
h = 0
html += '<div id="' + id + '_band' + i + '" ';
html += 'style="display:block;position:absolute;';
html += 'left:' + ((i * bw + 1)|0);
html += 'px;top:' + ((height - height * h)|0);
html += 'px;width:' + (bw - 2);
html += 'px;height:' + ((height * h)|0);
html += 'px;" class="band"></div>';
}
parent.innerHTML = html;
for(i = 0; i < bands; i++) {
var el = document.getElementById(id + '_band' + i);
bandElements.push(el);
}
}
this.setVolume = function(vol) {
if (arguments.length === 0)
return volume;
if (vol < 0) vol = 0;
if (vol > 1) vol = 1;
volume = vol;
}
this.setVolume(volume);
this.createSnapshot = function() {
var h, y, el;
getFFT(bandValues);
for(var i = 0; i < bands; i++) {
h = calcBand(i);
el = bandElements[i].style;
el.top = ((height - height * h)|0) + 'px';
el.height = ((height * h)|0) + 'px';
}
parent.innerHTML = html;
}
//init bands
getFFT(oldBandValues);
createBands();
//GO
setInterval(me.createSnapshot, 100);
return this;
}
var sp = makeSpectrum('logo', 250, 100, null, 0);
var vol = 0;
function fadeIn() {
vol += 0.02;
sp.setVolume(vol);
if (vol < 1) setTimeout(fadeIn, 60);
}
fadeIn();
The code is not optimized so it runs a little hungry on CPU. It's mainly the way html is generated for the bands. I would prefer to do it on a canvas element which would work much more efficient, but as multitude of browser support is required I left it with this :-)
UPDATE:
Optimized the loop setting height and top on a cached element. It also has a method setVolume()
which can used to set the overall "volume" from a loop etc.
Updated the example (link at top) with a fade-in and new code.
UPDATE 2:
Added more realism in the lower frequence and by simulating a BPM based of on the internal clock. I now make the time affect the three first bands (if number of bands allow):
var d = (new Date()).getMilliseconds() % 10; //get milliseonds of second
band[0] = band[0] * 0.2 + (d / 10) * 0.8; //affect 1. band 80% + 20% random
band[1] = band[1] * 0.3 + (d / 10) * 0.7; //affect 2. band 70% + 30% random
band[2] = band[2] * 0.5 + (d / 10) * 0.5; //affect 3. band 50% + 50% random
It's perhaps subtle, but just to add a little bit more of realism.
Styled version here with mute functionality:
http://jsfiddle.net/AbdiasSoftware/hVkPN/
Upvotes: 9
Reputation: 1976
I did a bit of toying around with volumes, delays and curving. Here's my jsfiddle take on it. Comes fairly close to it, I think :-) But definitely improvable (there's a slight issue with the rightmost channel).
[Edit:] Ok, I improved over it a bit more. Here's another jsfiddle
As you can see, you don't necessarily need a bezier curve implementation to get the curving working.
$('.channel').each(function () {
animate($(this));
});
var beat = 200;
var volume = 1;
setInterval(function() {
beat = 60 + Math.random()*100;
volume = Math.min(1,Math.max(0.1,volume + Math.random()-0.5));
}, 200);
function animate($channel) {
var curving = Math.max(1, 30*Math.abs($channel.prevAll().length - 3));
curving = curving + 100*volume;
var height = Math.max(1,Math.min(curving, 240) + (Math.random()*50-25));
$channel.animate({
height: Math.round(height) + 'px',
}, {
duration: beat,
complete: function() { animate($channel); }
});
}
Upvotes: 1
Reputation: 67479
I think the best way to make the animation look real is to use real data and to calculate real values for the bars. Doing the animation on actual data will make the bars go up and down in a realistic manner, it may be hard to imitate this behavior without doing the processing.
What the equalizer bars tell you is what is the amplitude of the different frequency ranges. You can calculate these bars from an actual audio file using a function called FFT (Fast Fourier Transform).
There are implementations of FFT for pretty much all languages, even for Javascript. The algorithm will work as follows:
The FFT is pretty CPU intensive so you may want to calculate the bars for enough frames and then use the calculated values in your script.
If you want an example to get you started, have a look at this demo: Fast Fourier Transforms with audiolib.js.
This demo generates a synthetic waveform, plays it on your browser (not necessary for you) and draws a real time equalizer in a Canvas object. The example on this page does not work for me for some reason, so I downloaded the source code gist and installed it on my computer to make it work.
Upvotes: 2
Reputation: 30416
I think I'm closer (but I've done a lot so I thought providing it as an answer to my own question was the best approach, I'm still open to better answers, or feedback(ha) on issues with this approach). Here is the (complicated) code I've implemented thus far (demo).
Using a cleaned up version of the HTML and CSS here is the JavaScript:
var amp = 0,
flutter_range = 10,
channels = $('.channel'),
turnItUp = setInterval(function () {
amp += 0.01;
}, 50),
flutter = setInterval(function () {
var levels = bezier([[0.3], [0.95], [1], [0]]),
channel;
for(channel = 0; channel < channels.length; channel++) {
$(channels[channel]).animate({
height: 245-(Math.round(((Math.random() * (flutter_range*2))-flutter_range)+(levels(channel/channels.length)*amp)*245))+'px'
}, 50);
}
}, 100),
//from: https://gist.github.com/atomizer/1049745
bezier = function (pts) {
return function (t) {
for (var a = pts; a.length > 1; a = b) // do..while loop in disguise
for (var i = 0, b = [], j; i < a.length - 1; i++) // cycle over control points
for (b[i] = [], j = 0; j < a[i].length; j++) // cycle over dimensions
b[i][j] = a[i][j] * (1 - t) + a[i + 1][j] * t; // interpolation
return a[0];
}
};
setTimeout(function () {
window.clearInterval(turnItUp);
}, 5000);
The way it works is to turn an "amp" up over a 5 second period while filing out a bezier curve, and then applying a random "flutter" to the data giving it a "audio feel". The bezier function comes from this gist.
Upvotes: 3