Merge pull request #598 from vlidholt/master

Adds rough sound support for sprites
diff --git a/examples/game/lib/game_demo_world.dart b/examples/game/lib/game_demo_world.dart
index f86816d..cc683e6 100644
--- a/examples/game/lib/game_demo_world.dart
+++ b/examples/game/lib/game_demo_world.dart
@@ -28,6 +28,10 @@
 
   SpriteSheet _spriteSheet;
   SpriteSheet _spriteSheetUI;
+
+  Map<String,SoundEffect> _sounds;
+  SoundPool _soundPool = SoundPool.sharedInstance();
+
   Navigator _navigator;
 
   // Inputs
@@ -54,7 +58,7 @@
 
   Function _gameOverCallback;
 
-  GameDemoWorld(App app, this._navigator, ImageMap images, this._spriteSheet, this._spriteSheetUI, this._gameOverCallback) : super(new Size(_gameSizeWidth, _gameSizeHeight)) {
+  GameDemoWorld(App app, this._navigator, ImageMap images, this._spriteSheet, this._spriteSheetUI, this._sounds, this._gameOverCallback) : super(new Size(_gameSizeWidth, _gameSizeHeight)) {
     // Fetch images
     _imgNebula = images["assets/nebula.png"];
 
@@ -140,6 +144,8 @@
     laser.constrainProportions = true;
     _lasers.add(laser);
     _gameLayer.addChild(laser);
+
+    _soundPool.play(_sounds["laser"]);
   }
 
   void addNebula() {
@@ -225,6 +231,8 @@
     }
 
     _gameLayer.addChild(explosionNode);
+
+    _soundPool.play(_sounds["explosion"]);
   }
 
   void update(double dt) {
diff --git a/examples/game/lib/main.dart b/examples/game/lib/main.dart
index b142cde..68ca68d 100644
--- a/examples/game/lib/main.dart
+++ b/examples/game/lib/main.dart
@@ -28,6 +28,7 @@
 SpriteSheet _spriteSheet;
 SpriteSheet _spriteSheetUI;
 GameDemoApp _app;
+Map<String,SoundEffect> _sounds = {};
 
 main() async {
   _loader = new ImageMap(_bundle);
@@ -47,6 +48,12 @@
 
   _app = new GameDemoApp();
 
+  _sounds["explosion"] = new SoundEffect('https://github.com/slembcke/GalacticGuardian.spritebuilder/raw/GDC/Packages/SpriteBuilder%20Resources.sbpack/TempSounds/Explosion.wav');
+  _sounds["laser"] = new SoundEffect('https://github.com/slembcke/GalacticGuardian.spritebuilder/raw/GDC/Packages/SpriteBuilder%20Resources.sbpack/TempSounds/Laser.wav');
+
+  await _sounds["explosion"].load();
+  await _sounds["laser"].load();
+
   runApp(_app);
 }
 
@@ -103,6 +110,7 @@
               _loader,
               _spriteSheet,
               _spriteSheetUI,
+              _sounds,
               (lastScore) {
                 setState(() {_lastScore = lastScore;});
               }
diff --git a/examples/game/lib/sound.dart b/examples/game/lib/sound.dart
new file mode 100644
index 0000000..51c9740
--- /dev/null
+++ b/examples/game/lib/sound.dart
@@ -0,0 +1,162 @@
+part of sprites;
+
+// TODO: The sound effects should probably use Android's SoundPool instead of
+// MediaPlayer as it is more efficient and flexible for playing back sound effects
+
+typedef void SoundCompleteCallback();
+
+class SoundEffect {
+  SoundEffect(this._url);
+
+  // TODO: Remove load method from SoundEffect
+  Future load() async {
+    UrlResponse response = await fetchUrl(_url);
+    _data = response.body;
+  }
+
+  String _url;
+  Object _data;
+}
+
+class SoundStream {
+  SoundStream(
+    this.sound,
+    this.tag,
+    this.loop,
+    this.time,
+    this.volume,
+    this.pitch,
+    this.pan,
+    this.callback
+  );
+
+  // TODO: Make these properties work
+  SoundEffect sound;
+  bool playing = false;
+  bool loop = false;
+  double time = 0.0;
+  double volume = 1.0;
+  double pitch = 1.0;
+  double pan = 0.0;
+  Object tag;
+
+  // TODO: Implement completion callback. On completion, sounds should
+  // also be removed from the list of playing sounds.
+  SoundCompleteCallback callback;
+
+  MediaPlayerProxy _player;
+}
+
+SoundPool _sharedSoundPool;
+
+class SoundPool {
+
+  static SoundPool sharedInstance() {
+    if (_sharedSoundPool == null) {
+      _sharedSoundPool = new SoundPool();
+    }
+    return _sharedSoundPool;
+  }
+
+  SoundPool() {
+    _mediaService = new MediaServiceProxy.unbound();
+    shell.requestService(null, _mediaService);
+  }
+
+  MediaServiceProxy _mediaService;
+  List<SoundStream> _playingSounds = [];
+
+  // TODO: This should no longer be needed when moving to SoundPool backing
+  Map<SoundEffect,MediaPlayerProxy> _mediaPlayers = {};
+
+  Future _prepare(SoundStream playingSound) async {
+    await playingSound._player.ptr.prepare(playingSound.sound._data);
+  }
+
+  // TODO: Move sound loading here
+  // TODO: Support loading sounds from bundles
+  // Future<SoundEffect> load(url) async {
+  //   ...
+  // }
+
+  // TODO: Add sound unloader
+  // unload(SoundEffect effect) {
+  //   ...
+  // }
+
+  // TODO: Add paused property (should pause playback of all sounds)
+  bool paused;
+
+  SoundStream play(
+    SoundEffect sound,
+    [Object tag,
+      bool loop = false,
+      double volume = 1.0,
+      double pitch = 1.0,
+      double pan = 0.0,
+      double startTime = 0.0,
+      SoundCompleteCallback callback = null]) {
+
+    // Create new PlayingSound object
+    SoundStream playingSound = new SoundStream(
+      sound,
+      tag,
+      loop,
+      startTime,
+      volume,
+      pitch,
+      pan,
+      callback
+    );
+
+    // TODO: Replace this with calls to SoundPool
+    if (_mediaPlayers[sound] == null) {
+      // Create player
+      playingSound._player = new MediaPlayerProxy.unbound();
+      _mediaService.ptr.createPlayer(playingSound._player);
+
+      // Prepare sound, then play it
+      _prepare(playingSound).then((_) {
+        playingSound._player.ptr.seekTo((startTime * 1000.0).toInt());
+        playingSound._player.ptr.start();
+      });
+
+      _playingSounds.add(playingSound);
+      _mediaPlayers[sound] = playingSound._player;
+    } else {
+      // Reuse player
+      playingSound._player = _mediaPlayers[sound];
+      playingSound._player.ptr.seekTo((startTime * 1000.0).toInt());
+      playingSound._player.ptr.start();
+    }
+
+    return playingSound;
+  }
+
+  void stop(Object tag) {
+    for (int i = _playingSounds.length; i >= 0; i--) {
+      SoundStream playingSound = _playingSounds[i];
+      if (playingSound.tag == tag) {
+        playingSound._player.ptr.pause();
+        _playingSounds.removeAt(i);
+      }
+    }
+  }
+
+  List<SoundStream> playingSoundsForTag(Object tag) {
+    List<SoundStream> list = [];
+    for (SoundStream playingSound in _playingSounds) {
+      if (playingSound.tag == tag) {
+        list.add(playingSound);
+      }
+    }
+    return list;
+  }
+
+  void stopAll() {
+    for (SoundStream playingSound in _playingSounds) {
+      playingSound._player.ptr.pause();
+    }
+    _playingSounds = [];
+  }
+}
diff --git a/examples/game/lib/sprites.dart b/examples/game/lib/sprites.dart
index 801dbc0..155dcc2 100644
--- a/examples/game/lib/sprites.dart
+++ b/examples/game/lib/sprites.dart
@@ -10,12 +10,16 @@
 import 'dart:typed_data';
 import 'dart:sky';
 
+import 'package:mojo/mojo/url_response.mojom.dart';
 import 'package:sky/animation/curves.dart';
 import 'package:sky/base/scheduler.dart' as scheduler;
 import 'package:sky/mojo/asset_bundle.dart';
+import 'package:sky/mojo/net/fetch.dart';
+import 'package:sky/mojo/shell.dart' as shell;
 import 'package:sky/rendering/box.dart';
 import 'package:sky/rendering/object.dart';
 import 'package:sky/widgets/framework.dart';
+import 'package:sky_services/media/media.mojom.dart';
 import 'package:vector_math/vector_math.dart';
 
 part 'action.dart';
@@ -26,6 +30,7 @@
 part 'node3d.dart';
 part 'node_with_size.dart';
 part 'particle_system.dart';
+part 'sound.dart';
 part 'sprite.dart';
 part 'spritesheet.dart';
 part 'sprite_box.dart';