Подтвердить что ты не робот

Переписывание 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 имеет к этому какое-то отношение, и ошибка не дает мне понять, что попробовать. Любые идеи?

4b9b3361

Ответ 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])" />