1

使用 Java 进入复杂的音频世界我正在使用这个库,它基本上是我改进并发布在 Github 上的。

该库的主要类是StreamPlayer,代码有​​注释,易于理解。


问题是它支持许多功能,除了速度增加/减少音频速度。比方说,当您更改视频速度时,就像 YouTube 所做的那样。

我不知道如何实现这样的功能。我的意思是,将音频写入采样率时我该怎么办targetFormat?我每次都必须一次又一次地重新启动音频....

AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate()*2, nSampleSizeInBits, sourceFormat.getChannels(),
                nSampleSizeInBits / 8 * sourceFormat.getChannels(), sourceFormat.getSampleRate(), false);

播放音频的代码是:

/**
 * Main loop.
 *
 * Player Status == STOPPED || SEEKING = End of Thread + Freeing Audio Resources.<br>
 * Player Status == PLAYING = Audio stream data sent to Audio line.<br>
 * Player Status == PAUSED = Waiting for another status.
 */
@Override
public Void call() {
    //  int readBytes = 1
    //  byte[] abData = new byte[EXTERNAL_BUFFER_SIZE]
    int nBytesRead = 0;
    int audioDataLength = EXTERNAL_BUFFER_SIZE;
    ByteBuffer audioDataBuffer = ByteBuffer.allocate(audioDataLength);
    audioDataBuffer.order(ByteOrder.LITTLE_ENDIAN);

    // Lock stream while playing.
    synchronized (audioLock) {
        // Main play/pause loop.
        while ( ( nBytesRead != -1 ) && status != Status.STOPPED && status != Status.SEEKING && status != Status.NOT_SPECIFIED) {
            try {
                //Playing?
                if (status == Status.PLAYING) {

                    // System.out.println("Inside Stream Player Run method")
                    int toRead = audioDataLength;
                    int totalRead = 0;

                    // Reads up a specified maximum number of bytes from audio stream   
                    //wtf i have written here xaxaxoaxoao omg //to fix! cause it is complicated
                    for (; toRead > 0
                            && ( nBytesRead = audioInputStream.read(audioDataBuffer.array(), totalRead, toRead) ) != -1; toRead -= nBytesRead, totalRead += nBytesRead)

                        // Check for under run
                        if (sourceDataLine.available() >= sourceDataLine.getBufferSize())
                            logger.info(() -> "Underrun> Available=" + sourceDataLine.available() + " , SourceDataLineBuffer=" + sourceDataLine.getBufferSize());

                    //Check if anything has been read
                    if (totalRead > 0) {
                        trimBuffer = audioDataBuffer.array();
                        if (totalRead < trimBuffer.length) {
                            trimBuffer = new byte[totalRead];
                            //Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array
                            // The number of components copied is equal to the length argument. 
                            System.arraycopy(audioDataBuffer.array(), 0, trimBuffer, 0, totalRead);
                        }

                        //Writes audio data to the mixer via this source data line
                        sourceDataLine.write(trimBuffer, 0, totalRead);

                        // Compute position in bytes in encoded stream.
                        int nEncodedBytes = getEncodedStreamPosition();

                        // Notify all registered Listeners
                        listeners.forEach(listener -> {
                            if (audioInputStream instanceof PropertiesContainer) {
                                // Pass audio parameters such as instant
                                // bit rate, ...
                                listener.progress(nEncodedBytes, sourceDataLine.getMicrosecondPosition(), trimBuffer, ( (PropertiesContainer) audioInputStream ).properties());
                            } else
                                // Pass audio parameters
                                listener.progress(nEncodedBytes, sourceDataLine.getMicrosecondPosition(), trimBuffer, emptyMap);
                        });

                    }

                } else if (status == Status.PAUSED) {

                    //Flush and stop the source data line 
                    if (sourceDataLine != null && sourceDataLine.isRunning()) {
                        sourceDataLine.flush();
                        sourceDataLine.stop();
                    }
                    try {
                        while (status == Status.PAUSED) {
                            Thread.sleep(50);
                        }
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                        logger.warning("Thread cannot sleep.\n" + ex);
                    }
                }
            } catch (IOException ex) {
                logger.log(Level.WARNING, "\"Decoder Exception: \" ", ex);
                status = Status.STOPPED;
                generateEvent(Status.STOPPED, getEncodedStreamPosition(), null);
            }
        }

        // Free audio resources.
        if (sourceDataLine != null) {
            sourceDataLine.drain();
            sourceDataLine.stop();
            sourceDataLine.close();
            sourceDataLine = null;
        }

        // Close stream.
        closeStream();

        // Notification of "End Of Media"
        if (nBytesRead == -1)
            generateEvent(Status.EOM, AudioSystem.NOT_SPECIFIED, null);

    }
    //Generate Event
    status = Status.STOPPED;
    generateEvent(Status.STOPPED, AudioSystem.NOT_SPECIFIED, null);

    //Log
    logger.info("Decoding thread completed");

    return null;
}

如果需要,请随意下载并单独查看库。:) 我需要一些帮助...图书馆链接

4

2 回答 2

5

简短的回答:

为了加快一个人说话的速度,请使用我的 Sonic 算法的Sonic.java本机 Java 实现。如何使用它的示例在Main.Java中。Android 的 AudioTrack 使用相同算法的 C 语言版本。要加快音乐或电影的速度,请查找基于 WSOLA 的库。

臃肿的答案:

加快语音比听起来更复杂。简单地增加采样率而不调整样本会导致扬声器听起来像花栗鼠。我听过的基本上有两种线性加速语音的好方案:像 WSOLA 这样的基于固定帧的方案,以及像 PICOLA 这样的音高同步方案,Sonic 使用它来实现高达 2 倍的速度。我听过的另一种方案是基于 FFT 的,IMO 应该避免这些实现。我听说基于 FFT 可以做得很好,但我知道的开源版本在我上次检查时没有可用的,可能是在 2014 年。

我必须发明一种新的算法来实现大于 2X 的速度,因为 PICOLA 只是降低了整个音高周期,只要您不连续丢弃两个音高周期,它就可以很好地工作。对于超过 2X 的速度,Sonic 会混合来自每个输入音高周期的部分样本,同时保留每个输入音高周期的一些频率信息。这适用于大多数语音,尽管某些语言(如匈牙利语)的词性似乎很短,以至于 PICOLA 也会破坏一些音素。然而,你可以在不破坏音素的情况下删除一个音高周期的一般规则似乎在大多数情况下都很好用。

音高同步方案专注于一个扬声器,并且通常会使该扬声器比固定框架方案更清晰,但以牺牲非语音为代价。然而,对于大多数扬声器来说,在低于约 1.5X 的速度下很难听到音高同步方案相对于固定帧方案的改进。这是因为像 WSOLA 这样的固定帧算法基本上模拟像 PICOLA 这样的音高同步方案,当只有一个扬声器并且每帧不超过一个音高周期需要丢弃时。如果 WSOLA 很好地适应了扬声器,那么在这种情况下,数学原理基本相同。例如,如果它能够及时选择 +/- 一帧的声音片段,那么 50 毫秒的固定帧将允许 WSOLA 模拟 PICOLA 对大多数基本音高 > 100 Hz 的扬声器。然而,使用这些设置,WSOLA 会杀死一个具有 95 Hz 深沉嗓音的男性。此外,当参数未进行最佳调整时,WSOLA 也可能会扼杀部分语音,例如在句子结尾处,我们的基本音高会显着下降。此外,当速度超过 2X 时,WSOLA 通常会分崩离析,就像 PICOLA 一样,它开始连续下降多个音高周期。

从积极的方面来说,WSOLA 将使包括音乐在内的大多数声音即使不是高保真度也可以理解。使用 WSOLA 和 PICOLA 等重叠加法 (OLA) 方案,在不引入大量失真的情况下获取非谐波多声部声音并改变速度是不可能的。做好这件事需要分离不同的声音,独立改变它们的速度,并将结果混合在一起。但是,大多数音乐都足够和谐,可以使用 WSOLA 听起来不错。

事实证明,2X 以上的 WSOLA 质量差是人们很少以高于 2X 的速度收听的原因之一。人们根本不喜欢它。一旦 Audible.com 在 Android 上从 WSOLA 切换到类似 Sonic 的算法,他们就能够将支持的速度范围从 2X 增加到 3X。过去几年我没有在 iOS 上听过,但截至 2014 年,iOS 上的 Audible.com 以 3X 的速度收听很痛苦,因为他们使用了内置的 iOS WSOLA 库。从那时起,他们可能已经修复了它。

于 2018-07-06T13:43:52.810 回答
2

查看您链接的库,这似乎不是专门针对此播放速度问题的好地方;您有什么理由不使用AudioTrack吗?它似乎支持你需要的一切。

编辑 1: AudioTrack 是特定于 Android 的,但 OP 的问题是基于桌面 javaSE 的;我只会把它留在这里以供将来参考。

1.使用AudioTrack和调整播放速度(Android)

感谢另一个 SO 帖子(此处)的答案,发布了一个类,它使用内置的AudioTrack来处理播放期间的速度调整。

public class AudioActivity extends Activity {
AudioTrack audio = new AudioTrack(AudioManager.STREAM_MUSIC,
        44100,
        AudioFormat.CHANNEL_OUT_STEREO,
        AudioFormat.ENCODING_PCM_16BIT,
        intSize, //size of pcm file to read in bytes
        AudioTrack.MODE_STATIC);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //read track from file
        File file = new File(getFilesDir(), fileName);

        int size = (int) file.length();
        byte[] data = new byte[size];

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            fileInputStream.read(data, 0, size);
            fileInputStream.close();

            audio.write(data, 0, data.length);
        } catch (IOException e) {}
    }

    //change playback speed by factor
    void changeSpeed(double factor) {
        audio.setPlaybackRate((int) (audio.getPlaybackRate() * factor));
    }
}

这只是使用一个文件在一个写入命令中流式传输整个文件,但您可以以其他方式对其进行调整(setPlayBackRate方法是您需要的主要部分)。

2.应用您自己的播放速度调整

调整播放速度的原理有两种:

  • 调整采样率
  • 改变单位时间的样本数

由于您使用的是初始采样率(因为我假设您必须在调整采样率时重置库并停止音频?),您将不得不调整每单位时间的样本数。

例如,为了加快音频缓冲区的播放速度,您可以使用这个伪代码(Python 风格),感谢Coobird此处)。

original_samples = [0, 0.1, 0.2, 0.3, 0.4, 0.5]

def faster(samples):
    new_samples = []
    for i = 0 to samples.length:
        if i is even:
            new_samples.add(0.5 * (samples[i] + samples[i+1]))
    return new_samples

faster_samples = faster(original_samples)

这只是加快播放速度的一个例子,并不是唯一的算法,而是一个开始的算法。计算出加速缓冲区后,您可以将其写入音频输出,数据将以您选择应用的任何缩放比例播放。

要减慢音频速度,请通过在当前缓冲区值之间添加数据点并根据需要进行插值来应用相反的方法。

请注意,在调整播放速度时,通常值得在所需的最大频率处进行低通滤波,以避免不必要的伪影。


如您所见,第二次尝试是一项更具挑战性的任务,因为它需要您自己实现此类功能,所以我可能会使用第一次,但认为值得一提的是第二次。

于 2018-07-03T08:06:24.110 回答