最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How can I generate random data that simulates an audio signal? - Stack Overflow

programmeradmin0浏览0评论

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.

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.

Share Improve this question edited May 31, 2013 at 22:29 Peter O. 32.9k14 gold badges84 silver badges97 bronze badges asked May 28, 2013 at 16:32 Jason SperskeJason Sperske 30.4k8 gold badges76 silver badges127 bronze badges 6
  • 4 I don't know what an audio signal is supposed to "feel" like, but I'm pretty sure it's not the same as random noise. Perhaps you should set the value for each bar as, say a random value within plus or minus two of the previous bar's value? – Blazemonger Commented May 28, 2013 at 16:38
  • Maybe I need to combine a few signals, random noise for "fluttering" and a curve for the "turning it up" effect. – Jason Sperske Commented May 28, 2013 at 16:40
  • I've been working on implementing a bezier curve. Here is my progress so far: jsfiddle.net/w9QAf/5 – Jason Sperske Commented May 28, 2013 at 17:10
  • You probably want a shape similar to y=1/(1+x^2). – Blazemonger Commented May 28, 2013 at 17:26
  • 2 @JasonSperske Have you considered using data from an actual freeware audio file? This question is interesting, but for practical purposes that's probably what I'd do. – Benjamin Gruenbaum Commented Jun 1, 2013 at 1:39
 |  Show 1 more comment

4 Answers 4

Reset to default 9 +100

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/

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.

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:

  • decide on a frame rate to refresh.
  • Load 1/fps of a second worth of samples from your audio file into a memory buffer (uncompressed, if it is .mp3 convert it to .wav first)
  • Send the buffer to the FFT function. You will get another buffer with an array of values in return.
  • Decide how many bars you want to show and find a portion of the FFT buffer that has that many samples with interesting data. You can also condense the output FFT array by averaging a number of consecutive values.
  • Each value in the array (or averaged group) gives you the height of a bar.
  • That will give you one frame of your animation. Take the next 1/fps of a second of audio and repeat the whole process to get the next frame.

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.

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); }
    });
}
发布评论

评论列表(0)

  1. 暂无评论