From e5e908e65a1a5583d3dc25c31ddcf5b9ca6f6961 Mon Sep 17 00:00:00 2001 From: km0 Date: Sun, 19 Mar 2023 17:25:59 +0100 Subject: [PATCH] first test --- .gitignore | 4 +- README.md | 184 +------------------------------------ public/audioDestination.js | 131 ++++++++++++++++++++++++++ public/style.css | 44 ++++----- public/teletap.js | 100 ++++++++++++++++++++ server.js | 11 +-- views/.wander.html.swp | Bin 12288 -> 0 bytes views/destination.html | 53 +---------- views/index.html | 60 +----------- 9 files changed, 266 insertions(+), 321 deletions(-) create mode 100644 public/audioDestination.js create mode 100644 public/teletap.js delete mode 100644 views/.wander.html.swp diff --git a/.gitignore b/.gitignore index 40b878d..65d7854 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules/ \ No newline at end of file +node_modules/ +.env +package-lock.json diff --git a/README.md b/README.md index f0aa2b9..72e7d92 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -# DRW +# Teletap -![Drawing a bird](cover.jpg) +A small playground for networked sound experiments with Web Audio API. -A small app for collecting drawings in real time. Runs on a small express server that connects sources (where to draw) and destinations (where to display) via websockets. ## Setup @@ -26,181 +25,8 @@ or in alternative `npm start` -Then open your browser to `localhost:3000` and there you can draw. +Then open your browser to `localhost:3000` and there you can tap on the screen to generate sounds.. -If you open another tab and navigate to `localhost:3000/destination`, there you will receive the drawings. +If you open another tab and navigate to `localhost:3000/destination`, there you will receive the sounds. Note that you will not hear anything until you press on the `Join Audio` button. This is because for security policies, browsers don't play any sound before a user's interaction. -This destination page is just an example! The app is meant to be open-end, meaning that the destination of the drawing is up to you! (ah ah! less work for us). Originally it was coupled with [vvvv](https://visualprogramming.net/), but it can be implemented with any platform supporting the websocket protocol (the browser, pure data, max, touch designer, p5js, etc). - -## How does it work - -![A websocket connection is like ping pong](img/pingpong.jpg) - -This app works like the game of ping pong. Two (or more) atlethic players connect to a server to exchange messages through the fast ball of [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). - -There are four main elements in this app: - -**Source** - -A source is a client that connects to the application in order to send drawings. -To connect as a source simply connect to the server. - -This is what happens when you visit the [drawing page](https://hub.xpub.nl/soupboat/drw/). -For an example, look at the script section in the `views/index.html` file in the folder. - -**Destination** - -A destination is a client that connects to the application in order to receive drawings. -To connect as a destination, greet the server with an hello message: - -`{"type": "hello"}` - -This is what happens when you visit the [destination page](https://hub.xpub.nl/soupboat/drw/destination). For an example of the code, look at the script section in the `view/destination.html` section. Right after opening the websocket connection, the script will send a greeting to the server. - -**Websocket Messages** - -Websockets are a type of connection especially suited to real time application. -Here they are small `JSON` messages of different types. Imagine it as small Javascript objects or Python dictionaries - -``` -{ - "type": "test", - "data": "hello this is a test message!" -} - -``` - -Here they always come with a `type` property, that is used in both server and clients to trigger specific functions depending on it. - -At the moment the app logic is built on these messages: - -- **hello** - register the client as a destination - - _example:_ - `{"type": "hello"}` - -- **drawings** - message that contains [SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) of a drawing - - _example:_ - `{"type": "drawings", "paths": "M 10 10 H 90 V 90 H 10 L 10 10"}` - - maybe should be renamed just drawing - -- **theme** - message that suggests to the player a theme to draw about - - _example:_ - `{"type": "theme", "theme": "animals"}` - -**Server** - -The server is the table that grants to the connected clients to exchange messages. It mainly takes care of keeping track of who is connected, and to send the right message to the right client. For an insight of how does it work look at the comments in the `server.js` file! - -## Going online - -Eventually you want to put online your drawing app. - -To be able to use this app on the [Soupboat](hub.xpub.nl/soupboat) (or other servers connected in the [hub.xpub.nl](hub.xpub.nl) ecosystem) some additional configurations are needed. - -Note that the following details are tailored to the particular case of our server. Other instances could require different setups. - -This is one possible workflow. - -Clone the repo and install the requirements as you would do locally. - -``` -git clone https://git.xpub.nl/kamo/drw -cd drw -npm install -``` - -### Environmental variables - -There are a couple of environmental variables to set: one refers to the port where to mount the application, the other is related to the prefix to add to the application urls. - -``` -nano .env -``` - -Will create a new `.env` file where to add the values for this specific environment. -In the case of the soupboat, for example: - -``` -PORT=3000 -PREFIX=/soupboat/drw/ -``` - -Save and exit. - -The port is where Express will mount the application. This is by default set to 3000, but in this case we need to pick a port not already in use. -When deciding which port to use, check your NGINX configurations file (see next section), or simply test if the port you want is already in use. - -`sudo lsof -i:3000` - -For example, will print the process currently using the port 3000. If nothing is printed out, then the port is available. - -Read more about it here: [Check if port is in use Linux](https://www.cyberciti.biz/faq/unix-linux-check-if-port-is-in-use-command/) - -The prefix variable is a way to deal with the _hub.xpub.nl_ ecosystem. Here our base url is `hub.xpub.nl`. Notice that is missing the `/soupboat/drw/` part. -The deal of the prefix is to leave out from the code these parts of the address, that otherwise should be repeated in every url and navigation element of the app. - -This also make the code a bit more portable, meaning that you can test it locally and online without messing around with the urls in the code. - -The app is written in order to provide some default values if an `.env` file is not found, and that's why it works locally even without specifying any environmental variables. - -### NGINX Configuration - -To make it works behind a reverse-proxy open the NGINX configuration file - -``` -sudo nano /etc/nginx/sites-available/default -``` - -and inside the server section add a new location: - -``` -server { - - #note that your configurations may differ! - listen 80 default_server; - listen [::]:80 default_server; - - root /var/www/html - - # ADD FROM HERE - location /drw/ { - proxy_pass http://localhost:3000/soupboat/drw/; - include proxy_params; - proxy_set_header Host $http_host; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } - # TO HERE -} -``` - -The lines that you should edit according your configurations are: - -- `location /drw/` - The name of the location _/drw/_ is up to you, and it's the address where the app will be served. In this case will be _hub.xpub.nl/soupboat/drw/_. -- `proxy_pass http://localhost:3000/soupboat/drw/;` - The very same name, as well as eventual prefixes, need to be specified in the line of _proxy pass_. - The port, in this example set to _3000_, it's the port where Express is mounting the application. By default is 3000, but you can edit it according to the configurations of the express server. - -The three `proxy_set_header` Host, Upgrade and Connection are necessary to make the Websocket connection work. - -Once you add these info save and exit. -To check that the NGINX configuration file is ok run - -`sudo nginx -t` - -If it prints that everything is fine, reload nginx to apply the configurations. -If there are errors instead reopen the configurations file and fix them! - -**Watch out**: reloading nginx when the configurations are broken means disaster. Always run the test before reloading! - -Then you can start the app as you would do locally. - -`node server.js` +This app uses the same architecture of [DRw](https://git.xpub.nl/kamo/drw). Find more info there! diff --git a/public/audioDestination.js b/public/audioDestination.js new file mode 100644 index 0000000..23f328b --- /dev/null +++ b/public/audioDestination.js @@ -0,0 +1,131 @@ +const audioContext = new AudioContext(); + +const tap = document.querySelector('.container') +const address = document.querySelector('#address').innerHTML + +const socket = new ReconnectingWebSocket( + location.origin.replace(/^http/, "ws") + address + +); + +socket.onopen = (event) => { + socket.send(JSON.stringify({ type: "hello" })); + console.log("Connected as destination!"); +}; + + + + +let noiseDuration = 0.05 +const playNoise = (bandHz = 1000, time = 0) => { + + const bufferSize = audioContext.sampleRate * noiseDuration + + // Create an empty buffer + const noiseBuffer = new AudioBuffer({ + length: bufferSize, + sampleRate: audioContext.sampleRate, + }) + + // Fill the buffer with noise + const data = noiseBuffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + + // Create a buffer source from data + const noise = new AudioBufferSourceNode(audioContext, { + buffer: noiseBuffer, + }) + + // Filter the output + const bandpass = new BiquadFilterNode(audioContext, { + type: "bandpass", + frequency: bandHz + }) + + noise.connect(bandpass).connect(audioContext.destination); + noise.start(time); + +} + + + const playPulse = (freq=440, lfoFreq=30, duration=1, time=0) => { + + const osc = new OscillatorNode(audioContext, { + type: "square", + frequency: freq + }) + + const amp = new GainNode(audioContext, { + value: 0 + }) + + const lfo = new OscillatorNode(audioContext, { + type: "sine", + frequency: lfoFreq + }) + + lfo.connect(amp.gain) + osc.connect(amp).connect(audioContext.destination) + lfo.start() + osc.start(time) + // osc.stop(time + duration) + + } + + +const spawnGradient = (x, y) => { + const gradient = document.createElement('div') + gradient.classList.add('gradient') + gradient.style.translate = `${x}px ${y}px` + gradient.style.scale = 0 + + let red = x / tap.clientWidth * 255 + let green = y / tap.clientHeight * 255 + let blue = 0 + + gradient.style.background = `radial-gradient(circle, rgba(${red},${green},${blue},1) 0%, rgba(${red},${green},${blue},0) 25%)` + + tap.appendChild(gradient) + grow(gradient) + +} + +const grow = (el) => { + let scale = Number(el.style.scale) || 0 + el.style.scale = scale + 0.1 + requestAnimationFrame(()=> grow(el)) + +} + + + + +const emit = (x, y) => { + playPulse(x, y * 0.01) + spawnGradient(x, y) + } + + +const join = document.querySelector('#join') +join.addEventListener('click', ()=>{ + playNoise() +}) + + + + +socket.onmessage = (event) => { + let message; + try { + message = JSON.parse(event.data); + } catch (e) {} + + console.log("received a message! ", message) + if (message?.type == 'pulse'){ + emit(message.x, message.y) + } +}; + + diff --git a/public/style.css b/public/style.css index f68f94c..66c546c 100644 --- a/public/style.css +++ b/public/style.css @@ -1,33 +1,21 @@ -html, -body { - font-family: sans-serif; - background-color: dodgerblue; +.container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; } -#svgElement { - background-color: white; +.container.destination { + pointer-events: none; } -.destination #svgElement { - background: none; -} - -button { - display: block; - background-color: white; - border: 2px solid currentColor; - padding: 8px 24px; - border-radius: 24px; - font-size: 24px; - margin-top: 16px; -} - -.hidden { - display: none; -} - -#theme { - font-weight: bold; - -webkit-text-stroke: 1px black; - -webkit-text-fill-color: white; +.gradient { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + display: inline-block; + pointer-events: none; } diff --git a/public/teletap.js b/public/teletap.js new file mode 100644 index 0000000..9583999 --- /dev/null +++ b/public/teletap.js @@ -0,0 +1,100 @@ +const tap = document.querySelector('.container') +const address = document.querySelector('#address').innerHTML +const socket = new ReconnectingWebSocket(location.origin.replace(/^http/, "ws") + address); +const audioContext = new AudioContext(); + +let noiseDuration = 0.05 +const playNoise = (bandHz = 1000, time = 0) => { + + const bufferSize = audioContext.sampleRate * noiseDuration + + // Create an empty buffer + const noiseBuffer = new AudioBuffer({ + length: bufferSize, + sampleRate: audioContext.sampleRate, + }) + + // Fill the buffer with noise + const data = noiseBuffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + + // Create a buffer source from data + const noise = new AudioBufferSourceNode(audioContext, { + buffer: noiseBuffer, + }) + + // Filter the output + const bandpass = new BiquadFilterNode(audioContext, { + type: "bandpass", + frequency: bandHz + }) + + noise.connect(bandpass).connect(audioContext.destination); + noise.start(time); + +} + + + const playPulse = (freq=440, lfoFreq=30, duration=1, time=0) => { + + const osc = new OscillatorNode(audioContext, { + type: "square", + frequency: freq + }) + + const amp = new GainNode(audioContext, { + value: 0 + }) + + const lfo = new OscillatorNode(audioContext, { + type: "sine", + frequency: lfoFreq + }) + + lfo.connect(amp.gain) + osc.connect(amp).connect(audioContext.destination) + lfo.start() + osc.start(time) + // osc.stop(time + duration) + + } + + +const spawnGradient = (x, y) => { + const gradient = document.createElement('div') + gradient.classList.add('gradient') + gradient.style.translate = `${x}px ${y}px` + gradient.style.scale = 0 + + let red = x / tap.clientWidth * 255 + let green = y / tap.clientHeight * 255 + let blue = 0 + + gradient.style.background = `radial-gradient(circle, rgba(${red},${green},${blue},1) 0%, rgba(${red},${green},${blue},0) 25%)` + + tap.appendChild(gradient) + grow(gradient) + +} + +const grow = (el) => { + let scale = Number(el.style.scale) || 0 + el.style.scale = scale + 0.1 + requestAnimationFrame(()=> grow(el)) + +} + + + + +tap.addEventListener("click", (e) => { + playPulse(e.clientX, e.clientY * 0.01) + spawnGradient(e.clientX, e.clientY) + console.log('sending pulse') + socket.send(JSON.stringify({type: 'pulse', x: e.clientX, y: e.clientY })) + }) + + + diff --git a/server.js b/server.js index 59eb722..e700c18 100644 --- a/server.js +++ b/server.js @@ -19,7 +19,7 @@ const PUBLIC = process.env.PUBLIC || ""; const router = express.Router(); const routes = (app) => { app.get("/", (req, res) => { - res.render("index", { + res.render("index", { address: PREFIX, }); }); @@ -27,11 +27,6 @@ const routes = (app) => { res.render("destination", { address: PREFIX, }); - }); - app.get("/wander", (req, res)=>{ - res.render("wander", { - address: PREFIX, - }) }); app.get("/*", (req, res) => { res.sendFile(req.url, { root: "public" }); @@ -63,8 +58,7 @@ var theme = ""; const messageProcessor = { default: (ws, msg) => unknownMsg(msg), hello: (ws, msg) => registerDest(ws, msg), - drawings: (ws, msg) => toDest(msg), - theme: (ws, msg) => ((theme = msg.theme), broadcast(msg)), + pulse: (ws, msg) => toDest(msg) }; // Message processor functions @@ -98,6 +92,7 @@ const toDest = (msg) => { // Send a message to all the connected Users const broadcast = (msg) => { let message = JSON.stringify(msg); + console.log('broadcasting: ', message) for (const user of USERS.values()) { if (user?.readyState === WebSocket.OPEN && !DESTINATIONS.has(user)) { user.send(message); diff --git a/views/.wander.html.swp b/views/.wander.html.swp deleted file mode 100644 index 376cf6ae9da6a85f0141c195d13089b2a2e23eed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2ON^ocNYa=AuKCCg3{AnJJW7YciUC- zVl!g&U`&+6cu*5Pn0WAYGip2_iNq5(&l(drVq#24Jel=h-95uBGq5C_NNP5}epJ<0 z^?m<(Ze}XQ=MNsCdyOK&=Mh4_DgA3-{A2_9YAYeUQ4eJDdY{?3h38HjJ(iz7bZll~ z@z70+zac9uw8KWF;6$xL(`7AQXj#6)BBLhi-c1cvhEhN&aAyiM#8Q4@3)#J^6i@Z- z+BW*wqlV^uo+z4OvsPmUGO$|3%m;E!7O+ZYyuf@c@rV$!N=fP@C3M)A>-~mtEj@(d8p%Oaq z>8^d1IT3rk!MK?A-MS^*z&{XKb*Ae%bBeYsZ&_zvE=LBch#!X%m}hX&@t7eZ%jcdY z7&YnXr&fH{nhAo);YPbVMhD+J-Lca{)eN^Jo5Qb5@qO^!_O4yDLc z$R9?mx!>r}om4+!iJFnbtmkN+7L7eQx`T4t^4Lgo$e)!pr;|^hpRH8jC?jY6VpOOZlxiZ zu5l3s%Pik=9Z}Q9>BPE4QAo2JQN(;P6L>+S-NItY^*mYL4}5m3at;dB8Ru4&8PZ4I zt5XhcnE;8Lz;4vhs9{G8E6kk7?#(E1qdPAmgfDo zr8u@{aemn40!up0Bs7m|&lAlk(BNZ?>b*&ZU8$D^Kl<}Oh^^?1J_Iw4j( zlWI<6wOqf-wH!uZGKi>c3A=`whNN^vrFN+Q(8AGq1RQauv!;yAv4oYjAfgT`k-aAU zNp0eO>Pd4Wc|xx~%BF9GHTB3k1J{78fG0?2@&j^1&ruHRpb z3$Mt|E1;WNQnZ~^TUoIjC&F0hb~PD&GIx_^P(RjDdYmCqMgreXvdTg%haT&ll*Tog zvM}fm8*Fq@XMuUJ3nE?Hj=s(J3#JMr=Dl8D$2uK>iD|jN<;wlnahq}@&P-t|ZdT*R zOuOz)@(??asbTHmENZeiz)bILnuqZ1xYWr^W=!1s%(p5Y%U3LWxf%rx-$5osDB`bF zj7h4IKYo5XXq%eM*@GOAJ%QKc-8mejNSs;OapoFe6Ivqxu6 Yym)MmCWp?|)z!1|d;S0L(EEP>e?zA`=l}o! diff --git a/views/destination.html b/views/destination.html index 7c0de32..09c6d23 100644 --- a/views/destination.html +++ b/views/destination.html @@ -6,58 +6,13 @@ Draw draw draw + -

Display

-
- -
+
+ {{address}} + - diff --git a/views/index.html b/views/index.html index f6086af..a6cf2e1 100644 --- a/views/index.html +++ b/views/index.html @@ -6,66 +6,14 @@ Draw draw draw - - + + -

Draw

-
- - - -
-