Added support for stoppable audio streams

Changed audio system design to support audio instances backed by either a Sound or Music instance
Made NpcDialogBox the owner of the audio it's playing to ensure proper disposal
This commit is contained in:
Collin Smith
2019-03-04 21:58:38 -08:00
parent 2569fbb185
commit 9386ba4e8f
4 changed files with 79 additions and 58 deletions

View File

@ -1,12 +1,12 @@
package gdx.diablo; package gdx.diablo;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetDescriptor; import com.badlogic.gdx.assets.AssetDescriptor;
import com.badlogic.gdx.assets.AssetManager; import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.utils.ObjectFloatMap;
import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pools;
import gdx.diablo.codec.excel.Sounds; import gdx.diablo.codec.excel.Sounds;
@ -19,21 +19,18 @@ public class Audio {
private final AssetManager assets; private final AssetManager assets;
private final ObjectMap<Sounds.Entry, AssetDescriptor<?>> descriptors = new ObjectMap<>(); private final ObjectMap<Sounds.Entry, AssetDescriptor<?>> descriptors = new ObjectMap<>();
private final ObjectFloatMap<Sounds.Entry> playing = new ObjectFloatMap<>();
public Audio(AssetManager assets) { public Audio(AssetManager assets) {
this.assets = assets; this.assets = assets;
} }
public synchronized void play(final Sounds.Entry sound, boolean global) { // TODO: Fix memory leak and dispose sound after playing
if (sound.FileName.isEmpty()) return; // TODO: Support group size
// TODO: Fix memory leak and dispose sound after playing // TODO: global vs local sounds
// TODO: Support group size public synchronized Instance play(Sounds.Entry sound, boolean global) {
// TODO: global vs local sounds if (sound.FileName.isEmpty()) return null;
if (sound.Stream) { if (sound.Stream) {
Music s; Music stream;
AssetDescriptor<Music> descriptor = (AssetDescriptor<Music>) descriptors.get(sound); AssetDescriptor<Music> descriptor = (AssetDescriptor<Music>) descriptors.get(sound);
if (descriptor == null) { if (descriptor == null) {
descriptor = new AssetDescriptor<>((global ? GLOBAL : LOCAL) + sound.FileName, Music.class); descriptor = new AssetDescriptor<>((global ? GLOBAL : LOCAL) + sound.FileName, Music.class);
@ -41,55 +38,79 @@ public class Audio {
assets.load(descriptor); assets.load(descriptor);
assets.finishLoadingAsset(descriptor); assets.finishLoadingAsset(descriptor);
s = assets.get(descriptor); stream = assets.get(descriptor);
s.setVolume(sound.Volume / 255f); stream.setVolume(sound.Volume / 255f);
s.play();
} else { } else {
s = assets.get(descriptor); stream = assets.get(descriptor);
} }
if (sound.Defer_Inst && s.isPlaying()) { if (sound.Defer_Inst && stream.isPlaying()) {
return; return null;
} }
s.play(); stream.play();
Instance instance = Instance.obtain(stream, -1);
return instance;
} else { } else {
final Sound s;
AssetDescriptor<Sound> descriptor = (AssetDescriptor<Sound>) descriptors.get(sound); AssetDescriptor<Sound> descriptor = (AssetDescriptor<Sound>) descriptors.get(sound);
if (descriptor == null) { if (descriptor == null) {
descriptor = new AssetDescriptor<>((global ? GLOBAL : LOCAL) + sound.FileName, Sound.class); descriptor = new AssetDescriptor<>((global ? GLOBAL : LOCAL) + sound.FileName, Sound.class);
descriptors.put(sound, descriptor); descriptors.put(sound, descriptor);
assets.load(descriptor); assets.load(descriptor);
assets.finishLoadingAsset(descriptor); assets.finishLoadingAsset(descriptor);
s = assets.get(descriptor);
} else {
s = assets.get(descriptor);
} }
long id = s.play(sound.Volume / 255f); Sound sfx = assets.get(descriptor);
if (id == -1) { return Instance.obtain(sfx, sfx.play(sound.Volume / 255f));
Gdx.app.postRunnable(new Runnable() { }
@Override }
public void run() {
s.play(sound.Volume / 255f); public static class Instance implements Pool.Poolable {
}
}); boolean stream;
Object delegate;
long id;
static Instance obtain(Object delegate, long id) {
Instance instance = Pools.obtain(Instance.class);
instance.stream = delegate instanceof Music;
instance.delegate = delegate;
instance.id = id;
return instance;
}
@Override
public void reset() {
delegate = null;
id = -1;
}
public void stop() {
if (stream) {
((Music) delegate).stop();
} else {
((Sound) delegate).stop(id);
}
}
public void setVolume(float volume) {
if (stream) {
((Music) delegate).setVolume(volume);
} else {
((Sound) delegate).setVolume(id, volume);
} }
} }
} }
public void play(int id, boolean global) { public Instance play(int id, boolean global) {
Sounds.Entry sound = Diablo.files.Sounds.get(id); Sounds.Entry sound = Diablo.files.Sounds.get(id);
play(sound, global); return play(sound, global);
} }
public int play(String id, boolean global) { public Instance play(String id, boolean global) {
if (id.isEmpty()) return 0; if (id.isEmpty()) return null;
Sounds.Entry sound = Diablo.files.Sounds.get(id); Sounds.Entry sound = Diablo.files.Sounds.get(id);
if (sound == null) return 0; if (sound == null) return null;
play(sound, global); return play(sound, global);
return sound.Index;
} }
} }

View File

@ -9,6 +9,7 @@ import com.badlogic.gdx.utils.IntSet;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import gdx.diablo.Audio;
import gdx.diablo.Diablo; import gdx.diablo.Diablo;
import gdx.diablo.entity.Monster; import gdx.diablo.entity.Monster;
import gdx.diablo.map.DS1; import gdx.diablo.map.DS1;
@ -36,7 +37,6 @@ public class Npc extends AI {
float actionTimer = 0; float actionTimer = 0;
boolean actionPerformed = false; boolean actionPerformed = false;
NpcMenu menu; NpcMenu menu;
int activeAudio;
public Npc(Monster entity) { public Npc(Monster entity) {
super(entity); super(entity);
@ -49,8 +49,8 @@ public class Npc extends AI {
// I.e., akara_act1_intro -> akara_act1_intro_sor automatically if it exists // I.e., akara_act1_intro -> akara_act1_intro_sor automatically if it exists
String name = entity.getName().toLowerCase(); String name = entity.getName().toLowerCase();
String id = name + "_greeting_1"; String id = name + "_greeting_1";
int index = Diablo.audio.play(id, false); Audio.Instance instance = Diablo.audio.play(id, false);
if (index == 0) { if (instance == null) {
id = name + "_greeting_inactive_1"; id = name + "_greeting_inactive_1";
Diablo.audio.play(id, false); Diablo.audio.play(id, false);
} }
@ -74,10 +74,7 @@ public class Npc extends AI {
public void clicked(InputEvent event, float x, float y) { public void clicked(InputEvent event, float x, float y) {
String name = entity.getName().toLowerCase(); String name = entity.getName().toLowerCase();
String id = name + "_act1_intro"; String id = name + "_act1_intro";
Diablo.audio.play(id, false); gameScreen.setDialog(new NpcDialogBox(id, new NpcDialogBox.DialogCompletionListener() {
gameScreen.setDialog(new NpcDialogBox(Diablo.files.speech.get(id).soundstr, new NpcDialogBox.DialogCompletionListener() {
@Override @Override
public void onCompleted(NpcDialogBox d) { public void onCompleted(NpcDialogBox d) {
gameScreen.setDialog(null); gameScreen.setDialog(null);
@ -91,9 +88,7 @@ public class Npc extends AI {
public void clicked(InputEvent event, float x, float y) { public void clicked(InputEvent event, float x, float y) {
String name = entity.getName().toLowerCase(); String name = entity.getName().toLowerCase();
String id = name + "_act1_gossip_1"; String id = name + "_act1_gossip_1";
Diablo.audio.play(id, false); gameScreen.setDialog(new NpcDialogBox(id, new NpcDialogBox.DialogCompletionListener() {
gameScreen.setDialog(new NpcDialogBox(Diablo.files.speech.get(id).soundstr, new NpcDialogBox.DialogCompletionListener() {
@Override @Override
public void onCompleted(NpcDialogBox d) { public void onCompleted(NpcDialogBox d) {
gameScreen.setDialog(null); gameScreen.setDialog(null);
@ -101,12 +96,7 @@ public class Npc extends AI {
})); }));
} }
}) })
.addCancel(new NpcMenu.CancellationListener() { .addCancel(null)
@Override
public void onCancelled() {
// TODO: stop audio
}
})
.build()); .build());
} }

View File

@ -682,6 +682,7 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable
if (this.dialog != dialog) { if (this.dialog != dialog) {
if (this.dialog != null) { if (this.dialog != null) {
this.dialog.remove(); this.dialog.remove();
this.dialog.dispose();
if (menu != null) menu.setVisible(true); if (menu != null) menu.setVisible(true);
} }

View File

@ -2,23 +2,25 @@ package gdx.diablo.widget;
import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.Disposable;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
import gdx.diablo.Audio;
import gdx.diablo.Diablo; import gdx.diablo.Diablo;
import gdx.diablo.codec.FontTBL; import gdx.diablo.codec.FontTBL;
import gdx.diablo.graphics.BorderedPaletteIndexedDrawable; import gdx.diablo.graphics.BorderedPaletteIndexedDrawable;
public class NpcDialogBox extends Table { public class NpcDialogBox extends Table implements Disposable {
DialogCompletionListener listener; DialogCompletionListener listener;
TextArea textArea; TextArea textArea;
ScrollPane scrollPane; ScrollPane scrollPane;
float scrollSpeed; float scrollSpeed;
float position; Audio.Instance audio;
public NpcDialogBox(String key, DialogCompletionListener listener) { public NpcDialogBox(String sound, DialogCompletionListener listener) {
this.listener = listener; this.listener = listener;
setBackground(new BorderedPaletteIndexedDrawable()); setBackground(new BorderedPaletteIndexedDrawable());
setTouchable(Touchable.disabled); setTouchable(Touchable.disabled);
@ -28,6 +30,7 @@ public class NpcDialogBox extends Table {
// problem seems to be with fontformat11 metrics, applying scalar to line height // problem seems to be with fontformat11 metrics, applying scalar to line height
final float lineScalar = 0.85f; final float lineScalar = 0.85f;
final FontTBL.BitmapFont FONT = Diablo.fonts.fontformal11; final FontTBL.BitmapFont FONT = Diablo.fonts.fontformal11;
String key = Diablo.files.speech.get(sound).soundstr;
String text = Diablo.string.lookup(key); String text = Diablo.string.lookup(key);
String[] parts = text.split("\n", 2); String[] parts = text.split("\n", 2);
scrollSpeed = NumberUtils.toFloat(parts[0]) / 60 * FONT.getLineHeight() * lineScalar; scrollSpeed = NumberUtils.toFloat(parts[0]) / 60 * FONT.getLineHeight() * lineScalar;
@ -55,6 +58,7 @@ public class NpcDialogBox extends Table {
pack(); pack();
scrollPane.setScrollY(-scrollPane.getScrollHeight() + textArea.getStyle().font.getLineHeight() / 2); scrollPane.setScrollY(-scrollPane.getScrollHeight() + textArea.getStyle().font.getLineHeight() / 2);
audio = Diablo.audio.play(sound, false);
} }
@Override @Override
@ -66,6 +70,11 @@ public class NpcDialogBox extends Table {
} }
} }
@Override
public void dispose() {
audio.stop();
}
public interface DialogCompletionListener { public interface DialogCompletionListener {
void onCompleted(NpcDialogBox d); void onCompleted(NpcDialogBox d);
} }