0

我正在尝试用 Java 编写节奏游戏。我正在播放带有 a 的音频文件,javax.sound.sampled.Clip我想知道我所在的确切位置。

根据我的研究,节奏游戏的一个好习惯是使用音乐播放中的位置而不是系统时间来跟踪时间(以避免音频和游戏之间的转换)。

System.nanoTime()我已经编写了一个用于保持一致帧速率的主循环。但是当我尝试获取剪辑的时间时,我有几帧返回相同的时间。这是我的代码的摘录:

while (running) {
    // Get a consistent frame rate
    now = System.nanoTime();
    delta += (now - lastTime) / timePerTick;
    timer += now - lastTime;
    lastTime = now;
    if (delta >= 1) {
        if(!paused) {
            tick((long) Math.floor(delta * timePerTick / 1000000));
        }
        display.render();
        ticks++;
        delta--;
    }
    // Show FPS ...
}

// ...

private void tick(long frameTime) {
    // Doing some stuff ...
    System.out.println(clip.getMicrosecondPosition());
}

我期待一个一致的输出(120 fps 的每帧增量约为 8,333 µs),但我在几帧中得到了相同的时间。

控制台返回:

0
0
748
748
748
10748
10748
10748
20748
20748
20748
30748

我怎样才能始终如一地准确地跟踪我在歌曲中的位置?javax Clip 有什么替代品吗?

4

2 回答 2

1

有许多困难需要处理。首先,Java 不提供实时保证,因此处理部分声音数据的时间不一定对应于听到它的时间。其次,操作系统系统时钟缺乏粒度很容易搞砸,我猜这是导致您看到多次出现时间的原因。几年前,我们在 java-gaming.org 上对此进行了多次讨论。[编辑:现在我认为您的检查循环速度比剪辑使用的缓冲区大小的粒度更快。]

我知道处理获得真正精确时间的唯一方法是Clip完全放弃使用,而是在输出时计算帧数SourceDataLine。我在这方面取得了很好的成功。它是输出到本机声音渲染代码之前的最后一点,因此具有最佳时机。

也就是说,有可能以固定间隔(对应于四分音符或十六分音符)实现某种脉冲(例如,使用 util.Timer)并让 TimerTask 启动Clip对象的播放。我自己没有尝试过,但这对于节奏游戏来说已经足够了。为此,您将使用Clipobjects 而不是 SourceDataLine. 但请确保每个Clip都已满载并在播放时准备就绪。不要犯为每次播放重新加载它们的常见错误。

但是当我最后一次尝试做这样的事情时,使用一个常见的游戏循环,结果并不那么热。

JavaFX 非常适合游戏编写!我发现图形比 AWT 更容易处理。我写了一个开始使用 JavaFX 进行游戏编程的教程。如果您使用的是 JavaFX 11,就使用 Eclipse 进行设置而言,它已经过时了。但如果您使用的是 Java 8,我认为它仍然涵盖了基础知识。

在 JGO,人们处于几个不同的阵营。许多人喜欢使用 Libgdx,一个库,我们中的一些人继续使用 JavaFX 或 AWT/Swing。还有另外几个图书馆也受到不同成员的支持。

JavaFX有自己的声音输出,但我没有玩过,不知道它是否比Clip更好的节奏精度。提示都很好,但是鉴于 Java 的线程之间不断切换的系统,从提示中获取可用的时间信息总是很困难。

只是在审查JGO上的内容。您可能会发现这个线程很有趣,OP 试图在其中制作节拍器。 http://www.java-gaming.org/topics/java-audio-metronome-timing-and-speed-problems/33591/view.html

[编辑:PS,我写了一个可靠的节拍器,并且有一个足够可靠的事件回放系统,可以以无缝的节奏将音乐事件串在一起,并且听众可以实时跟踪正在播放的音符的音量包络等内容. 最终应该可以做你想做的事。]

于 2019-05-02T19:22:59.273 回答
0

问题clip.getMicrosecondPosition()在于它只能告诉您已发送到本机音频系统或对应于正在呈现的缓冲区的音频的位置。

这意味着什么?

播放音频时,您通常会在内存中(如 a 中)有一堆样本Clip发送到本机音频系统进行播放。现在,单独发送每个样品效率非常低,这就是为什么您总是批量发送样品的原因。就javax.sound.sampled术语而言,您正在使用其write(buf)方法将它们写入SourceDataLine 。现在,当您写入数据时,它不会直接发送到扬声器,而是写入缓冲区。然后,本机系统从该缓冲区中获取样本并将它们发送给扬声器。系统可以再次选择从该缓冲区批量采集样本。

因此,当您询问剪辑的微秒位置时,它可能不会真正为您提供实际渲染的位置(在扬声器上播放),而是从内存剪辑写入行缓冲区或系统从线路的内部缓冲区中获取了什么。

所以分辨率clip.getMicrosecondPosition()可能取决于缓冲区大小。

要影响使用的缓冲区大小,您可能想在创建剪辑时尝试这样的事情:

int bufferSize = 1024;
AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
    AudioSystem.NOT_SPECIFIED, 16, 2, 4, AudioSystem.NOT_SPECIFIED, true);
DataLine.Info info = new DataLine.Info(Clip.class, format, bufferSize);
Clip clip = (Clip) AudioSystem.getLine(info)

这要求音频系统在播放剪辑时使用仅 1024 字节的缓冲区。使用每个样本 16 位、每帧 2 个通道和 44100 Hz 的采样率的格式,这将对应于 256 帧(4 字节/帧),即 256/44100=0.0058 秒,一个非常短的缓冲区。

对于高分辨率(低延迟音频),最好使用短缓冲区。但是,当缓冲区太短时,您可能会遇到缓冲区下溢,这意味着您的音频将“挂起”或“卡顿”。这对用户来说非常烦人。请记住,Java 使用垃圾收集。因此,当 Java 忙于丢弃垃圾(缺乏实时功能)时,您可能根本无法将音频样本连续写入系统。

希望这可以帮助。祝你好运!

于 2019-05-03T07:38:00.923 回答