From 5d5a34c1409ebca8f7b1787b78456141297f9b4b Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 17:36:46 -0500 Subject: [PATCH 01/12] initial commit --- .gitignore | 40 ++++++++++ README.md | 22 ++++++ gulpfile.js | 41 ++++++++++ package.json | 34 ++++++++ src/audio-api.js | 134 ++++++++++++++++++++++++++++++++ src/audio-view.js | 193 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 464 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 src/audio-api.js create mode 100644 src/audio-view.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5da69d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# Distribution directory +dist/ +lib/ + +### Vim ### +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1fd577 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# shared-views + +[![Build Status](https://travis-ci.org/anyWareSculpture/shared-views.svg?branch=master)](https://travis-ci.org/anyWareSculpture/shared-views) + +[![codecov.io](http://codecov.io/github/anyWareSculpture/shared-views/coverage.svg?branch=master)](http://codecov.io/github/anyWareSculpture/shared-views?branch=master) + +![codecov.io](http://codecov.io/github/anyWareSculpture/shared-views/branch.svg?branch=master) + +## Usage +This library models the game logic shared by all anyWare implementations. + +When installed (or built), modules are stored in a `lib/` directory. Thus when requiring files, make sure that you are adding that before the path of the file you are requiring. In addition, ensure that you are requiring each individual file. `require('@anyware/shared-views')` alone will not work. + +Example of correct usage: + + const MyThing = require('@anyware/shared-views/lib/things/my-thing'); + +This was implemented this way for a variety of reasons: + +1. Requiring individual files only gets those files and their dependencies. That way it isn't necessary to include the entire library if you only need a few parts. +2. This means that we don't have to keep any `index.js` or something up to date all the time. You can access whatever you want using the `lib/` directory. + diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..69396e3 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,41 @@ +var gulp = require('gulp'); + +var runSequence = require('run-sequence'); + +var gulpUtils = require('@anyware/gulp-utils'); + +MINIMUM_CODE_COVERAGE = 90; + +// Create shared tasks +require('@anyware/gulp-utils/tasks/test-task')( + gulp, + 'test', // taskName + ['src/**/*.js', '!src/index.js'], // filesToCover + 'test/**/*-test.js', // testFiles + process.env.TRAVIS ? 'spec' : 'nyan', // reporter + MINIMUM_CODE_COVERAGE // minimumCodeCoverage +); +require('@anyware/gulp-utils/tasks/submit-coverage-task')( + gulp, + 'submit-coverage' // taskName +); +require('@anyware/gulp-utils/tasks/lint-task')( + gulp, + 'lint', // taskName + ["src/**/*.js", "test/**/*.js"] // files +); +require('@anyware/gulp-utils/tasks/transpile-task')( + gulp, + 'build', // taskName + 'src/**/*.js', // targetFiles + 'lib' // destinationDirectory +); + +gulp.task('default', function(callback) { + return runSequence('lint', 'test', 'build', callback); +}); + +gulp.task('watch', ['build'], function watch() { + gulp.watch('src/**/*.js', ['build']); +}); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..57ed1c1 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "@anyware/shared-views", + "version": "1.0.0", + "description": "", + "main": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/anyWareSculpture/shared-views.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/anyWareSculpture/shared-views/issues" + }, + "homepage": "https://github.com/anyWareSculpture/shared-views#readme", + "dependencies": { + "@anyware/game-logic": "^11.1.0", + "promise-decode-audio-data": "^0.2.0" + }, + "devDependencies": { + "@anyware/gulp-utils": "^1.3.3", + "babel": "^5.4.7", + "chai": "^2.3.0", + "eslint": "^0.24.0", + "eslint-plugin-react": "^2.6.2", + "gulp": "^3.9.0", + "rewire": "^2.3.3", + "run-sequence": "^1.1.0", + "sinon": "^1.14.1" + } +} diff --git a/src/audio-api.js b/src/audio-api.js new file mode 100644 index 0000000..0996448 --- /dev/null +++ b/src/audio-api.js @@ -0,0 +1,134 @@ +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +require('promise-decode-audio-data'); + +// FIXME: Defer this to window.onload() ? +const context = initContext(); + +let isNode = false; + +export class Sound { + constructor({ url, loop = false, fadeIn = 0, fadeOut = fadeIn, name = path.basename(url, '.wav') } = {}) { + + assert(url); + + this.url = url; + this.loop = loop; + this.fadeIn = fadeIn; + this.fadeOut = fadeOut; + this.name = name; + this.gain = context.createGain(); + if (!isNode) this.gain.connect(context.destination); + this.head = this.gain; + } + + // Returns a promise to fully load all needed assets for this sound + load() { + console.log('loading ' + this.url); + // FIXME: Node support: + // if (isNode) fetch = promisify(fs.readFile)(__dirname + '/../' + this.url).then(buffer => buffer); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', this.url, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = e => { + if (xhr.status == 200) resolve(xhr.response); + else reject(xhr.response); + } + xhr.onerror = e => reject(e); + xhr.send(); + }) + .then(buffer => { + console.log(`loaded ${this.url} - ${buffer.length} bytes`); + if (!buffer) console.log(`Buffer error: ${this.url}`); + return context.decodeAudioData(buffer); + }) + .then(soundBuffer => { + console.log(`decoded ${this.url}`); + this.buffer = soundBuffer; + return this; + }); + } + + play() { + if (this.fadeIn > 0) { + this.gain.gain.setValueAtTime(0, context.currentTime); + this.gain.gain.linearRampToValueAtTime(1, context.currentTime + this.fadeIn); + } + + this.source = context.createBufferSource(); + this.source.buffer = this.buffer; + this.source.loop = this.loop; + this.source.connect(this.head); + if (isNode) this.gain.connect(context.destination); + this.source.start(context.currentTime); + } + + stop() { + if (this.fadeOut > 0) { + var volume = this.gain.gain.value; + this.gain.gain.cancelScheduledValues(context.currentTime); + this.gain.gain.setValueAtTime(volume, context.currentTime); + this.gain.gain.linearRampToValueAtTime(0,context.currentTime + volume*this.fadeOut); + this.gain.gain.setValueAtTime(1, context.currentTime + volume*this.fadeOut); + if (this.source) this.source.stop(context.currentTime + volume*this.fadeOut); + } + else { + if (this.source) this.source.stop(); + } + } +} + +/* + Sound with a VCF (Voltage Controlled Filter). The VCF is currently hardcoded since we only use it once +*/ +export class VCFSound extends Sound { + constructor({ url, fadeIn = 0, fadeOut = fadeIn, name = path.basename(url, '.wav') } = {}) { + super({url, loop: true, fadeIn, fadeOut, name}); + } + + play() { + // FIXME: If running on node.js + if (!context.createBiquadFilter) return super.play(); + + const lowpass = context.createBiquadFilter(); + lowpass.Q.value = 2; + lowpass.frequency.value = 2200; + lowpass.type = 'lowpass'; + lowpass.connect(this.head); + this.head = lowpass; + + var lfogain = context.createGain(); + lfogain.gain.value = 2000; + + var lfo = context.createOscillator(); + lfo.type = 'sine'; + lfo.frequency.value = 0.333; + lfogain.connect(lowpass.frequency); + lfo.connect(lfogain); + lfo.start(context.currentTime); + + super.play(); + } + + stop() { + super.stop(); + } +} + +let decode; +function initContext() { + if (typeof AudioContext !== "undefined") { + return new AudioContext(); + } else if (typeof webkitAudioContext !== "undefined") { + return new webkitAudioContext(); + } else if (typeof NodeAudioContext !== "undefined") { + isNode = true; + return new NodeAudioContext(); + } + else { + throw new Error('AudioContext not supported. :('); + } +} diff --git a/src/audio-view.js b/src/audio-view.js new file mode 100644 index 0000000..95dce6f --- /dev/null +++ b/src/audio-view.js @@ -0,0 +1,193 @@ +/* + Game event Sound Logic + + Enter alone mode Alone_Mode/Pulse_amb_loop FIXME + Handshake Alone_Mode/Hand_Shake_01 changes.handshakes (Set of bool) + +Mole: + Panel activated Game_01/G01_LED_XX + Active panel touched Game_01/G01_Success_01 + Non-active panel touched Game_01/G01_Negative_01 + +Disk: + +Simon: + Panel activated during pattern animation + Game_03/G03_LED_XX + Correct panel touched + Game_03/G03_LED_XX + Wrong panel touched + Game_03/G03_Negative_01 + Won level (after short delay) + Game_03/G03_Success_01 + Won all levels (after short delay) + Game_03/G03_Light_Show_01 + +*/ + +const _ = require('lodash'); + +const SculptureStore = require('@anyware/game-logic/lib/sculpture-store'); +const GAMES = require('@anyware/game-logic/lib/constants/games'); +import {Sound, VCFSound} from './audio-api'; + +export default class AudioView { + constructor(store, config, dispatcher) { + this.store = store; + this.config = config; + } + + // Loads all sounds, calls callback([err]) when done + load(callback) { + // Maps logical sound identifiers to filenames. We'll load these sounds next. + this.sounds = { + alone: { + ambient: new VCFSound({url: 'sounds/Alone_Mode/Pulse_Amb_Loop.wav', fadeIn: 3}), + handshake: 'sounds/Alone_Mode/Hand_Shake_01.wav' + }, + mole: { + success: 'sounds/Game_01/G01_Success_01.wav', + failure: 'sounds/Game_01/G01_Negative_01.wav', + panels: [0,1,2].map(stripId => _.range(10).map(panelId => `sounds/Game_01/G01_LED_${("0"+(stripId*10+panelId+1)).slice(-2)}.wav`)) + }, +// disk: { +// }, + simon: { + panels: [0,1,2].map(stripId => _.range(10).map(panelId => `sounds/Game_03/G03_LED_${("0"+(stripId*10+panelId+1)).slice(-2)}.wav`)), + success: 'sounds/Game_03/G03_Success_01.wav', + failure: 'sounds/Game_03/G03_Negative_01.wav', + show: 'sounds/Game_03/G03_Light_Show_01.wav' + } + }; + + // Traverse this.sounds and replace the filenames with valid sound objects. + this._promises = []; + this._traverse(this.sounds); + + // _loadSound() will create promises. We call the callback once all promises resolve + console.log(`${this._promises.length} promises`); + Promise.all(this._promises) + // Don't listen to events until we've loaded all sounds + .then(() => this.store.on(SculptureStore.EVENT_CHANGE, this._handleChanges.bind(this))) + .then(() => callback(null)) + .catch(callback.bind(null)); + } + + // Traverses sound config objects and replaces nodes with valid, loaded, sounds + _traverse(node) { + for (let key in node) { + const value = node[key]; + let sound; + if (typeof value === 'string') sound = node[key] = new Sound({url: value}); + else if (value instanceof Sound) sound = value; + if (sound) this._loadSound(sound); + else this._traverse(value); + } + } + + // Returns a promise to provide a comple, usable sound. + _loadSound(sound) { + const promise = sound.load(); + this._promises.push(promise); + return promise; + } + + _handleChanges(changes) { + if (this._animating) { + return; + } + + if (changes.currentGame === GAMES.HANDSHAKE) this.sounds.alone.ambient.play(); + + this._handleHandshakeChanges(changes); + this._handleStatusChanges(changes); + this._handleLightChanges(changes); + } + + _handleHandshakeChanges(changes) { + if (changes.handshakes) { + // Did someone shake my hand? + if (changes.handshakes[this.config.username]) { + this.sounds.alone.ambient.stop(); + this.sounds.alone.handshake.play(); + } + } + } + + _handleStatusChanges(changes) { + if (changes.status) { + let statusSounds; + + if (this.store.isPlayingMoleGame) { + statusSounds = { + [SculptureStore.STATUS_SUCCESS]: this.sounds.mole.success, + [SculptureStore.STATUS_FAILURE]: this.sounds.mole.failure + }; + } + if (this.store.isPlayingSimonGame) { + statusSounds = { + [SculptureStore.STATUS_SUCCESS]: this.sounds.simon.success, + [SculptureStore.STATUS_FAILURE]: this.sounds.simon.failure + }; + } + + const statusSound = statusSounds[changes.status]; + if (statusSound) statusSound.play(); + } + } + + _handleLightChanges(changes) { + const lightChanges = changes.lights; + if (!lightChanges || !this.store.isReady) { + return; + } + + if (this.store.isPlayingMoleGame) { + const lightArray = this.lightArray; + for (let stripId of Object.keys(lightChanges)) { + const panels = lightChanges[stripId].panels; + for (let panelId of Object.keys(panels)) { + const panelChanges = panels[panelId]; + if (panelChanges.intensity > 90) { + this.sounds.mole.panels[stripId][panelId].play(); + } + if (panelChanges.hasOwnProperty("active")) { + if (panelChanges.active) { + const molegame = this.store.currentGameLogic; + const moledata = this.store.data.get('mole'); + const currentTarget = molegame.getTargetPanels(moledata.get('targetIndex')); + + // FIXME: The problem here is that currentTarget gets removed by the MoleGameLogic before this event reaches us. We may have to transport this info in the changes object as that's currently not done. + + if (currentTarget.has(stripId, panelId)) { + this.sounds.mole.success.play(); + } + else { + this.sounds.mole.failure.play(); +// console.log(`Play ${stripId}:${panelId}`); +// this.panelsounds[stripId][panelId].play(); + } + } + } + + } + } + } + if (this.store.isPlayingSimonGame) { + const lightArray = this.lightArray; + for (let stripId of Object.keys(lightChanges)) { + const panels = lightChanges[stripId].panels; + for (let panelId of Object.keys(panels)) { + const panelChanges = panels[panelId]; + if (panelChanges.active || panelChanges.intensity > 90) { + this.sounds.simon.panels[stripId][panelId].play(); + } + } + } + } + } + + get lightArray() { + return this.store.data.get('lights'); + } +} From d680bdbcd6ff55ccb8d9bb64cf5466bcec3e8fa6 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 19:04:22 -0500 Subject: [PATCH 02/12] Removed unused variable --- src/audio-api.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/audio-api.js b/src/audio-api.js index 0996448..89e009b 100644 --- a/src/audio-api.js +++ b/src/audio-api.js @@ -118,7 +118,6 @@ export class VCFSound extends Sound { } } -let decode; function initContext() { if (typeof AudioContext !== "undefined") { return new AudioContext(); From 2857a912d7d59daadf83a76ea76af1fa74409678 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 19:05:12 -0500 Subject: [PATCH 03/12] Removed legacy code --- src/audio-api.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/audio-api.js b/src/audio-api.js index 89e009b..0e8f762 100644 --- a/src/audio-api.js +++ b/src/audio-api.js @@ -121,8 +121,6 @@ export class VCFSound extends Sound { function initContext() { if (typeof AudioContext !== "undefined") { return new AudioContext(); - } else if (typeof webkitAudioContext !== "undefined") { - return new webkitAudioContext(); } else if (typeof NodeAudioContext !== "undefined") { isNode = true; return new NodeAudioContext(); From 553df3cdd91bbd16a942327cdb0a69e37044d22c Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 19:07:44 -0500 Subject: [PATCH 04/12] Clarify that this is about views ahared between runtimes --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1fd577..3fea218 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ ![codecov.io](http://codecov.io/github/anyWareSculpture/shared-views/branch.svg?branch=master) ## Usage -This library models the game logic shared by all anyWare implementations. + +This repository contains shared views used by multiple anyWare runtimes. When installed (or built), modules are stored in a `lib/` directory. Thus when requiring files, make sure that you are adding that before the path of the file you are requiring. In addition, ensure that you are requiring each individual file. `require('@anyware/shared-views')` alone will not work. Example of correct usage: - const MyThing = require('@anyware/shared-views/lib/things/my-thing'); + const AudioView = require('@anyware/shared-views/lib/audio-view); This was implemented this way for a variety of reasons: From 8841f42d9737812b8fd9b1b082931ea111931f83 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 19:47:48 -0500 Subject: [PATCH 05/12] Reduce side effects to make code clearer --- src/audio-view.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/audio-view.js b/src/audio-view.js index 95dce6f..03e4671 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -62,9 +62,9 @@ export default class AudioView { // Traverse this.sounds and replace the filenames with valid sound objects. this._promises = []; - this._traverse(this.sounds); + this._traverse(this.sounds, this._promises); - // _loadSound() will create promises. We call the callback once all promises resolve + // _traverse() will create promises. We call the callback once all promises resolve console.log(`${this._promises.length} promises`); Promise.all(this._promises) // Don't listen to events until we've loaded all sounds @@ -74,24 +74,18 @@ export default class AudioView { } // Traverses sound config objects and replaces nodes with valid, loaded, sounds - _traverse(node) { + // populates the given promises array with promises of loaded sounds + _traverse(node, promises) { for (let key in node) { const value = node[key]; let sound; if (typeof value === 'string') sound = node[key] = new Sound({url: value}); else if (value instanceof Sound) sound = value; - if (sound) this._loadSound(sound); - else this._traverse(value); + if (sound) promises.push(sound.load()); + else this._traverse(value, promises); } } - // Returns a promise to provide a comple, usable sound. - _loadSound(sound) { - const promise = sound.load(); - this._promises.push(promise); - return promise; - } - _handleChanges(changes) { if (this._animating) { return; From d1edd31d1b71fea1ec5a51725c56ca4a6fdc273b Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 19:50:50 -0500 Subject: [PATCH 06/12] Coding style: Update comments to jsdoc --- src/audio-api.js | 10 ++++++---- src/audio-view.js | 10 +++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/audio-api.js b/src/audio-api.js index 0e8f762..98d4649 100644 --- a/src/audio-api.js +++ b/src/audio-api.js @@ -23,7 +23,9 @@ export class Sound { this.head = this.gain; } - // Returns a promise to fully load all needed assets for this sound + /** + * Returns a promise to fully load all needed assets for this sound + */ load() { console.log('loading ' + this.url); // FIXME: Node support: @@ -81,9 +83,9 @@ export class Sound { } } -/* - Sound with a VCF (Voltage Controlled Filter). The VCF is currently hardcoded since we only use it once -*/ +/** + * Sound with a VCF (Voltage Controlled Filter). The VCF is currently hardcoded since we only use it once + */ export class VCFSound extends Sound { constructor({ url, fadeIn = 0, fadeOut = fadeIn, name = path.basename(url, '.wav') } = {}) { super({url, loop: true, fadeIn, fadeOut, name}); diff --git a/src/audio-view.js b/src/audio-view.js index 03e4671..7603dba 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -37,7 +37,9 @@ export default class AudioView { this.config = config; } - // Loads all sounds, calls callback([err]) when done + /** + * Loads all sounds, calls callback([err]) when done + */ load(callback) { // Maps logical sound identifiers to filenames. We'll load these sounds next. this.sounds = { @@ -73,8 +75,10 @@ export default class AudioView { .catch(callback.bind(null)); } - // Traverses sound config objects and replaces nodes with valid, loaded, sounds - // populates the given promises array with promises of loaded sounds + /** + * Traverses sound config objects and replaces nodes with valid, loaded, sounds + * populates the given promises array with promises of loaded sounds + */ _traverse(node, promises) { for (let key in node) { const value = node[key]; From b0bdd953d99c29c0c069cfd3486f3dc4b34a51c6 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 21:34:53 -0500 Subject: [PATCH 07/12] No animation support yet --- src/audio-view.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/audio-view.js b/src/audio-view.js index 7603dba..6241ee6 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -91,10 +91,6 @@ export default class AudioView { } _handleChanges(changes) { - if (this._animating) { - return; - } - if (changes.currentGame === GAMES.HANDSHAKE) this.sounds.alone.ambient.play(); this._handleHandshakeChanges(changes); From b5ad56857a3ffa556a0409ff4609d1b96942dcf9 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 22 Nov 2015 21:44:08 -0500 Subject: [PATCH 08/12] Remove temporary code => separate branch --- src/audio-view.js | 93 +---------------------------------------------- 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/src/audio-view.js b/src/audio-view.js index 6241ee6..ebe4c0c 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -91,97 +91,6 @@ export default class AudioView { } _handleChanges(changes) { - if (changes.currentGame === GAMES.HANDSHAKE) this.sounds.alone.ambient.play(); - - this._handleHandshakeChanges(changes); - this._handleStatusChanges(changes); - this._handleLightChanges(changes); - } - - _handleHandshakeChanges(changes) { - if (changes.handshakes) { - // Did someone shake my hand? - if (changes.handshakes[this.config.username]) { - this.sounds.alone.ambient.stop(); - this.sounds.alone.handshake.play(); - } - } - } - - _handleStatusChanges(changes) { - if (changes.status) { - let statusSounds; - - if (this.store.isPlayingMoleGame) { - statusSounds = { - [SculptureStore.STATUS_SUCCESS]: this.sounds.mole.success, - [SculptureStore.STATUS_FAILURE]: this.sounds.mole.failure - }; - } - if (this.store.isPlayingSimonGame) { - statusSounds = { - [SculptureStore.STATUS_SUCCESS]: this.sounds.simon.success, - [SculptureStore.STATUS_FAILURE]: this.sounds.simon.failure - }; - } - - const statusSound = statusSounds[changes.status]; - if (statusSound) statusSound.play(); - } - } - - _handleLightChanges(changes) { - const lightChanges = changes.lights; - if (!lightChanges || !this.store.isReady) { - return; - } - - if (this.store.isPlayingMoleGame) { - const lightArray = this.lightArray; - for (let stripId of Object.keys(lightChanges)) { - const panels = lightChanges[stripId].panels; - for (let panelId of Object.keys(panels)) { - const panelChanges = panels[panelId]; - if (panelChanges.intensity > 90) { - this.sounds.mole.panels[stripId][panelId].play(); - } - if (panelChanges.hasOwnProperty("active")) { - if (panelChanges.active) { - const molegame = this.store.currentGameLogic; - const moledata = this.store.data.get('mole'); - const currentTarget = molegame.getTargetPanels(moledata.get('targetIndex')); - - // FIXME: The problem here is that currentTarget gets removed by the MoleGameLogic before this event reaches us. We may have to transport this info in the changes object as that's currently not done. - - if (currentTarget.has(stripId, panelId)) { - this.sounds.mole.success.play(); - } - else { - this.sounds.mole.failure.play(); -// console.log(`Play ${stripId}:${panelId}`); -// this.panelsounds[stripId][panelId].play(); - } - } - } - - } - } - } - if (this.store.isPlayingSimonGame) { - const lightArray = this.lightArray; - for (let stripId of Object.keys(lightChanges)) { - const panels = lightChanges[stripId].panels; - for (let panelId of Object.keys(panels)) { - const panelChanges = panels[panelId]; - if (panelChanges.active || panelChanges.intensity > 90) { - this.sounds.simon.panels[stripId][panelId].play(); - } - } - } - } - } - - get lightArray() { - return this.store.data.get('lights'); + // FIXME: Turn changes in sculpture store into audio here } } From c17636f361762b06d5bb4e926610af7034cea199 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 23 Nov 2015 18:21:32 -0500 Subject: [PATCH 09/12] Added disk game assets --- src/audio-view.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/audio-view.js b/src/audio-view.js index ebe4c0c..5e59a80 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -52,8 +52,13 @@ export default class AudioView { failure: 'sounds/Game_01/G01_Negative_01.wav', panels: [0,1,2].map(stripId => _.range(10).map(panelId => `sounds/Game_01/G01_LED_${("0"+(stripId*10+panelId+1)).slice(-2)}.wav`)) }, -// disk: { -// }, + disk: { + ambient: 'sounds/Game_02/G02_Amb_Breath_Loop_01.wav', + loop: 'sounds/Game_02/G02_Disk_Loop_01.wav', + lighteffect: 'sounds/Game_02/G02_Lights_01.wav', + success: 'sounds/Game_02/G02_Success_01.wav', + show: 'sounds/Game_02/G02_Success_final_01.wav' + }, simon: { panels: [0,1,2].map(stripId => _.range(10).map(panelId => `sounds/Game_03/G03_LED_${("0"+(stripId*10+panelId+1)).slice(-2)}.wav`)), success: 'sounds/Game_03/G03_Success_01.wav', From e07dc6ad3fc016c199a23077375c49845d65b8c5 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Tue, 24 Nov 2015 15:29:44 -0500 Subject: [PATCH 10/12] Added rate and gain parameters --- src/audio-api.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/audio-api.js b/src/audio-api.js index 98d4649..2c1fcac 100644 --- a/src/audio-api.js +++ b/src/audio-api.js @@ -9,14 +9,18 @@ const context = initContext(); let isNode = false; export class Sound { - constructor({ url, loop = false, fadeIn = 0, fadeOut = fadeIn, name = path.basename(url, '.wav') } = {}) { + constructor({ url, loop = false, fadeIn = 0, fadeOut = fadeIn, rate = 1, gain = 1, name = path.basename(url, '.wav') } = {}) { assert(url); this.url = url; - this.loop = loop; - this.fadeIn = fadeIn; - this.fadeOut = fadeOut; + this.params = { + loop, + fadeIn, + fadeOut, + rate, + gain + }; this.name = name; this.gain = context.createGain(); if (!isNode) this.gain.connect(context.destination); @@ -43,7 +47,7 @@ export class Sound { xhr.send(); }) .then(buffer => { - console.log(`loaded ${this.url} - ${buffer.length} bytes`); + console.log(`loaded ${this.url} - ${buffer.byteLength} bytes`); if (!buffer) console.log(`Buffer error: ${this.url}`); return context.decodeAudioData(buffer); }) @@ -55,27 +59,29 @@ export class Sound { } play() { - if (this.fadeIn > 0) { + if (this.params.fadeIn > 0) { this.gain.gain.setValueAtTime(0, context.currentTime); - this.gain.gain.linearRampToValueAtTime(1, context.currentTime + this.fadeIn); + this.gain.gain.linearRampToValueAtTime(this.params.gain, context.currentTime + this.params.fadeIn); } this.source = context.createBufferSource(); this.source.buffer = this.buffer; - this.source.loop = this.loop; + this.source.loop = this.params.loop; + if (this.params.rate != 1) this.source.playbackRate.value = this.params.rate; + if (this.params.gain != 1) this.gain.gain.value = this.params.gain; this.source.connect(this.head); if (isNode) this.gain.connect(context.destination); this.source.start(context.currentTime); } stop() { - if (this.fadeOut > 0) { + if (this.params.fadeOut > 0) { var volume = this.gain.gain.value; this.gain.gain.cancelScheduledValues(context.currentTime); this.gain.gain.setValueAtTime(volume, context.currentTime); - this.gain.gain.linearRampToValueAtTime(0,context.currentTime + volume*this.fadeOut); - this.gain.gain.setValueAtTime(1, context.currentTime + volume*this.fadeOut); - if (this.source) this.source.stop(context.currentTime + volume*this.fadeOut); + this.gain.gain.linearRampToValueAtTime(0,context.currentTime + volume*this.params.fadeOut); + this.gain.gain.setValueAtTime(1, context.currentTime + volume*this.params.fadeOut); + if (this.source) this.source.stop(context.currentTime + volume*this.params.fadeOut); } else { if (this.source) this.source.stop(); From 16593a2754eac3bcba99b5159627d16e0c04e847 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Tue, 24 Nov 2015 15:42:23 -0500 Subject: [PATCH 11/12] Configure disk game sounds --- src/audio-view.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/audio-view.js b/src/audio-view.js index 5e59a80..2cf70c1 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -53,8 +53,9 @@ export default class AudioView { panels: [0,1,2].map(stripId => _.range(10).map(panelId => `sounds/Game_01/G01_LED_${("0"+(stripId*10+panelId+1)).slice(-2)}.wav`)) }, disk: { - ambient: 'sounds/Game_02/G02_Amb_Breath_Loop_01.wav', - loop: 'sounds/Game_02/G02_Disk_Loop_01.wav', + ambient: new Sound({url: 'sounds/Game_02/G02_Amb_Breath_Loop_01.wav', loop: true}), + loop: new Sound({url: 'sounds/Game_02/G02_Disk_Loop_Ref_01.wav', loop: true, rate: 2, gain: 0.3, fadeIn: 10}), + distance: new Sound({url: 'sounds/Game_02/G02_Disk_Loop_01.wav', loop: true, rate: 2, gain: 0.5, fadeIn: 10}), lighteffect: 'sounds/Game_02/G02_Lights_01.wav', success: 'sounds/Game_02/G02_Success_01.wav', show: 'sounds/Game_02/G02_Success_final_01.wav' From 3b049d4cffa60f51fc1e65552d7b7aea4055dc44 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Tue, 24 Nov 2015 16:00:49 -0500 Subject: [PATCH 12/12] Implemented handshake sounds in single player mode --- src/audio-view.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/audio-view.js b/src/audio-view.js index 2cf70c1..6d72f7a 100644 --- a/src/audio-view.js +++ b/src/audio-view.js @@ -97,6 +97,19 @@ export default class AudioView { } _handleChanges(changes) { - // FIXME: Turn changes in sculpture store into audio here + if (this.store.isPlayingHandshakeGame) this._handleHandshakeGame(changes); + } + + _handleHandshakeGame(changes) { + // On startup, or when Start State becomes active, play ambient sound + if (changes.currentGame === GAMES.HANDSHAKE) this.sounds.alone.ambient.play(); + + if (changes.handshakes) { + // Did someone shake my hand? + if (changes.handshakes[this.config.username]) { + this.sounds.alone.ambient.stop(); + this.sounds.alone.handshake.play(); + } + } } }