├── node
├── .npmrc
├── public
│ ├── sounds
│ │ ├── notes
│ │ │ ├── ab1.wav
│ │ │ ├── ab2.wav
│ │ │ ├── ab3.wav
│ │ │ ├── bb1.wav
│ │ │ ├── bb2.wav
│ │ │ ├── c1.wav
│ │ │ ├── c2.wav
│ │ │ ├── db1.wav
│ │ │ ├── db2.wav
│ │ │ ├── eb1.wav
│ │ │ ├── eb2.wav
│ │ │ ├── f1.wav
│ │ │ ├── f2.wav
│ │ │ ├── g1.wav
│ │ │ └── g2.wav
│ │ ├── loops
│ │ │ ├── voices.mp3
│ │ │ ├── beatbox.mp3
│ │ │ └── drum_loop.wav
│ │ └── samples
│ │ │ ├── clap.wav
│ │ │ ├── kick.wav
│ │ │ ├── snap.wav
│ │ │ ├── bongo.wav
│ │ │ ├── rimshot.wav
│ │ │ ├── snare.wav
│ │ │ ├── voice.wav
│ │ │ └── woosh.wav
│ ├── fonts
│ │ ├── Quicksand-Bold.ttf
│ │ ├── Quicksand-Light.ttf
│ │ └── Quicksand-Regular.ttf
│ ├── irs
│ │ ├── HOA3_BRIRs-medium_01-08ch.wav
│ │ ├── HOA3_BRIRs-medium_09-16ch.wav
│ │ ├── HOA3_filters_virtual_01-08ch.wav
│ │ ├── HOA3_filters_virtual_09-16ch.wav
│ │ ├── room-medium-1-furnished-src-20-Set1_16b_01-08ch.wav
│ │ └── room-medium-1-furnished-src-20-Set1_16b_09-16ch.wav
│ └── stream
│ │ └── readme.txt
├── .babelrc
├── sass
│ ├── _overrides.scss
│ ├── main.scss
│ ├── _01-mixins.scss
│ ├── _configuration.scss
│ ├── _03-services.scss
│ ├── _02-commons.scss
│ └── _04-survey.scss
├── src
│ ├── client
│ │ ├── player
│ │ │ ├── NuMain.js
│ │ │ ├── index.js
│ │ │ ├── NuBaseModule.js
│ │ │ ├── NuTemplate.js
│ │ │ ├── Nu.js
│ │ │ ├── NuStream.js
│ │ │ ├── NuScore.js
│ │ │ ├── PlayerExperience.js
│ │ │ ├── NuRoomReverb.js
│ │ │ ├── NuOutput.js
│ │ │ ├── NuProbe.js
│ │ │ ├── NuPath.js
│ │ │ ├── NuGroups.js
│ │ │ ├── NuLoop.js
│ │ │ ├── NuSynth.js
│ │ │ └── NuDisplay.js
│ │ └── controller
│ │ │ ├── index.js
│ │ │ └── ControllerExperience.js
│ └── server
│ │ ├── NuProbe.js
│ │ ├── NuLoop.js
│ │ ├── NuSynth.js
│ │ ├── NuDisplay.js
│ │ ├── NuScore.js
│ │ ├── Nu.js
│ │ ├── NuGrain.js
│ │ ├── NuTemplate.js
│ │ ├── NuMain.js
│ │ ├── index.js
│ │ ├── NuGroups.js
│ │ ├── NuBaseModule.js
│ │ ├── NuPath.js
│ │ ├── config
│ │ └── default.js
│ │ ├── PlayerExperience.js
│ │ ├── NuStream.js
│ │ └── NuOutput.js
├── _install_macos.sh
├── bin
│ ├── server
│ ├── styles
│ ├── log
│ └── javascripts
├── html
│ └── default.ejs
└── package.json
├── .gitignore
├── utils
└── openChromeArray.sh
├── LICENSE
├── README.md
└── maxmsp
├── extras
└── nu.error.maxpat
└── patchers
├── nu.template.maxpat
├── nu.output.maxpat
├── nu.synth.maxpat
└── nu.loop.maxpat
/node/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/node/public/sounds/notes/ab1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/ab1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/ab2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/ab2.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/ab3.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/ab3.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/bb1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/bb1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/bb2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/bb2.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/c1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/c1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/c2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/c2.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/db1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/db1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/db2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/db2.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/eb1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/eb1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/eb2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/eb2.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/f1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/f1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/f2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/f2.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/g1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/g1.wav
--------------------------------------------------------------------------------
/node/public/sounds/notes/g2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/notes/g2.wav
--------------------------------------------------------------------------------
/node/public/sounds/loops/voices.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/loops/voices.mp3
--------------------------------------------------------------------------------
/node/public/sounds/samples/clap.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/clap.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/kick.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/kick.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/snap.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/snap.wav
--------------------------------------------------------------------------------
/node/public/fonts/Quicksand-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/fonts/Quicksand-Bold.ttf
--------------------------------------------------------------------------------
/node/public/fonts/Quicksand-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/fonts/Quicksand-Light.ttf
--------------------------------------------------------------------------------
/node/public/sounds/loops/beatbox.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/loops/beatbox.mp3
--------------------------------------------------------------------------------
/node/public/sounds/loops/drum_loop.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/loops/drum_loop.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/bongo.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/bongo.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/rimshot.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/rimshot.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/snare.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/snare.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/voice.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/voice.wav
--------------------------------------------------------------------------------
/node/public/sounds/samples/woosh.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/sounds/samples/woosh.wav
--------------------------------------------------------------------------------
/node/public/fonts/Quicksand-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/fonts/Quicksand-Regular.ttf
--------------------------------------------------------------------------------
/node/public/irs/HOA3_BRIRs-medium_01-08ch.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/irs/HOA3_BRIRs-medium_01-08ch.wav
--------------------------------------------------------------------------------
/node/public/irs/HOA3_BRIRs-medium_09-16ch.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/irs/HOA3_BRIRs-medium_09-16ch.wav
--------------------------------------------------------------------------------
/node/public/irs/HOA3_filters_virtual_01-08ch.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/irs/HOA3_filters_virtual_01-08ch.wav
--------------------------------------------------------------------------------
/node/public/irs/HOA3_filters_virtual_09-16ch.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/irs/HOA3_filters_virtual_09-16ch.wav
--------------------------------------------------------------------------------
/node/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceMap": "inline",
3 | "presets": ["es2015"],
4 | "plugins": ["transform-runtime", "transform-es2015-modules-commonjs"]
5 | }
6 |
--------------------------------------------------------------------------------
/node/public/stream/readme.txt:
--------------------------------------------------------------------------------
1 | This folder is used as a cache for the NuStream module. Audio files saved here will be automatically deleted by the server upon streaming, or at server start.
2 |
--------------------------------------------------------------------------------
/node/public/irs/room-medium-1-furnished-src-20-Set1_16b_01-08ch.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/irs/room-medium-1-furnished-src-20-Set1_16b_01-08ch.wav
--------------------------------------------------------------------------------
/node/public/irs/room-medium-1-furnished-src-20-Set1_16b_09-16ch.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ircam-cosima/soundworks-nu/HEAD/node/public/irs/room-medium-1-furnished-src-20-Set1_16b_09-16ch.wav
--------------------------------------------------------------------------------
/node/sass/_overrides.scss:
--------------------------------------------------------------------------------
1 | // Entry point for application specific styles
2 |
3 | .controller-background {
4 | background-color: black;
5 | }
6 |
7 | .huge {
8 | font-size: 70px;
9 | }
10 |
--------------------------------------------------------------------------------
/node/src/client/player/NuMain.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuMain: misc. config setup
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuMain extends NuBaseModule {
8 | constructor(playerExperience) {
9 | super(playerExperience, 'nuMain');
10 | }
11 |
12 | // reload page
13 | reload(){
14 | window.location.reload(true);
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Transpiled files and dependencies
2 | node/node_modules
3 | node/public/js
4 | node/public/css
5 | node/dist
6 |
7 | # ignore all config except default
8 | node/src/server/config/*
9 | !node/src/server/config/default.js
10 |
11 | # Application instance specific
12 | node/logs
13 | node/db
14 |
15 | # ignore junk files
16 | npm-debug.log
17 | .DS_Store
18 | Thumbs.db
19 |
20 | # dynamic audio file list gen upon transpile
21 | node/src/client/shared/audioFiles.js
--------------------------------------------------------------------------------
/node/src/server/NuProbe.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuProbe: get motion etc. info from given client in OSC
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuProbe extends NuBaseModule {
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuProbe');
10 |
11 | // to be saved params to send to client when connects:
12 | this.params = {
13 | touch: false,
14 | orientation: false,
15 | acceleration: false,
16 | energy: false
17 | };
18 | }
19 |
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/node/src/server/NuLoop.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuLoop: Nu module sequencer-like (drum machine)
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuLoop extends NuBaseModule {
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuLoop');
10 |
11 | // local attributes
12 | this.params = { period: 2.0,
13 | divisions: 16,
14 | jitter: 0.0,
15 | jitterMemory: false,
16 | masterGain: 1.0
17 | };
18 |
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/node/src/server/NuSynth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuSynth: distributed synthetizer, sending "note" information via OSC
3 | * to trigger real notes in local synthetizer
4 | **/
5 |
6 | import NuBaseModule from './NuBaseModule'
7 |
8 | export default class NuSynth extends NuBaseModule {
9 | constructor(serverExperience) {
10 | super(serverExperience, 'nuSynth');
11 |
12 | // local attributes
13 | this.params = {
14 | volume: 1.0,
15 | synthType: 'square',
16 | attackTime: 0.1,
17 | releaseTime: 0.1,
18 | periodicWave: [0, 0, 1, 0]
19 | };
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/node/src/server/NuDisplay.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuDisplay: visual feedback
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuDisplay extends NuBaseModule {
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuDisplay');
10 |
11 | // to be saved params to send to client when connects:
12 | this.params = {
13 | 'feedbackGain': 1.0,
14 | 'enableFeedback': true,
15 | 'restColor': [0,0,0],
16 | 'activeColor': [255, 255, 255],
17 | 'text1': 'clientId',
18 | 'text2': ' ',
19 | 'text3': 'in the forest at night',
20 | };
21 | }
22 |
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/node/sass/main.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Mixins
3 | */
4 | @import '01-mixins';
5 |
6 | /**
7 | * Defines some global layout variables (background-color, text-color, etc...)
8 | * Change values to change the general look of the application
9 | */
10 | @import 'configuration';
11 |
12 | /**
13 | * Class helpers (.small, .big for font sizes, .btn, etc..)
14 | */
15 | @import '02-commons';
16 |
17 | /**
18 | * Services styles.
19 | */
20 | @import '03-services';
21 |
22 | /**
23 | * Default styles for the `survey` scene.
24 | */
25 | @import '04-survey';
26 |
27 | /**
28 | * Entry point for application specific styling and overriding.
29 | */
30 | @import 'overrides';
31 |
--------------------------------------------------------------------------------
/node/_install_macos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # ensure current dir is .
4 | LIB_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
5 | cd $LIB_DIR
6 |
7 | # check for homebrew install
8 | which -s brew
9 | if [[ $? != 0 ]] ; then
10 | # Suggest to install Homebrew
11 | echo 'script aborted: install homebrew to continue (check https://brew.sh/index_fr.html)'
12 | exit
13 | else
14 | brew update
15 | fi
16 |
17 | # install node
18 | brew install node
19 |
20 | # install and transpile project
21 | npm install
22 | npm run transpile
23 |
24 | # output success
25 | echo '\n'
26 | echo '---------------------------'
27 | echo '## installation complete ##'
28 | echo '---------------------------'
29 | echo '\n'
--------------------------------------------------------------------------------
/node/src/client/player/index.js:
--------------------------------------------------------------------------------
1 | // import client side soundworks and player experience
2 | import * as soundworks from 'soundworks/client';
3 | import PlayerExperience from './PlayerExperience';
4 | import serviceViews from '../shared/serviceViews';
5 |
6 | function bootstrap() {
7 | // initialize the client with configuration received
8 | // from the server through the `index.html`
9 | // @see {~/src/server/index.js}
10 | // @see {~/html/default.ejs}
11 | const config = Object.assign({ appContainer: '#container' }, window.soundworksConfig);
12 | soundworks.client.init(config.clientType, config);
13 |
14 | // configure views for the services
15 | soundworks.client.setServiceInstanciationHook((id, instance) => {
16 | if (serviceViews.has(id))
17 | instance.view = serviceViews.get(id, config);
18 | });
19 |
20 | // create client side (player) experience and start the client
21 | const experience = new PlayerExperience(config.assetsDomain);
22 | soundworks.client.start();
23 | }
24 |
25 | window.addEventListener('load', bootstrap);
26 |
--------------------------------------------------------------------------------
/node/src/client/controller/index.js:
--------------------------------------------------------------------------------
1 | // import client side soundworks and player experience
2 | import * as soundworks from 'soundworks/client';
3 | import ControllerExperience from './ControllerExperience';
4 | import serviceViews from '../shared/serviceViews';
5 |
6 | function bootstrap() {
7 | // initialize the client with configuration received
8 | // from the server through the `index.html`
9 | // @see {~/src/server/index.js}
10 | // @see {~/html/default.ejs}
11 | const config = Object.assign({ appContainer: '#container' }, window.soundworksConfig);
12 | soundworks.client.init(config.clientType, config);
13 |
14 | // configure views for the services
15 | soundworks.client.setServiceInstanciationHook((id, instance) => {
16 | if (serviceViews.has(id))
17 | instance.view = serviceViews.get(id, config);
18 | });
19 |
20 | // create client side (player) experience and start the client
21 | const experience = new ControllerExperience(config.assetsDomain);
22 | soundworks.client.start();
23 | }
24 |
25 | window.addEventListener('load', bootstrap);
26 |
--------------------------------------------------------------------------------
/node/src/server/NuScore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuScore: define sequences of sounds to play
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuScore extends NuBaseModule {
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuScore');
10 |
11 | // to be saved params to send to client when connects:
12 | this.params = {
13 | gain: 1.0,
14 | perc: 1.0,
15 | delay: 2.0, // set delay before start score (for rendez-vous time sync. based mechanism)
16 | };
17 | }
18 |
19 | // overwrite enterPlayer to make sure saved score will not be sent to client upon conection (saved in this.params.setScore)
20 | // got to find a better mechanism for this whole saved param spread business..
21 | enterPlayer(client){
22 | // send to new client information regarding current groups parameters
23 | Object.keys(this.params).forEach( (key) => {
24 | if( key != 'startScore' ){
25 | this.e.send(client, this.moduleName, [key, this.params[key]] );
26 | }
27 | });
28 | }
29 |
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/node/src/server/Nu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Nu: Simple wrapper to export all Nu modules
3 | **/
4 |
5 | export { default as Display } from './NuDisplay';
6 | export { default as Output } from './NuOutput';
7 | export { default as Groups } from './NuGroups';
8 | export { default as RoomReverb } from './NuRoomReverb';
9 | export { default as Path } from './NuPath';
10 | export { default as Loop } from './NuLoop';
11 | export { default as Template } from './NuTemplate';
12 | export { default as Grain } from './NuGrain';
13 | export { default as Probe } from './NuProbe';
14 | export { default as Synth } from './NuSynth';
15 | export { default as Stream } from './NuStream';
16 | export { default as Main } from './NuMain';
17 | export { default as Score } from './NuScore';
18 |
19 | // ------------------------------------------------------------
20 | // UTILS
21 | // ------------------------------------------------------------
22 |
23 | // convert "stringified numbers" (e.g. '10.100') element of arayIn to Numbers
24 | Array.prototype.numberify = function() {
25 | this.forEach( (elmt, index) => {
26 | if( !isNaN(elmt) )
27 | this[index] = Number(this[index]);
28 | });
29 | return this;
30 | };
--------------------------------------------------------------------------------
/node/bin/server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var colors = require('colors');
3 | var fse = require('fs-extra');
4 | var log = require('./log');
5 | var path = require('path');
6 | var childProcess = require('child_process');
7 |
8 | 'use strict';
9 |
10 | var cwd = process.cwd();
11 |
12 | /**
13 | * Process hosting the server
14 | */
15 | var server = null;
16 |
17 | /**
18 | * Run the `serverIndex` in a forked process
19 | */
20 | function start(serverIndex) {
21 | try {
22 | if (fse.statSync(serverIndex).isFile()) {
23 | if (server)
24 | stop();
25 |
26 | log.serverStart();
27 | server = childProcess.fork(serverIndex);
28 | }
29 | } catch(err) {
30 | log.serverError(serverIndex);
31 | }
32 | }
33 |
34 | /**
35 | * Kill the forked process hosting the server
36 | */
37 | function stop() {
38 | if (server) {
39 | log.serverStop();
40 | server.kill();
41 | server = null;
42 | }
43 | }
44 |
45 | /**
46 | *
47 | */
48 | function restart() {
49 | stop();
50 | start();
51 | }
52 |
53 | /**
54 | * Kill server on uncaughtException
55 | */
56 | process.on('uncaughtException', function (err) {
57 | console.log('Uncaught Exception: '.red);
58 | console.error(err.stack);
59 |
60 | stop();
61 | process.exit();
62 | });
63 |
64 | module.exports = {
65 | start: start,
66 | stop: stop,
67 | restart: restart,
68 | };
69 |
70 |
71 |
--------------------------------------------------------------------------------
/node/sass/_01-mixins.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------
2 | // MIXINS
3 | // -----------------------------------------------------------------
4 |
5 | @mixin font-face($family, $weight, $file) {
6 | $filepath: "../fonts/" + $file;
7 | @font-face {
8 | font-family: "#{$family}";
9 | font-weight: $weight;
10 | src: url($filepath + ".ttf") format('truetype');
11 | }
12 | }
13 |
14 | @mixin keyframes($animation-name) {
15 | @-webkit-keyframes #{$animation-name} {
16 | @content;
17 | }
18 | @-moz-keyframes #{$animation-name} {
19 | @content;
20 | }
21 | @-ms-keyframes #{$animation-name} {
22 | @content;
23 | }
24 | @-o-keyframes #{$animation-name} {
25 | @content;
26 | }
27 | @keyframes #{$animation-name} {
28 | @content;
29 | }
30 | }
31 |
32 | @mixin prefix($name, $value) {
33 | @each $vendor in ('-webkit-', '-moz-', '-ms-', '-o-', '') {
34 | #{$vendor}#{$name}: #{$value};
35 | }
36 | }
37 |
38 | @mixin transition($str) {
39 | -webkit-transition: $str;
40 | -moz-transition: $str;
41 | -ms-transition: $str;
42 | -o-transition: $str;
43 | transition: $str;
44 | }
45 |
46 | @mixin animation($str) {
47 | -webkit-animation: #{$str};
48 | -moz-animation: #{$str};
49 | -ms-animation: #{$str};
50 | -o-animation: #{$str};
51 | animation: #{$str};
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/node/src/server/NuGrain.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuGrain: Granular synthesis (based on soundworks-shaker). An audio track is segmented
3 | * and segments are sorted by loudness. Segments are afterwards playing in a sequencer,
4 | * the current active segment being selected based on shaking energy or OSC client sent
5 | * energy.
6 | **/
7 |
8 | import NuBaseModule from './NuBaseModule'
9 |
10 | export default class NuGrain extends NuBaseModule {
11 | constructor(serverExperience) {
12 | super(serverExperience, 'nuGrain');
13 |
14 | // local attributes
15 | this.params = {
16 | gain: 1.0,
17 | enable: 0,
18 | override: 1.0,
19 | energy: 0,
20 | randomVar: 1,
21 | engineParams: {}
22 | };
23 |
24 | }
25 |
26 | /**
27 | * had to redefine the enterPlayer method here to send a "reset" message once
28 | * all initial parameters were sent, to make sure the granular engine of the
29 | * new player will indeed take into account said parameters
30 | **/
31 | enterPlayer(client){
32 | // send to new client information regarding current groups parameters
33 | Object.keys(this.params).forEach( (key) => {
34 | this.e.send(client, this.moduleName, [key, this.params[key]] );
35 | });
36 | // reset granular engine to take preset values into account
37 | this.e.send( client, this.moduleName, ['reset'] );
38 | }
39 |
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/utils/openChromeArray.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CHROME_APP=/Applications/Browsers/Google\ Chrome.app/Contents/MacOS/Google\ Chrome
4 | URL="http://127.0.0.1:8000"
5 |
6 | windowWidth=300
7 | windowHeight=300
8 | I=4
9 | J=4
10 |
11 | if [[ $1 == full ]]; then
12 | echo "full mode"
13 | I=4
14 | J=4
15 | windowHeight=100
16 | windowWidth=100
17 | fi
18 |
19 | for i in $(seq 1 $I);
20 | do
21 | posY=$(( ($i - 1) * windowWidth ))
22 | for j in $(seq 1 $J);
23 | do
24 | posX=$(( ($j - 1) * windowHeight ))
25 | # echo "window pos: $posX $posY dim: $windowWidth $windowHeight"
26 |
27 | # "${CHROME_APP}" --user-data-dir=${PROFILE} --window-size=$windowWidth,$windowHeight --window-position=$posX,$posY --app=$URL
28 |
29 | "${CHROME_APP}" --profile-directory="Default" \
30 | --app="data:text/html,
31 |
" &
38 | sleep 0.1
39 | if [[ $1 != full ]]; then
40 | sleep 0.7 # ensures order in client pos / id
41 | fi
42 |
43 | done
44 | done
45 |
46 |
47 | # if [[ $1 == full ]]; then
48 | # read -p "Press any key to continue... "
49 | # pkill -f "${CHROME_APP}"
50 | # fi
51 |
52 | # "${CHROME_APP}"
53 |
54 | # --window-size=800,600 --window-position=580,240 --app="http://127.0.0.1:8000"
--------------------------------------------------------------------------------
/node/src/server/NuTemplate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuTemplate: example of how to create a Nu module
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuTemplate extends NuBaseModule{
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuTemplate');
10 |
11 | // to be saved params to send to client when connects:
12 | this.params = { gain: 1.0, fileId: 0 };
13 |
14 | // binding
15 | this.serverMethod = this.serverMethod.bind(this);
16 | }
17 |
18 | // send a global timed instruction to all players from OSC client
19 | serverMethod(args){
20 | console.log('--', args)
21 | // extract arguments
22 | let playerId = args[0];
23 | let delay = args[1];
24 |
25 | // define rdv time (sec) in which to blink synchronously
26 | let rdvTime = this.e.sync.getSyncTime() + delay;
27 | console.log(rdvTime, delay, this.e.sync.getSyncTime());
28 |
29 | // send to all a rdv time
30 | if( playerId == -1 ){
31 | this.e.broadcast('player', null, 'nuTemplate_methodTriggeredFromServer', rdvTime );
32 | return;
33 | }
34 |
35 | // msg is player specific: get player from server map
36 | let client = this.e.playerMap.get( playerId );
37 | // discard if player not defined
38 | if( client === undefined ){ return; }
39 | // send player specific msg
40 | this.e.send(client, 'nuTemplate_methodTriggeredFromServer', rdvTime );
41 | }
42 |
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/node/src/client/player/NuBaseModule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuBaseModule: base class extended by all Nu modules
3 | **/
4 |
5 | import * as soundworks from 'soundworks/client';
6 | const client = soundworks.client;
7 | const audioContext = soundworks.audioContext;
8 |
9 | export default class NuBaseModule {
10 | constructor(playerExperience, moduleName) {
11 |
12 | // local attributes
13 | this.e = playerExperience;
14 | this.moduleName = moduleName;
15 | this.params = {};
16 |
17 | // setup receive callbacks
18 | this.e.receive(this.moduleName, (args) => {
19 | // get header
20 | let name = args.shift();
21 | // convert singleton array if need be
22 | args = (args.length == 1) ? args[0] : args;
23 | // process msg
24 | this.paramCallback(name, args);
25 | });
26 |
27 | // notify module is ready to receive msg
28 | this.e.send('moduleReady', this.moduleName);
29 |
30 | // binding
31 | this.paramCallback = this.paramCallback.bind(this);
32 | }
33 |
34 | /**
35 | * default callback applied to all incoming 'OSC' messages
36 | * (come from OSC at least, the protocol would however be web-socket
37 | * since message pre-processed by soundworks server
38 | **/
39 | paramCallback(name, args){
40 | // either route to internal function
41 | if( this[name] !== undefined ){ this[name]( args ); }
42 | // or to this.params value
43 | else{ this.params[name] = args; }
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/node/bin/styles:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var fse = require('fs-extra');
3 | var klaw = require('klaw');
4 | var log = require('./log');
5 | var path = require('path');
6 | var sass = require('node-sass');
7 |
8 | 'use strict';
9 |
10 | /**
11 | * Find all files recursively in `srcDirectory` that pass the allowed function
12 | * and are not prefixed by `_`. Transpile them to the `distDirectory`
13 | */
14 | function processSass(srcDirectory, distDirectory, isAllowed, sassOptions) {
15 | klaw(srcDirectory)
16 | .on('data', function(item) {
17 | var filename = item.path;
18 | var basename = path.basename(filename);
19 |
20 | if (isAllowed(filename) && !/^_/.test(basename)) {
21 | var relFilename = path.relative(srcDirectory, filename);
22 | var outFilename = path.join(distDirectory, relFilename);
23 | outFilename = outFilename.replace(/\.scss$/, '.css');
24 |
25 | Object.assign(sassOptions, {
26 | file: filename,
27 | outFilename: outFilename
28 | });
29 |
30 | sass.render(sassOptions, function(err, result) {
31 | if (err)
32 | return log.sassError(err);
33 |
34 | fse.outputFile(outFilename, result.css, function(err) {
35 | if (err)
36 | return console.error(err.message);
37 |
38 | log.sassSuccess(outFilename);
39 | });
40 | });
41 | }
42 | });
43 | }
44 |
45 | module.exports = {
46 | process: processSass,
47 | };
48 |
--------------------------------------------------------------------------------
/node/src/client/player/NuTemplate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuTemplate: example of how to create a Nu module
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 | import * as soundworks from 'soundworks/client';
7 |
8 | const client = soundworks.client;
9 | const audioContext = soundworks.audioContext;
10 |
11 | export default class NuTemplate extends NuBaseModule {
12 | constructor(playerExperience) {
13 | super(playerExperience, 'nuTemplate');
14 |
15 | // local attributes
16 | this.params = {
17 | 'gain': 1.0
18 | };
19 |
20 | // binding
21 | this.directToClientMethod = this.directToClientMethod.bind(this);
22 | this.methodTriggeredFromServer = this.methodTriggeredFromServer.bind(this);
23 |
24 | // setup receive callbacks
25 | this.e.receive('nuTemplate_methodTriggeredFromServer', this.methodTriggeredFromServer);
26 | }
27 |
28 | // trigger event directly from OSC client
29 | directToClientMethod(value){
30 | this.e.renderer.blink([0, this.params.gain * value, 0], 0.4);
31 | console.log('blinking now!');
32 | }
33 |
34 | // re-routed event for sync. playback: server add a rdv time to msg sent by OSC client
35 | methodTriggeredFromServer(rdvTime){
36 | // get rel time (sec) in which I must blink
37 | let timeRemaining = rdvTime - this.e.sync.getSyncTime();
38 | console.log('will blink in', timeRemaining, 'seconds');
39 | setTimeout( () => { this.e.renderer.blink([0, 160, 200], 0.4); }, timeRemaining * 1000 );
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, IRCAM – Centre Pompidou (France, Paris)
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above copyright notice, this
12 | list of conditions and the following disclaimer in the documentation and/or
13 | other materials provided with the distribution.
14 |
15 | * Neither the name of the IRCAM nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/node/html/default.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <%= data.appName %>
10 | <% if (data.clientType !== data.defaultType) { %>
11 | | <%= data.clientType %>
12 | <% } %>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 | <% if (data.env === 'production') { %>
27 |
28 | <% } else { %>
29 |
30 | <% } %>
31 |
32 | <% if (data.env === 'production' && data.gaId) { %>
33 |
42 | <% } %>
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nü Soundworks
2 |
3 | Framework source code. See the [project website](https://ircam-cosima.github.io/soundworks-nu/) for more details.
4 |
5 | ## License and Credits
6 |
7 | The Nü framework is released under the [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause).
8 |
9 | Nü has been developped at IRCAM-CNRS within the [CoSiMa](http://cosima.ircam.fr/) research project, supported by the French National Research Agency (ANR).
10 |
11 | ## Install Node.js
12 |
13 | Node.js or "npm" is a toolbox / framework / magic wizard for javascript & web developers, required to run Nü. Check the official [Node.js installation guide](https://docs.npmjs.com/getting-started/installing-node). Nü has been developped with npm 3.9.5 and node v6.2.2.
14 |
15 | ## Install Nü (main)
16 |
17 | ```sh
18 | git clone https://github.com/ircam-cosima/soundworks-nu.git soundworks-nu
19 | cd soundworks-nu/node
20 | npm install
21 | echo '## DEV ## working with develop version of soundworks even here, requires transpile'
22 | cd node_modules/soundworks
23 | npm run transpile
24 | cd ../..
25 | echo '## DEV ##'
26 | npm run transpile
27 | npm run start
28 | ```
29 |
30 | ## Install Nü (for nü developers only)
31 |
32 | ```sh
33 | git clone https://github.com/ircam-cosima/soundworks-nu
34 | cd soundworks-nu
35 | git checkout develop
36 | git pull
37 | cd node
38 | npm install
39 | cd node_modules/soundworks
40 | npm run transpile
41 | cd ../..
42 | npm run watch
43 | ```
44 |
45 | ## How to use
46 |
47 | * Start the server (see Install)
48 | * Connect client to server (default: open your browser at 127.0.0.1:8000)
49 | * Use Max/MSP modules to control client's behavior (starting with nu.main)
50 |
--------------------------------------------------------------------------------
/node/src/server/NuMain.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuMain: misc. config setup
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuMain extends NuBaseModule {
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuMain');
10 |
11 | setTimeout( () => {
12 | // sync. clocks
13 | const clockInterval = 0.1; // refresh interval in seconds
14 | setInterval( () => {
15 | this.e.osc.send('/nuMain/clock', this.e.sync.getSyncTime());
16 | }, 1000 * clockInterval);
17 | }, 1000);
18 |
19 | }
20 |
21 | enterPlayer(client){
22 |
23 | // direct forward of players message to OSC client
24 | this.e.receive(client, 'osc', (header, args) => {
25 | // append client index to msg
26 | args.unshift(client.index);
27 | // forward to OSC
28 | this.e.osc.send(header, args);
29 | });
30 | }
31 |
32 | exitPlayer(client){
33 | // update local attributes
34 | this.e.coordinatesMap.delete( client.index );
35 | // update osc mapper
36 | this.e.osc.send('/nuMain/playerRemoved', client.index );
37 | }
38 |
39 | /**
40 | * method triggered by OSC client upon connection, requiring an update on all
41 | * the "knowledge" the server already gathered about the current experiment setup
42 | **/
43 | updateRequest(){
44 | // send back players position at osc client request
45 | this.e.coordinatesMap.forEach((item, key)=>{
46 | this.e.osc.send('/nuMain/playerPos', [key, item[0], item[1]] );
47 | });
48 | }
49 |
50 | // force all players to reload the current page
51 | reloadPlayers(){
52 | // re-route to clients
53 | this.e.broadcast( 'player', null, 'nuMain', ['reload'] );
54 | }
55 |
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/node/src/client/player/Nu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Nu: Simple wrapper to export all Nu modules
3 | **/
4 |
5 | export { default as Output } from './NuOutput';
6 | export { default as Groups } from './NuGroups';
7 | export { default as RoomReverb } from './NuRoomReverb';
8 | export { default as Path } from './NuPath';
9 | export { default as Loop } from './NuLoop';
10 | export { default as Template } from './NuTemplate';
11 | export { default as Grain } from './NuGrain';
12 | export { default as Probe } from './NuProbe';
13 | export { default as Synth } from './NuSynth';
14 | export { default as Stream } from './NuStream';
15 | export { default as Main } from './NuMain';
16 | export { default as Score } from './NuScore';
17 |
18 | /**
19 | * the NuDisplay is a bit specific, not exposed as other modules
20 | * but rather invoked directly in PlayerExperience
21 | **/
22 | // export { default as Display } from './NuDisplay';`
23 |
24 |
25 | // ------------------------------------------------------------
26 | // UTILS
27 | // ------------------------------------------------------------
28 |
29 | // fix for Safari that doesn't implement Float32Array.slice yet
30 | if (!Float32Array.prototype.slice) {
31 | Float32Array.prototype.slice = function(begin, end) {
32 | var target = new Float32Array(end - begin);
33 |
34 | for (var i = 0; i < begin + end; ++i) {
35 | target[i] = this[begin + i];
36 | }
37 | return target;
38 | };
39 | }
40 |
41 | // couterpart of copyToChannel, without overwrite
42 | AudioBuffer.prototype.addToChannel = function( source, channelNumber, startInChannel ){
43 | let chData = this.getChannelData( channelNumber );
44 | let l = source.length;
45 |
46 | for (let i = 0; i < l; i++){
47 | chData[startInChannel + i] += source[i];
48 | }
49 | return this;
50 | }
--------------------------------------------------------------------------------
/node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "soundworks-nu",
3 | "authors": [
4 | "Jean-Philippe Lambert",
5 | "David Poirier-Quinot"
6 | ],
7 | "description": "Modules for real-time distributed rendering on cellphones",
8 | "license": "BSD-3-Clause",
9 | "version": "1.1.1",
10 | "scripts": {
11 | "minify": "node ./bin/runner --minify",
12 | "prewatch": "npm run transpile",
13 | "start": "node ./bin/runner --start",
14 | "transpile": "node ./bin/runner --transpile",
15 | "watch": "node --harmony ./bin/runner --watch"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/ircam-cosima/soundworks-nu"
20 | },
21 | "dependencies": {
22 | "ambisonics": "0.2.3",
23 | "audio": "1.2.0",
24 | "audio-encode-wav": "0.2.0",
25 | "babel-runtime": "6.26.0",
26 | "bufferutil": "1.3.0",
27 | "debug": "2.6.9",
28 | "directory-tree": "2.2.4",
29 | "fs": "0.0.1-security",
30 | "osc": "2.2.4",
31 | "recorderjs": "mattdiamond/Recorderjs.git",
32 | "soundworks": "collective-soundworks/soundworks.git#v2.1.0",
33 | "source-map-support": "0.4.18",
34 | "typedarray-methods": "1.0.1",
35 | "utf-8-validate": "1.2.2",
36 | "waves-lfo": "wavesjs/waves-lfo#v1.0.1",
37 | "web-audio-api": "0.2.2",
38 | "webworker-threads": "0.7.17",
39 | "ws": "1.1.5"
40 | },
41 | "devDependencies": {
42 | "babel-core": "6.26.3",
43 | "babel-plugin-transform-es2015-modules-commonjs": "6.26.2",
44 | "babel-plugin-transform-runtime": "6.23.0",
45 | "babel-preset-es2015": "6.24.1",
46 | "browserify": "14.5.0",
47 | "colors": "1.4.0",
48 | "fs-extra": "2.1.2",
49 | "klaw": "1.3.1",
50 | "node-sass": "4.14.1",
51 | "uglify-js": "3.10.2",
52 | "watch": "1.0.2",
53 | "watchify": "3.11.1"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/node/src/server/index.js:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register'; // enable sourcemaps in node
2 | import path from 'path';
3 | import * as soundworks from 'soundworks/server';
4 | import PlayerExperience from './PlayerExperience';
5 |
6 | const configName = process.env.ENV || 'default';
7 | const configPath = path.join(__dirname, 'config', configName);
8 | let config = null;
9 |
10 | // rely on node `require` for synchronicity
11 | try {
12 | config = require(configPath).default;
13 | } catch(err) {
14 | console.error(`Invalid ENV "${configName}", file "${configPath}.js" not found`);
15 | process.exit(1);
16 | }
17 |
18 | // configure express environment ('production' enables cache systems)
19 | process.env.NODE_ENV = config.env;
20 | // initialize application with configuration options
21 | soundworks.server.init(config);
22 |
23 | // define the configuration object to be passed to the `.ejs` template
24 | soundworks.server.setClientConfigDefinition((clientType, config, httpRequest) => {
25 | return {
26 | clientType: clientType,
27 | env: config.env,
28 | appName: config.appName,
29 | websockets: config.websockets,
30 | version: config.version,
31 | defaultType: config.defaultClient,
32 | assetsDomain: config.assetsDomain,
33 | };
34 | });
35 |
36 | // create the experience
37 | // activities must be mapped to client types:
38 | // - the `'player'` clients (who take part in the scenario by connecting to the
39 | // server through the root url) need to communicate with the `checkin` (see
40 | // `src/server/playerExperience.js`) and the server side `playerExperience`.
41 | // - we could also map activities to additional client types (thus defining a
42 | // route (url) of the following form: `/${clientType}`)
43 | const experience = new PlayerExperience(['player', 'controller']);
44 |
45 | // start application
46 | soundworks.server.start();
47 |
--------------------------------------------------------------------------------
/node/sass/_configuration.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------
2 | // GLOBALS
3 | // -----------------------------------------------------------------
4 |
5 | $backgroundColor: #080808;
6 | $foregroundColor: #ffffff;
7 | $textColor: rgba(255, 255, 255, 0.9);
8 | $borderSize: 1px;
9 | $borderRadius: 2px;
10 | $mainFont: Quicksand, arial, sans-serif;
11 | $loadingProgressBarHeight: 10px;
12 |
13 | // -----------------------------------------------------------------
14 | // IMPORTS FONTS
15 | // -----------------------------------------------------------------
16 |
17 | @include font-face('Quicksand', 100, 'Quicksand-Light');
18 | @include font-face('Quicksand', 400, 'Quicksand-Regular');
19 | @include font-face('Quicksand', 700, 'Quicksand-Bold');
20 |
21 |
22 | // -----------------------------------------------------------------
23 | // COLOR LIST
24 | // -----------------------------------------------------------------
25 |
26 | /**
27 | * These color swatches were inspired by the swatches
28 | * found in Flat UI, by designmodo.
29 | * http://designmodo.com/flat-free/
30 | */
31 |
32 | $color-list: (
33 | turquoise #1abc9c,
34 | green-sea #16a085,
35 | emerald #2ecc71,
36 | nephritis #27ae60,
37 | peter-river #3498db,
38 | belize-hole #2980b9,
39 | amethyst #9b59b6,
40 | wisteria #8e44ad,
41 | wet-asphalt #34495e,
42 | midnight-blue #2c3e50,
43 | sun-flower #f1c40f,
44 | orange #f39c12,
45 | carrot #e67e22,
46 | pumpkin #d35400,
47 | alizarin #e74c3c,
48 | pomegranate #c0392b,
49 | clouds #ecf0f1,
50 | silver #bdc3c7,
51 | concrete #95a5a6,
52 | asbestos #7f8c8d,
53 | black #000000,
54 | white #ffffff
55 | );
56 |
57 | $turquoise: #1abc9c;
58 | $green-sea: #16a085;
59 | $emerald: #2ecc71;
60 | $nephritis: #27ae60;
61 | $peter-river: #3498db;
62 | $belize-hole: #2980b9;
63 | $amethyst: #9b59b6;
64 | $wisteria: #8e44ad;
65 | $wet-asphalt: #34495e;
66 | $midnight-blue: #2c3e50;
67 | $sun-flower: #f1c40f;
68 | $orange: #f39c12;
69 | $carrot: #e67e22;
70 | $pumpkin: #d35400;
71 | $alizarin: #e74c3c;
72 | $pomegranate: #c0392b;
73 | $clouds: #ecf0f1;
74 | $silver: #bdc3c7;
75 | $concrete: #95a5a6;
76 | $asbestos: #7f8c8d;
77 | $black: #000000;
78 | $white: #ffffff;
79 |
--------------------------------------------------------------------------------
/node/src/client/player/NuStream.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuStream: live audio stream from OSC client to players
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 | import * as soundworks from 'soundworks/client';
7 |
8 | const client = soundworks.client;
9 | const audioContext = soundworks.audioContext;
10 |
11 | export default class NuStream extends NuBaseModule {
12 | constructor(playerExperience) {
13 | super(playerExperience, 'nuStream');
14 |
15 | // local attributes
16 | this.params = {};
17 |
18 | // binding
19 | this.rawSocketCallback = this.rawSocketCallback.bind(this);
20 |
21 | // setup socket reveive callbacks (receiving raw audio data)
22 | this.e.rawSocket.receive('nuStream', this.rawSocketCallback );
23 |
24 | // output gain
25 | this.out = audioContext.createGain();
26 | this.out.connect( this.e.nuOutput.in );
27 | }
28 |
29 | // set audio gain out
30 | gain(val){
31 | this.out.gain.value = val;
32 | }
33 |
34 | /**
35 | * enable / disable streaming (only thing enabled / disabled here is visual feedback,
36 | * actual streaming is done automatically when receiving audio data from dedicated web-socket
37 | **/
38 | onOff(value){
39 | if( value ){ this.e.renderer.enable(); }
40 | else{ this.e.renderer.disable(); }
41 | }
42 |
43 | /*
44 | * callback executed when rawsocket data received from server (streamed audio data)
45 | */
46 | rawSocketCallback(data) {
47 |
48 | // decode
49 | let packetId = Math.round( data.slice(0, 1) * 100 ) / 100; // other digit are not relevant
50 | let audioArray = new Float32Array(data.slice(1, data.length));
51 |
52 | // get start time
53 | const now = this.e.sync.getSyncTime();
54 | let sysTime = this.params.startTime + ( packetId ) * this.params.packetTime + this.params.delayTime;
55 | let relOffset = sysTime - now;
56 |
57 | // discard data if start time passed (packet deprecated)
58 | if( relOffset < 0 ){
59 | this.e.renderer.blink([100, 0, 0]);
60 | return;
61 | }
62 |
63 | // create audio buffer
64 | let audioBuffer = audioContext.createBuffer(1, audioArray.length, 44100);
65 | audioBuffer.getChannelData(0).set(audioArray);
66 |
67 | // create audio source
68 | let src = audioContext.createBufferSource();
69 | src.buffer = audioBuffer;
70 | src.connect( this.out );
71 |
72 | // start source
73 | src.start(audioContext.currentTime + relOffset);
74 |
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/node/src/server/NuGroups.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuGroup: Nu module to assign audio tracks to groups of players
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 |
7 | export default class NuGroups extends NuBaseModule {
8 | constructor(serverExperience) {
9 | super(serverExperience, 'nuGroups');
10 |
11 | // binding
12 | this.getGroup = this.getGroup.bind(this);
13 | this.enterPlayer = this.enterPlayer.bind(this);
14 |
15 | // local attributes
16 | this.groupMap = new Map();
17 | }
18 |
19 | /**
20 | * override default paramCallback from parent, to be able to redefine how OSC
21 | * parameters are copied locally (based on the "groupMap" object)
22 | **/
23 | paramCallback(msg){
24 | let playerId = msg.shift();
25 | let name = msg.shift();
26 | let msgStippedOfPlayerId = [name].concat(msg);
27 |
28 | // if player specific instruction
29 | if( playerId !== -1 ){
30 | let client = this.e.playerMap.get( playerId );
31 | if( client === undefined ){ return; }
32 | this.e.send( client, this.moduleName, msgStippedOfPlayerId );
33 | }
34 |
35 | // if instruction concerns all the players
36 | else{
37 | // broadcast msg
38 | this.e.broadcast( 'player', null, this.moduleName, msgStippedOfPlayerId );
39 | // store value
40 | let groupId = msg.shift();
41 | let value = msg.shift();
42 | // get associated group
43 | let group = this.getGroup( groupId );
44 | // save values
45 | group[name] = value;
46 | }
47 | }
48 |
49 | // get local group based on id
50 | getGroup(groupId) {
51 | // get already existing group
52 | if (this.groupMap.has(groupId))
53 | return this.groupMap.get(groupId);
54 | // create new group
55 | let group = { time: 0, startTime: 0, onOff: 0, volume: 1, loop: 1 };
56 | // store new group in local map
57 | this.groupMap.set(groupId, group);
58 | // return created group
59 | return group;
60 | }
61 |
62 | /**
63 | * override default enterPlayer from parent, to be able to redefine how OSC
64 | * parameters are bundled to connecting clients (based on the "groupMap" object)
65 | **/
66 | enterPlayer(client){
67 | // send to new client information regarding current groups parameters
68 | this.groupMap.forEach( (group, groupId) => {
69 | Object.keys(group).forEach( (key) => {
70 | this.e.send(client, 'nuGroups', [key, groupId, group[key]]);
71 | });
72 | });
73 | }
74 |
75 | }
--------------------------------------------------------------------------------
/node/src/client/player/NuScore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuScore: define sequences of sounds to play
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 | import * as soundworks from 'soundworks/client';
7 |
8 | const client = soundworks.client;
9 | const audioContext = soundworks.audioContext;
10 |
11 | export default class NuScore extends NuBaseModule {
12 | constructor(playerExperience) {
13 | super(playerExperience, 'nuScore');
14 |
15 | // local attributes
16 | this.masterGain = audioContext.createGain();
17 | this.masterGain.connect(this.e.nuOutput.in);
18 | this.srcMap = new Map();
19 | }
20 |
21 | // set audio gain out
22 | gain(val){
23 | this.masterGain.gain.value = val;
24 | }
25 |
26 | /*
27 | * message callback: play sound
28 | */
29 | startScore( args ) {
30 | let timeSent = args.shift();
31 | let nuNotes = args.length/2;
32 | var time, name, buffer, startTime, duration;
33 |
34 | // define times
35 | var now = this.e.sync.getSyncTime();
36 | var syncStartTime = timeSent + this.params.delay - now;
37 |
38 | // loop over score samples, fill in output buffer with samples
39 | for (let i = 0; i < nuNotes; i++) {
40 | // get note params
41 | time = args[ 2*i ];
42 | name = args[ 2*i + 1 ];
43 | buffer = this.e.loader.data[name];
44 |
45 | // skip note if undefined audio buffer
46 | if( buffer === undefined ){ continue; }
47 |
48 | // create and connect source
49 | let src = audioContext.createBufferSource();
50 | src.buffer = buffer;
51 | src.connect( this.masterGain );
52 |
53 | // start source
54 | startTime = syncStartTime + time;
55 | duration = this.params.perc * buffer.duration;
56 | // play in future if rendez-vous time not yet reached (should be default behavior)
57 | if (startTime > 0){ src.start(audioContext.currentTime + startTime, 0, duration); }
58 | // play with advance in buffer to keep sync.
59 | else { src.start(audioContext.currentTime, -startTime, duration); }
60 |
61 | // add src to local map for eventual reset
62 | this.srcMap.set(src, src);
63 | src.onended = () => {
64 | this.srcMap.delete(src);
65 | this.e.renderer.disable();
66 | };
67 |
68 | // enable visual rendering
69 | this.e.renderer.enable();
70 | }
71 |
72 | }
73 |
74 | // kill audio
75 | reset(){
76 | this.srcMap.forEach( (src) => {
77 | // stop source
78 | src.stop();
79 | // remove associated visual feedback
80 | this.e.renderer.disable();
81 | });
82 | }
83 |
84 | }
--------------------------------------------------------------------------------
/node/src/server/NuBaseModule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuBaseModule: base class extended by all Nu modules
3 | **/
4 |
5 | export default class NuBaseModule {
6 | constructor(serverExperience, moduleName) {
7 |
8 | // local attributes
9 | this.e = serverExperience;
10 | this.moduleName = moduleName;
11 |
12 | // to be saved parameters to send to client when connects
13 | this.params = {};
14 |
15 | // binding
16 | this.enterPlayer = this.enterPlayer.bind(this);
17 | this.exitPlayer = this.exitPlayer.bind(this);
18 | this.paramCallback = this.paramCallback.bind(this);
19 |
20 | // setup osx msg receive callback: format msg and apply this.paramCallback
21 | this.e.osc.receive( '/' + this.moduleName, (msgRaw) => {
22 | // shape msg into array of arguments
23 | let msg = msgRaw.split(' ');
24 | msg.numberify();
25 | // pass msg to callback
26 | this.paramCallback( msg );
27 | });
28 |
29 | }
30 |
31 | /**
32 | * local equivalent of soundworks "enter" method
33 | * only applied for clients of type 'player'
34 | * init client's modules with all Nu parameters saved in server
35 | **/
36 | enterPlayer(client){
37 | // send to new client information regarding current groups parameters
38 | Object.keys(this.params).forEach( (key) => {
39 | this.e.send(client, this.moduleName, [key, this.params[key]] );
40 | });
41 | }
42 |
43 | /**
44 | * local equivalent of soundworks exit.
45 | * only applied for clients of type 'players'. No default behavior,
46 | * module-specific override if need be.
47 | **/
48 | exitPlayer(client){}
49 |
50 | /**
51 | * callback that handles Nu msg from OSC client. Supposes that every msg
52 | * received contains playerId, re-route to concerned player or to internal
53 | * params/function if playerId == -1 (all concerned)
54 | **/
55 | paramCallback(msg){
56 | // extract data
57 | let playerId = msg.shift(); // concerned player id
58 | let name = msg.shift(); // method / argument name
59 |
60 | // eventually convert remaining array to singleton
61 | let args = (msg.length == 1) ? msg[0] : msg;
62 |
63 | // call local dedicated method if defined
64 | if( this[name] !== undefined ){
65 | this[name]( [playerId].concat(args) );
66 | return;
67 | }
68 |
69 | // if player specific instruction, send to player
70 | if( playerId !== -1 ){
71 | let client = this.e.playerMap.get( playerId );
72 | if( client === undefined ){ return; }
73 | this.e.send( client, this.moduleName, [name].concat(args) );
74 | return
75 | }
76 |
77 | // all players are concerned: broadcast and save local
78 | // (to broadcast current config to newcomers)
79 | this.e.broadcast( 'player', null, this.moduleName, [name].concat(args) );
80 | // store value
81 | this.params[name] = args;
82 | }
83 |
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/node/bin/log:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var pkg = require('../package.json');
3 | var colors = require('colors');
4 | var path = require('path');
5 |
6 | 'use strict';
7 |
8 | var prefix = '[' + pkg.name + ']\t';
9 | var cwd = process.cwd();
10 |
11 | function toRel(target) {
12 | return path.relative(cwd, target);
13 | }
14 |
15 | function getDeltaTime(time) {
16 | return new Date().getTime() - time;
17 | }
18 |
19 | var log = {
20 | transpileSuccess: function(src, dest, startTime) {
21 | var time = getDeltaTime(startTime);
22 | var msg = prefix + '%s: successfully transpiled to "%s" (%sms)'.green;
23 | console.log(msg, toRel(src), toRel(dest), time);
24 | },
25 |
26 | transpileError: function(err) {
27 | var parts = err.message.split(':');
28 | var msg = prefix + '%s'.red;
29 |
30 | console.log(msg, toRel(err.message));
31 | console.log(err.codeFrame);
32 | },
33 |
34 | bundleStart: function(dest) {
35 | var msg = prefix + '%s: bundle start'.yellow;
36 | console.log(msg, toRel(dest));
37 | },
38 |
39 | bundleSuccess: function(dest, startTime) {
40 | var time = getDeltaTime(startTime);
41 | var msg = prefix + '%s: successfully bundled (%sms)'.green;
42 | console.log(msg, toRel(dest), time);
43 | },
44 |
45 | bundleError: function(dest, err) {
46 | var msg = prefix + '%s: bundle error'.red;
47 | console.log(msg, toRel(dest));
48 | console.log('> ' + err.message);
49 | },
50 |
51 | deleteFile: function(filename) {
52 | var msg = prefix + '%s: successfully removed'.yellow;
53 | console.log(msg, toRel(filename));
54 | },
55 |
56 | minifyStart: function(filename) {
57 | var msg = prefix + '%s: minify start'.yellow;
58 | console.log(msg, toRel(filename));
59 | },
60 |
61 | minifySuccess: function(filename, outFilename, startTime) {
62 | var msg = prefix + '%s: successfully minified (%sms)'.green;
63 | var time = getDeltaTime(startTime);
64 | console.log(msg, toRel(outFilename), time);
65 | },
66 |
67 | sassSuccess: function(dest) {
68 | var msg = prefix + '%s: successfully written'.green
69 | console.log(msg, toRel(dest));
70 | },
71 |
72 | sassError: function(err) {
73 | var msg = prefix + ('%s (%s:%s): sass error').red;
74 | console.log(msg, toRel(err.file), err.line, err.column);
75 | console.log('> ' + err.message);
76 | },
77 |
78 | serverStart: function() {
79 | console.log(prefix + 'server start'.cyan);
80 | },
81 |
82 | serverStop: function() {
83 | console.log(prefix + 'server stop'.cyan);
84 | },
85 |
86 | serverError: function(serverIndex) {
87 | var msg = prefix + '%s: not found, run `npm run transpile`'.red;
88 | console.log(msg, toRel(serverIndex));
89 | },
90 |
91 | watchWarning: function(directory) {
92 | var msg = prefix + '%s: is not a directory, restart the script to watch it'.yellow;
93 | console.log(msg, toRel(directory));
94 | }
95 | };
96 |
97 | module.exports = log;
98 |
--------------------------------------------------------------------------------
/node/sass/_03-services.scss:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------
2 | // VIEW COMPONENTS
3 | // ------------------------------------------------------------
4 |
5 | // @todo - change to id
6 | .space {
7 | svg {
8 | overflow: hidden;
9 | background-color: #242424;
10 | }
11 |
12 | .point {
13 | fill: $foregroundColor;
14 |
15 | &.selected {
16 | fill: darken($foregroundColor, 70%);
17 | }
18 | }
19 |
20 | .line {
21 | stroke: $foregroundColor;
22 | stroke-width: 1px;
23 |
24 | &.arrow {
25 | marker-mid: url(#marker-arrow);
26 | }
27 | }
28 |
29 | // .marker-circle {
30 | // stroke: none;
31 | // fill: $foregroundColor;
32 | // }
33 |
34 | .marker-arrow {
35 | fill: $foregroundColor;
36 | }
37 | }
38 |
39 | .select {
40 | color: $backgroundColor;
41 | background-color: $foregroundColor;
42 | font-size: 1.3rem;
43 | line-height: 3rem;
44 | height: 3rem;
45 | }
46 |
47 | // ------------------------------------------------------------
48 | // AUTH
49 | // ------------------------------------------------------------
50 |
51 | #service-auth {
52 | input[type=password], .btn {
53 | width: 100%;
54 | }
55 |
56 | input[type=password] {
57 | font-size: 1.4rem;
58 | height: 30px;
59 | margin-bottom: 12px;
60 | text-align: center;
61 | color: #343434;
62 | }
63 | }
64 |
65 | // ------------------------------------------------------------
66 | // CHECKIN
67 | // ------------------------------------------------------------
68 |
69 | #service-checkin {
70 | .checkin-label {
71 | margin: 0 auto;
72 | background-color: $belize-hole;
73 | border-radius: 50%;
74 | }
75 |
76 | $portraitBoxSize: 160px;
77 | $landscapeBoxSize: 120px;
78 |
79 | &.portrait .checkin-label {
80 | flex-basis: $portraitBoxSize;
81 | height: $portraitBoxSize;
82 |
83 | p {
84 | line-height: $portraitBoxSize;
85 | }
86 | }
87 |
88 | &.landscape .checkin-label {
89 | flex-basis: $landscapeBoxSize;
90 | height: $landscapeBoxSize;
91 |
92 | p {
93 | line-height: $landscapeBoxSize;
94 | }
95 | }
96 | }
97 |
98 | // ------------------------------------------------------------
99 | // AUDIO BUFFER MANAGER
100 | // ------------------------------------------------------------
101 |
102 | #service-audio-buffer-manager {
103 | .progress-wrap {
104 | margin: 12px auto 0;
105 | border: solid 1px white;
106 | border-radius: 5px;
107 | height: $loadingProgressBarHeight;
108 | overflow: hidden;
109 | flex-basis: 80%;
110 | }
111 |
112 | .progress-bar {
113 | background-color: white;
114 | height: $loadingProgressBarHeight;
115 | width: 0; // updated from javascript
116 | @include transition(width 150ms);
117 | }
118 | }
119 |
120 | // ------------------------------------------------------------
121 | // BASIC SHARED CONTROLLERS
122 | // ------------------------------------------------------------
123 |
124 | #basic-shared-controller {
125 | margin: 0 auto;
126 | max-width: 800px;
127 | padding: 10px;
128 | box-sizing: border-box;
129 |
130 | h1 {
131 | margin: 8px 0;
132 | }
133 | }
134 |
135 | // ------------------------------------------------------------
136 | // LANGUAGE
137 | // ------------------------------------------------------------
138 |
139 | #service-language {
140 | .section-center {
141 | padding: 10px;
142 | }
143 |
144 | .btn {
145 | width: 100%;
146 | margin-bottom: 20px;
147 | }
148 | }
149 |
150 |
--------------------------------------------------------------------------------
/node/sass/_02-commons.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------
2 | // RESET - GENERAL LAYOUT
3 | // -----------------------------------------------------------------
4 |
5 | /**
6 | * The following code is inspired by:
7 | * http://meyerweb.com/eric/tools/css/reset/
8 | * v2.0 | 20110126
9 | * License: none (public domain)
10 | */
11 | html, body, div, span, h1, h2, h3, h4, h5, h6, p, a, button, input, option, select {
12 | margin: 0;
13 | padding: 0;
14 | border: 0;
15 | font-size: 100%;
16 | font: inherit;
17 | vertical-align: baseline;
18 | }
19 |
20 | body {
21 | line-height: 1;
22 | }
23 |
24 | ol, ul {
25 | list-style: none;
26 | }
27 |
28 | html, body {
29 | width: 100%;
30 | height: 100%;
31 | background-color: $backgroundColor;
32 | }
33 |
34 | body {
35 | font-family: $mainFont;
36 | font-size: 62.5%;
37 | line-height: normal;
38 | color: $textColor;
39 | }
40 |
41 | h1, h2, h3, h4, h5, h6 {
42 | font-weight: bold;
43 | }
44 |
45 | p {
46 | margin: 0;
47 | padding: 0;
48 | }
49 |
50 | // -----------------------------------------------------------------
51 | // LAYOUT HELPERS
52 | // -----------------------------------------------------------------
53 |
54 | // default container of the application
55 | #container {
56 | position: relative;
57 | }
58 |
59 | .foreground {
60 | position: relative;
61 | z-index: 1;
62 | }
63 |
64 | .background {
65 | position: absolute;
66 | top: 0;
67 | left: 0;
68 | }
69 |
70 | .fit-container {
71 | display: block;
72 | width: 100%;
73 | height: 100%;
74 | }
75 |
76 | // center horizontally
77 | .flex-center {
78 | box-sizing: border-box;
79 | padding: 10px;
80 | display: flex;
81 | display: -webkit-flex; /* Safari */
82 |
83 | & > * {
84 | text-align: center;
85 | flex-basis: 100%;
86 | -webkit-flex-basis: 100%;
87 | width: 100%;
88 | }
89 | }
90 |
91 | // center horizontally and vertically
92 | .flex-middle {
93 | @extend .flex-center;
94 | align-items: center;
95 | -webkit-align-items: center;
96 | }
97 |
98 | // create the soft blink animation from keyframe mixin
99 | @include keyframes(soft-blink) {
100 | 0% { opacity: 1.0; }
101 | 50% { opacity: 0.3; }
102 | 100% { opacity: 1.0; }
103 | }
104 |
105 | // -----------------------------------------------------------------
106 | // TAG HELPERS
107 | // -----------------------------------------------------------------
108 |
109 | .huge {
110 | font-size: 3rem;
111 | }
112 |
113 | .big {
114 | font-size: 2rem;
115 | }
116 |
117 | p, .normal {
118 | font-size: 1.5rem;
119 | }
120 |
121 | .small {
122 | font-size: 1rem;
123 | }
124 |
125 | .bold {
126 | font-weight: bold;
127 | }
128 |
129 | .italic {
130 | font-style: italic;
131 | }
132 |
133 | .btn {
134 | background-color: $backgroundColor;
135 | border: $borderSize solid $textColor;
136 | border-radius: $borderRadius;
137 | color: $textColor;
138 | padding: 6px 0;
139 | font-size: 1rem;
140 | display: block;
141 | letter-spacing: 0.05em;
142 |
143 | &:active, &.active {
144 | background-color: $textColor;
145 | color: $backgroundColor;
146 | }
147 |
148 | &:focus {
149 | outline: none;
150 | }
151 |
152 | &.disabled, &[disabled] {
153 | opacity: 0.5;
154 | }
155 |
156 | &.pushed {
157 | background-color: $textColor;
158 | color: $backgroundColor;
159 | }
160 |
161 | &.released {
162 | background-color: $backgroundColor;
163 | color: $textColor;
164 | }
165 | }
166 |
167 | .landscape .portrait-only {
168 | display: none;
169 | }
170 |
171 | // transform a
in ' ' in landscape mode
172 | .landscape br.portrait-only {
173 | content: ' ';
174 | display: inline;
175 |
176 | &:after {
177 | content: ' ';
178 | }
179 | }
180 |
181 | .soft-blink {
182 | @include animation('soft-blink 3.6s ease-in-out infinite');
183 | }
184 |
--------------------------------------------------------------------------------
/maxmsp/extras/nu.error.maxpat:
--------------------------------------------------------------------------------
1 | {
2 | "patcher" : {
3 | "fileversion" : 1,
4 | "appversion" : {
5 | "major" : 7,
6 | "minor" : 3,
7 | "revision" : 3,
8 | "architecture" : "x86",
9 | "modernui" : 1
10 | }
11 | ,
12 | "rect" : [ 1365.0, 384.0, 275.0, 221.0 ],
13 | "bglocked" : 0,
14 | "openinpresentation" : 0,
15 | "default_fontsize" : 12.0,
16 | "default_fontface" : 0,
17 | "default_fontname" : "Arial",
18 | "gridonopen" : 1,
19 | "gridsize" : [ 15.0, 15.0 ],
20 | "gridsnaponopen" : 1,
21 | "objectsnaponopen" : 1,
22 | "statusbarvisible" : 2,
23 | "toolbarvisible" : 1,
24 | "lefttoolbarpinned" : 0,
25 | "toptoolbarpinned" : 0,
26 | "righttoolbarpinned" : 0,
27 | "bottomtoolbarpinned" : 0,
28 | "toolbars_unpinned_last_save" : 0,
29 | "tallnewobj" : 0,
30 | "boxanimatetime" : 200,
31 | "enablehscroll" : 1,
32 | "enablevscroll" : 1,
33 | "devicewidth" : 0.0,
34 | "description" : "",
35 | "digest" : "",
36 | "tags" : "",
37 | "style" : "",
38 | "subpatcher_template" : "",
39 | "boxes" : [ {
40 | "box" : {
41 | "id" : "obj-7",
42 | "linecount" : 2,
43 | "maxclass" : "comment",
44 | "numinlets" : 1,
45 | "numoutlets" : 0,
46 | "patching_rect" : [ 70.0, 26.0, 150.0, 33.0 ],
47 | "style" : "",
48 | "text" : "popup window to warn of an undefined argument"
49 | }
50 |
51 | }
52 | , {
53 | "box" : {
54 | "id" : "obj-6",
55 | "maxclass" : "newobj",
56 | "numinlets" : 1,
57 | "numoutlets" : 1,
58 | "outlettype" : [ "" ],
59 | "patching_rect" : [ 27.0, 141.0, 184.0, 22.0 ],
60 | "style" : "",
61 | "text" : "prepend (argument not defined):"
62 | }
63 |
64 | }
65 | , {
66 | "box" : {
67 | "id" : "obj-2",
68 | "maxclass" : "newobj",
69 | "numinlets" : 1,
70 | "numoutlets" : 2,
71 | "outlettype" : [ "", "bang" ],
72 | "patching_rect" : [ 27.0, 64.0, 30.0, 22.0 ],
73 | "style" : "",
74 | "text" : "t l b"
75 | }
76 |
77 | }
78 | , {
79 | "box" : {
80 | "id" : "obj-24",
81 | "maxclass" : "newobj",
82 | "numinlets" : 1,
83 | "numoutlets" : 0,
84 | "patching_rect" : [ 27.0, 171.0, 99.0, 22.0 ],
85 | "style" : "",
86 | "text" : "print Nü.Error"
87 | }
88 |
89 | }
90 | , {
91 | "box" : {
92 | "comment" : "",
93 | "id" : "obj-1",
94 | "index" : 1,
95 | "maxclass" : "inlet",
96 | "numinlets" : 0,
97 | "numoutlets" : 1,
98 | "outlettype" : [ "" ],
99 | "patching_rect" : [ 27.0, 26.0, 30.0, 30.0 ],
100 | "style" : ""
101 | }
102 |
103 | }
104 | , {
105 | "box" : {
106 | "fontname" : "Arial",
107 | "fontsize" : 13.0,
108 | "id" : "obj-5",
109 | "linecount" : 2,
110 | "maxclass" : "message",
111 | "numinlets" : 2,
112 | "numoutlets" : 1,
113 | "outlettype" : [ "" ],
114 | "patching_rect" : [ 133.0, 78.0, 109.0, 38.0 ],
115 | "style" : "",
116 | "text" : ";\rmax maxwindow"
117 | }
118 |
119 | }
120 | ],
121 | "lines" : [ {
122 | "patchline" : {
123 | "destination" : [ "obj-2", 0 ],
124 | "disabled" : 0,
125 | "hidden" : 0,
126 | "source" : [ "obj-1", 0 ]
127 | }
128 |
129 | }
130 | , {
131 | "patchline" : {
132 | "destination" : [ "obj-5", 0 ],
133 | "disabled" : 0,
134 | "hidden" : 0,
135 | "source" : [ "obj-2", 1 ]
136 | }
137 |
138 | }
139 | , {
140 | "patchline" : {
141 | "destination" : [ "obj-6", 0 ],
142 | "disabled" : 0,
143 | "hidden" : 0,
144 | "source" : [ "obj-2", 0 ]
145 | }
146 |
147 | }
148 | , {
149 | "patchline" : {
150 | "destination" : [ "obj-24", 0 ],
151 | "disabled" : 0,
152 | "hidden" : 0,
153 | "source" : [ "obj-6", 0 ]
154 | }
155 |
156 | }
157 | ]
158 | }
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/node/src/client/player/PlayerExperience.js:
--------------------------------------------------------------------------------
1 | import * as soundworks from 'soundworks/client';
2 |
3 | import NuDisplay from './NuDisplay';
4 | import * as Nu from './Nu'
5 | import audioFiles from '../shared/audioFiles';
6 |
7 | const audioContext = soundworks.audioContext;
8 | const client = soundworks.client;
9 |
10 | const viewTemplate = `
11 |
12 |
13 |
14 |
17 |
18 |
21 |
22 |
25 |
26 |
27 | `;
28 |
29 | /*
30 | * The PlayerExperience script defines the behavior of default clients (of type 'player').
31 | * Here it simply imports and instantiates all Nu modules.
32 | */
33 |
34 | export default class PlayerExperience extends soundworks.Experience {
35 | constructor(assetsDomain) {
36 | super();
37 |
38 | // require soundworks services
39 | this.platform = this.require('platform', { features: ['web-audio'] });
40 | this.params = this.require('shared-params');
41 | this.sharedConfig = this.require('shared-config');
42 | this.sync = this.require('sync');
43 | this.checkin = this.require('checkin', { showDialog: false });
44 | this.scheduler = this.require('sync-scheduler', { lookahead: 0.050 });
45 | this.rawSocket = this.require('raw-socket');
46 | this.loader = this.require('audio-buffer-manager', {
47 | assetsDomain: assetsDomain,
48 | files: audioFiles,
49 | });
50 | this.motionInput = this.require('motion-input', {
51 | descriptors: ['accelerationIncludingGravity', 'deviceorientation', 'energy']
52 | });
53 |
54 | // auto-click if #emulate found in url (for debug / laptop-based sessions)
55 | if( window.location.hash === "#emulate" ) { this.emulateClick(); }
56 | }
57 |
58 | start() {
59 | super.start();
60 |
61 | // initialize the view
62 | this.view = new soundworks.CanvasView(viewTemplate, {}, {}, {
63 | id: this.id,
64 | preservePixelRatio: true,
65 | });
66 |
67 | // as show can be async, we make sure that the view is actually rendered
68 | this.show().then(() => {
69 |
70 | // initialize renderer
71 | this.renderer = new NuDisplay(this);
72 | this.view.addRenderer(this.renderer);
73 |
74 | // init client position in room (AWAITING REAL LOC. SYSTEM)
75 | let coordinates = this.sharedConfig.get('setup.coordinates');
76 | this.coordinates = coordinates[client.index];
77 | // random coordinates if reached end of predefined coords
78 | if( this.coordinates === undefined){ this.coordinates = [ Math.random(), Math.random() ]; }
79 | this.send('coordinates', this.coordinates);
80 |
81 | // init Nu modules
82 | Object.keys(Nu).forEach( (nuClass) => {
83 | this['nu' + nuClass] = new Nu[nuClass](this);
84 | });
85 |
86 | // disable text selection, magnifier, and screen move on swipe on ios
87 | document.getElementsByTagName("body")[0].addEventListener("touchstart",
88 | function(e) { e.returnValue = false });
89 | });
90 |
91 | }
92 |
93 | /**
94 | * simulate user click to skip welcome screen (used e.g. for prototyping sessions on laptop)
95 | * won't work on mobile (need a REAL user input to start audio)
96 | **/
97 | emulateClick() {
98 | // prepare click and gui elmt on which to click
99 | const $el = document.querySelector('#service-platform');
100 | const event = new MouseEvent('mousedown', { 'view': window, 'bubbles': true, 'cancelable': true });
101 | // click if we've got gui elmt
102 | if( $el !== null ){ $el.dispatchEvent(event); }
103 | // re-iterate while not started (sometimes, even a click on gui will not start...)
104 | if( this.coordinates === undefined ){
105 | setTimeout(() => { this.emulateClick(); console.log('click delayed'); }, 300)
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/node/src/server/NuPath.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuPath: Nu module to move a sound through players topology, based on
3 | * emission points. A path is composed of emission points coupled with
4 | * emission time. Each point is used as a source image to produce a tap
5 | * in player's IR.
6 | **/
7 |
8 | import NuBaseModule from './NuBaseModule'
9 |
10 | export default class NuPath extends NuBaseModule {
11 | constructor(serverExperience) {
12 | super(serverExperience, 'nuPath');
13 |
14 | // to be saved params to send to client when connects:
15 | this.params = { masterGain: 1.0,
16 | propagationSpeed: 100.0,
17 | propagationGain: 0.9,
18 | propagationRxMinGain: 0.01,
19 | audioFileId: "snap",
20 | perc: 1,
21 | loop: true,
22 | accSlope: 0,
23 | timeBound: 0 };
24 |
25 | // this variable is intern to server, no need to broadcast it to clients upon connection
26 | this._rdvDelay = 2.0;
27 |
28 | // binding
29 | this.setPath = this.setPath.bind(this);
30 | this.startPath = this.startPath.bind(this);
31 | this.rdvDelay = this.rdvDelay.bind(this);
32 | }
33 |
34 | setPath(args){
35 | args.shift(); // playerId, not used, here to keep uniform the module impl.
36 | // extract from arguments
37 | let pathId = args.shift();
38 |
39 | // shape args from [x0 y0 t0 ... xN yN tN] to [ [t0, [x0, y0]], [tN, [xN, yN]] ]
40 | let pathArray = [];
41 | for( let i = 0; i < args.length; i+=3){
42 | let time = args[i];
43 | let pos = [ args[i+1], args[i+2] ];
44 | pathArray.push( [time, pos] );
45 | }
46 |
47 | // avoid zero propagation speed
48 | let propagationSpeed = this.params.propagationSpeed;
49 | if( Math.abs(propagationSpeed) < 0.1 ) propagationSpeed = 0.1;
50 |
51 | // create IR for each player
52 | let dist, time, gain, timeMin = 0;
53 | let irsArray = [];
54 | this.e.coordinatesMap.forEach(( clientPos, clientId) => {
55 |
56 | // init
57 | irsArray[clientId] = [];
58 | gain = 1.0;
59 |
60 | // loop over path
61 | pathArray.forEach(( item, index ) => {
62 | let pathTime = item[0];
63 | let pathPos = item[1];
64 | // compute IR taps
65 | dist = Math.sqrt(Math.pow(clientPos[0] - pathPos[0], 2) + Math.pow(clientPos[1] - pathPos[1], 2));
66 | time = pathTime + ( dist / propagationSpeed );
67 | // gain *= Math.pow( this.params.propagationGain, dist );
68 | // the gain doesn't decrease along the path, rather it decreases as the player
69 | // gets further away from current path point:
70 | gain = Math.pow( this.params.propagationGain, dist );
71 | // save tap if valid
72 | if (gain >= this.params.propagationRxMinGain) {
73 | // push IR in array
74 | irsArray[clientId].push(time, gain);
75 | // prepare handle neg speed
76 | if (time < timeMin) timeMin = time;
77 | }
78 | });
79 | });
80 |
81 | // send IRs (had to split in two (see above) because of timeMin)
82 | this.e.coordinatesMap.forEach((clientPos, clientId) => {
83 | // get IR
84 | let ir = irsArray[ clientId ];
85 | // add init time offset (useful for negative speed)
86 | ir.unshift( timeMin ); // add time min
87 | ir.unshift( pathId ); // add path id
88 | // shape for sending
89 | let msgArray = new Float32Array( ir );
90 | // send
91 | let client = this.e.playerMap.get( clientId );
92 | this.e.rawSocket.send( client, this.moduleName, msgArray );
93 | });
94 | }
95 |
96 | // trigger path rendering in clients
97 | startPath(args){
98 | args.shift(); // playerId, not used, here to keep uniform the module impl.
99 | let pathId = args.shift();
100 | // set rendez-vous time in 2 seconds from now.
101 | let rdvTime = this.e.sync.getSyncTime() + this._rdvDelay;
102 | this.e.broadcast('player', null, this.moduleName, ['startPath', pathId, rdvTime] );
103 | }
104 |
105 | // define delay before rdv time, in sec, from moment when play path msg is received
106 | // (for syync. play)
107 | rdvDelay(args){
108 | args.shift(); // playerId, not used, here to keep uniform the module impl.
109 | this._rdvDelay = args.shift();
110 | }
111 |
112 | // reset clients (stop all sounds)
113 | reset(){
114 | // re-route to clients
115 | this.e.broadcast( 'player', null, this.moduleName, ['reset'] );
116 | }
117 |
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/node/src/server/config/default.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | const cwd = process.cwd();
3 |
4 | // build coordinates grid
5 | const W = 4;
6 | const H = 4;
7 | const coordinates = [];
8 | for(let j = 1; j <= H; j ++ ){
9 | for(let i = 1; i <= W; i ++ ){
10 | coordinates.push([i,j]);
11 | }
12 | }
13 |
14 | // Configuration of the application.
15 | // Other entries can be added (as long as their name doesn't conflict with
16 | // existing ones) to define global parameters of the application (e.g. BPM,
17 | // synth parameters) that can then be shared easily among all clients using
18 | // the `shared-config` service.
19 | export default {
20 | // name of the application, used in the `.ejs` template and by default in
21 | // the `platform` service to populate its view
22 | appName: 'Nü',
23 |
24 | // name of the environnement ('production' enable cache in express application)
25 | env: 'development',
26 |
27 | // version of application, can be used to force reload css and js files
28 | // from server (cf. `html/default.ejs`)
29 | version: '0.0.1',
30 |
31 | // name of the default client type, i.e. the client that can access the
32 | // application at its root URL
33 | defaultClient: 'player',
34 |
35 | // define from where the assets (static files) should be loaded, these value
36 | // could also refer to a separate server for scalability reasons. This value
37 | // should also be used client-side to configure the `loader` service.
38 | assetsDomain: '/',
39 |
40 | // port used to open the http server, in production this value is typically 80
41 | port: 8000,
42 |
43 | // describe the location where the experience takes places, theses values are
44 | // used by the `placer`, `checkin` and `locator` services.
45 | // if one of these service is required, this entry shouldn't be removed.
46 | setup: {
47 | area: {
48 | width: W+1,
49 | height: H+1,
50 | // path to an image to be used in the area representation
51 | background: null,
52 | },
53 | // list of predefined labels
54 | labels: null,
55 | // list of predefined coordinates given as an array of `[x:Number, y:Number]`
56 | coordinates: coordinates,
57 | // maximum number of clients allowed in a position
58 | maxClientsPerPosition: 1,
59 | // maximum number of positions (may limit or be limited by the number of
60 | // labels and/or coordinates)
61 | capacity: Infinity,
62 | },
63 |
64 | // socket.io configuration
65 | websockets: {
66 | url: '',
67 | transports: ['websocket'],
68 | // @note: EngineIO defaults
69 | // pingTimeout: 3000,
70 | // pingInterval: 1000,
71 | // upgradeTimeout: 10000,
72 | // maxHttpBufferSize: 10E7,
73 | },
74 |
75 | // define if the HTTP server should be launched using secure connections.
76 | // For development purposes when set to `true` and no certificates are given
77 | // (cf. `httpsInfos`), a self-signed certificate is created.
78 | useHttps: false,
79 |
80 | // paths to the key and certificate to be used in order to launch the https
81 | // server. Both entries are required otherwise a self-signed certificate
82 | // is generated.
83 | httpsInfos: {
84 | key: null,
85 | cert: null,
86 | },
87 |
88 | // password to be used by the `auth` service
89 | password: '',
90 |
91 | // configuration of the `osc` service
92 | osc: {
93 | // IP of the currently running node server
94 | receiveAddress: '127.0.0.1',
95 | // port listening for incomming messages
96 | receivePort: 57121,
97 | // IP of the remote application
98 | sendAddress: '127.0.0.1',
99 | // port where the remote application is listening for messages
100 | sendPort: 57120,
101 | },
102 |
103 | // configuration of the `raw-socket` service
104 | rawSocket: {
105 | // port used for socket connection
106 | port: 8085
107 | },
108 |
109 | // define if the server should use gzip compression for static files
110 | enableGZipCompression: true,
111 |
112 | // location of the public directory (accessible through http(s) requests)
113 | publicDirectory: path.join(cwd, 'public'),
114 |
115 | // directory where the server templating system looks for the `ejs` templates
116 | templateDirectory: path.join(cwd, 'html'),
117 |
118 | // bunyan configuration
119 | logger: {
120 | name: 'soundworks',
121 | level: 'info',
122 | streams: [{
123 | level: 'info',
124 | stream: process.stdout,
125 | }, /* {
126 | level: 'info',
127 | path: path.join(process.cwd(), 'logs', 'soundworks.log'),
128 | } */]
129 | },
130 |
131 | // directory where error reported from the clients are written
132 | errorReporterDirectory: path.join(cwd, 'logs', 'clients'),
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/node/src/server/PlayerExperience.js:
--------------------------------------------------------------------------------
1 | import * as soundworks from 'soundworks/server';
2 | import * as Nu from './Nu';
3 |
4 | // server-side experience.
5 | export default class PlayerExperience extends soundworks.Experience {
6 | constructor(clientType) {
7 | super(clientType);
8 |
9 | // require services
10 | this.checkin = this.require('checkin');
11 | this.sharedConfig = this.require('shared-config');
12 | this.sharedConfig.share('setup', 'player'); // share `setup` entry to players
13 | this.sharedConfig.share('socketIO', 'player'); // share `socketIO` entry to players
14 | this.params = this.require('shared-params');
15 | this.audioBufferManager = this.require('audio-buffer-manager');
16 | this.syncScheduler = this.require('sync-scheduler');
17 | this.sync = this.require('sync');
18 | this.osc = this.require('osc');
19 | var protocol = [
20 | { channel: 'nuStream', type: 'Float32' },
21 | { channel: 'nuRoomReverb', type: 'Float32' },
22 | { channel: 'nuPath', type: 'Float32' },
23 | { channel: 'nuOutput', type: 'Float32' },
24 | ];
25 | this.rawSocket = this.require('raw-socket', { protocol: protocol });
26 |
27 | // bind methods
28 | this.checkinController = this.checkinController.bind(this);
29 |
30 | // local attributes
31 | this.playerMap = new Map();
32 | this.coordinatesMap = new Map();
33 | this.controllerMap = new Map();
34 | }
35 |
36 | start() {
37 | // init Nu modules
38 | Object.keys(Nu).forEach( (nuClass) => {
39 | this['nu' + nuClass] = new Nu[nuClass](this);
40 | });
41 | }
42 |
43 | enter(client) {
44 | super.enter(client);
45 |
46 | switch (client.type) {
47 | case 'player':
48 |
49 | // msg callback: receive client coordinates
50 | // (could use local service, this way lets open for pos estimation in client in the future)
51 | this.receive(client, 'coordinates', (xy) => {
52 | this.coordinatesMap.set( client.index, xy );
53 | // update client pos in osc client
54 | this.osc.send('/nuMain/playerPos', [client.index, xy[0], xy[1]] );
55 | });
56 |
57 | // update nu modules (when they're ready to receive)
58 | this.receive(client, 'moduleReady', (nuClassPrefixed) => {
59 | this[nuClassPrefixed].enterPlayer(client);
60 | });
61 |
62 | // update local attributes
63 | this.playerMap.set( client.index, client );
64 |
65 | break;
66 |
67 | case 'controller':
68 |
69 | // add controller to local map (not using checkin for controllers)
70 | let clientId = this.checkinController(client);
71 | // indicate to OSC that controller 'client.index' is present
72 | this.osc.send('/nuController', [clientId, 'enterExit', 1]);
73 | // direct forward to OSC
74 | this.receive(client, 'osc', (header, args) => {
75 | // append controller index to msg
76 | let clientId = this.controllerMap.get(client);
77 | args.unshift(clientId);
78 | // forward to OSC
79 | this.osc.send(header, args);
80 | });
81 |
82 | break;
83 | }
84 | }
85 |
86 | exit(client) {
87 | super.exit(client);
88 |
89 | switch (client.type) {
90 | case 'player':
91 |
92 | // update local attributes
93 | this.playerMap.delete( client.index );
94 |
95 | // update Nu modules
96 | Object.keys(Nu).forEach( (nuClass) => {
97 | this['nu' + nuClass].exitPlayer(client);
98 | });
99 |
100 | break;
101 |
102 | case 'controller':
103 |
104 | // update osc
105 | let clientId = this.controllerMap.get(client);
106 | this.osc.send('/nuController', [clientId, 'enterExit', 0]);
107 | // update local attributes
108 | this.controllerMap.delete(client);
109 |
110 | break;
111 | }
112 | }
113 |
114 | // equivalent of the checkin service (to avoid using checkin on controllers and screwing players numbering)
115 | checkinController(client){
116 | let clientId = this.controllerMap.get(client);
117 | // if already defined, simply return clientId
118 | if( clientId !== undefined ){ return clientId; }
119 | // get occupied IDs
120 | let indexArray = Array.from( this.controllerMap, x => x[1] );
121 | clientId = -1; let testId = 0;
122 | while( clientId == -1 ){
123 | if( indexArray.indexOf(testId) == -1 )
124 | clientId = testId;
125 | testId += 1;
126 | }
127 | // store new client index
128 | this.controllerMap.set(client, clientId);
129 | // send client index to client
130 | this.send(client, 'checkinId', clientId);
131 | // return Id
132 | return clientId
133 | }
134 |
135 | }
--------------------------------------------------------------------------------
/node/src/server/NuStream.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuStream: live audio stream from OSC client to players
3 | *
4 | * "streaming" from OSC client to soundworks server is based on disk writing / reading.
5 | * streaming from server to players is based on the rawsocket soundworks service
6 | **/
7 |
8 | import NuBaseModule from './NuBaseModule'
9 |
10 | // req audio read depts
11 | const fs = require('fs');
12 | var AudioContext = require('web-audio-api').AudioContext
13 | const audioContext = new AudioContext;
14 | const assetsPath = __dirname + '/../../public/stream/';
15 |
16 | export default class NuStream extends NuBaseModule {
17 | constructor(serverExperience) {
18 | super(serverExperience, 'nuStream');
19 |
20 | // local attributes
21 | this.pointerToStreamInterval = undefined;
22 | this.packetId = 0;
23 |
24 | // to be saved params to send to client when connects:
25 | this.params = {
26 | gain: 1.0, // audio gain
27 | delayTime: 4.0, // time between buffer rec / buffer played on client (for sync playback)
28 | packetTime: 2.0, // duration of a stream packet, in sec
29 | startTime: 0.0, // system time at which streaming stated (order given from OSC received)
30 | };
31 |
32 | // binding
33 | this.streamCallback = this.streamCallback.bind(this);
34 |
35 | // clean stream directory (delete old 'stream' files)
36 | this.cleanStreamDir();
37 | }
38 |
39 | // clean all files containing "stream" in assetsPath
40 | cleanStreamDir(){
41 | fs.readdir(assetsPath, (err, files) => {
42 | if( err ){ console.log('at ' + __filename + ':'); throw err; }
43 | for( let file of files ){
44 | if( file.search('stream') < 0 ){ continue; }
45 | fs.unlinkSync(assetsPath + file);
46 | }
47 | });
48 | }
49 |
50 | // enable / disable streaming module
51 | onOff(args){
52 | args.shift(); // playerId, not used, here to keep uniform the module impl.
53 | let value = args.shift();
54 | if( value ){
55 | // remove old files
56 | this.cleanStreamDir();
57 | // reset packet id
58 | this.packetId = 0;
59 | // broadcast start time for reference
60 | this.params.startTime = this.e.sync.getSyncTime();
61 | this.e.broadcast('player', null, 'nuStream', ['startTime', this.params.startTime] );
62 | // start streaming callback
63 | this.pointerToStreamInterval = setInterval( () => {
64 | this.streamCallback();
65 | }, 1000);
66 | }
67 |
68 | // remove streaming callback
69 | else{ clearInterval( this.pointerToStreamInterval ); }
70 |
71 | // notify clients of on/off status
72 | this.e.broadcast( 'player', null, this.moduleName, ['onOff', value] );
73 | }
74 |
75 | /**
76 | * callback in charge of reading audio file from disk,
77 | * broadcasting their audio content to players, and deleting
78 | * said audio files once used
79 | **/
80 | streamCallback(){
81 |
82 | // read files names from disk
83 | fs.readdir(assetsPath, (err, files) => {
84 |
85 | // search for a valid 'stream' file (should be only one there)
86 | var fileName = undefined;
87 | for( let file of files ){
88 | if( file.search('stream') >= 0 ){
89 | fileName = file;
90 | break;
91 | }
92 | }
93 |
94 | // discard if nothing found
95 | if( fileName === undefined ){ return; }
96 |
97 | // read file otherwise
98 | fs.readFile( assetsPath + fileName, (err, buf) => {
99 | if (err) { throw err; }
100 |
101 | // decode file to audiobuffer
102 | audioContext.decodeAudioData(buf, (audioBuffer) => {
103 | // debug
104 | console.log('\nread file:', fileName,
105 | 'nCh', audioBuffer.numberOfChannels,
106 | 'sampl.', audioBuffer.sampleRate + 'Hz',
107 | 'dur.', audioBuffer.length / audioBuffer.sampleRate + 's');
108 |
109 | // get timestamp from file name
110 | // let timeStamp = Number( fileName.slice(fileName.search('_') + 1, fileName.search('.wav') ) );
111 |
112 | // add packet id to array (need to create new one since Float32Array is fixed size)
113 | let audioArray = audioBuffer.getChannelData(0);
114 | var dataArray = new Float32Array( audioArray.length + 1 );
115 | dataArray[0] = this.packetId;
116 | dataArray.set(audioArray, 1);
117 |
118 | // send data to every clients
119 | this.e.clients.forEach( (client) => {
120 | if( client.type === 'player' ){
121 | this.e.rawSocket.send( client, 'nuStream', dataArray );
122 | }
123 | });
124 |
125 | // delete file
126 | fs.unlinkSync(assetsPath + fileName);
127 | console.log('deleting file', fileName);
128 |
129 | // incr. packet Id
130 | this.packetId += 1;
131 | },
132 | (err) => { throw err; }); });
133 |
134 | });
135 |
136 | }
137 |
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/node/sass/_04-survey.scss:
--------------------------------------------------------------------------------
1 |
2 | // ------------------------------------------------------------
3 | // SURVEY
4 | // ------------------------------------------------------------
5 |
6 | #survey {
7 | .section-center {
8 | padding: 10px;
9 | box-sizing: border-box;
10 | }
11 |
12 | .question {
13 | width: 100%;
14 | height: 100%;
15 | }
16 |
17 | // radio, checkbox
18 | .label {
19 | text-align: left;
20 | padding-bottom: 6px;
21 | font-style: italic;
22 | font-size: 1.1rem;
23 | }
24 |
25 | .answer {
26 | font-size: 1rem;
27 | position: relative;
28 | padding: 8px 0 6px 0;
29 | margin-bottom: 1px;
30 | border-radius: $borderRadius;
31 | @include transition(background-color 0.2s);
32 |
33 | &.radio, &.checkbox {
34 | padding-left: 50px;
35 | }
36 |
37 | &.selected {
38 | background-color: rgba(255, 255, 255, 0.2);
39 | }
40 |
41 | &.radio:before {
42 | content: '';
43 | display: block;
44 | position: absolute;
45 | left: 10px;
46 | width: 16px;
47 | height: 16px;
48 | background-color: transparent;
49 | border-radius: 50%;
50 | border: 1px solid white;
51 | @include transition(background-color 0.2s);
52 | }
53 |
54 | &.radio.selected:before {
55 | background-color: white;
56 | }
57 |
58 | &.checkbox:before {
59 | content: '';
60 | display: block;
61 | position: absolute;
62 | left: 10px;
63 | width: 16px;
64 | height: 16px;
65 | background-color: transparent;
66 | border-radius: $borderRadius;
67 | border: 1px solid white;
68 | @include transition(background-color 0.2s);
69 | }
70 |
71 | &.checkbox.selected:before {
72 | background-color: white;
73 | }
74 | }
75 |
76 | // SLIDER
77 | // reset slider styles
78 | input[type=range] {
79 | -webkit-appearance: none; /* Hides the slider so that custom slider can be made */
80 | width: 100%; /* Specific width is required for Firefox. */
81 | background-color: transparent;
82 | }
83 |
84 | input[type=range]::-webkit-slider-thumb {
85 | -webkit-appearance: none;
86 | }
87 |
88 | input[type=range]:focus {
89 | outline: none; /* Removes the blue border. You should probably do some kind of focus styling for accessibility reasons though. */
90 | }
91 |
92 | input[type=range]::-ms-track {
93 | width: 100%;
94 | cursor: pointer;
95 | background: transparent; /* Hides the slider so custom styles can be added */
96 | border-color: transparent;
97 | color: transparent;
98 | }
99 |
100 | // ---------------------------
101 | // handler
102 | // ---------------------------
103 |
104 | /* Special styling for WebKit/Blink */
105 | input[type=range]::-webkit-slider-thumb {
106 | -webkit-appearance: none;
107 | // border: 1px solid #000000;
108 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
109 | height: 40px;
110 | width: 40px;
111 | border-radius: 50%;
112 | background: #ffffff;
113 | cursor: pointer;
114 | margin-top: -17px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */
115 | // box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; /* Add cool effects to your sliders! */
116 | }
117 |
118 | /* All the same stuff for Firefox */
119 | input[type=range]::-moz-range-thumb {
120 | // border: 1px solid #000000;
121 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
122 | height: 40px;
123 | width: 40px;
124 | border-radius: 50%;
125 | background: #ffffff;
126 | cursor: pointer;
127 | }
128 |
129 | // ---------------------------
130 | // track
131 | // ---------------------------
132 | input[type=range]::-webkit-slider-runnable-track {
133 | height: 8px;
134 | cursor: pointer;
135 | background: rgba(255, 255, 255, 0.7);
136 | border-radius: 3px;
137 | border: 0.2px solid #010101;
138 | }
139 |
140 | input[type=range]:focus::-webkit-slider-runnable-track {
141 | background: rgba(255, 255, 255, 0.9);
142 | }
143 |
144 | input[type=range]::-moz-range-track {
145 | height: 8px;
146 | cursor: pointer;
147 | background: rgba(255, 255, 255, 0.7);
148 | border-radius: 3px;
149 | border: 0.2px solid #010101;
150 | }
151 |
152 | // slider
153 | input[type=range].slider {
154 | width: 100%;
155 | margin: 20px 0;
156 | float: left;
157 | }
158 |
159 | .textarea {
160 | color: $backgroundColor;
161 | padding: 10px;
162 | box-sizing: border-box;
163 | }
164 |
165 | .feedback {
166 | font-size: 2rem;
167 | display: block;
168 | text-align: center;
169 | }
170 |
171 | .counter {
172 | font-size: 2rem;
173 | font-style: italic;
174 | position: absolute;
175 | top: 20px;
176 | right: 20px;
177 | }
178 |
179 | .thanks {
180 | font-size: 2rem;
181 | }
182 |
183 | @media (min-width: 10px) and (orientation: portrait) {
184 | & {
185 | position: relative;
186 | }
187 |
188 | .counter {
189 | position: relative;
190 | top: 0;
191 | left: 0;
192 | right: 0;
193 | padding: 20px;
194 | text-align: right;
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/node/bin/javascripts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | var babel = require('babel-core');
3 | var browserify = require('browserify');
4 | var fse = require('fs-extra');
5 | var log = require('./log');
6 | var path = require('path');
7 | var watchify = require('watchify');
8 |
9 | 'use strict';
10 |
11 | /**
12 | * Create a transpiler object binded to a `srcDirectory` and a `distDirectory`
13 | */
14 | function getTranspiler(srcDirectory, distDirectory, isAllowed, babelOptions, browserifyOptions) {
15 |
16 | /**
17 | * Add watchify to the browserify options. client only.
18 | */
19 | if (browserifyOptions !== undefined) {
20 | Object.assign(browserifyOptions, {
21 | cache: {}, // required for watchify
22 | packageCache: {}, // required for watchify
23 | });
24 |
25 | if (browserifyOptions.plugin)
26 | browserifyOptions.plugin.push(watchify);
27 | else
28 | browserifyOptions.plugin = [watchify];
29 | }
30 |
31 | /**
32 | * Returns the name of the target transpiled file
33 | */
34 | function getTarget(filename) {
35 | var relFilename = path.relative(srcDirectory, filename);
36 | var outFilename = path.join(distDirectory, relFilename);
37 | return outFilename;
38 | }
39 |
40 | /**
41 | * Returns the path transpiled `index.js` file the client folder in which resides
42 | * the given `filename`
43 | */
44 | function getClientEntryPoint(filename) {
45 | var folderName = getClientFolderName(filename);
46 | var entryPoint = path.join(distDirectory, folderName, 'index.js');
47 | return entryPoint;
48 | }
49 |
50 | /**
51 | * Returns the name of the 1rst level folder inside `srcDirectory` in which the
52 | * client side javascript file resides.
53 | */
54 | function getClientFolderName(filename) {
55 | var relFilename = path.relative(srcDirectory, filename);
56 | var folderName = relFilename.split(path.sep)[0];
57 | return folderName;
58 | }
59 |
60 | /**
61 | * returns the transpiler to be consumed.
62 | */
63 | var transpiler = {
64 | /**
65 | * Transpile the given file from es6 to es5. If the given stack is not empty
66 | * call the method recursively till its empty. When the stack is empty,
67 | * execute the callback.
68 | */
69 | transpile: function(filename, stack, callback) {
70 | /**
71 | * If stack is not empty transpile the next entry, else execute the
72 | * callback if any.
73 | */
74 | function next() {
75 | if (stack && stack.length > 0)
76 | transpiler.transpile(stack.shift(), stack, callback);
77 | else if (stack.length === 0 && callback)
78 | callback();
79 | }
80 |
81 | if (filename === undefined || !isAllowed(filename))
82 | return next();
83 |
84 | var outFilename = getTarget(filename);
85 | var startTime = new Date().getTime();
86 |
87 | babel.transformFile(filename, babelOptions, function(err, result) {
88 | if (err)
89 | return log.transpileError(err);
90 |
91 | fse.outputFile(outFilename, result.code, function(err) {
92 | if (err)
93 | return console.error(err.message);
94 |
95 | log.transpileSuccess(filename, outFilename, startTime);
96 | next();
97 | });
98 | });
99 | },
100 |
101 | /** @private */
102 | _bundlers: [],
103 |
104 | /**
105 | * Transform a given file to it's browserified version, client only.
106 | * Only clients have their browserified counterparts, each folder in `src/client`
107 | * is considered has a separate browserified client file. The `index.js` in each
108 | * folder defines the entry point of the particular client. The browserified
109 | * file is name after the name of the folder.
110 | */
111 | bundle: function(filename, bundleDistDirectory, ensureFile, stopWhenDone) {
112 | if (filename === undefined || !isAllowed(filename, ensureFile))
113 | return;
114 |
115 | // get the entry point of the client (`index.js`)
116 | var entryPoint = getClientEntryPoint(filename);
117 | var outBasename = getClientFolderName(filename) + '.js';
118 | var outFilename = path.join(bundleDistDirectory, outBasename);
119 |
120 | if (!this._bundlers[entryPoint]) {
121 | var bundler = browserify(entryPoint, browserifyOptions);
122 |
123 | function rebundle() {
124 | fse.ensureFileSync(outFilename); // ensure file exists
125 |
126 | var startTime = new Date().getTime();
127 | var writeStream = fse.createWriteStream(outFilename);
128 |
129 | bundler.bundle()
130 | .on('error', function(err) {
131 | log.bundleError(outFilename, err);
132 | })
133 | .on('end', function() {
134 | if (stopWhenDone) {
135 | // don't remove, handle some bug of watchify when file is empty
136 | setTimeout(function() { bundler.close(); }, 100);
137 | }
138 |
139 | log.bundleSuccess(outFilename, startTime);
140 | })
141 | .pipe(writeStream);
142 |
143 | log.bundleStart(outFilename);
144 | }
145 |
146 | if (!stopWhenDone) {
147 | bundler.on('update', rebundle);
148 | this._bundlers[entryPoint] = bundler;
149 | }
150 |
151 | rebundle();
152 | }
153 | },
154 |
155 | /**
156 | * Delete the transpiled file.
157 | */
158 | delete: function(filename, callback) {
159 | var outFilename = getTarget(filename);
160 |
161 | fse.stat(outFilename, function(err, stats) {
162 | if (err)
163 | return console.log(err.message);
164 |
165 | if (stats.isFile()) {
166 | fse.remove(outFilename, function(err) {
167 | if (err)
168 | return console.log(err.message);
169 |
170 | log.deleteFile(outFilename);
171 |
172 | if (callback)
173 | callback();
174 | });
175 | } else {
176 | callback();
177 | }
178 | });
179 | },
180 | };
181 |
182 | return transpiler;
183 | }
184 |
185 | module.exports = {
186 | getTranspiler: getTranspiler
187 | };
188 |
--------------------------------------------------------------------------------
/node/src/client/controller/ControllerExperience.js:
--------------------------------------------------------------------------------
1 | import * as soundworks from 'soundworks/client';
2 |
3 | const audioContext = soundworks.audioContext;
4 | const client = soundworks.client;
5 |
6 | const viewTemplate = `
7 |
8 |
9 |
10 |
13 |
14 |
15 |
<%= checkinId %>
16 |
17 |
18 |
19 |
<%= subtitle %>
20 |
21 |
22 |
23 | `;
24 |
25 |
26 | /* Description:
27 | ...
28 | */
29 |
30 | export default class PlayerExperience extends soundworks.Experience {
31 | constructor(assetsDomain) {
32 | super();
33 |
34 | // soundworks services
35 | this.motionInput = this.require('motion-input', {
36 | descriptors: ['accelerationIncludingGravity', 'deviceorientation', 'energy']
37 | });
38 |
39 | // binding
40 | this.touchGestureDetect = this.touchGestureDetect.bind(this);
41 | this.touchCallback = this.touchCallback.bind(this);
42 |
43 | // local attributes
44 | this.throttle = {
45 | 'acc': [Infinity, Infinity, Infinity],
46 | 'accThreshold': 2.5,
47 | 'ori': [Infinity, Infinity, Infinity],
48 | 'oriThreshold': 3,
49 | 'energy': Infinity,
50 | 'energyThreshold': 0.1,
51 | 'touch': [Infinity, Infinity]
52 | };
53 | this.touchDataMap = new Map();
54 |
55 | }
56 |
57 | start() {
58 | super.start();
59 |
60 | // receive and display controller id
61 | this.receive('checkinId', (id) => {
62 | // initialize the view
63 | this.view = new soundworks.CanvasView(viewTemplate, { subtitle: `controller`, checkinId: id }, {}, {
64 | id: this.id,
65 | preservePixelRatio: true,
66 | });
67 | // start exp. once view showed
68 | this.show().then( this.startOnceViewShowed() );
69 | });
70 |
71 | }
72 |
73 | startOnceViewShowed() {
74 |
75 | // setup motion input listeners
76 | if (this.motionInput.isAvailable('accelerationIncludingGravity')) {
77 | this.motionInput.addListener('accelerationIncludingGravity', (data) => {
78 | // throttle
79 | let delta = Math.abs(this.throttle.acc[0] - data[0]) +
80 | Math.abs(this.throttle.acc[1] - data[1]) +
81 | Math.abs(this.throttle.acc[2] - data[2]);
82 | if( delta < this.throttle.accThreshold ){ return }
83 | // save new throttle values
84 | this.throttle.acc[0] = data[0];
85 | this.throttle.acc[1] = data[1];
86 | this.throttle.acc[2] = data[2];
87 | // send to OSC via server
88 | this.send('osc', '/nuController', ['acceleration', data[0], data[1], data[2]] );
89 | });
90 | }
91 |
92 | // setup motion input listeners
93 | if (this.motionInput.isAvailable('deviceorientation')) {
94 | this.motionInput.addListener('deviceorientation', (data) => {
95 | // throttle
96 | let delta = Math.abs(this.throttle.ori[0] - data[0]) +
97 | Math.abs(this.throttle.ori[1] - data[1]) +
98 | Math.abs(this.throttle.ori[2] - data[2]);
99 | if( delta < this.throttle.oriThreshold ){ return }
100 | // save new throttle values
101 | this.throttle.ori[0] = data[0];
102 | this.throttle.ori[1] = data[1];
103 | this.throttle.ori[2] = data[2];
104 | // send to OSC via server
105 | this.send('osc', '/nuController', ['orientation', data[0], data[1], data[2]] );
106 | });
107 | }
108 |
109 | // setup motion input listeners
110 | if (this.motionInput.isAvailable('energy')) {
111 | this.motionInput.addListener('energy', (data) => {
112 | // throttle
113 | let delta = Math.abs(this.throttle.energy - data);
114 | if( delta < this.throttle.energyThreshold ){ return }
115 | // save new throttle values
116 | this.throttle.energy = data;
117 | // send to OSC via server
118 | this.send('osc', '/nuController', ['energy', data] );
119 | });
120 | }
121 |
122 |
123 | // disable text selection, magnifier, and screen move on swipe on ios
124 | document.getElementsByTagName("body")[0].addEventListener("touchstart",
125 | function(e) { e.returnValue = false });
126 |
127 | const surface = new soundworks.TouchSurface(this.view.$el);
128 |
129 | // setup touch listeners
130 | surface.addListener('touchstart', (id, normX, normY) => {
131 | // notify touch on
132 | this.send('osc', '/nuController', ['touchOn', 1] );
133 | // reset touch memory
134 | this.touchDataMap.set(id, []);
135 | // general callback
136 | this.touchCallback(id, normX, normY);
137 | });
138 |
139 | surface.addListener('touchmove', (id, normX, normY) => {
140 | // general callback
141 | this.touchCallback(id, normX, normY);
142 | });
143 |
144 | surface.addListener('touchend', (id, normX, normY) => {
145 | // general callback
146 | this.touchCallback(id, normX, normY);
147 | // notify touch off
148 | this.send('osc', '/nuController', ['touchOn', 0] );
149 | // gesture detection
150 | this.touchGestureDetect(this.touchDataMap.get(id));
151 | });
152 |
153 | }
154 |
155 | touchCallback(id, normX, normY){
156 | // save touch data
157 | this.touchDataMap.get(id).push([audioContext.currentTime, normX, normY]);
158 | // send touch pos
159 | this.send('osc', '/nuController', ['touchPos', id, normX, normY]);
160 | }
161 |
162 | touchGestureDetect(data) {
163 | let N = data.length - 1;
164 | let pathVect = [data[N][1] - data[0][1], data[N][2] - data[0][2]];
165 | let pathDuration = data[N][0] - data[0][0];
166 |
167 | // discard slow movements
168 | if (pathDuration > 2.0) return;
169 |
170 | // swipe up
171 | if (pathVect[1] > 0.4)
172 | this.send('osc', '/nuController', ['gesture', 'swipe', 0] );
173 | // swipe down
174 | if (pathVect[1] < -0.4)
175 | this.send('osc', '/nuController', ['gesture', 'swipe', 1] );
176 | }
177 |
178 | }
--------------------------------------------------------------------------------
/node/src/client/player/NuRoomReverb.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuRoomReverb: Nu module in charge of room reverb where players
3 | * emit bursts when the acoustical wave passes them by
4 | **/
5 |
6 | import NuBaseModule from './NuBaseModule'
7 | import * as soundworks from 'soundworks/client';
8 |
9 | const client = soundworks.client;
10 | const audioContext = soundworks.audioContext;
11 |
12 | export default class NuRoomReverb extends NuBaseModule {
13 | constructor(playerExperience) {
14 | super(playerExperience, 'nuRoomReverb');
15 |
16 | // local attributes
17 | this.irMap = new Map();
18 | this.srcSet = new Set();
19 | this.params = {};
20 |
21 | // binding
22 | this.rawSocketCallback = this.rawSocketCallback.bind(this);
23 | this.emitAtPos = this.emitAtPos.bind(this);
24 | this.reset = this.reset.bind(this);
25 |
26 | // setup socket reveive callbacks (receiving raw audio data)
27 | this.e.rawSocket.receive('nuRoomReverb', this.rawSocketCallback );
28 | }
29 |
30 | /*
31 | * callback when websocket event (msg containing new IR sent by server) is received
32 | */
33 | rawSocketCallback(interleavedIrArray) {
34 |
35 | // extract header
36 | let emitterId = interleavedIrArray[0];
37 | let minTime = interleavedIrArray[1];
38 | // exctract data
39 | interleavedIrArray = interleavedIrArray.slice(2, interleavedIrArray.length);
40 |
41 | // de-interleave + get max delay for IR buffer size
42 | let irTime = [],
43 | irGain = [],
44 | irDuration = 0.0;
45 | for (let i = 0; i < interleavedIrArray.length / 2; i++) {
46 | irTime[i] = interleavedIrArray[2 * i] - minTime;
47 | irGain[i] = interleavedIrArray[2 * i + 1];
48 | irDuration = Math.max(irDuration, irTime[i]);
49 | }
50 |
51 | // store IR
52 | let ir = { times: irTime, gains: irGain, duration: irDuration };
53 | this.irMap.set(emitterId, ir);
54 |
55 | // feedback user that IR has been loaded
56 | this.e.renderer.blink([0, 100, 0]);
57 | }
58 |
59 |
60 | /*
61 | * message callback: play sound convolved with IR
62 | */
63 | emitAtPos(args) {
64 | let irId = args.shift();
65 | let syncStartTime = args.shift();
66 |
67 | // check if designated audioFile exists in loader
68 | if (this.e.loader.data[this.params.audioFileId] == undefined) {
69 | console.warn('required audio file id', this.params.audioFileId, 'not in client index, actual content:', this.e.loader.options.files);
70 | return;
71 | }
72 |
73 | // check if IR not available yet: slightly flash red otherwise
74 | if (!this.irMap.has(irId)) {
75 | this.e.renderer.blink([160, 0, 0]);
76 | console.warn('IR', irId, 'not yet defined in client, need to update propagation');
77 | return;
78 | }
79 |
80 | // init
81 | let ir = this.irMap.get(irId);
82 |
83 | // create empty sound src
84 | let src = audioContext.createBufferSource();
85 | let inputBuffer = this.e.loader.data[this.params.audioFileId];
86 | let outputDuration = ir.duration + inputBuffer.duration + 1;
87 | let outputBuffer = audioContext.createBuffer(1, Math.max(outputDuration * audioContext.sampleRate, 512), audioContext.sampleRate);
88 |
89 | // fill sound source with delayed audio buffer version (tap delay line mecanism)
90 | let inputData = inputBuffer.getChannelData(0);
91 | let outputData = outputBuffer.getChannelData(0);
92 | ir.times.forEach((tapTime, index) => {
93 |
94 | // get tap time and gain
95 | let tapGain = ir.gains[index];
96 | let tapdelayInSample = Math.floor(tapTime * audioContext.sampleRate);
97 |
98 | // get input start point based on time since propagation started
99 | let offsetTimeInSamples = Math.floor(this.params.timeBound * tapdelayInSample);
100 | if (this.params.loop) offsetTimeInSamples %= inputBuffer.length;
101 |
102 | // if end of audio input not reached yet
103 | if (offsetTimeInSamples < inputBuffer.length) {
104 |
105 | // eventually read only a chunk of input buffer
106 | let numSamplesToFill = inputBuffer.length - offsetTimeInSamples;
107 | numSamplesToFill = Math.floor(numSamplesToFill * this.params.perc);
108 |
109 | // if reading speed acc with time passed
110 | let readSpeed = 1 + this.params.accSlope * tapTime;
111 | numSamplesToFill = Math.floor(numSamplesToFill / readSpeed);
112 |
113 | // copy tap to output buffer
114 | for (let i = 0; i < numSamplesToFill; i++)
115 | outputData[tapdelayInSample + i] += (tapGain * inputData[offsetTimeInSamples + Math.round(i * readSpeed)]);
116 | }
117 |
118 | });
119 |
120 | // normalize output buffer
121 | let maxOutputValue = 0.0;
122 | for (let i = 0; i < outputBuffer.length; i++) {
123 | maxOutputValue = Math.max(Math.abs(outputData[i]), maxOutputValue);
124 | }
125 | let normFactor = Math.max.apply(null, ir.gains) / Math.max(maxOutputValue, 1.0);
126 | // console.log('max:', maxOutputValue, 'norm:', normFactor);
127 |
128 | // replace audio source buffer with created output buffer
129 | src.buffer = outputBuffer;
130 |
131 | // create master gain (shared param, controlled from conductor)
132 | let gain = audioContext.createGain();
133 | gain.gain.value = normFactor * this.params.masterGain;
134 |
135 | // connect graph
136 | src.connect(gain);
137 | gain.connect( this.e.nuOutput.in );
138 |
139 | // play sound if rendez-vous time is in the future (else report bug)
140 | let now = this.e.sync.getSyncTime()
141 | if (syncStartTime > now) {
142 | let audioContextStartTime = audioContext.currentTime + syncStartTime - now;
143 | src.start(audioContextStartTime);
144 | // console.log('play scheduled in:', Math.round((syncStartTime - now) * 1000) / 1000, 'sec', 'at:', syncStartTime);
145 | } else {
146 | console.warn('no sound played, I received the instruction to play to late');
147 | this.e.renderer.blink([250, 0, 0]);
148 | }
149 |
150 | // setup screen color = f(amplitude) callback
151 | this.e.renderer.enable();
152 |
153 | // save source for eventual global reset
154 | this.srcSet.add(src);
155 |
156 | // timeout callback, runs when we finished playing
157 | setTimeout(() => {
158 | // disable visual feeback
159 | this.e.renderer.disable();
160 | // remove source from set
161 | this.srcSet.delete(src);
162 | }, (syncStartTime - now + src.buffer.duration) * 1000);
163 |
164 | }
165 |
166 | // stop all audio sources
167 | reset(){
168 | this.srcSet.forEach( (src) => {
169 | // stop source
170 | src.stop();
171 | // remove associated visual feedback
172 | this.e.renderer.disable();
173 | });
174 | }
175 |
176 | }
--------------------------------------------------------------------------------
/maxmsp/patchers/nu.template.maxpat:
--------------------------------------------------------------------------------
1 | {
2 | "patcher" : {
3 | "fileversion" : 1,
4 | "appversion" : {
5 | "major" : 7,
6 | "minor" : 3,
7 | "revision" : 3,
8 | "architecture" : "x86",
9 | "modernui" : 1
10 | }
11 | ,
12 | "rect" : [ 173.0, 514.0, 344.0, 282.0 ],
13 | "bglocked" : 0,
14 | "openinpresentation" : 0,
15 | "default_fontsize" : 12.0,
16 | "default_fontface" : 0,
17 | "default_fontname" : "Arial",
18 | "gridonopen" : 1,
19 | "gridsize" : [ 15.0, 15.0 ],
20 | "gridsnaponopen" : 1,
21 | "objectsnaponopen" : 1,
22 | "statusbarvisible" : 2,
23 | "toolbarvisible" : 1,
24 | "lefttoolbarpinned" : 0,
25 | "toptoolbarpinned" : 0,
26 | "righttoolbarpinned" : 0,
27 | "bottomtoolbarpinned" : 0,
28 | "toolbars_unpinned_last_save" : 0,
29 | "tallnewobj" : 0,
30 | "boxanimatetime" : 200,
31 | "enablehscroll" : 1,
32 | "enablevscroll" : 1,
33 | "devicewidth" : 0.0,
34 | "description" : "",
35 | "digest" : "",
36 | "tags" : "",
37 | "style" : "",
38 | "subpatcher_template" : "",
39 | "boxes" : [ {
40 | "box" : {
41 | "id" : "obj-8",
42 | "maxclass" : "message",
43 | "numinlets" : 2,
44 | "numoutlets" : 1,
45 | "outlettype" : [ "" ],
46 | "patching_rect" : [ 15.0, 98.0, 43.0, 22.0 ],
47 | "style" : "",
48 | "text" : "set $1"
49 | }
50 |
51 | }
52 | , {
53 | "box" : {
54 | "id" : "obj-6",
55 | "maxclass" : "newobj",
56 | "numinlets" : 1,
57 | "numoutlets" : 1,
58 | "outlettype" : [ "" ],
59 | "patching_rect" : [ 15.0, 189.5, 71.0, 22.0 ],
60 | "style" : "",
61 | "text" : "prepend #1"
62 | }
63 |
64 | }
65 | , {
66 | "box" : {
67 | "id" : "obj-3",
68 | "maxclass" : "message",
69 | "numinlets" : 2,
70 | "numoutlets" : 1,
71 | "outlettype" : [ "" ],
72 | "patching_rect" : [ 228.0, 164.0, 50.0, 22.0 ],
73 | "style" : "",
74 | "text" : "gain $1"
75 | }
76 |
77 | }
78 | , {
79 | "box" : {
80 | "id" : "obj-2",
81 | "maxclass" : "message",
82 | "numinlets" : 2,
83 | "numoutlets" : 1,
84 | "outlettype" : [ "" ],
85 | "patching_rect" : [ 157.0, 130.0, 140.0, 22.0 ],
86 | "style" : "",
87 | "text" : "directToClientMethod $1"
88 | }
89 |
90 | }
91 | , {
92 | "box" : {
93 | "id" : "obj-12",
94 | "maxclass" : "newobj",
95 | "numinlets" : 1,
96 | "numoutlets" : 0,
97 | "patching_rect" : [ 15.0, 245.5, 65.0, 22.0 ],
98 | "style" : "",
99 | "text" : "s toServer"
100 | }
101 |
102 | }
103 | , {
104 | "box" : {
105 | "id" : "obj-13",
106 | "maxclass" : "newobj",
107 | "numinlets" : 1,
108 | "numoutlets" : 1,
109 | "outlettype" : [ "" ],
110 | "patching_rect" : [ 15.0, 217.5, 123.0, 22.0 ],
111 | "style" : "",
112 | "text" : "prepend /nuTemplate"
113 | }
114 |
115 | }
116 | , {
117 | "box" : {
118 | "id" : "obj-4",
119 | "maxclass" : "message",
120 | "numinlets" : 2,
121 | "numoutlets" : 1,
122 | "outlettype" : [ "" ],
123 | "patching_rect" : [ 86.0, 98.0, 101.0, 22.0 ],
124 | "style" : "",
125 | "text" : "serverMethod $1"
126 | }
127 |
128 | }
129 | , {
130 | "box" : {
131 | "id" : "obj-24",
132 | "maxclass" : "newobj",
133 | "numinlets" : 1,
134 | "numoutlets" : 0,
135 | "patching_rect" : [ 265.0, 23.0, 53.0, 22.0 ],
136 | "style" : "",
137 | "text" : "nu.error"
138 | }
139 |
140 | }
141 | , {
142 | "box" : {
143 | "id" : "obj-23",
144 | "maxclass" : "newobj",
145 | "numinlets" : 5,
146 | "numoutlets" : 5,
147 | "outlettype" : [ "", "", "", "", "" ],
148 | "patching_rect" : [ 15.0, 55.0, 303.0, 22.0 ],
149 | "style" : "",
150 | "text" : "route playerId serverMethod directToClientMethod gain"
151 | }
152 |
153 | }
154 | , {
155 | "box" : {
156 | "comment" : "",
157 | "id" : "obj-1",
158 | "index" : 0,
159 | "maxclass" : "inlet",
160 | "numinlets" : 0,
161 | "numoutlets" : 1,
162 | "outlettype" : [ "" ],
163 | "patching_rect" : [ 15.0, 9.0, 30.0, 30.0 ],
164 | "style" : ""
165 | }
166 |
167 | }
168 | ],
169 | "lines" : [ {
170 | "patchline" : {
171 | "destination" : [ "obj-23", 0 ],
172 | "disabled" : 0,
173 | "hidden" : 0,
174 | "source" : [ "obj-1", 0 ]
175 | }
176 |
177 | }
178 | , {
179 | "patchline" : {
180 | "destination" : [ "obj-12", 0 ],
181 | "disabled" : 0,
182 | "hidden" : 0,
183 | "source" : [ "obj-13", 0 ]
184 | }
185 |
186 | }
187 | , {
188 | "patchline" : {
189 | "destination" : [ "obj-6", 0 ],
190 | "disabled" : 0,
191 | "hidden" : 0,
192 | "source" : [ "obj-2", 0 ]
193 | }
194 |
195 | }
196 | , {
197 | "patchline" : {
198 | "destination" : [ "obj-2", 0 ],
199 | "disabled" : 0,
200 | "hidden" : 0,
201 | "source" : [ "obj-23", 2 ]
202 | }
203 |
204 | }
205 | , {
206 | "patchline" : {
207 | "destination" : [ "obj-24", 0 ],
208 | "disabled" : 0,
209 | "hidden" : 0,
210 | "source" : [ "obj-23", 4 ]
211 | }
212 |
213 | }
214 | , {
215 | "patchline" : {
216 | "destination" : [ "obj-3", 0 ],
217 | "disabled" : 0,
218 | "hidden" : 0,
219 | "source" : [ "obj-23", 3 ]
220 | }
221 |
222 | }
223 | , {
224 | "patchline" : {
225 | "destination" : [ "obj-4", 0 ],
226 | "disabled" : 0,
227 | "hidden" : 0,
228 | "source" : [ "obj-23", 1 ]
229 | }
230 |
231 | }
232 | , {
233 | "patchline" : {
234 | "destination" : [ "obj-8", 0 ],
235 | "disabled" : 0,
236 | "hidden" : 0,
237 | "source" : [ "obj-23", 0 ]
238 | }
239 |
240 | }
241 | , {
242 | "patchline" : {
243 | "destination" : [ "obj-6", 0 ],
244 | "disabled" : 0,
245 | "hidden" : 0,
246 | "source" : [ "obj-3", 0 ]
247 | }
248 |
249 | }
250 | , {
251 | "patchline" : {
252 | "destination" : [ "obj-6", 0 ],
253 | "disabled" : 0,
254 | "hidden" : 0,
255 | "source" : [ "obj-4", 0 ]
256 | }
257 |
258 | }
259 | , {
260 | "patchline" : {
261 | "destination" : [ "obj-13", 0 ],
262 | "disabled" : 0,
263 | "hidden" : 0,
264 | "source" : [ "obj-6", 0 ]
265 | }
266 |
267 | }
268 | , {
269 | "patchline" : {
270 | "destination" : [ "obj-6", 0 ],
271 | "disabled" : 0,
272 | "hidden" : 0,
273 | "source" : [ "obj-8", 0 ]
274 | }
275 |
276 | }
277 | ],
278 | "dependency_cache" : [ {
279 | "name" : "nu.error.maxpat",
280 | "bootpath" : "~/Projects/Cosima/devs/Nu/soundworks-nu/maxmsp/extras",
281 | "type" : "JSON",
282 | "implicit" : 1
283 | }
284 | ],
285 | "autosave" : 0
286 | }
287 |
288 | }
289 |
--------------------------------------------------------------------------------
/node/src/client/player/NuOutput.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuOutput: render output, either directly to audioContext.destination or
3 | * to spatialization engine for debug sessions (i.e. to get a feel of the final
4 | * result while players are emulated on server's laptop). Spatialization is based
5 | * Ambisonic encoding plugged in binaural decoding.
6 | **/
7 |
8 | import NuBaseModule from './NuBaseModule'
9 | import * as soundworks from 'soundworks/client';
10 | import * as ambisonics from 'ambisonics';
11 | import Recorder from 'recorderjs';
12 |
13 | const client = soundworks.client;
14 | const audioContext = soundworks.audioContext;
15 |
16 | /**
17 | * convert Cartesian to spherical coordinates
18 | * (azimuth is in xy. elev at zero is in xy, positive up)
19 | **/
20 | const cart2sph = function(xyz){
21 | let r2d = 180 / Math.PI;
22 | let d = Math.sqrt( Math.pow(xyz[0], 2) + Math.pow(xyz[1], 2) + Math.pow(xyz[2], 2) );
23 | let a = r2d * Math.atan2(xyz[0], xyz[1]);
24 | let e = 0;
25 | if( d !== 0 ){ e = r2d * Math.asin(xyz[2] / d); }
26 | return [a,e,d];
27 | }
28 |
29 | export default class NuOutput extends NuBaseModule {
30 | constructor(playerExperience) {
31 | super(playerExperience, 'nuOutput');
32 |
33 | // local attributes
34 | this.params = { userPos: [0, 0, 0] };
35 |
36 | /**
37 | * input gain (connected to analyzer for visual feedback)
38 | * (had to do it the other around since Safari's analyser would
39 | * remain frozen this.in was the gain connected to the analyser)
40 | **/
41 | this.in = this.e.renderer.audioAnalyser.in;
42 | this.masterGain = audioContext.createGain();
43 | this.out = audioContext.createGain();
44 |
45 | // create Ambisonic encoder / decoder
46 | this.maxOrder = 3;
47 | this.ambiOrderValue = 3;
48 | this.encoder = new ambisonics.monoEncoder(audioContext, this.ambiOrderValue);
49 | this.limiter = new ambisonics.orderLimiter(audioContext, this.maxOrder, this.maxOrder);
50 | this.decoder = new ambisonics.binDecoder(audioContext, this.ambiOrderValue);
51 |
52 | // create additional gain to compensate for badly norm. room IR
53 | this.ambiGain = audioContext.createGain();
54 |
55 | // create audio recorder, a too large bufferLen here will produce unsync
56 | // recordings when server will add client's buffers together (the javascript
57 | // node "starting" to record samples in indpt worker will fire in between
58 | // "start record" and in bufferLen samples, and that "randomly" for each client)
59 | this.recorder = new Recorder( this.out, {bufferLen: 512} );
60 | this.startRecTime = 0.0;
61 |
62 | // init coordinates
63 | let coordXY = this.e.coordinates;
64 | this.coordXYZ = [ coordXY[0], coordXY[1], 0];
65 |
66 | // connect graph
67 | this.in.connect( this.masterGain );
68 | this.out.connect( audioContext.destination );
69 | this.ambiGain.connect( this.encoder.in )
70 | this.encoder.out.connect( this.limiter.in );
71 | this.limiter.out.connect( this.decoder.in );
72 | }
73 |
74 | // trigger session recording to disk
75 | record(val) {
76 | // start recording
77 | if (val) {
78 | // start recording
79 | this.recorder.clear();
80 | this.recorder.record();
81 | this.startRecTime = this.e.sync.getSyncTime();
82 | }
83 | // stop recording
84 | else {
85 | // stop recorder
86 | this.recorder.stop();
87 | // get recorder buffer and send audio data to server
88 | this.recorder.getBuffer( (buffers) => {
89 |
90 | // create empty buffer for interleaving before send
91 | let headerLength = 3;
92 | var interleavedBuffer = new Float32Array( 2*buffers[0].length + headerLength);
93 |
94 | // add header
95 | interleavedBuffer[0] = client.index;
96 | interleavedBuffer[1] = this.startRecTime;
97 | interleavedBuffer[2] = audioContext.sampleRate;
98 |
99 | // fill interleaved buffer
100 | for( let i = 0; i < buffers[0].length; i++ ){
101 | interleavedBuffer[ headerLength + 2*i ] = buffers[0][i];
102 | interleavedBuffer[ headerLength + 2*i + 1] = buffers[1][i];
103 | }
104 |
105 | // send audio data
106 | this.e.rawSocket.send( this.moduleName, interleavedBuffer );
107 | });
108 |
109 | }
110 | }
111 |
112 | // set audio gain out
113 | gain(val){
114 | this.masterGain.gain.value = val;
115 | }
116 |
117 | // enable / disable spatialization of player based on its position in the room
118 | enableSpat(val){
119 | if(val){
120 | try{ this.masterGain.disconnect( this.out ); }
121 | catch(e){ if( e.name !== 'InvalidAccessError'){ console.error(e); } }
122 | this.masterGain.connect( this.ambiGain );
123 | this.decoder.out.connect( this.out );
124 | }
125 | else{
126 | try{
127 | this.decoder.out.disconnect( this.out );
128 | this.masterGain.disconnect( this.ambiGain );
129 | }
130 | catch(e){ if( e.name !== 'InvalidAccessError'){ console.error(e); } }
131 | this.masterGain.connect( this.out );
132 | }
133 | }
134 |
135 | // set encoding Ambisonic order
136 | ambiOrder(val){
137 | // filter order in
138 | if( val > 3 || val < 1 ){ return; }
139 | this.limiter.updateOrder( val );
140 | this.limiter.out.connect( this.decoder.in );
141 | }
142 |
143 | /**
144 | * enable spatialized room reverberation
145 | * (replace dry Ambisonic IR with Room Ambisonic IR)
146 | **/
147 | enableRoom(val){
148 | let irUrl = '';
149 | if( val ){
150 | // different IR for reverb (+ gain adjust for iso-loudness)
151 | irUrl = 'irs/HOA3_BRIRs-medium.wav';
152 | this.ambiGain.gain.value = 0.5;
153 | }
154 | else{
155 | irUrl = 'irs/HOA3_filters_virtual.wav';
156 | this.ambiGain.gain.value = 1.0;
157 | }
158 | // load HOA to bianural filters in decoder
159 | var loader_filters = new ambisonics.HOAloader(audioContext, this.maxOrder, irUrl, (buffer) => { this.decoder.updateFilters(buffer); } );
160 | loader_filters.load();
161 | }
162 |
163 | /** define fake user position (the user here is the composer / debugger,
164 | * sitting in front of browsers simulating the players, deciding where
165 | * he/she wants to be in the room during the composition / debug session)
166 | **/
167 | userPos(args){
168 | this.params.userPos[0] = args[0];
169 | this.params.userPos[1] = args[1];
170 | this.setPos();
171 | }
172 |
173 | // update player position
174 | setPos(){
175 | // get rel. pos from user (debug listener)
176 | let relXYZ = [];
177 | for( let i = 0; i < 3; i++ ){
178 | relXYZ.push( this.params.userPos[i] - this.coordXYZ[i] );
179 | }
180 | let coordSph = cart2sph( relXYZ );
181 | // update encoder parameters
182 | this.encoder.azim = coordSph[0];
183 | this.encoder.elev = coordSph[1];
184 | this.encoder.updateGains();
185 | }
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/node/src/client/player/NuProbe.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuProbe: get motion etc. info from given client in OSC
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 | import * as soundworks from 'soundworks/client';
7 |
8 | const client = soundworks.client;
9 | const audioContext = soundworks.audioContext;
10 |
11 | export default class NuProbe extends NuBaseModule {
12 | constructor(playerExperience) {
13 | super(playerExperience, 'nuProbe');
14 |
15 | // local attributes
16 | this.params = {};
17 | this.throttle = {
18 | 'acc': [Infinity, Infinity, Infinity],
19 | 'accThreshold': 2.5,
20 | 'ori': [Infinity, Infinity, Infinity],
21 | 'oriThreshold': 3,
22 | 'energy': Infinity,
23 | 'energyThreshold': 0.1,
24 | 'touch': [Infinity, Infinity]
25 | };
26 | this.callBackStatus = {
27 | ori: false,
28 | acc: false,
29 | energy: false,
30 | touch: false
31 | };
32 | this.surface = new soundworks.TouchSurface(this.e.view.$el);
33 |
34 | // binding
35 | this.orientationCallback = this.orientationCallback.bind(this);
36 | this.accelerationCallback = this.accelerationCallback.bind(this);
37 | this.energyCallback = this.energyCallback.bind(this);
38 | this.touchStartCallback = this.touchStartCallback.bind(this);
39 | this.touchMoveCallback = this.touchMoveCallback.bind(this);
40 | this.touchEndCallback = this.touchEndCallback.bind(this);
41 | }
42 |
43 | // Note: all callbacks in aftewards section are enabled / disabled with the methods
44 | // at scripts' end (via OSC msg)
45 |
46 | orientationCallback(data){
47 | // for computers, otherwise they'll send [null,null,null] at startup
48 | if( data[0] === null ){ return; }
49 | // throttle
50 | let delta = Math.abs(this.throttle.ori[0] - data[0]) +
51 | Math.abs(this.throttle.ori[1] - data[1]) +
52 | Math.abs(this.throttle.ori[2] - data[2]);
53 | if( delta < this.throttle.oriThreshold ){ return }
54 | // save new throttle values
55 | this.throttle.ori[0] = data[0];
56 | this.throttle.ori[1] = data[1];
57 | this.throttle.ori[2] = data[2];
58 | // send to OSC via server
59 | this.e.send('osc', '/' + this.moduleName, ['orientation', data[0], data[1], data[2]] );
60 | }
61 |
62 | accelerationCallback(data){
63 | // throttle
64 | let delta = Math.abs(this.throttle.acc[0] - data[0]) +
65 | Math.abs(this.throttle.acc[1] - data[1]) +
66 | Math.abs(this.throttle.acc[2] - data[2]);
67 | if( delta < this.throttle.accThreshold ){ return }
68 | // save new throttle values
69 | this.throttle.acc[0] = data[0];
70 | this.throttle.acc[1] = data[1];
71 | this.throttle.acc[2] = data[2];
72 | // send to OSC via server
73 | this.e.send('osc', '/' + this.moduleName, ['acceleration', data[0], data[1], data[2]] );
74 | }
75 |
76 | energyCallback(data){
77 | // throttle
78 | let delta = Math.abs(this.throttle.energy - data);
79 | if( delta < this.throttle.energyThreshold ){ return }
80 | // save new throttle values
81 | this.throttle.energy = data;
82 | // send to OSC via server
83 | this.e.send('osc', '/' + this.moduleName, ['energy', data] );
84 | }
85 |
86 | touchStartCallback(id, normX, normY){
87 | // notify touch on
88 | this.e.send('osc', '/' + this.moduleName, ['touchOn', 1] );
89 | // common touch callback
90 | this.touchCommonCallback(id, normX, normY);
91 | }
92 |
93 | touchMoveCallback(id, normX, normY){
94 | // common touch callback
95 | this.touchCommonCallback(id, normX, normY);
96 | }
97 |
98 | touchEndCallback(id, normX, normY){
99 | // notify touch off
100 | this.e.send('osc', '/' + this.moduleName, ['touchOn', 0] );
101 | // common touch callback
102 | this.touchCommonCallback(id, normX, normY);
103 | }
104 |
105 | touchCommonCallback(id, normX, normY){
106 | // ATTEMPT AT CROSSMODULE POSTING: FUNCTIONAL BUT ORIGINAL USE NO LONGER CONSIDERED: TODELETE WHEN CONFIRMED
107 | // window.postMessage(['nuRenderer', 'touch', id, normX, normY], location.origin);
108 | // ----------
109 | // send touch pos
110 | this.e.send('osc', '/' + this.moduleName, ['touchPos', id, normX, normY]);
111 | }
112 |
113 | // Note: hereafter are the OSC triggered functions used to enable / disable
114 | // hereabove callbacks
115 |
116 | touch(onOff){
117 | // enable if not already enabled
118 | if( onOff && !this.callBackStatus.touch ){
119 | this.surface.addListener('touchstart', this.touchStartCallback);
120 | this.surface.addListener('touchmove', this.touchMoveCallback);
121 | this.surface.addListener('touchend', this.touchEndCallback);
122 | this.callBackStatus.touch = true;
123 | }
124 | // disable if not already disabled
125 | if( !onOff && this.callBackStatus.touch ){
126 | this.surface.removeListener('touchstart', this.touchStartCallback);
127 | this.surface.removeListener('touchmove', this.touchMoveCallback);
128 | this.surface.removeListener('touchend', this.touchEndCallback);
129 | this.callBackStatus.touch = false;
130 | }
131 | }
132 |
133 | orientation(onOff){
134 | // discard instruction if motionInput not available
135 | if (!this.e.motionInput.isAvailable('deviceorientation')){ return; }
136 | // enable if not already enabled
137 | if( onOff && !this.callBackStatus.ori ){
138 | this.e.motionInput.addListener('deviceorientation', this.orientationCallback);
139 | this.callBackStatus.ori = true;
140 | }
141 | // disable if not already disabled
142 | if( !onOff && this.callBackStatus.ori ){
143 | this.e.motionInput.removeListener('deviceorientation', this.orientationCallback);
144 | this.callBackStatus.ori = false;
145 | }
146 | }
147 |
148 | acceleration(onOff){
149 | // discard instruction if motionInput not available
150 | if (!this.e.motionInput.isAvailable('accelerationIncludingGravity')){ return; }
151 | // enable if not already enabled
152 | if( onOff && !this.callBackStatus.acc ){
153 | this.e.motionInput.addListener('accelerationIncludingGravity', this.accelerationCallback);
154 | this.callBackStatus.acc = true;
155 | }
156 | // disable if not already disabled
157 | if( !onOff && this.callBackStatus.acc ){
158 | this.e.motionInput.removeListener('accelerationIncludingGravity', this.accelerationCallback);
159 | this.callBackStatus.acc = false;
160 | }
161 | }
162 |
163 | energy(onOff){
164 | // discard instruction if motionInput not available
165 | if (!this.e.motionInput.isAvailable('energy')){ return; }
166 | // enable if not already enabled
167 | if( onOff && !this.callBackStatus.energy ){
168 | this.e.motionInput.addListener('energy', this.energyCallback);
169 | this.callBackStatus.energy = true;
170 | }
171 | // disable if not already disabled
172 | if( !onOff && this.callBackStatus.energy ){
173 | this.e.motionInput.removeListener('energy', this.energyCallback);
174 | this.callBackStatus.energy = false;
175 | }
176 | }
177 |
178 | }
--------------------------------------------------------------------------------
/node/src/client/player/NuPath.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuPath: Nu module to move a sound through players topology, based on
3 | * emission points. A path is composed of emission points coupled with
4 | * emission time. Each point is used as a source image to produce a tap
5 | * in player's IR.
6 | **/
7 |
8 | import NuBaseModule from './NuBaseModule'
9 | import * as soundworks from 'soundworks/client';
10 |
11 | const client = soundworks.client;
12 | const audioContext = soundworks.audioContext;
13 |
14 | export default class NuPath extends NuBaseModule {
15 | constructor(playerExperience) {
16 | super(playerExperience, 'nuPath');
17 |
18 | // local attributes
19 | this.irMap = new Map();
20 | this.srcSet = new Set();
21 | this.params = {};
22 |
23 | // binding
24 | this.rawSocketCallback = this.rawSocketCallback.bind(this);
25 | this.startPath = this.startPath.bind(this);
26 | this.reset = this.reset.bind(this);
27 |
28 | // setup socket reveive callbacks (receiving raw audio data)
29 | this.e.rawSocket.receive(this.moduleName, this.rawSocketCallback );
30 | }
31 |
32 | /*
33 | * callback when websocket event (msg containing new IR sent by server) is received
34 | */
35 | rawSocketCallback(interleavedIrArray) {
36 |
37 | // extract header
38 | let pathId = interleavedIrArray[0];
39 | let minTime = interleavedIrArray[1];
40 | // exctract data
41 | interleavedIrArray = interleavedIrArray.slice(2, interleavedIrArray.length);
42 | // console.log('pathId', pathId, 'minTime', minTime, 'ir', interleavedIrArray);
43 |
44 | // de-interleave + get max delay for IR buffer size
45 | let irTime = [0.0], // init at zero to handle scenarii where IR array is empty
46 | irGain = [0.0],
47 | irDuration = 0.0;
48 | for (let i = 0; i < interleavedIrArray.length / 2; i++) {
49 | irTime[i] = interleavedIrArray[2 * i] - minTime;
50 | irGain[i] = interleavedIrArray[2 * i + 1];
51 | irDuration = Math.max(irDuration, irTime[i]);
52 | }
53 |
54 | // store ir
55 | let ir = { times: irTime, gains: irGain, duration: irDuration };
56 | this.irMap.set(pathId, ir);
57 |
58 | // feedback user that IR has been loaded
59 | this.e.renderer.blink([0, 100, 0]);
60 | }
61 |
62 | /*
63 | * message callback: play sound
64 | */
65 | startPath( args ) {
66 | // get parameters
67 | let irId = args.shift();
68 | let syncStartTime = args.shift();
69 |
70 | // check if designated audioFile exists in loader
71 | if (this.e.loader.data[this.params.audioFileId] == undefined) {
72 | console.warn('required audio file id', this.params.audioFileId, 'not in client index, actual content:', this.e.loader.options.files);
73 | return;
74 | }
75 |
76 | // check if IR not available yet: slightly flash red otherwise
77 | if (!this.irMap.has(irId)) {
78 | this.e.renderer.blink([160, 0, 0]);
79 | console.warn('IR', irId, 'not yet defined in client, need to update propagation');
80 | return;
81 | }
82 |
83 | // init
84 | let ir = this.irMap.get(irId);
85 |
86 | // create empty sound src
87 | let src = audioContext.createBufferSource();
88 | let inputBuffer = this.e.loader.data[this.params.audioFileId];
89 | let outputDuration = ir.duration + inputBuffer.duration + 1;
90 | let outputBuffer = audioContext.createBuffer(1, Math.max(outputDuration * audioContext.sampleRate, 512), audioContext.sampleRate);
91 |
92 | // this.controlParams = {audioFileId: 0, segment: {perc: 1, loop: true, accSlope: 0, timeBound: 0} };
93 |
94 |
95 | // fill sound source with delayed audio buffer version (tap delay line mecanism)
96 | let inputData = inputBuffer.getChannelData(0);
97 | let outputData = outputBuffer.getChannelData(0);
98 | ir.times.forEach((tapTime, index) => {
99 |
100 | // get tap time and gain
101 | let tapGain = ir.gains[index];
102 | let tapdelayInSample = Math.floor(tapTime * audioContext.sampleRate);
103 |
104 | // get input start point based on time since propagation started
105 | let offsetTimeInSamples = Math.floor(this.params.timeBound * tapdelayInSample);
106 | if (this.params.loop) offsetTimeInSamples %= inputBuffer.length;
107 |
108 | // if end of audio input not reached yet
109 | if (offsetTimeInSamples < inputBuffer.length) {
110 |
111 | // eventually read only a chunk of input buffer
112 | let numSamplesToFill = inputBuffer.length - offsetTimeInSamples;
113 | numSamplesToFill = Math.floor(numSamplesToFill * this.params.perc);
114 |
115 | // if reading speed acc with time passed
116 | let readSpeed = 1 + this.params.accSlope * tapTime;
117 | numSamplesToFill = Math.floor(numSamplesToFill / readSpeed);
118 |
119 | // copy tap to output buffer
120 | for (let i = 0; i < numSamplesToFill; i++)
121 | outputData[tapdelayInSample + i] += (tapGain * inputData[offsetTimeInSamples + Math.round(i * readSpeed)]);
122 | }
123 |
124 | });
125 |
126 | // normalize output buffer
127 | let maxOutputValue = 0.0;
128 | for (let i = 0; i < outputBuffer.length; i++) {
129 | maxOutputValue = Math.max(Math.abs(outputData[i]), maxOutputValue);
130 | }
131 | let normFactor = Math.max.apply(null, ir.gains) / Math.max(maxOutputValue, 1.0);
132 | // console.log('max:', maxOutputValue, 'norm:', normFactor);
133 |
134 | // replace audio source buffer with created output buffer
135 | src.buffer = outputBuffer;
136 |
137 | // create master gain (shared param, controlled from conductor)
138 | let gain = audioContext.createGain();
139 | gain.gain.value = normFactor * this.params.masterGain;
140 |
141 | // connect graph
142 | src.connect(gain);
143 | gain.connect( this.e.nuOutput.in );
144 |
145 | // play sound if rendez-vous time is in the future (else advance in buffer)
146 | let now = this.e.sync.getSyncTime();
147 | if (syncStartTime > now) {
148 | let audioContextStartTime = audioContext.currentTime + syncStartTime - now;
149 | src.start(audioContextStartTime);
150 | // console.log('play scheduled in:', Math.round((syncStartTime - now) * 1000) / 1000, 'sec', 'at:', syncStartTime);
151 | } else {
152 | console.warn('no sound played, I received the instruction to play too late');
153 | // this.e.renderer.blink([250, 0, 0]);
154 | let inBufferTime = now - syncStartTime;
155 | if( inBufferTime < src.buffer.duration ){
156 | src.start(audioContext.currentTime, inBufferTime);
157 | }
158 | }
159 |
160 | // setup screen color = f(amplitude) callback
161 | this.e.renderer.enable();
162 |
163 | // save source for eventual global reset
164 | this.srcSet.add(src);
165 |
166 | // timeout callback, runs when we finished playing
167 | setTimeout(() => {
168 | this.e.renderer.disable();
169 | // remove source from set
170 | this.srcSet.delete(src);
171 | }, (syncStartTime - now + src.buffer.duration) * 1000);
172 |
173 | }
174 |
175 | // kill audio
176 | reset(){
177 | this.srcSet.forEach( (src) => {
178 | // stop source
179 | src.stop();
180 | // remove associated visual feedback
181 | this.e.renderer.disable();
182 | });
183 | }
184 |
185 | }
--------------------------------------------------------------------------------
/node/src/client/player/NuGroups.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuGroup: Nu module to assign audio tracks to groups of players.
3 | * the term "group" hereafter is to be intepreted as "track" more often than not
4 | **/
5 |
6 | import NuBaseModule from './NuBaseModule'
7 | import * as soundworks from 'soundworks/client';
8 |
9 | const client = soundworks.client;
10 | const audioContext = soundworks.audioContext;
11 |
12 | export default class NuGroups extends NuBaseModule {
13 | constructor(playerExperience) {
14 | super(playerExperience, 'nuGroups');
15 |
16 | // local attributes
17 | this.groupMap = new Map();
18 | this.localGain = audioContext.createGain();
19 | this.localGain.gain.value = 1.0;
20 | this.localGain.connect( this.e.nuOutput.in );
21 |
22 | // binding
23 | this.onOff = this.onOff.bind(this);
24 | this.volume = this.volume.bind(this);
25 | this.localVolume = this.localVolume.bind(this);
26 | this.linkPlayerToGroup = this.linkPlayerToGroup.bind(this);
27 | this.loop = this.loop.bind(this);
28 | this.getGroup = this.getGroup.bind(this);
29 | }
30 |
31 | paramCallback(name, args){
32 | // either route to internal function
33 | if( this[name] !== undefined )
34 | if( args.length == 2 ) this[name](args[0], args[1]);
35 | else this[name](args);
36 | // or to this.params value
37 | else
38 | this.params[name] = args;
39 | }
40 |
41 | onOff(groupId, value) {
42 |
43 | // get group
44 | let group = this.getGroup( groupId );
45 |
46 | // stop group (src)
47 | if( value === 0 ){
48 | // stop source
49 | group.src.stop(0);
50 | // notify renderer we don't need it anymore
51 | this.e.renderer.disable();
52 | }
53 |
54 | // start group (src)
55 | else{
56 | // get time delay since order to start has been given
57 | let timeOffset = this.e.scheduler.syncTime - value;
58 | // modulo buffer length for slow / late connected players
59 | timeOffset %= group.src.buffer.duration;
60 | // make sure timeOffset is positive (if e.g. player not yet perfectly sync.)
61 | timeOffset = Math.max(timeOffset, 0.0);
62 | // start source at group time
63 | group.src.start(audioContext.currentTime, timeOffset);
64 | // remember start time
65 | group.startTime = value;
66 | // enable render
67 | this.e.renderer.enable();
68 | }
69 | }
70 |
71 | // TODO: a player not in a group shouldn't play its sound as happends now with above on/off
72 | // function. Rather, only when both on/off and linked are ok should player start to play.
73 | // this would require a sync. mechanism with groups already started when linked to player.
74 |
75 | // set player to group (track) volume
76 | linkPlayerToGroup(groupId, value){
77 | // get group
78 | let group = this.getGroup( groupId );
79 | // apply value
80 | group.linkGain.gain.value = value;
81 | }
82 |
83 | // set group volume
84 | volume(groupId, value){
85 | // get group
86 | let group = this.getGroup( groupId );
87 | // set group value
88 | group.gain.gain.value = value;
89 | }
90 |
91 | // set player volume (for all its tracks) groupId is dummy here, for uniform inputs wrt other methods
92 | localVolume(groupId, value){
93 | // set local value
94 | this.localGain.gain.value = value;
95 | }
96 |
97 | // stop all currently playing groups
98 | clear(){
99 | // loop over groups
100 | this.groupMap.forEach( (group) => {
101 | // stop source
102 | group.src.stop(0);
103 | // notify renderer we don't need it anymore
104 | this.e.renderer.disable();
105 | });
106 | // reset local map
107 | this.groupMap = new Map();
108 | }
109 |
110 | // set group time
111 | time(groupId, value){
112 | console.log('time function not implemented yet (in NuGroup.js)');
113 | }
114 |
115 | // enable / disable group loop
116 | loop(groupId, value){
117 | // get group
118 | let group = this.getGroup( groupId );
119 | // set group value
120 | group.src.loop = value;
121 | }
122 |
123 | // get group based on id, create if need be
124 | getGroup(groupId) {
125 | // get already existing group
126 | if( this.groupMap.has(groupId) )
127 | return this.groupMap.get(groupId);
128 |
129 | // check if audio buffer associated to group exists
130 | let buffer = this.e.loader.data[groupId];
131 | if (buffer === undefined) {
132 | console.warn('required audio file id', groupId, 'not in client index, actual content:', this.e.loader.options.files, '-> initializing empty audio source..');
133 | buffer = audioContext.createBuffer(1, 22050, 44100);
134 | }
135 |
136 | // create new group
137 | let group = { time: 0, startTime: 0 };
138 |
139 | // create new audio source
140 | group.src = new AudioSourceNode(buffer);
141 |
142 | // create group gain
143 | group.gain = audioContext.createGain();
144 | group.gain.gain.value = 1.0;
145 |
146 | // create group-player link gain
147 | group.linkGain = audioContext.createGain();
148 | group.linkGain.gain.value = 1.0;
149 |
150 | // connect graph
151 | group.src.out.connect(group.gain);
152 | group.gain.connect(group.linkGain);
153 | group.linkGain.connect(this.localGain);
154 |
155 | // store new group in local map
156 | this.groupMap.set(groupId, group);
157 |
158 | // return created group
159 | return group;
160 | }
161 |
162 | // ramp gain node to "targetValue" in fadeTime secs
163 | fadeGainTo(gainNode, targetValue, fadeTime){
164 | // reset eventual planned changes
165 | gainNode.gain.cancelScheduledValues(audioContext.currentTime);
166 | if( fadeTime > 0 ){
167 | // let currentValue = gainNode.gain.value;
168 | gainNode.gain.setValueAtTime(gainNode.gain.value, audioContext.currentTime);
169 | gainNode.gain.linearRampToValueAtTime(targetValue, audioContext.currentTime + fadeTime);
170 | // console.log('fade in / out from', gainNode.gain.value, 'to', targetValue, 'in', fadeTime, 'sec');
171 | }
172 | else{
173 | gainNode.gain.setValueAtTime(targetValue, audioContext.currentTime);
174 | }
175 | }
176 |
177 | }
178 |
179 | // "surcharged" audio source node class
180 | class AudioSourceNode {
181 | constructor(buffer){
182 | // local gain
183 | this.out = audioContext.createGain();
184 | this.out.gain.value = 1.0;
185 | // locals
186 | this.buffer = buffer;
187 | this.src = this.getNewSource();
188 | this._loop = 0;
189 |
190 | }
191 | // start audio source at time, with time offset
192 | start(time = 0, offset = 0){
193 | // stop eventual old source
194 | this.stop(0);
195 | // create new source
196 | this.src = this.getNewSource();
197 | // start source
198 | this.src.start(time, offset);
199 | }
200 |
201 | // stop source (doesn't crash if source already stopped)
202 | stop(time = 0){
203 | try{
204 | this.src.stop(time);
205 | }
206 | catch(e){
207 | if( e.name !== 'InvalidStateError'){ console.error(e); }
208 | }
209 | }
210 |
211 | // set source loop
212 | set loop(value){
213 | this._loop = value;
214 | this.src.loop = value;
215 | }
216 |
217 | // ...
218 | get loop(){
219 | return this._loop;
220 | }
221 |
222 | // create new audio source node
223 | getNewSource(){
224 | // create source
225 | let src = audioContext.createBufferSource();
226 | // fill in buffer
227 | src.buffer = this.buffer;
228 | // set src attributes
229 | src.loop = this._loop;
230 | // connect graph
231 | src.connect(this.out);
232 | return src;
233 | }
234 | }
235 |
236 |
237 |
238 |
239 |
--------------------------------------------------------------------------------
/node/src/server/NuOutput.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuOutput: render output, either directly to audioContext.destination or
3 | * to spatialization engine for debug sessions (i.e. to get a feel of the final
4 | * result while players are emulated on server's laptop)
5 | **/
6 |
7 | import NuBaseModule from './NuBaseModule'
8 | import * as soundworks from 'soundworks/server';
9 |
10 | // read audio init
11 | const fs = require('fs');
12 | var encodeWav = require('audio-encode-wav');
13 | import Audio from 'audio';
14 | var AudioContext = require('web-audio-api').AudioContext
15 | const audioContext = new AudioContext;
16 | const savePath = __dirname + '/../../../';
17 |
18 | export default class NuOutput extends NuBaseModule {
19 | constructor(serverExperience) {
20 | super(serverExperience, 'nuOutput');
21 |
22 | // to be saved parameters to send to client when connects:
23 | this.params = {
24 | gain: 1.0,
25 | enableSpat: false,
26 | ambiOrder: 3,
27 | enableRoom: false,
28 | userPos: [0, 0, 0],
29 | };
30 |
31 | // locals
32 | this.logClientRec = [];
33 | this.storedAudioBuffer = undefined;
34 | this.startRecTime = 0.0;
35 | this.discardUnsync = false;
36 |
37 | // binding
38 | this.rawSocketCallback = this.rawSocketCallback.bind(this);
39 | this.checkIfAllRecordArrived = this.checkIfAllRecordArrived.bind(this);
40 | this.writeAudioToDisk = this.writeAudioToDisk.bind(this);
41 | }
42 |
43 | enterPlayer(client){
44 | // send to new client information regarding current groups parameters
45 | Object.keys(this.params).forEach( (key) => {
46 | this.e.send(client, this.moduleName, [key, this.params[key]] );
47 | });
48 |
49 | // setup socket receive callbacks (receiving raw audio data)
50 | this.e.rawSocket.receive(client, this.moduleName, this.rawSocketCallback );
51 | }
52 |
53 | /**
54 | * start / stop recording: will receive audio data from all clients,
55 | * and concatenate them into output file written to disk
56 | **/
57 | record(args){
58 | let playerId = args.shift();
59 | // playerId not used at the moment, only for uniformity with other modules
60 | let val = args.shift();
61 |
62 | // save recording start time
63 | if( val === 1 ){
64 | this.startRecTime = this.e.sync.getSyncTime();
65 | }
66 |
67 | // forward msg to players
68 | this.e.broadcast( 'player', null, this.moduleName, ['record', val] );
69 | }
70 |
71 | // record callback, called once per client sending audio data at recording's end
72 | rawSocketCallback( interleavedBuffer ) {
73 | // skip empty sockets (at startup)
74 | if( interleavedBuffer.length === 0 ){ return; }
75 |
76 | // create buffer to store audio data if not already created
77 | // WARNING: here defining final size based on first client's packet length
78 | // (not wrong, but will skip data from larger packet if e.g. another client started
79 | // recording a few ms earlier)
80 | let headerLength = 3;
81 | if( this.storedAudioBuffer === undefined ){
82 | this.storedAudioBuffer = new Float32Array( interleavedBuffer.length - headerLength );
83 | }
84 |
85 | // extract header (client index)
86 | let clientId = interleavedBuffer[0];
87 | let startTime = interleavedBuffer[1];
88 | let sampleRate = interleavedBuffer[2];
89 | interleavedBuffer = interleavedBuffer.slice(headerLength, interleavedBuffer.length);
90 |
91 | // get write offset (for sync. write across clients)
92 | let startOffset = Math.floor( (startTime - this.startRecTime) * sampleRate );
93 |
94 | // check if client's clock is not yet correctly sync.
95 | if( startOffset < 0 ){
96 | console.warn('\n### client', clientId, 'data in advance of', this.startRecTime - startTime, 'sec (expect weird sync.)\n');
97 | if( this.discardUnsync ){ startOffset = -1; }
98 | else{ startOffset = 0; }
99 | }
100 |
101 | // proceed to writing audio values to local buffer
102 | if( startOffset >= 0 ){
103 | var index = 0;
104 | for( let i = 0; i < interleavedBuffer.length; i++ ){
105 | index = startOffset + i;
106 | if( index >= this.storedAudioBuffer.length ){ break; }
107 | this.storedAudioBuffer[ index ] += interleavedBuffer[i];
108 | }
109 | }
110 |
111 | // if all client's sent their data..
112 | if( this.checkIfAllRecordArrived( clientId ) ){
113 | // .. write audio to disk
114 | this.writeAudioToDisk( sampleRate );
115 | // clear locals for next rec session
116 | this.logClientRec = [];
117 | this.storedAudioBuffer = undefined;
118 | this.startRecTime = 0.0;
119 | }
120 |
121 | }
122 |
123 | /**
124 | * add clientId to list of client who already sent audio data,
125 | * return true if all clients connected to soundworks server already sent
126 | * their data to trigger record to disk method in raw socket callback
127 | **/
128 | checkIfAllRecordArrived( clientId ){
129 | // check if client already sent recording
130 | if( this.logClientRec.indexOf( clientId ) >= 0 ){
131 | console.error( this.moduleName, 'received twice data from client', clientId );
132 | }
133 |
134 | // flag client as having sent recording
135 | this.logClientRec.push( clientId );
136 |
137 | // check if all clients sent recordings
138 | // TODO: find a way to break out of forEach loop asa one is not in it
139 | let allReceived = true;
140 | this.e.playerMap.forEach( (client, id) => {
141 | if( this.logClientRec.indexOf( id ) < 0 ){
142 | allReceived = false;
143 | }
144 | });
145 | // return flag
146 | return allReceived;
147 | }
148 |
149 | // write audio data to disk
150 | writeAudioToDisk( sampleRate ){
151 | // create new audio container
152 | let audioContainer = new Audio({ length: 2*this.storedAudioBuffer.length, sampleRate: sampleRate });
153 |
154 | // setup fade-in / out of signal
155 | let fadeTime = 0.01; // in sec
156 | let fadeDurInSamp = fadeTime * sampleRate;
157 |
158 | // get max audio data value for normalization
159 | let maxVal = 0.1;
160 | for( let i = 0; i < this.storedAudioBuffer.length; i++ ){
161 | maxVal = Math.max( Math.abs(this.storedAudioBuffer[i]), maxVal);
162 | }
163 |
164 | // fill audio container
165 | var val;
166 | let indexStartFadeOut = (this.storedAudioBuffer.length / 2) - fadeDurInSamp
167 | for( let i = 0; i < this.storedAudioBuffer.length / 2; i++ ){
168 | for( let j = 0; j < 2; j++ ){
169 | // mapping
170 | val = this.storedAudioBuffer[2*i + j] / maxVal; // norm
171 | val = (val + 1.0) / 2.0; // from -1 1 to 0 1
172 | val *= 30000;
173 |
174 | // fade in
175 | if( i < fadeDurInSamp ){ val *= i / (fadeDurInSamp -1 ); }
176 | // fade out
177 | if( i >= indexStartFadeOut ){
178 | val *= 1 - (i - indexStartFadeOut) / ( fadeDurInSamp - 1 );
179 | }
180 |
181 | // write to container
182 | audioContainer.write(val, 2*i+j);
183 | }
184 | }
185 |
186 | // encode audio container to wav
187 | let wav = encodeWav(audioContainer);
188 |
189 | // get output file name / path
190 | let date = new Date();
191 | let fileName = savePath + 'nu-rec-'
192 | + date.getFullYear() + '-'
193 | + date.getMonth() + '-'
194 | + date.getDate() + '-'
195 | + date.getHours() + '-'
196 | + date.getMinutes() + '-'
197 | + date.getSeconds()
198 | + '.wav';
199 |
200 | // write to disk
201 | fs.writeFileSync( fileName, wav);
202 | console.log('saved audio file to disk: \n', fileName);
203 | }
204 |
205 | }
206 |
207 |
--------------------------------------------------------------------------------
/maxmsp/patchers/nu.output.maxpat:
--------------------------------------------------------------------------------
1 | {
2 | "patcher" : {
3 | "fileversion" : 1,
4 | "appversion" : {
5 | "major" : 7,
6 | "minor" : 3,
7 | "revision" : 3,
8 | "architecture" : "x86",
9 | "modernui" : 1
10 | }
11 | ,
12 | "rect" : [ 193.0, 926.0, 392.0, 350.0 ],
13 | "bglocked" : 0,
14 | "openinpresentation" : 0,
15 | "default_fontsize" : 12.0,
16 | "default_fontface" : 0,
17 | "default_fontname" : "Arial",
18 | "gridonopen" : 1,
19 | "gridsize" : [ 15.0, 15.0 ],
20 | "gridsnaponopen" : 1,
21 | "objectsnaponopen" : 1,
22 | "statusbarvisible" : 2,
23 | "toolbarvisible" : 1,
24 | "lefttoolbarpinned" : 0,
25 | "toptoolbarpinned" : 0,
26 | "righttoolbarpinned" : 0,
27 | "bottomtoolbarpinned" : 0,
28 | "toolbars_unpinned_last_save" : 0,
29 | "tallnewobj" : 0,
30 | "boxanimatetime" : 200,
31 | "enablehscroll" : 1,
32 | "enablevscroll" : 1,
33 | "devicewidth" : 0.0,
34 | "description" : "",
35 | "digest" : "",
36 | "tags" : "",
37 | "style" : "",
38 | "subpatcher_template" : "",
39 | "boxes" : [ {
40 | "box" : {
41 | "id" : "obj-3",
42 | "maxclass" : "newobj",
43 | "numinlets" : 1,
44 | "numoutlets" : 1,
45 | "outlettype" : [ "" ],
46 | "patching_rect" : [ 15.0, 260.5, 69.0, 22.0 ],
47 | "style" : "",
48 | "text" : "prepend -1"
49 | }
50 |
51 | }
52 | , {
53 | "box" : {
54 | "id" : "obj-9",
55 | "maxclass" : "newobj",
56 | "numinlets" : 1,
57 | "numoutlets" : 1,
58 | "outlettype" : [ "" ],
59 | "patching_rect" : [ 285.0, 255.5, 81.0, 22.0 ],
60 | "style" : "",
61 | "text" : "prepend gain"
62 | }
63 |
64 | }
65 | , {
66 | "box" : {
67 | "id" : "obj-8",
68 | "maxclass" : "newobj",
69 | "numinlets" : 1,
70 | "numoutlets" : 1,
71 | "outlettype" : [ "" ],
72 | "patching_rect" : [ 231.0, 221.5, 102.0, 22.0 ],
73 | "style" : "",
74 | "text" : "prepend userPos"
75 | }
76 |
77 | }
78 | , {
79 | "box" : {
80 | "id" : "obj-7",
81 | "maxclass" : "newobj",
82 | "numinlets" : 1,
83 | "numoutlets" : 1,
84 | "outlettype" : [ "" ],
85 | "patching_rect" : [ 177.0, 187.5, 92.0, 22.0 ],
86 | "style" : "",
87 | "text" : "prepend record"
88 | }
89 |
90 | }
91 | , {
92 | "box" : {
93 | "id" : "obj-6",
94 | "maxclass" : "newobj",
95 | "numinlets" : 1,
96 | "numoutlets" : 1,
97 | "outlettype" : [ "" ],
98 | "patching_rect" : [ 123.0, 153.5, 115.0, 22.0 ],
99 | "style" : "",
100 | "text" : "prepend ambiOrder"
101 | }
102 |
103 | }
104 | , {
105 | "box" : {
106 | "id" : "obj-2",
107 | "maxclass" : "newobj",
108 | "numinlets" : 1,
109 | "numoutlets" : 1,
110 | "outlettype" : [ "" ],
111 | "patching_rect" : [ 69.0, 119.5, 126.0, 22.0 ],
112 | "style" : "",
113 | "text" : "prepend enableRoom"
114 | }
115 |
116 | }
117 | , {
118 | "box" : {
119 | "id" : "obj-158",
120 | "maxclass" : "newobj",
121 | "numinlets" : 1,
122 | "numoutlets" : 0,
123 | "patching_rect" : [ 15.0, 320.0, 65.0, 22.0 ],
124 | "style" : "",
125 | "text" : "s toServer"
126 | }
127 |
128 | }
129 | , {
130 | "box" : {
131 | "id" : "obj-56",
132 | "maxclass" : "newobj",
133 | "numinlets" : 1,
134 | "numoutlets" : 1,
135 | "outlettype" : [ "" ],
136 | "patching_rect" : [ 15.0, 290.0, 111.0, 22.0 ],
137 | "style" : "",
138 | "text" : "prepend /nuOutput"
139 | }
140 |
141 | }
142 | , {
143 | "box" : {
144 | "id" : "obj-30",
145 | "maxclass" : "newobj",
146 | "numinlets" : 1,
147 | "numoutlets" : 1,
148 | "outlettype" : [ "" ],
149 | "patching_rect" : [ 15.0, 85.5, 119.0, 22.0 ],
150 | "style" : "",
151 | "text" : "prepend enableSpat"
152 | }
153 |
154 | }
155 | , {
156 | "box" : {
157 | "id" : "obj-24",
158 | "maxclass" : "newobj",
159 | "numinlets" : 1,
160 | "numoutlets" : 0,
161 | "patching_rect" : [ 306.0, 24.0, 53.0, 22.0 ],
162 | "style" : "",
163 | "text" : "nu.error"
164 | }
165 |
166 | }
167 | , {
168 | "box" : {
169 | "id" : "obj-23",
170 | "maxclass" : "newobj",
171 | "numinlets" : 7,
172 | "numoutlets" : 7,
173 | "outlettype" : [ "", "", "", "", "", "", "" ],
174 | "patching_rect" : [ 15.0, 53.0, 344.0, 22.0 ],
175 | "style" : "",
176 | "text" : "route enableSpat enableRoom ambiOrder record userPos gain"
177 | }
178 |
179 | }
180 | , {
181 | "box" : {
182 | "comment" : "",
183 | "id" : "obj-1",
184 | "index" : 0,
185 | "maxclass" : "inlet",
186 | "numinlets" : 0,
187 | "numoutlets" : 1,
188 | "outlettype" : [ "" ],
189 | "patching_rect" : [ 15.0, 9.0, 30.0, 30.0 ],
190 | "style" : ""
191 | }
192 |
193 | }
194 | ],
195 | "lines" : [ {
196 | "patchline" : {
197 | "destination" : [ "obj-23", 0 ],
198 | "disabled" : 0,
199 | "hidden" : 0,
200 | "source" : [ "obj-1", 0 ]
201 | }
202 |
203 | }
204 | , {
205 | "patchline" : {
206 | "destination" : [ "obj-3", 0 ],
207 | "disabled" : 0,
208 | "hidden" : 0,
209 | "source" : [ "obj-2", 0 ]
210 | }
211 |
212 | }
213 | , {
214 | "patchline" : {
215 | "destination" : [ "obj-2", 0 ],
216 | "disabled" : 0,
217 | "hidden" : 0,
218 | "source" : [ "obj-23", 1 ]
219 | }
220 |
221 | }
222 | , {
223 | "patchline" : {
224 | "destination" : [ "obj-24", 0 ],
225 | "disabled" : 0,
226 | "hidden" : 0,
227 | "source" : [ "obj-23", 6 ]
228 | }
229 |
230 | }
231 | , {
232 | "patchline" : {
233 | "destination" : [ "obj-30", 0 ],
234 | "disabled" : 0,
235 | "hidden" : 0,
236 | "source" : [ "obj-23", 0 ]
237 | }
238 |
239 | }
240 | , {
241 | "patchline" : {
242 | "destination" : [ "obj-6", 0 ],
243 | "disabled" : 0,
244 | "hidden" : 0,
245 | "source" : [ "obj-23", 2 ]
246 | }
247 |
248 | }
249 | , {
250 | "patchline" : {
251 | "destination" : [ "obj-7", 0 ],
252 | "disabled" : 0,
253 | "hidden" : 0,
254 | "source" : [ "obj-23", 3 ]
255 | }
256 |
257 | }
258 | , {
259 | "patchline" : {
260 | "destination" : [ "obj-8", 0 ],
261 | "disabled" : 0,
262 | "hidden" : 0,
263 | "source" : [ "obj-23", 4 ]
264 | }
265 |
266 | }
267 | , {
268 | "patchline" : {
269 | "destination" : [ "obj-9", 0 ],
270 | "disabled" : 0,
271 | "hidden" : 0,
272 | "source" : [ "obj-23", 5 ]
273 | }
274 |
275 | }
276 | , {
277 | "patchline" : {
278 | "destination" : [ "obj-56", 0 ],
279 | "disabled" : 0,
280 | "hidden" : 0,
281 | "source" : [ "obj-3", 0 ]
282 | }
283 |
284 | }
285 | , {
286 | "patchline" : {
287 | "destination" : [ "obj-3", 0 ],
288 | "disabled" : 0,
289 | "hidden" : 0,
290 | "source" : [ "obj-30", 0 ]
291 | }
292 |
293 | }
294 | , {
295 | "patchline" : {
296 | "destination" : [ "obj-158", 0 ],
297 | "disabled" : 0,
298 | "hidden" : 0,
299 | "source" : [ "obj-56", 0 ]
300 | }
301 |
302 | }
303 | , {
304 | "patchline" : {
305 | "destination" : [ "obj-3", 0 ],
306 | "disabled" : 0,
307 | "hidden" : 0,
308 | "source" : [ "obj-6", 0 ]
309 | }
310 |
311 | }
312 | , {
313 | "patchline" : {
314 | "destination" : [ "obj-3", 0 ],
315 | "disabled" : 0,
316 | "hidden" : 0,
317 | "source" : [ "obj-7", 0 ]
318 | }
319 |
320 | }
321 | , {
322 | "patchline" : {
323 | "destination" : [ "obj-3", 0 ],
324 | "disabled" : 0,
325 | "hidden" : 0,
326 | "source" : [ "obj-8", 0 ]
327 | }
328 |
329 | }
330 | , {
331 | "patchline" : {
332 | "destination" : [ "obj-3", 0 ],
333 | "disabled" : 0,
334 | "hidden" : 0,
335 | "source" : [ "obj-9", 0 ]
336 | }
337 |
338 | }
339 | ],
340 | "dependency_cache" : [ {
341 | "name" : "nu.error.maxpat",
342 | "bootpath" : "~/Projects/Cosima/devs/Nu/soundworks-nu/maxmsp/extras",
343 | "type" : "JSON",
344 | "implicit" : 1
345 | }
346 | ],
347 | "autosave" : 0
348 | }
349 |
350 | }
351 |
--------------------------------------------------------------------------------
/node/src/client/player/NuLoop.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuLoop: Nu module sequencer-like (drum machine)
3 | **/
4 |
5 | import NuBaseModule from './NuBaseModule'
6 | import * as soundworks from 'soundworks/client';
7 | import audioFiles from '../shared/audioFiles';
8 |
9 | const client = soundworks.client;
10 | const audioContext = soundworks.audioContext;
11 |
12 | // previous impl. was based on audio buffer calls view integer.
13 | // adapt: from name to integar to avoid changing whole code here.
14 | const audioFileNameToId = new Map();
15 | const audioFileIdToName = new Map();
16 | var count = 0;
17 | for (var key in audioFiles) {
18 | if (audioFiles.hasOwnProperty(key)) {
19 | audioFileNameToId.set(key, count);
20 | audioFileIdToName.set(count, key);
21 | count += 1;
22 | }
23 | }
24 |
25 | export default class NuLoop extends NuBaseModule {
26 | constructor(playerExperience) {
27 | super(playerExperience, 'nuLoop');
28 |
29 | // local attributes
30 | this.params = {};
31 | let audioBuffers = this.e.loader.data;
32 | this.synth = new SampleSynth(audioBuffers, this.e.nuOutput.in);
33 | this.loops = new Matrix(audioBuffers.length, this.params.divisions);
34 |
35 | // binding
36 | this.updateNumDivisions = this.updateNumDivisions.bind(this);
37 | this.setTrackSlot = this.setTrackSlot.bind(this);
38 | this.start = this.start.bind(this);
39 | this.remove = this.remove.bind(this);
40 | this.reset = this.reset.bind(this);
41 | this.updateNumDivisions = this.updateNumDivisions.bind(this);
42 | this.getSlotTime = this.getSlotTime.bind(this);
43 | this.divisions = this.divisions.bind(this);
44 | }
45 |
46 | // set number of divisions in the loop
47 | divisions(value){
48 | this.params.divisions = value;
49 | this.updateNumDivisions();
50 | }
51 |
52 | // update loop maps size
53 | updateNumDivisions(){
54 | // shut down all loops
55 | this.reset();
56 | // resize loop map
57 | let numTracks = audioFileNameToId.size;
58 | this.loops = new Matrix(numTracks, this.params.divisions);
59 | }
60 |
61 | // set loop period
62 | period(value){
63 | this.params.period = Math.round(value * 10) / 10;
64 | }
65 |
66 | // set general output volume
67 | masterGain(value){
68 | this.synth.output.gain.value = value;
69 | }
70 |
71 | // enable / disable a slot in the loop (a "beat")
72 | setTrackSlot(args){
73 |
74 | // extract parameters from args array
75 | let trackName = args.shift();
76 | let slotId = args.shift();
77 | let onOff = args.shift();
78 |
79 | // check valid trackName / trackId
80 | let trackId = audioFileNameToId.get(trackName);
81 | if( trackId === undefined) {
82 | console.warn('required track ', trackName, 'not available, actual content:', this.e.loader.options.files);
83 | return;
84 | }
85 | // check valid slotId
86 | if( slotId >= this.params.divisions || slotId < 0) {
87 | console.warn('required slot id', slotId, 'is not available in current setup (should be in [ 0,', this.params.divisions - 1, ']');
88 | return;
89 | }
90 |
91 | // add event (sound) in loop
92 | if( onOff ){
93 | // discard start already started source
94 | if( this.loops.mat[trackId][slotId] !== undefined ) { return; }
95 | // start new loop event
96 | let slotTime = this.getSlotTime(this.e.scheduler.syncTime, slotId);
97 | this.start(slotTime, {trackId: trackId, slotId: slotId}, true);
98 | // enable visual feedback
99 | this.e.renderer.enable();
100 | }
101 |
102 | // remove event from loop
103 | else{
104 | // this.looper.stop(time, soundParams, true);
105 | this.remove(trackId, slotId);
106 | // disable visual feedback
107 | this.e.renderer.disable();
108 | }
109 |
110 | }
111 |
112 | // compute event time in loop
113 | getSlotTime(currentTime, slotId){
114 | let currentTimeInMeasure = currentTime % this.params.period;
115 | let measureStartTime = currentTime - currentTimeInMeasure;
116 | let slotTime = slotId * ( this.params.period / this.params.divisions );
117 | // console.log('current time', currentTime, 'measure start time', measureStartTime, 'slot time', measureStartTime + slotTime, 'slot id', slotId, this.params);
118 | return measureStartTime + slotTime;
119 | }
120 |
121 | // start new loop
122 | start(time, soundParams) {
123 | // create new loop
124 | const loop = new Loop(this, soundParams);
125 | // add loop to set
126 | this.loops.mat[soundParams.trackId][soundParams.slotId] = loop;
127 | // add loop to scheduler
128 | this.e.scheduler.add(loop, time);
129 | }
130 |
131 | // callback: called at each loop (in scheduler)
132 | advanceLoop(time, loop) {
133 | const soundParams = loop.soundParams;
134 | const params = this.params;
135 | // trigger sound
136 | const duration = this.synth.trigger(this.e.scheduler.audioTime, soundParams);
137 | // add jitter (randomness to beat exact time)
138 | let jitter = this.params.jitter * // jitter gain in [0:1]
139 | Math.random() * // random value in [0:1[
140 | ( this.params.period / this.params.divisions); // normalization (jitter never goes beyond other time slots)
141 | // get absolute time for current loop if not required to keep track of old jitter injected previously
142 | // (otherwise, keep jittered time offset for current loop)
143 | if( !this.params.jitterMemory ){
144 | time = this.getSlotTime(time, loop.soundParams.slotId);
145 | }
146 | // console.log('time', time, 'period', this.params.period, 'jitter', jitter, 'next time:', time + this.params.period + jitter);
147 | // return next time (it's how the advanceLoop works)
148 | return time + this.params.period + jitter;
149 | }
150 |
151 | // remove loop by index (both track index and time slot index)
152 | remove(trackId, slotId) {
153 | // get corresponding loop
154 | let loop = this.loops.mat[trackId][slotId];
155 | // check if loop is defined
156 | if( loop === undefined ) { return; }
157 | // remove loop from scheduler
158 | this.e.scheduler.remove(loop);
159 | // delete loop from set
160 | this.loops.mat[trackId][slotId] = undefined;
161 | }
162 |
163 | // remove all loops from scheduler
164 | reset() {
165 | let loop;
166 | for( let i = 0; i < this.loops.i; i++ ){
167 | for( let j = 0; j < this.loops.j; j++ ){
168 | loop = this.loops.mat[i][j];
169 | if( loop !== undefined ){
170 | // remove loop
171 | this.e.scheduler.remove(loop);
172 | // disable renderer
173 | this.e.renderer.disable();
174 | }
175 | }
176 | }
177 | this.loops.clear(); // clear set
178 | }
179 |
180 | }
181 |
182 | // loop corresponding to a single audio sample
183 | class Loop extends soundworks.audio.AudioTimeEngine {
184 | constructor(looper, soundParams) {
185 | super();
186 | this.looper = looper;
187 | this.soundParams = soundParams; // drop parameters
188 | }
189 |
190 | advanceTime(time) {
191 | return this.looper.advanceLoop(time, this); // just call daddy
192 | }
193 | }
194 |
195 | // rough i.j matrix like array class
196 | class Matrix{
197 | constructor(i, j){
198 | this.i = i;
199 | this.j = j;
200 | this.mat = [];
201 | this.init();
202 | }
203 |
204 | clear(){
205 | this.init();
206 | }
207 |
208 | init(){
209 | for (let ii = 0; ii < this.i; ii++) {
210 | this.mat[ii] = [];
211 | for (let jj = 0; jj < this.j; jj++) {
212 | this.mat[ii][jj] = undefined;
213 | }
214 | }
215 | }
216 |
217 | }
218 |
219 | // in charge of playing the final sample
220 | class SampleSynth {
221 | constructor(audioBuffers, output) {
222 | this.audioBuffers = audioBuffers;
223 | this.output = audioContext.createGain();
224 | this.output.connect( output );
225 | this.output.gain.value = 1;
226 | }
227 |
228 | trigger(time, params) {
229 | let duration = 0;
230 |
231 | let trackName = audioFileIdToName.get(params.trackId);
232 | if (this.audioBuffers[trackName] === undefined) { return duration; }
233 |
234 | const b1 = this.audioBuffers[trackName];
235 |
236 | duration += b1.duration;
237 |
238 | const s1 = audioContext.createBufferSource();
239 | s1.buffer = b1;
240 | s1.connect(this.output);
241 | s1.start(time);
242 |
243 | return duration;
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/node/src/client/player/NuSynth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuSynth: distributed synthetizer, sending "note" information via OSC
3 | * to trigger real notes in local synthetizer
4 | **/
5 |
6 | import NuBaseModule from './NuBaseModule'
7 | import * as soundworks from 'soundworks/client';
8 |
9 | const client = soundworks.client;
10 | const audioContext = soundworks.audioContext;
11 |
12 | export default class NuSynth extends NuBaseModule {
13 | constructor(playerExperience) {
14 | super(playerExperience, 'nuSynth');
15 | // create synth
16 | this.audioSynth = new AudioSynth(this.e);
17 | // create master gain
18 | this.localGain = audioContext.createGain();
19 | // connect graph
20 | this.audioSynth.out.connect(this.localGain);
21 | this.localGain.connect(this.e.nuOutput.in);
22 | // binding
23 | this.noteOnOff = this.noteOnOff.bind(this);
24 | this.volume = this.volume.bind(this);
25 | }
26 |
27 | // set note on / off (args = [noteId, onOff status])
28 | noteOnOff(args){
29 | let noteId = args.shift();
30 | let status = args.shift();
31 | this.audioSynth.playNote(noteId, status);
32 | }
33 |
34 | // stop all playing notes
35 | clear(){
36 | this.audioSynth.noteMap.forEach( (note, noteId) => {
37 | this.audioSynth.playNote(noteId, 0);
38 | });
39 | }
40 |
41 | // set local volume
42 | volume(value){
43 | this.localGain.gain.value = value;
44 | }
45 |
46 | /**
47 | * set noteId volume, "linking" the note to the player
48 | * (e.g. to attribute a unique note to each player)
49 | **/
50 | linkPlayerToNote(args){
51 | let noteId = args.shift();
52 | let volume = args.shift();
53 | this.audioSynth.setNoteVolume(noteId, volume);
54 | }
55 |
56 | // define local synthetizer used for audio rendering
57 | synthType(type){
58 | this.audioSynth.setType(type);
59 | }
60 |
61 | // define synth. attack time
62 | attackTime(value){
63 | this.audioSynth.attackTime = value;
64 | }
65 |
66 | // define synth. release time
67 | releaseTime(value){
68 | this.audioSynth.releaseTime = value;
69 | }
70 |
71 | // define synth waveform
72 | periodicWave(args){
73 | let halfLength = Math.floor(args.length/2);
74 | var real = new Float32Array(halfLength);
75 | var imag = new Float32Array(halfLength);
76 | for (let i = 0; i < args.length/2; i++) {
77 | real[i] = args[2*i];
78 | imag[i] = args[2*i+1];
79 | }
80 | this.audioSynth.setPeriodicWave(audioContext.createPeriodicWave(real, imag));
81 | }
82 |
83 | }
84 |
85 | // a (really) basic audio synthetizer, based on the WebAudio Oscillator node
86 | class AudioSynth {
87 | constructor(soundworksClient){
88 | this.e = soundworksClient;
89 |
90 | // local attributes
91 | this.noteMap = new Map();
92 | this.type = 'square';
93 | this.isPlaying = false;
94 | this.numNotesPlayed = 0;
95 | this.attackTime = 0.1; // in sec
96 | this.releaseTime = 0.1; // in sec
97 | // create custom periodic wave
98 | var real = new Float32Array(2);
99 | var imag = new Float32Array(2);
100 | real[0] = 0; imag[0] = 0; real[1] = 1; imag[1] = 0;
101 | this.periodicWave = audioContext.createPeriodicWave(real, imag);
102 | // output gain
103 | this.out = audioContext.createGain();
104 | this.out.gain.value = 1.0;
105 | // notes ferquencies
106 | let noteFreqTable = [
107 | 130.813,
108 | 138.591,
109 | 146.832,
110 | 155.563,
111 | 164.814,
112 | 174.614,
113 | 184.997,
114 | 195.998,
115 | 207.652,
116 | 220,
117 | 233.082,
118 | 246.942,
119 | ];
120 |
121 | // add octaves (not just? true)
122 | let numOctaves = 4;
123 | const initTableLength = noteFreqTable.length;
124 | for( let i = 1; i < numOctaves; i++ ){
125 | for( let j = 0; j < initTableLength; j++ ){
126 | noteFreqTable.push( noteFreqTable[j] * Math.pow(2, i) );
127 | }
128 | }
129 |
130 | // create notes
131 | for (let i = 0; i < noteFreqTable.length; i++) {
132 | // create note gain (note volume)
133 | let gain = audioContext.createGain();
134 | gain.gain.value = 1.0;
135 | // create note envelope gain
136 | let envelopeGain = audioContext.createGain();
137 | envelopeGain.gain.value = 0.0;
138 | envelopeGain.connect(this.out);
139 | // connect graph
140 | gain.connect(envelopeGain);
141 | envelopeGain.connect(this.out);
142 | // store note
143 | this.noteMap.set(i, {
144 | freq: noteFreqTable[i],
145 | gain: gain,
146 | envelopeGain: envelopeGain,
147 | osc: undefined,
148 | timeout: undefined,
149 | isPlaying: false,
150 | });
151 | }
152 | }
153 |
154 | // set master volume
155 | setVolume(value){
156 | this.out.gain.value = value;
157 | }
158 |
159 | // set note specific volume
160 | setNoteVolume(noteId, value){
161 | // get note
162 | let note = this.noteMap.get(noteId);
163 | // discard if note does't exist
164 | if( note === undefined ){ return; }
165 | // set note gain
166 | note.gain.gain.value = value;
167 | }
168 |
169 | // synth type (waveform)
170 | setType(value){
171 | this.type = value;
172 | }
173 |
174 | // define synth. waveform
175 | setPeriodicWave(wave){
176 | this.periodicWave = wave;
177 | }
178 |
179 | // play a note on the synth.
180 | playNote(noteId, status){
181 | // get note based on id
182 | let note = this.noteMap.get(noteId);
183 | // discard if note undefined
184 | if( note === undefined ){
185 | this.e.renderer.blink([200, 0, 0]);
186 | console.warn('note', noteId, 'not defined');
187 | return;
188 | }
189 | // note ON
190 | if( status ){
191 | // discard if note already playing
192 | if(note.isPlaying){ return; }
193 | // create osc.
194 | let osc = audioContext.createOscillator();
195 | // setup osc
196 | if( this.type === 'custom' )
197 | osc.setPeriodicWave(this.periodicWave);
198 | else
199 | osc.type = this.type;
200 | osc.frequency.value = note.freq; // value in hertz
201 | // connect graph
202 | osc.connect(note.gain);
203 | // handle envelope
204 | let now = audioContext.currentTime;
205 | note.envelopeGain.gain.cancelScheduledValues(now);
206 | note.envelopeGain.gain.setValueAtTime(note.envelopeGain.gain.value, now);
207 | note.envelopeGain.gain.linearRampToValueAtTime(1, now + this.attackTime);
208 | // note.envelopeGain.gain.setTargetAtTime(1, now, 1/this.attackTime);
209 | // add to note
210 | note.osc = osc;
211 | // cancell eventual previous timeout (to avoid stoopping new note in old timeout)
212 | if( note.timeout !== undefined ){
213 | clearTimeout(note.timeout);
214 | note.timeout = undefined;
215 | }
216 | else{
217 | // update counter (for render)
218 | this.numNotesPlayed += 1;
219 | }
220 | // start
221 | osc.start();
222 | note.isPlaying = true;
223 | }
224 | // note OFF
225 | else{
226 | // discard if note never set to on
227 | if(note.osc === undefined){ return; }
228 | // handle enveloppe
229 | let now = audioContext.currentTime;
230 | note.envelopeGain.gain.cancelScheduledValues(now);
231 | note.envelopeGain.gain.setValueAtTime(note.envelopeGain.gain.value, now);
232 | note.envelopeGain.gain.linearRampToValueAtTime(0, now + this.releaseTime);
233 | // note.envelopeGain.gain.setTargetAtTime(0, now, 1/this.attackTime);
234 | // stop oscillator
235 | note.osc.stop(now + this.releaseTime);
236 | // schedule oscillator kill
237 | note.timeout = setTimeout(() => {
238 | // decrement renderer counter
239 | this.numNotesPlayed -= 1;
240 | this.updateRendererStatus();
241 | // kill osc if re-started since (discard if osc already killed)
242 | if(note.osc === undefined){ return; }
243 | try{ // weird Safari behavior...
244 | note.osc.stop();
245 | }
246 | catch(e){
247 | if( e.name !== 'InvalidStateError'){ console.error(e); }
248 | }
249 | note.isPlaying = false
250 | note.osc = undefined;
251 | // delete timeout reference
252 | note.timeout = undefined;
253 | }, this.releaseTime*1000);
254 | }
255 |
256 | this.updateRendererStatus();
257 | }
258 |
259 | // enable / disable visualization (enable as long as at least one note plays)
260 | updateRendererStatus(){
261 | if( this.numNotesPlayed === 1 && !this.isPlaying){
262 | this.e.renderer.enable();
263 | this.isPlaying = true;
264 | }
265 | else if( this.numNotesPlayed === 0 ){
266 | this.e.renderer.disable();
267 | this.isPlaying = false;
268 | }
269 | }
270 |
271 | }
--------------------------------------------------------------------------------
/node/src/client/player/NuDisplay.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NuDisplay: nu module in charge of visual feedback
3 | **/
4 |
5 | import * as soundworks from 'soundworks/client';
6 | const client = soundworks.client;
7 | const audioContext = soundworks.audioContext;
8 |
9 | export default class NuDisplay extends soundworks.Canvas2dRenderer {
10 | constructor(playerExperience) {
11 | super(1/24); // update rate = 0: synchronize updates to frame rate
12 | this.moduleName = 'nuDisplay';
13 |
14 | // local attributes
15 | this.e = playerExperience;
16 | this.params = {
17 | 'feedbackGain': 1.0,
18 | 'enableFeedback': true
19 | };
20 |
21 | this.colors = {
22 | 'rest': [0,0,0],
23 | 'active': [255, 255, 255],
24 | 'current': [0,0,0]
25 | };
26 |
27 | this.audioAnalyser = new AudioAnalyser();
28 | this.bkgChangeColor = false;
29 | this.numOfElmtInNeedOfMe = 0;
30 |
31 | // this.bkgColorArray = [0,0,0];
32 | this.blinkStatus = { isBlinking: false, savedBkgColor: [0,0,0] };
33 |
34 | // binding
35 | this.analyserCallback = this.analyserCallback.bind(this);
36 |
37 | // setup receive callbacks
38 | this.e.receive(this.moduleName, (args) => {
39 | // get header
40 | let name = args.shift();
41 | // convert singleton array if need be
42 | args = (args.length == 1) ? args[0] : args;
43 | if( this.params[name] !== undefined )
44 | this.params[name] = args; // parameter set
45 | else
46 | this[name](args); // function call
47 | });
48 |
49 | // notify module is ready to receive msg
50 | this.e.send('moduleReady', this.moduleName);
51 |
52 | // ATTEMPT AT CROSSMODULE POSTING: FUNCTIONAL BUT ORIGINAL USE NO LONGER CONSIDERED: TODELETE WHEN CONFIRMED
53 | // setup internal callback
54 | // console.log('setup event listener')
55 | // window.addEventListener("message", (event) => {
56 | // console.log('received msg', event);
57 | // if( event.origin !== location.origin || event.data[0] !== 'nuDisplay' )
58 | // return;
59 | // console.log(event.data[4]);
60 | // this.restColor([255*event.data[4], 0, 0]);
61 | // }, false);
62 | // ----------
63 |
64 | }
65 |
66 | // define rest color: the screen color when no sound is playing
67 | restColor(rgb){
68 | this.colors.rest = rgb;
69 | // update background only if analyser not active
70 | if( this.numOfElmtInNeedOfMe == 0 ){
71 | this.setCurrentColorAmpl(0);
72 | this.overrideForceRender = true;
73 | }
74 | }
75 |
76 | // define active color: the screen color when sound is playing
77 | activeColor(rgb){
78 | this.colors.active = rgb;
79 | }
80 |
81 | /**
82 | * Initialize rederer state.
83 | * @param {Number} dt - time since last update in seconds.
84 | */
85 | init() {}
86 |
87 | /**
88 | * Update rederer state.
89 | * @param {Number} dt - time since last update in seconds.
90 | */
91 | update(dt) {}
92 |
93 | /**
94 | * Draw into canvas.
95 | * Method is called by animation frame loop in current frame rate.
96 | * @param {CanvasRenderingContext2D} ctx - canvas 2D rendering context
97 | */
98 | render(ctx) {
99 | if ( this.bkgChangeColor && this.params.enableFeedback || this.overrideForceRender ) {
100 | // console.log(this.bkgColor);
101 | // ctx.save();
102 | ctx.globalAlpha = 1;
103 | ctx.fillStyle = 'rgb('
104 | + Math.round(this.colors.current[0]) + ','
105 | + Math.round(this.colors.current[1]) + ','
106 | + Math.round(this.colors.current[2]) + ')';
107 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
108 | this.overrideForceRender = false;
109 | this.bkgChangeColor = false
110 | }
111 | }
112 |
113 | // enable display, i.e. add +1 to its stack of "I need you display" clients
114 | enable(){
115 | this.numOfElmtInNeedOfMe += 1;
116 | // if need to be triggered on for the first time:
117 | if( this.numOfElmtInNeedOfMe == 1 ){
118 | requestAnimationFrame(this.analyserCallback);
119 | }
120 | }
121 |
122 | // disable display, i.e. remove 1 from its stack of "I need you display" clients
123 | disable(){
124 | // decrement status
125 | this.numOfElmtInNeedOfMe = Math.max(this.numOfElmtInNeedOfMe-1, 0);
126 | // reset background color
127 | if( this.numOfElmtInNeedOfMe == 0 ){
128 | this.bkgChangeColor = true;
129 | }
130 | }
131 |
132 | /*
133 | * Change GUI background color based on current amplitude of sound being played
134 | */
135 | analyserCallback() {
136 | if( this.numOfElmtInNeedOfMe >= 1 || !this.blinkStatus.isBlinking ) {
137 | // call me once, I'll call myself over and over
138 | requestAnimationFrame(this.analyserCallback);
139 | // change background color based on current amplitude
140 | let amp = this.audioAnalyser.getAmplitude();
141 | amp *= this.params.feedbackGain;
142 | this.setCurrentColorAmpl(amp);
143 | // notify to change color at next animation frame
144 | this.bkgChangeColor = true;
145 | }
146 | }
147 |
148 | // amplitude to color converter
149 | setCurrentColorAmpl(amp){
150 | for (let i = 0; i < this.colors.current.length; i++) {
151 | this.colors.current[i] = this.colors.rest[i] +
152 | amp * ( this.colors.active[i] - this.colors.rest[i] );
153 | }
154 | }
155 |
156 | // change screen color to 'color' for 'time' duration (in sec)
157 | blink(color, time = 0.4){
158 | // discard if already blinking
159 | if( this.blinkStatus.isBlinking ){ return; }
160 | this.blinkStatus.isBlinking = true;
161 | // save current background color
162 | for (let i = 0; i < 3; i++)
163 | this.blinkStatus.savedBkgColor[i] = this.colors.current[i];
164 | // change bkg color
165 | this.colors.current = color;
166 | this.bkgChangeColor = true;
167 | setTimeout(() => {
168 | for (let i = 0; i < 3; i++)
169 | this.colors.current[i] = this.blinkStatus.savedBkgColor[i];
170 | this.blinkStatus.isBlinking = false
171 | this.bkgChangeColor = true;
172 | }, time * 1000);
173 | }
174 |
175 | // defined text (on top of the player's screen) from OSC client (header)
176 | text1(args){
177 | let str = this.formatText(args);
178 | document.getElementById('text1').innerHTML = str;
179 | }
180 |
181 | // defined text (on middle of the player's screen) from OSC client (instructions)
182 | text2(args){
183 | let str = this.formatText(args);
184 | document.getElementById('text2').innerHTML = str;
185 | }
186 |
187 | // defined text (on bottom of the player's screen) from OSC client (sub-instructions)
188 | text3(args){
189 | let str = this.formatText(args);
190 | document.getElementById('text3').innerHTML = str;
191 | }
192 |
193 | // convert array of elements to string
194 | formatText(args){
195 | let str = '';
196 | // simple string
197 | if( typeof args === 'string' ){ str = args; }
198 | // array of strings
199 | else{ args.forEach( (elmt) => { str += ' ' + elmt; }); }
200 | // replace "cliendId" with actual client index and other conventional naming
201 | str = str.replace("clientId", client.index);
202 | str = str.replace("None", '');
203 | // return formatted string
204 | return str;
205 | }
206 |
207 | // set analyzer min audio dB range (clip)
208 | dBmin(value){
209 | if( value > -100 && value < 0 && value < this.audioAnalyser.in.maxDecibels )
210 | this.audioAnalyser.in.minDecibels = value;
211 | }
212 |
213 | // set analyzer max audio dB range (clip)
214 | dBmax(value){
215 | if( value > -100 && value < 0 && value < this.audioAnalyser.in.minDecibels )
216 | this.audioAnalyser.in.maxDecibels = value;
217 | }
218 |
219 | // set visualizer smoothing time constant (to avoid epileptic prone behaviors from player's devices)
220 | smoothingTimeConstant(value){
221 | if( value >= 0 && value <= 1 )
222 | this.audioAnalyser.in.smoothingTimeConstant = value;
223 | }
224 |
225 | // set min frequency considered by the analyzer
226 | freqMin(value){
227 | if( value > 0 && value < this.audioAnalyser.maxFreq ){
228 | this.audioAnalyser.minFreq = value;
229 | this.audioAnalyser.updateBinNorm();
230 | }
231 | }
232 |
233 | // set max frequency considered by the analyzer
234 | freqMax(value){
235 | if( value < 20000 && value > this.audioAnalyser.minFreq ){
236 | this.audioAnalyser.maxFreq = value;
237 | this.audioAnalyser.updateBinNorm();
238 | }
239 | }
240 |
241 | }
242 |
243 | /**
244 | * Audio analyzer for visual feedback of sound amplitude on screen
245 | */
246 |
247 | class AudioAnalyser {
248 | constructor() {
249 | // input node
250 | this.in = audioContext.createAnalyser();
251 | this.in.smoothingTimeConstant = 0.2;
252 | this.in.fftSize = 32;
253 | // compression
254 | this.in.minDecibels = -100;
255 | this.in.maxDecibels = -50;
256 | // limit analyser spectrum
257 | this.minFreq = 200; // in Hz
258 | this.maxFreq = 8000; // in Hz
259 | this.updateBinNorm();
260 | // pre-allocation of freqs ampl. array
261 | this.magnitudes = new Uint8Array(this.in.frequencyBinCount);
262 | }
263 |
264 | // update normalization parameters
265 | updateBinNorm(){
266 | let norm = this.in.fftSize / audioContext.sampleRate;
267 | this.minBin = Math.round(this.minFreq * norm);
268 | this.maxBin = Math.round(this.maxFreq * norm);
269 | this.binsNormalisation = 1 / (this.maxBin - this.minBin + 1);
270 | }
271 |
272 | // return current analyzer amplitude (no freq. specific) between 0 and 1
273 | getAmplitude() {
274 | // extract data from analyzer
275 | this.in.getByteFrequencyData(this.magnitudes);
276 | // get average amplitude value
277 | let amplitude = 0.0;
278 | for( let i = this.minBin; i <= this.maxBin; ++i ) {
279 | amplitude += this.magnitudes[i];
280 | }
281 | amplitude *= this.binsNormalisation / 250;
282 | // let norm = this.in.frequencyBinCount * 100; // arbitrary value, to be cleaned
283 | return amplitude;
284 | }
285 |
286 | }
--------------------------------------------------------------------------------
/maxmsp/patchers/nu.synth.maxpat:
--------------------------------------------------------------------------------
1 | {
2 | "patcher" : {
3 | "fileversion" : 1,
4 | "appversion" : {
5 | "major" : 7,
6 | "minor" : 3,
7 | "revision" : 3,
8 | "architecture" : "x86",
9 | "modernui" : 1
10 | }
11 | ,
12 | "rect" : [ 391.0, 237.0, 625.0, 387.0 ],
13 | "bglocked" : 0,
14 | "openinpresentation" : 0,
15 | "default_fontsize" : 12.0,
16 | "default_fontface" : 0,
17 | "default_fontname" : "Arial",
18 | "gridonopen" : 1,
19 | "gridsize" : [ 15.0, 15.0 ],
20 | "gridsnaponopen" : 1,
21 | "objectsnaponopen" : 1,
22 | "statusbarvisible" : 2,
23 | "toolbarvisible" : 1,
24 | "lefttoolbarpinned" : 0,
25 | "toptoolbarpinned" : 0,
26 | "righttoolbarpinned" : 0,
27 | "bottomtoolbarpinned" : 0,
28 | "toolbars_unpinned_last_save" : 0,
29 | "tallnewobj" : 0,
30 | "boxanimatetime" : 200,
31 | "enablehscroll" : 1,
32 | "enablevscroll" : 1,
33 | "devicewidth" : 0.0,
34 | "description" : "",
35 | "digest" : "",
36 | "tags" : "",
37 | "style" : "",
38 | "subpatcher_template" : "",
39 | "boxes" : [ {
40 | "box" : {
41 | "id" : "obj-4",
42 | "maxclass" : "message",
43 | "numinlets" : 2,
44 | "numoutlets" : 1,
45 | "outlettype" : [ "" ],
46 | "patching_rect" : [ 505.666656, 182.666672, 37.0, 22.0 ],
47 | "style" : "",
48 | "text" : "clear"
49 | }
50 |
51 | }
52 | , {
53 | "box" : {
54 | "id" : "obj-24",
55 | "maxclass" : "newobj",
56 | "numinlets" : 1,
57 | "numoutlets" : 0,
58 | "patching_rect" : [ 533.0, 23.0, 53.0, 22.0 ],
59 | "style" : "",
60 | "text" : "nu.error"
61 | }
62 |
63 | }
64 | , {
65 | "box" : {
66 | "id" : "obj-43",
67 | "maxclass" : "newobj",
68 | "numinlets" : 1,
69 | "numoutlets" : 1,
70 | "outlettype" : [ "" ],
71 | "patching_rect" : [ 444.0, 153.0, 123.0, 22.0 ],
72 | "style" : "",
73 | "text" : "prepend releaseTime"
74 | }
75 |
76 | }
77 | , {
78 | "box" : {
79 | "id" : "obj-41",
80 | "maxclass" : "newobj",
81 | "numinlets" : 1,
82 | "numoutlets" : 1,
83 | "outlettype" : [ "" ],
84 | "patching_rect" : [ 383.0, 121.0, 116.0, 22.0 ],
85 | "style" : "",
86 | "text" : "prepend attackTime"
87 | }
88 |
89 | }
90 | , {
91 | "box" : {
92 | "id" : "obj-39",
93 | "maxclass" : "newobj",
94 | "numinlets" : 1,
95 | "numoutlets" : 1,
96 | "outlettype" : [ "" ],
97 | "patching_rect" : [ 321.666656, 89.0, 130.0, 22.0 ],
98 | "style" : "",
99 | "text" : "prepend periodicWave"
100 | }
101 |
102 | }
103 | , {
104 | "box" : {
105 | "id" : "obj-37",
106 | "maxclass" : "newobj",
107 | "numinlets" : 1,
108 | "numoutlets" : 1,
109 | "outlettype" : [ "" ],
110 | "patching_rect" : [ 260.0, 182.666672, 113.0, 22.0 ],
111 | "style" : "",
112 | "text" : "prepend synthType"
113 | }
114 |
115 | }
116 | , {
117 | "box" : {
118 | "id" : "obj-35",
119 | "maxclass" : "newobj",
120 | "numinlets" : 1,
121 | "numoutlets" : 1,
122 | "outlettype" : [ "" ],
123 | "patching_rect" : [ 199.0, 151.666672, 148.0, 22.0 ],
124 | "style" : "",
125 | "text" : "prepend linkPlayerToNote"
126 | }
127 |
128 | }
129 | , {
130 | "box" : {
131 | "id" : "obj-33",
132 | "maxclass" : "newobj",
133 | "numinlets" : 1,
134 | "numoutlets" : 1,
135 | "outlettype" : [ "" ],
136 | "patching_rect" : [ 137.0, 120.666664, 113.0, 22.0 ],
137 | "style" : "",
138 | "text" : "prepend noteOnOff"
139 | }
140 |
141 | }
142 | , {
143 | "box" : {
144 | "id" : "obj-26",
145 | "maxclass" : "newobj",
146 | "numinlets" : 1,
147 | "numoutlets" : 1,
148 | "outlettype" : [ "" ],
149 | "patching_rect" : [ 76.333336, 89.0, 97.0, 22.0 ],
150 | "style" : "",
151 | "text" : "prepend volume"
152 | }
153 |
154 | }
155 | , {
156 | "box" : {
157 | "id" : "obj-3",
158 | "maxclass" : "message",
159 | "numinlets" : 2,
160 | "numoutlets" : 1,
161 | "outlettype" : [ "" ],
162 | "patching_rect" : [ 15.0, 89.0, 43.0, 22.0 ],
163 | "style" : "",
164 | "text" : "set $1"
165 | }
166 |
167 | }
168 | , {
169 | "box" : {
170 | "id" : "obj-11",
171 | "maxclass" : "newobj",
172 | "numinlets" : 1,
173 | "numoutlets" : 1,
174 | "outlettype" : [ "" ],
175 | "patching_rect" : [ 15.0, 269.0, 113.0, 22.0 ],
176 | "style" : "",
177 | "text" : "prepend #1"
178 | }
179 |
180 | }
181 | , {
182 | "box" : {
183 | "id" : "obj-158",
184 | "maxclass" : "newobj",
185 | "numinlets" : 1,
186 | "numoutlets" : 0,
187 | "patching_rect" : [ 15.0, 333.5, 65.0, 22.0 ],
188 | "style" : "",
189 | "text" : "s toServer"
190 | }
191 |
192 | }
193 | , {
194 | "box" : {
195 | "id" : "obj-56",
196 | "maxclass" : "newobj",
197 | "numinlets" : 1,
198 | "numoutlets" : 1,
199 | "outlettype" : [ "" ],
200 | "patching_rect" : [ 15.0, 304.5, 105.0, 22.0 ],
201 | "style" : "",
202 | "text" : "prepend /nuSynth"
203 | }
204 |
205 | }
206 | , {
207 | "box" : {
208 | "id" : "obj-23",
209 | "maxclass" : "newobj",
210 | "numinlets" : 10,
211 | "numoutlets" : 10,
212 | "outlettype" : [ "", "", "", "", "", "", "", "", "", "" ],
213 | "patching_rect" : [ 15.0, 52.0, 571.0, 22.0 ],
214 | "style" : "",
215 | "text" : "route playerId volume noteOnOff linkPlayerToNote synthType periodicWave attackTime releaseTime clear"
216 | }
217 |
218 | }
219 | , {
220 | "box" : {
221 | "comment" : "",
222 | "id" : "obj-1",
223 | "index" : 0,
224 | "maxclass" : "inlet",
225 | "numinlets" : 0,
226 | "numoutlets" : 1,
227 | "outlettype" : [ "" ],
228 | "patching_rect" : [ 15.0, 9.0, 30.0, 30.0 ],
229 | "style" : ""
230 | }
231 |
232 | }
233 | ],
234 | "lines" : [ {
235 | "patchline" : {
236 | "destination" : [ "obj-23", 0 ],
237 | "disabled" : 0,
238 | "hidden" : 0,
239 | "source" : [ "obj-1", 0 ]
240 | }
241 |
242 | }
243 | , {
244 | "patchline" : {
245 | "destination" : [ "obj-56", 0 ],
246 | "disabled" : 0,
247 | "hidden" : 0,
248 | "source" : [ "obj-11", 0 ]
249 | }
250 |
251 | }
252 | , {
253 | "patchline" : {
254 | "destination" : [ "obj-24", 0 ],
255 | "disabled" : 0,
256 | "hidden" : 0,
257 | "source" : [ "obj-23", 9 ]
258 | }
259 |
260 | }
261 | , {
262 | "patchline" : {
263 | "destination" : [ "obj-26", 0 ],
264 | "disabled" : 0,
265 | "hidden" : 0,
266 | "source" : [ "obj-23", 1 ]
267 | }
268 |
269 | }
270 | , {
271 | "patchline" : {
272 | "destination" : [ "obj-3", 0 ],
273 | "disabled" : 0,
274 | "hidden" : 0,
275 | "source" : [ "obj-23", 0 ]
276 | }
277 |
278 | }
279 | , {
280 | "patchline" : {
281 | "destination" : [ "obj-33", 0 ],
282 | "disabled" : 0,
283 | "hidden" : 0,
284 | "source" : [ "obj-23", 2 ]
285 | }
286 |
287 | }
288 | , {
289 | "patchline" : {
290 | "destination" : [ "obj-35", 0 ],
291 | "disabled" : 0,
292 | "hidden" : 0,
293 | "source" : [ "obj-23", 3 ]
294 | }
295 |
296 | }
297 | , {
298 | "patchline" : {
299 | "destination" : [ "obj-37", 0 ],
300 | "disabled" : 0,
301 | "hidden" : 0,
302 | "source" : [ "obj-23", 4 ]
303 | }
304 |
305 | }
306 | , {
307 | "patchline" : {
308 | "destination" : [ "obj-39", 0 ],
309 | "disabled" : 0,
310 | "hidden" : 0,
311 | "source" : [ "obj-23", 5 ]
312 | }
313 |
314 | }
315 | , {
316 | "patchline" : {
317 | "destination" : [ "obj-4", 0 ],
318 | "disabled" : 0,
319 | "hidden" : 0,
320 | "source" : [ "obj-23", 8 ]
321 | }
322 |
323 | }
324 | , {
325 | "patchline" : {
326 | "destination" : [ "obj-41", 0 ],
327 | "disabled" : 0,
328 | "hidden" : 0,
329 | "source" : [ "obj-23", 6 ]
330 | }
331 |
332 | }
333 | , {
334 | "patchline" : {
335 | "destination" : [ "obj-43", 0 ],
336 | "disabled" : 0,
337 | "hidden" : 0,
338 | "source" : [ "obj-23", 7 ]
339 | }
340 |
341 | }
342 | , {
343 | "patchline" : {
344 | "destination" : [ "obj-11", 0 ],
345 | "disabled" : 0,
346 | "hidden" : 0,
347 | "source" : [ "obj-26", 0 ]
348 | }
349 |
350 | }
351 | , {
352 | "patchline" : {
353 | "destination" : [ "obj-11", 0 ],
354 | "disabled" : 0,
355 | "hidden" : 0,
356 | "source" : [ "obj-3", 0 ]
357 | }
358 |
359 | }
360 | , {
361 | "patchline" : {
362 | "destination" : [ "obj-11", 0 ],
363 | "disabled" : 0,
364 | "hidden" : 0,
365 | "source" : [ "obj-33", 0 ]
366 | }
367 |
368 | }
369 | , {
370 | "patchline" : {
371 | "destination" : [ "obj-11", 0 ],
372 | "disabled" : 0,
373 | "hidden" : 0,
374 | "source" : [ "obj-35", 0 ]
375 | }
376 |
377 | }
378 | , {
379 | "patchline" : {
380 | "destination" : [ "obj-11", 0 ],
381 | "disabled" : 0,
382 | "hidden" : 0,
383 | "source" : [ "obj-39", 0 ]
384 | }
385 |
386 | }
387 | , {
388 | "patchline" : {
389 | "destination" : [ "obj-11", 0 ],
390 | "disabled" : 0,
391 | "hidden" : 0,
392 | "source" : [ "obj-4", 0 ]
393 | }
394 |
395 | }
396 | , {
397 | "patchline" : {
398 | "destination" : [ "obj-11", 0 ],
399 | "disabled" : 0,
400 | "hidden" : 0,
401 | "source" : [ "obj-41", 0 ]
402 | }
403 |
404 | }
405 | , {
406 | "patchline" : {
407 | "destination" : [ "obj-11", 0 ],
408 | "disabled" : 0,
409 | "hidden" : 0,
410 | "source" : [ "obj-43", 0 ]
411 | }
412 |
413 | }
414 | , {
415 | "patchline" : {
416 | "destination" : [ "obj-158", 0 ],
417 | "disabled" : 0,
418 | "hidden" : 0,
419 | "source" : [ "obj-56", 0 ]
420 | }
421 |
422 | }
423 | ],
424 | "dependency_cache" : [ {
425 | "name" : "nu.error.maxpat",
426 | "bootpath" : "~/Projects/Cosima/devs/Nu/soundworks-nu/maxmsp/extras",
427 | "type" : "JSON",
428 | "implicit" : 1
429 | }
430 | ],
431 | "autosave" : 0
432 | }
433 |
434 | }
435 |
--------------------------------------------------------------------------------
/maxmsp/patchers/nu.loop.maxpat:
--------------------------------------------------------------------------------
1 | {
2 | "patcher" : {
3 | "fileversion" : 1,
4 | "appversion" : {
5 | "major" : 7,
6 | "minor" : 3,
7 | "revision" : 3,
8 | "architecture" : "x86",
9 | "modernui" : 1
10 | }
11 | ,
12 | "rect" : [ 597.0, 363.0, 541.0, 397.0 ],
13 | "bglocked" : 0,
14 | "openinpresentation" : 0,
15 | "default_fontsize" : 12.0,
16 | "default_fontface" : 0,
17 | "default_fontname" : "Arial",
18 | "gridonopen" : 1,
19 | "gridsize" : [ 15.0, 15.0 ],
20 | "gridsnaponopen" : 1,
21 | "objectsnaponopen" : 1,
22 | "statusbarvisible" : 2,
23 | "toolbarvisible" : 1,
24 | "lefttoolbarpinned" : 0,
25 | "toptoolbarpinned" : 0,
26 | "righttoolbarpinned" : 0,
27 | "bottomtoolbarpinned" : 0,
28 | "toolbars_unpinned_last_save" : 0,
29 | "tallnewobj" : 0,
30 | "boxanimatetime" : 200,
31 | "enablehscroll" : 1,
32 | "enablevscroll" : 1,
33 | "devicewidth" : 0.0,
34 | "description" : "",
35 | "digest" : "",
36 | "tags" : "",
37 | "style" : "",
38 | "subpatcher_template" : "",
39 | "boxes" : [ {
40 | "box" : {
41 | "id" : "obj-27",
42 | "maxclass" : "message",
43 | "numinlets" : 2,
44 | "numoutlets" : 1,
45 | "outlettype" : [ "" ],
46 | "patching_rect" : [ 435.444458, 219.0, 37.0, 22.0 ],
47 | "style" : "",
48 | "text" : "reset"
49 | }
50 |
51 | }
52 | , {
53 | "box" : {
54 | "id" : "obj-19",
55 | "maxclass" : "message",
56 | "numinlets" : 2,
57 | "numoutlets" : 1,
58 | "outlettype" : [ "" ],
59 | "patching_rect" : [ 382.888885, 271.0, 43.0, 22.0 ],
60 | "style" : "",
61 | "text" : "set $1"
62 | }
63 |
64 | }
65 | , {
66 | "box" : {
67 | "id" : "obj-17",
68 | "maxclass" : "newobj",
69 | "numinlets" : 1,
70 | "numoutlets" : 1,
71 | "outlettype" : [ "" ],
72 | "patching_rect" : [ 330.333344, 307.5, 71.0, 22.0 ],
73 | "style" : "",
74 | "text" : "prepend #2"
75 | }
76 |
77 | }
78 | , {
79 | "box" : {
80 | "id" : "obj-11",
81 | "maxclass" : "newobj",
82 | "numinlets" : 1,
83 | "numoutlets" : 1,
84 | "outlettype" : [ "" ],
85 | "patching_rect" : [ 15.0, 290.0, 113.0, 22.0 ],
86 | "style" : "",
87 | "text" : "prepend #1"
88 | }
89 |
90 | }
91 | , {
92 | "box" : {
93 | "id" : "obj-12",
94 | "maxclass" : "newobj",
95 | "numinlets" : 1,
96 | "numoutlets" : 0,
97 | "patching_rect" : [ 15.0, 342.5, 65.0, 22.0 ],
98 | "style" : "",
99 | "text" : "s toServer"
100 | }
101 |
102 | }
103 | , {
104 | "box" : {
105 | "id" : "obj-13",
106 | "maxclass" : "newobj",
107 | "numinlets" : 1,
108 | "numoutlets" : 1,
109 | "outlettype" : [ "" ],
110 | "patching_rect" : [ 15.0, 316.5, 101.0, 22.0 ],
111 | "style" : "",
112 | "text" : "prepend /nuLoop"
113 | }
114 |
115 | }
116 | , {
117 | "box" : {
118 | "id" : "obj-10",
119 | "maxclass" : "newobj",
120 | "numinlets" : 1,
121 | "numoutlets" : 1,
122 | "outlettype" : [ "" ],
123 | "patching_rect" : [ 330.333344, 336.5, 124.0, 22.0 ],
124 | "style" : "",
125 | "text" : "prepend setTrackSlot"
126 | }
127 |
128 | }
129 | , {
130 | "box" : {
131 | "id" : "obj-4",
132 | "maxclass" : "message",
133 | "numinlets" : 2,
134 | "numoutlets" : 1,
135 | "outlettype" : [ "" ],
136 | "patching_rect" : [ 15.0, 89.0, 43.0, 22.0 ],
137 | "style" : "",
138 | "text" : "set $1"
139 | }
140 |
141 | }
142 | , {
143 | "box" : {
144 | "id" : "obj-9",
145 | "maxclass" : "newobj",
146 | "numinlets" : 1,
147 | "numoutlets" : 1,
148 | "outlettype" : [ "" ],
149 | "patching_rect" : [ 277.0, 225.5, 81.0, 22.0 ],
150 | "style" : "",
151 | "text" : "prepend jitter"
152 | }
153 |
154 | }
155 | , {
156 | "box" : {
157 | "id" : "obj-8",
158 | "maxclass" : "newobj",
159 | "numinlets" : 1,
160 | "numoutlets" : 1,
161 | "outlettype" : [ "" ],
162 | "patching_rect" : [ 225.0, 191.5, 124.0, 22.0 ],
163 | "style" : "",
164 | "text" : "prepend jitterMemory"
165 | }
166 |
167 | }
168 | , {
169 | "box" : {
170 | "id" : "obj-7",
171 | "maxclass" : "newobj",
172 | "numinlets" : 1,
173 | "numoutlets" : 1,
174 | "outlettype" : [ "" ],
175 | "patching_rect" : [ 172.0, 157.5, 120.0, 22.0 ],
176 | "style" : "",
177 | "text" : "prepend masterGain"
178 | }
179 |
180 | }
181 | , {
182 | "box" : {
183 | "id" : "obj-6",
184 | "maxclass" : "newobj",
185 | "numinlets" : 1,
186 | "numoutlets" : 1,
187 | "outlettype" : [ "" ],
188 | "patching_rect" : [ 120.0, 123.5, 91.0, 22.0 ],
189 | "style" : "",
190 | "text" : "prepend period"
191 | }
192 |
193 | }
194 | , {
195 | "box" : {
196 | "id" : "obj-2",
197 | "maxclass" : "newobj",
198 | "numinlets" : 1,
199 | "numoutlets" : 1,
200 | "outlettype" : [ "" ],
201 | "patching_rect" : [ 67.555557, 89.0, 98.0, 22.0 ],
202 | "style" : "",
203 | "text" : "prepend division"
204 | }
205 |
206 | }
207 | , {
208 | "box" : {
209 | "id" : "obj-24",
210 | "maxclass" : "newobj",
211 | "numinlets" : 1,
212 | "numoutlets" : 0,
213 | "patching_rect" : [ 454.0, 23.0, 53.0, 22.0 ],
214 | "style" : "",
215 | "text" : "nu.error"
216 | }
217 |
218 | }
219 | , {
220 | "box" : {
221 | "id" : "obj-23",
222 | "maxclass" : "newobj",
223 | "numinlets" : 10,
224 | "numoutlets" : 10,
225 | "outlettype" : [ "", "", "", "", "", "", "", "", "", "" ],
226 | "patching_rect" : [ 15.0, 55.0, 492.0, 22.0 ],
227 | "style" : "",
228 | "text" : "route playerId divisions period masterGain jitterMemory jitter setTrackSlot trackName reset"
229 | }
230 |
231 | }
232 | , {
233 | "box" : {
234 | "comment" : "",
235 | "id" : "obj-1",
236 | "index" : 0,
237 | "maxclass" : "inlet",
238 | "numinlets" : 0,
239 | "numoutlets" : 1,
240 | "outlettype" : [ "" ],
241 | "patching_rect" : [ 15.0, 9.0, 30.0, 30.0 ],
242 | "style" : ""
243 | }
244 |
245 | }
246 | ],
247 | "lines" : [ {
248 | "patchline" : {
249 | "destination" : [ "obj-23", 0 ],
250 | "disabled" : 0,
251 | "hidden" : 0,
252 | "source" : [ "obj-1", 0 ]
253 | }
254 |
255 | }
256 | , {
257 | "patchline" : {
258 | "destination" : [ "obj-11", 0 ],
259 | "disabled" : 0,
260 | "hidden" : 0,
261 | "source" : [ "obj-10", 0 ]
262 | }
263 |
264 | }
265 | , {
266 | "patchline" : {
267 | "destination" : [ "obj-13", 0 ],
268 | "disabled" : 0,
269 | "hidden" : 0,
270 | "source" : [ "obj-11", 0 ]
271 | }
272 |
273 | }
274 | , {
275 | "patchline" : {
276 | "destination" : [ "obj-12", 0 ],
277 | "disabled" : 0,
278 | "hidden" : 0,
279 | "source" : [ "obj-13", 0 ]
280 | }
281 |
282 | }
283 | , {
284 | "patchline" : {
285 | "destination" : [ "obj-10", 0 ],
286 | "disabled" : 0,
287 | "hidden" : 0,
288 | "source" : [ "obj-17", 0 ]
289 | }
290 |
291 | }
292 | , {
293 | "patchline" : {
294 | "destination" : [ "obj-17", 0 ],
295 | "disabled" : 0,
296 | "hidden" : 0,
297 | "source" : [ "obj-19", 0 ]
298 | }
299 |
300 | }
301 | , {
302 | "patchline" : {
303 | "destination" : [ "obj-11", 0 ],
304 | "disabled" : 0,
305 | "hidden" : 0,
306 | "source" : [ "obj-2", 0 ]
307 | }
308 |
309 | }
310 | , {
311 | "patchline" : {
312 | "destination" : [ "obj-17", 0 ],
313 | "disabled" : 0,
314 | "hidden" : 0,
315 | "source" : [ "obj-23", 6 ]
316 | }
317 |
318 | }
319 | , {
320 | "patchline" : {
321 | "destination" : [ "obj-19", 0 ],
322 | "disabled" : 0,
323 | "hidden" : 0,
324 | "source" : [ "obj-23", 7 ]
325 | }
326 |
327 | }
328 | , {
329 | "patchline" : {
330 | "destination" : [ "obj-2", 0 ],
331 | "disabled" : 0,
332 | "hidden" : 0,
333 | "source" : [ "obj-23", 1 ]
334 | }
335 |
336 | }
337 | , {
338 | "patchline" : {
339 | "destination" : [ "obj-24", 0 ],
340 | "disabled" : 0,
341 | "hidden" : 0,
342 | "source" : [ "obj-23", 9 ]
343 | }
344 |
345 | }
346 | , {
347 | "patchline" : {
348 | "destination" : [ "obj-27", 0 ],
349 | "disabled" : 0,
350 | "hidden" : 0,
351 | "source" : [ "obj-23", 8 ]
352 | }
353 |
354 | }
355 | , {
356 | "patchline" : {
357 | "destination" : [ "obj-4", 0 ],
358 | "disabled" : 0,
359 | "hidden" : 0,
360 | "source" : [ "obj-23", 0 ]
361 | }
362 |
363 | }
364 | , {
365 | "patchline" : {
366 | "destination" : [ "obj-6", 0 ],
367 | "disabled" : 0,
368 | "hidden" : 0,
369 | "source" : [ "obj-23", 2 ]
370 | }
371 |
372 | }
373 | , {
374 | "patchline" : {
375 | "destination" : [ "obj-7", 0 ],
376 | "disabled" : 0,
377 | "hidden" : 0,
378 | "source" : [ "obj-23", 3 ]
379 | }
380 |
381 | }
382 | , {
383 | "patchline" : {
384 | "destination" : [ "obj-8", 0 ],
385 | "disabled" : 0,
386 | "hidden" : 0,
387 | "source" : [ "obj-23", 4 ]
388 | }
389 |
390 | }
391 | , {
392 | "patchline" : {
393 | "destination" : [ "obj-9", 0 ],
394 | "disabled" : 0,
395 | "hidden" : 0,
396 | "source" : [ "obj-23", 5 ]
397 | }
398 |
399 | }
400 | , {
401 | "patchline" : {
402 | "destination" : [ "obj-11", 0 ],
403 | "disabled" : 0,
404 | "hidden" : 0,
405 | "source" : [ "obj-27", 0 ]
406 | }
407 |
408 | }
409 | , {
410 | "patchline" : {
411 | "destination" : [ "obj-11", 0 ],
412 | "disabled" : 0,
413 | "hidden" : 0,
414 | "source" : [ "obj-4", 0 ]
415 | }
416 |
417 | }
418 | , {
419 | "patchline" : {
420 | "destination" : [ "obj-11", 0 ],
421 | "disabled" : 0,
422 | "hidden" : 0,
423 | "source" : [ "obj-6", 0 ]
424 | }
425 |
426 | }
427 | , {
428 | "patchline" : {
429 | "destination" : [ "obj-11", 0 ],
430 | "disabled" : 0,
431 | "hidden" : 0,
432 | "source" : [ "obj-7", 0 ]
433 | }
434 |
435 | }
436 | , {
437 | "patchline" : {
438 | "destination" : [ "obj-11", 0 ],
439 | "disabled" : 0,
440 | "hidden" : 0,
441 | "source" : [ "obj-8", 0 ]
442 | }
443 |
444 | }
445 | , {
446 | "patchline" : {
447 | "destination" : [ "obj-11", 0 ],
448 | "disabled" : 0,
449 | "hidden" : 0,
450 | "source" : [ "obj-9", 0 ]
451 | }
452 |
453 | }
454 | ],
455 | "dependency_cache" : [ {
456 | "name" : "nu.error.maxpat",
457 | "bootpath" : "~/Projects/Cosima/devs/Nu/soundworks-nu/maxmsp/extras",
458 | "type" : "JSON",
459 | "implicit" : 1
460 | }
461 | ],
462 | "autosave" : 0
463 | }
464 |
465 | }
466 |
--------------------------------------------------------------------------------