├── .gitignore
├── README.md
├── fourhandslogo.png
├── index.js
├── loop.html
├── package.json
└── public
├── audio.png
├── css
└── style.css
├── favicon.png
├── fourhandslogo.png
├── fourhandslogo.svg
├── index.html
├── index_backwards.html
├── js
├── main.js
└── main_backwards.js
├── keyboard.svg
└── keyboard2.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | .nyc_output/
2 | coverage/
3 | node_modules/
4 | npm-debug.log
5 | package-lock.json
6 | test/*.log
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fourhands, the p2p piano
2 |
3 | 
4 |
5 | Fourhands uses WebRTC to establish p2p connections for minimal latency 2-person
6 | jamming using MIDI keyboards.
7 |
8 | For seamless collaboration on Fourhands, one-way time of 20 ms or less is
9 | ideal. Typically this can be achieved on wired connections for fairly nearby
10 | players (within 35 miles / 50 km. Anecdotally, a friend has also achieved this
11 | between SF and LA -- 350 miles / 500 km).
12 |
13 | Try it here: [create a room and share the link to invite another
14 | player](https://fourhands.jminjie.com).
15 |
16 | ## Prior art
17 | Online jamming has been achieved already, but often not in an accessible and
18 | unstructured way.
19 |
20 | - [Jacktrip](https://news.stanford.edu/2020/09/18/jacktrip-software-allows-musicians-sync-performances-online/)
21 | allows fairly nearby players to jam in real time, but requires a local
22 | machine with a static IP (an AWS instance will not work, since the audio
23 | cable must be plugged in to the machine).
24 | - A few different apps including
25 | [Endless](https://www.theverge.com/2020/3/31/21201913/endlesss-app-music-remotely-jam-out-loops-real-time),
26 | [NinJam](https://www.cockos.com/ninjam/), and [Jammr](https://jammr.net/)
27 | allow collaborative looping, in which your playing is shared n measures after
28 | you play it.
29 | - [Jamlink](https://musicplayers.com/2011/11/musicianlink-jamlink/) shares
30 | audio for remote jamming but requires custom hardware.
31 | - My own [collaborative piano](https://piano.jminjie.com) allows multiple
32 | participants to share a piano (like Google Docs for piano). The latency is
33 | too high to jam, but this does work for sharing ideas when songwriting
34 | remotely.
35 | - See an [overview of other options
36 | here](https://acousticguitar.com/virtual-jamming-the-latest-tools-for-playing-together-in-real-time/).
37 |
38 | Comparatively, Fourhands is a simple in-browser solution for which all you need
39 | is a MIDI keyboard and an optionally wired internet. Because only MIDI data is
40 | shared, it is limited to instruments which can output MIDI.
41 |
42 | ## Development
43 | For self hosting, after cloning the repo run `npm install` to install necessary
44 | packages.
45 |
46 | Deploy with `node index.js debug`. This will serve the files
47 | needed for the page (index.html and js/) and also start the NodeJs server
48 | (index.js).
49 |
50 | If you have local SSL keys you can deploy with HTTPS using `node index.js`.
51 | Note that a secure connection is required for MIDI input (localhost is secure
52 | by default).
53 |
54 | Client should be available at localhost:30001.
55 |
56 | ## Browser support
57 | Chrome and Edge work. Firefox does not work as there is [no support for
58 | MIDI](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess). Other
59 | browsers are not tested, but should work if they support MIDI, Tone.js, web
60 | sockets, and WebRTC.
61 |
--------------------------------------------------------------------------------
/fourhandslogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jminjie/fourhands/9aa31d962f0be515049979d42731ec4da84bfb09/fourhandslogo.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var os = require('os');
4 | var nodeStatic = require('node-static');
5 | var socketIO = require('socket.io');
6 |
7 | var fileServer = new nodeStatic.Server('./public');
8 |
9 | const DEBUG = (process.argv[2] == "debug") ? true : false;
10 |
11 | if (DEBUG) {
12 | console.log("Running in debug mode. Note that MIDI is not available without HTTPS");
13 | console.log("Up on localhost:30001");
14 | const http = require('http');
15 | var app = http.createServer(function(req, res) {
16 | log(req.url);
17 | fileServer.serve(req, res);
18 | }).listen(30001);
19 | } else {
20 | const Https = require('https');
21 | const Fs = require('fs');
22 | var secureApp = Https.createServer({
23 | key: Fs.readFileSync('/etc/letsencrypt/live/jminjie.com/privkey.pem'),
24 | cert: Fs.readFileSync('/etc/letsencrypt/live/jminjie.com/cert.pem'),
25 | ca: Fs.readFileSync('/etc/letsencrypt/live/jminjie.com/chain.pem')
26 | }, function(req, res) {
27 | fileServer.serve(req, res);
28 | }).listen(30001);
29 | }
30 |
31 | function log(m) {
32 | console.log(m);
33 | }
34 |
35 | var io = socketIO.listen(DEBUG ? app : secureApp);
36 | io.sockets.on('connection', function(socket) {
37 |
38 | socket.on('message', function({ m, r }) {
39 | log('Client said: ', m);
40 | socket.broadcast.to(r).emit('message', m);
41 | });
42 |
43 |
44 | socket.on('create or join', function(room) {
45 | log('Received request to create or join room ' + room);
46 |
47 | var clientsInRoom = io.sockets.adapter.rooms[room];
48 | var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
49 | log('Room ' + room + ' currently has ' + numClients + ' client(s)');
50 |
51 | if (numClients === 0) {
52 | socket.join(room);
53 | log('Client ID ' + socket.id + ' created room ' + room);
54 | socket.emit('created', room, socket.id);
55 | } else if (numClients === 1) {
56 | log('Client ID ' + socket.id + ' joined room ' + room);
57 | io.sockets.in(room).emit('join', room);
58 | socket.join(room);
59 | socket.emit('joined', room, socket.id);
60 | io.sockets.in(room).emit('ready', room);
61 | //socket.broadcast.emit('ready', room);
62 | } else { // max two clients
63 | socket.emit('full', room);
64 | }
65 | });
66 |
67 | socket.on('ipaddr', function() {
68 | var ifaces = os.networkInterfaces();
69 | for (var dev in ifaces) {
70 | ifaces[dev].forEach(function(details) {
71 | if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
72 | socket.emit('ipaddr', details.address);
73 | }
74 | });
75 | }
76 | });
77 |
78 | socket.on('bye', function(room) {
79 | console.log(`Peer said bye on room ${room}.`);
80 | io.sockets.in(room).emit('bye', room);
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/loop.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | How to use looper
4 |
5 | Each player gets their own looper. Each looper can only have one layer
6 | Press "Record loop" to start recording your loop. Will automatically set the looper instrument to your current sampler.
7 | Press "End record " to finish recording the loop, or "Play/pause" to finish recording and immediately play back
8 | Press "Play/Pause" to play or pause the loop
9 | To record a new loop press "Record loop" again
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fourhands",
3 | "version": "0.0.2",
4 | "description": "two piano piano",
5 | "dependencies": {
6 | "node-static": "^0.7.11",
7 | "socket.io": "^2.3.0"
8 | },
9 | "main": "index.js",
10 | "devDependencies": {},
11 | "scripts": {
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC"
17 | }
18 |
--------------------------------------------------------------------------------
/public/audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jminjie/fourhands/9aa31d962f0be515049979d42731ec4da84bfb09/public/audio.png
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 1em 1em 0em 1em;
3 | width: 610px;
4 | margin: auto;
5 | background: #E3ECF8;
6 | }
7 |
8 | h1, h2 {
9 | text-align: center;
10 | }
11 |
12 | #footer {
13 | position:static;
14 | left:0px;
15 | bottom:0px;
16 | height:130px;
17 | width:100%;
18 | }
19 |
20 | /* IE 6 */
21 | * html #footer {
22 | position:absolute;
23 | top:expression((0-(footer.offsetHeight)+(document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight)+(ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop))+'px');
24 | }
25 |
26 | #pts_canvas {
27 | position: relative;
28 | top: 5px;
29 | left: 5px;
30 | height:30px;
31 | }
32 |
33 | #pts2_canvas {
34 | position: relative;
35 | top: 5px;
36 | left: 5px;
37 | height:30px;
38 | }
39 |
40 | .sound-overlay {
41 | text-align: center;
42 | z-index: 20;
43 | border-radius: 6px;
44 | position:fixed;
45 | width: 320px;
46 | height: 230px;
47 | top: 50%;
48 | left: 50%;
49 | margin-left: -180px; /* Negative half of width. */
50 | margin-top: -180px; /* Negative half of height. */
51 | font-size: 16px;
52 | line-height: 24px;
53 | padding: 20px 20px 20px 20px;
54 | box-shadow: 0 0 0 1500px rgba(0, 0, 0, .5);
55 | background: white;
56 | .accept-sound {
57 | cursor: pointer;
58 | }
59 | }
60 |
61 | .d-none {
62 | display: none;
63 | }
64 |
65 | .hide {
66 | display: none;
67 | }
68 |
69 | .gainslider {
70 | }
71 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jminjie/fourhands/9aa31d962f0be515049979d42731ec4da84bfb09/public/favicon.png
--------------------------------------------------------------------------------
/public/fourhandslogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jminjie/fourhands/9aa31d962f0be515049979d42731ec4da84bfb09/public/fourhandslogo.png
--------------------------------------------------------------------------------
/public/fourhandslogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fourhands
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Plug in a MIDI device or use keys ASDFGHJK. Try refreshing if device is not detected.
14 | No player connected. Share the URL with another player.
15 |
16 |
42 |
43 |
44 |
68 |
69 |
70 |
71 |
Record loop
72 |
End record
73 |
Play/pause
74 |
(Instructions)
75 |
76 |
77 |
Set loop sampler
78 |
79 | Piano
80 | Classic electric piano
81 | Banjo
82 | Harpsichord
83 | Organ
84 | Acoustic bass
85 | Clavier
86 | Electric bass
87 | Drum
88 | Wurlitzer
89 | Guitar
90 | Mandolin
91 |
92 | Release:
93 | Gain:
94 | Decay:
95 |
96 |
97 | Diagnostics
98 |
99 |
Send test p2p message
100 |
Toggle visual effects
101 |
For seamless jamming, one-way time of 20 ms or less is ideal. Typically this can be achieved on
102 | wired connections for fairly nearby players (within 35 miles / 50 km).
103 |
Round trip time (ms):
104 |
Est oneway time (ms):
105 |
106 |
107 |
108 |
This website makes sound
109 |
You will hear your own playing as well as the other player in the room
110 |
111 |
Allow
112 |
113 |
114 |
115 |
116 |
117 |
118 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/public/index_backwards.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fourhands
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Plug in a MIDI device or use keys ASDFGHJK. Try refreshing if device is not detected.
14 | No player connected. Share the URL with another player.
15 |
16 |
42 |
43 |
44 |
68 |
69 |
70 |
71 |
Record loop
72 |
End record
73 |
Play/pause
74 |
(Instructions)
75 |
76 |
77 |
Set loop sampler
78 |
79 | Piano
80 | Classic electric piano
81 | Banjo
82 | Harpsichord
83 | Organ
84 | Acoustic bass
85 | Clavier
86 | Electric bass
87 | Drum
88 | Wurlitzer
89 | Guitar
90 | Mandolin
91 |
92 | Release:
93 | Gain:
94 | Decay:
95 |
96 |
97 | Diagnostics
98 |
99 |
Send test p2p message
100 |
Toggle visual effects
101 |
For seamless jamming, one-way time of 20 ms or less is ideal. Typically this can be achieved on
102 | wired connections for fairly nearby players (within 35 miles / 50 km).
103 |
Round trip time (ms):
104 |
Est oneway time (ms):
105 |
106 |
107 |
108 |
This website makes sound
109 |
You will hear your own playing as well as the other player in the room
110 |
111 |
Allow
112 |
113 |
114 |
115 |
116 |
117 |
118 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/public/js/main.js:
--------------------------------------------------------------------------------
1 | /***************************************************************************r
2 | * *
3 | * Initial setup
4 | ****************************************************************************/
5 |
6 | var configuration = {
7 | 'iceServers': [{
8 | 'urls': 'stun:stun1.l.google.com:19302'
9 | }]
10 | };
11 |
12 | var sendBtn = document.getElementById('send');
13 |
14 | // Attach event handlers
15 | sendBtn.addEventListener('click', sendTestMessage);
16 |
17 | // Disable send buttons by default.
18 | sendBtn.disabled = true;
19 |
20 | // Create a random room if not already present in the URL.
21 | var isInitiator;
22 |
23 | var room = window.location.hash.substring(1);
24 | if (!room) {
25 | room = window.location.hash = randomToken();
26 | }
27 |
28 | /****************************************************************************
29 | * Signaling server
30 | ****************************************************************************/
31 |
32 | // Connect to the signaling server
33 | var socket = io.connect();
34 |
35 | socket.on('created', function(room, clientId) {
36 | console.log('Created room', room, '- my client ID is', clientId);
37 | isInitiator = true;
38 | });
39 |
40 | socket.on('joined', function(room, clientId) {
41 | console.log('This peer has joined room', room, 'with client ID', clientId);
42 | isInitiator = false;
43 | createPeerConnection(isInitiator, configuration);
44 | });
45 |
46 | socket.on('ready', function() {
47 | console.log('Socket is ready');
48 | createPeerConnection(isInitiator, configuration);
49 | });
50 |
51 | socket.on('full', function(room) {
52 | alert('Room ' + room + ' is full. Try again later.');
53 | });
54 |
55 | socket.on('message', function(message) {
56 | //console.log('Client received message:', message);
57 | signalingMessageCallback(message);
58 | });
59 |
60 | // Joining a room.
61 | socket.emit('create or join', room);
62 |
63 | socket.on('bye', function(room) {
64 | console.log(`Peer leaving room ${room}.`);
65 | sendBtn.disabled = true;
66 | document.getElementById("peer-status").innerHTML = "Lost connection from partner.";
67 | document.getElementById("peer-status").style.color = "#000";
68 | document.getElementById("ping").innerHTML = "";
69 | document.getElementById("ping2").innerHTML = "";
70 | // If peer did not create the room, re-enter to be creator.
71 | if (!isInitiator) {
72 | window.location.reload();
73 | }
74 | });
75 |
76 | window.addEventListener('unload', function() {
77 | console.log(`Unloading window. Notifying peers in ${room}.`);
78 | socket.emit('bye', room);
79 | });
80 |
81 | window.addEventListener('load', function() {
82 | if (document.cookie.indexOf("cookie_soundon=") < 0) {
83 | document.querySelector('.sound-overlay').classList.remove('d-none');
84 | }
85 | });
86 |
87 | /**
88 | * Send message to signaling server
89 | */
90 | function sendMessageToServer(message) {
91 | //console.log('Client sending message to server:', message, ' room:', room);
92 | socket.emit('message', { m: message, r: room })
93 | }
94 |
95 | /****************************************************************************
96 | * WebRTC peer connection and data channel
97 | ****************************************************************************/
98 |
99 | var peerConn;
100 | var dataChannel;
101 |
102 | function signalingMessageCallback(message) {
103 | if (message == null) {
104 | console.log("signalingMessageCallback is ignoring null message");
105 | return;
106 | }
107 | if (message.type === 'offer') {
108 | console.log('Got offer. Sending answer to peer.');
109 | peerConn.setRemoteDescription(new RTCSessionDescription(message), function() {},
110 | logError);
111 | peerConn.createAnswer(onLocalSessionCreated, logError);
112 |
113 | } else if (message.type === 'answer') {
114 | console.log('Got answer.');
115 | peerConn.setRemoteDescription(new RTCSessionDescription(message), function() {},
116 | logError);
117 |
118 | } else if (message.type === 'candidate') {
119 | peerConn.addIceCandidate(new RTCIceCandidate({
120 | candidate: message.candidate,
121 | sdpMLineIndex: message.label,
122 | sdpMid: message.id
123 | }));
124 |
125 | }
126 | }
127 |
128 | function createPeerConnection(isInitiator, config) {
129 | console.log('Creating Peer connection as initiator?', isInitiator, 'config:',
130 | config);
131 | peerConn = new RTCPeerConnection(config);
132 |
133 | // send any ice candidates to the other peer
134 | peerConn.onicecandidate = function(event) {
135 | //console.log('icecandidate event:', event);
136 | if (event.candidate) {
137 | sendMessageToServer({
138 | type: 'candidate',
139 | label: event.candidate.sdpMLineIndex,
140 | id: event.candidate.sdpMid,
141 | candidate: event.candidate.candidate
142 | });
143 | } else {
144 | console.log('End of candidates.');
145 | }
146 | };
147 |
148 | if (isInitiator) {
149 | console.log('Creating Data Channel');
150 | dataChannel = peerConn.createDataChannel('midi-data');
151 | onDataChannelCreated(dataChannel);
152 |
153 | console.log('Creating an offer');
154 | peerConn.createOffer()
155 | .then(function(offer) {
156 | return peerConn.setLocalDescription(offer);
157 | })
158 | .then(() => {
159 | console.log('sending local desc:', peerConn.localDescription);
160 | sendMessageToServer(peerConn.localDescription);
161 | })
162 | .catch(logError);
163 |
164 | } else {
165 | peerConn.ondatachannel = function(event) {
166 | console.log('ondatachannel:', event.channel);
167 | dataChannel = event.channel;
168 | onDataChannelCreated(dataChannel);
169 | };
170 | }
171 | }
172 |
173 | function onLocalSessionCreated(desc) {
174 | console.log('local session created:', desc);
175 | peerConn.setLocalDescription(desc).then(function() {
176 | console.log('sending local desc:', peerConn.localDescription);
177 | sendMessageToServer(peerConn.localDescription);
178 | }).catch(logError);
179 | }
180 |
181 | function peerConnected() {
182 | return dataChannel && peerConn.connectionState == "connected";
183 | }
184 |
185 | var pingTime = 0;
186 |
187 | function sendPing() {
188 | if (peerConnected()) {
189 | dataChannel.send('ping');
190 | pingTime = Date.now();
191 | } else {
192 | }
193 | }
194 |
195 | function onDataChannelCreated(channel) {
196 | console.log('onDataChannelCreated:', channel);
197 |
198 | channel.onopen = function() {
199 | console.log('CHANNEL opened!!!');
200 | document.getElementById("peer-status").innerHTML = "Partner connected.";
201 | document.getElementById("peer-status").style.color = "green";
202 | sendPing();
203 | window.setInterval(sendPing, 1000);
204 | sendBtn.disabled = false;
205 | // when connecting, send sampler info in case player changed already
206 | onSetMySamplerButtonPress();
207 | onSetLoopSamplerButtonPress();
208 | };
209 |
210 | channel.onclose = function () {
211 | console.log('Channel closed.');
212 | sendBtn.disabled = true;
213 | document.getElementById("peer-status").innerHTML = "Lost connection from partner.";
214 | document.getElementById("peer-status").style.color = "#000";
215 | document.getElementById("ping").innerHTML = "";
216 | document.getElementById("ping2").innerHTML = "";
217 | }
218 |
219 | channel.onmessage = function onmessage(event) {
220 | if (typeof event.data === 'string') {
221 | if (event.data == "ping") {
222 | dataChannel.send("pong");
223 | return;
224 | }
225 | if (event.data == "pong") {
226 | let ping = Date.now() - pingTime;
227 | document.getElementById("ping").innerHTML = ping;
228 | document.getElementById("ping2").innerHTML = Math.floor(ping/2);
229 | return;
230 | }
231 | if (event.data.substring(0, 9) == "mySampler") {
232 | let samplerData = event.data.split(' ');
233 | changeTheirSampler(samplerData[1], samplerData[2], samplerData[3], samplerData[4], samplerData[5]);
234 | console.log(event.data);
235 | return;
236 | }
237 | // only you can set your own loop, so we don't need to listen for myLoopSampler
238 | if (event.data.substring(0, 16) == "theirLoopSampler") {
239 | let samplerData = event.data.split(' ');
240 | changeTheirLoopSampler(samplerData[1], samplerData[2], samplerData[3], samplerData[4], samplerData[5]);
241 | console.log(event.data);
242 | return;
243 | }
244 | if (event.data.substring(0, 12) == "theirSampler") {
245 | let samplerData = event.data.split(' ');
246 | changeMySampler(samplerData[1], samplerData[2], samplerData[3], samplerData[4], samplerData[5]);
247 | console.log(event.data);
248 | return;
249 | }
250 | console.log(event.data, Date.now());
251 | let midiData = event.data.split('-');
252 | if (midiData.length == 3) {
253 | // looks like midi data to me, lets just try to play it
254 | playMidi(THEM, parseInt(midiData[0]), parseInt(midiData[1]), parseInt(midiData[2]));
255 | }
256 | if (midiData.length == 4 && midiData[0] == "LOOP") {
257 | // loop midi data
258 | playMidi(THEIR_LOOP, parseInt(midiData[1]), parseInt(midiData[2]), parseInt(midiData[3]));
259 | }
260 | return;
261 | }
262 | };
263 | }
264 |
265 | function playTheirMidi(command, byte1, byte2) {
266 | }
267 |
268 | /****************************************************************************
269 | * MIDI things
270 | ****************************************************************************/
271 |
272 | var NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
273 |
274 | // pedal for both samplers
275 | var myPedal = false;
276 | var theirPedal = false;
277 | var myLoopPedal = false;
278 | var theirLoopPedal = false;
279 |
280 | // currently pressed keys for both samplers (used when releasing pedal)
281 | var myPressedKeys = new Set()
282 | var theirPressedKeys = new Set()
283 | var myLoopPressedKeys = new Set()
284 | var theirLoopPressedKeys = new Set()
285 |
286 | const ME = 0;
287 | const THEM = 1;
288 | const MY_LOOP = 2;
289 | const THEIR_LOOP = 3;
290 |
291 | // gain for both samplers
292 | var myGain = 1.0;
293 | var theirGain = 1.0;
294 | var myLoopGain = 1.0;
295 | var theirLoopGain = 1.0;
296 |
297 | Tone.context.latencyHint = "fastest";
298 |
299 | // octave for computer keyboard entry
300 | var octave = 0;
301 |
302 | // local loop data
303 | document.getElementById("startLoopButton").disabled = false;
304 | document.getElementById("stopLoopButton").disabled = true;
305 | document.getElementById("playPauseLoopButton").disabled = true;
306 | var loopStartTime;
307 | var loopLength;
308 | var recording = false;
309 | var loopData = [];
310 | var playingLoop = false;
311 | var loopId;
312 |
313 | if (navigator.requestMIDIAccess) {
314 | console.log('This browser supports WebMIDI!');
315 | document.getElementById("browser-status").innerHTML = "Browser supports MIDI";
316 | document.getElementById("browser-status").style.color = "green";
317 | } else {
318 | console.log('WebMIDI is not supported in this browser.');
319 | document.getElementById("browser-status").innerHTML = "No browser support for MIDI. Consider trying Chrome or Edge";
320 | document.getElementById("browser-status").style.color = "red";
321 | }
322 |
323 | try {
324 | navigator.requestMIDIAccess()
325 | .then(onMIDISuccess, onMIDIFailure);
326 | } catch (e) {
327 | console.log(e);
328 | }
329 |
330 | const default_reverb = new Tone.Reverb(1.5).toDestination();
331 |
332 | var mySampler = new Tone.Sampler({
333 | urls: {
334 | A1: "A1.mp3",
335 | A2: "A2.mp3",
336 | A3: "A3.mp3",
337 | A4: "A4.mp3",
338 | A5: "A5.mp3",
339 | A6: "A6.mp3",
340 | A7: "A7.mp3",
341 | },
342 | release: 0.6,
343 | baseUrl: "https://tonejs.github.io/audio/salamander/",
344 | }).connect(default_reverb).toDestination();
345 |
346 | var myLoopSampler = new Tone.Sampler({
347 | urls: {
348 | A1: "A1.mp3",
349 | A2: "A2.mp3",
350 | A3: "A3.mp3",
351 | A4: "A4.mp3",
352 | A5: "A5.mp3",
353 | A6: "A6.mp3",
354 | A7: "A7.mp3",
355 | },
356 | release: 0.6,
357 | baseUrl: "https://tonejs.github.io/audio/salamander/",
358 | }).connect(default_reverb).toDestination();
359 |
360 | var theirLoopSampler = new Tone.Sampler({
361 | urls: {
362 | A1: "A1.mp3",
363 | A2: "A2.mp3",
364 | A3: "A3.mp3",
365 | A4: "A4.mp3",
366 | A5: "A5.mp3",
367 | A6: "A6.mp3",
368 | A7: "A7.mp3",
369 | },
370 | release: 0.6,
371 | baseUrl: "https://tonejs.github.io/audio/salamander/",
372 | }).connect(default_reverb).toDestination();
373 |
374 | var theirSampler = new Tone.Sampler({
375 | urls: {
376 | A1: "A1.mp3",
377 | A2: "A2.mp3",
378 | A3: "A3.mp3",
379 | A4: "A4.mp3",
380 | A5: "A5.mp3",
381 | A6: "A6.mp3",
382 | A7: "A7.mp3",
383 | },
384 | release: 0.6,
385 | baseUrl: "https://tonejs.github.io/audio/salamander/",
386 | }).connect(default_reverb).toDestination();
387 |
388 |
389 | function sendTestMessage() {
390 | if (!dataChannel) {
391 | logError('Connection has not been initiated. ' +
392 | 'Get two peers in the same room first');
393 | return;
394 | } else if (dataChannel.readyState === 'closed') {
395 | logError('Connection was lost. Peer closed the connection.');
396 | return;
397 | }
398 | dataChannel.send("Test message");
399 | sendPing();
400 | }
401 |
402 | function randomToken() {
403 | return Math.floor((1 + Math.random()) * 1e16).toString(16).substring(8);
404 | }
405 |
406 | function logError(err) {
407 | if (!err) return;
408 | if (typeof err === 'string') {
409 | console.warn(err);
410 | } else {
411 | console.warn(err.toString(), err);
412 | }
413 | }
414 |
415 | function playMidi(who, command, byte1, byte2) {
416 | switch (command) {
417 | case 144: // keyDown
418 | if (byte2 > 0) {
419 | keyDown(who, byte1, byte2);
420 | } else {
421 | keyUp(who, byte1);
422 | }
423 | break;
424 | case 128: // keyUp
425 | keyUp(who, byte1);
426 | break;
427 | case 176: // special command
428 | if (byte1 == 64) { // pedal
429 | if (byte2 == 0) {
430 | pedalOff(who);
431 | } else {
432 | pedalOn(who);
433 | }
434 | }
435 | break;
436 | }
437 | if (recording && who == ME) {
438 | addToLoop(command, byte1, byte2);
439 | }
440 | }
441 |
442 | function onSetMySamplerButtonPress() {
443 | //let url = document.getElementById("mysamplerurl").value;
444 |
445 | var dropdown = document.getElementById("dropdownsampler");
446 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
447 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
448 |
449 | let rel = document.getElementById("mysamplerrelease").value;
450 | let gain = document.getElementById("mygain").value;
451 | let decay = document.getElementById("mydecay").value;
452 | changeMySampler(url, rel, gain, decay, samplerValue);
453 | if (peerConnected()) {
454 | dataChannel.send("mySampler " + url + " " + rel + " " + gain + " " + decay + " " + samplerValue);
455 | }
456 | }
457 |
458 | function onSetLoopSamplerButtonPress() {
459 | var dropdown = document.getElementById("loopdropdownsampler");
460 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
461 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
462 |
463 | let rel = document.getElementById("loopsamplerrelease").value;
464 | let gain = document.getElementById("loopgain").value;
465 | let decay = document.getElementById("loopdecay").value;
466 | changeLoopSampler(url, rel, gain, decay, samplerValue);
467 | if (peerConnected()) {
468 | dataChannel.send("theirLoopSampler " + url + " " + rel + " " + gain + " " + decay + " " + samplerValue);
469 | }
470 | }
471 |
472 | function changeMySampler(url, rel, gain, decay, samplerValue) {
473 | console.log("changeMySampler");
474 | fetch(url + 'config.json')
475 | .then(response => response.json())
476 | .then(function (mapping) {
477 | mySampler = new Tone.Sampler({
478 | urls: mapping,
479 | release: rel,
480 | baseUrl: url,
481 | }).toDestination();
482 | if (parseFloat(decay) > 0) {
483 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
484 | mySampler.connect(reverb);
485 | }
486 | myGain = parseFloat(gain);
487 | sound = Sound.from( mySampler, mySampler.context ).analyze(256);
488 | //document.getElementById("mysamplerurl").value = url;
489 | document.getElementById("dropdownsampler").value = samplerValue;
490 | document.getElementById("mysamplerrelease").value = rel;
491 | document.getElementById("mygain").value = gain;
492 | document.getElementById("mydecay").value = decay;
493 | });
494 | }
495 |
496 | function changeLoopSampler(url, rel, gain, decay, samplerValue) {
497 | console.log("changeLoopSampler");
498 | fetch(url + 'config.json')
499 | .then(response => response.json())
500 | .then(function (mapping) {
501 | myLoopSampler = new Tone.Sampler({
502 | urls: mapping,
503 | release: rel,
504 | baseUrl: url,
505 | }).toDestination();
506 | if (parseFloat(decay) > 0) {
507 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
508 | myLoopSampler.connect(reverb);
509 | }
510 | myLoopGain = parseFloat(gain);
511 | document.getElementById("loopdropdownsampler").value = samplerValue;
512 | document.getElementById("loopsamplerrelease").value = rel;
513 | document.getElementById("loopgain").value = gain;
514 | document.getElementById("loopdecay").value = decay;
515 | });
516 | }
517 |
518 | function changeTheirLoopSampler(url, rel, gain, decay) {
519 | console.log("changeTheirLoopSampler");
520 | fetch(url + 'config.json')
521 | .then(response => response.json())
522 | .then(function (mapping) {
523 | theirLoopSampler = new Tone.Sampler({
524 | urls: mapping,
525 | release: rel,
526 | baseUrl: url,
527 | }).toDestination();
528 | if (parseFloat(decay) > 0) {
529 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
530 | theirLoopSampler.connect(reverb);
531 | }
532 | theirLoopGain = parseFloat(gain);
533 | });
534 | }
535 |
536 | function onSetTheirSamplerButtonPress() {
537 | var dropdown = document.getElementById("theirdropdownsampler");
538 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
539 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
540 |
541 | let rel = document.getElementById("theirsamplerrelease").value;
542 | let gain = document.getElementById("theirgain").value;
543 | let decay = document.getElementById("theirdecay").value;
544 | changeTheirSampler(url, rel, gain, decay, samplerValue);
545 | if (peerConnected()) {
546 | dataChannel.send("theirSampler " + url + " " + rel + " " + gain + " " + decay + " " + samplerValue);
547 | }
548 | }
549 |
550 | function changeTheirSampler(url, rel, gain, decay, samplerValue) {
551 | console.log("changeTheirSampler");
552 | fetch(url + 'config.json')
553 | .then(response => response.json())
554 | .then(function (mapping) {
555 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
556 | theirSampler = new Tone.Sampler({
557 | urls: mapping,
558 | release: rel,
559 | baseUrl: url,
560 | }).toDestination();
561 | if (parseFloat(decay) > 0) {
562 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
563 | theirSampler.connect(reverb);
564 | }
565 | theirGain = parseFloat(gain);
566 | sound2 = Sound.from( theirSampler, theirSampler.context ).analyze(256);
567 | document.getElementById("theirdropdownsampler").value = samplerValue;
568 | document.getElementById("theirsamplerrelease").value = rel;
569 | document.getElementById("theirgain").value = gain;
570 | document.getElementById("theirdecay").value = decay;
571 | });
572 | }
573 |
574 | function keyDown(who, midiValue, velocity) {
575 | let note = getNote(midiValue);
576 | if (who === ME) {
577 | myPressedKeys.add(note);
578 | mySampler.triggerAttack(note, Tone.context.currentTime, velocity*myGain/120)
579 | } else if (who === THEM) {
580 | theirPressedKeys.add(note);
581 | theirSampler.triggerAttack(note, Tone.context.currentTime, velocity*theirGain/120)
582 | } else if (who === MY_LOOP) {
583 | myLoopPressedKeys.add(note);
584 | myLoopSampler.triggerAttack(note, Tone.context.currentTime, velocity*myLoopGain/120)
585 | } else if (who === THEIR_LOOP) {
586 | theirLoopPressedKeys.add(note);
587 | theirLoopSampler.triggerAttack(note, Tone.context.currentTime, velocity*theirLoopGain/120)
588 | }
589 | }
590 |
591 | function keyUp(who, midiValue) {
592 | let note = getNote(midiValue);
593 | if (who === ME) {
594 | myPressedKeys.delete(note)
595 | if (!myPedal) {
596 | mySampler.triggerRelease(note, Tone.context.currentTime)
597 | }
598 | } else if (who === THEM) {
599 | theirPressedKeys.delete(note)
600 | if (!theirPedal) {
601 | theirSampler.triggerRelease(note, Tone.context.currentTime)
602 | }
603 | } else if (who === MY_LOOP) {
604 | myLoopPressedKeys.delete(note)
605 | if (!myLoopPedal) {
606 | myLoopSampler.triggerRelease(note, Tone.context.currentTime)
607 | }
608 | } else if (who === THEIR_LOOP) {
609 | theirLoopPressedKeys.delete(note)
610 | if (!theirLoopPedal) {
611 | theirLoopSampler.triggerRelease(note, Tone.context.currentTime)
612 | }
613 | }
614 | }
615 |
616 | function getNote(midiValue) {
617 | let noteLetter = NOTES[midiValue%12];
618 | let octave = Math.floor(midiValue/12)-1;
619 | return noteLetter + octave;
620 | }
621 |
622 | function onMIDIFailure() {
623 | console.log('Could not access your MIDI devices.');
624 | }
625 |
626 | function onMIDISuccess(midiAccess) {
627 | console.log(midiAccess);
628 |
629 | var inputs = midiAccess.inputs;
630 | var outputs = midiAccess.outputs;
631 | var deviceInfoMessage = "List of devices: [";
632 | for (var input of midiAccess.inputs.values()) {
633 | deviceInfoMessage += input.name + ", ";
634 | input.onmidimessage = onMidiMessage;
635 | }
636 | deviceInfoMessage += "]";
637 | if (inputs.size > 0) {
638 | document.getElementById("midi-status").innerHTML = deviceInfoMessage;
639 | document.getElementById("midi-status").style.color = "green";
640 | }
641 | }
642 |
643 | document.addEventListener('keydown', function(event) {
644 | if (event.repeat == true) {
645 | return;
646 | }
647 | if (event.srcElement.localName == "input") {
648 | return;
649 | }
650 | let midiKeyCode = -1;
651 | switch (event.code) {
652 | case "KeyA":
653 | midiKeyCode = 60;
654 | break;
655 | case "KeyW":
656 | midiKeyCode = 61;
657 | break;
658 | case "KeyS":
659 | midiKeyCode = 62;
660 | break;
661 | case "KeyE":
662 | midiKeyCode = 63;
663 | break;
664 | case "KeyD":
665 | midiKeyCode = 64;
666 | break;
667 | case "KeyF":
668 | midiKeyCode = 65;
669 | break;
670 | case "KeyT":
671 | midiKeyCode = 66;
672 | break;
673 | case "KeyG":
674 | midiKeyCode = 67;
675 | break;
676 | case "KeyY":
677 | midiKeyCode = 68;
678 | break;
679 | case "KeyH":
680 | midiKeyCode = 69;
681 | break;
682 | case "KeyU":
683 | midiKeyCode = 70;
684 | break;
685 | case "KeyJ":
686 | midiKeyCode = 71;
687 | break;
688 | case "KeyK":
689 | midiKeyCode = 72;
690 | break;
691 | case "KeyZ":
692 | octave--;
693 | break;
694 | case "KeyX":
695 | octave++;
696 | break;
697 | }
698 | if (midiKeyCode != -1) {
699 | midiKeyCode += octave*12;
700 | playMidi(ME, 144, midiKeyCode, 80);
701 | if (peerConnected()) {
702 | let midiInfo = '144-' + midiKeyCode + '-80';
703 | dataChannel.send(midiInfo);
704 | }
705 | }
706 | });
707 |
708 | document.addEventListener('keyup', function(event) {
709 | if (event.repeat == true) {
710 | return;
711 | }
712 | if (event.srcElement.localName == "input") {
713 | return;
714 | }
715 | let midiKeyCode = -1;
716 | switch (event.code) {
717 | case "KeyA":
718 | midiKeyCode = 60;
719 | break;
720 | case "KeyW":
721 | midiKeyCode = 61;
722 | break;
723 | case "KeyS":
724 | midiKeyCode = 62;
725 | break;
726 | case "KeyE":
727 | midiKeyCode = 63;
728 | break;
729 | case "KeyD":
730 | midiKeyCode = 64;
731 | break;
732 | case "KeyF":
733 | midiKeyCode = 65;
734 | break;
735 | case "KeyT":
736 | midiKeyCode = 66;
737 | break;
738 | case "KeyG":
739 | midiKeyCode = 67;
740 | break;
741 | case "KeyY":
742 | midiKeyCode = 68;
743 | break;
744 | case "KeyH":
745 | midiKeyCode = 69;
746 | break;
747 | case "KeyU":
748 | midiKeyCode = 70;
749 | break;
750 | case "KeyJ":
751 | midiKeyCode = 71;
752 | break;
753 | case "KeyK":
754 | midiKeyCode = 72;
755 | break;
756 | }
757 | if (midiKeyCode != -1) {
758 | midiKeyCode += octave*12;
759 | playMidi(ME, 128, midiKeyCode, 0);
760 | if (peerConnected()) {
761 | let midiInfo = '128-' + midiKeyCode + '-0';
762 | dataChannel.send(midiInfo);
763 | }
764 | }
765 | });
766 |
767 | function onMidiMessage(message) {
768 | var command = message.data[0];
769 | var byte1 = message.data[1];
770 | // a velocity value might not be included with a noteOff command
771 | var byte2 = (message.data.length > 2) ? message.data[2] : 0;
772 |
773 | if (peerConnected()) {
774 | let midiInfo = command + '-' + byte1 + '-' + byte2;
775 | dataChannel.send(midiInfo);
776 | }
777 | playMidi(ME, command, byte1, byte2)
778 | }
779 |
780 | function onLoopMidiMessage(message) {
781 | var command = message.data[0];
782 | var byte1 = message.data[1];
783 | // a velocity value might not be included with a noteOff command
784 | var byte2 = (message.data.length > 2) ? message.data[2] : 0;
785 |
786 | if (peerConnected()) {
787 | let midiInfo = command + '-' + byte1 + '-' + byte2;
788 | dataChannel.send("LOOP-" + midiInfo);
789 | }
790 | playMidi(MY_LOOP, command, byte1, byte2)
791 | }
792 |
793 | function pedalOff(who) {
794 | if (who === ME) {
795 | myPedal = false;
796 | let releaseKeys = getAllKeysWhichArentPressed(who);
797 | mySampler.triggerRelease(releaseKeys, Tone.context.currentTime)
798 | } else if (who === THEM) {
799 | theirPedal = false;
800 | let releaseKeys = getAllKeysWhichArentPressed(who);
801 | theirSampler.triggerRelease(releaseKeys, Tone.context.currentTime)
802 | } else if (who === MY_LOOP) {
803 | myLoopPedal = false;
804 | let releaseKeys = getAllKeysWhichArentPressed(who);
805 | myLoopSampler.triggerRelease(releaseKeys, Tone.context.currentTime)
806 | } else if (who === THEIR_LOOP) {
807 | theirLoopPedal = false;
808 | let releaseKeys = getAllKeysWhichArentPressed(who);
809 | theirLoopSampler.triggerRelease(releaseKeys, Tone.context.currentTime)
810 | }
811 | }
812 |
813 | function pedalOn(who) {
814 | if (who === ME) {
815 | myPedal = true;
816 | } else if (who === THEM) {
817 | theirPedal = true;
818 | } else if (who === MY_LOOP) {
819 | myLoopPedal = true;
820 | } else if (who === THEIR_LOOP) {
821 | theirLoopPedal = true;
822 | }
823 | }
824 |
825 | var ALL_KEYS = []
826 | // A1 to C8
827 | for (let i = 21; i < 108; i++) {
828 | ALL_KEYS.push(getNote(i));
829 | }
830 |
831 | function getAllKeysWhichArentPressed(who) {
832 | if (who === ME) {
833 | let toReturn = [];
834 | for (let i = 0; i < ALL_KEYS.length; i++) {
835 | if (!myPressedKeys.has(ALL_KEYS[i])) {
836 | toReturn.push(ALL_KEYS[i]);
837 | }
838 | }
839 | return toReturn;
840 | } else if (who === THEM) {
841 | let toReturn = [];
842 | for (let i = 0; i < ALL_KEYS.length; i++) {
843 | if (!theirPressedKeys.has(ALL_KEYS[i])) {
844 | toReturn.push(ALL_KEYS[i]);
845 | }
846 | }
847 | return toReturn;
848 | } else if (who === MY_LOOP) {
849 | let toReturn = [];
850 | for (let i = 0; i < ALL_KEYS.length; i++) {
851 | if (!myLoopPressedKeys.has(ALL_KEYS[i])) {
852 | toReturn.push(ALL_KEYS[i]);
853 | }
854 | }
855 | return toReturn;
856 | } else if (who === THEIR_LOOP) {
857 | let toReturn = [];
858 | for (let i = 0; i < ALL_KEYS.length; i++) {
859 | if (!theirLoopPressedKeys.has(ALL_KEYS[i])) {
860 | toReturn.push(ALL_KEYS[i]);
861 | }
862 | }
863 | return toReturn;
864 | }
865 | }
866 |
867 | function addToLoop(command, byte1, byte2) {
868 | if (loopData.length === 0 && command == 128) {
869 | // if first note in loop is key up, assume we missed keydown and add it automatically
870 | loopData.push({
871 | time: 0,
872 | command: 144, // keydown
873 | byte1: byte1,
874 | byte2: 80, // assume 80 velocity
875 | });
876 | }
877 | loopData.push({
878 | time: Date.now() - loopStartTime,
879 | command: command,
880 | byte1: byte1,
881 | byte2: byte2,
882 | });
883 | }
884 |
885 | function playPauseLoop() {
886 | if (recording) {
887 | finishLoop();
888 | }
889 | if (playingLoop == false) {
890 | playingLoop = true;
891 | playLoopOnce();
892 | loopId = setInterval(playLoopOnce, loopLength);
893 | } else {
894 | clearInterval(loopId);
895 | stopPlayingLoop();
896 | playingLoop = false;
897 | }
898 | }
899 |
900 | loopTimeoutIds = []
901 | function playLoopOnce() {
902 | for (let note of loopData) {
903 | noteData = [note.command, note.byte1, note.byte2];
904 | let message = {
905 | data: noteData,
906 | };
907 | loopTimeoutIds.push(setTimeout(onLoopMidiMessage, note.time, message));
908 | }
909 | }
910 |
911 | function stopPlayingLoop() {
912 | for (let id of loopTimeoutIds) {
913 | clearTimeout(id);
914 | }
915 | }
916 |
917 | function beginLoop() {
918 | loopData = [];
919 | console.log("begin loop");
920 | loopStartTime = Date.now();
921 | recording = true;
922 | document.getElementById("startLoopButton").disabled = true;
923 | document.getElementById("stopLoopButton").disabled = false;
924 | document.getElementById("playPauseLoopButton").disabled = false;
925 | // automatically set loop sampler things equal to my sampler
926 | //let url = document.getElementById("mysamplerurl").value;
927 | var dropdown = document.getElementById("dropdownsampler");
928 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
929 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
930 |
931 | let rel = document.getElementById("mysamplerrelease").value;
932 | let gain = document.getElementById("mygain").value;
933 | let decay = document.getElementById("mydecay").value;
934 | changeLoopSampler(url, rel, gain, decay, samplerValue);
935 | if (peerConnected()) {
936 | dataChannel.send("theirLoopSampler " + url + " " + rel + " " + gain + " " + decay);
937 | }
938 | // if pedal is down at start of loop, add to loop
939 | if (myPedal) {
940 | addToLoop(176, 64, 1);
941 | }
942 | }
943 |
944 | function finishLoop() {
945 | console.log("finish loop");
946 | recording = false;
947 | loopLength = Date.now() - loopStartTime;
948 | document.getElementById("startLoopButton").disabled = false;
949 | document.getElementById("stopLoopButton").disabled = true;
950 | document.getElementById("playPauseLoopButton").disabled = false;
951 | }
952 |
953 | var paused = false;
954 | function toggleVisual() {
955 | if (paused) {
956 | space.resume();
957 | space2.resume();
958 | paused = false;
959 | } else {
960 | paused = true;
961 | space.pause();
962 | space2.pause();
963 | }
964 | }
965 |
966 |
967 | /****************************************************************************
968 | * Visualization logic
969 | ****************************************************************************/
970 | Pts.namespace( window );
971 | var space = new CanvasSpace("#pts");
972 | space.setup({ bgcolor: "powderblue" });
973 | var form = space.getForm();
974 |
975 | var sound = Sound.from( mySampler, mySampler.context ).analyze(256);
976 |
977 | space.add({
978 | animate: (time) => {
979 | if (mySampler.context.state === 'suspended') { // mostly for safari
980 | form.fillOnly("#fff").text( [20, 30], "Click anywhere to start" );
981 | }
982 |
983 | var area = space.size;
984 | var idx = space.pointer.$divide( area ).floor();
985 | var rect = [idx.$multiply(area), idx.$multiply(area).add(area)];
986 |
987 | let t1 = sound.timeDomainTo( area, rect[0].$subtract(0, area.y/2) );
988 | let t2 = t1.map( t => t.$add(0, area.y) ).reverse();
989 | let freqs = sound.freqDomainTo( [area.x*2, area.y/2], [rect[0].x, 0] ).map( f => [[f.x, rect[0].y+area.y/2-f.y], [f.x, rect[0].y+area.y/2+f.y]] );
990 |
991 | form.fillOnly("powderblue").polygon( t1.concat(t2) );
992 | form.strokeOnly("white", Math.ceil(area.x/128) ).lines( freqs );
993 | },
994 | action: (type, x, y) => {
995 | if (type === "up") { // for safari
996 | if (mySampler.context.state === 'suspended') {
997 | mySampler.context.resume();
998 | }
999 | }
1000 | }
1001 | });
1002 | space.autoResize = false;
1003 | space.play();
1004 |
1005 | var space2 = new CanvasSpace("#pts2");
1006 | space2.setup({ bgcolor: "#F9E79F" });
1007 | var form2 = space2.getForm();
1008 |
1009 | var sound2 = Sound.from( theirSampler, theirSampler.context ).analyze(256);
1010 |
1011 | space2.add({
1012 | animate: (time) => {
1013 | if (theirSampler.context.state === 'suspended') { // mostly for safari
1014 | form2.fillOnly("#fff").text( [20, 30], "Click anywhere to start" );
1015 | }
1016 |
1017 | var area = space2.size;
1018 | var idx = space2.pointer.$divide( area ).floor();
1019 | var rect = [idx.$multiply(area), idx.$multiply(area).add(area)];
1020 |
1021 | let t1 = sound2.timeDomainTo( area, rect[0].$subtract(0, area.y/2) );
1022 | let t2 = t1.map( t => t.$add(0, area.y) ).reverse();
1023 | let freqs = sound2.freqDomainTo( [area.x*2, area.y/2], [rect[0].x, 0] ).map( f => [[f.x, rect[0].y+area.y/2-f.y], [f.x, rect[0].y+area.y/2+f.y]] );
1024 |
1025 | form2.fillOnly("#F9E79F").polygon( t1.concat(t2) );
1026 | form2.strokeOnly("black", Math.ceil(area.x/128) ).lines( freqs );
1027 | },
1028 | action: (type, x, y) => {
1029 | if (type === "up") { // for safari
1030 | if (theirSampler.context.state === 'suspended') {
1031 | theirSampler.context.resume();
1032 | }
1033 | }
1034 | }
1035 | });
1036 | space2.play();
1037 |
1038 | function acceptSound() {
1039 | document.cookie = "cookie_soundon=true";
1040 | document.querySelector('.sound-overlay').classList.add('d-none');
1041 | Tone.start();
1042 | }
1043 |
1044 | var diagnosticsOpen = false;
1045 | function toggleDiagnostics() {
1046 | if (diagnosticsOpen) {
1047 | diagnosticsOpen = false;
1048 | document.querySelector('.diagnostics').classList.add('d-none');
1049 | } else {
1050 | diagnosticsOpen = true;
1051 | document.querySelector('.diagnostics').classList.remove('d-none');
1052 | }
1053 | }
1054 |
--------------------------------------------------------------------------------
/public/js/main_backwards.js:
--------------------------------------------------------------------------------
1 | /***************************************************************************r
2 | * *
3 | * Initial setup
4 | ****************************************************************************/
5 |
6 | var configuration = {
7 | 'iceServers': [{
8 | 'urls': 'stun:stun1.l.google.com:19302'
9 | }]
10 | };
11 |
12 | var sendBtn = document.getElementById('send');
13 |
14 | // Attach event handlers
15 | sendBtn.addEventListener('click', sendTestMessage);
16 |
17 | // Disable send buttons by default.
18 | sendBtn.disabled = true;
19 |
20 | // Create a random room if not already present in the URL.
21 | var isInitiator;
22 |
23 | var room = window.location.hash.substring(1);
24 | if (!room) {
25 | room = window.location.hash = randomToken();
26 | }
27 |
28 | /****************************************************************************
29 | * Signaling server
30 | ****************************************************************************/
31 |
32 | // Connect to the signaling server
33 | var socket = io.connect();
34 |
35 | socket.on('created', function(room, clientId) {
36 | console.log('Created room', room, '- my client ID is', clientId);
37 | isInitiator = true;
38 | });
39 |
40 | socket.on('joined', function(room, clientId) {
41 | console.log('This peer has joined room', room, 'with client ID', clientId);
42 | isInitiator = false;
43 | createPeerConnection(isInitiator, configuration);
44 | });
45 |
46 | socket.on('ready', function() {
47 | console.log('Socket is ready');
48 | createPeerConnection(isInitiator, configuration);
49 | });
50 |
51 | socket.on('full', function(room) {
52 | alert('Room ' + room + ' is full. Try again later.');
53 | });
54 |
55 | socket.on('message', function(message) {
56 | //console.log('Client received message:', message);
57 | signalingMessageCallback(message);
58 | });
59 |
60 | // Joining a room.
61 | socket.emit('create or join', room);
62 |
63 | socket.on('bye', function(room) {
64 | console.log(`Peer leaving room ${room}.`);
65 | sendBtn.disabled = true;
66 | document.getElementById("peer-status").innerHTML = "Lost connection from partner.";
67 | document.getElementById("peer-status").style.color = "#000";
68 | document.getElementById("ping").innerHTML = "";
69 | document.getElementById("ping2").innerHTML = "";
70 | // If peer did not create the room, re-enter to be creator.
71 | if (!isInitiator) {
72 | window.location.reload();
73 | }
74 | });
75 |
76 | window.addEventListener('unload', function() {
77 | console.log(`Unloading window. Notifying peers in ${room}.`);
78 | socket.emit('bye', room);
79 | });
80 |
81 | window.addEventListener('load', function() {
82 | if (document.cookie.indexOf("cookie_soundon=") < 0) {
83 | document.querySelector('.sound-overlay').classList.remove('d-none');
84 | }
85 | });
86 |
87 | /**
88 | * Send message to signaling server
89 | */
90 | function sendMessageToServer(message) {
91 | //console.log('Client sending message to server:', message, ' room:', room);
92 | socket.emit('message', { m: message, r: room })
93 | }
94 |
95 | /****************************************************************************
96 | * WebRTC peer connection and data channel
97 | ****************************************************************************/
98 |
99 | var peerConn;
100 | var dataChannel;
101 |
102 | function signalingMessageCallback(message) {
103 | if (message == null) {
104 | console.log("signalingMessageCallback is ignoring null message");
105 | return;
106 | }
107 | if (message.type === 'offer') {
108 | console.log('Got offer. Sending answer to peer.');
109 | peerConn.setRemoteDescription(new RTCSessionDescription(message), function() {},
110 | logError);
111 | peerConn.createAnswer(onLocalSessionCreated, logError);
112 |
113 | } else if (message.type === 'answer') {
114 | console.log('Got answer.');
115 | peerConn.setRemoteDescription(new RTCSessionDescription(message), function() {},
116 | logError);
117 |
118 | } else if (message.type === 'candidate') {
119 | peerConn.addIceCandidate(new RTCIceCandidate({
120 | candidate: message.candidate,
121 | sdpMLineIndex: message.label,
122 | sdpMid: message.id
123 | }));
124 |
125 | }
126 | }
127 |
128 | function createPeerConnection(isInitiator, config) {
129 | console.log('Creating Peer connection as initiator?', isInitiator, 'config:',
130 | config);
131 | peerConn = new RTCPeerConnection(config);
132 |
133 | // send any ice candidates to the other peer
134 | peerConn.onicecandidate = function(event) {
135 | //console.log('icecandidate event:', event);
136 | if (event.candidate) {
137 | sendMessageToServer({
138 | type: 'candidate',
139 | label: event.candidate.sdpMLineIndex,
140 | id: event.candidate.sdpMid,
141 | candidate: event.candidate.candidate
142 | });
143 | } else {
144 | console.log('End of candidates.');
145 | }
146 | };
147 |
148 | if (isInitiator) {
149 | console.log('Creating Data Channel');
150 | dataChannel = peerConn.createDataChannel('midi-data');
151 | onDataChannelCreated(dataChannel);
152 |
153 | console.log('Creating an offer');
154 | peerConn.createOffer()
155 | .then(function(offer) {
156 | return peerConn.setLocalDescription(offer);
157 | })
158 | .then(() => {
159 | console.log('sending local desc:', peerConn.localDescription);
160 | sendMessageToServer(peerConn.localDescription);
161 | })
162 | .catch(logError);
163 |
164 | } else {
165 | peerConn.ondatachannel = function(event) {
166 | console.log('ondatachannel:', event.channel);
167 | dataChannel = event.channel;
168 | onDataChannelCreated(dataChannel);
169 | };
170 | }
171 | }
172 |
173 | function onLocalSessionCreated(desc) {
174 | console.log('local session created:', desc);
175 | peerConn.setLocalDescription(desc).then(function() {
176 | console.log('sending local desc:', peerConn.localDescription);
177 | sendMessageToServer(peerConn.localDescription);
178 | }).catch(logError);
179 | }
180 |
181 | function peerConnected() {
182 | return dataChannel && peerConn.connectionState == "connected";
183 | }
184 |
185 | var pingTime = 0;
186 |
187 | function sendPing() {
188 | if (peerConnected()) {
189 | dataChannel.send('ping');
190 | pingTime = Date.now();
191 | } else {
192 | }
193 | }
194 |
195 | function onDataChannelCreated(channel) {
196 | console.log('onDataChannelCreated:', channel);
197 |
198 | channel.onopen = function() {
199 | console.log('CHANNEL opened!!!');
200 | document.getElementById("peer-status").innerHTML = "Partner connected.";
201 | document.getElementById("peer-status").style.color = "green";
202 | sendPing();
203 | window.setInterval(sendPing, 1000);
204 | sendBtn.disabled = false;
205 | // when connecting, send sampler info in case player changed already
206 | onSetMySamplerButtonPress();
207 | onSetLoopSamplerButtonPress();
208 | };
209 |
210 | channel.onclose = function () {
211 | console.log('Channel closed.');
212 | sendBtn.disabled = true;
213 | document.getElementById("peer-status").innerHTML = "Lost connection from partner.";
214 | document.getElementById("peer-status").style.color = "#000";
215 | document.getElementById("ping").innerHTML = "";
216 | document.getElementById("ping2").innerHTML = "";
217 | }
218 |
219 | channel.onmessage = function onmessage(event) {
220 | if (typeof event.data === 'string') {
221 | if (event.data == "ping") {
222 | dataChannel.send("pong");
223 | return;
224 | }
225 | if (event.data == "pong") {
226 | let ping = Date.now() - pingTime;
227 | document.getElementById("ping").innerHTML = ping;
228 | document.getElementById("ping2").innerHTML = Math.floor(ping/2);
229 | return;
230 | }
231 | if (event.data.substring(0, 9) == "mySampler") {
232 | let samplerData = event.data.split(' ');
233 | changeTheirSampler(samplerData[1], samplerData[2], samplerData[3], samplerData[4], samplerData[5]);
234 | console.log(event.data);
235 | return;
236 | }
237 | // only you can set your own loop, so we don't need to listen for myLoopSampler
238 | if (event.data.substring(0, 16) == "theirLoopSampler") {
239 | let samplerData = event.data.split(' ');
240 | changeTheirLoopSampler(samplerData[1], samplerData[2], samplerData[3], samplerData[4], samplerData[5]);
241 | console.log(event.data);
242 | return;
243 | }
244 | if (event.data.substring(0, 12) == "theirSampler") {
245 | let samplerData = event.data.split(' ');
246 | changeMySampler(samplerData[1], samplerData[2], samplerData[3], samplerData[4], samplerData[5]);
247 | console.log(event.data);
248 | return;
249 | }
250 | console.log(event.data, Date.now());
251 | let midiData = event.data.split('-');
252 | if (midiData.length == 3) {
253 | // looks like midi data to me, lets just try to play it
254 | playMidi(THEM, parseInt(midiData[0]), parseInt(midiData[1]), parseInt(midiData[2]));
255 | }
256 | if (midiData.length == 4 && midiData[0] == "LOOP") {
257 | // loop midi data
258 | playMidi(THEIR_LOOP, parseInt(midiData[1]), parseInt(midiData[2]), parseInt(midiData[3]));
259 | }
260 | return;
261 | }
262 | };
263 | }
264 |
265 | function playTheirMidi(command, byte1, byte2) {
266 | }
267 |
268 | /****************************************************************************
269 | * MIDI things
270 | ****************************************************************************/
271 |
272 | var NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
273 |
274 | // pedal for both samplers
275 | var myPedal = false;
276 | var theirPedal = false;
277 | var myLoopPedal = false;
278 | var theirLoopPedal = false;
279 |
280 | // currently pressed keys for both samplers (used when releasing pedal)
281 | var myPressedKeys = new Set()
282 | var theirPressedKeys = new Set()
283 | var myLoopPressedKeys = new Set()
284 | var theirLoopPressedKeys = new Set()
285 |
286 | const ME = 0;
287 | const THEM = 1;
288 | const MY_LOOP = 2;
289 | const THEIR_LOOP = 3;
290 |
291 | // gain for both samplers
292 | var myGain = 1.0;
293 | var theirGain = 1.0;
294 | var myLoopGain = 1.0;
295 | var theirLoopGain = 1.0;
296 |
297 | Tone.context.latencyHint = "fastest";
298 |
299 | // octave for computer keyboard entry
300 | var octave = 0;
301 |
302 | // local loop data
303 | document.getElementById("startLoopButton").disabled = false;
304 | document.getElementById("stopLoopButton").disabled = true;
305 | document.getElementById("playPauseLoopButton").disabled = true;
306 | var loopStartTime;
307 | var loopLength;
308 | var recording = false;
309 | var loopData = [];
310 | var playingLoop = false;
311 | var loopId;
312 |
313 | if (navigator.requestMIDIAccess) {
314 | console.log('This browser supports WebMIDI!');
315 | document.getElementById("browser-status").innerHTML = "Browser supports MIDI";
316 | document.getElementById("browser-status").style.color = "green";
317 | } else {
318 | console.log('WebMIDI is not supported in this browser.');
319 | document.getElementById("browser-status").innerHTML = "No browser support for MIDI. Consider trying Chrome or Edge";
320 | document.getElementById("browser-status").style.color = "red";
321 | }
322 |
323 | try {
324 | navigator.requestMIDIAccess()
325 | .then(onMIDISuccess, onMIDIFailure);
326 | } catch (e) {
327 | console.log(e);
328 | }
329 |
330 | const default_reverb = new Tone.Reverb(1.5).toDestination();
331 |
332 | var mySampler = new Tone.Sampler({
333 | urls: {
334 | A1: "A1.mp3",
335 | A2: "A2.mp3",
336 | A3: "A3.mp3",
337 | A4: "A4.mp3",
338 | A5: "A5.mp3",
339 | A6: "A6.mp3",
340 | A7: "A7.mp3",
341 | },
342 | release: 0.6,
343 | baseUrl: "https://tonejs.github.io/audio/salamander/",
344 | }).connect(default_reverb).toDestination();
345 |
346 | var myLoopSampler = new Tone.Sampler({
347 | urls: {
348 | A1: "A1.mp3",
349 | A2: "A2.mp3",
350 | A3: "A3.mp3",
351 | A4: "A4.mp3",
352 | A5: "A5.mp3",
353 | A6: "A6.mp3",
354 | A7: "A7.mp3",
355 | },
356 | release: 0.6,
357 | baseUrl: "https://tonejs.github.io/audio/salamander/",
358 | }).connect(default_reverb).toDestination();
359 |
360 | var theirLoopSampler = new Tone.Sampler({
361 | urls: {
362 | A1: "A1.mp3",
363 | A2: "A2.mp3",
364 | A3: "A3.mp3",
365 | A4: "A4.mp3",
366 | A5: "A5.mp3",
367 | A6: "A6.mp3",
368 | A7: "A7.mp3",
369 | },
370 | release: 0.6,
371 | baseUrl: "https://tonejs.github.io/audio/salamander/",
372 | }).connect(default_reverb).toDestination();
373 |
374 | var theirSampler = new Tone.Sampler({
375 | urls: {
376 | A1: "A1.mp3",
377 | A2: "A2.mp3",
378 | A3: "A3.mp3",
379 | A4: "A4.mp3",
380 | A5: "A5.mp3",
381 | A6: "A6.mp3",
382 | A7: "A7.mp3",
383 | },
384 | release: 0.6,
385 | baseUrl: "https://tonejs.github.io/audio/salamander/",
386 | }).connect(default_reverb).toDestination();
387 |
388 |
389 | function sendTestMessage() {
390 | if (!dataChannel) {
391 | logError('Connection has not been initiated. ' +
392 | 'Get two peers in the same room first');
393 | return;
394 | } else if (dataChannel.readyState === 'closed') {
395 | logError('Connection was lost. Peer closed the connection.');
396 | return;
397 | }
398 | dataChannel.send("Test message");
399 | sendPing();
400 | }
401 |
402 | function randomToken() {
403 | return Math.floor((1 + Math.random()) * 1e16).toString(16).substring(8);
404 | }
405 |
406 | function logError(err) {
407 | if (!err) return;
408 | if (typeof err === 'string') {
409 | console.warn(err);
410 | } else {
411 | console.warn(err.toString(), err);
412 | }
413 | }
414 |
415 | function playMidi(who, command, byte1, byte2) {
416 | switch (command) {
417 | case 144: // keyDown
418 | if (byte2 > 0) {
419 | keyDown(who, byte1, byte2);
420 | } else {
421 | keyUp(who, byte1);
422 | }
423 | break;
424 | case 128: // keyUp
425 | keyUp(who, byte1);
426 | break;
427 | case 176: // special command
428 | if (byte1 == 64) { // pedal
429 | if (byte2 == 0) {
430 | pedalOff(who);
431 | } else {
432 | pedalOn(who);
433 | }
434 | }
435 | break;
436 | }
437 | if (recording && who == ME) {
438 | addToLoop(command, byte1, byte2);
439 | }
440 | }
441 |
442 | function onSetMySamplerButtonPress() {
443 | //let url = document.getElementById("mysamplerurl").value;
444 |
445 | var dropdown = document.getElementById("dropdownsampler");
446 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
447 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
448 |
449 | let rel = document.getElementById("mysamplerrelease").value;
450 | let gain = document.getElementById("mygain").value;
451 | let decay = document.getElementById("mydecay").value;
452 | changeMySampler(url, rel, gain, decay, samplerValue);
453 | if (peerConnected()) {
454 | dataChannel.send("mySampler " + url + " " + rel + " " + gain + " " + decay + " " + samplerValue);
455 | }
456 | }
457 |
458 | function onSetLoopSamplerButtonPress() {
459 | var dropdown = document.getElementById("loopdropdownsampler");
460 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
461 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
462 |
463 | let rel = document.getElementById("loopsamplerrelease").value;
464 | let gain = document.getElementById("loopgain").value;
465 | let decay = document.getElementById("loopdecay").value;
466 | changeLoopSampler(url, rel, gain, decay, samplerValue);
467 | if (peerConnected()) {
468 | dataChannel.send("theirLoopSampler " + url + " " + rel + " " + gain + " " + decay + " " + samplerValue);
469 | }
470 | }
471 |
472 | function changeMySampler(url, rel, gain, decay, samplerValue) {
473 | console.log("changeMySampler");
474 | fetch(url + 'config.json')
475 | .then(response => response.json())
476 | .then(function (mapping) {
477 | mySampler = new Tone.Sampler({
478 | urls: mapping,
479 | release: rel,
480 | baseUrl: url,
481 | }).toDestination();
482 | if (parseFloat(decay) > 0) {
483 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
484 | mySampler.connect(reverb);
485 | }
486 | myGain = parseFloat(gain);
487 | sound = Sound.from( mySampler, mySampler.context ).analyze(256);
488 | //document.getElementById("mysamplerurl").value = url;
489 | document.getElementById("dropdownsampler").value = samplerValue;
490 | document.getElementById("mysamplerrelease").value = rel;
491 | document.getElementById("mygain").value = gain;
492 | document.getElementById("mydecay").value = decay;
493 | });
494 | }
495 |
496 | function changeLoopSampler(url, rel, gain, decay, samplerValue) {
497 | console.log("changeLoopSampler");
498 | fetch(url + 'config.json')
499 | .then(response => response.json())
500 | .then(function (mapping) {
501 | myLoopSampler = new Tone.Sampler({
502 | urls: mapping,
503 | release: rel,
504 | baseUrl: url,
505 | }).toDestination();
506 | if (parseFloat(decay) > 0) {
507 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
508 | myLoopSampler.connect(reverb);
509 | }
510 | myLoopGain = parseFloat(gain);
511 | document.getElementById("loopdropdownsampler").value = samplerValue;
512 | document.getElementById("loopsamplerrelease").value = rel;
513 | document.getElementById("loopgain").value = gain;
514 | document.getElementById("loopdecay").value = decay;
515 | });
516 | }
517 |
518 | function changeTheirLoopSampler(url, rel, gain, decay) {
519 | console.log("changeTheirLoopSampler");
520 | fetch(url + 'config.json')
521 | .then(response => response.json())
522 | .then(function (mapping) {
523 | theirLoopSampler = new Tone.Sampler({
524 | urls: mapping,
525 | release: rel,
526 | baseUrl: url,
527 | }).toDestination();
528 | if (parseFloat(decay) > 0) {
529 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
530 | theirLoopSampler.connect(reverb);
531 | }
532 | theirLoopGain = parseFloat(gain);
533 | });
534 | }
535 |
536 | function onSetTheirSamplerButtonPress() {
537 | var dropdown = document.getElementById("theirdropdownsampler");
538 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
539 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
540 |
541 | let rel = document.getElementById("theirsamplerrelease").value;
542 | let gain = document.getElementById("theirgain").value;
543 | let decay = document.getElementById("theirdecay").value;
544 | changeTheirSampler(url, rel, gain, decay, samplerValue);
545 | if (peerConnected()) {
546 | dataChannel.send("theirSampler " + url + " " + rel + " " + gain + " " + decay + " " + samplerValue);
547 | }
548 | }
549 |
550 | function changeTheirSampler(url, rel, gain, decay, samplerValue) {
551 | console.log("changeTheirSampler");
552 | fetch(url + 'config.json')
553 | .then(response => response.json())
554 | .then(function (mapping) {
555 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
556 | theirSampler = new Tone.Sampler({
557 | urls: mapping,
558 | release: rel,
559 | baseUrl: url,
560 | }).toDestination();
561 | if (parseFloat(decay) > 0) {
562 | let reverb = new Tone.Reverb(parseFloat(decay)).toDestination();
563 | theirSampler.connect(reverb);
564 | }
565 | theirGain = parseFloat(gain);
566 | sound2 = Sound.from( theirSampler, theirSampler.context ).analyze(256);
567 | document.getElementById("theirdropdownsampler").value = samplerValue;
568 | document.getElementById("theirsamplerrelease").value = rel;
569 | document.getElementById("theirgain").value = gain;
570 | document.getElementById("theirdecay").value = decay;
571 | });
572 | }
573 |
574 | function keyDown(who, midiValue, velocity) {
575 | let note = getNote(midiValue);
576 | if (who === ME) {
577 | myPressedKeys.add(note);
578 | mySampler.triggerAttack(note, Tone.context.currentTime, velocity*myGain/120)
579 | } else if (who === THEM) {
580 | theirPressedKeys.add(note);
581 | theirSampler.triggerAttack(note, Tone.context.currentTime, velocity*theirGain/120)
582 | } else if (who === MY_LOOP) {
583 | myLoopPressedKeys.add(note);
584 | myLoopSampler.triggerAttack(note, Tone.context.currentTime, velocity*myLoopGain/120)
585 | } else if (who === THEIR_LOOP) {
586 | theirLoopPressedKeys.add(note);
587 | theirLoopSampler.triggerAttack(note, Tone.context.currentTime, velocity*theirLoopGain/120)
588 | }
589 | }
590 |
591 | function keyUp(who, midiValue) {
592 | let note = getNote(midiValue);
593 | if (who === ME) {
594 | myPressedKeys.delete(note)
595 | if (!myPedal) {
596 | mySampler.triggerRelease(note, Tone.context.currentTime)
597 | }
598 | } else if (who === THEM) {
599 | theirPressedKeys.delete(note)
600 | if (!theirPedal) {
601 | theirSampler.triggerRelease(note, Tone.context.currentTime)
602 | }
603 | } else if (who === MY_LOOP) {
604 | myLoopPressedKeys.delete(note)
605 | if (!myLoopPedal) {
606 | myLoopSampler.triggerRelease(note, Tone.context.currentTime)
607 | }
608 | } else if (who === THEIR_LOOP) {
609 | theirLoopPressedKeys.delete(note)
610 | if (!theirLoopPedal) {
611 | theirLoopSampler.triggerRelease(note, Tone.context.currentTime)
612 | }
613 | }
614 | }
615 |
616 | function getNote(midiValue) {
617 | //let noteLetter = NOTES[midiValue%12];
618 | //let octave = Math.floor(midiValue/12)-1;
619 | //return noteLetter + octave;
620 | let flippedMidiValue = 128 - midiValue - 4;
621 | let noteLetter = NOTES[flippedMidiValue%12];
622 | let octave = Math.floor(flippedMidiValue/12)-1;
623 | return noteLetter + octave;
624 |
625 | }
626 |
627 | function onMIDIFailure() {
628 | console.log('Could not access your MIDI devices.');
629 | }
630 |
631 | function onMIDISuccess(midiAccess) {
632 | console.log(midiAccess);
633 |
634 | var inputs = midiAccess.inputs;
635 | var outputs = midiAccess.outputs;
636 | var deviceInfoMessage = "List of devices: [";
637 | for (var input of midiAccess.inputs.values()) {
638 | deviceInfoMessage += input.name + ", ";
639 | input.onmidimessage = onMidiMessage;
640 | }
641 | deviceInfoMessage += "]";
642 | if (inputs.size > 0) {
643 | document.getElementById("midi-status").innerHTML = deviceInfoMessage;
644 | document.getElementById("midi-status").style.color = "green";
645 | }
646 | }
647 |
648 | document.addEventListener('keydown', function(event) {
649 | if (event.repeat == true) {
650 | return;
651 | }
652 | if (event.srcElement.localName == "input") {
653 | return;
654 | }
655 | let midiKeyCode = -1;
656 | switch (event.code) {
657 | case "KeyA":
658 | midiKeyCode = 60;
659 | break;
660 | case "KeyW":
661 | midiKeyCode = 61;
662 | break;
663 | case "KeyS":
664 | midiKeyCode = 62;
665 | break;
666 | case "KeyE":
667 | midiKeyCode = 63;
668 | break;
669 | case "KeyD":
670 | midiKeyCode = 64;
671 | break;
672 | case "KeyF":
673 | midiKeyCode = 65;
674 | break;
675 | case "KeyT":
676 | midiKeyCode = 66;
677 | break;
678 | case "KeyG":
679 | midiKeyCode = 67;
680 | break;
681 | case "KeyY":
682 | midiKeyCode = 68;
683 | break;
684 | case "KeyH":
685 | midiKeyCode = 69;
686 | break;
687 | case "KeyU":
688 | midiKeyCode = 70;
689 | break;
690 | case "KeyJ":
691 | midiKeyCode = 71;
692 | break;
693 | case "KeyK":
694 | midiKeyCode = 72;
695 | break;
696 | case "KeyZ":
697 | octave--;
698 | break;
699 | case "KeyX":
700 | octave++;
701 | break;
702 | }
703 | if (midiKeyCode != -1) {
704 | midiKeyCode += octave*12;
705 | playMidi(ME, 144, midiKeyCode, 80);
706 | if (peerConnected()) {
707 | let midiInfo = '144-' + midiKeyCode + '-80';
708 | dataChannel.send(midiInfo);
709 | }
710 | }
711 | });
712 |
713 | document.addEventListener('keyup', function(event) {
714 | if (event.repeat == true) {
715 | return;
716 | }
717 | if (event.srcElement.localName == "input") {
718 | return;
719 | }
720 | let midiKeyCode = -1;
721 | switch (event.code) {
722 | case "KeyA":
723 | midiKeyCode = 60;
724 | break;
725 | case "KeyW":
726 | midiKeyCode = 61;
727 | break;
728 | case "KeyS":
729 | midiKeyCode = 62;
730 | break;
731 | case "KeyE":
732 | midiKeyCode = 63;
733 | break;
734 | case "KeyD":
735 | midiKeyCode = 64;
736 | break;
737 | case "KeyF":
738 | midiKeyCode = 65;
739 | break;
740 | case "KeyT":
741 | midiKeyCode = 66;
742 | break;
743 | case "KeyG":
744 | midiKeyCode = 67;
745 | break;
746 | case "KeyY":
747 | midiKeyCode = 68;
748 | break;
749 | case "KeyH":
750 | midiKeyCode = 69;
751 | break;
752 | case "KeyU":
753 | midiKeyCode = 70;
754 | break;
755 | case "KeyJ":
756 | midiKeyCode = 71;
757 | break;
758 | case "KeyK":
759 | midiKeyCode = 72;
760 | break;
761 | }
762 | if (midiKeyCode != -1) {
763 | midiKeyCode += octave*12;
764 | playMidi(ME, 128, midiKeyCode, 0);
765 | if (peerConnected()) {
766 | let midiInfo = '128-' + midiKeyCode + '-0';
767 | dataChannel.send(midiInfo);
768 | }
769 | }
770 | });
771 |
772 | function onMidiMessage(message) {
773 | var command = message.data[0];
774 | var byte1 = message.data[1];
775 | // a velocity value might not be included with a noteOff command
776 | var byte2 = (message.data.length > 2) ? message.data[2] : 0;
777 |
778 | if (peerConnected()) {
779 | let midiInfo = command + '-' + byte1 + '-' + byte2;
780 | dataChannel.send(midiInfo);
781 | }
782 | playMidi(ME, command, byte1, byte2)
783 | }
784 |
785 | function onLoopMidiMessage(message) {
786 | var command = message.data[0];
787 | var byte1 = message.data[1];
788 | // a velocity value might not be included with a noteOff command
789 | var byte2 = (message.data.length > 2) ? message.data[2] : 0;
790 |
791 | if (peerConnected()) {
792 | let midiInfo = command + '-' + byte1 + '-' + byte2;
793 | dataChannel.send("LOOP-" + midiInfo);
794 | }
795 | playMidi(MY_LOOP, command, byte1, byte2)
796 | }
797 |
798 | function pedalOff(who) {
799 | if (who === ME) {
800 | myPedal = false;
801 | let releaseKeys = getAllKeysWhichArentPressed(who);
802 | mySampler.triggerRelease(releaseKeys, Tone.context.currentTime)
803 | } else if (who === THEM) {
804 | theirPedal = false;
805 | let releaseKeys = getAllKeysWhichArentPressed(who);
806 | theirSampler.triggerRelease(releaseKeys, Tone.context.currentTime)
807 | } else if (who === MY_LOOP) {
808 | myLoopPedal = false;
809 | let releaseKeys = getAllKeysWhichArentPressed(who);
810 | myLoopSampler.triggerRelease(releaseKeys, Tone.context.currentTime)
811 | } else if (who === THEIR_LOOP) {
812 | theirLoopPedal = false;
813 | let releaseKeys = getAllKeysWhichArentPressed(who);
814 | theirLoopSampler.triggerRelease(releaseKeys, Tone.context.currentTime)
815 | }
816 | }
817 |
818 | function pedalOn(who) {
819 | if (who === ME) {
820 | myPedal = true;
821 | } else if (who === THEM) {
822 | theirPedal = true;
823 | } else if (who === MY_LOOP) {
824 | myLoopPedal = true;
825 | } else if (who === THEIR_LOOP) {
826 | theirLoopPedal = true;
827 | }
828 | }
829 |
830 | var ALL_KEYS = []
831 | // A1 to C8
832 | for (let i = 21; i < 108; i++) {
833 | ALL_KEYS.push(getNote(i));
834 | }
835 |
836 | function getAllKeysWhichArentPressed(who) {
837 | if (who === ME) {
838 | let toReturn = [];
839 | for (let i = 0; i < ALL_KEYS.length; i++) {
840 | if (!myPressedKeys.has(ALL_KEYS[i])) {
841 | toReturn.push(ALL_KEYS[i]);
842 | }
843 | }
844 | return toReturn;
845 | } else if (who === THEM) {
846 | let toReturn = [];
847 | for (let i = 0; i < ALL_KEYS.length; i++) {
848 | if (!theirPressedKeys.has(ALL_KEYS[i])) {
849 | toReturn.push(ALL_KEYS[i]);
850 | }
851 | }
852 | return toReturn;
853 | } else if (who === MY_LOOP) {
854 | let toReturn = [];
855 | for (let i = 0; i < ALL_KEYS.length; i++) {
856 | if (!myLoopPressedKeys.has(ALL_KEYS[i])) {
857 | toReturn.push(ALL_KEYS[i]);
858 | }
859 | }
860 | return toReturn;
861 | } else if (who === THEIR_LOOP) {
862 | let toReturn = [];
863 | for (let i = 0; i < ALL_KEYS.length; i++) {
864 | if (!theirLoopPressedKeys.has(ALL_KEYS[i])) {
865 | toReturn.push(ALL_KEYS[i]);
866 | }
867 | }
868 | return toReturn;
869 | }
870 | }
871 |
872 | function addToLoop(command, byte1, byte2) {
873 | if (loopData.length === 0 && command == 128) {
874 | // if first note in loop is key up, assume we missed keydown and add it automatically
875 | loopData.push({
876 | time: 0,
877 | command: 144, // keydown
878 | byte1: byte1,
879 | byte2: 80, // assume 80 velocity
880 | });
881 | }
882 | loopData.push({
883 | time: Date.now() - loopStartTime,
884 | command: command,
885 | byte1: byte1,
886 | byte2: byte2,
887 | });
888 | }
889 |
890 | function playPauseLoop() {
891 | if (recording) {
892 | finishLoop();
893 | }
894 | if (playingLoop == false) {
895 | playingLoop = true;
896 | playLoopOnce();
897 | loopId = setInterval(playLoopOnce, loopLength);
898 | } else {
899 | clearInterval(loopId);
900 | stopPlayingLoop();
901 | playingLoop = false;
902 | }
903 | }
904 |
905 | loopTimeoutIds = []
906 | function playLoopOnce() {
907 | for (let note of loopData) {
908 | noteData = [note.command, note.byte1, note.byte2];
909 | let message = {
910 | data: noteData,
911 | };
912 | loopTimeoutIds.push(setTimeout(onLoopMidiMessage, note.time, message));
913 | }
914 | }
915 |
916 | function stopPlayingLoop() {
917 | for (let id of loopTimeoutIds) {
918 | clearTimeout(id);
919 | }
920 | }
921 |
922 | function beginLoop() {
923 | loopData = [];
924 | console.log("begin loop");
925 | loopStartTime = Date.now();
926 | recording = true;
927 | document.getElementById("startLoopButton").disabled = true;
928 | document.getElementById("stopLoopButton").disabled = false;
929 | document.getElementById("playPauseLoopButton").disabled = false;
930 | // automatically set loop sampler things equal to my sampler
931 | //let url = document.getElementById("mysamplerurl").value;
932 | var dropdown = document.getElementById("dropdownsampler");
933 | var samplerValue = dropdown.options[dropdown.selectedIndex].value;
934 | var url = "https://jminjie.github.io/samples/" + samplerValue + '/';
935 |
936 | let rel = document.getElementById("mysamplerrelease").value;
937 | let gain = document.getElementById("mygain").value;
938 | let decay = document.getElementById("mydecay").value;
939 | changeLoopSampler(url, rel, gain, decay, samplerValue);
940 | if (peerConnected()) {
941 | dataChannel.send("theirLoopSampler " + url + " " + rel + " " + gain + " " + decay);
942 | }
943 | // if pedal is down at start of loop, add to loop
944 | if (myPedal) {
945 | addToLoop(176, 64, 1);
946 | }
947 | }
948 |
949 | function finishLoop() {
950 | console.log("finish loop");
951 | recording = false;
952 | loopLength = Date.now() - loopStartTime;
953 | document.getElementById("startLoopButton").disabled = false;
954 | document.getElementById("stopLoopButton").disabled = true;
955 | document.getElementById("playPauseLoopButton").disabled = false;
956 | }
957 |
958 | var paused = false;
959 | function toggleVisual() {
960 | if (paused) {
961 | space.resume();
962 | space2.resume();
963 | paused = false;
964 | } else {
965 | paused = true;
966 | space.pause();
967 | space2.pause();
968 | }
969 | }
970 |
971 |
972 | /****************************************************************************
973 | * Visualization logic
974 | ****************************************************************************/
975 | Pts.namespace( window );
976 | var space = new CanvasSpace("#pts");
977 | space.setup({ bgcolor: "powderblue" });
978 | var form = space.getForm();
979 |
980 | var sound = Sound.from( mySampler, mySampler.context ).analyze(256);
981 |
982 | space.add({
983 | animate: (time) => {
984 | if (mySampler.context.state === 'suspended') { // mostly for safari
985 | form.fillOnly("#fff").text( [20, 30], "Click anywhere to start" );
986 | }
987 |
988 | var area = space.size;
989 | var idx = space.pointer.$divide( area ).floor();
990 | var rect = [idx.$multiply(area), idx.$multiply(area).add(area)];
991 |
992 | let t1 = sound.timeDomainTo( area, rect[0].$subtract(0, area.y/2) );
993 | let t2 = t1.map( t => t.$add(0, area.y) ).reverse();
994 | let freqs = sound.freqDomainTo( [area.x*2, area.y/2], [rect[0].x, 0] ).map( f => [[f.x, rect[0].y+area.y/2-f.y], [f.x, rect[0].y+area.y/2+f.y]] );
995 |
996 | form.fillOnly("powderblue").polygon( t1.concat(t2) );
997 | form.strokeOnly("white", Math.ceil(area.x/128) ).lines( freqs );
998 | },
999 | action: (type, x, y) => {
1000 | if (type === "up") { // for safari
1001 | if (mySampler.context.state === 'suspended') {
1002 | mySampler.context.resume();
1003 | }
1004 | }
1005 | }
1006 | });
1007 | space.autoResize = false;
1008 | space.play();
1009 |
1010 | var space2 = new CanvasSpace("#pts2");
1011 | space2.setup({ bgcolor: "#F9E79F" });
1012 | var form2 = space2.getForm();
1013 |
1014 | var sound2 = Sound.from( theirSampler, theirSampler.context ).analyze(256);
1015 |
1016 | space2.add({
1017 | animate: (time) => {
1018 | if (theirSampler.context.state === 'suspended') { // mostly for safari
1019 | form2.fillOnly("#fff").text( [20, 30], "Click anywhere to start" );
1020 | }
1021 |
1022 | var area = space2.size;
1023 | var idx = space2.pointer.$divide( area ).floor();
1024 | var rect = [idx.$multiply(area), idx.$multiply(area).add(area)];
1025 |
1026 | let t1 = sound2.timeDomainTo( area, rect[0].$subtract(0, area.y/2) );
1027 | let t2 = t1.map( t => t.$add(0, area.y) ).reverse();
1028 | let freqs = sound2.freqDomainTo( [area.x*2, area.y/2], [rect[0].x, 0] ).map( f => [[f.x, rect[0].y+area.y/2-f.y], [f.x, rect[0].y+area.y/2+f.y]] );
1029 |
1030 | form2.fillOnly("#F9E79F").polygon( t1.concat(t2) );
1031 | form2.strokeOnly("black", Math.ceil(area.x/128) ).lines( freqs );
1032 | },
1033 | action: (type, x, y) => {
1034 | if (type === "up") { // for safari
1035 | if (theirSampler.context.state === 'suspended') {
1036 | theirSampler.context.resume();
1037 | }
1038 | }
1039 | }
1040 | });
1041 | space2.play();
1042 |
1043 | function acceptSound() {
1044 | document.cookie = "cookie_soundon=true";
1045 | document.querySelector('.sound-overlay').classList.add('d-none');
1046 | Tone.start();
1047 | }
1048 |
1049 | var diagnosticsOpen = false;
1050 | function toggleDiagnostics() {
1051 | if (diagnosticsOpen) {
1052 | diagnosticsOpen = false;
1053 | document.querySelector('.diagnostics').classList.add('d-none');
1054 | } else {
1055 | diagnosticsOpen = true;
1056 | document.querySelector('.diagnostics').classList.remove('d-none');
1057 | }
1058 | }
1059 |
--------------------------------------------------------------------------------
/public/keyboard.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/public/keyboard2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------