Android 的 Javafx 音频性能(MediaPlayer 和 NativeAudioService)

Audio performance with Javafx for Android (MediaPlayer and NativeAudioService)

我已经使用 JavaFX 创建了一款运行良好的桌面游戏(20000 Java 行。作为一款游戏,实时约束很重要(玩家的响应时间动作)。

最终目的是运行这个应用跟Android。从PC到"transfer the Java code"到Android我都差不多完成了,虽然遇到了一些实时的麻烦。我想现在几乎都解决了。

例如,我已经最小化了用于检测两个移动设备之间的影响的 Shape 或 Rectangle.intersect(node1, node2) 调用的 CPU 时间(消耗)。这样,真实时间就被除以3了。太棒了!

为了测试此 Android 版本,我使用 Eclipse + Neon2、JavaFX、JavaFXports + gluon 和我的 phone (Archos Diamond S)。


但是,对于 Android phones,我遇到了一个与 MediaPlayer 和 NativeAudioSrvice 生成的声音相关的实时问题。

然而,我遵循了建议同步模式的建议: javafxports how to call android native Media Player

第一个问题:

这个媒体播放器是否存在异步模式class?我认为这可以解决这个延迟问题? 在实践中,我尝试了异步解决方案......但没有成功:由于使用 MediaPlayer 生成音频而导致的实时问题仍然存在:音频生成成本从 50 毫秒到 80 毫秒,而主循环处理每个 运行s 110 毫秒。每个音频生成都会干扰主处理执行。

并且,在每个周期性任务中(速率:110 毫秒),我可以播放多个这样的声音。并且,在一个跟踪中,最多有六个声音激活(一起)大约 300 毫秒(相对于主要循环任务的 110 毫秒)

问题:

如何提高 NativeAudio 的性能 class(特别是方法 play() 及其调用会产生实时问题:setDataSource(...)、prepare()和 start() )?


解决方案

主要处理必须是 "synchronized" 方法,以确保此完整处理将是 运行,没有任何音频中断。

此外,每个用于生成声音的完整处理都在一个专用线程下,定义了 Thread.MIN_PRIORITY 优先级。

现在,主要处理是每 110 毫秒 运行,并且当它开始时,它不会被任何音频生成干扰。显示非常"soft"(没有更多的生涩移动)。

只是有个小问题:当audio seDataSource(), start() 或prepare() 方法已经开始时,似乎下一个主要处理应该等待方法结束才能开始(待定)

我希望这个解决方案可以帮助其他人。它适用于使用 MediaPlayer 生成音频的任何情况。


JAVA解法代码

主要处理是这样定义的:

public static ***synchronized*** void mainProcessing() {
// the method handles the impacts, explosions, sounds, movings, ... , in other words almost the entiere game  .. in a CRITICAL SECTION
}

/****************************************************/

在实现"NativeAudioService"的NativeAudioclass中:

    @Override
        public void play() {
            if (bSon) {
                Task<Void> taskSound = new Task<Void>() {

                @Override
                protected Void call() throws Exception {
                generateSound(); 
                return null;
                }}; 

              Thread threadSound = new Thread(taskSound);
              threadSound.setPriority(Thread.MIN_PRIORITY);
              threadSound.start();
            } 
        }


  /****************************************************/    
  private void generateSound() {
        currentPosition = 0;
        nbTask++;
        noTask = nbTask;
        try {
            if (mediaPlayer != null) {
                stop();
            }
            mediaPlayer = new MediaPlayer();

            AssetFileDescriptor afd = FXActivity.getInstance().getAssets().openFd(audioFileName);

            mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());      

            mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);

            float floatLevel = (float) audioLevel;
            mediaPlayer.setVolume(floatLevel, floatLevel);

            mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                    if (nbCyclesAudio >= 1) {
                              mediaPlayer.start();
                          nbCyclesAudio--;
                        } else {
                        mediaPlayer.stop(); 
                        mediaPlayer.release(); // for freeing the resource - useful for the phone codec 
                        mediaPlayer = null;
                        }
                }
            });

            mediaPlayer.prepare();

            mediaPlayer.start();
            nbCyclesAudio--;

        } catch (IOException e) {
            }
      }

鉴于您要播放大量简短的音频文件,并且您希望在很短的时间内按需播放,因此我对您的实施方式做了一些更改 mentioned。基本上,我将为所有文件创建一次 AssetFileDescriptor,而且我会一直使用同一个 MediaPlayer 实例。

设计沿用了 Charm Down library 的模式,因此您需要将包名称保留在下方。

编辑

根据 OP 的反馈,我将实现更改为每个音频文件都有一个 MediaPlayer,这样您就可以随时播放其中的任何一个。

  1. 来源Packages/Java:

包:com.gluonhq.charm.down.plugins

AudioService 界面

public interface AudioService {
    void addAudioName(String audioName);
    void play(String audioName, double volume);
    void stop(String audioName);
    void pause(String audioName);
    void resume(String audioName);
    void release();
}

AudioServiceFactory class

public class AudioServiceFactory extends DefaultServiceFactory<AudioService> {

    public AudioServiceFactory() {
        super(AudioService.class);
    }

}
  1. Android/Java 套餐

包:com.gluonhq.charm.down.plugins.android

AndroidAudioService class

public class AndroidAudioService implements AudioService {

    private final Map<String, MediaPlayer> playList;
    private final Map<String, Integer> positionList;

    public AndroidAudioService() {
        playList = new HashMap<>();
        positionList = new HashMap<>();
    }

    @Override
    public void addAudioName(String audioName) {
        MediaPlayer mediaPlayer = new MediaPlayer();
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mediaPlayer.setOnCompletionListener(m -> pause(audioName)); // don't call stop, allows reuse
        try {
            mediaPlayer.setDataSource(FXActivity.getInstance().getAssets().openFd(audioName));
            mediaPlayer.setOnPreparedListener(mp -> {
                System.out.println("Adding  audio resource " + audioName);
                playList.put(audioName, mp);
                positionList.put(audioName, 0);
            });
            mediaPlayer.prepareAsync();
        } catch (IOException ex) {
            System.out.println("Error retrieving audio resource " + audioName + " " + ex);
        }

    }

    @Override
    public void play(String audioName, double volume) {
        MediaPlayer mp = playList.get(audioName);
        if (mp != null) {
            if (positionList.get(audioName) > 0) {
                positionList.put(audioName, 0);
                mp.pause();
                mp.seekTo(0);
            }
            mp.start();
        }

    }

    @Override
    public void stop(String audioName) {
        MediaPlayer mp = playList.get(audioName);
        if (mp != null) {
            mp.stop();
        }
    }

    @Override
    public void pause(String audioName) {
        MediaPlayer mp = playList.get(audioName);
        if (mp != null) {
            mp.pause();
            positionList.put(audioName, mp.getCurrentPosition());
        }
    }

    @Override
    public void resume(String audioName) {
        MediaPlayer mp = playList.get(audioName);
        if (mp != null) {
            mp.start();
            mp.seekTo(positionList.get(audioName));
        }
    }

    @Override
    public void release() {
        for (MediaPlayer mp : playList.values()) {
            if (mp != null) {
                mp.stop();
                mp.release();
            }
        }

    }

}
  1. 样本

我添加了五个简短的音频文件(来自 here),并在我的主视图中添加了五个按钮:

@Override
public void start(Stage primaryStage) throws Exception {

    Button play1 = new Button("p1");
    Button play2 = new Button("p2");
    Button play3 = new Button("p3");
    Button play4 = new Button("p4");
    Button play5 = new Button("p5");
    HBox hBox = new HBox(10, play1, play2, play3, play4, play5);
    hBox.setAlignment(Pos.CENTER);

    Services.get(AudioService.class).ifPresent(audio -> {

        audio.addAudioName("beep28.mp3");
        audio.addAudioName("beep36.mp3");
        audio.addAudioName("beep37.mp3");
        audio.addAudioName("beep39.mp3");
        audio.addAudioName("beep50.mp3");

        play1.setOnAction(e -> audio.play("beep28.mp3", 5));
        play2.setOnAction(e -> audio.play("beep36.mp3", 5));
        play3.setOnAction(e -> audio.play("beep37.mp3", 5));
        play4.setOnAction(e -> audio.play("beep39.mp3", 5));
        play5.setOnAction(e -> audio.play("beep50.mp3", 5));
    });

    Scene scene = new Scene(new StackPane(hBox), Screen.getPrimary().getVisualBounds().getWidth(), 
                        Screen.getPrimary().getVisualBounds().getHeight());
    primaryStage.setScene(scene);
    primaryStage.show();
}

@Override
public void stop() throws Exception {
    Services.get(AudioService.class).ifPresent(AudioService::release);
}

准备步骤在应用程序启动和服务实例化时发生,因此稍后播放任何音频文件时,不会有任何延迟。

我没有检查在添加多个带有大音频文件的媒体播放器时是否会出现任何内存问题,因为这不是最初的情况。也许缓存策略在这种情况下会有所帮助(请参阅 Gluon Charm Down 中的 CacheService)。