annotate README @ 60:42bbb176b90f

first pass at wiki page
author Robert McIntyre <rlm@mit.edu>
date Sat, 03 Dec 2011 23:06:01 -0600
parents 5afa49c5a7d3
children 76581e11fb72
rev   line source
rlm@59 1 ======Capture Audio/Video to a File======
rlm@59 2
rlm@59 3 So you've made your cool new JMonkeyEngine3 game and you want to
rlm@59 4 create a demo video to show off your hard work. Or maybe you want to
rlm@59 5 make a cutscene for your game using the physics and characters in the
rlm@59 6 game itself. Screen capturing is the most straightforward way to do
rlm@59 7 this, but it can slow down your game and produce low-quality video and
rlm@59 8 audio as a result. A better way is to record video and audio directly
rlm@59 9 from the game while it is running.
rlm@59 10
rlm@60 11
rlm@60 12 ===== Simple Way =====
rlm@60 13
rlm@60 14 If all you just want to record video at 30fps with no sound, then look
rlm@60 15 no further then jMonkeyEngine3's build in ''VideoRecorderAppState''
rlm@60 16 class.
rlm@60 17
rlm@60 18 Add the following code to your simpleInitApp() method.
rlm@60 19
rlm@60 20 <code java>
rlm@60 21 stateManager.attach(new VideoRecorderAppState()); //start recording
rlm@60 22 </code>
rlm@60 23
rlm@60 24 The game will run slow, but the recording will be in high-quality and
rlm@60 25 normal speed. The video files will be stored in your user home
rlm@60 26 directory, if you want to save to another file, specify it in the
rlm@60 27 VideoRecorderAppState constructor. Recording starts when the state is
rlm@60 28 attached and ends when the application quits or the state is detached.
rlm@60 29
rlm@60 30 That's all!
rlm@60 31
rlm@60 32 ===== Advanced Way =====
rlm@60 33
rlm@60 34 If you want to record audio as well, record at different framerates,
rlm@60 35 or record from multiple viewpoints at once, then there's a full
rlm@60 36 solution for doing this already made for you here:
rlm@59 37
rlm@59 38 http://www.aurellem.com/releases/jmeCapture-latest.zip
rlm@59 39 http://www.aurellem.com/releases/jmeCapture-latest.tar.bz2
rlm@59 40
rlm@59 41 Download the archive in your preferred format, extract,
rlm@59 42 add the jars to your project, and you are ready to go.
rlm@59 43
rlm@59 44 The javadoc is here:
rlm@59 45 http://www.aurellem.com/jmeCapture/docs/
rlm@59 46
rlm@60 47 To capture video and audio you use the
rlm@59 48 ''com.aurellem.capture.Capture'' class, which has two methods,
rlm@59 49 ''captureAudio'' and ''captureVideo'', and the
rlm@59 50 ''com.aurellem.capture.IsoTimer class'', which sets the audio and
rlm@60 51 video framerate.
rlm@59 52
rlm@59 53 The steps are as simple as:
rlm@59 54
rlm@59 55 <code java>
rlm@59 56 yourApp.setTimer(new IsoTimer(desiredFramesPerSecond));
rlm@59 57 </code>
rlm@59 58
rlm@59 59 This causes jMonkeyEngine to take as much time as it needs to fully
rlm@59 60 calculate every frame of the video and audio. You will see your game
rlm@59 61 speed up and slow down depending on how computationally demanding your
rlm@59 62 game is, but the final recorded audio and video will be perfectly
rlm@59 63 sychronized and will run at exactly the fps which you specified.
rlm@59 64
rlm@59 65 <code java>
rlm@59 66 captureVideo(yourApp, targetVideoFile);
rlm@59 67 captureAudio(yourApp, targetAudioFile);
rlm@59 68 </code>
rlm@59 69
rlm@59 70 These will cause the app to record audio and video when it is run.
rlm@59 71 Audio and video will stop being recorded when the app stops. Your
rlm@59 72 audio will be recorded as a 44,100 Hz linear PCM wav file, while the
rlm@59 73 video will be recorded according to the following rules:
rlm@59 74
rlm@59 75 1.) (Preferred) If you supply an empty directory as the file, then
rlm@59 76 the video will be saved as a sequence of .png files, one file per
rlm@59 77 frame. The files start at 0000000.png and increment from there.
rlm@59 78 You can then combine the frames into your preferred
rlm@59 79 container/codec. If the directory is not empty, then writing
rlm@59 80 video frames to it will fail, and nothing will be written.
rlm@59 81
rlm@59 82 2.) If the filename ends in ".avi" then the frames will be encoded as
rlm@59 83 a RAW stream inside an AVI 1.0 container. The resulting file
rlm@59 84 will be quite large and you will probably want to re-encode it to
rlm@59 85 your preferred container/codec format. Be advised that some
rlm@59 86 video payers cannot process AVI with a RAW stream, and that AVI
rlm@59 87 1.0 files generated by this method that exceed 2.0GB are invalid
rlm@59 88 according to the AVI 1.0 spec (but many programs can still deal
rlm@59 89 with them.) Thanks to Werner Randelshofer for his excellent work
rlm@60 90 which made the AVI file writer option possible.
rlm@59 91
rlm@59 92 3.) Any non-directory file ending in anything other than ".avi" will
rlm@59 93 be processed through Xuggle. Xuggle provides the option to use
rlm@59 94 many codecs/containers, but you will have to install it on your
rlm@59 95 system yourself in order to use this option. Please visit
rlm@59 96 http://www.xuggle.com/ to learn how to do this.
rlm@59 97
rlm@60 98 Note that you will not hear any sound if you choose to record sound to
rlm@60 99 a file.
rlm@60 100
rlm@60 101 ==== Basic Example ====
rlm@60 102
rlm@60 103 Here is a complete example showing how to capture both audio and video
rlm@60 104 from one of jMonkeyEngine3's advanced demo applications.
rlm@60 105
rlm@60 106 <code java>
rlm@60 107 import java.io.File;
rlm@60 108 import java.io.IOException;
rlm@60 109
rlm@60 110 import jme3test.water.TestPostWater;
rlm@60 111
rlm@60 112 import com.aurellem.capture.Capture;
rlm@60 113 import com.aurellem.capture.IsoTimer;
rlm@60 114 import com.jme3.app.SimpleApplication;
rlm@60 115
rlm@60 116
rlm@60 117 /**
rlm@60 118 * Demonstrates how to use basic Audio/Video capture with a
rlm@60 119 * jMonkeyEngine application. You can use these techniques to make
rlm@60 120 * high quality cutscenes or demo videos, even on very slow laptops.
rlm@60 121 *
rlm@60 122 * @author Robert McIntyre
rlm@60 123 */
rlm@60 124
rlm@60 125 public class Basic {
rlm@60 126
rlm@60 127 public static void main(String[] ignore) throws IOException{
rlm@60 128 File video = File.createTempFile("JME-water-video", ".avi");
rlm@60 129 File audio = File.createTempFile("JME-water-audio", ".wav");
rlm@60 130
rlm@60 131 SimpleApplication app = new TestPostWater();
rlm@60 132 app.setTimer(new IsoTimer(60));
rlm@60 133 app.setShowSettings(false);
rlm@60 134
rlm@60 135 Capture.captureVideo(app, video);
rlm@60 136 Capture.captureAudio(app, audio);
rlm@60 137
rlm@60 138 app.start();
rlm@60 139
rlm@60 140 System.out.println(video.getCanonicalPath());
rlm@60 141 System.out.println(audio.getCanonicalPath());
rlm@60 142 }
rlm@60 143 }
rlm@60 144 </code>
rlm@60 145
rlm@60 146 ==== How it works ====
rlm@60 147
rlm@60 148 A standard JME3 application that extends =SimpleApplication= or
rlm@60 149 =Application= tries as hard as it can to keep in sync with
rlm@60 150 /user-time/. If a ball is rolling at 1 game-mile per game-hour in the
rlm@60 151 game, and you wait for one user-hour as measured by the clock on your
rlm@60 152 wall, then the ball should have traveled exactly one game-mile. In
rlm@60 153 order to keep sync with the real world, the game throttles its physics
rlm@60 154 engine and graphics display. If the computations involved in running
rlm@60 155 the game are too intense, then the game will first skip frames, then
rlm@60 156 sacrifice physics accuracy. If there are particuraly demanding
rlm@60 157 computations, then you may only get 1 fps, and the ball may tunnel
rlm@60 158 through the floor or obstacles due to inaccurate physics simulation,
rlm@60 159 but after the end of one user-hour, that ball will have traveled one
rlm@60 160 game-mile.
rlm@60 161
rlm@60 162 When we're recording video, we don't care if the game-time syncs with
rlm@60 163 user-time, but instead whether the time in the recorded video
rlm@60 164 (video-time) syncs with user-time. To continue the analogy, if we
rlm@60 165 recorded the ball rolling at 1 game-mile per game-hour and watched the
rlm@60 166 video later, we would want to see 30 fps video of the ball rolling at
rlm@60 167 1 video-mile per /user-hour/. It doesn't matter how much user-time it
rlm@60 168 took to simulate that hour of game-time to make the high-quality
rlm@60 169 recording.
rlm@60 170
rlm@60 171 The IsoTimer ignores real-time and always reports that the same amount
rlm@60 172 of time has passed every time it is called. That way, one can put code
rlm@60 173 to write each video/audio frame to a file without worrying about that
rlm@60 174 code itself slowing down the game to the point where the recording
rlm@60 175 would be useless.
rlm@60 176
rlm@60 177
rlm@60 178 ==== Advanced Example ====
rlm@60 179
rlm@60 180 The package from aurellem.com was made for AI research and can do more
rlm@60 181 than just record a single stream of audio and video. You can use it
rlm@60 182 to:
rlm@60 183
rlm@60 184 1.) Create multiple independent listeners that each hear the world
rlm@60 185 from their own perspective.
rlm@60 186
rlm@60 187 2.) Process the sound data in any way you wish.
rlm@60 188
rlm@60 189 3.) Do the same for visual data.
rlm@60 190
rlm@60 191 Here is a more advanced example, which can also be found along with
rlm@60 192 other examples in the jmeCapture.jar file included in the
rlm@60 193 distribution.
rlm@60 194
rlm@60 195 <code java>
rlm@60 196 package com.aurellem.capture.examples;
rlm@60 197
rlm@60 198 import java.io.File;
rlm@60 199 import java.io.FileNotFoundException;
rlm@60 200 import java.io.IOException;
rlm@60 201 import java.lang.reflect.Field;
rlm@60 202 import java.nio.ByteBuffer;
rlm@60 203
rlm@60 204 import javax.sound.sampled.AudioFormat;
rlm@60 205
rlm@60 206 import org.tritonus.share.sampled.FloatSampleTools;
rlm@60 207
rlm@60 208 import com.aurellem.capture.AurellemSystemDelegate;
rlm@60 209 import com.aurellem.capture.Capture;
rlm@60 210 import com.aurellem.capture.IsoTimer;
rlm@60 211 import com.aurellem.capture.audio.CompositeSoundProcessor;
rlm@60 212 import com.aurellem.capture.audio.MultiListener;
rlm@60 213 import com.aurellem.capture.audio.SoundProcessor;
rlm@60 214 import com.aurellem.capture.audio.WaveFileWriter;
rlm@60 215 import com.jme3.app.SimpleApplication;
rlm@60 216 import com.jme3.audio.AudioNode;
rlm@60 217 import com.jme3.audio.Listener;
rlm@60 218 import com.jme3.cinematic.MotionPath;
rlm@60 219 import com.jme3.cinematic.events.AbstractCinematicEvent;
rlm@60 220 import com.jme3.cinematic.events.MotionTrack;
rlm@60 221 import com.jme3.material.Material;
rlm@60 222 import com.jme3.math.ColorRGBA;
rlm@60 223 import com.jme3.math.FastMath;
rlm@60 224 import com.jme3.math.Quaternion;
rlm@60 225 import com.jme3.math.Vector3f;
rlm@60 226 import com.jme3.scene.Geometry;
rlm@60 227 import com.jme3.scene.Node;
rlm@60 228 import com.jme3.scene.shape.Box;
rlm@60 229 import com.jme3.scene.shape.Sphere;
rlm@60 230 import com.jme3.system.AppSettings;
rlm@60 231 import com.jme3.system.JmeSystem;
rlm@60 232
rlm@60 233 /**
rlm@60 234 *
rlm@60 235 * Demonstrates advanced use of the audio capture and recording
rlm@60 236 * features. Multiple perspectives of the same scene are
rlm@60 237 * simultaneously rendered to different sound files.
rlm@60 238 *
rlm@60 239 * A key limitation of the way multiple listeners are implemented is
rlm@60 240 * that only 3D positioning effects are realized for listeners other
rlm@60 241 * than the main LWJGL listener. This means that audio effects such
rlm@60 242 * as environment settings will *not* be heard on any auxiliary
rlm@60 243 * listeners, though sound attenuation will work correctly.
rlm@60 244 *
rlm@60 245 * Multiple listeners as realized here might be used to make AI
rlm@60 246 * entities that can each hear the world from their own perspective.
rlm@60 247 *
rlm@60 248 * @author Robert McIntyre
rlm@60 249 */
rlm@60 250
rlm@60 251 public class Advanced extends SimpleApplication {
rlm@60 252
rlm@60 253 /**
rlm@60 254 * You will see three grey cubes, a blue sphere, and a path which
rlm@60 255 * circles each cube. The blue sphere is generating a constant
rlm@60 256 * monotone sound as it moves along the track. Each cube is
rlm@60 257 * listening for sound; when a cube hears sound whose intensity is
rlm@60 258 * greater than a certain threshold, it changes its color from
rlm@60 259 * grey to green.
rlm@60 260 *
rlm@60 261 * Each cube is also saving whatever it hears to a file. The
rlm@60 262 * scene from the perspective of the viewer is also saved to a
rlm@60 263 * video file. When you listen to each of the sound files
rlm@60 264 * alongside the video, the sound will get louder when the sphere
rlm@60 265 * approaches the cube that generated that sound file. This
rlm@60 266 * shows that each listener is hearing the world from its own
rlm@60 267 * perspective.
rlm@60 268 *
rlm@60 269 */
rlm@60 270 public static void main(String[] args) {
rlm@60 271 Advanced app = new Advanced();
rlm@60 272 AppSettings settings = new AppSettings(true);
rlm@60 273 settings.setAudioRenderer(AurellemSystemDelegate.SEND);
rlm@60 274 JmeSystem.setSystemDelegate(new AurellemSystemDelegate());
rlm@60 275 app.setSettings(settings);
rlm@60 276 app.setShowSettings(false);
rlm@60 277 app.setPauseOnLostFocus(false);
rlm@60 278
rlm@60 279 try {
rlm@60 280 Capture.captureVideo(app, File.createTempFile("advanced",".avi"));
rlm@60 281 Capture.captureAudio(app, File.createTempFile("advanced", ".wav"));
rlm@60 282 }
rlm@60 283 catch (IOException e) {e.printStackTrace();}
rlm@60 284
rlm@60 285 app.start();
rlm@60 286 }
rlm@60 287
rlm@60 288
rlm@60 289 private Geometry bell;
rlm@60 290 private Geometry ear1;
rlm@60 291 private Geometry ear2;
rlm@60 292 private Geometry ear3;
rlm@60 293 private AudioNode music;
rlm@60 294 private MotionTrack motionControl;
rlm@60 295
rlm@60 296 private Geometry makeEar(Node root, Vector3f position){
rlm@60 297 Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
rlm@60 298 Geometry ear = new Geometry("ear", new Box(1.0f, 1.0f, 1.0f));
rlm@60 299 ear.setLocalTranslation(position);
rlm@60 300 mat.setColor("Color", ColorRGBA.Green);
rlm@60 301 ear.setMaterial(mat);
rlm@60 302 root.attachChild(ear);
rlm@60 303 return ear;
rlm@60 304 }
rlm@60 305
rlm@60 306 private Vector3f[] path = new Vector3f[]{
rlm@60 307 // loop 1
rlm@60 308 new Vector3f(0, 0, 0),
rlm@60 309 new Vector3f(0, 0, -10),
rlm@60 310 new Vector3f(-2, 0, -14),
rlm@60 311 new Vector3f(-6, 0, -20),
rlm@60 312 new Vector3f(0, 0, -26),
rlm@60 313 new Vector3f(6, 0, -20),
rlm@60 314 new Vector3f(0, 0, -14),
rlm@60 315 new Vector3f(-6, 0, -20),
rlm@60 316 new Vector3f(0, 0, -26),
rlm@60 317 new Vector3f(6, 0, -20),
rlm@60 318 // loop 2
rlm@60 319 new Vector3f(5, 0, -5),
rlm@60 320 new Vector3f(7, 0, 1.5f),
rlm@60 321 new Vector3f(14, 0, 2),
rlm@60 322 new Vector3f(20, 0, 6),
rlm@60 323 new Vector3f(26, 0, 0),
rlm@60 324 new Vector3f(20, 0, -6),
rlm@60 325 new Vector3f(14, 0, 0),
rlm@60 326 new Vector3f(20, 0, 6),
rlm@60 327 new Vector3f(26, 0, 0),
rlm@60 328 new Vector3f(20, 0, -6),
rlm@60 329 new Vector3f(14, 0, 0),
rlm@60 330 // loop 3
rlm@60 331 new Vector3f(8, 0, 7.5f),
rlm@60 332 new Vector3f(7, 0, 10.5f),
rlm@60 333 new Vector3f(6, 0, 20),
rlm@60 334 new Vector3f(0, 0, 26),
rlm@60 335 new Vector3f(-6, 0, 20),
rlm@60 336 new Vector3f(0, 0, 14),
rlm@60 337 new Vector3f(6, 0, 20),
rlm@60 338 new Vector3f(0, 0, 26),
rlm@60 339 new Vector3f(-6, 0, 20),
rlm@60 340 new Vector3f(0, 0, 14),
rlm@60 341 // begin ellipse
rlm@60 342 new Vector3f(16, 5, 20),
rlm@60 343 new Vector3f(0, 0, 26),
rlm@60 344 new Vector3f(-16, -10, 20),
rlm@60 345 new Vector3f(0, 0, 14),
rlm@60 346 new Vector3f(16, 20, 20),
rlm@60 347 new Vector3f(0, 0, 26),
rlm@60 348 new Vector3f(-10, -25, 10),
rlm@60 349 new Vector3f(-10, 0, 0),
rlm@60 350 // come at me!
rlm@60 351 new Vector3f(-28.00242f, 48.005623f, -34.648228f),
rlm@60 352 new Vector3f(0, 0 , -20),
rlm@60 353 };
rlm@60 354
rlm@60 355 private void createScene() {
rlm@60 356 Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
rlm@60 357 bell = new Geometry( "sound-emitter" , new Sphere(15,15,1));
rlm@60 358 mat.setColor("Color", ColorRGBA.Blue);
rlm@60 359 bell.setMaterial(mat);
rlm@60 360 rootNode.attachChild(bell);
rlm@60 361
rlm@60 362 ear1 = makeEar(rootNode, new Vector3f(0, 0 ,-20));
rlm@60 363 ear2 = makeEar(rootNode, new Vector3f(0, 0 ,20));
rlm@60 364 ear3 = makeEar(rootNode, new Vector3f(20, 0 ,0));
rlm@60 365
rlm@60 366 MotionPath track = new MotionPath();
rlm@60 367
rlm@60 368 for (Vector3f v : path){
rlm@60 369 track.addWayPoint(v);
rlm@60 370 }
rlm@60 371 track.setCurveTension(0.80f);
rlm@60 372
rlm@60 373 motionControl = new MotionTrack(bell,track);
rlm@60 374
rlm@60 375 // for now, use reflection to change the timer...
rlm@60 376 // motionControl.setTimer(new IsoTimer(60));
rlm@60 377 try {
rlm@60 378 Field timerField;
rlm@60 379 timerField = AbstractCinematicEvent.class.getDeclaredField("timer");
rlm@60 380 timerField.setAccessible(true);
rlm@60 381 try {timerField.set(motionControl, new IsoTimer(60));}
rlm@60 382 catch (IllegalArgumentException e) {e.printStackTrace();}
rlm@60 383 catch (IllegalAccessException e) {e.printStackTrace();}
rlm@60 384 }
rlm@60 385 catch (SecurityException e) {e.printStackTrace();}
rlm@60 386 catch (NoSuchFieldException e) {e.printStackTrace();}
rlm@60 387
rlm@60 388 motionControl.setDirectionType(MotionTrack.Direction.PathAndRotation);
rlm@60 389 motionControl.setRotation(new Quaternion().fromAngleNormalAxis(-FastMath.HALF_PI, Vector3f.UNIT_Y));
rlm@60 390 motionControl.setInitialDuration(20f);
rlm@60 391 motionControl.setSpeed(1f);
rlm@60 392
rlm@60 393 track.enableDebugShape(assetManager, rootNode);
rlm@60 394 positionCamera();
rlm@60 395 }
rlm@60 396
rlm@60 397
rlm@60 398 private void positionCamera(){
rlm@60 399 this.cam.setLocation(new Vector3f(-28.00242f, 48.005623f, -34.648228f));
rlm@60 400 this.cam.setRotation(new Quaternion(0.3359635f, 0.34280345f, -0.13281013f, 0.8671653f));
rlm@60 401 }
rlm@60 402
rlm@60 403 private void initAudio() {
rlm@60 404 org.lwjgl.input.Mouse.setGrabbed(false);
rlm@60 405 music = new AudioNode(assetManager, "Sound/Effects/Beep.ogg", false);
rlm@60 406
rlm@60 407 rootNode.attachChild(music);
rlm@60 408 audioRenderer.playSource(music);
rlm@60 409 music.setPositional(true);
rlm@60 410 music.setVolume(1f);
rlm@60 411 music.setReverbEnabled(false);
rlm@60 412 music.setDirectional(false);
rlm@60 413 music.setMaxDistance(200.0f);
rlm@60 414 music.setRefDistance(1f);
rlm@60 415 //music.setRolloffFactor(1f);
rlm@60 416 music.setLooping(false);
rlm@60 417 audioRenderer.pauseSource(music);
rlm@60 418 }
rlm@60 419
rlm@60 420 public class Dancer implements SoundProcessor {
rlm@60 421 Geometry entity;
rlm@60 422 float scale = 2;
rlm@60 423 public Dancer(Geometry entity){
rlm@60 424 this.entity = entity;
rlm@60 425 }
rlm@60 426
rlm@60 427 /**
rlm@60 428 * this method is irrelevant since there is no state to cleanup.
rlm@60 429 */
rlm@60 430 public void cleanup() {}
rlm@60 431
rlm@60 432
rlm@60 433 /**
rlm@60 434 * Respond to sound! This is the brain of an AI entity that
rlm@60 435 * hears it's surroundings and reacts to them.
rlm@60 436 */
rlm@60 437 public void process(ByteBuffer audioSamples, int numSamples, AudioFormat format) {
rlm@60 438 audioSamples.clear();
rlm@60 439 byte[] data = new byte[numSamples];
rlm@60 440 float[] out = new float[numSamples];
rlm@60 441 audioSamples.get(data);
rlm@60 442 FloatSampleTools.byte2floatInterleaved(data, 0, out, 0,
rlm@60 443 numSamples/format.getFrameSize(), format);
rlm@60 444
rlm@60 445 float max = Float.NEGATIVE_INFINITY;
rlm@60 446 for (float f : out){if (f > max) max = f;}
rlm@60 447 audioSamples.clear();
rlm@60 448
rlm@60 449 if (max > 0.1){entity.getMaterial().setColor("Color", ColorRGBA.Green);}
rlm@60 450 else {entity.getMaterial().setColor("Color", ColorRGBA.Gray);}
rlm@60 451 }
rlm@60 452 }
rlm@60 453
rlm@60 454 private void prepareEar(Geometry ear, int n){
rlm@60 455 if (this.audioRenderer instanceof MultiListener){
rlm@60 456 MultiListener rf = (MultiListener)this.audioRenderer;
rlm@60 457
rlm@60 458 Listener auxListener = new Listener();
rlm@60 459 auxListener.setLocation(ear.getLocalTranslation());
rlm@60 460
rlm@60 461 rf.addListener(auxListener);
rlm@60 462 WaveFileWriter aux = null;
rlm@60 463
rlm@60 464 try {aux = new WaveFileWriter(new File("/home/r/tmp/ear"+n+".wav"));}
rlm@60 465 catch (FileNotFoundException e) {e.printStackTrace();}
rlm@60 466
rlm@60 467 rf.registerSoundProcessor(auxListener,
rlm@60 468 new CompositeSoundProcessor(new Dancer(ear), aux));
rlm@60 469 }
rlm@60 470 }
rlm@60 471
rlm@60 472
rlm@60 473 public void simpleInitApp() {
rlm@60 474 this.setTimer(new IsoTimer(60));
rlm@60 475 initAudio();
rlm@60 476
rlm@60 477 createScene();
rlm@60 478
rlm@60 479 prepareEar(ear1, 1);
rlm@60 480 prepareEar(ear2, 1);
rlm@60 481 prepareEar(ear3, 1);
rlm@60 482
rlm@60 483 motionControl.play();
rlm@60 484 }
rlm@60 485
rlm@60 486 public void simpleUpdate(float tpf) {
rlm@60 487 if (music.getStatus() != AudioNode.Status.Playing){
rlm@60 488 music.play();
rlm@60 489 }
rlm@60 490 Vector3f loc = cam.getLocation();
rlm@60 491 Quaternion rot = cam.getRotation();
rlm@60 492 listener.setLocation(loc);
rlm@60 493 listener.setRotation(rot);
rlm@60 494 music.setLocalTranslation(bell.getLocalTranslation());
rlm@60 495 }
rlm@60 496
rlm@60 497 }
rlm@60 498 </code>
rlm@60 499
rlm@60 500 <iframe width="420" height="315"
rlm@60 501 src="http://www.youtube.com/embed/oCEfK0yhDrY"
rlm@60 502 frameborder="0" allowfullscreen>
rlm@60 503 </iframe>
rlm@60 504
rlm@60 505 ===== More Information =====
rlm@60 506
rlm@60 507 This is the old page showing the first version of this idea
rlm@60 508 http://aurellem.org/cortex/html/capture-video.html
rlm@60 509
rlm@60 510 All source code can be found here:
rlm@60 511
rlm@60 512 http://hg.bortreb.com/audio-send
rlm@60 513 http://hg.bortreb.com/jmeCapture
rlm@60 514
rlm@60 515 More information on the modifications to OpenAL to support multiple
rlm@60 516 listeners can be found here.
rlm@60 517
rlm@60 518 http://aurellem.org/audio-send/html/ear.html