Files
ace-step-ui/server/audio-editor/recorder.js
T

223 lines
6.2 KiB
JavaScript
Executable File

(function ( w, d, PKAE ) {
'use strict';
var PKREC = function ( app ) {
var q = this;
var media_stream_source = null;
var audio_stream = null;
var audio_context = null;
var script_processor = null;
var buffer_size = 2048 * 2; // * 2 ?
var channel_num = 1;
var channel_num_out = 1;
var is_active = false;
var starting_offset = 0;
var ending_offset = 0;
var sample_rate = 0;
var temp_buffers = [];
var temp_buffer_index = -1;
var jumps = 1;
var end_record_func = null;
var start_record_func = null;
// temp vars
var curr_offset = 0;
var first_skip = 8; // skip first samples to evade the button's click
var fetchBufferFunction = function( ev ) {
if (first_skip > 0) {
--first_skip;
return ;
}
curr_offset += ev.inputBuffer.duration * sample_rate;
if (ending_offset <= curr_offset)
{
ending_offset > 0 && q.stop ();
return ;
}
var float_array = ev.inputBuffer.getChannelData (0).slice (0);
temp_buffers[ ++temp_buffer_index ] = float_array;
if (--jumps === 0)
{
requestAnimationFrame(function(){
jumps = 4;
app.engine.wavesurfer.DrawTemp ( starting_offset, temp_buffers );
});
}
};
this.isActive = function () {
return (is_active);
};
this.setEndingOffset = function ( ending_offset_seconds ) {
ending_offset = ending_offset_seconds; // #### * 100
};
this.start = function ( _at_offset, _end_callback, _start_callback, _sample_rate ) {
if (is_active) return (false);
if (!navigator.mediaDevices)
{
app.fireEvent ('ErrorRec');
app.fireEvent ('ShowError', 'No recording device found');
return (false);
}
starting_offset = _at_offset / 1;
if (isNaN (starting_offset) || !starting_offset) starting_offset = 0;
curr_offset = starting_offset;
audio_context = app.engine.wavesurfer.backend.getAudioContext ();
if (!audio_context)
{
app.fireEvent ('ErrorRec');
app.fireEvent ('ShowError', 'No recording device found');
return (false);
}
if (audio_context.currentTime === 0) {
app.engine.wavesurfer.backend.source.start (0);
app.engine.wavesurfer.backend.source.stop (0);
}
if (!_sample_rate)
{
if (app.engine.wavesurfer.backend.buffer) {
sample_rate = app.engine.wavesurfer.backend.buffer.sampleRate;
}
else {
sample_rate = audio_context.sampleRate;
}
}
end_record_func = function (offset, buffers, _callback) {
async function downsampleAudioBuffer(buffers, sourceSampleRate, targetSampleRate) {
// Step 1: Concatenate the Float32Array chunks
const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
const concatenated = new Float32Array(totalLength);
let offset = 0;
for (let i = 0; i < buffers.length; i++) {
concatenated.set(buffers[i], offset);
offset += buffers[i].length;
}
// Step 2: Create an AudioBuffer from the concatenated data at the source sample rate
// Create a temporary AudioContext to build the AudioBuffer
const tempCtx = new AudioContext({ sampleRate: sourceSampleRate });
const audioBuffer = tempCtx.createBuffer(1, totalLength, sourceSampleRate);
audioBuffer.copyToChannel(concatenated, 0, 0);
// Release the temporary context if you don't need it anymore
tempCtx.close();
// Step 3: Use an OfflineAudioContext to resample the AudioBuffer to the target sample rate
const duration = audioBuffer.duration;
const newLength = Math.ceil(duration * targetSampleRate);
const offlineCtx = new OfflineAudioContext(1, newLength, targetSampleRate);
const source = offlineCtx.createBufferSource();
source.buffer = audioBuffer;
source.connect(offlineCtx.destination);
source.start(0);
// Render the resampled AudioBuffer
const renderedBuffer = await offlineCtx.startRendering();
// Return the downsampled Float32Array
return renderedBuffer.getChannelData(0);
}
var source_sample_rate = audio_context ? audio_context.sampleRate : 48000;
if (source_sample_rate === sample_rate) {
_callback ();
_end_callback (offset, buffers);
return ;
}
downsampleAudioBuffer(buffers, source_sample_rate, sample_rate).then(newBuffer => {
_callback ();
_end_callback (offset, [newBuffer]);
}).catch(error => {
_callback ();
console.error("Error during downsampling:", error);
});
// _end_callback (offset, buffers);
};
start_record_func = _start_callback;
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(function( stream ) {
audio_stream = stream;
media_stream_source = audio_context.createMediaStreamSource ( stream );
script_processor = audio_context.createScriptProcessor (
buffer_size, channel_num, channel_num_out
);
media_stream_source.connect ( script_processor );
script_processor.connect ( audio_context.destination );
is_active = true;
start_record_func && start_record_func ();
script_processor.onaudioprocess = fetchBufferFunction;
}).catch(function(error) {
app.fireEvent ('ErrorRec');
if (error && error.message)
{
app.fireEvent ('ShowError', error.message);
}
});
return (true);
};
this.stop = function ( cancel_recording ) {
if (!is_active) return ;
// fire one last callback to clean temp_buffers?
audio_stream.getTracks().forEach(function (stream) {
stream.stop ();
});
script_processor.onaudioprocess = null;
media_stream_source.disconnect ();
script_processor.disconnect ();
app.engine.wavesurfer.DrawTemp ( null );
if (temp_buffers.length > 0 && !cancel_recording)
end_record_func && end_record_func ( starting_offset / sample_rate, temp_buffers, function (){
is_active = false;
});
else
end_record_func && end_record_func ( null, null, function(){
is_active = false;
});
sample_rate = 0;
first_skip = 8;
jumps = 1;
temp_buffer_index = -1;
starting_offset = ending_offset = 0;
temp_buffers = [];
audio_stream = null; audio_context = null;
end_record_func = start_record_func = null;
};
// ---
};
PKAE._deps.rec = PKREC;
})( window, document, PKAudioEditor );