Создайте форму полного трека с помощью Web Audio API

Движущаяся волна в реальном времени

Я в настоящее время играю с API Web Audio и сделал спектр, используя холст.

function animate(){
 var a=new Uint8Array(analyser.frequencyBinCount),
     y=new Uint8Array(analyser.frequencyBinCount),b,c,d;
 analyser.getByteTimeDomainData(y);
 analyser.getByteFrequencyData(a);
 b=c=a.length;
 d=w/c;
 ctx.clearRect(0,0,w,h);
 while(b--){
  var bh=a[b]+1;
  ctx.fillStyle='hsla('+(b/c*240)+','+(y[b]/255*100|0)+'%,50%,1)';
  ctx.fillRect(1*b,h-bh,1,bh);
  ctx.fillRect(1*b,y[b],1,1);
 }
 animation=webkitRequestAnimationFrame(animate);
}

Мини вопрос: есть ли способ не писать 2 раза new Uint8Array(analyser.frequencyBinCount)?

DEMO

добавьте файл MP3/MP4 и подождите. (проверено в Chrome)

http://jsfiddle.net/pc76H/2/

Но есть много проблем. Я не могу найти надлежащую документацию различных аудио фильтров.

Кроме того, если вы посмотрите на спектр, вы заметите, что после 70% или диапазона нет данных. Что это значит? что может быть от 16кГц до 20кц нет звука? Я бы применил текст к холсту, чтобы показать различные HZ. но где??

Я обнаружил, что возвращаемые данные имеют степень 32 в длину и максимум 2048, а высота всегда 256.

НО реальный вопрос в том... Я хочу создать движущуюся форму волны, как в traktor.

Я уже делал это некоторое время назад с помощью PHP, он конвертирует файл в низкий битрейт, чем извлекает данные и преобразует их в изображение. я где-то нашел сценарий... но я не помню где... примечание: нужен LAME

<?php
$a=$_GET["f"];
if(file_exists($a)){
    if(file_exists($a.".png")){
        header("Content-Type: image/png");
        echo file_get_contents($a.".png");
    }else{
        $b=3000;$c=300;define("d",3);
        ini_set("max_execution_time","30000");
        function n($g,$h){
            $g=hexdec(bin2hex($g));
            $h=hexdec(bin2hex($h));
            return($g+($h*256));
        };
        $k=substr(md5(time()),0,10);
        copy(realpath($a),"/var/www/".$k."_o.mp3");
        exec("lame /var/www/{$k}_o.mp3 -f -m m -b 16 --resample 8 /var/www/{$k}.mp3 && lame --decode /var/www/{$k}.mp3 /var/www/{$k}.wav");
        //system("lame {$k}_o.mp3 -f -m m -b 16 --resample 8 {$k}.mp3 && lame --decode {$k}.mp3 {$k}.wav");
        @unlink("/var/www/{$k}_o.mp3");
        @unlink("/var/www/{$k}.mp3");
        $l="/var/www/{$k}.wav";
        $m=fopen($l,"r");
        $n[]=fread($m,4);
        $n[]=bin2hex(fread($m,4));
        $n[]=fread($m,4);
        $n[]=fread($m,4);
        $n[]=bin2hex(fread($m,4));
        $n[]=bin2hex(fread($m,2));
        $n[]=bin2hex(fread($m,2));
        $n[]=bin2hex(fread($m,4));
        $n[]=bin2hex(fread($m,4));
        $n[]=bin2hex(fread($m,2));
        $n[]=bin2hex(fread($m,2));
        $n[]=fread($m,4);
        $n[]=bin2hex(fread($m,4));
        $o=hexdec(substr($n[10],0,2));
        $p=$o/8;
        $q=hexdec(substr($n[6],0,2));
        if($q==2){$r=40;}else{$r=80;};
        while(!feof($m)){
            $t=array();
            for($i=0;$i<$p;$i++){
                $t[$i]=fgetc($m);
            };
            switch($p){
                case 1:$s[]=n($t[0],$t[1]);break;
                case 2:if(ord($t[1])&128){$u=0;}else{$u=128;};$u=chr((ord($t[1])&127)+$u);$s[]= floor(n($t[0],$u)/256);break;
            };
            fread($m,$r);
        };
        fclose($m);
        unlink("/var/www/{$k}.wav");
        $x=imagecreatetruecolor(sizeof($s)/d,$c);
        imagealphablending($x,false);
        imagesavealpha($x,true);
        $y=imagecolorallocatealpha($x,255,255,255,127);
        imagefilledrectangle($x,0,0,sizeof($s)/d,$c,$y);
        for($d=0;$d<sizeof($s);$d+=d){
            $v=(int)($s[$d]/255*$c);
            imageline($x,$d/d,0+($c-$v),$d/d,$c-($c-$v),imagecolorallocate($x,255,0,255));
        };
        $z=imagecreatetruecolor($b,$c);
        imagealphablending($z,false);
        imagesavealpha($z,true);
        imagefilledrectangle($z,0,0,$b,$c,$y);
        imagecopyresampled($z,$x,0,0,0,0,$b,$c,sizeof($s)/d,$c);
        imagepng($z,realpath($a).".png");
        header("Content-Type: image/png");
        imagepng($z);
        imagedestroy($z);
    };
}else{
    echo $a;
};

?>

Скрипт работает... но вы ограничены максимальным размером изображения 4k пикселей.

таким образом, у вас нет хорошей формы волны, если она должна присутствовать только в течение нескольких миллисекунд.

Что мне нужно для хранения / создания сигнала в реальном времени, такого как приложение traktors или этот скрипт php? Кстати, у трактора также есть цветная форма волны (нет сценария php).

РЕДАКТИРОВАТЬ

Я переписал твой сценарий, чтобы он соответствовал моей идее... он относительно быстрый.

Как вы можете видеть внутри функции createArray, я вставляю различные строки в объект с ключом в виде координаты x.

Я просто беру самый высокий номер.

Здесь мы могли бы поиграть с цветами.

var ajaxB,AC,B,LC,op,x,y,ARRAY={},W=1024,H=256;
var aMax=Math.max.apply.bind(Math.max, Math);
function error(a){
 console.log(a);
};
function createDrawing(){
 console.log('drawingArray');
 var C=document.createElement('canvas');
 C.width=W;
 C.height=H;
 document.body.appendChild(C);
 var context=C.getContext('2d');
 context.save();
 context.strokeStyle='#121';
 context.globalCompositeOperation='lighter';
 L2=W*1;
 while(L2--){
  context.beginPath();
  context.moveTo(L2,0);
  context.lineTo(L2+1,ARRAY[L2]);
  context.stroke();
 }
 context.restore();
};
function createArray(a){
 console.log('creatingArray');
 B=a;
 LC=B.getChannelData(0);// Float32Array describing left channel
 L=LC.length;  
 op=W/L;
 for(var i=0;i<L;i++){
  x=W*i/L|0;
  y=LC[i]*H/2;
  if(ARRAY[x]){
   ARRAY[x].push(y)
  }else{
   !ARRAY[x-1]||(ARRAY[x-1]=aMax(ARRAY[x-1]));
   // the above line contains an array of values
   // which could be converted to a color 
   // or just simply create a gradient 
   // based on avg max min (frequency???) whatever
   ARRAY[x]=[y]
  }
 };
 createDrawing();
};
function decode(){
 console.log('decodingMusic');
 AC=new webkitAudioContext
 AC.decodeAudioData(this.response,createArray,error);
};
function loadMusic(url){
 console.log('loadingMusic');   
 ajaxB=new XMLHttpRequest;
 ajaxB.open('GET',url);
 ajaxB.responseType='arraybuffer';    
 ajaxB.onload=decode;
 ajaxB.send();
}
loadMusic('AudioOrVideo.mp4');

4 ответа

Решение

Итак, что я хотел бы сделать, это загрузить звук с помощью XMLHttpRequest, затем декодировать его с помощью webaudio, а затем отобразить его "осторожно", чтобы получить искомые цвета.

Я только что сделал быструю версию, вставив копию из моих проектов, и она вполне работает, как вы можете видеть на следующем рисунке:

Проблема в том, что это медленно, как ад. Чтобы иметь (более) приличную скорость, вам придется выполнить некоторые вычисления, чтобы уменьшить количество линий, которые нужно нарисовать на холсте, потому что при 441000 Гц вы очень быстро получаете слишком много линий для рисования.

// AUDIO CONTEXT
window.AudioContext = window.AudioContext || window.webkitAudioContext ;

if (!AudioContext) alert('This site cannot be run in your Browser. Try a recent Chrome or Firefox. ');

var audioContext = new AudioContext();
var currentBuffer  = null;

// CANVAS
var canvasWidth = 512,  canvasHeight = 120 ;
var newCanvas   = createCanvas (canvasWidth, canvasHeight);
var context     = null;

window.onload = appendCanvas;
function appendCanvas() { document.body.appendChild(newCanvas);
                          context = newCanvas.getContext('2d'); }

// MUSIC LOADER + DECODE
function loadMusic(url) {   
    var req = new XMLHttpRequest();
    req.open( "GET", url, true );
    req.responseType = "arraybuffer";    
    req.onreadystatechange = function (e) {
          if (req.readyState == 4) {
             if(req.status == 200)
                  audioContext.decodeAudioData(req.response, 
                    function(buffer) {
                             currentBuffer = buffer;
                             displayBuffer(buffer);
                    }, onDecodeError);
             else
                  alert('error during the load.Wrong url or cross origin issue');
          }
    } ;
    req.send();
}

function onDecodeError() {  alert('error while decoding your file.');  }

// MUSIC DISPLAY
function displayBuffer(buff /* is an AudioBuffer */) {
   var leftChannel = buff.getChannelData(0); // Float32Array describing left channel     
   var lineOpacity = canvasWidth / leftChannel.length  ;      
   context.save();
   context.fillStyle = '#222' ;
   context.fillRect(0,0,canvasWidth,canvasHeight );
   context.strokeStyle = '#121';
   context.globalCompositeOperation = 'lighter';
   context.translate(0,canvasHeight / 2);
   context.globalAlpha = 0.06 ; // lineOpacity ;
   for (var i=0; i<  leftChannel.length; i++) {
       // on which line do we get ?
       var x = Math.floor ( canvasWidth * i / leftChannel.length ) ;
       var y = leftChannel[i] * canvasHeight / 2 ;
       context.beginPath();
       context.moveTo( x  , 0 );
       context.lineTo( x+1, y );
       context.stroke();
   }
   context.restore();
   console.log('done');
}

function createCanvas ( w, h ) {
    var newCanvas = document.createElement('canvas');
    newCanvas.width  = w;     newCanvas.height = h;
    return newCanvas;
};


loadMusic('could_be_better.mp3');

Изменить: проблема здесь в том, что у нас слишком много данных для рисования. Возьмите 3-минутный mp3, у вас будет 3*60*44100 = около 8.000.000 линий для рисования. На дисплее, скажем, с разрешением 1024 пикселей, что составляет 8000 строк на пиксель...
В приведенном выше коде холст выполняет "ресэмплинг", рисуя линии с низкой непрозрачностью и в "более светлом" режиме композиции (например, пиксель r, g, b будет сложен).
Чтобы ускорить процесс, вы должны сами повторно сэмплировать, но чтобы получить некоторые цвета, это не просто понижающая дискретизация, вам придется обрабатывать набор (в пределах массива производительности, наиболее вероятно) 'сегментов', по одному на каждый горизонтальный пиксель (скажем, 1024), и в каждом сегменте вы вычисляете накопленное звуковое давление, дисперсию, минимальное, максимальное и затем во время отображения вы решаете, как вы будете воспроизводить это с помощью цветов.
Например:
значения между 0 positiveMin очень ясны. (любой образец ниже этой точки).
значения между PositiveMin и positiveAverage - дисперсия темнее,
значения между positiveAverage - дисперсия и positiveAverage + дисперсия темнее,
и значения между PositiveAverage + Variance и PositiveMax легче.
(то же самое для отрицательных значений) Это дает 5 цветов для каждого сегмента, и это все еще довольно трудная задача для вас, чтобы кодировать и для браузера вычислять.
Я не знаю, может ли производительность с этим справиться, но я боюсь, что статистическая точность и цветовая кодировка программного обеспечения, о котором вы упоминаете, не могут быть достигнуты в браузере (очевидно, не в реальном времени), и что вы ' Придется пойти на некоторые компромиссы.

Изменить 2:
Я пытался получить некоторые цвета из статистики, но это не удалось. Теперь я думаю, что ребята из тректора также меняют цвет в зависимости от частоты.... тут довольно много работы....

Во всяком случае, просто для записи, код для среднего / среднего изменения следует.
(дисперсия была слишком низкой, мне пришлось использовать среднее отклонение).

// MUSIC DISPLAY
function displayBuffer2(buff /* is an AudioBuffer */) {
   var leftChannel = buff.getChannelData(0); // Float32Array describing left channel       
   // we 'resample' with cumul, count, variance
   // Offset 0 : PositiveCumul  1: PositiveCount  2: PositiveVariance
   //        3 : NegativeCumul  4: NegativeCount  5: NegativeVariance
   // that makes 6 data per bucket
   var resampled = new Float64Array(canvasWidth * 6 );
   var i=0, j=0, buckIndex = 0;
   var min=1e3, max=-1e3;
   var thisValue=0, res=0;
   var sampleCount = leftChannel.length;
   // first pass for mean
   for (i=0; i<sampleCount; i++) {
        // in which bucket do we fall ?
        buckIndex = 0 | ( canvasWidth * i / sampleCount );
        buckIndex *= 6;
        // positive or negative ?
        thisValue = leftChannel[i];
        if (thisValue>0) {
            resampled[buckIndex    ] += thisValue;
            resampled[buckIndex + 1] +=1;               
        } else if (thisValue<0) {
            resampled[buckIndex + 3] += thisValue;
            resampled[buckIndex + 4] +=1;                           
        }
        if (thisValue<min) min=thisValue;
        if (thisValue>max) max = thisValue;
   }
   // compute mean now
   for (i=0, j=0; i<canvasWidth; i++, j+=6) {
       if (resampled[j+1] != 0) {
             resampled[j] /= resampled[j+1]; ;
       }
       if (resampled[j+4]!= 0) {
             resampled[j+3] /= resampled[j+4];
       }
   }
   // second pass for mean variation  ( variance is too low)
   for (i=0; i<leftChannel.length; i++) {
        // in which bucket do we fall ?
        buckIndex = 0 | (canvasWidth * i / leftChannel.length );
        buckIndex *= 6;
        // positive or negative ?
        thisValue = leftChannel[i];
        if (thisValue>0) {
            resampled[buckIndex + 2] += Math.abs( resampled[buckIndex] - thisValue );               
        } else  if (thisValue<0) {
            resampled[buckIndex + 5] += Math.abs( resampled[buckIndex + 3] - thisValue );                           
        }
   }
   // compute mean variation/variance now
   for (i=0, j=0; i<canvasWidth; i++, j+=6) {
        if (resampled[j+1]) resampled[j+2] /= resampled[j+1];
        if (resampled[j+4]) resampled[j+5] /= resampled[j+4];   
   }
   context.save();
   context.fillStyle = '#000' ;
   context.fillRect(0,0,canvasWidth,canvasHeight );
   context.translate(0.5,canvasHeight / 2);   
  context.scale(1, 200);

   for (var i=0; i< canvasWidth; i++) {
        j=i*6;
       // draw from positiveAvg - variance to negativeAvg - variance 
       context.strokeStyle = '#F00';
       context.beginPath();
       context.moveTo( i  , (resampled[j] - resampled[j+2] ));
       context.lineTo( i  , (resampled[j +3] + resampled[j+5] ) );
       context.stroke();
       // draw from positiveAvg - variance to positiveAvg + variance 
       context.strokeStyle = '#FFF';
       context.beginPath();
       context.moveTo( i  , (resampled[j] - resampled[j+2] ));
       context.lineTo( i  , (resampled[j] + resampled[j+2] ) );
       context.stroke();
       // draw from negativeAvg + variance to negativeAvg - variance 
       // context.strokeStyle = '#FFF';
       context.beginPath();
       context.moveTo( i  , (resampled[j+3] + resampled[j+5] ));
       context.lineTo( i  , (resampled[j+3] - resampled[j+5] ) );
       context.stroke();
   }
   context.restore();
   console.log('done 231 iyi');
}

// AUDIO CONTEXT
window.AudioContext = (window.AudioContext || 
window.webkitAudioContext || 
window.mozAudioContext || 
window.oAudioContext || 
window.msAudioContext);

if (!AudioContext) alert('This site cannot be run in your Browser. Try a recent Chrome or Firefox. ');

var audioContext = new AudioContext();
var currentBuffer  = null;

// CANVAS
var canvasWidth = window.innerWidth,  canvasHeight = 120 ;
var newCanvas   = createCanvas (canvasWidth, canvasHeight);
var context     = null;

window.onload = appendCanvas;
function appendCanvas() { document.body.appendChild(newCanvas);
                          context = newCanvas.getContext('2d'); }

// MUSIC LOADER + DECODE
function loadMusic(url) {   
    var req = new XMLHttpRequest();
    req.open( "GET", url, true );
    req.responseType = "arraybuffer";    
    req.onreadystatechange = function (e) {
          if (req.readyState == 4) {
             if(req.status == 200)
                  audioContext.decodeAudioData(req.response, 
                    function(buffer) {
                             currentBuffer = buffer;
                             displayBuffer(buffer);
                    }, onDecodeError);
             else
                  alert('error during the load.Wrong url or cross origin issue');
          }
    } ;
    req.send();
}

function onDecodeError() {  alert('error while decoding your file.');  }

// MUSIC DISPLAY
function displayBuffer(buff /* is an AudioBuffer */) {
  
  var drawLines = 500;
   var leftChannel = buff.getChannelData(0); // Float32Array describing left channel     
   var lineOpacity = canvasWidth / leftChannel.length  ;      
   context.save();
   context.fillStyle = '#080808' ;
   context.fillRect(0,0,canvasWidth,canvasHeight );
   context.strokeStyle = '#46a0ba';
   context.globalCompositeOperation = 'lighter';
   context.translate(0,canvasHeight / 2);
   //context.globalAlpha = 0.6 ; // lineOpacity ;
   context.lineWidth=1;
   var totallength = leftChannel.length;
   var eachBlock = Math.floor(totallength / drawLines);
   var lineGap = (canvasWidth/drawLines);

  context.beginPath();
   for(var i=0;i<=drawLines;i++){
      var audioBuffKey = Math.floor(eachBlock * i);
       var x = i*lineGap;
       var y = leftChannel[audioBuffKey] * canvasHeight / 2;
       context.moveTo( x, y );
       context.lineTo( x, (y*-1) );
   }
   context.stroke();
   context.restore();
}

function createCanvas ( w, h ) {
    var newCanvas = document.createElement('canvas');
    newCanvas.width  = w;     newCanvas.height = h;
    return newCanvas;
};


loadMusic('https://raw.githubusercontent.com/katspaugh/wavesurfer.js/master/example/media/demo.wav');

Привет также столкнулся с проблемой времени загрузки. Просто я контролировал это, уменьшая количество линий, которые нужно нарисовать, и небольшое размещение вызова функции canvas. см. следующий код для справки.

// AUDIO CONTEXT
window.AudioContext = (window.AudioContext || 
window.webkitAudioContext || 
window.mozAudioContext || 
window.oAudioContext || 
window.msAudioContext);

if (!AudioContext) alert('This site cannot be run in your Browser. Try a recent Chrome or Firefox. ');

var audioContext = new AudioContext();
var currentBuffer  = null;

// CANVAS
var canvasWidth = window.innerWidth,  canvasHeight = 120 ;
var newCanvas   = createCanvas (canvasWidth, canvasHeight);
var context     = null;

window.onload = appendCanvas;
function appendCanvas() { document.body.appendChild(newCanvas);
                          context = newCanvas.getContext('2d'); }

// MUSIC LOADER + DECODE
function loadMusic(url) {   
    var req = new XMLHttpRequest();
    req.open( "GET", url, true );
    req.responseType = "arraybuffer";    
    req.onreadystatechange = function (e) {
          if (req.readyState == 4) {
             if(req.status == 200)
                  audioContext.decodeAudioData(req.response, 
                    function(buffer) {
                             currentBuffer = buffer;
                             displayBuffer(buffer);
                    }, onDecodeError);
             else
                  alert('error during the load.Wrong url or cross origin issue');
          }
    } ;
    req.send();
}

function onDecodeError() {  alert('error while decoding your file.');  }

// MUSIC DISPLAY
function displayBuffer(buff /* is an AudioBuffer */) {
  
  var drawLines = 500;
   var leftChannel = buff.getChannelData(0); // Float32Array describing left channel     
   var lineOpacity = canvasWidth / leftChannel.length  ;      
   context.save();
   context.fillStyle = '#080808' ;
   context.fillRect(0,0,canvasWidth,canvasHeight );
   context.strokeStyle = '#46a0ba';
   context.globalCompositeOperation = 'lighter';
   context.translate(0,canvasHeight / 2);
   //context.globalAlpha = 0.6 ; // lineOpacity ;
   context.lineWidth=1;
   var totallength = leftChannel.length;
   var eachBlock = Math.floor(totallength / drawLines);
   var lineGap = (canvasWidth/drawLines);

  context.beginPath();
   for(var i=0;i<=drawLines;i++){
      var audioBuffKey = Math.floor(eachBlock * i);
       var x = i*lineGap;
       var y = leftChannel[audioBuffKey] * canvasHeight / 2;
       context.moveTo( x, y );
       context.lineTo( x, (y*-1) );
   }
   context.stroke();
   context.restore();
}

function createCanvas ( w, h ) {
    var newCanvas = document.createElement('canvas');
    newCanvas.width  = w;     newCanvas.height = h;
    return newCanvas;
};


loadMusic('could_be_better.mp3');

это немного устарело, извините за удар, но это единственный пост об отображении полной формы волны с помощью Web Audio Api, и я хотел бы рассказать, какой метод я использовал.

Этот метод не идеален, но он пропускает только отображаемый звук и только один раз. ему также удается отобразить фактическую форму волны для коротких файлов или большого увеличения:

и убедительная диаграмма громкости для файлов с уменьшенным масштабом:

вот как это выглядит при среднем увеличении, тоже вроде приятно:

обратите внимание, что оба увеличения используют один и тот же алгоритм. Я все еще борюсь с масштабами (увеличенная форма волны больше, чем уменьшенная (хотя и не намного больше, чем отображается на изображениях)

Этот алгоритм, который я нахожу, довольно эффективен (я могу изменить масштаб на 4 минуты музыки, и он безупречно перерисовывается каждые 0,1 секунды)

      function drawWaveform (audioBuffer, canvas, pos = 0.5, zoom = 1) {
  const canvasCtx = canvas.getContext("2d")
  const width = canvas.clientWidth
  const height = canvas.clientHeight
  canvasCtx.clearRect(0, 0, width, height)
  canvasCtx.fillStyle  = "rgb(255, 0, 0)"

  // calculate displayed part of audio 
  // and slice audio buffer to only process that part
  const bufferLength = audioBuffer.length
  const zoomLength = bufferLength / zoom
  const start = Math.max(0, bufferLength * pos - zoomLength / 2)
  const end = Math.min(bufferLength, start + zoomLength)
  const rawAudioData = audioBuffer.getChannelData(0).slice(start, end)

  // process chunks corresponding to 1 pixel width
  const chunkSize = Math.max(1, Math.floor(rawAudioData.length / width))
  const values = []
  for (let x = 0; x < width; x++) {
    const start = x*chunkSize
    const end = start + chunkSize
    const chunk = rawAudioData.slice(start, end)
    // calculate the total positive and negative area
    let positive = 0
    let negative = 0
    chunk.forEach(val => 
      val > 0 && (positive += val) || val < 0 && (negative += val)
    )
    // make it mean (this part makes dezommed audio smaller, needs improvement)
    negative /= chunk.length
    positive /= chunk.length
    // calculate amplitude of the wave
    chunkAmp = -(negative - positive)
    // draw the bar corresponding to this pixel
    canvasCtx.fillRect(
      x,
      height / 2 - positive * height,
      1,
      Math.max(1, chunkAmp * height)
    )
  }
}

Чтобы использовать это:

      async function decodeAndDisplayAudio (audioData) { 
  const source = audioCtx.createBufferSource()

  source.buffer = await audioCtx.decodeAudioData(audioData)

  drawWaveform(source.buffer, canvas, 0.5, 1) 
  // change position (0//start -> 0.5//middle -> 1//end) 
  // and zoom (0.5//full -> 400//zoomed) as you wish
}

// audioData comes raw from the file (server send it in my case)
decodeAndDisplayAudio(audioData)

Другие вопросы по тегам