【发布时间】:2019-08-28 20:08:35
【问题描述】:
我正在尝试创建一个 Java 应用程序,它能够播放音频、录制用户声音并判断用户是否在正确的时间唱歌。
目前,我只专注于录制和播放音频(曲调识别超出范围)。
为此,我使用了 Java 音频 API 中的 TargetDataLine 和 SourceDataLine。首先,我开始录音,然后启动音频播放。由于我想确保用户在正确的时间唱歌,所以我需要在录制的音频和播放的音频之间保持同步。
例如,如果音频在录音后 1 秒开始播放,我知道我会忽略记录缓冲区中的第一秒数据。
我使用以下代码进行测试(代码远非完美,但仅用于测试目的)。
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
class AudioSynchro {
private TargetDataLine targetDataLine;
private SourceDataLine sourceDataLine;
private AudioInputStream ais;
private AudioFormat recordAudioFormat;
private AudioFormat playAudioFormat;
public AudioSynchro(String sourceFile) throws IOException, UnsupportedAudioFileException {
ais = AudioSystem.getAudioInputStream(new File(sourceFile));
recordAudioFormat = new AudioFormat(44100f, 16, 1, true, false);
playAudioFormat = ais.getFormat();
}
//Enumerate the mixers
public void enumerate() {
try {
Mixer.Info[] mixerInfo =
AudioSystem.getMixerInfo();
System.out.println("Available mixers:");
for(int cnt = 0; cnt < mixerInfo.length;
cnt++){
System.out.println(mixerInfo[cnt].
getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
//Init datalines
public void initDataLines() throws LineUnavailableException {
Mixer.Info[] mixerInfo =
AudioSystem.getMixerInfo();
DataLine.Info targetDataLineInfo = new DataLine.Info(TargetDataLine.class, recordAudioFormat);
Mixer targetMixer = AudioSystem.getMixer(mixerInfo[5]);
targetDataLine = (TargetDataLine)targetMixer.getLine(targetDataLineInfo);
DataLine.Info sourceDataLineInfo = new DataLine.Info(SourceDataLine.class, playAudioFormat);
Mixer sourceMixer = AudioSystem.getMixer(mixerInfo[3]);
sourceDataLine = (SourceDataLine)sourceMixer.getLine(sourceDataLineInfo);
}
public void startRecord() throws LineUnavailableException {
AudioInputStream stream = new AudioInputStream(targetDataLine);
targetDataLine.open(recordAudioFormat);
byte currentByteBuffer[] = new byte[512];
Runnable readAudioStream = new Runnable() {
@Override
public void run() {
int count = 0;
try {
targetDataLine.start();
while ((count = stream.read(currentByteBuffer)) != -1) {
//Do something
}
}
catch(Exception e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(readAudioStream);
thread.start();
}
public void startPlay() throws LineUnavailableException {
sourceDataLine.open(playAudioFormat);
sourceDataLine.start();
Runnable playAudio = new Runnable() {
@Override
public void run() {
try {
int nBytesRead = 0;
byte[] abData = new byte[8192];
while (nBytesRead != -1) {
nBytesRead = ais.read(abData, 0, abData.length);
if (nBytesRead >= 0) {
int nBytesWritten = sourceDataLine.write(abData, 0, nBytesRead);
}
}
sourceDataLine.drain();
sourceDataLine.close();
}
catch(Exception e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(playAudio);
thread.start();
}
public void printStats() {
Runnable stats = new Runnable() {
@Override
public void run() {
while(true) {
long targetDataLinePosition = targetDataLine.getMicrosecondPosition();
long sourceDataLinePosition = sourceDataLine.getMicrosecondPosition();
long delay = targetDataLinePosition - sourceDataLinePosition;
System.out.println(targetDataLinePosition+"\t"+sourceDataLinePosition+"\t"+delay);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(stats);
thread.start();
}
public static void main(String[] args) {
try {
AudioSynchro audio = new AudioSynchro("C:\\dev\\intellij-ws\\guitar-challenge\\src\\main\\resources\\com\\ouestdev\\guitarchallenge\\al_adagi.mid");
audio.enumerate();
audio.initDataLines();
audio.startRecord();
audio.startPlay();
audio.printStats();
} catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
e.printStackTrace();
}
}
}
代码初始化 2 条数据线,开始录音,开始播放音频并显示统计信息。 enumerate() 方法用于显示系统上可用的混合器。您必须根据您的系统更改 initDataLines() 方法中使用的混合器来进行自己的测试。 printStats 方法()启动一个线程,询问 2 个数据线的位置(以微秒为单位)。这是我尝试用来跟踪同步的数据。我观察到的是 2 条数据线并非一直保持同步。这是我的输出控制台的简短摘录:
130000 0 130000
150000 748 149252
170000 20748 149252
190000 40748 149252
210000 60748 149252
230000 80748 149252
250000 100748 149252
270000 120748 149252
290000 140748 149252
310000 160748 149252
330000 180748 149252
350000 190748 159252
370000 210748 159252
390000 240748 149252
410000 260748 149252
430000 280748 149252
450000 300748 149252
470000 310748 159252
490000 340748 149252
510000 350748 159252
530000 370748 159252
正如我们所见,延迟可能会定期变化 10 毫秒,因此我无法准确判断录制缓冲区中的哪个位置与播放缓冲区的开头匹配。特别是在前面的例子中,我不知道我应该从位置 149252 还是 159252 开始。 在音频处理方面,10 毫秒很重要,我想要更准确的东西(1 或 2 毫秒是可以接受的)。 而且,当两个度量之间存在差异时,仍然存在 10 毫秒的差距,这听起来很奇怪。
然后我尝试进一步推动我的测试,但我没有得到更好的结果: - 尝试使用更大或更小的缓冲区 - 为播放尝试了两倍大的缓冲区。由于音频文件是立体声的,因此消耗了更多字节(2 字节/帧用于录制,4 字节/帧用于播放) - 尝试在同一音频设备上录制和播放
在我看来,同步 2 个缓冲区有两种策略: - 我尝试做什么。精确确定播放开始的记录缓冲区中的位置。 - 同步开始录制和播放。
在这两种策略中,我需要保证保持同步。
你们中有人遇到过这类问题吗?
目前,我将 Java 12 和 JavaFx 用于我的应用程序,但我已准备好使用另一个框架。我没有尝试过,但使用框架 lwjgl(https://www.lwjgl.org/ 基于 OpenAl)或珠子(http://www.beadsproject.net/)可能会获得更好的结果和更多的控制。如果你们中有人知道他的框架并且可以给我一个回报,我很感兴趣。
最后,最后一个可接受的解决方案是更改编程语言。
【问题讨论】:
标签: java audio synchronization audio-streaming audio-recording