Sound

Playing audio in HTML5 can be difficult. All modern browsers use slightly different implementations of the standard. Google Chrome supports the Web Audio API which is not a HTML5 standard yet, but offers the best audio playback capabilities for games. Another problem is the support of all the audio codecs (mp3/ogg/wav) used in different browsers. To hide all the problems and differences the StageXL library wraps those APIs and provides the familiar Sound APIs of Flash. The library also selects the codec which is supported by the browser. You just have to provide the audio file in mp3 and ogg format, this way your application will work in all major browsers. Nevertheless Firefox and especially Internet Explorer have latency problems, let's hope those browser vendors are working on this issue.





library demo;

import 'dart:math';
import 'dart:html' as html;
import 'package:stagexl/stagexl.dart';

part 'sound_demo.dart';
part 'piano.dart';
part 'piano_key.dart';

Stage stage = new Stage(html.querySelector('#stage'), webGL: true);
RenderLoop renderLoop = new RenderLoop();
ResourceManager resourceManager  = new ResourceManager();

void main() {
     
  renderLoop.addStage(stage);
  
  resourceManager
    ..addBitmapData('KeyBlack','images/piano/KeyBlack.png')
    ..addBitmapData('KeyWhite0','images/piano/KeyWhite0.png')
    ..addBitmapData('KeyWhite1','images/piano/KeyWhite1.png')
    ..addBitmapData('KeyWhite2','images/piano/KeyWhite2.png')
    ..addBitmapData('KeyWhite3','images/piano/KeyWhite3.png')
    ..addBitmapData('Finger','images/piano/Finger.png')
    ..addBitmapData('Background','images/piano/Background.jpg')
    ..addSound('Cheer','sounds/Cheer.mp3')
    ..addSound('Note1','sounds/piano/Note1.mp3')
    ..addSound('Note2','sounds/piano/Note2.mp3')
    ..addSound('Note3','sounds/piano/Note3.mp3')
    ..addSound('Note4','sounds/piano/Note4.mp3')
    ..addSound('Note5','sounds/piano/Note5.mp3')
    ..addSound('Note6','sounds/piano/Note6.mp3')
    ..addSound('Note7','sounds/piano/Note7.mp3')
    ..addSound('Note8','sounds/piano/Note8.mp3')
    ..addSound('Note9','sounds/piano/Note9.mp3')
    ..addSound('Note10','sounds/piano/Note10.mp3')
    ..addSound('Note11','sounds/piano/Note11.mp3')
    ..addSound('Note12','sounds/piano/Note12.mp3')
    ..addSound('Note13','sounds/piano/Note13.mp3')
    ..addSound('Note14','sounds/piano/Note14.mp3')
    ..addSound('Note15','sounds/piano/Note15.mp3')
    ..addSound('Note16','sounds/piano/Note16.mp3')
    ..addSound('Note17','sounds/piano/Note17.mp3')
    ..addSound('Note18','sounds/piano/Note18.mp3')
    ..addSound('Note19','sounds/piano/Note19.mp3')
    ..addSound('Note20','sounds/piano/Note20.mp3')
    ..addSound('Note21','sounds/piano/Note21.mp3')
    ..addSound('Note22','sounds/piano/Note22.mp3')
    ..addSound('Note23','sounds/piano/Note23.mp3')
    ..addSound('Note24','sounds/piano/Note24.mp3')
    ..addSound('Note25','sounds/piano/Note25.mp3');        
  
  resourceManager.load()
    .then((_) => stage.addChild(new SoundDemo()))
    .catchError((e) => print(e));
}
part of demo;

class SoundDemo extends DisplayObjectContainer{
  
  final List<String> _heyJudeNotes = [
    'C4', 'A3', 'A3', 'C4', 'D4', 'G3',
    'G3', 'A3', 'A3#', 'F4', 'F4', 'E4', 'C4', 'D4', 'C4', 'A3#', 'A3',
    'C4', 'D4', 'D4', 'D4', 'G4', 'F4', 'E4', 'F4', 'D4', 'C4',
    'F3', 'G3', 'A3', 'D4', 'C4', 'C4', 'A3#', 'A3', 'E3', 'F3'];

  final List<String> _heyJudeLyrics = [
    'Hey ', 'Jude, ', "don't ", 'make ', 'it ', 'bad.<br>',
    'Take ', 'a ', 'sad ', 'song ', 'and ', 'make ', 'it ', 'better.<br>',  '',  '',  '',
    'Remember ', '', '', 'to ', 'let ', 'her ', 'into ', '', 'your ', 'heart.<br>',
    'Than ', 'you ', 'can ', 'start ', '', 'to ', 'make ', 'things ', 'better.', '', ' '];

  SoundDemo() {
    
    var background = new Bitmap(resourceManager.getBitmapData('Background'));
    addChild(background);
  
    var piano = new Piano(_heyJudeNotes, _heyJudeLyrics);
    piano.x = 120;
    piano.y = 30;
    addChild(piano);
  
    html.query('#startOver').onClick.listen((e) => piano.resetSong());
  }
}
part of demo;

class Piano extends DisplayObjectContainer {

  final List<String> songNotes;
  final List<String> songLyrics;
  
  final List<String> _pianoNotes = [
    'C3', 'C3#', 'D3', 'D3#', 'E3', 'F3', 'F3#', 'G3', 'G3#', 'A3', 'A3#', 'B3',
    'C4', 'C4#', 'D4', 'D4#', 'E4', 'F4', 'F4#', 'G4', 'G4#', 'A4', 'A4#', 'B4', 'C5'];
  
  Map<String, PianoKey> _pianoKeys = new Map<String, PianoKey>();
  Bitmap _karaokeFinger;
  int _songNoteIndex = 0;
  
  Piano(this.songNotes, this.songLyrics) {
    
    // add piano keys
    for(int n = 0, x = 0; n < _pianoNotes.length; n++) {
      var sound = resourceManager.getSound('Note${n+1}');
      var pianoNote = _pianoNotes[n];
      var pianoKey = _pianoKeys[pianoNote] = new PianoKey(this, pianoNote, sound);
      
      if (pianoNote.endsWith('#')) {
        pianoKey.x = x - 16;
        pianoKey.y = 35;
        addChild(pianoKey);
      } else {
        pianoKey.x = x;
        pianoKey.y = 35;
        addChildAt(pianoKey, 0);
        x = x + 47;
      }
    }

    // prepare karaoke finger
    _karaokeFinger = new Bitmap(resourceManager.getBitmapData('Finger'));
    _karaokeFinger.pivotX = 20;
    addChild(_karaokeFinger);
    
    _updateKaraoke();
  }

  //---------------------------------------------------------------------------------

  checkSongNote(String note) {
    
    // is it the next note of the song?
    if (_songNoteIndex < songNotes.length && songNotes[_songNoteIndex] == note) {
      if (_songNoteIndex == songNotes.length - 1)
        resourceManager.getSound('Cheer').play();
      
      _songNoteIndex++;
      _updateKaraoke();
    }
  }

  //---------------------------------------------------------------------------------

  resetSong() {
    _songNoteIndex = 0;
    _karaokeFinger.alpha = 1;
    _updateKaraoke();
  }
  
  //---------------------------------------------------------------------------------

  _updateKaraoke() {
    
    // update karaoke lyrics
    var lyricsDiv = html.query('#lyrics');
    var wordIndex = -1;
    lyricsDiv.innerHtml = '';

    for(int w = 0, last = 0; w < songLyrics.length; w++)  {
      if (songLyrics[w] != '') last = w;
      if (w == this._songNoteIndex) wordIndex = last;
    }

    for(int w = 0; w < songLyrics.length; w++) {
      if (w == wordIndex) {
        lyricsDiv.appendHtml('<span id="word">${songLyrics[w]}</span>');
      } else {
        lyricsDiv.appendHtml(songLyrics[w]);
      }
    }

    // update finger position
    if (_songNoteIndex < songNotes.length) {
      var songNote = songNotes[_songNoteIndex];
      if (_pianoKeys.containsKey(songNote)) {
        var pianoKey = _pianoKeys[songNote];
        juggler.removeTweens(_karaokeFinger);
        _karaokeFinger.y = 0;
            
        stage.juggler.tween(_karaokeFinger, 0.4, TransitionFunction.easeInOutCubic)
          .animate.x.to(pianoKey.x + pianoKey.width / 2);
        stage.juggler.tween(this._karaokeFinger, 0.4, TransitionFunction.sine)
          .animate.y.to(-10);
      }
    } else {
      stage.juggler.tween(_karaokeFinger, 0.4, TransitionFunction.linear)
        .animate.alpha.to(0);
    }
  }
}
part of demo;

class PianoKey extends Sprite {
  
  final Piano piano;
  final String note;
  final Sound sound;

  PianoKey(this.piano, this.note, this.sound) {
    
    String key;

    if (note.endsWith('#')) {
      key = 'KeyBlack';
    } else if (note.startsWith('C5')) {
      key = 'KeyWhite0';
    } else if (note.startsWith('C') || note.startsWith('F')) {
      key = 'KeyWhite1';
    } else if (note.startsWith('D') || note.startsWith('G') || note.startsWith('A')) {
      key = 'KeyWhite2';
    } else if (note.startsWith('E') || note.startsWith('B')) {
      key = 'KeyWhite3';
    }

    // draw the key
    var bitmapData = resourceManager.getBitmapData(key);
    var bitmap = new Bitmap(bitmapData);
    this.addChild(bitmap);

    // print note on key
    var textColor = note.endsWith('#') ? Color.White : Color.Black;
    var textFormat = new TextFormat('Helvetica,Arial', 10, textColor, align:TextFormatAlign.CENTER);

    var textField = new TextField();
    textField.defaultTextFormat = textFormat;
    textField.text = note;
    textField.width = bitmapData.width;
    textField.height = 20;
    textField.mouseEnabled = false;
    textField.y = bitmapData.height - 20;
    addChild(textField);

    // add event handlers
    this.useHandCursor = true;
    this.onMouseDown.listen(_keyDown);
    this.onMouseOver.listen(_keyDown);
    this.onMouseUp.listen(_keyUp);
    this.onMouseOut.listen(_keyUp);
  }

  _keyDown(MouseEvent me) {
    if (me.buttonDown) {
      this.sound.play();
      this.alpha = 0.7;
      this.piano.checkSongNote(this.note);
    }
  }

  _keyUp(MouseEvent me) {
      this.alpha = 1.0;
  }
}