Update with basic positional audio using an override on libgdx

This commit is contained in:
Retera 2021-01-24 17:22:06 -05:00
parent 8111441f16
commit 28a24471fc
29 changed files with 3135 additions and 86 deletions

View File

@ -20,7 +20,7 @@ Path07="."
[Map]
//FilePath="CombatUnitTests.w3x"
//FilePath="PitchRoll.w3x"
//FilePath="PeonStartingBase.w3x"
FilePath="PeonStartingBase.w3x"
//FilePath="MyStromguarde.w3m"
//FilePath="ColdArrows.w3m"
//FilePath="DungeonGoldMine.w3m"
@ -43,4 +43,4 @@ Path07="."
//FilePath="test2.w3x"
//FilePath="FarseerHoldPositionTest.w3x"
//FilePath="Ramps.w3m"
FilePath="V1\Farm.w3x"
//FilePath="V1\Farm.w3x"

View File

@ -1,17 +1,22 @@
package com.etheller.warsmash.viewer5;
import com.badlogic.gdx.audio.Sound;
import com.etheller.warsmash.viewer5.gl.Extensions;
public class AudioBufferSource {
public Sound buffer;
private AudioPanner panner;
public void connect(final AudioPanner panner) {
this.panner = panner;
}
public void start(final int value, final float volume, final float pitch) {
if (this.buffer != null) {
this.buffer.play(volume, pitch, 0.0f);
if (!this.panner.listener.is3DSupported() || this.panner.isWithinListenerDistance()) {
Extensions.audio.play(this.buffer, volume, pitch, this.panner.x, this.panner.y, this.panner.z,
this.panner.listener.is3DSupported(), this.panner.maxDistance, this.panner.refDistance);
}
}
}
}

View File

@ -2,9 +2,13 @@ package com.etheller.warsmash.viewer5;
public class AudioContext {
private boolean running = false;
public Listener listener = new Listener();
public AudioDestination destination = new AudioDestination() {
};
public Listener listener;
public AudioDestination destination;
public AudioContext(final Listener listener, final AudioDestination destination) {
this.listener = listener;
this.destination = destination;
}
public void suspend() {
this.running = false;
@ -18,45 +22,65 @@ public class AudioContext {
this.running = true;
}
public static class Listener {
private float x;
private float y;
private float z;
private float forwardX;
private float forwardY;
private float forwardZ;
private float upX;
private float upY;
private float upZ;
public static interface Listener {
public void setPosition(final float x, final float y, final float z) {
this.x = x;
this.y = y;
this.z = z;
}
float getX();
float getY();
float getZ();
public void setPosition(final float x, final float y, final float z);
public void setOrientation(final float forwardX, final float forwardY, final float forwardZ, final float upX,
final float upY, final float upZ) {
this.forwardX = forwardX;
this.forwardY = forwardY;
this.forwardZ = forwardZ;
this.upX = upX;
this.upY = upY;
this.upZ = upZ;
final float upY, final float upZ);
}
}
boolean is3DSupported();
Listener DO_NOTHING = new Listener() {
private float x;
private float y;
private float z;
public AudioPanner createPanner() {
return new AudioPanner() {
@Override
public void setPosition(final float x, final float y, final float z) {
System.err.println("audio panner set position not implemented");
this.x = x;
this.y = y;
this.z = z;
}
@Override
public float getX() {
return x;
}
@Override
public float getY() {
return y;
}
@Override
public float getZ() {
return z;
}
@Override
public void setOrientation(final float forwardX, final float forwardY, final float forwardZ,
final float upX, final float upY, final float upZ) {
}
@Override
public boolean is3DSupported() {
return false;
}
};
}
public AudioPanner createPanner() {
return new AudioPanner(this.listener) {
@Override
public void connect(final AudioDestination destination) {
System.err.println("audio panner connect dest not implemented");
}
};
}

View File

@ -1,10 +1,39 @@
package com.etheller.warsmash.viewer5;
import com.etheller.warsmash.viewer5.AudioContext.Listener;
public abstract class AudioPanner {
public abstract void setPosition(float x, float y, float z);
public Listener listener;
public float x;
public float y;
public float z;
public AudioPanner(final Listener listener) {
this.listener = listener;
}
public void setPosition(final float x, final float y, final float z) {
this.x = x;
this.y = y;
this.z = z;
}
public void setDistances(final float maxDistance, final float refDistance) {
this.maxDistance = maxDistance;
this.refDistance = refDistance;
this.maxDistanceSq = maxDistance * maxDistance;
}
public float maxDistance;
public float refDistance;
public float maxDistanceSq;
public abstract void connect(AudioDestination destination);
public boolean isWithinListenerDistance() {
final float dx = this.listener.getX() - this.x;
final float dy = this.listener.getY() - this.y;
final float dz = this.listener.getZ() - this.z;
return ((dx * dx) + (dy * dy) + (dz * dz)) <= this.maxDistanceSq;
}
}

View File

@ -12,6 +12,7 @@ import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.GL30;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Rectangle;
import com.etheller.warsmash.viewer5.gl.Extensions;
import com.etheller.warsmash.viewer5.gl.WebGL;
import com.etheller.warsmash.viewer5.handlers.w3x.DynamicShadowManager;
@ -92,7 +93,7 @@ public abstract class Scene {
public boolean enableAudio() {
if (this.audioContext == null) {
this.audioContext = new AudioContext();
this.audioContext = Extensions.audio.createContext(this instanceof WorldScene);
}
if (!this.audioContext.isRunning()) {
this.audioContext.resume();
@ -197,7 +198,7 @@ public abstract class Scene {
final float upZ = this.camera.directionZ.z;
final AudioContext.Listener listener = this.audioContext.listener;
listener.setPosition(-x, -y, -z);
listener.setPosition(x, y, z);
listener.setOrientation(forwardX, forwardY, forwardZ, upX, upY, upZ);
}

View File

@ -0,0 +1,13 @@
package com.etheller.warsmash.viewer5.gl;
import com.badlogic.gdx.audio.Sound;
import com.etheller.warsmash.viewer5.AudioContext;
public interface AudioExtension {
AudioContext createContext(boolean world);
float getDuration(Sound sound);
void play(Sound buffer, final float volume, final float pitch, final float x, final float y, final float z,
final boolean is3DSound, float maxDistance, float refDistance);
}

View File

@ -7,7 +7,7 @@ public class Extensions {
public static WireframeExtension wireframeExtension;
public static SoundLengthExtension soundLengthExtension;
public static AudioExtension audio;
public static int GL_LINE = 0;
public static int GL_FILL = 0;

View File

@ -1,7 +0,0 @@
package com.etheller.warsmash.viewer5.gl;
import com.badlogic.gdx.audio.Sound;
public interface SoundLengthExtension {
float getDuration(Sound sound);
}

View File

@ -38,8 +38,7 @@ public class EventObjectSnd extends EmittedObject<MdxComplexInstance, EventObjec
// Panner settings
panner.setPosition(location.x, location.y, location.z);
panner.maxDistance = emitterObject.distanceCutoff;
panner.refDistance = emitterObject.minDistance;
panner.setDistances(emitterObject.distanceCutoff, emitterObject.minDistance);
panner.connect(audioContext.destination);
// Source.

View File

@ -30,13 +30,15 @@ public class SplatModel implements Comparable<SplatModel> {
private final List<SplatMover> splatInstances;
private final boolean unshaded;
private final boolean noDepthTest;
private final boolean highPriority;
public SplatModel(final GL30 gl, final ViewerTextureRenderable texture, final List<float[]> locations,
final float[] centerOffset, final List<Consumer<SplatMover>> unitMapping, final boolean unshaded,
final boolean noDepthTest) {
final boolean noDepthTest, final boolean highPriority) {
this.texture = texture;
this.unshaded = unshaded;
this.noDepthTest = noDepthTest;
this.highPriority = highPriority;
this.batches = new ArrayList<>();
this.color = new float[] { 1, 1, 1, 1 };
@ -261,6 +263,10 @@ public class SplatModel implements Comparable<SplatModel> {
return this.noDepthTest;
}
public boolean isHighPriority() {
return this.highPriority;
}
public SplatMover add(final float x, final float y, final float x2, final float y2, final float zDepthUpward,
final float[] centerOffset) {
this.locations.add(new float[] { x, y, x2, y2, zDepthUpward });

View File

@ -91,8 +91,7 @@ public final class UnitAckSound {
// Panner settings
panner.setPosition(unit.location[0], unit.location[1], unit.location[2]);
panner.maxDistance = this.distanceCutoff;
panner.refDistance = this.minDistance;
panner.setDistances(this.distanceCutoff, this.minDistance);
panner.connect(audioContext.destination);
// Source.
@ -103,7 +102,7 @@ public final class UnitAckSound {
source.start(0, this.volume,
(this.pitch + ((float) Math.random() * this.pitchVariance * 2)) - this.pitchVariance);
this.lastPlayedSound = source.buffer;
final float duration = Extensions.soundLengthExtension.getDuration(this.lastPlayedSound);
final float duration = Extensions.audio.getDuration(this.lastPlayedSound);
unit.lastUnitResponseEndTimeMillis = millisTime + (long) (1000 * duration);
return true;
}

View File

@ -82,7 +82,7 @@ public final class UnitSound {
return false;
}
if (play(audioContext, unit.location[0], unit.location[1])) {
final float duration = Extensions.soundLengthExtension.getDuration(this.lastPlayedSound);
final float duration = Extensions.audio.getDuration(this.lastPlayedSound);
unit.lastUnitResponseEndTimeMillis = millisTime + (long) (1000 * duration);
return true;
}
@ -106,8 +106,7 @@ public final class UnitSound {
// Panner settings
panner.setPosition(x, y, 0);
panner.maxDistance = this.distanceCutoff;
panner.refDistance = this.minDistance;
panner.setDistances(this.distanceCutoff, this.minDistance);
panner.connect(audioContext.destination);
// Source.

View File

@ -995,7 +995,7 @@ public class War3MapViewer extends AbstractMdxModelViewer {
final float s = uberSplatInfo.getFieldFloatValue("Scale");
if (this.unitsReady) {
buildingUberSplatDynamicIngame = this.terrain.addUberSplat(texturePath, unitX, unitY, 1, s,
false, false);
false, false, false);
}
else {
if (!this.terrain.splats.containsKey(texturePath)) {
@ -1318,9 +1318,8 @@ public class War3MapViewer extends AbstractMdxModelViewer {
final float x = unit.getX();
final float y = unit.getY();
System.out.println("Selecting a unit at " + x + "," + y);
final float z = unit.getSelectionHeight();
splats.get(path).locations.add(new float[] { x - (selectionSize / 2), y - (selectionSize / 2),
x + (selectionSize / 2), y + (selectionSize / 2), z + 5 });
x + (selectionSize / 2), y + (selectionSize / 2), 5 });
splats.get(path).unitMapping.add(new Consumer<SplatModel.SplatMover>() {
@Override
public void accept(final SplatMover t) {
@ -1338,7 +1337,7 @@ public class War3MapViewer extends AbstractMdxModelViewer {
final String path = entry.getKey();
final Splat locations = entry.getValue();
final SplatModel model = new SplatModel(Gdx.gl30, (Texture) load(path, PathSolver.DEFAULT, null),
locations.locations, this.terrain.centerOffset, locations.unitMapping, true, false);
locations.locations, this.terrain.centerOffset, locations.unitMapping, true, false, true);
model.color[0] = 0;
model.color[1] = 1;
model.color[2] = 0;

View File

@ -1044,7 +1044,7 @@ public class Terrain {
// Render the cliffs
for (final SplatModel splat : this.uberSplatModelsList) {
if (splat.isNoDepthTest() == onTopLayer) {
if (splat.isHighPriority() == onTopLayer) {
splat.render(gl, shader);
}
}
@ -1433,7 +1433,7 @@ public class Terrain {
final SplatModel splatModel = new SplatModel(Gdx.gl30,
(Texture) this.viewer.load(path, PathSolver.DEFAULT, null), splat.locations, this.centerOffset,
splat.unitMapping.isEmpty() ? null : splat.unitMapping, false, false);
splat.unitMapping.isEmpty() ? null : splat.unitMapping, false, false, false);
splatModel.color[3] = splat.opacity;
this.addSplatBatchModel(path, splatModel);
}
@ -1450,11 +1450,11 @@ public class Terrain {
}
public SplatMover addUberSplat(final String path, final float x, final float y, final float z, final float scale,
final boolean unshaded, final boolean noDepthTest) {
final boolean unshaded, final boolean noDepthTest, final boolean highPriority) {
SplatModel splatModel = this.uberSplatModels.get(path);
if (splatModel == null) {
splatModel = new SplatModel(Gdx.gl30, (Texture) this.viewer.load(path, PathSolver.DEFAULT, null),
new ArrayList<>(), this.centerOffset, new ArrayList<>(), unshaded, noDepthTest);
new ArrayList<>(), this.centerOffset, new ArrayList<>(), unshaded, noDepthTest, highPriority);
this.addSplatBatchModel(path, splatModel);
}
return splatModel.add(x - scale, y - scale, x + scale, y + scale, z, this.centerOffset);
@ -1465,7 +1465,7 @@ public class Terrain {
SplatModel splatModel = this.uberSplatModels.get(texture);
if (splatModel == null) {
splatModel = new SplatModel(Gdx.gl30, (Texture) this.viewer.load(texture, PathSolver.DEFAULT, null),
new ArrayList<>(), this.centerOffset, new ArrayList<>(), false, false);
new ArrayList<>(), this.centerOffset, new ArrayList<>(), false, false, false);
splatModel.color[3] = opacity;
this.addSplatBatchModel(texture, splatModel);
}

View File

@ -87,11 +87,6 @@ public class RenderDestructable extends RenderDoodad implements RenderWidget {
return this.y;
}
@Override
public float getSelectionHeight() {
return 0;
}
@Override
public void unassignSelectionCircle() {
this.selectionCircle = null;

View File

@ -72,6 +72,7 @@ public class RenderUnit implements RenderWidget {
private final RenderUnitTypeData typeData;
public final MdxModel specialArtModel;
public SplatMover uberSplat;
private float selectionHeight;
public RenderUnit(final War3MapViewer map, final MdxModel model, final MutableGameObject row, final float x,
final float y, final float z, final int playerIndex, final UnitSoundset soundset,
@ -127,6 +128,7 @@ public class RenderUnit implements RenderWidget {
instance.uniformScale(row.getFieldAsFloat(scale, 0));
this.selectionScale = row.getFieldAsFloat(War3MapViewer.UNIT_SELECT_SCALE, 0) * selectionCircleScaleFactor;
this.selectionHeight = row.getFieldAsFloat(UNIT_SELECT_HEIGHT, 0);
int orientationInterpolationOrdinal = row.getFieldAsInteger(ORIENTATION_INTERPOLATION, 0);
if ((orientationInterpolationOrdinal < 0)
|| (orientationInterpolationOrdinal >= OrientationInterpolation.VALUES.length)) {
@ -289,6 +291,7 @@ public class RenderUnit implements RenderWidget {
this.corpse = corpse;
this.boneCorpse = boneCorpse;
this.location[2] = this.simulationUnit.getFlyHeight() + groundHeight;
final float selectionCircleHeight = this.selectionHeight + groundHeight;
this.instance.moveTo(this.location);
float simulationFacing = this.simulationUnit.getFacing();
if (simulationFacing < 0) {
@ -394,12 +397,14 @@ public class RenderUnit implements RenderWidget {
map.worldScene.instanceMoved(this.instance, this.location[0], this.location[1]);
if (this.shadow != null) {
this.shadow.move(dx, dy, map.terrain.centerOffset);
this.shadow.setHeightAbsolute(currentWalkableUnder != null, this.location[2] + map.imageWalkableZOffset);
this.shadow.setHeightAbsolute(currentWalkableUnder != null, groundHeight + map.imageWalkableZOffset);
}
if (this.selectionCircle != null) {
this.selectionCircle.move(dx, dy, map.terrain.centerOffset);
this.selectionCircle.setHeightAbsolute(currentWalkableUnder != null,
this.location[2] + map.imageWalkableZOffset);
this.selectionCircle.setHeightAbsolute(
(currentWalkableUnder != null)
|| ((movementType == MovementType.FLY) || (movementType == MovementType.HOVER)),
selectionCircleHeight + map.imageWalkableZOffset);
}
this.unitAnimationListenerImpl.update();
if (!dead && this.simulationUnit.isConstructing()) {
@ -576,11 +581,6 @@ public class RenderUnit implements RenderWidget {
return this.simulationUnit.getUnitType().isBuilding();
}
@Override
public float getSelectionHeight() {
return this.row.getFieldAsFloat(UNIT_SELECT_HEIGHT, 0);
}
@Override
public float getSelectionScale() {
return this.selectionScale;

View File

@ -20,8 +20,6 @@ public interface RenderWidget {
float getY();
float getSelectionHeight();
void unassignSelectionCircle();
void assignSelectionCircle(SplatMover t);

View File

@ -1008,7 +1008,7 @@ public class MeleeUI implements CUnitStateListener, CommandButtonListener, Comma
final ViewerTextureRenderable greenPixmap = new ViewerTextureRenderable.GdxViewerTextureRenderable(
MeleeUI.this.cursorModelUnderneathPathingRedGreenPixmapTexture);
MeleeUI.this.cursorModelUnderneathPathingRedGreenSplatModel = new SplatModel(Gdx.gl30, greenPixmap,
new ArrayList<>(), viewer.terrain.centerOffset, new ArrayList<>(), true, false);
new ArrayList<>(), viewer.terrain.centerOffset, new ArrayList<>(), true, false, true);
MeleeUI.this.cursorModelUnderneathPathingRedGreenSplatModel.color[3] = 0.20f;
}
}
@ -1077,7 +1077,7 @@ public class MeleeUI implements CUnitStateListener, CommandButtonListener, Comma
if (MeleeUI.this.placementCursor == null) {
MeleeUI.this.placementCursor = viewer.terrain.addUberSplat(
MeleeUI.this.rootFrame.getSkinField("PlacementCursor"), clickLocationTemp.x,
clickLocationTemp.y, 10, radius, true, true);
clickLocationTemp.y, 10, radius, true, true, true);
}
MeleeUI.this.placementCursor.setLocation(clickLocationTemp.x, clickLocationTemp.y,
viewer.terrain.centerOffset);

View File

@ -0,0 +1,487 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.badlogic.gdx.backends.lwjgl;
import java.awt.Canvas;
import java.io.File;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.ApplicationLogger;
import com.badlogic.gdx.Audio;
import com.badlogic.gdx.Files;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.LifecycleListener;
import com.badlogic.gdx.Net;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Clipboard;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.SnapshotArray;
import com.etheller.warsmash.audio.OpenALAudio;
/**
* An OpenGL surface fullscreen or in a lightweight window. This was modified by
* Retera in accordance with the permission to do so from the Apache 2.0 license
* listed at the top of the file. The ONLY reason for this modified file
* override currently is to use a replacement for the OpenALAudio class that
* will now support the 3D sound!
*/
public class LwjglApplication implements Application {
protected final LwjglGraphics graphics;
protected OpenALAudio audio;
protected final LwjglFiles files;
protected final LwjglInput input;
protected final LwjglNet net;
protected final ApplicationListener listener;
protected Thread mainLoopThread;
protected boolean running = true;
protected final Array<Runnable> runnables = new Array<Runnable>();
protected final Array<Runnable> executedRunnables = new Array<Runnable>();
protected final SnapshotArray<LifecycleListener> lifecycleListeners = new SnapshotArray<LifecycleListener>(
LifecycleListener.class);
protected int logLevel = LOG_INFO;
protected ApplicationLogger applicationLogger;
protected String preferencesdir;
protected Files.FileType preferencesFileType;
public LwjglApplication(final ApplicationListener listener, final String title, final int width, final int height) {
this(listener, createConfig(title, width, height));
}
public LwjglApplication(final ApplicationListener listener) {
this(listener, null, 640, 480);
}
public LwjglApplication(final ApplicationListener listener, final LwjglApplicationConfiguration config) {
this(listener, config, new LwjglGraphics(config));
}
public LwjglApplication(final ApplicationListener listener, final Canvas canvas) {
this(listener, new LwjglApplicationConfiguration(), new LwjglGraphics(canvas));
}
public LwjglApplication(final ApplicationListener listener, final LwjglApplicationConfiguration config,
final Canvas canvas) {
this(listener, config, new LwjglGraphics(canvas, config));
}
public LwjglApplication(final ApplicationListener listener, final LwjglApplicationConfiguration config,
final LwjglGraphics graphics) {
LwjglNativesLoader.load();
setApplicationLogger(new LwjglApplicationLogger());
if (config.title == null) {
config.title = listener.getClass().getSimpleName();
}
this.graphics = graphics;
if (!LwjglApplicationConfiguration.disableAudio) {
try {
this.audio = new OpenALAudio(config.audioDeviceSimultaneousSources, config.audioDeviceBufferCount,
config.audioDeviceBufferSize);
}
catch (final Throwable t) {
log("LwjglApplication", "Couldn't initialize audio, disabling audio", t);
LwjglApplicationConfiguration.disableAudio = true;
}
}
this.files = new LwjglFiles();
this.input = new LwjglInput();
this.net = new LwjglNet();
this.listener = listener;
this.preferencesdir = config.preferencesDirectory;
this.preferencesFileType = config.preferencesFileType;
Gdx.app = this;
Gdx.graphics = graphics;
Gdx.audio = this.audio;
Gdx.files = this.files;
Gdx.input = this.input;
Gdx.net = this.net;
initialize();
}
private static LwjglApplicationConfiguration createConfig(final String title, final int width, final int height) {
final LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.title = title;
config.width = width;
config.height = height;
config.vSyncEnabled = true;
return config;
}
private void initialize() {
this.mainLoopThread = new Thread("LWJGL Application") {
@Override
public void run() {
LwjglApplication.this.graphics.setVSync(LwjglApplication.this.graphics.config.vSyncEnabled);
try {
LwjglApplication.this.mainLoop();
}
catch (final Throwable t) {
if (LwjglApplication.this.audio != null) {
LwjglApplication.this.audio.dispose();
}
Gdx.input.setCursorCatched(false);
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
else {
throw new GdxRuntimeException(t);
}
}
}
};
this.mainLoopThread.start();
}
void mainLoop() {
final SnapshotArray<LifecycleListener> lifecycleListeners = this.lifecycleListeners;
try {
this.graphics.setupDisplay();
}
catch (final LWJGLException e) {
throw new GdxRuntimeException(e);
}
this.listener.create();
this.graphics.resize = true;
int lastWidth = this.graphics.getWidth();
int lastHeight = this.graphics.getHeight();
this.graphics.lastTime = System.nanoTime();
boolean wasActive = true;
while (this.running) {
Display.processMessages();
if (Display.isCloseRequested()) {
exit();
}
final boolean isActive = Display.isActive();
if (wasActive && !isActive) { // if it's just recently minimized from active state
wasActive = false;
synchronized (lifecycleListeners) {
final LifecycleListener[] listeners = lifecycleListeners.begin();
for (int i = 0, n = lifecycleListeners.size; i < n; ++i) {
listeners[i].pause();
}
lifecycleListeners.end();
}
this.listener.pause();
}
if (!wasActive && isActive) { // if it's just recently focused from minimized state
wasActive = true;
synchronized (lifecycleListeners) {
final LifecycleListener[] listeners = lifecycleListeners.begin();
for (int i = 0, n = lifecycleListeners.size; i < n; ++i) {
listeners[i].resume();
}
lifecycleListeners.end();
}
this.listener.resume();
}
boolean shouldRender = false;
if (this.graphics.canvas != null) {
final int width = this.graphics.canvas.getWidth();
final int height = this.graphics.canvas.getHeight();
if ((lastWidth != width) || (lastHeight != height)) {
lastWidth = width;
lastHeight = height;
Gdx.gl.glViewport(0, 0, lastWidth, lastHeight);
this.listener.resize(lastWidth, lastHeight);
shouldRender = true;
}
}
else {
this.graphics.config.x = Display.getX();
this.graphics.config.y = Display.getY();
if (this.graphics.resize || Display.wasResized()
|| ((int) (Display.getWidth() * Display.getPixelScaleFactor()) != this.graphics.config.width)
|| ((int) (Display.getHeight()
* Display.getPixelScaleFactor()) != this.graphics.config.height)) {
this.graphics.resize = false;
this.graphics.config.width = (int) (Display.getWidth() * Display.getPixelScaleFactor());
this.graphics.config.height = (int) (Display.getHeight() * Display.getPixelScaleFactor());
Gdx.gl.glViewport(0, 0, this.graphics.config.width, this.graphics.config.height);
if (this.listener != null) {
this.listener.resize(this.graphics.config.width, this.graphics.config.height);
}
this.graphics.requestRendering();
}
}
if (executeRunnables()) {
shouldRender = true;
}
// If one of the runnables set running to false, for example after an exit().
if (!this.running) {
break;
}
this.input.update();
shouldRender |= this.graphics.shouldRender();
this.input.processEvents();
if (this.audio != null) {
this.audio.update();
}
if (!isActive && (this.graphics.config.backgroundFPS == -1)) {
shouldRender = false;
}
int frameRate = isActive ? this.graphics.config.foregroundFPS : this.graphics.config.backgroundFPS;
if (shouldRender) {
this.graphics.updateTime();
this.graphics.frameId++;
this.listener.render();
Display.update(false);
}
else {
// Sleeps to avoid wasting CPU in an empty loop.
if (frameRate == -1) {
frameRate = 10;
}
if (frameRate == 0) {
frameRate = this.graphics.config.backgroundFPS;
}
if (frameRate == 0) {
frameRate = 30;
}
}
if (frameRate > 0) {
Display.sync(frameRate);
}
}
synchronized (lifecycleListeners) {
final LifecycleListener[] listeners = lifecycleListeners.begin();
for (int i = 0, n = lifecycleListeners.size; i < n; ++i) {
listeners[i].pause();
listeners[i].dispose();
}
lifecycleListeners.end();
}
this.listener.pause();
this.listener.dispose();
Display.destroy();
if (this.audio != null) {
this.audio.dispose();
}
if (this.graphics.config.forceExit) {
System.exit(-1);
}
}
public boolean executeRunnables() {
synchronized (this.runnables) {
for (int i = this.runnables.size - 1; i >= 0; i--) {
this.executedRunnables.add(this.runnables.get(i));
}
this.runnables.clear();
}
if (this.executedRunnables.size == 0) {
return false;
}
do {
this.executedRunnables.pop().run();
}
while (this.executedRunnables.size > 0);
return true;
}
@Override
public ApplicationListener getApplicationListener() {
return this.listener;
}
@Override
public Audio getAudio() {
return this.audio;
}
@Override
public Files getFiles() {
return this.files;
}
@Override
public LwjglGraphics getGraphics() {
return this.graphics;
}
@Override
public Input getInput() {
return this.input;
}
@Override
public Net getNet() {
return this.net;
}
@Override
public ApplicationType getType() {
return ApplicationType.Desktop;
}
@Override
public int getVersion() {
return 0;
}
public void stop() {
this.running = false;
try {
this.mainLoopThread.join();
}
catch (final Exception ex) {
}
}
@Override
public long getJavaHeap() {
return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
}
@Override
public long getNativeHeap() {
return getJavaHeap();
}
ObjectMap<String, Preferences> preferences = new ObjectMap<String, Preferences>();
@Override
public Preferences getPreferences(final String name) {
if (this.preferences.containsKey(name)) {
return this.preferences.get(name);
}
else {
final Preferences prefs = new LwjglPreferences(
new LwjglFileHandle(new File(this.preferencesdir, name), this.preferencesFileType));
this.preferences.put(name, prefs);
return prefs;
}
}
@Override
public Clipboard getClipboard() {
return new LwjglClipboard();
}
@Override
public void postRunnable(final Runnable runnable) {
synchronized (this.runnables) {
this.runnables.add(runnable);
Gdx.graphics.requestRendering();
}
}
@Override
public void debug(final String tag, final String message) {
if (this.logLevel >= LOG_DEBUG) {
getApplicationLogger().debug(tag, message);
}
}
@Override
public void debug(final String tag, final String message, final Throwable exception) {
if (this.logLevel >= LOG_DEBUG) {
getApplicationLogger().debug(tag, message, exception);
}
}
@Override
public void log(final String tag, final String message) {
if (this.logLevel >= LOG_INFO) {
getApplicationLogger().log(tag, message);
}
}
@Override
public void log(final String tag, final String message, final Throwable exception) {
if (this.logLevel >= LOG_INFO) {
getApplicationLogger().log(tag, message, exception);
}
}
@Override
public void error(final String tag, final String message) {
if (this.logLevel >= LOG_ERROR) {
getApplicationLogger().error(tag, message);
}
}
@Override
public void error(final String tag, final String message, final Throwable exception) {
if (this.logLevel >= LOG_ERROR) {
getApplicationLogger().error(tag, message, exception);
}
}
@Override
public void setLogLevel(final int logLevel) {
this.logLevel = logLevel;
}
@Override
public int getLogLevel() {
return this.logLevel;
}
@Override
public void setApplicationLogger(final ApplicationLogger applicationLogger) {
this.applicationLogger = applicationLogger;
}
@Override
public ApplicationLogger getApplicationLogger() {
return this.applicationLogger;
}
@Override
public void exit() {
postRunnable(new Runnable() {
@Override
public void run() {
LwjglApplication.this.running = false;
}
});
}
@Override
public void addLifecycleListener(final LifecycleListener listener) {
synchronized (this.lifecycleListeners) {
this.lifecycleListeners.add(listener);
}
}
@Override
public void removeLifecycleListener(final LifecycleListener listener) {
synchronized (this.lifecycleListeners) {
this.lifecycleListeners.removeValue(listener, true);
}
}
}

View File

@ -0,0 +1,66 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFormat.Encoding;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.TargetDataLine;
import com.badlogic.gdx.audio.AudioRecorder;
import com.badlogic.gdx.utils.GdxRuntimeException;
/** @author mzechner */
public class JavaSoundAudioRecorder implements AudioRecorder {
private TargetDataLine line;
private byte[] buffer = new byte[1024 * 4];
public JavaSoundAudioRecorder(final int samplingRate, final boolean isMono) {
try {
final AudioFormat format = new AudioFormat(Encoding.PCM_SIGNED, samplingRate, 16, isMono ? 1 : 2,
isMono ? 2 : 4, samplingRate, false);
this.line = AudioSystem.getTargetDataLine(format);
this.line.open(format, this.buffer.length);
this.line.start();
}
catch (final Exception ex) {
throw new GdxRuntimeException("Error creating JavaSoundAudioRecorder.", ex);
}
}
@Override
public void read(final short[] samples, final int offset, final int numSamples) {
if (this.buffer.length < (numSamples * 2)) {
this.buffer = new byte[numSamples * 2];
}
final int toRead = numSamples * 2;
int read = 0;
while (read != toRead) {
read += this.line.read(this.buffer, read, toRead - read);
}
for (int i = 0, j = 0; i < (numSamples * 2); i += 2, j++) {
samples[offset + j] = (short) ((this.buffer[i + 1] << 8) | (this.buffer[i] & 0xff));
}
}
@Override
public void dispose() {
this.line.close();
}
}

View File

@ -0,0 +1,163 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import java.io.ByteArrayOutputStream;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.GdxRuntimeException;
import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.MP3Decoder;
import javazoom.jl.decoder.OutputBuffer;
/** @author Nathan Sweet */
public class Mp3 {
static public class Music extends OpenALMusic {
// Note: This uses a slightly modified version of JLayer.
private Bitstream bitstream;
private OutputBuffer outputBuffer;
private MP3Decoder decoder;
public Music(final OpenALAudio audio, final FileHandle file) {
super(audio, file);
if (audio.noDevice) {
return;
}
this.bitstream = new Bitstream(file.read());
this.decoder = new MP3Decoder();
this.bufferOverhead = 4096;
try {
final Header header = this.bitstream.readFrame();
if (header == null) {
throw new GdxRuntimeException("Empty MP3");
}
final int channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2;
this.outputBuffer = new OutputBuffer(channels, false);
this.decoder.setOutputBuffer(this.outputBuffer);
setup(channels, header.getSampleRate());
}
catch (final BitstreamException e) {
throw new GdxRuntimeException("error while preloading mp3", e);
}
}
@Override
public int read(final byte[] buffer) {
try {
boolean setup = this.bitstream == null;
if (setup) {
this.bitstream = new Bitstream(this.file.read());
this.decoder = new MP3Decoder();
}
int totalLength = 0;
final int minRequiredLength = buffer.length - (OutputBuffer.BUFFERSIZE * 2);
while (totalLength <= minRequiredLength) {
final Header header = this.bitstream.readFrame();
if (header == null) {
break;
}
if (setup) {
final int channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2;
this.outputBuffer = new OutputBuffer(channels, false);
this.decoder.setOutputBuffer(this.outputBuffer);
setup(channels, header.getSampleRate());
setup = false;
}
try {
this.decoder.decodeFrame(header, this.bitstream);
}
catch (final Exception ignored) {
// JLayer's decoder throws ArrayIndexOutOfBoundsException sometimes!?
}
this.bitstream.closeFrame();
final int length = this.outputBuffer.reset();
System.arraycopy(this.outputBuffer.getBuffer(), 0, buffer, totalLength, length);
totalLength += length;
}
return totalLength;
}
catch (final Throwable ex) {
reset();
throw new GdxRuntimeException("Error reading audio data.", ex);
}
}
@Override
public void reset() {
if (this.bitstream == null) {
return;
}
try {
this.bitstream.close();
}
catch (final BitstreamException ignored) {
}
this.bitstream = null;
}
}
static public class Sound extends OpenALSound {
// Note: This uses a slightly modified version of JLayer.
public Sound(final OpenALAudio audio, final FileHandle file) {
super(audio);
if (audio.noDevice) {
return;
}
final ByteArrayOutputStream output = new ByteArrayOutputStream(4096);
final Bitstream bitstream = new Bitstream(file.read());
final MP3Decoder decoder = new MP3Decoder();
try {
OutputBuffer outputBuffer = null;
int sampleRate = -1, channels = -1;
while (true) {
final Header header = bitstream.readFrame();
if (header == null) {
break;
}
if (outputBuffer == null) {
channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2;
outputBuffer = new OutputBuffer(channels, false);
decoder.setOutputBuffer(outputBuffer);
sampleRate = header.getSampleRate();
}
try {
decoder.decodeFrame(header, bitstream);
}
catch (final Exception ignored) {
// JLayer's decoder throws ArrayIndexOutOfBoundsException sometimes!?
}
bitstream.closeFrame();
output.write(outputBuffer.getBuffer(), 0, outputBuffer.reset());
}
bitstream.close();
setup(output.toByteArray(), channels, sampleRate);
}
catch (final Throwable ex) {
throw new GdxRuntimeException("Error reading audio data.", ex);
}
}
}
}

View File

@ -0,0 +1,89 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import java.io.ByteArrayOutputStream;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.StreamUtils;
/** @author Nathan Sweet */
public class Ogg {
static public class Music extends OpenALMusic {
private OggInputStream input;
private OggInputStream previousInput;
public Music(final OpenALAudio audio, final FileHandle file) {
super(audio, file);
if (audio.noDevice) {
return;
}
this.input = new OggInputStream(file.read());
setup(this.input.getChannels(), this.input.getSampleRate());
}
@Override
public int read(final byte[] buffer) {
if (this.input == null) {
this.input = new OggInputStream(this.file.read(), this.previousInput);
setup(this.input.getChannels(), this.input.getSampleRate());
this.previousInput = null; // release this reference
}
return this.input.read(buffer);
}
@Override
public void reset() {
StreamUtils.closeQuietly(this.input);
this.previousInput = null;
this.input = null;
}
@Override
protected void loop() {
StreamUtils.closeQuietly(this.input);
this.previousInput = this.input;
this.input = null;
}
}
static public class Sound extends OpenALSound {
public Sound(final OpenALAudio audio, final FileHandle file) {
super(audio);
if (audio.noDevice) {
return;
}
OggInputStream input = null;
try {
input = new OggInputStream(file.read());
final ByteArrayOutputStream output = new ByteArrayOutputStream(4096);
final byte[] buffer = new byte[2048];
while (!input.atEnd()) {
final int length = input.read(buffer);
if (length == -1) {
break;
}
output.write(buffer, 0, length);
}
setup(output.toByteArray(), input.getChannels(), input.getSampleRate());
}
finally {
StreamUtils.closeQuietly(input);
}
}
}
}

View File

@ -0,0 +1,520 @@
/**
* Copyright (c) 2007, Slick 2D
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
* conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution. Neither the name of the Slick 2D nor the names of
* its contributors may be used to endorse or promote products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.etheller.warsmash.audio;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.StreamUtils;
import com.jcraft.jogg.Packet;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.SyncState;
import com.jcraft.jorbis.Block;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.DspState;
import com.jcraft.jorbis.Info;
/**
* An input stream to read Ogg Vorbis.
*
* @author kevin
*/
public class OggInputStream extends InputStream {
private final static int BUFFER_SIZE = 512;
/** The conversion buffer size */
private int convsize = BUFFER_SIZE * 4;
/** The buffer used to read OGG file */
private byte[] convbuffer;
/** The stream we're reading the OGG file from */
private final InputStream input;
/** The audio information from the OGG header */
private final Info oggInfo = new Info(); // struct that stores all the static vorbis bitstream settings
/** True if we're at the end of the available data */
private boolean endOfStream;
/** The Vorbis SyncState used to decode the OGG */
private final SyncState syncState = new SyncState(); // sync and verify incoming physical bitstream
/** The Vorbis Stream State used to decode the OGG */
private final StreamState streamState = new StreamState(); // take physical pages, weld into a logical stream of
// packets
/** The current OGG page */
private final Page page = new Page(); // one Ogg bitstream page. Vorbis packets are inside
/** The current packet page */
private final Packet packet = new Packet(); // one raw packet of data for decode
/** The comment read from the OGG file */
private final Comment comment = new Comment(); // struct that stores all the bitstream user comments
/** The Vorbis DSP stat eused to decode the OGG */
private final DspState dspState = new DspState(); // central working state for the packet->PCM decoder
/** The OGG block we're currently working with to convert PCM */
private final Block vorbisBlock = new Block(this.dspState); // local working space for packet->PCM decode
/** Temporary scratch buffer */
byte[] buffer;
/** The number of bytes read */
int bytes = 0;
/** The true if we should be reading big endian */
boolean bigEndian = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN);
/** True if we're reached the end of the current bit stream */
boolean endOfBitStream = true;
/** True if we're initialise the OGG info block */
boolean inited = false;
/** The index into the byte array we currently read from */
private int readIndex;
/** The byte array store used to hold the data read from the ogg */
private byte[] outBuffer;
private int outIndex;
/** The total number of bytes */
private int total;
/**
* Create a new stream to decode OGG data
*
* @param input The input stream from which to read the OGG file
*/
public OggInputStream(final InputStream input) {
this(input, null);
}
/**
* Create a new stream to decode OGG data, reusing buffers from another stream.
*
* It's not a good idea to use the old stream instance afterwards.
*
* @param input The input stream from which to read the OGG file
* @param previousStream The stream instance to reuse buffers from, may be null
*/
public OggInputStream(final InputStream input, final OggInputStream previousStream) {
if (previousStream == null) {
this.convbuffer = new byte[this.convsize];
this.outBuffer = new byte[4096 * 500];
}
else {
this.convbuffer = previousStream.convbuffer;
this.outBuffer = previousStream.outBuffer;
}
this.input = input;
try {
this.total = input.available();
}
catch (final IOException ex) {
throw new GdxRuntimeException(ex);
}
init();
}
/**
* Get the number of bytes on the stream
*
* @return The number of the bytes on the stream
*/
public int getLength() {
return this.total;
}
public int getChannels() {
return this.oggInfo.channels;
}
public int getSampleRate() {
return this.oggInfo.rate;
}
/** Initialise the streams and thread involved in the streaming of OGG data */
private void init() {
initVorbis();
readPCM();
}
/** @see java.io.InputStream#available() */
@Override
public int available() {
return this.endOfStream ? 0 : 1;
}
/** Initialise the vorbis decoding */
private void initVorbis() {
this.syncState.init();
}
/**
* Get a page and packet from that page
*
* @return True if there was a page available
*/
private boolean getPageAndPacket() {
// grab some data at the head of the stream. We want the first page
// (which is guaranteed to be small and only contain the Vorbis
// stream initial header) We need the first page to get the stream
// serialno.
// submit a 4k block to libvorbis' Ogg layer
int index = this.syncState.buffer(BUFFER_SIZE);
if (index == -1) {
return false;
}
this.buffer = this.syncState.data;
if (this.buffer == null) {
this.endOfStream = true;
return false;
}
try {
this.bytes = this.input.read(this.buffer, index, BUFFER_SIZE);
}
catch (final Exception e) {
throw new GdxRuntimeException("Failure reading Vorbis.", e);
}
this.syncState.wrote(this.bytes);
// Get the first page.
if (this.syncState.pageout(this.page) != 1) {
// have we simply run out of data? If so, we're done.
if (this.bytes < BUFFER_SIZE) {
return false;
}
// error case. Must not be Vorbis data
throw new GdxRuntimeException("Input does not appear to be an Ogg bitstream.");
}
// Get the serial number and set up the rest of decode.
// serialno first; use it to set up a logical stream
this.streamState.init(this.page.serialno());
// extract the initial header from the first page and verify that the
// Ogg bitstream is in fact Vorbis data
// I handle the initial header first instead of just having the code
// read all three Vorbis headers at once because reading the initial
// header is an easy way to identify a Vorbis bitstream and it's
// useful to see that functionality seperated out.
this.oggInfo.init();
this.comment.init();
if (this.streamState.pagein(this.page) < 0) {
// error; stream version mismatch perhaps
throw new GdxRuntimeException("Error reading first page of Ogg bitstream.");
}
if (this.streamState.packetout(this.packet) != 1) {
// no page? must not be vorbis
throw new GdxRuntimeException("Error reading initial header packet.");
}
if (this.oggInfo.synthesis_headerin(this.comment, this.packet) < 0) {
// error case; not a vorbis header
throw new GdxRuntimeException("Ogg bitstream does not contain Vorbis audio data.");
}
// At this point, we're sure we're Vorbis. We've set up the logical
// (Ogg) bitstream decoder. Get the comment and codebook headers and
// set up the Vorbis decoder
// The next two packets in order are the comment and codebook headers.
// They're likely large and may span multiple pages. Thus we reead
// and submit data until we get our two pacakets, watching that no
// pages are missing. If a page is missing, error out; losing a
// header page is the only place where missing data is fatal. */
int i = 0;
while (i < 2) {
while (i < 2) {
int result = this.syncState.pageout(this.page);
if (result == 0) {
break; // Need more data
// Don't complain about missing or corrupt data yet. We'll
// catch it at the packet output phase
}
if (result == 1) {
this.streamState.pagein(this.page); // we can ignore any errors here
// as they'll also become apparent
// at packetout
while (i < 2) {
result = this.streamState.packetout(this.packet);
if (result == 0) {
break;
}
if (result == -1) {
// Uh oh; data at some point was corrupted or missing!
// We can't tolerate that in a header. Die.
throw new GdxRuntimeException("Corrupt secondary header.");
}
this.oggInfo.synthesis_headerin(this.comment, this.packet);
i++;
}
}
}
// no harm in not checking before adding more
index = this.syncState.buffer(BUFFER_SIZE);
if (index == -1) {
return false;
}
this.buffer = this.syncState.data;
try {
this.bytes = this.input.read(this.buffer, index, BUFFER_SIZE);
}
catch (final Exception e) {
throw new GdxRuntimeException("Failed to read Vorbis.", e);
}
if ((this.bytes == 0) && (i < 2)) {
throw new GdxRuntimeException("End of file before finding all Vorbis headers.");
}
this.syncState.wrote(this.bytes);
}
this.convsize = BUFFER_SIZE / this.oggInfo.channels;
// OK, got and parsed all three headers. Initialize the Vorbis
// packet->PCM decoder.
this.dspState.synthesis_init(this.oggInfo); // central decode state
this.vorbisBlock.init(this.dspState); // local state for most of the decode
// so multiple block decodes can
// proceed in parallel. We could init
// multiple vorbis_block structures
// for vd here
return true;
}
/** Decode the OGG file as shown in the jogg/jorbis examples */
private void readPCM() {
boolean wrote = false;
while (true) { // we repeat if the bitstream is chained
if (this.endOfBitStream) {
if (!getPageAndPacket()) {
break;
}
this.endOfBitStream = false;
}
if (!this.inited) {
this.inited = true;
return;
}
final float[][][] _pcm = new float[1][][];
final int[] _index = new int[this.oggInfo.channels];
// The rest is just a straight decode loop until end of stream
while (!this.endOfBitStream) {
while (!this.endOfBitStream) {
int result = this.syncState.pageout(this.page);
if (result == 0) {
break; // need more data
}
if (result == -1) { // missing or corrupt data at this page position
// throw new GdxRuntimeException("Corrupt or missing data in bitstream.");
Gdx.app.log("gdx-audio", "Error reading OGG: Corrupt or missing data in bitstream.");
}
else {
this.streamState.pagein(this.page); // can safely ignore errors at
// this point
while (true) {
result = this.streamState.packetout(this.packet);
if (result == 0) {
break; // need more data
}
if (result == -1) { // missing or corrupt data at this page position
// no reason to complain; already complained above
}
else {
// we have a packet. Decode it
int samples;
if (this.vorbisBlock.synthesis(this.packet) == 0) { // test for success!
this.dspState.synthesis_blockin(this.vorbisBlock);
}
// **pcm is a multichannel float vector. In stereo, for
// example, pcm[0] is left, and pcm[1] is right. samples is
// the size of each channel. Convert the float values
// (-1.<=range<=1.) to whatever PCM format and write it out
while ((samples = this.dspState.synthesis_pcmout(_pcm, _index)) > 0) {
final float[][] pcm = _pcm[0];
// boolean clipflag = false;
final int bout = (samples < this.convsize ? samples : this.convsize);
// convert floats to 16 bit signed ints (host order) and
// interleave
for (int i = 0; i < this.oggInfo.channels; i++) {
int ptr = i * 2;
// int ptr=i;
final int mono = _index[i];
for (int j = 0; j < bout; j++) {
int val = (int) (pcm[i][mono + j] * 32767.);
// might as well guard against clipping
if (val > 32767) {
val = 32767;
}
if (val < -32768) {
val = -32768;
}
if (val < 0) {
val = val | 0x8000;
}
if (this.bigEndian) {
this.convbuffer[ptr] = (byte) (val >>> 8);
this.convbuffer[ptr + 1] = (byte) (val);
}
else {
this.convbuffer[ptr] = (byte) (val);
this.convbuffer[ptr + 1] = (byte) (val >>> 8);
}
ptr += 2 * (this.oggInfo.channels);
}
}
final int bytesToWrite = 2 * this.oggInfo.channels * bout;
if ((this.outIndex + bytesToWrite) > this.outBuffer.length) {
throw new GdxRuntimeException("Ogg block too big to be buffered: "
+ bytesToWrite + ", " + (this.outBuffer.length - this.outIndex));
}
else {
System.arraycopy(this.convbuffer, 0, this.outBuffer, this.outIndex,
bytesToWrite);
this.outIndex += bytesToWrite;
}
wrote = true;
this.dspState.synthesis_read(bout); // tell libvorbis how
// many samples we
// actually consumed
}
}
}
if (this.page.eos() != 0) {
this.endOfBitStream = true;
}
if ((!this.endOfBitStream) && (wrote)) {
return;
}
}
}
if (!this.endOfBitStream) {
this.bytes = 0;
final int index = this.syncState.buffer(BUFFER_SIZE);
if (index >= 0) {
this.buffer = this.syncState.data;
try {
this.bytes = this.input.read(this.buffer, index, BUFFER_SIZE);
}
catch (final Exception e) {
throw new GdxRuntimeException("Error during Vorbis decoding.", e);
}
}
else {
this.bytes = 0;
}
this.syncState.wrote(this.bytes);
if (this.bytes == 0) {
this.endOfBitStream = true;
}
}
}
// clean up this logical bitstream; before exit we see if we're
// followed by another [chained]
this.streamState.clear();
// ogg_page and ogg_packet structs always point to storage in
// libvorbis. They're never freed or manipulated directly
this.vorbisBlock.clear();
this.dspState.clear();
this.oggInfo.clear(); // must be called last
}
// OK, clean up the framer
this.syncState.clear();
this.endOfStream = true;
}
@Override
public int read() {
if (this.readIndex >= this.outIndex) {
this.outIndex = 0;
readPCM();
this.readIndex = 0;
if (this.outIndex == 0) {
return -1;
}
}
int value = this.outBuffer[this.readIndex];
if (value < 0) {
value = 256 + value;
}
this.readIndex++;
return value;
}
public boolean atEnd() {
return this.endOfStream && (this.readIndex >= this.outIndex);
}
@Override
public int read(final byte[] b, final int off, final int len) {
for (int i = 0; i < len; i++) {
final int value = read();
if (value >= 0) {
b[i] = (byte) value;
}
else {
if (i == 0) {
return -1;
}
return i;
}
}
return len;
}
@Override
public int read(final byte[] b) {
return read(b, 0, b.length);
}
@Override
public void close() {
StreamUtils.closeQuietly(this.input);
}
}

View File

@ -0,0 +1,477 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import static org.lwjgl.openal.AL10.AL_BUFFER;
import static org.lwjgl.openal.AL10.AL_NO_ERROR;
import static org.lwjgl.openal.AL10.AL_ORIENTATION;
import static org.lwjgl.openal.AL10.AL_PAUSED;
import static org.lwjgl.openal.AL10.AL_PLAYING;
import static org.lwjgl.openal.AL10.AL_POSITION;
import static org.lwjgl.openal.AL10.AL_SOURCE_STATE;
import static org.lwjgl.openal.AL10.AL_STOPPED;
import static org.lwjgl.openal.AL10.AL_VELOCITY;
import static org.lwjgl.openal.AL10.alDeleteSources;
import static org.lwjgl.openal.AL10.alGenSources;
import static org.lwjgl.openal.AL10.alGetError;
import static org.lwjgl.openal.AL10.alGetSourcei;
import static org.lwjgl.openal.AL10.alListener;
import static org.lwjgl.openal.AL10.alSourcePause;
import static org.lwjgl.openal.AL10.alSourcePlay;
import static org.lwjgl.openal.AL10.alSourceStop;
import static org.lwjgl.openal.AL10.alSourcei;
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import com.badlogic.gdx.Audio;
import com.badlogic.gdx.audio.AudioDevice;
import com.badlogic.gdx.audio.AudioRecorder;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.LongMap;
import com.badlogic.gdx.utils.ObjectMap;
/** @author Nathan Sweet */
public class OpenALAudio implements Audio {
private final int deviceBufferSize;
private final int deviceBufferCount;
private IntArray idleSources, allSources;
private LongMap<Integer> soundIdToSource;
private IntMap<Long> sourceToSoundId;
private long nextSoundId = 0;
private final ObjectMap<String, Class<? extends OpenALSound>> extensionToSoundClass = new ObjectMap();
private final ObjectMap<String, Class<? extends OpenALMusic>> extensionToMusicClass = new ObjectMap();
private OpenALSound[] recentSounds;
private int mostRecetSound = -1;
Array<OpenALMusic> music = new Array(false, 1, OpenALMusic.class);
boolean noDevice = false;
public OpenALAudio() {
this(16, 9, 512);
}
public OpenALAudio(final int simultaneousSources, final int deviceBufferCount, final int deviceBufferSize) {
this.deviceBufferSize = deviceBufferSize;
this.deviceBufferCount = deviceBufferCount;
registerSound("ogg", Ogg.Sound.class);
registerMusic("ogg", Ogg.Music.class);
registerSound("wav", Wav.Sound.class);
registerMusic("wav", Wav.Music.class);
registerSound("mp3", Mp3.Sound.class);
registerMusic("mp3", Mp3.Music.class);
try {
AL.create();
}
catch (final LWJGLException ex) {
this.noDevice = true;
ex.printStackTrace();
return;
}
this.allSources = new IntArray(false, simultaneousSources);
for (int i = 0; i < simultaneousSources; i++) {
final int sourceID = alGenSources();
if (alGetError() != AL_NO_ERROR) {
break;
}
this.allSources.add(sourceID);
}
this.idleSources = new IntArray(this.allSources);
this.soundIdToSource = new LongMap<Integer>();
this.sourceToSoundId = new IntMap<Long>();
final FloatBuffer orientation = (FloatBuffer) BufferUtils.createFloatBuffer(6)
.put(new float[] { 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f }).flip();
alListener(AL_ORIENTATION, orientation);
final FloatBuffer velocity = (FloatBuffer) BufferUtils.createFloatBuffer(3)
.put(new float[] { 0.0f, 0.0f, 0.0f }).flip();
alListener(AL_VELOCITY, velocity);
final FloatBuffer position = (FloatBuffer) BufferUtils.createFloatBuffer(3)
.put(new float[] { 0.0f, 0.0f, 0.0f }).flip();
alListener(AL_POSITION, position);
this.recentSounds = new OpenALSound[simultaneousSources];
}
public void registerSound(final String extension, final Class<? extends OpenALSound> soundClass) {
if (extension == null) {
throw new IllegalArgumentException("extension cannot be null.");
}
if (soundClass == null) {
throw new IllegalArgumentException("soundClass cannot be null.");
}
this.extensionToSoundClass.put(extension, soundClass);
}
public void registerMusic(final String extension, final Class<? extends OpenALMusic> musicClass) {
if (extension == null) {
throw new IllegalArgumentException("extension cannot be null.");
}
if (musicClass == null) {
throw new IllegalArgumentException("musicClass cannot be null.");
}
this.extensionToMusicClass.put(extension, musicClass);
}
@Override
public OpenALSound newSound(final FileHandle file) {
if (file == null) {
throw new IllegalArgumentException("file cannot be null.");
}
final Class<? extends OpenALSound> soundClass = this.extensionToSoundClass.get(file.extension().toLowerCase());
if (soundClass == null) {
throw new GdxRuntimeException("Unknown file extension for sound: " + file);
}
try {
return soundClass.getConstructor(new Class[] { OpenALAudio.class, FileHandle.class }).newInstance(this,
file);
}
catch (final Exception ex) {
throw new GdxRuntimeException("Error creating sound " + soundClass.getName() + " for file: " + file, ex);
}
}
@Override
public OpenALMusic newMusic(final FileHandle file) {
if (file == null) {
throw new IllegalArgumentException("file cannot be null.");
}
final Class<? extends OpenALMusic> musicClass = this.extensionToMusicClass.get(file.extension().toLowerCase());
if (musicClass == null) {
throw new GdxRuntimeException("Unknown file extension for music: " + file);
}
try {
return musicClass.getConstructor(new Class[] { OpenALAudio.class, FileHandle.class }).newInstance(this,
file);
}
catch (final Exception ex) {
throw new GdxRuntimeException("Error creating music " + musicClass.getName() + " for file: " + file, ex);
}
}
int obtainSource(final boolean isMusic) {
if (this.noDevice) {
return 0;
}
for (int i = 0, n = this.idleSources.size; i < n; i++) {
final int sourceId = this.idleSources.get(i);
final int state = alGetSourcei(sourceId, AL_SOURCE_STATE);
if ((state != AL_PLAYING) && (state != AL_PAUSED)) {
if (isMusic) {
this.idleSources.removeIndex(i);
}
else {
if (this.sourceToSoundId.containsKey(sourceId)) {
final long soundId = this.sourceToSoundId.get(sourceId);
this.sourceToSoundId.remove(sourceId);
this.soundIdToSource.remove(soundId);
}
final long soundId = this.nextSoundId++;
this.sourceToSoundId.put(sourceId, soundId);
this.soundIdToSource.put(soundId, sourceId);
}
alSourceStop(sourceId);
alSourcei(sourceId, AL_BUFFER, 0);
AL10.alSourcef(sourceId, AL10.AL_GAIN, 1);
AL10.alSourcef(sourceId, AL10.AL_PITCH, 1);
AL10.alSource3f(sourceId, AL10.AL_POSITION, 0, 0, 1f);
return sourceId;
}
}
return -1;
}
void freeSource(final int sourceID) {
if (this.noDevice) {
return;
}
alSourceStop(sourceID);
alSourcei(sourceID, AL_BUFFER, 0);
if (this.sourceToSoundId.containsKey(sourceID)) {
final long soundId = this.sourceToSoundId.remove(sourceID);
this.soundIdToSource.remove(soundId);
}
this.idleSources.add(sourceID);
}
void freeBuffer(final int bufferID) {
if (this.noDevice) {
return;
}
for (int i = 0, n = this.idleSources.size; i < n; i++) {
final int sourceID = this.idleSources.get(i);
if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
if (this.sourceToSoundId.containsKey(sourceID)) {
final long soundId = this.sourceToSoundId.remove(sourceID);
this.soundIdToSource.remove(soundId);
}
alSourceStop(sourceID);
alSourcei(sourceID, AL_BUFFER, 0);
}
}
}
void stopSourcesWithBuffer(final int bufferID) {
if (this.noDevice) {
return;
}
for (int i = 0, n = this.idleSources.size; i < n; i++) {
final int sourceID = this.idleSources.get(i);
if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
if (this.sourceToSoundId.containsKey(sourceID)) {
final long soundId = this.sourceToSoundId.remove(sourceID);
this.soundIdToSource.remove(soundId);
}
alSourceStop(sourceID);
}
}
}
void pauseSourcesWithBuffer(final int bufferID) {
if (this.noDevice) {
return;
}
for (int i = 0, n = this.idleSources.size; i < n; i++) {
final int sourceID = this.idleSources.get(i);
if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
alSourcePause(sourceID);
}
}
}
void resumeSourcesWithBuffer(final int bufferID) {
if (this.noDevice) {
return;
}
for (int i = 0, n = this.idleSources.size; i < n; i++) {
final int sourceID = this.idleSources.get(i);
if (alGetSourcei(sourceID, AL_BUFFER) == bufferID) {
if (alGetSourcei(sourceID, AL_SOURCE_STATE) == AL_PAUSED) {
alSourcePlay(sourceID);
}
}
}
}
public void update() {
if (this.noDevice) {
return;
}
for (int i = 0; i < this.music.size; i++) {
this.music.items[i].update();
}
}
public long getSoundId(final int sourceId) {
if (!this.sourceToSoundId.containsKey(sourceId)) {
return -1;
}
return this.sourceToSoundId.get(sourceId);
}
public void stopSound(final long soundId) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
alSourceStop(sourceId);
}
public void pauseSound(final long soundId) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
alSourcePause(sourceId);
}
public void resumeSound(final long soundId) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
if (alGetSourcei(sourceId, AL_SOURCE_STATE) == AL_PAUSED) {
alSourcePlay(sourceId);
}
}
public void setSoundGain(final long soundId, final float volume) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
AL10.alSourcef(sourceId, AL10.AL_GAIN, volume);
}
public void setSoundLooping(final long soundId, final boolean looping) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
alSourcei(sourceId, AL10.AL_LOOPING, looping ? AL10.AL_TRUE : AL10.AL_FALSE);
}
public void setSoundPitch(final long soundId, final float pitch) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
AL10.alSourcef(sourceId, AL10.AL_PITCH, pitch);
}
public void setSoundPan(final long soundId, final float pan, final float volume) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
AL10.alSource3f(sourceId, AL10.AL_POSITION, MathUtils.cos(((pan - 1) * MathUtils.PI) / 2), 0,
MathUtils.sin(((pan + 1) * MathUtils.PI) / 2));
AL10.alSourcef(sourceId, AL10.AL_GAIN, volume);
}
public void setSoundPosition(final long soundId, final float x, final float y, final float z,
final boolean is3DSound, final float maxDistance, final float refDistance) {
if (!this.soundIdToSource.containsKey(soundId)) {
return;
}
final int sourceId = this.soundIdToSource.get(soundId);
AL10.alSource3f(sourceId, AL10.AL_POSITION, x, y, z);
AL10.alSourcef(sourceId, AL10.AL_MAX_DISTANCE, maxDistance);
AL10.alSourcef(sourceId, AL10.AL_REFERENCE_DISTANCE, refDistance);
AL10.alSourcef(sourceId, AL10.AL_ROLLOFF_FACTOR, 1.0f);
AL10.alSourcei(sourceId, AL10.AL_SOURCE_RELATIVE, is3DSound ? AL10.AL_FALSE : AL10.AL_TRUE);
}
public void dispose() {
if (this.noDevice) {
return;
}
for (int i = 0, n = this.allSources.size; i < n; i++) {
final int sourceID = this.allSources.get(i);
final int state = alGetSourcei(sourceID, AL_SOURCE_STATE);
if (state != AL_STOPPED) {
alSourceStop(sourceID);
}
alDeleteSources(sourceID);
}
this.sourceToSoundId.clear();
this.soundIdToSource.clear();
AL.destroy();
while (AL.isCreated()) {
try {
Thread.sleep(10);
}
catch (final InterruptedException e) {
}
}
}
@Override
public AudioDevice newAudioDevice(final int sampleRate, final boolean isMono) {
if (this.noDevice) {
return new AudioDevice() {
@Override
public void writeSamples(final float[] samples, final int offset, final int numSamples) {
}
@Override
public void writeSamples(final short[] samples, final int offset, final int numSamples) {
}
@Override
public void setVolume(final float volume) {
}
@Override
public boolean isMono() {
return isMono;
}
@Override
public int getLatency() {
return 0;
}
@Override
public void dispose() {
}
};
}
return new OpenALAudioDevice(this, sampleRate, isMono, this.deviceBufferSize, this.deviceBufferCount);
}
@Override
public AudioRecorder newAudioRecorder(final int samplingRate, final boolean isMono) {
if (this.noDevice) {
return new AudioRecorder() {
@Override
public void read(final short[] samples, final int offset, final int numSamples) {
}
@Override
public void dispose() {
}
};
}
return new JavaSoundAudioRecorder(samplingRate, isMono);
}
/**
* Retains a list of the most recently played sounds and stops the sound played
* least recently if necessary for a new sound to play
*/
protected void retain(final OpenALSound sound, final boolean stop) {
// Move the pointer ahead and wrap
this.mostRecetSound++;
this.mostRecetSound %= this.recentSounds.length;
if (stop) {
// Stop the least recent sound (the one we are about to bump off the buffer)
if (this.recentSounds[this.mostRecetSound] != null) {
this.recentSounds[this.mostRecetSound].stop();
}
}
this.recentSounds[this.mostRecetSound] = sound;
}
/** Removes the disposed sound from the least recently played list */
public void forget(final OpenALSound sound) {
for (int i = 0; i < this.recentSounds.length; i++) {
if (this.recentSounds[i] == sound) {
this.recentSounds[i] = null;
}
}
}
}

View File

@ -0,0 +1,265 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import static org.lwjgl.openal.AL10.AL_BUFFERS_PROCESSED;
import static org.lwjgl.openal.AL10.AL_FALSE;
import static org.lwjgl.openal.AL10.AL_FORMAT_MONO16;
import static org.lwjgl.openal.AL10.AL_FORMAT_STEREO16;
import static org.lwjgl.openal.AL10.AL_GAIN;
import static org.lwjgl.openal.AL10.AL_INVALID_VALUE;
import static org.lwjgl.openal.AL10.AL_LOOPING;
import static org.lwjgl.openal.AL10.AL_NO_ERROR;
import static org.lwjgl.openal.AL10.AL_PLAYING;
import static org.lwjgl.openal.AL10.AL_SOURCE_STATE;
import static org.lwjgl.openal.AL10.alBufferData;
import static org.lwjgl.openal.AL10.alDeleteBuffers;
import static org.lwjgl.openal.AL10.alGenBuffers;
import static org.lwjgl.openal.AL10.alGetError;
import static org.lwjgl.openal.AL10.alGetSourcef;
import static org.lwjgl.openal.AL10.alGetSourcei;
import static org.lwjgl.openal.AL10.alSourcePlay;
import static org.lwjgl.openal.AL10.alSourceQueueBuffers;
import static org.lwjgl.openal.AL10.alSourceUnqueueBuffers;
import static org.lwjgl.openal.AL10.alSourcef;
import static org.lwjgl.openal.AL10.alSourcei;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL11;
import com.badlogic.gdx.audio.AudioDevice;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.GdxRuntimeException;
/** @author Nathan Sweet */
public class OpenALAudioDevice implements AudioDevice {
static private final int bytesPerSample = 2;
private final OpenALAudio audio;
private final int channels;
private IntBuffer buffers;
private int sourceID = -1;
private final int format, sampleRate;
private boolean isPlaying;
private float volume = 1;
private float renderedSeconds;
private final float secondsPerBuffer;
private byte[] bytes;
private final int bufferSize;
private final int bufferCount;
private final ByteBuffer tempBuffer;
public OpenALAudioDevice(final OpenALAudio audio, final int sampleRate, final boolean isMono, final int bufferSize,
final int bufferCount) {
this.audio = audio;
this.channels = isMono ? 1 : 2;
this.bufferSize = bufferSize;
this.bufferCount = bufferCount;
this.format = this.channels > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
this.sampleRate = sampleRate;
this.secondsPerBuffer = (float) bufferSize / bytesPerSample / this.channels / sampleRate;
this.tempBuffer = BufferUtils.createByteBuffer(bufferSize);
}
@Override
public void writeSamples(final short[] samples, final int offset, final int numSamples) {
if ((this.bytes == null) || (this.bytes.length < (numSamples * 2))) {
this.bytes = new byte[numSamples * 2];
}
final int end = Math.min(offset + numSamples, samples.length);
for (int i = offset, ii = 0; i < end; i++) {
final short sample = samples[i];
this.bytes[ii++] = (byte) (sample & 0xFF);
this.bytes[ii++] = (byte) ((sample >> 8) & 0xFF);
}
writeSamples(this.bytes, 0, numSamples * 2);
}
@Override
public void writeSamples(final float[] samples, final int offset, final int numSamples) {
if ((this.bytes == null) || (this.bytes.length < (numSamples * 2))) {
this.bytes = new byte[numSamples * 2];
}
final int end = Math.min(offset + numSamples, samples.length);
for (int i = offset, ii = 0; i < end; i++) {
float floatSample = samples[i];
floatSample = MathUtils.clamp(floatSample, -1f, 1f);
final int intSample = (int) (floatSample * 32767);
this.bytes[ii++] = (byte) (intSample & 0xFF);
this.bytes[ii++] = (byte) ((intSample >> 8) & 0xFF);
}
writeSamples(this.bytes, 0, numSamples * 2);
}
public void writeSamples(final byte[] data, int offset, int length) {
if (length < 0) {
throw new IllegalArgumentException("length cannot be < 0.");
}
if (this.sourceID == -1) {
this.sourceID = this.audio.obtainSource(true);
if (this.sourceID == -1) {
return;
}
if (this.buffers == null) {
this.buffers = BufferUtils.createIntBuffer(this.bufferCount);
alGenBuffers(this.buffers);
if (alGetError() != AL_NO_ERROR) {
throw new GdxRuntimeException("Unabe to allocate audio buffers.");
}
}
alSourcei(this.sourceID, AL_LOOPING, AL_FALSE);
alSourcef(this.sourceID, AL_GAIN, this.volume);
// Fill initial buffers.
int queuedBuffers = 0;
for (int i = 0; i < this.bufferCount; i++) {
final int bufferID = this.buffers.get(i);
final int written = Math.min(this.bufferSize, length);
this.tempBuffer.clear();
this.tempBuffer.put(data, offset, written).flip();
alBufferData(bufferID, this.format, this.tempBuffer, this.sampleRate);
alSourceQueueBuffers(this.sourceID, bufferID);
length -= written;
offset += written;
queuedBuffers++;
}
// Queue rest of buffers, empty.
this.tempBuffer.clear().flip();
for (int i = queuedBuffers; i < this.bufferCount; i++) {
final int bufferID = this.buffers.get(i);
alBufferData(bufferID, this.format, this.tempBuffer, this.sampleRate);
alSourceQueueBuffers(this.sourceID, bufferID);
}
alSourcePlay(this.sourceID);
this.isPlaying = true;
}
while (length > 0) {
final int written = fillBuffer(data, offset, length);
length -= written;
offset += written;
}
}
/** Blocks until some of the data could be buffered. */
private int fillBuffer(final byte[] data, final int offset, final int length) {
final int written = Math.min(this.bufferSize, length);
outer: while (true) {
int buffers = alGetSourcei(this.sourceID, AL_BUFFERS_PROCESSED);
while (buffers-- > 0) {
final int bufferID = alSourceUnqueueBuffers(this.sourceID);
if (bufferID == AL_INVALID_VALUE) {
break;
}
this.renderedSeconds += this.secondsPerBuffer;
this.tempBuffer.clear();
this.tempBuffer.put(data, offset, written).flip();
alBufferData(bufferID, this.format, this.tempBuffer, this.sampleRate);
alSourceQueueBuffers(this.sourceID, bufferID);
break outer;
}
// Wait for buffer to be free.
try {
Thread.sleep((long) (1000 * this.secondsPerBuffer));
}
catch (final InterruptedException ignored) {
}
}
// A buffer underflow will cause the source to stop.
if (!this.isPlaying || (alGetSourcei(this.sourceID, AL_SOURCE_STATE) != AL_PLAYING)) {
alSourcePlay(this.sourceID);
this.isPlaying = true;
}
return written;
}
public void stop() {
if (this.sourceID == -1) {
return;
}
this.audio.freeSource(this.sourceID);
this.sourceID = -1;
this.renderedSeconds = 0;
this.isPlaying = false;
}
public boolean isPlaying() {
if (this.sourceID == -1) {
return false;
}
return this.isPlaying;
}
@Override
public void setVolume(final float volume) {
this.volume = volume;
if (this.sourceID != -1) {
alSourcef(this.sourceID, AL_GAIN, volume);
}
}
public float getPosition() {
if (this.sourceID == -1) {
return 0;
}
return this.renderedSeconds + alGetSourcef(this.sourceID, AL11.AL_SEC_OFFSET);
}
public void setPosition(final float position) {
this.renderedSeconds = position;
}
public int getChannels() {
return this.format == AL_FORMAT_STEREO16 ? 2 : 1;
}
public int getRate() {
return this.sampleRate;
}
@Override
public void dispose() {
if (this.buffers == null) {
return;
}
if (this.sourceID != -1) {
this.audio.freeSource(this.sourceID);
this.sourceID = -1;
}
alDeleteBuffers(this.buffers);
this.buffers = null;
}
@Override
public boolean isMono() {
return this.channels == 1;
}
@Override
public int getLatency() {
return (int) (this.secondsPerBuffer * this.bufferCount * 1000);
}
}

View File

@ -0,0 +1,396 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import static org.lwjgl.openal.AL10.AL_BUFFERS_PROCESSED;
import static org.lwjgl.openal.AL10.AL_BUFFERS_QUEUED;
import static org.lwjgl.openal.AL10.AL_FALSE;
import static org.lwjgl.openal.AL10.AL_FORMAT_MONO16;
import static org.lwjgl.openal.AL10.AL_FORMAT_STEREO16;
import static org.lwjgl.openal.AL10.AL_GAIN;
import static org.lwjgl.openal.AL10.AL_INVALID_VALUE;
import static org.lwjgl.openal.AL10.AL_LOOPING;
import static org.lwjgl.openal.AL10.AL_NO_ERROR;
import static org.lwjgl.openal.AL10.AL_PLAYING;
import static org.lwjgl.openal.AL10.AL_POSITION;
import static org.lwjgl.openal.AL10.AL_SOURCE_STATE;
import static org.lwjgl.openal.AL10.alBufferData;
import static org.lwjgl.openal.AL10.alDeleteBuffers;
import static org.lwjgl.openal.AL10.alGenBuffers;
import static org.lwjgl.openal.AL10.alGetError;
import static org.lwjgl.openal.AL10.alGetSourcef;
import static org.lwjgl.openal.AL10.alGetSourcei;
import static org.lwjgl.openal.AL10.alSource3f;
import static org.lwjgl.openal.AL10.alSourcePause;
import static org.lwjgl.openal.AL10.alSourcePlay;
import static org.lwjgl.openal.AL10.alSourceQueueBuffers;
import static org.lwjgl.openal.AL10.alSourceStop;
import static org.lwjgl.openal.AL10.alSourceUnqueueBuffers;
import static org.lwjgl.openal.AL10.alSourcef;
import static org.lwjgl.openal.AL10.alSourcei;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL11;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.GdxRuntimeException;
/** @author Nathan Sweet */
public abstract class OpenALMusic implements Music {
static private final int bufferSize = 4096 * 10;
static private final int bufferCount = 3;
static private final int bytesPerSample = 2;
static private final byte[] tempBytes = new byte[bufferSize];
static private final ByteBuffer tempBuffer = BufferUtils.createByteBuffer(bufferSize);
private final FloatArray renderedSecondsQueue = new FloatArray(bufferCount);
private final OpenALAudio audio;
private IntBuffer buffers;
private int sourceID = -1;
private int format, sampleRate;
private boolean isLooping, isPlaying;
private float volume = 1;
private float pan = 0;
private float renderedSeconds, maxSecondsPerBuffer;
protected final FileHandle file;
protected int bufferOverhead = 0;
private OnCompletionListener onCompletionListener;
public OpenALMusic(final OpenALAudio audio, final FileHandle file) {
this.audio = audio;
this.file = file;
this.onCompletionListener = null;
}
protected void setup(final int channels, final int sampleRate) {
this.format = channels > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
this.sampleRate = sampleRate;
this.maxSecondsPerBuffer = (float) (bufferSize - this.bufferOverhead)
/ (bytesPerSample * channels * sampleRate);
}
@Override
public void play() {
if (this.audio.noDevice) {
return;
}
if (this.sourceID == -1) {
this.sourceID = this.audio.obtainSource(true);
if (this.sourceID == -1) {
return;
}
this.audio.music.add(this);
if (this.buffers == null) {
this.buffers = BufferUtils.createIntBuffer(bufferCount);
alGenBuffers(this.buffers);
final int errorCode = alGetError();
if (errorCode != AL_NO_ERROR) {
throw new GdxRuntimeException("Unable to allocate audio buffers. AL Error: " + errorCode);
}
}
alSourcei(this.sourceID, AL_LOOPING, AL_FALSE);
setPan(this.pan, this.volume);
boolean filled = false; // Check if there's anything to actually play.
for (int i = 0; i < bufferCount; i++) {
final int bufferID = this.buffers.get(i);
if (!fill(bufferID)) {
break;
}
filled = true;
alSourceQueueBuffers(this.sourceID, bufferID);
}
if (!filled && (this.onCompletionListener != null)) {
this.onCompletionListener.onCompletion(this);
}
if (alGetError() != AL_NO_ERROR) {
stop();
return;
}
}
if (!this.isPlaying) {
alSourcePlay(this.sourceID);
this.isPlaying = true;
}
}
@Override
public void stop() {
if (this.audio.noDevice) {
return;
}
if (this.sourceID == -1) {
return;
}
this.audio.music.removeValue(this, true);
reset();
this.audio.freeSource(this.sourceID);
this.sourceID = -1;
this.renderedSeconds = 0;
this.renderedSecondsQueue.clear();
this.isPlaying = false;
}
@Override
public void pause() {
if (this.audio.noDevice) {
return;
}
if (this.sourceID != -1) {
alSourcePause(this.sourceID);
}
this.isPlaying = false;
}
@Override
public boolean isPlaying() {
if (this.audio.noDevice) {
return false;
}
if (this.sourceID == -1) {
return false;
}
return this.isPlaying;
}
@Override
public void setLooping(final boolean isLooping) {
this.isLooping = isLooping;
}
@Override
public boolean isLooping() {
return this.isLooping;
}
@Override
public void setVolume(final float volume) {
this.volume = volume;
if (this.audio.noDevice) {
return;
}
if (this.sourceID != -1) {
alSourcef(this.sourceID, AL_GAIN, volume);
}
}
@Override
public float getVolume() {
return this.volume;
}
@Override
public void setPan(final float pan, final float volume) {
this.volume = volume;
this.pan = pan;
if (this.audio.noDevice) {
return;
}
if (this.sourceID == -1) {
return;
}
alSource3f(this.sourceID, AL_POSITION, MathUtils.cos(((pan - 1) * MathUtils.PI) / 2), 0,
MathUtils.sin(((pan + 1) * MathUtils.PI) / 2));
alSourcef(this.sourceID, AL_GAIN, volume);
}
@Override
public void setPosition(final float position) {
if (this.audio.noDevice) {
return;
}
if (this.sourceID == -1) {
return;
}
final boolean wasPlaying = this.isPlaying;
this.isPlaying = false;
alSourceStop(this.sourceID);
alSourceUnqueueBuffers(this.sourceID, this.buffers);
while (this.renderedSecondsQueue.size > 0) {
this.renderedSeconds = this.renderedSecondsQueue.pop();
}
if (position <= this.renderedSeconds) {
reset();
this.renderedSeconds = 0;
}
while (this.renderedSeconds < (position - this.maxSecondsPerBuffer)) {
if (read(tempBytes) <= 0) {
break;
}
this.renderedSeconds += this.maxSecondsPerBuffer;
}
this.renderedSecondsQueue.add(this.renderedSeconds);
boolean filled = false;
for (int i = 0; i < bufferCount; i++) {
final int bufferID = this.buffers.get(i);
if (!fill(bufferID)) {
break;
}
filled = true;
alSourceQueueBuffers(this.sourceID, bufferID);
}
this.renderedSecondsQueue.pop();
if (!filled) {
stop();
if (this.onCompletionListener != null) {
this.onCompletionListener.onCompletion(this);
}
}
alSourcef(this.sourceID, AL11.AL_SEC_OFFSET, position - this.renderedSeconds);
if (wasPlaying) {
alSourcePlay(this.sourceID);
this.isPlaying = true;
}
}
@Override
public float getPosition() {
if (this.audio.noDevice) {
return 0;
}
if (this.sourceID == -1) {
return 0;
}
return this.renderedSeconds + alGetSourcef(this.sourceID, AL11.AL_SEC_OFFSET);
}
/**
* Fills as much of the buffer as possible and returns the number of bytes
* filled. Returns <= 0 to indicate the end of the stream.
*/
abstract public int read(byte[] buffer);
/** Resets the stream to the beginning. */
abstract public void reset();
/**
* By default, does just the same as reset(). Used to add special behaviour in
* Ogg.Music.
*/
protected void loop() {
reset();
}
public int getChannels() {
return this.format == AL_FORMAT_STEREO16 ? 2 : 1;
}
public int getRate() {
return this.sampleRate;
}
public void update() {
if (this.audio.noDevice) {
return;
}
if (this.sourceID == -1) {
return;
}
boolean end = false;
int buffers = alGetSourcei(this.sourceID, AL_BUFFERS_PROCESSED);
while (buffers-- > 0) {
final int bufferID = alSourceUnqueueBuffers(this.sourceID);
if (bufferID == AL_INVALID_VALUE) {
break;
}
this.renderedSeconds = this.renderedSecondsQueue.pop();
if (end) {
continue;
}
if (fill(bufferID)) {
alSourceQueueBuffers(this.sourceID, bufferID);
}
else {
end = true;
}
}
if (end && (alGetSourcei(this.sourceID, AL_BUFFERS_QUEUED) == 0)) {
stop();
if (this.onCompletionListener != null) {
this.onCompletionListener.onCompletion(this);
}
}
// A buffer underflow will cause the source to stop.
if (this.isPlaying && (alGetSourcei(this.sourceID, AL_SOURCE_STATE) != AL_PLAYING)) {
alSourcePlay(this.sourceID);
}
}
private boolean fill(final int bufferID) {
tempBuffer.clear();
int length = read(tempBytes);
if (length <= 0) {
if (this.isLooping) {
loop();
length = read(tempBytes);
if (length <= 0) {
return false;
}
if (this.renderedSecondsQueue.size > 0) {
this.renderedSecondsQueue.set(0, 0);
}
}
else {
return false;
}
}
final float previousLoadedSeconds = this.renderedSecondsQueue.size > 0 ? this.renderedSecondsQueue.first() : 0;
final float currentBufferSeconds = (this.maxSecondsPerBuffer * length) / bufferSize;
this.renderedSecondsQueue.insert(0, previousLoadedSeconds + currentBufferSeconds);
tempBuffer.put(tempBytes, 0, length).flip();
alBufferData(bufferID, this.format, tempBuffer, this.sampleRate);
return true;
}
@Override
public void dispose() {
stop();
if (this.audio.noDevice) {
return;
}
if (this.buffers == null) {
return;
}
alDeleteBuffers(this.buffers);
this.buffers = null;
this.onCompletionListener = null;
}
@Override
public void setOnCompletionListener(final OnCompletionListener listener) {
this.onCompletionListener = listener;
}
public int getSourceId() {
return this.sourceID;
}
}

View File

@ -0,0 +1,249 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import static org.lwjgl.openal.AL10.AL_BUFFER;
import static org.lwjgl.openal.AL10.AL_FALSE;
import static org.lwjgl.openal.AL10.AL_FORMAT_MONO16;
import static org.lwjgl.openal.AL10.AL_FORMAT_STEREO16;
import static org.lwjgl.openal.AL10.AL_GAIN;
import static org.lwjgl.openal.AL10.AL_LOOPING;
import static org.lwjgl.openal.AL10.AL_TRUE;
import static org.lwjgl.openal.AL10.alBufferData;
import static org.lwjgl.openal.AL10.alDeleteBuffers;
import static org.lwjgl.openal.AL10.alGenBuffers;
import static org.lwjgl.openal.AL10.alSourcePlay;
import static org.lwjgl.openal.AL10.alSourcef;
import static org.lwjgl.openal.AL10.alSourcei;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.badlogic.gdx.audio.Sound;
/** @author Nathan Sweet */
public class OpenALSound implements Sound {
private int bufferID = -1;
private final OpenALAudio audio;
private float duration;
public OpenALSound(final OpenALAudio audio) {
this.audio = audio;
}
void setup(final byte[] pcm, final int channels, final int sampleRate) {
final int bytes = pcm.length - (pcm.length % (channels > 1 ? 4 : 2));
final int samples = bytes / (2 * channels);
this.duration = samples / (float) sampleRate;
final ByteBuffer buffer = ByteBuffer.allocateDirect(bytes);
buffer.order(ByteOrder.nativeOrder());
buffer.put(pcm, 0, bytes);
buffer.flip();
if (this.bufferID == -1) {
this.bufferID = alGenBuffers();
alBufferData(this.bufferID, channels > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16, buffer.asShortBuffer(),
sampleRate);
}
}
@Override
public long play() {
return play(1);
}
@Override
public long play(final float volume) {
if (this.audio.noDevice) {
return 0;
}
int sourceID = this.audio.obtainSource(false);
if (sourceID == -1) {
// Attempt to recover by stopping the least recently played sound
this.audio.retain(this, true);
sourceID = this.audio.obtainSource(false);
}
else {
this.audio.retain(this, false);
}
// In case it still didn't work
if (sourceID == -1) {
return -1;
}
final long soundId = this.audio.getSoundId(sourceID);
alSourcei(sourceID, AL_BUFFER, this.bufferID);
alSourcei(sourceID, AL_LOOPING, AL_FALSE);
alSourcef(sourceID, AL_GAIN, volume);
alSourcePlay(sourceID);
return soundId;
}
@Override
public long loop() {
return loop(1);
}
@Override
public long loop(final float volume) {
if (this.audio.noDevice) {
return 0;
}
final int sourceID = this.audio.obtainSource(false);
if (sourceID == -1) {
return -1;
}
final long soundId = this.audio.getSoundId(sourceID);
alSourcei(sourceID, AL_BUFFER, this.bufferID);
alSourcei(sourceID, AL_LOOPING, AL_TRUE);
alSourcef(sourceID, AL_GAIN, volume);
alSourcePlay(sourceID);
return soundId;
}
@Override
public void stop() {
if (this.audio.noDevice) {
return;
}
this.audio.stopSourcesWithBuffer(this.bufferID);
}
@Override
public void dispose() {
if (this.audio.noDevice) {
return;
}
if (this.bufferID == -1) {
return;
}
this.audio.freeBuffer(this.bufferID);
alDeleteBuffers(this.bufferID);
this.bufferID = -1;
this.audio.forget(this);
}
@Override
public void stop(final long soundId) {
if (this.audio.noDevice) {
return;
}
this.audio.stopSound(soundId);
}
@Override
public void pause() {
if (this.audio.noDevice) {
return;
}
this.audio.pauseSourcesWithBuffer(this.bufferID);
}
@Override
public void pause(final long soundId) {
if (this.audio.noDevice) {
return;
}
this.audio.pauseSound(soundId);
}
@Override
public void resume() {
if (this.audio.noDevice) {
return;
}
this.audio.resumeSourcesWithBuffer(this.bufferID);
}
@Override
public void resume(final long soundId) {
if (this.audio.noDevice) {
return;
}
this.audio.resumeSound(soundId);
}
@Override
public void setPitch(final long soundId, final float pitch) {
if (this.audio.noDevice) {
return;
}
this.audio.setSoundPitch(soundId, pitch);
}
@Override
public void setVolume(final long soundId, final float volume) {
if (this.audio.noDevice) {
return;
}
this.audio.setSoundGain(soundId, volume);
}
@Override
public void setLooping(final long soundId, final boolean looping) {
if (this.audio.noDevice) {
return;
}
this.audio.setSoundLooping(soundId, looping);
}
@Override
public void setPan(final long soundId, final float pan, final float volume) {
if (this.audio.noDevice) {
return;
}
this.audio.setSoundPan(soundId, pan, volume);
}
public void setPosition(final long soundId, final float x, final float y, final float z, final boolean is3DSound,
final float maxDistance, final float refDistance) {
if (this.audio.noDevice) {
return;
}
this.audio.setSoundPosition(soundId, x, y, z, is3DSound, maxDistance, refDistance);
}
@Override
public long play(final float volume, final float pitch, final float pan) {
final long id = play();
setPitch(id, pitch);
setPan(id, pan, volume);
return id;
}
public long play(final float volume, final float pitch, final float x, final float y, final float z,
final boolean is3DSound, final float maxDistance, final float refDistance) {
final long id = play();
setPitch(id, pitch);
setVolume(id, volume);
setPosition(id, x, y, z, is3DSound, maxDistance, refDistance);
return id;
}
@Override
public long loop(final float volume, final float pitch, final float pan) {
final long id = loop();
setPitch(id, pitch);
setPan(id, pan, volume);
return id;
}
/** Returns the length of the sound in seconds. */
public float duration() {
return this.duration;
}
}

View File

@ -0,0 +1,196 @@
/*******************************************************************************
* Copyright 2011 See AUTHORS file.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.etheller.warsmash.audio;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.StreamUtils;
public class Wav {
static public class Music extends OpenALMusic {
private WavInputStream input;
public Music(final OpenALAudio audio, final FileHandle file) {
super(audio, file);
this.input = new WavInputStream(file);
if (audio.noDevice) {
return;
}
setup(this.input.channels, this.input.sampleRate);
}
@Override
public int read(final byte[] buffer) {
if (this.input == null) {
this.input = new WavInputStream(this.file);
setup(this.input.channels, this.input.sampleRate);
}
try {
return this.input.read(buffer);
}
catch (final IOException ex) {
throw new GdxRuntimeException("Error reading WAV file: " + this.file, ex);
}
}
@Override
public void reset() {
StreamUtils.closeQuietly(this.input);
this.input = null;
}
}
static public class Sound extends OpenALSound {
public Sound(final OpenALAudio audio, final FileHandle file) {
super(audio);
if (audio.noDevice) {
return;
}
WavInputStream input = null;
try {
input = new WavInputStream(file);
setup(StreamUtils.copyStreamToByteArray(input, input.dataRemaining), input.channels, input.sampleRate);
}
catch (final IOException ex) {
throw new GdxRuntimeException("Error reading WAV file: " + file, ex);
}
finally {
StreamUtils.closeQuietly(input);
}
}
}
/** @author Nathan Sweet */
static public class WavInputStream extends FilterInputStream {
public int channels, sampleRate, dataRemaining;
public WavInputStream(final FileHandle file) {
super(file.read());
try {
if ((read() != 'R') || (read() != 'I') || (read() != 'F') || (read() != 'F')) {
throw new GdxRuntimeException("RIFF header not found: " + file);
}
skipFully(4);
if ((read() != 'W') || (read() != 'A') || (read() != 'V') || (read() != 'E')) {
throw new GdxRuntimeException("Invalid wave file header: " + file);
}
final int fmtChunkLength = seekToChunk('f', 'm', 't', ' ');
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
// http://soundfile.sapp.org/doc/WaveFormat/
final int type = (read() & 0xff) | ((read() & 0xff) << 8);
if (type != 1) {
String name;
switch (type) {
case 0x0002:
name = "ADPCM";
break;
case 0x0003:
name = "IEEE float";
break;
case 0x0006:
name = "8-bit ITU-T G.711 A-law";
break;
case 0x0007:
name = "8-bit ITU-T G.711 u-law";
break;
case 0xFFFE:
name = "Extensible";
break;
default:
name = "Unknown";
}
throw new GdxRuntimeException(
"WAV files must be PCM, unsupported format: " + name + " (" + type + ")");
}
this.channels = (read() & 0xff) | ((read() & 0xff) << 8);
if ((this.channels != 1) && (this.channels != 2)) {
throw new GdxRuntimeException("WAV files must have 1 or 2 channels: " + this.channels);
}
this.sampleRate = (read() & 0xff) | ((read() & 0xff) << 8) | ((read() & 0xff) << 16)
| ((read() & 0xff) << 24);
skipFully(6);
final int bitsPerSample = (read() & 0xff) | ((read() & 0xff) << 8);
if (bitsPerSample != 16) {
throw new GdxRuntimeException("WAV files must have 16 bits per sample: " + bitsPerSample);
}
skipFully(fmtChunkLength - 16);
this.dataRemaining = seekToChunk('d', 'a', 't', 'a');
}
catch (final Throwable ex) {
StreamUtils.closeQuietly(this);
throw new GdxRuntimeException("Error reading WAV file: " + file, ex);
}
}
private int seekToChunk(final char c1, final char c2, final char c3, final char c4) throws IOException {
while (true) {
boolean found = read() == c1;
found &= read() == c2;
found &= read() == c3;
found &= read() == c4;
final int chunkLength = (read() & 0xff) | ((read() & 0xff) << 8) | ((read() & 0xff) << 16)
| ((read() & 0xff) << 24);
if (chunkLength == -1) {
throw new IOException("Chunk not found: " + c1 + c2 + c3 + c4);
}
if (found) {
return chunkLength;
}
skipFully(chunkLength);
}
}
private void skipFully(int count) throws IOException {
while (count > 0) {
final long skipped = this.in.skip(count);
if (skipped <= 0) {
throw new EOFException("Unable to skip.");
}
count -= skipped;
}
}
@Override
public int read(final byte[] buffer) throws IOException {
if (this.dataRemaining == 0) {
return -1;
}
final int length = Math.min(super.read(buffer), this.dataRemaining);
if (length == -1) {
return -1;
}
this.dataRemaining -= length;
return length;
}
}
}

View File

@ -1,9 +1,15 @@
package com.etheller.warsmash.desktop;
import static org.lwjgl.openal.AL10.AL_ORIENTATION;
import static org.lwjgl.openal.AL10.AL_POSITION;
import static org.lwjgl.openal.AL10.alListener;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL32;
@ -13,20 +19,22 @@ import com.badlogic.gdx.Graphics.DisplayMode;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.gdx.backends.lwjgl.audio.OpenALSound;
import com.badlogic.gdx.backends.lwjgl.LwjglNativesLoader;
import com.etheller.warsmash.WarsmashGdxMapGame;
import com.etheller.warsmash.audio.OpenALSound;
import com.etheller.warsmash.units.DataTable;
import com.etheller.warsmash.util.StringBundle;
import com.etheller.warsmash.viewer5.AudioContext;
import com.etheller.warsmash.viewer5.AudioContext.Listener;
import com.etheller.warsmash.viewer5.AudioDestination;
import com.etheller.warsmash.viewer5.gl.ANGLEInstancedArrays;
import com.etheller.warsmash.viewer5.gl.AudioExtension;
import com.etheller.warsmash.viewer5.gl.DynamicShadowExtension;
import com.etheller.warsmash.viewer5.gl.Extensions;
import com.etheller.warsmash.viewer5.gl.SoundLengthExtension;
import com.etheller.warsmash.viewer5.gl.WireframeExtension;
public class DesktopLauncher {
public static void main(final String[] arg) {
loadExtensions();
final DataTable warsmashIni = loadWarsmashIni();
final LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.useGL30 = true;
config.gles30ContextMajorVersion = 3;
@ -44,6 +52,8 @@ public class DesktopLauncher {
else {
config.fullscreen = true;
}
loadExtensions();
final DataTable warsmashIni = loadWarsmashIni();
new LwjglApplication(new WarsmashGdxMapGame(warsmashIni), config);
}
@ -62,6 +72,7 @@ public class DesktopLauncher {
}
public static void loadExtensions() {
LwjglNativesLoader.load();
Extensions.angleInstancedArrays = new ANGLEInstancedArrays() {
@Override
public void glVertexAttribDivisorANGLE(final int index, final int divisor) {
@ -98,7 +109,10 @@ public class DesktopLauncher {
GL11.glPolygonMode(face, mode);
}
};
Extensions.soundLengthExtension = new SoundLengthExtension() {
Extensions.audio = new AudioExtension() {
final FloatBuffer orientation = (FloatBuffer) BufferUtils.createFloatBuffer(6).clear();
final FloatBuffer position = (FloatBuffer) BufferUtils.createFloatBuffer(3).clear();
@Override
public float getDuration(final Sound sound) {
if (sound == null) {
@ -106,6 +120,73 @@ public class DesktopLauncher {
}
return ((OpenALSound) sound).duration();
}
@Override
public void play(final Sound buffer, final float volume, final float pitch, final float x, final float y,
final float z, final boolean is3dSound, final float maxDistance, final float refDistance) {
((OpenALSound) buffer).play(volume, pitch, x, y, z, is3dSound, maxDistance, refDistance);
}
@Override
public AudioContext createContext(final boolean world) {
Listener listener;
if (world) {
listener = new Listener() {
private float x;
private float y;
private float z;
@Override
public void setPosition(final float x, final float y, final float z) {
this.x = x;
this.y = y;
this.z = z;
position.put(0, x);
position.put(1, y);
position.put(2, z);
alListener(AL_POSITION, position);
}
@Override
public float getX() {
return this.x;
}
@Override
public float getY() {
return this.y;
}
@Override
public float getZ() {
return this.z;
}
@Override
public void setOrientation(final float forwardX, final float forwardY, final float forwardZ,
final float upX, final float upY, final float upZ) {
orientation.put(0, forwardX);
orientation.put(1, forwardY);
orientation.put(2, forwardZ);
orientation.put(3, upX);
orientation.put(4, upY);
orientation.put(5, upZ);
alListener(AL_ORIENTATION, orientation);
}
@Override
public boolean is3DSupported() {
return true;
}
};
}
else {
listener = Listener.DO_NOTHING;
}
return new AudioContext(listener, new AudioDestination() {
});
}
};
Extensions.GL_LINE = GL11.GL_LINE;
Extensions.GL_FILL = GL11.GL_FILL;