Переписывание Java-кода в JS - создание аудио из байтов?
Я пытаюсь переписать некоторый (очень простой) код Android, который я нашел написанным на Java, в статическое приложение HTML5 (мне не нужен сервер, чтобы что-то делать, я хотел бы сохранить его таким образом). У меня есть обширный опыт в области веб-разработки, но базовое понимание Java и даже меньше знаний в разработке Android.
Единственная функция приложения - взять некоторые цифры и преобразовать их в звуковой чирп из байтов. У меня нет абсолютно никакой проблемы перевода математической логики в JS. Там, где у меня возникают проблемы, это когда на самом деле получается звук. Это соответствующие части исходного кода:
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
// later in the code:
AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STATIC);
// some math, and then:
track.write(sound, 0, sound.length); // sound is an array of bytes
Как это сделать в JS? Я могу использовать dataURI для создания звука из байтов, но позволяет ли я контролировать другую информацию здесь (например, частоту дискретизации и т.д.)? Другими словами: Какой самый простой, наиболее точный способ сделать это в JS?
Обновление
Я пытаюсь воспроизвести то, что я нашел в этом ответе. Это важная часть моего кода:
window.onload = init;
var context; // Audio context
var buf; // Audio buffer
function init() {
if (!window.AudioContext) {
if (!window.webkitAudioContext) {
alert("Your browser does not support any AudioContext and cannot play back this audio.");
return;
}
window.AudioContext = window.webkitAudioContext;
}
context = new AudioContext();
}
function playByteArray( bytes ) {
var buffer = new Uint8Array( bytes.length );
buffer.set( new Uint8Array(bytes), 0 );
context.decodeAudioData(buffer.buffer, play);
}
function play( audioBuffer ) {
var source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect( context.destination );
source.start(0);
}
Однако, когда я запускаю это, я получаю эту ошибку:
Uncaught (in promise) DOMException: Unable to decode audio data
Который я нахожу довольно необычным, так как это такая общая ошибка, что ему удается красиво сказать мне точно приседать о том, что не так. Еще более удивительно, когда я отлаживал это шаг за шагом, хотя цепочка ошибок начинается (предположительно) с линией context.decodeAudioData(buffer.buffer, play);
, она на самом деле пробегает еще несколько строк в файле jQuery (3.2.1, несжатый), проходя через строки 5208, 5195, 5191, 5219, 5223 и, наконец, 5015 перед ошибкой. Я не знаю, почему jQuery имеет к этому какое-то отношение, и ошибка не дает мне понять, что попробовать. Любые идеи?
Ответы
Ответ 1
Я решил это сам. Я больше читал в MDN docs, объясняющем AudioBuffer, и реализовал две важные вещи:
- Мне не нужно было decodeAudioData (поскольку я сам создаю данные, там нечего декодировать). Я на самом деле взял этот бит из ответа, который я воспроизводил, и он ретроспективы, это было совершенно бесполезно.
- Поскольку я работаю с 16-битным стереосистемой PCM, это означало, что мне нужно было использовать Float32Array (по 2 канала, каждый 16 бит).
Конечно, у меня все еще была проблема с некоторыми моими вычислениями, которые приводили к искаженному звуку, но что касается создания самого звука, я в конечном итоге сделал это очень простое решение:
function playBytes(bytes) {
var floats = new Float32Array(bytes.length);
bytes.forEach(function( sample, i ) {
floats[i] = sample / 32767;
});
var buffer = context.createBuffer(1, floats.length, 48000),
source = context.createBufferSource();
buffer.getChannelData(0).set(floats);
source.buffer = buffer;
source.connect(context.destination);
source.start(0);
}
Возможно, я оптимизирую его немного дальше - часть 32767 должна произойти до этого, в той части, где я вычисляю данные, например. Кроме того, я создаю Float32Array с двумя каналами, а затем выводя один из них, потому что мне действительно не нужны оба. Я не мог понять, есть ли способ создать одноканальный моно файл с Int16Array, или если это даже необходимо\лучше.
Во всяком случае, это по существу это. Это действительно просто самое базовое решение, с минимальным пониманием моей части того, как правильно обрабатывать мои данные. Надеюсь, это поможет кому угодно.
Ответ 2
Если bytes
является ArrayBuffer
, нет необходимости создавать Uint8Array
. Вы можете передать ArrayBuffer
bytes
как параметр в AudioContext.decodeAudioData()
, который возвращает Promise
, цепочку .then()
в .decodeAudioData()
, вызовите функцию play
в качестве параметра.
В javascript
в stacksnippets элемент <input type="file">
используется для приема загрузки аудиофайла, FileReader.prototype.readAsArrayBuffer()
создает объект ArrayBuffer
из File
, который передается в playByteArray
.
window.onload = init;
var context; // Audio context
var buf; // Audio buffer
var reader = new FileReader(); // to create `ArrayBuffer` from `File`
function init() {
if (!window.AudioContext) {
if (!window.webkitAudioContext) {
alert("Your browser does not support any AudioContext and cannot play back this audio.");
return;
}
window.AudioContext = window.webkitAudioContext;
}
context = new AudioContext();
}
function handleFile(file) {
console.log(file);
reader.onload = function() {
console.log(reader.result instanceof ArrayBuffer);
playByteArray(reader.result); // pass `ArrayBuffer` to `playByteArray`
}
reader.readAsArrayBuffer(file);
};
function playByteArray(bytes) {
context.decodeAudioData(bytes)
.then(play)
.catch(function(err) {
console.error(err);
});
}
function play(audioBuffer) {
var source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(context.destination);
source.start(0);
}
<input type="file" accepts="audio/*" onchange="handleFile(this.files[0])" />