使用Java提高/降低AudioInputStream的音频播放速度

时间:2018-07-02 02:06:08

标签: java audio audioinputstream

  

使用Java进入复杂的音频世界,我正在使用this library,基本上我已经对其进行了改进并发布在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;
}

如果需要,可以免费下载并单独检查库。 :)对此我需要一些帮助... Library link

2 个答案:

答案 0 :(得分:3)

简短回答:

为加快单个人的讲话速度,请使用我的Sonic算法的Sonic.java本机Java实现。 Main.Java中提供了一个使用方法的示例。 Android的AudioTrack使用相同算法的C语言版本。为了加快音乐或电影的速度,请找到基于WSOLA的库。

膨胀的答案:

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

我不得不发明一种新的算法来处理大于2倍的速度,因为PICOLA只是丢弃了整个音调周期,只要您不连续丢两个音调周期,它就可以很好地工作。为了快于2倍,Sonic会在每个输入音调周期中混入一部分样本,同时保留每个采样点的一些频率信息。尽管大多数语言(例如匈牙利语)的部分语音似乎都太短了,甚至PICOLA都会弄乱某些音素,但这对大多数语音来说效果很好。但是,通常的规则是您可以在不改变音素的情况下放弃一个音调周期,在大多数情况下似乎效果很好。

音高同步方案专注于一个扬声器,并且通常使该扬声器比固定帧方案更清晰,但会牺牲非语音声音。但是,对于大多数扬声器来说,音调同步方案相对于固定帧方案的改进很难以小于1.5倍的速度听到。这是因为,当只有一个扬声器且每帧不需要删除一个以上音高周期时,像WSOLA这样的固定帧算法基本上会模拟像PICOLA这样的音高同步方案。如果WSOLA很好地适合说话者,则在这种情况下,数学运算基本相同。例如,如果它能够及时选择+/-一帧的声音片段,则50ms的固定帧将允许WSOLA为基本音高> 100 Hz的大多数扬声器模拟PICOLA。但是,使用这些设置,WSOLA将屠杀声音较深的男性,比如说95 Hz。同样,当参数没有得到最佳调整时,WSOLA也会破坏我们基本音调大幅下降的词性,例如句子结尾。此外,WSOLA通常会以大于2倍的速度崩溃,就像PICOLA一样,它开始连续下降多个音调周期。

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

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

答案 1 :(得分:2)

看一下您链接的库,似乎不是一个专门针对此播放速度问题的好地方;您没有使用AudioTrack的原因是什么?它似乎可以满足您的所有需求。

编辑1: AudioTrack是Android专用的,但是OP的问题是基于Desktop javaSE的;我只会把它留在这里以备将来参考。

1。使用AudioTrack并调整播放速度(Android)

感谢对另一个SO帖子(here)的回答,其中发布了一个类,该类使用内置的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风格),这要归功于Coobirdhere)。

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)

这只是加快播放速度的一个示例,不是唯一的实现方法,而是一个开始的算法。计算完加速缓冲区后,您可以将其写入音频输出,然后数据将以您选择应用的任意比例进行播放。

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

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


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