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

View File

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

View File

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