Требуется AudioBuffer .
. Вы можете копировать необработанные данные PCM в его каналы непосредственно из TypedArray.
Вы можете указать его sampleRate
, и AudioContext примет заботиться о повторной выборке, чтобы соответствовать настройкам звуковой карты.
Однако будьте осторожны, 2048 байт на чанк означает, что каждый чанк будет представлять только 5 мс аудиоданных. * Вы, вероятно, захотите увеличить это и реализовать некоторую стратегию буферизации.
Вот небольшая демонстрация в качестве доказательства концепции хранения данных чанков в буфер Float32Array.
const min_sample_duration = 2; // sec
const sample_rate = 88200; // Hz
// how much data is needed to play for at least min_sample_duration
const min_sample_size = min_sample_duration * sample_rate;
const fetching_interval = 100; // ms
// you'll probably want this much bigger
let chunk_size = 2048; // bytes
const log = document.getElementById( 'log' );
const btn = document.getElementById( 'btn' );
btn.onclick = e => {
let stopped = false;
let is_reading = false;
const ctx = new AudioContext();
// to control output volume
const gain = ctx.createGain();
gain.gain.value = 0.01;
gain.connect( ctx.destination );
// this will get updated at every new fetch
let fetched_data = new Float32Array( 0 );
// keep it accessible so we can stop() it
let active_node;
// let's begin
periodicFetch();
// UI update
btn.textContent = "stop";
btn.onclick = e => {
stopped = true;
if( active_node ) { active_node.stop(0); }
};
oninput = handleUIEvents;
// our fake fetcher, calls itself every 50ms
function periodicFetch() {
// data from server (here just some noise)
const noise = Float32Array.from( { length: chunk_size / 4 }, _ => (Math.random() * 1) - 0.5 );
// we concatenate the data just fetched with what we have already buffered
fetched_data = concatFloat32Arrays( fetched_data, noise );
// for demo only
log.textContent = "buffering: " + fetched_data.length + '/ ' + min_sample_size;
if( !stopped ) {
// do it again
setTimeout( periodicFetch , fetching_interval );
}
// if we are not actively reading and have fetched enough
if( !is_reading && fetched_data.length > min_sample_size ) {
readingLoop(); // start reading
}
}
function readingLoop() {
if( stopped || fetched_data.length < min_sample_size ) {
is_reading = false;
return;
}
// let the world know we are actively reading
is_reading = true;
// create a new AudioBuffer
const aud_buf = ctx.createBuffer( 1, fetched_data.length, sample_rate );
// copy our fetched data to its first channel
aud_buf.copyToChannel( fetched_data, 0 );
// clear the buffered data
fetched_data = new Float32Array( 0 );
// the actual player
active_node = ctx.createBufferSource();
active_node.buffer = aud_buf;
active_node.onended = readingLoop; // in case we buffered enough while playing
active_node.connect( gain );
active_node.start( 0 );
}
function handleUIEvents( evt ) {
const type = evt.target.name;
const value = evt.target.value;
switch( type ) {
case "chunk-size":
chunk_size = +value;
break;
case "volume":
gain.gain.value = +value;
break;
}
}
};
// helpers
function concatFloat32Arrays( arr1, arr2 ) {
if( !arr1 || !arr1.length ) {
return arr2 && arr2.slice();
}
if( !arr2 || !arr2.length ) {
return arr1 && arr1.slice();
}
const out = new Float32Array( arr1.length + arr2.length );
out.set( arr1 );
out.set( arr2, arr1.length );
return out;
}
label { display: block }
Размер каждого полученного фрагмента: