Производительность звука с Javafx для Android (MediaPlayer и NativeAudioService)
Я создал с JavaFX игру, которая отлично работает на рабочем столе (20000 строк Java. Поскольку это игра, важно ограничение в реальном времени (время реакции на действия игрока).
Конечная цель - запустить это приложение на Android. Я почти закончил "перенос Java-кода" с ПК на Android, даже если столкнулся с некоторыми проблемами в реальном времени. Я думаю, что почти все из них решены сейчас.
Например, я минимизировал время процессора (потребление) вызовов Shape или Rectangle.intersect(node1, node2), которые используются для обнаружения столкновений между двумя мобильными устройствами. Таким образом, реальное время было разделено на 3. Отлично!
Для тестирования этой версии Android я использую Eclipse + Neon2, JavaFX, JavaFXports + gluon и мой телефон (Archos Diamond S).
Но для телефонов Android у меня была проблема в реальном времени, связанная со звуками, которые генерируются с помощью MediaPlayer и NativeAudioSrvice.
Тем не менее, я следовал этому совету, который предлагает синхронный режим: javafxports, как вызвать Android родной Media Player
1-й вопрос:
Существует ли асинхронный режим с этим классом Mediaplayer? Я думаю, что это решило бы эту проблему задержки? На практике я попробовал асинхронное решение... безуспешно: проблема в реальном времени из-за генерации аудио с MediaPlayer остается: генерация аудио стоит от 50 мс до 80 мс, тогда как основная циклическая обработка выполняется каждые 110 мс. Каждое поколение аудио может мешать выполнению основной обработки.
И в каждом периодическом задании (частота 110 мс) я могу воспроизвести несколько таких звуков. И, в след, было до шести звуковых активаций, которые занимают (вместе) около 300 мс (против 110 мс основной циклической задачи)
ВОПРОС:
Как повысить производительность класса NativeAudio (особенно метода play() с его вызовами, которые создают проблему в реальном времени: setDataSource(...), prepare() и start())?
РЕШЕНИЕ
Основная обработка должна быть "синхронизированным" методом, чтобы быть уверенным, что эта полная обработка будет выполнена без какого-либо прерывания звука.
Более того, каждая полная обработка для генерации звука выполняется в отдельном потоке, определенном с приоритетом Thread.MIN_PRIORITY.
Теперь основная обработка выполняется каждые 110 мс, и, когда она начинается, она не может быть нарушена каким-либо поколением аудио. Дисплей очень "мягкий" (больше не дергается).
Есть только небольшая проблема: когда аудио seDataSource(), метод start () или prepare () начались, кажется, что следующая основная обработка должна ждать конца метода перед началом (TBC)
Я надеюсь, что это решение может помочь другим людям. Это применимо в любом случае аудио поколений с 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
}
/****************************************************/
В классе NativeAudio, который реализует "NativeAudioService":
@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) {
}
}
1 ответ
Я немного изменил реализацию, о которой вы упомянули, учитывая, что у вас есть куча коротких аудиофайлов для воспроизведения, и вы хотите очень короткое время для их воспроизведения по требованию. В основном я создам AssetFileDescriptor
для всех файлов один раз, а также я буду использовать один и тот же сингл MediaPlayer
Экземпляр все время.
Дизайн соответствует шаблону библиотеки Charm Down, поэтому вам нужно сохранить имена пакетов ниже.
РЕДАКТИРОВАТЬ
После отзыва ОП я изменил реализацию, чтобы иметь по одному MediaPlayer для каждого аудиофайла, чтобы вы могли воспроизвести любой из них в любое время.
- Исходные пакеты /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
учебный класс
public class AudioServiceFactory extends DefaultServiceFactory<AudioService> {
public AudioServiceFactory() {
super(AudioService.class);
}
}
- Пакеты Android/Java
пакет: com.gluonhq.charm.down.plugins.android
AndroidAudioService
учебный класс
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();
}
}
}
}
- Образец
Я добавил пять коротких аудиофайлов ( отсюда) и добавил пять кнопок в мой основной вид:
@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);
}
Этап подготовки выполняется, когда приложение запускается и создается экземпляр службы, поэтому при последующем воспроизведении любого из аудиофайлов задержка не будет.
Я не проверял, могут ли быть проблемы с памятью при добавлении нескольких медиаплееров с большими аудиофайлами, так как это не было первоначальным сценарием. Возможно, в этом случае поможет стратегия кэширования (см. CacheService в Gluon Charm Down).