This commit is contained in:
Dooho Yi 2022-02-23 17:28:25 +09:00
commit 4fde8c3c49
33 changed files with 8031 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.env
node_modules
.DS_Store

179
LICENSE Normal file
View file

@ -0,0 +1,179 @@
The LICENSE file for any project gives credit to the creator/author of the
project, copyright information for the project, and the legal terms under
which it's being shared. In other words, this is us using an MIT license to
say "we wrote this and you can do whatever you want with it."
******************************************************************************
~glitch-hello-node
******************************************************************************
MIT License
Copyright (c) 2021, Glitch, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************
THIRD-PARTY SOFTWARE
1. fastify: Fastify is a web framework focused on developer experience.
2. fastify-static: Plugin for serving static files as fast as possible.
3. handlebars.js: Minimal templating on steroids
4. point-of-view: Templates rendering plugin support for Fastify.
5. HK Grotesk: The font we're using.
******************************************************************************
1. fastify (also applies to fastify-formbody)
URL: https://www.fastify.io/
https://github.com/fastify/fastify
******************************************************************************
MIT License
Copyright (c) 2016-2020 The Fastify Team
The Fastify team members are listed at https://github.com/fastify/fastify#team
and in the README file.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************
END, fastify
******************************************************************************
******************************************************************************
2. fastify-static
URL: https://github.com/fastify/fastify-static
******************************************************************************
MIT License
Copyright (c) 2017-2018 Fastify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************
END, fastify-static
******************************************************************************
******************************************************************************
3. handlebars.js
URL: https://handlebarsjs.com/
https://github.com/handlebars-lang/handlebars.js
******************************************************************************
Copyright (C) 2011-2019 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
******************************************************************************
END, fastify-static
******************************************************************************
******************************************************************************
4. point-of-view
URL: https://github.com/fastify/point-of-view
******************************************************************************
MIT License
Copyright (c) 2017 Fastify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************
END, point-of-view
******************************************************************************
******************************************************************************
5. HK Grotesk
URL: https://hanken.co/products/hk-grotesk
******************************************************************************
HK Grotesk was designed by Hanken Design Co. It is shared using a SIL OFL
license. Full license text can be found at:
https://hanken.co/pages/web-fonts-eula
******************************************************************************
END, HK Grotesk
******************************************************************************

42
README.md Normal file
View file

@ -0,0 +1,42 @@
# Hello Node!
This project includes a Node.js server script and a web page that connects to it. The front-end page presents a form the visitor can use to submit a color name, sending the submitted value to the back-end API running on the server. The server returns info to the page that allows it to update the display with the chosen color. 🎨
[Node.js](https://nodejs.org/en/about/) is a popular runtime that lets you run server-side JavaScript. This project uses the [Fastify](https://www.fastify.io/) framework and explores basic templating with [Handlebars](https://handlebarsjs.com/).
## Prerequisites
You'll get best use out of this project if you're familiar with basic JavaScript. If you've written JavaScript for client-side web pages this is a little different because it uses server-side JS, but the syntax is the same!
## What's in this project?
`README.md`: Thats this file, where you can tell people what your cool website does and how you built it.
`public/style.css`: The styling rules for the pages in your site.
`server.js`: The **Node.js** server script for your new site. The JavaScript defines the endpoints in the site back-end, one to return the homepage and one to update with the submitted color. Each one sends data to a Handlebars template which builds these parameter values into the web page the visitor sees.
`package.json`: The NPM packages for your project's dependencies.
`src/`: This folder holds the site template along with some basic data files.
`src/pages/index.hbs`: This is the main page template for your site. The template receives parameters from the server script, which it includes in the page HTML. The page sends the user submitted color value in the body of a request, or as a query parameter to choose a random color.
`src/colors.json`: A collection of CSS color names. We use this in the server script to pick a random color, and to match searches against color names.
`src/seo.json`: When you're ready to share your new site or add a custom domain, change SEO/meta settings in here.
## Try this next 🏗️
Take a look in `TODO.md` for next steps you can try out in your new site!
___Want a minimal version of this project to build your own Node.js app? Check out [Blank Node](https://glitch.com/edit/#!/remix/glitch-blank-node)!___
![Glitch](https://cdn.glitch.com/a9975ea6-8949-4bab-addb-8a95021dc2da%2FLogo_Color.svg?v=1602781328576)
## You built this with Glitch!
[Glitch](https://glitch.com) is a friendly community where millions of people come together to build web apps and websites.
- Need more help? [Check out our Help Center](https://help.glitch.com/) for answers to any common questions.
- Ready to make it official? [Become a paid Glitch member](https://glitch.com/pricing) to boost your app with private sharing, more storage and memory, domains and more.

22
TODO.md Normal file
View file

@ -0,0 +1,22 @@
# TODO 🚧
Your new site is all yours so it doesn't matter if you break it! Try editing the code.
Let's keep track of the submitted favorites using an array. First add this code near the top of `server.js` (where the comment says `ADD FAVORITES ARRAY VARIABLE`):
```js
const favorites = [];
```
In the `POST` route, inside the `if(color)` block, add this code to save the submitted value to the array, and write it to the console:
```js
favorites.push(color);
console.log(favorites);
```
Click __Tools__ > __Logs__ at the bottom of Glitch to see the log statement in action when you submit new colors through the form.
## Keep going! 🚀
Clearly this is not a robust data storage approach and won't persist for long! Your Node apps can use a variety of databases, like [SQLite](https://glitch.com/~glitch-hello-sqlite) and [Airtable](https://glitch.com/~glitch-hello-airtable).

2129
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

40
package.json Normal file
View file

@ -0,0 +1,40 @@
{
"name": "hello-node",
"version": "0.0.1",
"description": "A simple Node app built on fastify, instantly up and running.",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"dotenv": "^11.0.0",
"fastify": "^3.25.3",
"fastify-formbody": "^5.2.0",
"fastify-multipart": "^5.2.1",
"fastify-static": "^4.5.0",
"fluent-ffmpeg": "^2.1.2",
"handlebars": "^4.7.7",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"nextcloud-node-client": "^1.8.2",
"point-of-view": "^5.0.0",
"socket.io": "~2.3.0",
"uuid": "^8.3.2"
},
"engines": {
"node": "12.x"
},
"repository": {
"url": "https://glitch.com/edit/#!/glitch-hello-node"
},
"license": "MIT",
"keywords": [
"node",
"glitch",
"express"
],
"devDependencies": {
"nodemon": "^2.0.15"
}
}

BIN
public/audio/_silence.wav Normal file

Binary file not shown.

BIN
public/audio/clap01.mp3 Normal file

Binary file not shown.

429
public/default.css Normal file
View file

@ -0,0 +1,429 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
word-break: keep-all;
}
body {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: white;
font-size: 15px;
line-height: 1.4;
font-family: 'Noto Sans KR', sans-serif;
}
.bg {
position: fixed;
top: 0;
left: 0;
background: linear-gradient(#000000, #222958 65%, #e7e7d6 95%);
background-repeat: no-repeat;
background-position: center;
height: 100vh;
width: 100vw;
/* z-index usage is up to you.. although there is no need of using it because the default stack context will work. */
z-index: -1; // this is optional
}
#krtitle {
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%);
writing-mode: vertical-lr;
}
#entitle {
position: absolute;
bottom: 15%;
left: 50%;
transform: translate(-50%);
writing-mode: vertical-lr;
}
.popup {
display: none;
position: absolute;
max-width: 800px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.3);
z-index: 100;
}
.content {
width: 100%;
height: 100%;
}
.lang {
position: fixed;
top: 20px;
right: 15px;
z-index: 2;
writing-mode: vertical-lr;
}
.first {
position: fixed;
top: 20px;
right: 18px;
z-index: 2;
writing-mode: vertical-lr;
}
.second {
position: fixed;
top: 20px;
right: 40px;
z-index: 2;
writing-mode: vertical-lr;
}
.third {
position: fixed;
top: 20px;
right: 60px;
z-index: 2;
writing-mode: vertical-lr;
}
audio {
cursor: pointer;
}
.sub {
position: fixed;
bottom: 15px;
left: 20px;
}
.list2 {
position: fixed;
bottom: 15px;
left: 50%;
text-align: center;
}
.parade {
position: fixed;
bottom: 15px;
right: 20px;
}
.list-sub {
position: fixed;
top: 36px;
right: 20px;
z-index: 2;
}
.list-parade {
position: fixed;
top: 68px;
right: 20px;
z-index: 2;
}
a,
a:visited,
a:hover,
a:focus,
a:active {
color: white;
text-decoration: none;
}
.notice {
position: absolute;
display: inline-block;
width: 550px;
top: 50px;
left: 50%;
transform: translate(-50%);
padding-bottom: 50px;
}
section {
margin-bottom: 100px;
}
.info {
margin-left: 40px;
}
.participation li {
margin-bottom: 15px;
}
.participation ol {
margin-top: 15px;
}
h1, h2, h3 {
font-weight: normal;
}
h1 {
position: relative;
display: block;
left: 50%;
transform: translate(-50%);
font-size: 15px;
margin-bottom: 50px;
writing-mode: vertical-lr;
text-align: center;
}
h2 {
position: relative;
display: block;
left: 50%;
transform: translate(-50%);
font-size: 15px;
margin-bottom: 15px;
writing-mode: vertical-lr;
text-align: center;
}
h3 {
font-size: 15px;
}
p {
font-size: 15px;
margin-bottom: 15px;
}
ol, ul {
list-style: none;
}
.submit li {
margin-bottom: 30px;
}
.submit li li {
margin-bottom: 0px;
}
.category {
margin-left: 40px;
}
.noscroll {
overflow: hidden;
}
.noscroll * {
touch-action: none;
}
small {
margin-top: 5px;
display: block;
}
canvas {
margin: 5px 0 5px 0;
}
.items {
padding: 50px 0 50px 0;
}
.items li {
position: relative;
vertical-align: top;
padding: 2px 0 2px 0;
}
.sound {
position: relative;
left: 50%;
transform: translate(-50%);
margin-top: 40px;
}
.delete {
margin-top: 15px;
}
.drawing {
position: relative;
display: inline-block;
width: 300px;
left: 50%;
transform: translate(-50%);
vertical-align: top;
text-align: center;
}
.name {
display: inline-block;
}
.sound {
display: inline-block;
}
.soundinfo {
padding-top: 30px;
}
.soundinfo li {
vertical-align: top;
}
.group,
.title,
.comment,
.sound,
.smallclose {
display: inline-block;
vertical-algin: top;
}
details summary::-webkit-details-marker {
display:none;
}
.roomsel {
position: absolute;
width: 100%;
height: 100%;
top: 0%;
left: 0%;
}
.roomsel button {
position: absolute;
bottom: 50px;
left: 50%;
transform: translate(-50%);
}
.intro {
animation-delay: 15s;
animation-name: fadeout;
animation-duration: 5s;
animation-fill-mode: forwards;
}
@keyframes fadeout {
0% {
opacity: 0.7;
}
100% {
opacity: 0;
display: none;
}
}
tr {
color: red;
}
tr,
td {
display: inline-block;
}
summary {
list-style: none;
outline: none;
cursor: pointer;
}
.delete {
position: absolute;
display: inline-block;
right: 0;
padding: 0;
cursor: pointer;
padding: 2px;
vertical-align: top;
}
.preview {
position: absolute;
display: inline-block;
left: 0;
padding: 0;
cursor: pointer;
padding: 2px;
vertical-align: top;
margin-top: 15px;
}
.intro {
top: 40%;
transform: translate(-50%);
}
button {
padding: 2px 6px 3px 6px;
font-size: 12px;
}
.roomsel .title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -30%);
height: 600px;
writing-mode: vertical-lr;
}
@media screen and (max-width: 700px) {
#krtitle {
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%);
writing-mode: vertical-lr;
}
#entitle {
position: absolute;
bottom: 10%;
left: 50%;
transform: translate(-50%);
writing-mode: vertical-lr;
}
.notice {
position: absolute;
display: inline-block;
width: 90%;
top: 50px;
left: 50%;
transform: translate(-50%);
padding-bottom: 20px;
}
.intro {
top: 30%;
transform: translate(-50%);
}
button {
padding: 8px 6px 5px 6px;
font-size: 12px;
}
.roomsel .title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -30%);
height: 600px;
writing-mode: vertical-lr;
}
}

1
public/js/Tone-14.8.36.min.js vendored Normal file

File diff suppressed because one or more lines are too long

28
public/js/p5-v0.3.11.sound.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
public/js/p5-v1.1.9.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

87
public/js/two-v0.8.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

54
public/score.json.archive Normal file
View file

@ -0,0 +1,54 @@
[
{
"object": {
"id": 2,
"type": "시작",
"src": "./imgs/03.png",
"audio": "./audio/bird.mp3",
"alt": "알트",
"size": { "base": 40, "random": 20 },
"y": { "base": 20, "random": 10 },
"showtime": 30000
},
"timegap": { "base": 19000, "random": 1000 }
},
{
"object": {
"id": 3,
"type": "시작",
"src": "./imgs/06.png",
"audio": "./audio/clap01.mp3",
"alt": "알트",
"size": { "base": 40, "random": 20 },
"y": { "base": 20, "random": 10 },
"showtime": 30000
},
"timegap": { "base": 19000, "random": 1000 }
},
{
"object": {
"id": 4,
"type": "시작",
"src": "./imgs/09.png",
"audio": "./audio/clap02.mp3",
"alt": "알트",
"size": { "base": 40, "random": 20 },
"y": { "base": 20, "random": 10 },
"showtime": 30000
},
"timegap": { "base": 19000, "random": 1000 }
},
{
"object": {
"id": 5,
"type": "시작",
"src": "./imgs/12.png",
"audio": "./audio/march_drum.mp3",
"alt": "알트",
"size": { "base": 40, "random": 20 },
"y": { "base": 20, "random": 10 },
"showtime": 30000
},
"timegap": { "base": 19000, "random": 1000 }
}
]

1
public/stream.m3u Normal file
View file

@ -0,0 +1 @@
http://116.122.163.142:8000/stream

51
public/style.css.archive Normal file
View file

@ -0,0 +1,51 @@
html,
body {
overflow: hidden;
margin: 0;
padding: 0;
}
p {
margin: 1vw;
font-size: 3vw;
font-family: "Do Hyeon", sans-serif;
color: rgb(255, 64, 180);
}
@keyframes rolling {
to {
transform: rotate(360deg);
}
}
.rotate {
animation: rolling 3s linear infinite;
}
.roomsel {
display: flex;
}
.roomsel button {
width: 10%;
margin: auto;
padding: 3vw 0vw;
background-color: white;
border: 0.5vw solid rgb(255, 64, 180);
border-radius: 1vw;
color: rgb(255, 64, 180);
font-size: 5vw;
font-weight: bold;
}
.roomsel button:hover {
background-color: rgb(255, 64, 180, 0.1);
}
.num {
margin: 2vw;
font-size: 3vw;
font-family: "Do Hyeon", sans-serif;
color: rgb(255, 64, 180);
}

351
puredata/livetest.pd Normal file
View file

@ -0,0 +1,351 @@
#N canvas 0 23 1440 803 12;
#X obj 232 291 f;
#X obj 232 315 + 1;
#X obj 232 339 mod;
#X floatatom 232 363 5 0 0 0 - - -;
#X floatatom 281 344 5 0 0 0 - - -;
#X obj 308 145 gauss 1 0;
#X obj 308 115 metro 100;
#X obj 308 95 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0
1;
#X obj 308 193 sel 1;
#X obj 308 169 > 1;
#X obj 308 222 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 389 91 vsl 30 128 1 3 0 0 empty empty empty 0 -9 0 10 -262144
-1 -1 9900 1;
#X floatatom 389 227 5 0 0 0 - - -;
#X obj 166 124 retro2 1000 30000;
#X obj 166 172 retro2 1000 30000;
#X obj 135 202 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 26 202 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1
-1;
#X obj 26 137 rscan2 1000 2000;
#X obj 281 276 bng 15 250 50 0 empty INFO news! 17 7 0 10 -262144 -1
-1;
#X obj 281 320 tabread entries;
#X msg 281 296 0;
#X obj 232 399 s FLOW;
#X obj 699 274 f;
#X obj 699 298 + 1;
#X obj 699 322 mod;
#X floatatom 748 327 5 0 0 0 - - -;
#X obj 795 158 gauss 1 0;
#X obj 795 128 metro 100;
#X obj 795 108 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0
1;
#X obj 795 206 sel 1;
#X obj 795 182 > 1;
#X obj 795 235 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 876 104 vsl 30 128 1 3 0 0 empty empty empty 0 -9 0 10 -262144
-1 -1 0 1;
#X floatatom 876 240 5 0 0 0 - - -;
#X msg 513 126 5;
#X obj 653 137 retro2 1000 30000;
#X obj 653 185 retro2 1000 30000;
#X obj 622 215 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 513 215 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 513 150 rscan2 1000 2000;
#X obj 748 259 bng 15 250 50 0 empty INFO news! 17 7 0 10 -262144 -1
-1;
#X obj 748 303 tabread entries;
#X obj 699 396 s FLOW;
#N canvas 0 23 593 706 base 0;
#X obj 34 66 oscformat -f i flow;
#X obj 55 182 oscformat -f b info;
#N canvas 0 23 450 292 websocket 0;
#X obj 20 117 netsend -u -b;
#X msg 20 74 connect localhost 57001;
#X obj 310 138 oscparse;
#X obj 310 114 netreceive -u -b;
#X msg 310 74 listen 57000;
#X obj 158 125 inlet;
#X obj 158 175 list prepend send;
#X obj 158 200 list trim;
#X obj 181 18 loadbang;
#X obj 310 162 list trim;
#X obj 310 186 outlet;
#X connect 1 0 0 0;
#X connect 2 0 9 0;
#X connect 3 0 2 0;
#X connect 4 0 3 0;
#X connect 5 0 6 0;
#X connect 6 0 7 0;
#X connect 7 0 0 0;
#X connect 8 0 1 0;
#X connect 8 0 4 0;
#X connect 9 0 10 0;
#X restore 34 215 pd websocket talk;
#X obj 55 158 metro 10000;
#X obj 55 138 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1
1;
#X obj 34 255 route info;
#X obj 48 285 print info;
#X obj 34 352 route entries flags bodies objects ones;
#X obj 446 551 table flag;
#X obj 446 581 table body;
#X obj 446 611 table object;
#X obj 446 641 table any;
#X obj 88 553 list trim;
#X msg 168 627 \; entries 0 72 12 16 25 19 \;;
#X msg 168 597 set \, addsemi;
#X obj 88 422 list prepend add flag 0;
#X obj 142 452 list prepend add body 0;
#X obj 196 482 list prepend add object 0;
#X obj 250 512 list prepend add any 0;
#X obj 88 578 t b a b;
#X obj 446 521 table entries;
#X obj 34 392 list prepend add entries 0;
#X obj 145 283 bng 15 250 50 0 INFO empty news! 17 7 0 10 -262144 -1
-1;
#X obj 34 36 r FLOW;
#X obj 55 108 loadbang;
#X obj 121 26 print flow;
#X connect 0 0 2 0;
#X connect 1 0 2 0;
#X connect 2 0 5 0;
#X connect 3 0 1 0;
#X connect 4 0 3 0;
#X connect 5 0 7 0;
#X connect 5 0 22 0;
#X connect 7 0 21 0;
#X connect 7 1 15 0;
#X connect 7 2 16 0;
#X connect 7 3 17 0;
#X connect 7 4 18 0;
#X connect 12 0 19 0;
#X connect 14 0 13 0;
#X connect 15 0 12 0;
#X connect 16 0 12 0;
#X connect 17 0 12 0;
#X connect 18 0 12 0;
#X connect 19 0 13 0;
#X connect 19 1 13 0;
#X connect 19 2 14 0;
#X connect 21 0 12 0;
#X connect 23 0 0 0;
#X connect 23 0 25 0;
#X connect 24 0 4 0;
#X restore 20 26 pd base;
#X text 163 55 ALL;
#X msg 748 279 1;
#X obj 699 349 tabread flag;
#X floatatom 699 373 5 0 0 0 - - -;
#X text 668 69 FLAG;
#X obj 1179 274 f;
#X obj 1179 298 + 1;
#X obj 1179 322 mod;
#X floatatom 1228 327 5 0 0 0 - - -;
#X obj 1275 158 gauss 1 0;
#X obj 1275 128 metro 100;
#X obj 1275 108 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1
0 1;
#X obj 1275 206 sel 1;
#X obj 1275 182 > 1;
#X obj 1275 235 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 1356 104 vsl 30 128 1 3 0 0 empty empty empty 0 -9 0 10 -262144
-1 -1 0 1;
#X floatatom 1356 240 5 0 0 0 - - -;
#X msg 993 126 5;
#X obj 1133 137 retro2 1000 30000;
#X obj 1133 185 retro2 1000 30000;
#X obj 1102 215 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 993 215 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 993 150 rscan2 1000 2000;
#X obj 1228 259 bng 15 250 50 0 empty INFO news! 17 7 0 10 -262144
-1 -1;
#X obj 1228 303 tabread entries;
#X obj 1179 396 s FLOW;
#X floatatom 1179 373 5 0 0 0 - - -;
#X obj 459 624 f;
#X obj 459 648 + 1;
#X obj 459 672 mod;
#X floatatom 508 677 5 0 0 0 - - -;
#X obj 555 508 gauss 1 0;
#X obj 555 478 metro 100;
#X obj 555 458 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0
1;
#X obj 555 556 sel 1;
#X obj 555 532 > 1;
#X obj 555 585 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 636 454 vsl 30 128 1 3 0 0 empty empty empty 0 -9 0 10 -262144
-1 -1 9400 1;
#X floatatom 636 590 5 0 0 0 - - -;
#X msg 273 476 5;
#X obj 413 487 retro2 1000 30000;
#X obj 413 535 retro2 1000 30000;
#X obj 382 565 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 303 525 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 273 500 rscan2 1000 2000;
#X obj 508 609 bng 15 250 50 0 empty INFO news! 17 7 0 10 -262144 -1
-1;
#X obj 508 653 tabread entries;
#X obj 459 746 s FLOW;
#X floatatom 459 723 5 0 0 0 - - -;
#X obj 939 624 f;
#X obj 939 648 + 1;
#X obj 939 672 mod;
#X floatatom 988 677 5 0 0 0 - - -;
#X obj 1035 508 gauss 1 0;
#X obj 1035 478 metro 100;
#X obj 1035 458 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1
0 1;
#X obj 1035 556 sel 1;
#X obj 1035 532 > 1;
#X obj 1035 585 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 1116 454 vsl 30 128 1 3 0 0 empty empty empty 0 -9 0 10 -262144
-1 -1 0 1;
#X floatatom 1116 590 5 0 0 0 - - -;
#X msg 753 476 5;
#X obj 893 487 retro2 1000 30000;
#X obj 893 535 retro2 1000 30000;
#X obj 862 565 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 753 565 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1;
#X obj 753 500 rscan2 1000 2000;
#X obj 988 609 bng 15 250 50 0 empty INFO news! 17 7 0 10 -262144 -1
-1;
#X obj 988 653 tabread entries;
#X obj 939 746 s FLOW;
#X floatatom 939 723 5 0 0 0 - - -;
#X text 1148 69 BODY;
#X msg 1228 279 2;
#X msg 508 629 3;
#X msg 988 629 4;
#X text 428 419 OBJECT;
#X text 908 419 ANY;
#X obj 1179 349 tabread body;
#X obj 459 699 tabread object;
#X obj 939 699 tabread any;
#X msg 26 113 15;
#X connect 0 0 1 0;
#X connect 1 0 0 1;
#X connect 1 0 2 0;
#X connect 2 0 3 0;
#X connect 3 0 21 0;
#X connect 5 0 9 0;
#X connect 6 0 5 0;
#X connect 7 0 6 0;
#X connect 8 0 10 0;
#X connect 9 0 8 0;
#X connect 10 0 0 0;
#X connect 11 0 12 0;
#X connect 12 0 9 1;
#X connect 13 0 0 0;
#X connect 14 0 0 0;
#X connect 16 0 0 0;
#X connect 17 0 16 0;
#X connect 17 1 15 0;
#X connect 18 0 20 0;
#X connect 19 0 2 1;
#X connect 19 0 4 0;
#X connect 20 0 19 0;
#X connect 22 0 23 0;
#X connect 23 0 22 1;
#X connect 23 0 24 0;
#X connect 24 0 46 0;
#X connect 26 0 30 0;
#X connect 27 0 26 0;
#X connect 28 0 27 0;
#X connect 29 0 31 0;
#X connect 30 0 29 0;
#X connect 31 0 22 0;
#X connect 32 0 33 0;
#X connect 33 0 30 1;
#X connect 34 0 39 0;
#X connect 35 0 22 0;
#X connect 36 0 22 0;
#X connect 38 0 22 0;
#X connect 39 0 38 0;
#X connect 39 1 37 0;
#X connect 40 0 45 0;
#X connect 41 0 24 1;
#X connect 41 0 25 0;
#X connect 45 0 41 0;
#X connect 46 0 47 0;
#X connect 47 0 42 0;
#X connect 49 0 50 0;
#X connect 50 0 49 1;
#X connect 50 0 51 0;
#X connect 51 0 121 0;
#X connect 53 0 57 0;
#X connect 54 0 53 0;
#X connect 55 0 54 0;
#X connect 56 0 58 0;
#X connect 57 0 56 0;
#X connect 58 0 49 0;
#X connect 59 0 60 0;
#X connect 60 0 57 1;
#X connect 61 0 66 0;
#X connect 62 0 49 0;
#X connect 63 0 49 0;
#X connect 65 0 49 0;
#X connect 66 0 65 0;
#X connect 66 1 64 0;
#X connect 67 0 116 0;
#X connect 68 0 51 1;
#X connect 68 0 52 0;
#X connect 70 0 69 0;
#X connect 71 0 72 0;
#X connect 72 0 71 1;
#X connect 72 0 73 0;
#X connect 73 0 122 0;
#X connect 75 0 79 0;
#X connect 76 0 75 0;
#X connect 77 0 76 0;
#X connect 78 0 80 0;
#X connect 79 0 78 0;
#X connect 80 0 71 0;
#X connect 81 0 82 0;
#X connect 82 0 79 1;
#X connect 83 0 88 0;
#X connect 84 0 71 0;
#X connect 85 0 71 0;
#X connect 87 0 71 0;
#X connect 88 0 87 0;
#X connect 88 1 86 0;
#X connect 89 0 117 0;
#X connect 90 0 73 1;
#X connect 90 0 74 0;
#X connect 92 0 91 0;
#X connect 93 0 94 0;
#X connect 94 0 93 1;
#X connect 94 0 95 0;
#X connect 95 0 123 0;
#X connect 97 0 101 0;
#X connect 98 0 97 0;
#X connect 99 0 98 0;
#X connect 100 0 102 0;
#X connect 101 0 100 0;
#X connect 102 0 93 0;
#X connect 103 0 104 0;
#X connect 104 0 101 1;
#X connect 105 0 110 0;
#X connect 106 0 93 0;
#X connect 107 0 93 0;
#X connect 109 0 93 0;
#X connect 110 0 109 0;
#X connect 110 1 108 0;
#X connect 111 0 118 0;
#X connect 112 0 95 1;
#X connect 112 0 96 0;
#X connect 114 0 113 0;
#X connect 116 0 68 0;
#X connect 117 0 90 0;
#X connect 118 0 112 0;
#X connect 121 0 70 0;
#X connect 122 0 92 0;
#X connect 123 0 114 0;
#X connect 124 0 17 0;

1750
puredata/nodejs/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
{
"name": "walkingtowardstheflow-nodejs",
"version": "1.0.0",
"description": "",
"main": "sender.js",
"scripts": {
"start": "node sender.js",
"dev": "nodemon sender.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"osc": "^2.2.3",
"socket.io-client": "^2.3.0"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}

99
puredata/nodejs/sender.js Normal file
View file

@ -0,0 +1,99 @@
const osc = require("osc");
const udp = new osc.UDPPort({
localAddress: '0.0.0.0', //<-- NOTE: '127.0.0.1' doesn't work!! for comm. between different machines
localPort: 57001, remoteAddress: '0.0.0.0', remotePort: 57000, metadata: true
});
//firstly establish/prepare osc conn.
Promise.all([
new Promise((resolve, reject) => udp.on("ready", () => resolve('resolve: udp ready.'))),
]).then(results => {
console.log(results[0]);
const io = require('socket.io-client');
const socket = io('https://walkingtowardstheflow.xyz');
socket.on('connect', () => {
console.log("[osc-receiver] i'm connected.");
socket.emit("room", 1, (res) => console.log(res));
});
socket.on('disconnect', () => console.log("[osc-receiver] i'm disconnected."));
//from browser
socket.on('post', p => {
console.log('post', p);
});
//from puredata
udp.on("message", function (m) {
// console.log("osc(pd)", m);
//
if (m.address == '/flow') {
socket.emit('flow', m.args[0].value);
}
//
else if (m.address == '/info') {
socket.emit('info', (fields) => {
console.log(fields.length);
//
var flags = [];
for (let k = 0; k < fields.length; k++) {
if (fields[k].group == "flag") flags.push({type:'f', value:k});
}
udp.send({
address: '/info/flags',
args: flags
})
//
var bodies = [];
for (let k = 0; k < fields.length; k++) {
if (fields[k].group == "body") bodies.push({type:'f', value:k});
}
udp.send({
address: '/info/bodies',
args: bodies
})
//
var objects = [];
for (let k = 0; k < fields.length; k++) {
if (fields[k].group == "object") objects.push({type:'f', value:k});
}
udp.send({
address: '/info/objects',
args: objects
})
//
var ones = [];
for (let k = 0; k < fields.length; k++) {
if (fields[k].group == "any") ones.push({type:'f', value:k});
}
udp.send({
address: '/info/ones',
args: ones
})
//
udp.send({
address: '/info/entries',
args:[
{type: 'f', value: fields.length},
{type: 'f', value: flags.length},
{type: 'f', value: bodies.length},
{type: 'f', value: objects.length},
{type: 'f', value: ones.length},
]
});
});
}
});
});
//osc.js - start service
udp.open();
udp.on("ready", () => console.log(
"[udp] ready (udp) : \n" +
"\tlistening on --> " + udp.options.localAddress + ":" + udp.options.localPort + "\n" +
"\tspeaking to -> " + udp.options.remoteAddress + ":" + udp.options.remotePort + "\n"
));

16
puredata/retro-help.pd Normal file
View file

@ -0,0 +1,16 @@
#N canvas 1 89 370 282 12;
#X obj 94 124 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0
1;
#X obj 94 189 print;
#X msg 189 95 1000 4000;
#X obj 94 143 retro 200 1800;
#X msg 200 125 200 1800;
#X text 2 7 <<<;
#X text 322 7 >>>;
#X text 2 237 <<<;
#X text 322 237 >>>;
#X text 95 39 * a randomized metro !;
#X connect 0 0 3 0;
#X connect 2 0 3 1;
#X connect 3 0 1 0;
#X connect 4 0 3 1;

57
puredata/retro.pd Normal file
View file

@ -0,0 +1,57 @@
#N canvas 310 409 364 373 12;
#X obj 43 211 del;
#X obj 43 261 spigot;
#X obj 139 72 tgl 15 0 empty empty empty 17 7 0 10 -4160 -257985 -1
1 1;
#X obj 234 86 expr \$2-\$1;
#X obj 145 216 random;
#X obj 234 36 loadbang;
#X obj 82 235 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1
1;
#X obj 50 47 inlet;
#X obj 43 286 outlet;
#X obj 50 104 t b f;
#X obj 113 191 t b b;
#X obj 158 72 bng 10 250 50 0 empty empty empty 17 7 0 10 -257985 -4160
-1;
#X text 2 7 <<<;
#X text 322 7 >>>;
#X text 2 337 <<<;
#X text 322 337 >>>;
#X obj 118 285 inlet;
#X obj 118 310 unpack f f;
#X obj 204 305 expr $f2-$f1;
#X floatatom 234 197 5 0 0 0 range - -;
#X floatatom 289 223 5 0 0 0 start - -;
#X obj 289 117 f \$1;
#X obj 145 242 +;
#X obj 234 61 t b b;
#X floatatom 171 69 5 0 0 1 r - -;
#X obj 288 278 outlet;
#X connect 0 0 1 0;
#X connect 1 0 8 0;
#X connect 1 0 11 0;
#X connect 2 0 9 0;
#X connect 3 0 19 0;
#X connect 4 0 22 0;
#X connect 5 0 23 0;
#X connect 6 0 1 1;
#X connect 7 0 2 0;
#X connect 9 0 11 0;
#X connect 9 1 6 0;
#X connect 10 0 0 0;
#X connect 10 1 4 0;
#X connect 11 0 10 0;
#X connect 16 0 17 0;
#X connect 16 0 18 0;
#X connect 17 0 20 0;
#X connect 18 0 19 0;
#X connect 19 0 4 1;
#X connect 20 0 22 1;
#X connect 21 0 20 0;
#X connect 22 0 0 1;
#X connect 22 0 24 0;
#X connect 22 0 25 0;
#X connect 23 0 3 0;
#X connect 23 1 21 0;
#X coords 0 -1 1 1 85 40 1 135 50;

62
puredata/retro2.pd Normal file
View file

@ -0,0 +1,62 @@
#N canvas 805 23 356 386 12;
#X obj 45 286 del;
#X obj 22 310 spigot;
#X obj 139 72 tgl 15 0 empty empty empty 17 7 0 10 -4160 -257985 -1
0 1;
#X obj 76 186 random;
#X obj 236 99 loadbang;
#X obj 106 289 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0
1;
#X obj 22 36 inlet;
#X obj 22 342 outlet;
#X obj 157 72 bng 10 250 50 0 empty empty empty 17 7 0 10 -257985 -4160
-1;
#X obj 236 29 inlet;
#X floatatom 165 140 5 0 0 3 range - -;
#X floatatom 238 180 5 0 0 3 start - -;
#X obj 76 250 +;
#X floatatom 170 69 5 0 0 1 r - -;
#X obj 173 317 outlet;
#X msg 202 238 stop;
#X obj 22 66 sel 1 0;
#X obj 202 212 t b b;
#X msg 243 255 0;
#X msg 161 236 1;
#X obj 22 104 t b b b;
#X obj 45 154 t b b;
#X obj 236 51 expr $f2-$f1 \; $f1;
#X obj 236 121 expr \$2-\$1 \; \$1;
#X text 20 7 retro->retro2: start bang/ stop->delay stop;
#X text 24 208 <-- initial bang bypass;
#X connect 0 0 1 0;
#X connect 1 0 7 0;
#X connect 1 0 8 0;
#X connect 2 0 16 0;
#X connect 3 0 12 0;
#X connect 4 0 23 0;
#X connect 5 0 1 1;
#X connect 6 0 2 0;
#X connect 8 0 21 0;
#X connect 9 0 22 0;
#X connect 10 0 3 1;
#X connect 11 0 12 1;
#X connect 12 0 0 1;
#X connect 12 0 13 0;
#X connect 12 0 14 0;
#X connect 15 0 0 0;
#X connect 16 0 20 0;
#X connect 16 1 17 0;
#X connect 17 0 15 0;
#X connect 17 1 18 0;
#X connect 18 0 5 0;
#X connect 19 0 5 0;
#X connect 20 0 1 0;
#X connect 20 1 21 0;
#X connect 20 2 19 0;
#X connect 21 0 0 0;
#X connect 21 1 3 0;
#X connect 22 0 10 0;
#X connect 22 1 11 0;
#X connect 23 0 10 0;
#X connect 23 1 11 0;
#X coords 0 -1 1 1 85 40 1 135 50;

55
puredata/rscan.pd Normal file
View file

@ -0,0 +1,55 @@
#N canvas 125 216 408 490 12;
#X obj 120 365 f;
#X obj 84 365 + 1;
#X obj 218 340 sel 1;
#X msg 138 340 0;
#X obj 218 315 ==;
#X obj 236 205 - 1;
#X obj 39 81 inlet;
#X msg 179 225 0;
#X msg 106 224 1;
#X obj 106 165 t b b a;
#X msg 39 225 0;
#X obj 236 230 max 0;
#X text 5 4 <<<;
#X text 345 4 >>>;
#X text 5 454 <<<;
#X text 345 454 >>>;
#X text 39 22 generate numbers from 0 to given (inlet-1);
#X text 84 82 1-command:;
#X text 124 98 - number-> target value;
#X text 124 112 - if <= 0 \, stop immediately;
#X obj 39 135 moses 1;
#X obj 278 385 outlet;
#X text 278 364 2#end bng;
#X msg 39 310 bang;
#X obj 120 390 t a a;
#X floatatom 152 415 5 0 0 0 - - -;
#X obj 152 439 outlet;
#X text 202 439 1#values;
#X obj 91 271 retro \$1 \$2;
#X text 39 38 arguments: random range => \$1 ~ \$2;
#X connect 0 0 1 0;
#X connect 0 0 24 0;
#X connect 1 0 0 1;
#X connect 2 0 7 0;
#X connect 2 0 21 0;
#X connect 3 0 0 1;
#X connect 4 0 2 0;
#X connect 5 0 11 0;
#X connect 6 0 20 0;
#X connect 7 0 28 0;
#X connect 8 0 28 0;
#X connect 9 0 8 0;
#X connect 9 1 3 0;
#X connect 9 2 5 0;
#X connect 10 0 23 0;
#X connect 10 0 28 0;
#X connect 11 0 4 1;
#X connect 20 0 10 0;
#X connect 20 1 9 0;
#X connect 23 0 21 0;
#X connect 24 0 4 0;
#X connect 24 1 25 0;
#X connect 25 0 26 0;
#X connect 28 0 0 0;

59
puredata/rscan2.pd Normal file
View file

@ -0,0 +1,59 @@
#N canvas 92 211 408 490 12;
#X obj 120 365 f;
#X obj 84 365 + 1;
#X obj 218 340 sel 1;
#X msg 138 340 0;
#X obj 218 315 ==;
#X obj 236 205 - 1;
#X obj 39 81 inlet;
#X msg 179 225 0;
#X msg 106 224 1;
#X obj 106 165 t b b a;
#X msg 39 225 0;
#X obj 236 230 max 0;
#X text 5 4 <<<;
#X text 345 4 >>>;
#X text 5 454 <<<;
#X text 345 454 >>>;
#X text 39 22 generate numbers from 0 to given (inlet-1);
#X text 84 82 1-command:;
#X text 124 98 - number-> target value;
#X text 124 112 - if <= 0 \, stop immediately;
#X obj 39 135 moses 1;
#X obj 278 385 outlet;
#X text 278 364 2#end bng;
#X msg 39 310 bang;
#X obj 120 390 t a a;
#X floatatom 152 415 5 0 0 0 - - -;
#X obj 152 439 outlet;
#X text 202 439 1#values;
#X text 39 38 arguments: random range => \$1 ~ \$2;
#X obj 96 268 retro2 \$1 \$2;
#X obj 289 242 inlet;
#X text 288 262 2-random range;
#X connect 0 0 1 0;
#X connect 0 0 24 0;
#X connect 1 0 0 1;
#X connect 2 0 7 0;
#X connect 2 0 21 0;
#X connect 3 0 0 1;
#X connect 4 0 2 0;
#X connect 5 0 11 0;
#X connect 6 0 20 0;
#X connect 7 0 29 0;
#X connect 8 0 29 0;
#X connect 9 0 8 0;
#X connect 9 1 3 0;
#X connect 9 2 5 0;
#X connect 10 0 23 0;
#X connect 10 0 29 0;
#X connect 11 0 4 1;
#X connect 20 0 10 0;
#X connect 20 1 9 0;
#X connect 23 0 21 0;
#X connect 24 0 4 0;
#X connect 24 1 25 0;
#X connect 25 0 26 0;
#X connect 29 0 0 0;
#X connect 30 0 29 1;
#X coords 0 -1 1 1 100 60 1 90 250;

363
server.js Normal file
View file

@ -0,0 +1,363 @@
//dotenv
require('dotenv').config();
//ffmpeg
var ffmpeg = require('fluent-ffmpeg');
//nextcloud client
const { Client, Server, GetFilesRecursivelyCommand, CommandStatus } = require("nextcloud-node-client");
const server = new Server({
basicAuth: {
password: process.env.nextcloud_PASSWD,
username: process.env.nextcloud_ID,
},
url: process.env.nextcloud_URL,
});
//built-in
const path = require("path");
const fs = require('fs').promises;
const util = require('util');
// const { pipeline } = require('stream');
// const pump = util.promisify(pipeline);
//uuid
const {
v1: uuidv1,
v4: uuidv4,
} = require('uuid');
//moment
const moment = require("moment-timezone");
//fastify
const fastify = require("fastify")({
logger: false,
});
fastify.register(require("fastify-static"), {
root: path.join(__dirname, "public"),
prefix: "/"
});
fastify.register(require("fastify-formbody"));
fastify.register(require("fastify-multipart"));
fastify.register(require("point-of-view"), {
engine: {
handlebars: require("handlebars")
}
});
//socket.io
var io = require("socket.io")(fastify.server, {
pingInterval: 1000,
pingTimeout: 3000
});
//get '/'
fastify.get("/", function (request, reply) {
reply.view("/src/pages/parade.html", {});
});
//get '/live
fastify.get("/live", function (request, reply) {
reply.view("/src/pages/live.html", {});
});
//get '/preview/:foldername' --> request.params.foldername
fastify.get("/preview/:foldername", function (request, reply) {
reply.view("/src/pages/preview.html", {});
});
//get '/entry', '/entry/', '/en/entry', '/en/entry/', '/entry/test'
["/entry", "/entry/", "/en/entry", "/en/entry/", "/entry/test"].forEach(function(item) {
fastify.get(item, async function (request, reply) {
//console.log(request.url);
let url = request.url.replace(/\/$/, '');
//get list
let list = await fs.readdir('/media/storage/public/sound-parade/');
list.reverse();
// console.log(list);
let folders = [];
for (const item of list) {
let json = await fs.readFile('/media/storage/public/sound-parade/' + item + '/fields.json')
.catch((err) => {
console.error(err);
});
if (json != undefined) {
var fields = JSON.parse(json.toString('utf8'));
folders.push({
foldername: item,
group: fields.group,
title: fields.title,
comment: fields.comment,
});
}
}
// console.log(folders);
//
if (url == "/entry") {
reply.view("/src/pages/entry.html", {
list: folders,
});
} else if (url == "/en/entry") {
reply.view("/src/pages/entry.en.html", {
list: folders,
});
} else if (url == "/entry/test") {
reply.view("/src/pages/entry.test.html", {
list: folders,
});
}
});
});
// --> https://stackoverflow.com/a/40899275
// all the regex didn't work for me -- a 'last resort' method
//get '/entries'
fastify.get("/entries", async function (request, reply) {
//get list
let list = await fs.readdir('/media/storage/public/sound-parade/');
reply.send(list);
});
//get '/fields'
fastify.get("/fields", async function (request, reply) {
//get list
let list = await fs.readdir('/media/storage/public/sound-parade/');
//list.reverse();
// console.log(list);
let folders = [];
for (const item of list) {
var fields = JSON.parse((await fs.readFile('/media/storage/public/sound-parade/' + item + '/fields.json')).toString('utf8'));
folders.push({
foldername: item,
group: fields.group,
title: fields.title,
comment: fields.comment,
});
}
reply.send(folders);
});
//get '/delete'
fastify.get("/delete/:foldername/:pass", async function (request, reply) {
//get pw
var fields = JSON.parse((await fs.readFile('/media/storage/public/sound-parade/' + request.params.foldername + '/fields.json')).toString('utf8'));
var res = false;
if (fields.pass == request.params.pass) {
// console.log('good pass');
//ok. let's move it to trashbin. (mv ../sound-parade.trash)
// await fs.rename('/media/storage/public/sound-parade/' + request.params.foldername, '/media/storage/public/sound-parade.trash/' + request.params.foldername);
const client = new Client(server);
const folder = await client.getFolder("/Storage/public/sound-parade/" + request.params.foldername);
await folder.move("/Storage/public/sound-parade.trash/" + request.params.foldername);
//
res = true;
} else {
// console.log('wrong pass');
}
reply.send({ result: res });
});
//post on '/entry'
fastify.post("/entry", async function (request, reply) {
// stores files to tmp dir and return paths
const files = await request.saveRequestFiles().catch(err => {
console.error(err);
});
let audiofile = files.find(f => f.fieldname == 'audiofile');
let pixelfile = files.find(f => f.fieldname == 'pixels');
let tmpdir = path.dirname(audiofile.filepath);
// console.log(audiofile.fields.message.value);
console.log("-- hi."); // got all files.
//conversion needed?
var conversion = false;
if (path.extname(audiofile.filename) !== ".mp3") {
conversion = true;
console.log("-- well.."); //conversion. is scheduled.
} else {
console.log("-- good"); //no conversion_
}
//upload
const client = new Client(server);
console.log("-- ready"); //file server opened
//create unique folder ==> timestamp + uuid
const folder = await client.createFolder("Storage/public/sound-parade/" + moment().tz('Asia/Seoul').format('YYYYMMDD-HHmmss-') + uuidv1());
//
const json = await folder.createFile("fields.json", Buffer.from(JSON.stringify({
group: audiofile.fields.group.value,
title: audiofile.fields.title.value,
comment: audiofile.fields.comment.value,
pass: audiofile.fields.pass.value
})));
const image = await folder.createFile("pixels.png", await fs.readFile(pixelfile.filepath));
//
if (conversion) {
console.log('---- hi conv');
//save original file as is. + we have scheduled a conversion.
let afile = await fs.readFile(audiofile.filepath)
.catch(err => {
console.error(err);
});
//
if (afile != undefined) {
const file = await folder.createFile(audiofile.filename, afile)
.catch(err => {
console.error(err);
console.log('---- createFile err.');
console.log('afile', afile);
});
} else {
console.log('afile == undef!');
}
console.log('---- yes conv');
//
} else {
console.log('---- no conv');
//rename & save original file.
const file = await folder.createFile("audio.mp3", await fs.readFile(audiofile.filepath).catch(err => console.error(err)));
}
console.log("-- saved"); //saved in file server.
//conversion needed?
var converted = false;
if (conversion) {
console.log("-- mp3..."); //converting to mp3...
function converter() {
return new Promise((resolve, reject) => {
//
let outputFile = tmpdir + "/converted.mp3";
// --> https://stackoverflow.com/a/36109219 (a quick tip on fluent-ffmpeg)
ffmpeg()
.addInput(audiofile.filepath)
.on("error", function(err) {
console.log("-- err:", err);
reject(err);
})
.on("end", function() {
console.log("-- fine."); //conversion succeesful
converted = true;
resolve(outputFile);
})
.outputOptions('-b:a 192000')
.output(outputFile)
.run();
});
}
await converter().catch((err) => { console.error(err); });
if (converted) {
const file = await folder.createFile("audio.mp3", await fs.readFile(tmpdir + '/converted.mp3'));
console.log("-- done");
}
}
console.log("-- well done");
reply.send('done!');
//reply.redirect('/submit');
});
//socket.io
//var score = require("./public/score.json");
//
//there will be 16 rooms called: "room0", "room1", ... , "room15"
//if any other room is requested.. well, we will simply reject.
var roommax = 16;
//
io.on("connection", function(socket) {
console.log("someone connected.");
socket.on("disconnect", function() { console.log("someone disconnected."); });
socket.on("room", function(room, fn) {
// parseInt(room)
if (room >= 0 && room < roommax) {
socket.join("room" + room);
fn(true);
} else {
fn(false);
}
});
socket.on("flow", function(req) {
io.emit("flow", req);
});
});
//
var pointer = 0; // pointer : 0 ~ (length-1)
var looper;
(looper = function(timeout) {
setTimeout(async function() {
//pointer = 20;
// console.log(score[pointer]);
//
for (var index = 0; index < roommax; index++) {
// NOTE: 'pointer' must be 'remembered' since 'pointer' will increase almost immediately! pass as argument => 'pointed'
// NOTE: 'index' is same => 'indexed'
setTimeout(function(pointed, indexed) {
// io.to("room" + indexed).emit("post", score[pointed]);
io.to("room" + indexed).emit("post", pointed);
// }, score[pointer].object.showtime * index, pointer, index);
}, 30000 * index, pointer, index);
}
//var timegap = 10000 + Math.random()*(40000);
var timegap = 1000 + Math.random()*(40000);
console.log(timegap);
//get # list
let list = await fs.readdir('/media/storage/public/sound-parade/');
console.log(list.length);
//loop over...
pointer++;
if (pointer >= list.length) pointer = 0;
looper(timegap);
}, timeout);
})(1000);
//listen
fastify.listen(10000, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
console.log(`Your app is listening on ${address}`)
fastify.log.info(`server listening on ${address}`)
});

386
src/pages/entry.en.html Normal file
View file

@ -0,0 +1,386 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Walking Towards the Flow | entry</title>
<link rel="stylesheet" href="/default.css" />
<style>
#defaultCanvas0 {
display: block;
}
.clear {
display: block;
}
img {
width: 100%;
max-width: 500px;
}
#p5 {
display: inline-block;
}
.tools {
padding: 1em 1em;
display: inline-block;
vertical-align: top;
}
.penselect {
margin: 1em 0em;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div class="content">
<div class="first"><a href="/entry">KR</a></div>
<div class="second"><a href="/">parade</a></div>
<div class="notice">
<section>
<h1>Walking &nbsp;Towards &nbsp;the Flow</h1>
<p>
Have you ever seen a parade outside the procession? Drums and shouts are heard in the distance, many beings shouting, flowing by, groups appearing and moving away...
</p>
<p>
«Walking Toward the Flow» is a sound parade that creates a procession of sounds in the online space. Anyone can participate. This parade is made by collecting the sounds you sent.
</p>
<p>
«Walking Toward the Flow» consists of a total of five 'flock' of sounds: the sounds of promises, the sounds of speaking, the sounds of the body, the sounds of objects around, and the sounds of someone. They will flow in groups for about 3 minutes each and a total of 15 minutes or more.
</p>
<p>
On the night of January 27 (Thursday), the «Walking to the Flow» parade will be broadcasted along with a live performance at the 'the Night of Ideas' hosted by Cultural Service of the Embassy of France in Korea, Liszt Institut Hungarian Cultural Center Seoul and Embassy of Belgium in Seoul and sponsored by UNESCO Korea and Institut Français. Thank you for your participation and interest.
</p>
<div class="info">
<p>
Recruiting sounds<br/>
January 17, 2022 (Mon) January 26, 2022 (Wed)
</p>
<p>
Live performance<br/>
January 27th, 2022 (Thursday)<br>
20:45-21:00 (KST), 12:45-13:00 (CET)<br/>
</p>
<p>
Place<br/>
<a href="https://walkingtowardstheflow.xyz">walkingtowardstheflow.xyz</a>, <a href="https://www.youtube.com/c/franceencoree">France en Corée - YouTube channel</a>
</p>
<p>
artists<br/>
<a href="https://dianaband.info" target="_blank">diana band</a> X <a href="https://cgyoon.kr/" target="_blank">Choong-geun Yoon</a> (Inquiry: wonjung24@gmail.com)
</p>
<p>
Parade Composition<br/>
Flock0 promises — Flock1 flags — Flock2 bodies — Flock3 objects — Flock4 someone
</p>
</div>
</section>
<section class="participation">
<h1>How to participate</h1>
<ol>
<li>1. Record the sound for about 30 seconds with a recording device or phone according to the characteristics of the flock.
<ol class="category">
<li>• Flock1 flags — Speak out five times about what you like or value.</li>
<li>• Flock2 bodies — Record the sound of your body. Applause and whistling.</li>
<li>• Flock3 objects — Find the sound of things around you. Bubble wrap sound, Beads sound.</li>
<li>• Flock4 someone — Collect the sound of others. animal, sound of water, place to take a walk.</li>
</ol>
</li>
<li>2. Upload the recorded sound file.</li>
<li>3. After filling out the title and description of the sound, draw the shape of the sound and submit it.</li>
</ol>
</section>
<section>
<h1>Sound Submission</h1>
<form id="form" method="POST" enctype="multipart/form-data">
<ul class="submit">
<li>
<h3>1. Sound Type</h3>
<ul>
<li>
<input type="radio" name="group" autocomplete="off" value="flag" /> Flock1 flags
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="body" /> Flock2 bodies
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="object" /> Flock3 objects
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="any" required /> Flock4 someone
</li>
</ul>
</li>
<li>
<h3>2. Sound File</h3>
<input type="file" name="audiofile" autocomplete="off" required />
</li>
<li>
<h3>3. Title</h3>
<input id="title" name="title" type="text" autocomplete="off" required />
</li>
<li>
<h3>4. Description of the sound</h3>
<input id="comment" name="comment" type="text" autocomplete="off" required />
</li>
<!-- pixels.png ~ made with p5.js -->
<li class="noscroll">
<h3>5. Draw an image of the sound</h3>
<div>
<div id="p5">
</div>
<div class="tools">
<div>
<input class="penselect" type="radio" value="pencil" name="penselect" id="penselect-pencil" autocomplete="off" checked>
<label for="penselect-pencil">pencil</label>
</div>
<div>
<input class="penselect" type="radio" value="erasor" name="penselect" id="penselect-erasor" autocomplete="off">
<label for="penselect-erasor">erasor</label>
</div>
</div>
</div>
<button type="button" class="clear">Erase all!</button>
</li>
<li>
<h3>6. passward is 2-digit numbers. It's required if you delete it.</h3>
<input id="pass" name="pass" type="text" maxlength="2" pattern="^\d{2}$" title="암호는 숫자x2개로 해주세요." required />
</li>
<li>
<input id="submit" type="submit" value="Upload" />
</li>
</ul>
<p>
↓ 
</p>
</form>
</section>
<section>
<h1>Sound &nbsp;List</h1>
<p>The sounds you submitted will be stacked in chronological order in the list below. By clicking on each shape, you can hear the sound corresponding to the shape and check the information about the sound.</p>
{{#each list}}
<div class="items" foldername="{{this.foldername}}">
<details>
<summary><img class="drawing" src="https://p.dianaband.info/public/sound-parade/{{this.foldername}}/pixels.png" /></summary>
<audio class="sound" preload="none" controls>
<source src="https://p.dianaband.info/public/sound-parade/{{this.foldername}}/audio.mp3" type="audio/mpeg">
</audio>
<ul class="soundinfo"><hr>
<li>Group | <div class="group">{{this.group}}</div></li><hr>
<li>Title | <div class="title">{{this.title}}</div></li><hr>
<li>Desc. | <div class="comment">{{this.comment}}</div></li><hr>
</ul>
<button class="preview" type="button" onclick="javascript:window.open('/preview/{{this.foldername}}', '_blank');">Preview</button>
<button class="delete" type="button" onclick="del(this)">Delete</button>
</details>
</div>
{{/each}}
</section>
</div><!-- div class="notice" -->
</div><!-- div class="content" -->
<script>
// -- https://stackoverflow.com/a/39577640
function dataURLtoBlob(dataURL) {
let array, binary, i, len;
binary = atob(dataURL.split(",")[1]);
array = [];
i = 0;
len = binary.length;
while (i < len) {
array.push(binary.charCodeAt(i));
i++;
}
return new Blob([new Uint8Array(array)], {
type: "image/png",
});
}
var p = [];
var cols = 17;
var rows = 17;
var unit = 15; //px
var img;
var penselect = "pencil";
var scrollable = true;
function setup() {
var cnv = createCanvas(cols*unit+2, rows*unit+2);
cnv.parent("p5");
img = createGraphics(cols*unit+2, rows*unit+2);
document.querySelectorAll('.penselect').forEach(item => item.onclick = () => {
penselect = document.querySelector('input[name="penselect"]:checked').value;
});
//
for (var i = 0; i < cols*rows; i++) p.push(0);
document.querySelectorAll('.clear').forEach(item => item.onclick = () => {
for (let a = 0; a < p.length; a++) p[a] = 0;
});
}
function draw() {
//clear
clear();
//draw the grid
stroke(255);
strokeWeight(0.2);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
noFill();
rect(c*unit+1, r*unit+1, unit, unit);
}
}
//pointer
fill(0, 255, 0);
strokeWeight(0);
//mouse way
circle(mouseX, mouseY, 10);
if (mouseIsPressed && mouseButton === LEFT) {
//find slot under the pointer
var mouseC = int(mouseX/unit);
var mouseR = int(mouseY/unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
if (penselect == "pencil") p[mouseC*cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC*cols + mouseR] = 0;
}
}
//touch way
if (touches.length > 0) {
circle(touches[0].x, touches[0].y, 10);
//find slot under the pointer
var mouseC = int(touches[0].x/unit);
var mouseR = int(touches[0].y/unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
// if (scrollable == true) {
// scrollable = false;
// // firefox browser @ my android phone -> url bar auto-hiding kills drawing experience.
// // --> https://stackoverflow.com/a/63221105
// document.body.style.marginTop = `-${window.pageYOffset}px`;
// document.body.style.position = 'fixed';
// document.body.style.overflowY = 'scroll';
// }
if (penselect == "pencil") p[mouseC*cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC*cols + mouseR] = 0;
} else {
// if (scrollable == false) {
// scrollable = true;
// // firefox browser @ my android phone -> url bar auto-hiding kills drawing experience.
// // --> https://stackoverflow.com/a/63221105
// document.body.style.position = '';
// document.body.style.overflowY = '';
// if (document.body.style.marginTop) {
// const scrollTop = -parseInt(document.body.style.marginTop, 10);
// document.body.style.marginTop = '';
// window.scrollTo(window.pageXOffset, scrollTop);
// }
// }
}
}
//draw img
image(img, 0, 0);
img.clear();
img.strokeWeight(0);
img.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c*cols + r] == 1) {
img.rect(c*unit+1, r*unit+1, unit, unit);
}
}
}
}
function submitForm(event) {
//TODO : first check if there is a drawing or not. (pixels)
//
var submit = document.getElementById("submit");
submit.setAttribute('disabled', 'disabled');
select("html").style('cursor', 'progress');
//
var form = document.getElementById("form");
var fd = new FormData(form);
//
var unit = 45; //mask&override 'unit' -> for crispy png.
var img2 = createGraphics(cols*unit+2, rows*unit+2);
img2.clear();
img2.strokeWeight(0);
img2.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c*cols + r] == 1) {
img2.rect(c*unit+1, r*unit+1, unit, unit);
}
}
}
var dataurl = img2.elt.toDataURL();
var pixels = dataURLtoBlob(dataurl);
fd.append("pixels", pixels, "pixels.png");
for(var pair of fd.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
//
var request = new XMLHttpRequest();
request.open("POST", "/entry");
request.onload = () => {
alert('Thank you!');
location.reload();
//location.assign("/en/submit");
}
request.send(fd);
//
event.preventDefault();
}
const form = document.getElementById('form');
form.addEventListener('submit', submitForm);
//// ---- for 'list' rendering ----
function del(that) {
console.log();
let text;
let pass = prompt("Please enter password!", "2-digit numbers");
if (/^\d{2}$/.test(pass)) {
var target = that.parentElement.parentElement.getAttribute('foldername');
const trydelete = async () => {
const response = await fetch('/delete/' + target + '/' + pass);
if (response.ok) {
const json = await response.json();
return Promise.resolve(json);
} else {
return Promise.reject('no response.');
}
}
trydelete().then((resp) => {
if (resp.result) {
alert("Delete Success!");
location.reload();
} else {
alert("Delete Failed-");
}
}).catch(console.log);
} else {
alert("Reminder: Password was 2-digit numbers...");
}
}
</script>
</body>
</html>

388
src/pages/entry.html Normal file
View file

@ -0,0 +1,388 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>흐름을 향하여 걷는 | 소개</title>
<link rel="stylesheet" href="/default.css" />
<style>
#defaultCanvas0 {
display: block;
}
.clear {
display: block;
}
img {
width: 100%;
max-width: 500px;
}
#p5 {
display: inline-block;
}
.tools {
padding: 1em 1em;
display: inline-block;
vertical-align: top;
}
.penselect {
margin: 1em 0em;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div class="content">
<div class="first"><a href="/en/entry">EN</a></div>
<div class="second"><a href="/" target="_blank">퍼레이드</a></div>
<div class="notice">
<section>
<h1>흐름을 &nbsp;향하여 &nbsp;걷는</h1>
<p>
퍼레이드를 행렬 밖에서 바라본 적이 있나요? 북소리와 함성소리가 멀리서 들리고, 많은 존재들이 외치고, 흐르듯 지나가고, 무리가 등장하고, 멀어지는......
</p>
<p>
«흐름을 향하여 걷는»은 온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다. 누구나 참여할 수 있는 이 퍼레이드는 여러분이 보내주시는 소리가 모여 만들어집니다.
</p>
<p>
«흐름을 향하여 걷는»은 총 다섯 개의 소리 무리로 이루어집니다. 약속하는 소리들, 말하는 소리들, 몸이 내는 소리들, 주변의 사물들, 그리고 누군가의 소리들이 무리를 지어 각 3분 내외 총 15분 가량 흘러가게 됩니다.
</p>
<p>
1월 27일(목) 밤, «흐름을 향하여 걷는» 퍼레이드는 라이브 공연과 함께 ‘사유의 밤’ 행사에서 송출될 예정입니다. ‘사유의 밤’은 주한 프랑스대사관 문화과, 주한 리스트 헝가리 문화원, 주한 벨기에 대사관이 주최하고, 유네스코 한국위원회와 프랑스 해외문화진흥원이 후원하여 개최됩니다.
많은 참여 및 관심 부탁드립니다.
</p>
<div class="info">
<p>
소리 모집<br/>
2022년 1월 17일(월)2022년 1월 26일(수)
</p>
<p>
라이브 공연<br/>
2022년 1월 27일(목)<br>
20:45-21:00 (KST), 12:45-13:00 (CET)<br/>
</p>
<p>
장소<br/>
<a href="https://walkingtowardstheflow.xyz">walkingtowardstheflow.xyz</a><br><a href="https://www.youtube.com/c/franceencoree">주한프랑스대사관 문화과 유튜브 채널</a>
</p>
<p>
작가<br/>
<a href="https://dianaband.info" target="_blank">다이애나밴드</a> X <a href="https://cgyoon.kr/" target="_blank">윤충근</a> (문의: wonjung24@gmail.com)
</p>
<p>
퍼레이드 구성<br/>
무리0 약속들 — 무리1 깃발들 — 무리2 신체들 — 무리3 사물들 — 무리4 누구들
</p>
</div>
</section>
<section class="participation">
<h1>참여 방법</h1>
<ol>
<li>1. 무리의 특성에 따라 녹음기기나 핸드폰으로 30초 가량의 소리를 녹음해주세요.
<ol class="category">
<li>• 무리1 깃발들 — 좋아하는 것, 가치에 대해 다섯 번 외쳐주세요.</li>
<li>• 무리2 신체들 — 몸에서 나는 소리를 녹음해 보아요. 박수, 휘파람도 좋아요.</li>
<li>• 무리3 사물들 — 주변 사물들의 소리를 찾아 주세요. 뽁뽁이 소리, 구슬 소리</li>
<li>• 무리4 누구들 — 누구의 소리를 모아주세요. 반려동물, 물 소리, 산책의 장소</li>
</ol>
</li>
<li>2. 녹음한 소리 파일을 업로드해주세요.</li>
<li>3. 소리의 제목과 묘사을 입력한 뒤, 소리의 모양을 그려 제출해주세요.</li>
</ol>
</section>
<section>
<h1>소리 제출</h1>
<form id="form" method="POST" enctype="multipart/form-data">
<ul class="submit">
<li>
<h3>1. 소리 유형</h3>
<ul>
<li>
<input type="radio" name="group" autocomplete="off" value="flag" /> 무리1 깃발들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="body" /> 무리2 신체들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="object" /> 무리3 사물들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="any" required /> 무리4 누구들
</li>
</ul>
</li>
<li>
<h3>2. 소리 파일</h3>
<input type="file" name="audiofile" autocomplete="off" required />
</li>
<li>
<h3>3. 소리 제목</h3>
<input id="title" name="title" type="text" autocomplete="off" required />
</li>
<li>
<h3>4. 소리 묘사</h3>
<input id="comment" name="comment" type="text" autocomplete="off" required />
</li>
<!-- pixels.png ~ made with p5.js -->
<li class="noscroll">
<h3>5. 소리 모양</h3>
<div>
<div id="p5">
</div>
<div class="tools">
<div>
<input class="penselect" type="radio" value="pencil" name="penselect" id="penselect-pencil" autocomplete="off" checked>
<label for="penselect-pencil">연필</label>
</div>
<div>
<input class="penselect" type="radio" value="erasor" name="penselect" id="penselect-erasor" autocomplete="off">
<label for="penselect-erasor">지우개</label>
</div>
</div>
</div>
<button type="button" class="clear">다시그리기!</button>
</li>
<li>
<h3>6. 비밀 번호</h3>
<input id="pass" name="pass" type="text" maxlength="2" pattern="^\d{2}$" title="암호는 숫자x2개로 해주세요." required />
<small>두 자리 숫자를 입력해주세요. 업로드한 소리를 삭제할 때 쓰입니다.</small>
</li>
<li>
<input id="submit" type="submit" value="보내기!" />
</li>
</ul>
<p>
↓ 
</p>
</form>
</section>
<section>
<h1>소리 목록</h1>
<p>제출해주신 소리는 아래 목록에 시간순으로 쌓입니다. 각각의 모양을 클릭하면, 모양에 해당하는 소리를 듣고 소리에 대한 정보를 확인할 수 있습니다.</p>
{{#each list}}
<div class="items" foldername="{{this.foldername}}">
<details>
<summary><img class="drawing" src="https://p.dianaband.info/public/sound-parade/{{this.foldername}}/pixels.png" /></summary>
<audio class="sound" preload="none" controls>
<source src="https://p.dianaband.info/public/sound-parade/{{this.foldername}}/audio.mp3" type="audio/mpeg">
</audio>
<ul class="soundinfo"><hr>
<li>유형 | <div class="group">{{this.group}}</div></li><hr>
<li>제목 | <div class="title">{{this.title}}</div></li><hr>
<li>묘사 | <div class="comment">{{this.comment}}</div></li><hr>
</ul>
<button class="preview" type="button" onclick="javascript:window.open('/preview/{{this.foldername}}', '_blank');">미리보기</button>
<button class="delete" type="button" onclick="del(this)">삭제</button>
</details>
</div>
{{/each}}
</section>
</div><!-- div class="notice" -->
</div><!-- div class="content" -->
<script>
// -- https://stackoverflow.com/a/39577640
function dataURLtoBlob(dataURL) {
let array, binary, i, len;
binary = atob(dataURL.split(",")[1]);
array = [];
i = 0;
len = binary.length;
while (i < len) {
array.push(binary.charCodeAt(i));
i++;
}
return new Blob([new Uint8Array(array)], {
type: "image/png",
});
}
var p = [];
var cols = 17;
var rows = 17;
var unit = 15; //px
var img;
var penselect = "pencil";
var scrollable = true;
function setup() {
var cnv = createCanvas(cols*unit+2, rows*unit+2);
cnv.parent("p5");
img = createGraphics(cols*unit+2, rows*unit+2);
document.querySelectorAll('.penselect').forEach(item => item.onclick = () => {
penselect = document.querySelector('input[name="penselect"]:checked').value;
});
//
for (var i = 0; i < cols*rows; i++) p.push(0);
document.querySelectorAll('.clear').forEach(item => item.onclick = () => {
for (let a = 0; a < p.length; a++) p[a] = 0;
});
}
function draw() {
//clear
clear();
//draw the grid
stroke(255);
strokeWeight(0.2);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
noFill();
rect(c*unit+1, r*unit+1, unit, unit);
}
}
//pointer
fill(0, 255, 0);
strokeWeight(0);
//mouse way
circle(mouseX, mouseY, 10);
if (mouseIsPressed && mouseButton === LEFT) {
//find slot under the pointer
var mouseC = int(mouseX/unit);
var mouseR = int(mouseY/unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
if (penselect == "pencil") p[mouseC*cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC*cols + mouseR] = 0;
}
}
//touch way
if (touches.length > 0) {
circle(touches[0].x, touches[0].y, 10);
//find slot under the pointer
var mouseC = int(touches[0].x/unit);
var mouseR = int(touches[0].y/unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
// if (scrollable == true) {
// scrollable = false;
// // firefox browser @ my android phone -> url bar auto-hiding kills drawing experience.
// // --> https://stackoverflow.com/a/63221105
// document.body.style.marginTop = `-${window.pageYOffset}px`;
// document.body.style.position = 'fixed';
// document.body.style.overflowY = 'scroll';
// }
if (penselect == "pencil") p[mouseC*cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC*cols + mouseR] = 0;
} else {
// if (scrollable == false) {
// scrollable = true;
// // firefox browser @ my android phone -> url bar auto-hiding kills drawing experience.
// // --> https://stackoverflow.com/a/63221105
// document.body.style.position = '';
// document.body.style.overflowY = '';
// if (document.body.style.marginTop) {
// const scrollTop = -parseInt(document.body.style.marginTop, 10);
// document.body.style.marginTop = '';
// window.scrollTo(window.pageXOffset, scrollTop);
// }
// }
}
}
//draw img
image(img, 0, 0);
img.clear();
img.strokeWeight(0);
img.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c*cols + r] == 1) {
img.rect(c*unit+1, r*unit+1, unit, unit);
}
}
}
}
function submitForm(event) {
//TODO : first check if there is a drawing or not. (pixels)
//
var submit = document.getElementById("submit");
submit.setAttribute('disabled', 'disabled');
select("html").style('cursor', 'progress');
//
var form = document.getElementById("form");
var fd = new FormData(form);
//
var unit = 45; //mask&override 'unit' -> for crispy png.
var img2 = createGraphics(cols*unit+2, rows*unit+2);
img2.clear();
img2.strokeWeight(0);
img2.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c*cols + r] == 1) {
img2.rect(c*unit+1, r*unit+1, unit, unit);
}
}
}
var dataurl = img2.elt.toDataURL();
var pixels = dataURLtoBlob(dataurl);
fd.append("pixels", pixels, "pixels.png");
for(var pair of fd.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
//
var request = new XMLHttpRequest();
request.open("POST", "/entry");
request.onload = () => {
alert('감사합니다!');
location.reload();
//location.assign("/submit");
}
request.send(fd);
//
event.preventDefault();
}
const form = document.getElementById('form');
form.addEventListener('submit', submitForm);
//// ---- for 'list' rendering ----
function del(that) {
console.log();
let text;
let pass = prompt("패스워드를 맞춰보세요!", "숫자2개");
if (/^\d{2}$/.test(pass)) {
var target = that.parentElement.parentElement.getAttribute('foldername');
const trydelete = async () => {
const response = await fetch('/delete/' + target + '/' + pass);
if (response.ok) {
const json = await response.json();
return Promise.resolve(json);
} else {
return Promise.reject('no response.');
}
}
trydelete().then((resp) => {
if (resp.result) {
alert("지우기 성공!");
location.reload();
} else {
alert("지우기 실패-");
}
}).catch(console.log);
} else {
alert("암호는 숫자2개...");
}
}
</script>
</body>
</html>

381
src/pages/entry.test.html Normal file
View file

@ -0,0 +1,381 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>흐름을 향하여 걷는 | 소개</title>
<link rel="stylesheet" href="/default.css" />
<style>
#defaultCanvas0 {
display: block;
}
.clear {
display: block;
}
img {
width: 100%;
max-width: 500px;
}
#p5 {
display: inline-block;
}
.tools {
padding: 1em 1em;
display: inline-block;
vertical-align: top;
}
.penselect {
margin: 1em 0em;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div class="content">
<div class="first"><a href="/en/entry">EN</a></div>
<div class="second"><a href="/" target="_blank"></a></div>
<div class="notice">
<section>
<h1>흐름을 &nbsp;향하여 &nbsp;걷는</h1>
<p>
퍼레이드를 행렬 밖에서 바라본 적이 있나요? 북소리와 함성소리가 멀리서 들리고, 많은 존재들이 외치고, 흐르듯 지나가고, 무리가 등장하고, 멀어지는......
</p>
<p>
«흐름을 향하여 걷는»은 온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다. 누구나 참여할 수 있는 이 퍼레이드는 여러분이 보내주시는 소리가 모여 만들어집니다.
</p>
<p>
«흐름을 향하여 걷는»은 총 다섯 개의 소리 무리로 이루어집니다. 약속하는 소리들, 말하는 소리들, 몸이 내는 소리들, 주변의 사물들, 그리고 누군가의 소리들이 무리를 지어 각 3분 내외 총 15분 가량 흘러가게 됩니다.
</p>
<p>
1월 27일(목) 밤, «흐름을 향하여 걷는» 퍼레이드는 라이브 공연과 함께 ‘사유의 밤’ 행사에서 송출될 예정입니다. ‘사유의 밤’은 주한 프랑스대사관 문화과, 주한 리스트 헝가리 문화원, 주한 벨기에 대사관이 주최하고, 유네스코 한국위원회와 프랑스 해외문화진흥원이 후원하여 개최됩니다.
많은 참여 및 관심 부탁드립니다.
</p>
<div class="info">
<p>
소리 모집<br/>
2022년 1월 17일(월)2022년 1월 26일(수)
</p>
<p>
라이브 공연<br/>
2022년 1월 27일(목)<br>
20:45-21:00 (KST), 12:45-13:00 (CET)<br/>
</p>
<p>
장소<br/>
<a href="https://walkingtowardstheflow.xyz">walkingtowardstheflow.xyz</a><br><a href="https://www.youtube.com/c/franceencoree">주한프랑스대사관 문화과 유튜브 채널</a>
</p>
<p>
작가<br/>
<a href="https://dianaband.info" target="_blank">다이애나밴드</a> X <a href="https://cgyoon.kr/" target="_blank">윤충근</a> (문의: wonjung24@gmail.com)
</p>
<p>
퍼레이드 구성<br/>
무리0 약속들 — 무리1 깃발들 — 무리2 신체들 — 무리3 사물들 — 무리4 누구들
</p>
</div>
</section>
<section class="participation">
<h1>참여 방법</h1>
<ol>
<li>1. 무리의 특성에 따라 녹음기기나 핸드폰으로 30초 가량의 소리를 녹음해주세요.
<ol class="category">
<li>• 무리1 깃발들 — 좋아하는 것, 가치에 대해 다섯 번 외쳐주세요.</li>
<li>• 무리2 신체들 — 몸에서 나는 소리를 녹음해 보아요. 박수, 휘파람도 좋아요.</li>
<li>• 무리3 사물들 — 주변 사물들의 소리를 찾아 주세요. 뽁뽁이 소리, 구슬 소리</li>
<li>• 무리4 누구들 — 누구의 소리를 모아주세요. 반려동물, 물 소리, 산책의 장소</li>
</ol>
</li>
<li>2. 녹음한 소리 파일을 업로드해주세요.</li>
<li>3. 소리의 제목과 묘사을 입력한 뒤, 소리의 모양을 그려 제출해주세요.</li>
</ol>
</section>
<section>
<h1>소리 제출</h1>
<form id="form" method="POST" enctype="multipart/form-data">
<ul class="submit">
<li>
<h3>1. 소리 유형</h3>
<ul>
<li>
<input type="radio" name="group" autocomplete="off" value="flag" /> 무리1 깃발들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="body" /> 무리2 신체들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="object" /> 무리3 사물들
</li>
<li>
<input type="radio" name="group" autocomplete="off" value="any" required /> 무리4 누구들
</li>
</ul>
</li>
<li>
<h3>2. 소리 파일</h3>
<input type="file" name="audiofile" autocomplete="off" required />
</li>
<li>
<h3>3. 소리 제목</h3>
<input id="title" name="title" type="text" autocomplete="off" required />
</li>
<li>
<h3>4. 소리 묘사</h3>
<input id="comment" name="comment" type="text" autocomplete="off" required />
</li>
<!-- pixels.png ~ made with p5.js -->
<li>
<h3>5. 소리 모양</h3>
<div>
<div id="p5">
</div>
<div class="tools">
<div>
<input class="penselect" type="radio" value="pencil" name="penselect" id="penselect-pencil" autocomplete="off" checked>
<label for="penselect-pencil">연필</label>
</div>
<div>
<input class="penselect" type="radio" value="erasor" name="penselect" id="penselect-erasor" autocomplete="off">
<label for="penselect-erasor">지우개</label>
</div>
</div>
</div>
<button type="button" class="clear">다시그리기!</button>
</li>
<li>
<h3>6. 비밀 번호</h3>
<input id="pass" name="pass" type="text" maxlength="2" pattern="^\d{2}$" title="암호는 숫자x2개로 해주세요." required />
<small>두 자리 숫자를 입력해주세요. 업로드한 소리를 삭제할 때 쓰입니다.</small>
</li>
<li>
<input id="submit" type="submit" value="보내기!" />
</li>
</ul>
</form>
</section>
<section>
<h1>소리 목록</h1>
<p>제출해주신 소리는 아래 목록에 시간순으로 쌓입니다. 각각의 모양을 클릭하면, 모양에 해당하는 소리를 듣고 소리에 대한 정보를 확인할 수 있습니다.</p>
{{#each list}}
<div class="items" foldername="{{this.foldername}}">
<details>
<summary><img class="drawing" src="https://p.dianaband.info/public/sound-parade/{{this.foldername}}/pixels.png" /></summary>
<audio class="sound" preload="none" controls>
<source src="https://p.dianaband.info/public/sound-parade/{{this.foldername}}/audio.mp3" type="audio/mpeg">
</audio>
<ul class="soundinfo"><hr>
<li>유형 | <div class="group">{{this.group}}</div></li><hr>
<li>제목 | <div class="title">{{this.title}}</div></li><hr>
<li>묘사 | <div class="comment">{{this.comment}}</div></li><hr>
</ul>
<button class="preview" type="button" onclick="javascript:window.open('/preview/{{this.foldername}}', '_blank');">미리보기</button>
<button class="delete" type="button" onclick="del(this)">삭제</button>
</details>
</div>
{{/each}}
</section>
</div><!-- div class="notice" -->
</div><!-- div class="content" -->
<script>
// -- https://stackoverflow.com/a/39577640
function dataURLtoBlob(dataURL) {
let array, binary, i, len;
binary = atob(dataURL.split(",")[1]);
array = [];
i = 0;
len = binary.length;
while (i < len) {
array.push(binary.charCodeAt(i));
i++;
}
return new Blob([new Uint8Array(array)], {
type: "image/png",
});
}
var p = [];
var cols = 17;
var rows = 17;
var unit = 15; //px
var img;
var penselect = "pencil";
var scrollable = true;
function setup() {
var cnv = createCanvas(cols*unit+2, rows*unit+2);
cnv.parent("p5");
img = createGraphics(cols*unit+2, rows*unit+2);
document.querySelectorAll('.penselect').forEach(item => item.onclick = () => {
penselect = document.querySelector('input[name="penselect"]:checked').value;
});
//
for (var i = 0; i < cols*rows; i++) p.push(0);
document.querySelectorAll('.clear').forEach(item => item.onclick = () => {
for (let a = 0; a < p.length; a++) p[a] = 0;
});
}
function draw() {
//clear
clear();
//draw the grid
stroke(255);
strokeWeight(0.2);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
noFill();
rect(c*unit+1, r*unit+1, unit, unit);
}
}
//pointer
fill(0, 255, 0);
strokeWeight(0);
//mouse way
circle(mouseX, mouseY, 10);
if (mouseIsPressed && mouseButton === LEFT) {
//find slot under the pointer
var mouseC = int(mouseX/unit);
var mouseR = int(mouseY/unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
if (penselect == "pencil") p[mouseC*cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC*cols + mouseR] = 0;
}
}
//touch way
if (touches.length > 0) {
circle(touches[0].x, touches[0].y, 10);
//find slot under the pointer
var mouseC = int(touches[0].x/unit);
var mouseR = int(touches[0].y/unit);
if (mouseC >= 0 && mouseC < cols && mouseR >= 0 && mouseR < rows) {
if (scrollable == true) {
scrollable = false;
document.body.style.marginTop = `-${window.pageYOffset}px`;
document.body.style.position = 'fixed';
document.body.style.overflowY = 'scroll';
}
if (penselect == "pencil") p[mouseC*cols + mouseR] = 1;
else if (penselect == "erasor") p[mouseC*cols + mouseR] = 0;
} else {
if (scrollable == false) {
scrollable = true;
document.body.style.position = '';
document.body.style.overflowY = '';
if (document.body.style.marginTop) {
const scrollTop = -parseInt(document.body.style.marginTop, 10);
document.body.style.marginTop = '';
window.scrollTo(window.pageXOffset, scrollTop);
}
}
}
}
//draw img
image(img, 0, 0);
img.clear();
img.strokeWeight(0);
img.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c*cols + r] == 1) {
img.rect(c*unit+1, r*unit+1, unit, unit);
}
}
}
}
function submitForm(event) {
//TODO : first check if there is a drawing or not. (pixels)
//
var submit = document.getElementById("submit");
submit.setAttribute('disabled', 'disabled');
select("html").style('cursor', 'progress');
//
var form = document.getElementById("form");
var fd = new FormData(form);
//
var unit = 45; //mask&override 'unit' -> for crispy png.
var img2 = createGraphics(cols*unit+2, rows*unit+2);
img2.clear();
img2.strokeWeight(0);
img2.fill(255);
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (p[c*cols + r] == 1) {
img2.rect(c*unit+1, r*unit+1, unit, unit);
}
}
}
var dataurl = img2.elt.toDataURL();
var pixels = dataURLtoBlob(dataurl);
fd.append("pixels", pixels, "pixels.png");
for(var pair of fd.entries()) {
console.log(pair[0]+ ', '+ pair[1]);
}
//
var request = new XMLHttpRequest();
request.open("POST", "/entry");
request.onload = () => {
alert('감사합니다!');
location.reload();
//location.assign("/submit");
}
request.send(fd);
//
event.preventDefault();
}
const form = document.getElementById('form');
form.addEventListener('submit', submitForm);
//// ---- for 'list' rendering ----
function del(that) {
console.log();
let text;
let pass = prompt("패스워드를 맞춰보세요!", "숫자2개");
if (/^\d{2}$/.test(pass)) {
var target = that.parentElement.parentElement.getAttribute('foldername');
const trydelete = async () => {
const response = await fetch('/delete/' + target + '/' + pass);
if (response.ok) {
const json = await response.json();
return Promise.resolve(json);
} else {
return Promise.reject('no response.');
}
}
trydelete().then((resp) => {
if (resp.result) {
alert("지우기 성공!");
location.reload();
} else {
alert("지우기 실패-");
}
}).catch(console.log);
} else {
alert("암호는 숫자2개...");
}
}
</script>
</body>
</html>

285
src/pages/live.html Normal file
View file

@ -0,0 +1,285 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>흐름을 향하여 걷는</title>
<link rel="stylesheet" href="/default.css" />
<style>
html,
body {
overflow: hidden;
}
</style>
<script src="/js/p5-v1.1.9.min.js"></script>
<script src="/js/socket-v2.3.0.io.slim.js"></script>
<script src="/js/Tone-14.8.36.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div class="content">
</div>
<script>
// // force https
// var http_confirm = location.href.split(":")[0];
// if (http_confirm == "http") {
// window.location.replace("https://" + location.host);
// }
//
var socket = io(location.host);
var n = 0;
var fr = 20;
var arr = [];
var looper;
var score;
var logo;
var silence;
var clap;
//promisify -> new Tone.Player
function AudioImport(url) {
return new Promise((resolve, reject) => {
var audio = new Tone.Player(url, () => resolve(audio));
});
}
async function setup() {
noCanvas();
if (windowWidth > 1500 && windowWidth > windowHeight) {
fr = 30;
} else {
fr = 20;
}
frameRate(fr);
//p5 'draw()' doesn't work if user is not looking at the tab.
noLoop();
// --> use custom looper.
}
//
var myroom = -1;
var intro;
var ready;
//
socket.on("connect", async function() {
console.log("connected!");
//
silence = (await AudioImport("/audio/_silence.wav")).toDestination();
clap = (await AudioImport("/audio/clap01.mp3")).toDestination();
//TESTING... fixed to room 1.
// myroom = 1;
if (myroom == -1 && selectAll(".roomsel").length == 0) {
//initial connection -> ask the room number.
var roomsel = createDiv();
roomsel.class("roomsel");
var b = createButton("라이브! Live Performance-", 1);
var d = createDiv("흐름을&nbsp; 향하여&nbsp; 걷는 &nbsp;&nbsp;Walking towards the Flow");
d.class("title");
b.mouseClicked(function() {
silence.start();
//clap.start();
setTimeout(() => {
selectAll(".roomsel").forEach(item => {
item.remove();
// 1 second popup '.intro' div
intro = createDiv(
"라이브<br>«흐름을 향하여 걷는»은 온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다. 누구나 참여할 수 있는 이 퍼레이드는 여러분이 보내주시는 소리가 모여 만들어집니다.<br>소리가 들리지 않는다면, 볼륨을 확인해주시고, 스마트폰 환경에서는 진동해제해주세요. <br><br>Live Performance<br>«Walking Toward the Flow» is a sound parade that creates a procession of sounds in the online space. Anyone can participate. This parade is made by collecting the sounds you sent.<br>If there is no sound, check the volume and turn off the vibration in the smartphone environment."
);
intro.class("notice intro"); //-> fadeout & disapear by css animation style.
});
}, 1000);
});
roomsel.child(d);
roomsel.child(b);
} else {
//re-connection -> just connect to remembered room!
socket.emit("room", myroom, function(res) {
if (res) {
console.log("entered the room -> " + myroom);
} else {
console.log("rejected!");
}
});
}
});
//var fading_factor = 0.3; //30%
var fading_factor = 0.5; //50%
socket.on("flow", async function(flow) {
console.log(flow);
var list = await new Promise((resolve, reject) => {
loadJSON("/entries", (json) => resolve(json));
})
console.log(list);
// var object = flow.object;
var object = {
"id": 1,
"type": "abc",
"src": "https://p.dianaband.info/public/sound-parade/" + list[flow] + "/pixels.png",
"audio": "https://p.dianaband.info/public/sound-parade/" + list[flow] + "/audio.mp3",
"alt": "알트",
"size": {
"base": 40,
"random": 20
},
"y": {
"base": 20,
"random": 10
},
"showtime": 20000
};
console.log(object);
var img = createImg(object.src, object.alt, "", async function(im) {
//로딩이 끝나면, start!
var pv = new Tone.PanVol(0, -99).toDestination();
var snd = await AudioImport(object.audio); // NOTE: url with spaces didn't work here.
snd.connect(pv).start();
snd.loop = true;
//로딩이 끝나면, show!
im.show();
//그림의 크기와 초기 위치 ==> 가로 보기인 경우
var width = 0;
if (windowWidth > windowHeight) {
width = (windowHeight * (object.size.base * 1.4 + object.size.random * Math.random())) / 100; // 좀더 크게 + 40% (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random()) * 0.5) / 100 // 좀더 위로 위로 - 50% (ratio)
);
//그림의 크기와 초기 위치 ==> 세로 보기인 경우
} else {
width = (windowHeight * (object.size.base + object.size.random * Math.random())) / 100; // json에서 정한 크기 그대로. (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random())) / 100 // json에서 정한 위치 그대로. (ratio)
);
}
//추가 정보들
im.attribute("data-type", object.type);
im.attribute("data-showtime", object.showtime / 1000); //milli-sec. -> seconds.
//'아이콘' 들은 애니메이션을 시켜줘야 함...
if (object.type == "icon") {
//
im.class("rotate");
im.style("animation-duration", object.rotate + "s");
var orgs = im.style("transform-origin").split(" ");
var str = parseFloat(orgs[0]) + object.pivot.x + "px";
str = str + " " + parseFloat(orgs[1]) + object.pivot.y + "px";
im.style("transform-origin", str);
//
}
//로딩이 다 되면, rendering array에 추가.
var bundle = {
ref: object,
img: im,
sound: snd,
panvol: pv,
width: width
};
arr.push(bundle);
});
//첨에는 hide
img.hide();
});
//p5 'draw()' doesn't work if user is not looking at the tab.
Tone.Transport.scheduleRepeat((time) => {
//
for (var i = arr.length - 1; i >= 0; i -= 1) {
var bundle = arr[i];
var img = bundle.img;
var showtime = parseFloat(img.attribute("data-showtime"));
var type = img.attribute("data-type");
var x = img.position().x;
var y = img.position().y;
y = y + random(-1, 1);
x = x - windowWidth / (fr * showtime);
//
if (type == "icon") {
img.style("z-index", "-1");
}
img.position(x, y);
var pan = (x / windowWidth) * 2 - 1;
//panning
var snd = bundle.sound;
var pv = bundle.panvol;
if (x >= -bundle.width && x < windowWidth) {
pan = ((x + bundle.width) / (windowWidth + bundle.width)) * 2 - 1;
pv.pan.value = pan;
pv.volume.value = 0;//(dB)
} else {
var range;
var knob;
if (x >= windowWidth) {
range = windowWidth * fading_factor
knob = x - windowWidth;
pv.pan.value = 1;
pv.volume.value = knob / range * -20;
} else if (x < -bundle.width) {
range = windowWidth * fading_factor
knob = (x + bundle.width) * -1;
pv.pan.value = -1;
pv.volume.value = knob / range * -20;
}
}
//remove with sound fade-out
var exit_x = -bundle.width - windowWidth * fading_factor;
if (x < exit_x) {
img.remove();
snd.stop();
delete snd;
delete pv;
arr.splice(i, 1);
}
}
//
}, 1.0/fr);
Tone.Transport.start()
// function randomvoiceplay() {
// (looper = function(timeout) {
// setTimeout(function() {
// voice[int(random(19))].play();
// looper(random(8000, 12000));
// }, timeout);
// })(8000);
// }
</script>
</body>
</html>

305
src/pages/parade.html Normal file
View file

@ -0,0 +1,305 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>흐름을 향하여 걷는</title>
<link rel="stylesheet" href="/default.css" />
<style>
html,
body {
overflow: hidden;
}
</style>
<script src="/js/p5-v1.1.9.min.js"></script>
<script src="/js/socket-v2.3.0.io.slim.js"></script>
<script src="/js/Tone-14.8.36.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div class="content">
</div>
<script>
// // force https
// var http_confirm = location.href.split(":")[0];
// if (http_confirm == "http") {
// window.location.replace("https://" + location.host);
// }
var socket = io(location.host);
var n = 0;
var fr = 20;
var arr = [];
var looper;
var score;
var logo;
var silence;
var clap;
//promisify -> new Tone.Player
function AudioImport(url) {
return new Promise((resolve, reject) => {
var audio = new Tone.Player(url, () => resolve(audio));
});
}
async function setup() {
noCanvas();
if (windowWidth > 1500 && windowWidth > windowHeight) {
fr = 30;
} else {
fr = 20;
}
frameRate(fr);
//p5 'draw()' doesn't work if user is not looking at the tab.
noLoop();
// --> use custom looper.
}
//
var myroom = -1;
var intro;
var ready;
//
socket.on("connect", async function() {
console.log("connected!");
//
silence = (await AudioImport("./audio/_silence.wav")).toDestination();
clap = (await AudioImport("./audio/clap01.mp3")).toDestination();
//TESTING... fixed to room 1.
// myroom = 1;
if (myroom == -1 && selectAll(".roomsel").length == 0) {
//initial connection -> ask the room number.
var roomsel = createDiv();
roomsel.class("roomsel");
var b = createButton("시작하기 Start!", 1);
var d = createDiv("<a href='/entry'>흐름을&nbsp; 향하여&nbsp; 걷는 &nbsp;&nbsp;Walking towards the Flow</a>");
d.class("title");
b.mouseClicked(function() {
silence.start();
clap.start();
myroom = parseInt(this.value());
socket.emit("room", myroom, function(res) {
if (res) {
console.log("entered the room -> " + myroom);
createP(str(myroom));
// setTimeout(function() {
// ready = createP("퍼레이드 시작합니다!!");
// ready.position(
// windowWidth / 2 - windowWidth / 10,
// windowHeight / 2
// );
// }, 1000);
} else {
console.log("rejected!");
}
});
setTimeout(() => {
selectAll(".roomsel").forEach(item => {
item.remove();
// 1 second popup '.intro' div
intro = createDiv(
"«흐름을 향하여 걷는»은 온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다. 누구나 참여할 수 있는 이 퍼레이드는 여러분이 보내주시는 소리가 모여 만들어집니다.<br>소리가 들리지 않는다면, 볼륨을 확인해주시고, 스마트폰 환경에서는 진동해제해주세요. <br><br> «Walking Toward the Flow» is a sound parade that creates a procession of sounds in the online space. Anyone can participate. This parade is made by collecting the sounds you sent.<br>If there is no sound, check the volume and turn off the vibration in the smartphone environment."
);
intro.class("notice intro"); //-> fadeout & disapear by css animation style.
});
}, 1000);
});
roomsel.child(d);
roomsel.child(b);
} else {
//re-connection -> just connect to remembered room!
socket.emit("room", myroom, function(res) {
if (res) {
console.log("entered the room -> " + myroom);
} else {
console.log("rejected!");
}
});
}
});
//var fading_factor = 0.3; //30%
var fading_factor = 0.5; //50%
socket.on("post", async function(post) {
console.log(post);
var list = await new Promise((resolve, reject) => {
loadJSON("/entries", (json) => resolve(json));
})
console.log(list);
// var object = post.object;
var object = {
"id": 1,
"type": "abc",
"src": "https://p.dianaband.info/public/sound-parade/" + list[post] + "/pixels.png",
"audio": "https://p.dianaband.info/public/sound-parade/" + list[post] + "/audio.mp3",
"alt": "알트",
"size": {
"base": 40,
"random": 20
},
"y": {
"base": 20,
"random": 10
},
"showtime": 20000
};
console.log(object);
var img = createImg(object.src, object.alt, "", async function(im) {
//로딩이 끝나면, start!
var pv = new Tone.PanVol(0, -99).toDestination();
var snd = await AudioImport(object.audio); // NOTE: url with spaces didn't work here.
snd.connect(pv).start();
snd.loop = true;
//로딩이 끝나면, show!
im.show();
//그림의 크기와 초기 위치 ==> 가로 보기인 경우
var width = 0;
if (windowWidth > windowHeight) {
width = (windowHeight * (object.size.base * 1.4 + object.size.random * Math.random())) / 100; // 좀더 크게 + 40% (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random()) * 0.5) / 100 // 좀더 위로 위로 - 50% (ratio)
);
//그림의 크기와 초기 위치 ==> 세로 보기인 경우
} else {
width = (windowHeight * (object.size.base + object.size.random * Math.random())) / 100; // json에서 정한 크기 그대로. (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random())) / 100 // json에서 정한 위치 그대로. (ratio)
);
}
//추가 정보들
im.attribute("data-type", object.type);
im.attribute("data-showtime", object.showtime / 1000); //milli-sec. -> seconds.
//'아이콘' 들은 애니메이션을 시켜줘야 함...
if (object.type == "icon") {
//
im.class("rotate");
im.style("animation-duration", object.rotate + "s");
var orgs = im.style("transform-origin").split(" ");
var str = parseFloat(orgs[0]) + object.pivot.x + "px";
str = str + " " + parseFloat(orgs[1]) + object.pivot.y + "px";
im.style("transform-origin", str);
//
}
//로딩이 다 되면, rendering array에 추가.
var bundle = {
ref: object,
img: im,
sound: snd,
panvol: pv,
width: width
};
arr.push(bundle);
});
//첨에는 hide
img.hide();
});
//p5 'draw()' doesn't work if user is not looking at the tab.
// --> custom looper is ok.
var looper;
(looper = function(timeout) {
setTimeout(async function() {
//
for (var i = arr.length - 1; i >= 0; i -= 1) {
var bundle = arr[i];
var img = bundle.img;
var showtime = parseFloat(img.attribute("data-showtime"));
var type = img.attribute("data-type");
var x = img.position().x;
var y = img.position().y;
y = y + random(-1, 1);
x = x - windowWidth / (fr * showtime);
//
if (type == "icon") {
img.style("z-index", "-1");
}
3;
img.position(x, y);
var pan = (x / windowWidth) * 2 - 1;
//panning
var snd = bundle.sound;
var pv = bundle.panvol;
if (x >= -bundle.width && x < windowWidth) {
pan = ((x + bundle.width) / (windowWidth + bundle.width)) * 2 - 1;
pv.pan.value = pan;
pv.volume.value = 0;//(dB)
} else {
var range;
var knob;
if (x >= windowWidth) {
range = windowWidth * fading_factor
knob = x - windowWidth;
pv.pan.value = 1;
pv.volume.value = knob / range * -20;
} else if (x < -bundle.width) {
range = windowWidth * fading_factor
knob = (x + bundle.width) * -1;
pv.pan.value = -1;
pv.volume.value = knob / range * -20;
}
}
//remove with sound fade-out
var exit_x = -bundle.width - windowWidth * fading_factor;
if (x < exit_x) {
img.remove();
snd.stop();
delete snd;
delete pv;
arr.splice(i, 1);
}
}
//
looper(1000/fr);
}, timeout);
})(1000/fr);
// function randomvoiceplay() {
// (looper = function(timeout) {
// setTimeout(function() {
// voice[int(random(19))].play();
// looper(random(8000, 12000));
// }, timeout);
// })(8000);
// }
</script>
</body>
</html>

376
src/pages/preview.html Normal file
View file

@ -0,0 +1,376 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>흐름을 향하여 걷는</title>
<link rel="stylesheet" href="/default.css" />
<style>
html,
body {
overflow: hidden;
}
</style>
<script src="/js/p5-v1.1.9.min.js"></script>
<script src="/js/socket-v2.3.0.io.slim.js"></script>
<script src="/js/Tone-14.8.36.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500&display=swap" rel="stylesheet" />
</head>
<body>
<div class="bg"></div>
<div class="content">
</div>
<script>
// // force https
// var http_confirm = location.href.split(":")[0];
// if (http_confirm == "http") {
// window.location.replace("https://" + location.host);
// }
//--> https://gist.github.com/mudge/5830382#gistcomment-3398873 + modified trigger to accentp an arg.
function EventEmitter() {
const eventRegister = {};
const on = (name, fn) => {
if (!eventRegister[name]) eventRegister[name] = [];
eventRegister[name].push(fn);
}
const trigger = (name, arg = undefined) => {
if (!eventRegister[name]) return false;
eventRegister[name].forEach((fn) => fn.call(this, arg));
}
const off = (name, fn) => {
if (eventRegister[name]) {
const index = eventRegister[name].indexOf(fn);
if (index >= 0) eventRegister[name].splice(index, 1);
}
}
return {
on, trigger, off
}
}
//get preview target foldername
var foldername = "";
var urlparts = window.location.pathname.replace(/\/\s*$/,'').split('/');
foldername = urlparts[2]; // the server's route will guarantee urlparts[2] existence.
console.log(foldername);
var playing = true;
//set-up a preview stage
async function orchestra() {
//collect info.
var entries = await new Promise(async (resolve, reject) => resolve((await fetch("/entries")).json()));
var fields = await new Promise(async (resolve, reject) => resolve((await fetch("/fields")).json()));
var group = fields[entries.findIndex((e) => e == foldername)].group;
var members = fields.filter(obj => obj.group == group);
var midx = members.findIndex((e) => e.foldername == foldername);
//
function mtrig(m) {
//
m = m % members.length;
//
var idx = entries.findIndex((e) => e == members[m].foldername);
if (idx >= 0) socket.trigger('post', idx);
}
//now, let's orchestrate!
var time = 5;
Tone.Transport.schedule((t) => mtrig(midx - 1), time);
time = time + 15 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx), time);
time = time + 16 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx + 1), time);
time = time + 5 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx + 2), time);
time = time + 5 + Math.random()*(4);
console.log(time);
Tone.Transport.schedule((t) => mtrig(midx), time);
time = time + 70;
console.log(time);
Tone.Transport.schedule((t) => {
playing = false;
});
//
Tone.Transport.start();
//
}
//
setTimeout(() => {
socket.trigger('connect');
}, 100);
//
//var socket = io(location.host);
var socket = new EventEmitter();
var n = 0;
var fr = 20;
var arr = [];
var looper;
var score;
var logo;
var silence;
var clap;
//promisify -> new Tone.Player
function AudioImport(url) {
return new Promise((resolve, reject) => {
var audio = new Tone.Player(url, () => resolve(audio));
});
}
async function setup() {
noCanvas();
if (windowWidth > 1500 && windowWidth > windowHeight) {
fr = 30;
} else {
fr = 20;
}
frameRate(fr);
//p5 'draw()' doesn't work if user is not looking at the tab.
noLoop();
// --> use custom looper.
}
//
var myroom = -1;
var intro;
var ready;
//
socket.on("connect", async function() {
console.log("connected!");
//
silence = (await AudioImport("/audio/_silence.wav")).toDestination();
clap = (await AudioImport("/audio/clap01.mp3")).toDestination();
//TESTING... fixed to room 1.
// myroom = 1;
if (myroom == -1 && selectAll(".roomsel").length == 0) {
//initial connection -> ask the room number.
var roomsel = createDiv();
roomsel.class("roomsel");
var b = createButton("미리보기 Preview!", 1);
var d = createDiv("흐름을&nbsp; 향하여&nbsp; 걷는 &nbsp;&nbsp;Walking towards the Flow");
d.class("title");
b.mouseClicked(function() {
silence.start();
//clap.start();
orchestra();
setTimeout(() => {
selectAll(".roomsel").forEach(item => {
item.remove();
// 1 second popup '.intro' div
intro = createDiv(
"미리보기<br>«흐름을 향하여 걷는»은 온라인 공간에 소리의 행렬을 만드는 사운드 퍼레이드입니다. 누구나 참여할 수 있는 이 퍼레이드는 여러분이 보내주시는 소리가 모여 만들어집니다.<br>소리가 들리지 않는다면, 볼륨을 확인해주시고, 스마트폰 환경에서는 진동해제해주세요. <br><br>Preview<br>«Walking Toward the Flow» is a sound parade that creates a procession of sounds in the online space. Anyone can participate. This parade is made by collecting the sounds you sent.<br>If there is no sound, check the volume and turn off the vibration in the smartphone environment."
);
intro.class("notice intro"); //-> fadeout & disapear by css animation style.
});
}, 1000);
});
roomsel.child(d);
roomsel.child(b);
} else {
//re-connection -> just connect to remembered room!
socket.emit("room", myroom, function(res) {
if (res) {
console.log("entered the room -> " + myroom);
} else {
console.log("rejected!");
}
});
}
});
//var fading_factor = 0.3; //30%
var fading_factor = 0.5; //50%
socket.on("post", async function(post) {
console.log(post);
var list = await new Promise((resolve, reject) => {
loadJSON("/entries", (json) => resolve(json));
})
console.log(list);
// var object = post.object;
var object = {
"id": 1,
"type": "abc",
"src": "https://p.dianaband.info/public/sound-parade/" + list[post] + "/pixels.png",
"audio": "https://p.dianaband.info/public/sound-parade/" + list[post] + "/audio.mp3",
"alt": "알트",
"size": {
"base": 40,
"random": 20
},
"y": {
"base": 20,
"random": 10
},
"showtime": 20000
};
console.log(object);
var img = createImg(object.src, object.alt, "", async function(im) {
//로딩이 끝나면, start!
var pv = new Tone.PanVol(0, -99).toDestination();
var snd = await AudioImport(object.audio); // NOTE: url with spaces didn't work here.
snd.connect(pv).start();
snd.loop = true;
//로딩이 끝나면, show!
im.show();
//그림의 크기와 초기 위치 ==> 가로 보기인 경우
var width = 0;
if (windowWidth > windowHeight) {
width = (windowHeight * (object.size.base * 1.4 + object.size.random * Math.random())) / 100; // 좀더 크게 + 40% (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random()) * 0.5) / 100 // 좀더 위로 위로 - 50% (ratio)
);
//그림의 크기와 초기 위치 ==> 세로 보기인 경우
} else {
width = (windowHeight * (object.size.base + object.size.random * Math.random())) / 100; // json에서 정한 크기 그대로. (ratio)
im.size(width, AUTO);
im.position(
windowWidth * (1 + fading_factor),
(windowHeight * (object.y.base + object.y.random * Math.random())) / 100 // json에서 정한 위치 그대로. (ratio)
);
}
//추가 정보들
im.attribute("data-type", object.type);
im.attribute("data-showtime", object.showtime / 1000); //milli-sec. -> seconds.
//'아이콘' 들은 애니메이션을 시켜줘야 함...
if (object.type == "icon") {
//
im.class("rotate");
im.style("animation-duration", object.rotate + "s");
var orgs = im.style("transform-origin").split(" ");
var str = parseFloat(orgs[0]) + object.pivot.x + "px";
str = str + " " + parseFloat(orgs[1]) + object.pivot.y + "px";
im.style("transform-origin", str);
//
}
//로딩이 다 되면, rendering array에 추가.
var bundle = {
ref: object,
img: im,
sound: snd,
panvol: pv,
width: width
};
arr.push(bundle);
});
//첨에는 hide
img.hide();
});
//p5 'draw()' doesn't work if user is not looking at the tab.
Tone.Transport.scheduleRepeat((time) => {
//
for (var i = arr.length - 1; i >= 0; i -= 1) {
var bundle = arr[i];
var img = bundle.img;
var showtime = parseFloat(img.attribute("data-showtime"));
var type = img.attribute("data-type");
var x = img.position().x;
var y = img.position().y;
y = y + random(-1, 1);
x = x - windowWidth / (fr * showtime);
//
if (type == "icon") {
img.style("z-index", "-1");
}
img.position(x, y);
var pan = (x / windowWidth) * 2 - 1;
//panning
var snd = bundle.sound;
var pv = bundle.panvol;
if (x >= -bundle.width && x < windowWidth) {
pan = ((x + bundle.width) / (windowWidth + bundle.width)) * 2 - 1;
pv.pan.value = pan;
pv.volume.value = 0;//(dB)
} else {
var range;
var knob;
if (x >= windowWidth) {
range = windowWidth * fading_factor
knob = x - windowWidth;
pv.pan.value = 1;
pv.volume.value = knob / range * -20;
} else if (x < -bundle.width) {
range = windowWidth * fading_factor
knob = (x + bundle.width) * -1;
pv.pan.value = -1;
pv.volume.value = knob / range * -20;
}
}
//remove with sound fade-out
var exit_x = -bundle.width - windowWidth * fading_factor;
if (x < exit_x) {
img.remove();
snd.stop();
delete snd;
delete pv;
arr.splice(i, 1);
if (arr.length == 0 && playing == false) {
setTimeout(() => {
createDiv("미리보기가 끝났습니다. <br>Preview is over.<br><br>").class("notice").style('text-align', 'center')
.child(createButton("퍼레이드 가기").attribute('onclick', 'location.href="/"').style('margin', '1em 1em'))
.child(createButton("닫기").attribute('onclick', 'window.close()').style('margin', '1em 1em'))
}, 3000);
}
}
}
//
}, 1.0/fr);
Tone.Transport.start()
// function randomvoiceplay() {
// (looper = function(timeout) {
// setTimeout(function() {
// voice[int(random(19))].play();
// looper(random(8000, 12000));
// }, timeout);
// })(8000);
// }
</script>
</body>
</html>