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; } }