将Java代码重写为JS - 如何从字节流创建音频?

13

我正在尝试将一些(非常简单的)Android代码重写为静态HTML5应用程序(我不需要服务器来执行任何操作,我想保持这种方式)。我具有丰富的Web开发背景,但对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会有任何关系,并且错误信息也没有给我任何提示。 有任何想法吗?

更新:我尝试使用这个解决方案https://dev59.com/SWAf5IYBdhLWcg3w52Pm,但是我收到“未捕获的(在promise中)DOMException:无法解码音频数据”,而且我真的不明白字节转换为声音的基础知识。 - yuvi
我认为你可能正在寻找Web音频API [AudioBuffer](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer)。我对此没有足够的经验来提供完整的答案,但我想评论一下以防万一。 - aberkow
var buffer = new Uint8Array( bytes.length ) 应该改为 var buffer = new ArrayBuffer( bytes.length ) 吗?为什么需要使用 Uint8Arraybytes 是什么? - guest271314
2个回答

5
如果bytes是一个ArrayBuffer,则无需创建Uint8Array。您可以将ArrayBufferbytes作为参数传递给AudioContext.decodeAudioData(),它返回一个Promise,使用.then()链来调用.decodeAudioData(),并将其作为参数与play函数一起调用。
在javascript的stacksnippets中,<input type="file">元素用于接受音频文件上传,FileReader.prototype.readAsArrayBuffer()File对象创建ArrayBuffer,然后将其传递给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])" />


1
我没有加载任何音频文件,而是通过计算创建了一个字节序列。无论如何,我自己解决了这个问题。首先,当我弄清它是16位声音时,我设法产生了声音,实际上我需要使用32FloatArray(2个16位通道)。我不得不采取额外的步骤,因为声音会失真,并且在一位知识渊博的朋友的帮助下,我们共同找出了一些计算本身的问题(但这并不是实际产生声音的关键点)。 - yuvi
无论如何,我很感激你所付出的努力,因此给你的回答点赞。 - yuvi
@yuvi,你能在回答中发布你的解决方案吗?请参见http://stackoverflow.com/help/self-answer。 - guest271314
1
是的,我计划在这个周末找到时间去做。 - yuvi

3
我自己解决了。我更深入地阅读了MDN文档解释AudioBuffer,并意识到两个重要的事情:
  1. 我不需要解码音频数据(因为我自己创建了数据,没有什么需要解码的)。实际上,我从我要复制的答案中采用了这一点,回过头来看,它完全是不必要的。
  2. 由于我使用的是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创建单声道文件,或者是否必要更好。
总之,这基本上就是它。这只是最基本的解决方案,我对如何正确处理我的数据有一些最小的理解。希望这能帮助到任何人。

如何调整此代码以适应8000采样率输入? - W.M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接