├── .gitattributes
├── .gitignore
├── README.md
├── nodemcu
└── code.ino
├── sonic-pi
├── custom-sounds
│ ├── 1.flac
│ ├── 2.flac
│ ├── 20210222_075958.jpg
│ ├── 3.flac
│ ├── 4.flac
│ ├── 5.flac
│ ├── Magnet_2.5.0_MAS_[TNT].dmg
│ ├── kahootsfx.flac
│ └── max.mp3
└── music.rb
├── web
├── .gitignore
├── README.md
├── lib
│ └── theme.js
├── package.json
├── pages
│ ├── _app.js
│ ├── api
│ │ └── event.js
│ ├── dashboard.js
│ ├── flash.js
│ └── index.js
├── public
│ └── vercel.svg
├── styles
│ ├── Home.module.css
│ └── globals.css
└── yarn.lock
└── websocket-python-translator
└── main.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🎶 sound-festival
2 |
3 | Software to power an interactive sound festival. And then adapted to go virtual on [Hack Club's Twitch](https://www.twitch.tv/hackclubhq)!
4 |
5 | Made of three parts: custom boards made with NodeMCUs, a website powering projections & a Sonic Pi music program. There are other little scripts powering certain things as well.
6 |
7 | Here are some pics:
8 |
9 | |
|
|
|
10 | |---|---|---|
11 | | The decorated control boards | The Sonic Pi program running | Students, young and old, enjoying the music |
12 |
13 | |
|
|
|
14 | |---|---|---|
15 | | Students & a principal trying it out | Curious students reading the music's code | Addicted students playing the Kahoot sound |
16 |
17 | Thank you to [@zachlatta](https://github.com/zachlatta), [@MatthewStanciu](https://github.com/MatthewStanciu), [@zfogg](https://github.com/zfogg), [@juliegoat](https://github.com/juliegoat) & [@MaxWofford](https://github.com/MaxWofford) for making voiceovers for the festival.
18 |
--------------------------------------------------------------------------------
/nodemcu/code.ino:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 |
5 | #include
6 |
7 | const char * ssid = "SECRET";
8 | const char * password = "PASSWORD";
9 |
10 | String serverName = "https://sound-festival.vercel.app/api/event";
11 |
12 | WiFiClientSecure client;
13 |
14 | void setup() {
15 |
16 | WiFi.begin(ssid, password);
17 | Serial.println("Connecting");
18 | while (WiFi.status() != WL_CONNECTED) {
19 | delay(500);
20 | Serial.print(".");
21 | }
22 | Serial.println("");
23 | Serial.print("Connected to WiFi network with IP Address: ");
24 | Serial.println(WiFi.localIP());
25 |
26 |
27 | client.setInsecure(); //the magic line, use with caution
28 | client.connect(serverName, '3000');
29 |
30 | // put your setup code here, to run once:
31 | Serial.begin(9600);
32 | pinMode(D0, INPUT);
33 | pinMode(D1, INPUT);
34 | pinMode(D2, INPUT);
35 | pinMode(D5, INPUT);
36 | pinMode(D6, INPUT);
37 | pinMode(D7, INPUT);
38 | pinMode(D8, INPUT);
39 | pinMode(LED_BUILTIN, OUTPUT);
40 | Serial.println(WiFi.macAddress());
41 | }
42 |
43 | void request(String urlPath) {
44 | if (WiFi.status() == WL_CONNECTED) {
45 | HTTPClient http;
46 |
47 |
48 |
49 | // Your Domain name with URL path or IP address with path
50 | http.begin(client, (serverName+urlPath).c_str());
51 |
52 | // Send HTTP GET request
53 | int httpResponseCode = http.GET();
54 |
55 | if (httpResponseCode > 0) {
56 | Serial.print("HTTP Response code: ");
57 | Serial.println(httpResponseCode);
58 | String payload = http.getString();
59 | Serial.println(payload);
60 | } else {
61 | Serial.print("Error code: ");
62 | Serial.println(http.getString());
63 | Serial.println(httpResponseCode);
64 | }
65 | // Free resources
66 | http.end();
67 | delay(500);
68 | }
69 | }
70 |
71 | void loop() {
72 | if (WiFi.macAddress() == "48:3F:DA:75:1A:86") {
73 |
74 | byte val = digitalRead(D8);
75 | if (val == HIGH) {
76 |
77 | Serial.println('8');
78 | Serial.println('\n');
79 | request("?sfx=3");
80 | } else {
81 |
82 | }
83 |
84 | val = digitalRead(D7);
85 | if (val == HIGH) {
86 |
87 | Serial.println('7');
88 | Serial.println('\n');
89 | request("?sfx=2");
90 | } else {
91 |
92 | }
93 | val = digitalRead(D6);
94 | if (val == HIGH) {
95 |
96 | Serial.println('6');
97 | Serial.println('\n');
98 | request("?sfx=1");
99 | } else {
100 |
101 | }
102 | val = digitalRead(D5);
103 | if (val == HIGH) {
104 |
105 | Serial.println('5');
106 | Serial.println('\n');
107 | request("?sfx=0");
108 | } else {
109 |
110 | }
111 | val = digitalRead(D2);
112 | if (val == HIGH) {
113 |
114 | Serial.println('2');
115 | Serial.println('\n');
116 | request("?beat=2");
117 | } else {
118 |
119 | }
120 | val = digitalRead(D1);
121 | if (val == HIGH) {
122 | Serial.println('1');
123 | Serial.println('\n');
124 | request("?beat=0");
125 |
126 | } else {
127 |
128 | }
129 | val = digitalRead(D0);
130 | if (val == HIGH) {
131 |
132 | Serial.println('0');
133 | Serial.println('\n');
134 | request("?beat=1");
135 | } else {
136 |
137 | }
138 | }
139 | if (WiFi.macAddress() == "C8:2B:96:2E:D2:48") {
140 |
141 | byte val = digitalRead(D1);
142 | if (val == HIGH) {
143 |
144 | Serial.println('1');
145 | Serial.println('\n');
146 | request("?sfx=4");
147 | } else {
148 |
149 | }
150 |
151 | val = digitalRead(D0);
152 | if (val == HIGH) {
153 |
154 | Serial.println('0');
155 | Serial.println('\n');
156 | request("?sfx=5");
157 | } else {
158 |
159 | }
160 | val = digitalRead(D7);
161 | if (val == HIGH) {
162 |
163 | Serial.println('7');
164 | Serial.println('\n');
165 | request("?sfx=6");
166 | } else {
167 |
168 | }
169 | val = digitalRead(D2);
170 | if (val == HIGH) {
171 |
172 | Serial.println('2');
173 | Serial.println('\n');
174 | request("?sfx=7");
175 | } else {
176 |
177 | }
178 | val = digitalRead(D5);
179 | if (val == HIGH) {
180 |
181 | Serial.println('5');
182 | Serial.println('\n');
183 | request("?beat=0");
184 | } else {
185 |
186 | }
187 | val = digitalRead(D6);
188 | if (val == HIGH) {
189 | Serial.println('6');
190 | Serial.println('\n');
191 | request("?beat=2");
192 |
193 | } else {
194 |
195 | }
196 | val = digitalRead(D8);
197 | if (val == HIGH) {
198 | Serial.println('8');
199 | Serial.println('\n');
200 | request("?beat=1");
201 | } else {
202 |
203 | }
204 | }
205 | if (WiFi.macAddress() == "48:3F:DA:75:1A:F7") {
206 |
207 | byte val = digitalRead(D2);
208 | if (val == HIGH) {
209 | Serial.println('2');
210 | Serial.println('\n');
211 | request("?sfx=11");
212 | } else {
213 |
214 | }
215 |
216 | val = digitalRead(D5);
217 | if (val == HIGH) {
218 | Serial.println('5');
219 | Serial.println('\n');
220 | request("?sfx=10");
221 | } else {
222 |
223 | }
224 | val = digitalRead(D6);
225 | if (val == HIGH) {
226 |
227 | Serial.println('6');
228 | Serial.println('\n');
229 | request("?sfx=9");
230 | } else {
231 |
232 | }
233 | val = digitalRead(D7);
234 | if (val == HIGH) {
235 |
236 | Serial.println('7');
237 | Serial.println('\n');
238 | request("?sfx=8");
239 | } else {
240 |
241 | }
242 | val = digitalRead(D1);
243 | if (val == HIGH) {
244 |
245 | Serial.println('1');
246 | Serial.println('\n');
247 | request("?beat=2");
248 | } else {
249 |
250 | }
251 | val = digitalRead(D0);
252 | if (val == HIGH) {
253 | Serial.println('0');
254 | Serial.println('\n');
255 | request("?beat=0");
256 |
257 | } else {
258 |
259 | }
260 | val = digitalRead(D8);
261 | if (val == HIGH) {
262 |
263 | Serial.println('8');
264 | Serial.println('\n');
265 | request("?beat=1");
266 | } else {
267 |
268 | }
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/1.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/1.flac
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/2.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/2.flac
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/20210222_075958.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/20210222_075958.jpg
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/3.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/3.flac
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/4.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/4.flac
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/5.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/5.flac
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/Magnet_2.5.0_MAS_[TNT].dmg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/Magnet_2.5.0_MAS_[TNT].dmg
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/kahootsfx.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/kahootsfx.flac
--------------------------------------------------------------------------------
/sonic-pi/custom-sounds/max.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/sonic-pi/custom-sounds/max.mp3
--------------------------------------------------------------------------------
/sonic-pi/music.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | ███████ ██████ ██ ██ ███ ██ ██████
4 | ██ ██ ██ ██ ██ ████ ██ ██ ██
5 | ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██
6 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
7 | ███████ ██████ ██████ ██ ████ ██████
8 |
9 |
10 | ███████ ███████ ███████ ████████ ██ ██ ██ █████ ██
11 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
12 | █████ █████ ███████ ██ ██ ██ ██ ███████ ██
13 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
14 | ██ ███████ ███████ ██ ██ ████ ██ ██ ███████
15 |
16 |
17 | Join at festival.hackclub.dev!
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Reference Doc:
48 |
49 | Moods:
50 |
51 | 0: Spooky
52 | 1: Electric
53 | 2: Undefined
54 |
55 | SFX:
56 |
57 | 0:
58 |
59 | =end
60 |
61 | set :x, 100
62 |
63 | live_loop :setter do
64 | y, c = sync "/midi:my_virtual_output:1/note_on"
65 | if(y != 100)
66 | set :x, y
67 | end
68 | set :b, c
69 | sleep 1
70 | end
71 |
72 | # Spooky Background Mood
73 | with_fx :band_eq, amp:0.2 do
74 | live_loop :haunted do
75 | if(get[:x]==0)
76 | sample :perc_bell, rate: rrand(1, 1.5)
77 | end
78 | sleep rrand(5, 7)
79 | end
80 | live_loop :guit do
81 | if(get[:x]==0)
82 | with_fx :echo, mix: 0.3, phase: 0.25 do
83 | sample :guit_em9, rate: 0.5
84 | end
85 | sample :guit_em9, rate: 1.5
86 | sleep 4
87 | end
88 | sleep 0.1
89 | end
90 | live_loop :boom do
91 | if(get[:x]==0)
92 | with_fx :reverb, room: 1 do
93 | sample :bd_boom, amp: 10, rate: 1
94 | end
95 | sleep 4
96 | end
97 | sleep 0.1
98 | end
99 | end
100 |
101 | # Electric Mood
102 |
103 | load_samples :guit_em9, :bd_haus
104 | with_fx :band_eq, amp:0.04 do
105 | live_loop :low do
106 | if(get[:x]==1)
107 | tick
108 | synth :zawa, wave: 1, phase: 0.25, release: 5, note: (knit :e1, 12, :c1, 4).look, cutoff: (line 60, 120, steps: 6).look
109 | end
110 | sleep 4
111 | end
112 | with_fx :reverb, room: 1 do
113 | live_loop :lands, auto_cue: false do
114 | get :x
115 | if(get[:x]==1)
116 | use_synth :dsaw
117 | use_random_seed 310003
118 | ns = (scale :e2, :minor_pentatonic, num_octaves: 4).take(8)
119 | 16.times do
120 | play ns.choose, detune: 12, release: 0.1, amp: 2, amp: rand + 0.5, cutoff: rrand(70, 120), amp: 2
121 | sleep 0.125
122 | end
123 | end
124 | sleep 0.125
125 | end
126 | end
127 | live_loop :fietsen do
128 | x, b = sync "/midi:my_virtual_output:1/note_on"
129 | sleep 0.25
130 | if(get[:x]==1)
131 | sample :guit_em9, rate: 1
132 | end
133 | end
134 | live_loop :tijd do
135 | if(get[:x]==1)
136 | sample :bd_haus, amp: 2.5, cutoff: 100
137 | end
138 | sleep 1
139 | end
140 | live_loop :ind do
141 | if(get[:x]==1)
142 | sample :loop_industrial, beat_stretch: 1
143 | end
144 | sleep 1
145 | end
146 | end
147 |
148 | # Undefined
149 |
150 | with_fx :band_eq, amp:0.1 do
151 | use_random_seed 667
152 | load_sample :ambi_lunar_land
153 | sleep 1
154 | live_loop :foo do
155 | if(get[:x]==2)
156 | with_fx :reverb, kill_delay: 0.2, room: 0.3 do
157 | 4.times do
158 | use_random_seed 4000
159 | 8.times do
160 | sleep 0.25
161 | play chord(:e3, :m7).choose, release: 0.1, pan: rrand(-1, 1, res: 0.9), amp: 1
162 | end
163 | end
164 | end
165 | end
166 | sleep 2
167 | end
168 | live_loop :bar, auto_cue: false do
169 | if(get[:x]==2)
170 | if rand < 0.5
171 | sample :ambi_lunar_land
172 | puts :comet_landing
173 | end
174 | sleep 8
175 | end
176 | sleep 0.1
177 | end
178 | end
179 |
180 | # SFX Zone
181 |
182 | live_loop :effects do
183 | x, b = sync "/midi:my_virtual_output:1/note_on"
184 | if(b == 0)
185 | sample :vinyl_rewind
186 | end
187 | if(b == 1)
188 | sample :drum_tom_hi_hard
189 | sleep 0.4
190 | sample :drum_tom_lo_hard, rate: 0.3
191 | sleep 0.4
192 | sample :drum_cymbal_open
193 | end
194 | if(b == 2)
195 | sample :elec_beep
196 | sleep 0.3
197 | sample :elec_beep
198 | sleep 0.3
199 | sample :elec_blip, rate: 0.3
200 | end
201 | if(b == 3)
202 | sample :misc_cineboom
203 | end
204 | if(b == 4)
205 | sample :perc_snap2
206 | sleep 0.3
207 | sample :perc_snap
208 | end
209 | if(b == 5)
210 | sample :perc_bell
211 | end
212 | if(b == 6)
213 | sample :misc_crow
214 | sleep 0.4
215 | sample :misc_crow, rate: 0.9
216 | end
217 | if(b == 7)
218 | sample :perc_till
219 | end
220 | if(b == 8)
221 | with_fx :echo do
222 | sample :ambi_choir
223 | end
224 | end
225 | if(b == 9)
226 | sample "/Users/sam/Documents/GitHub/sound-festival/sonic-pi/custom-sounds/kahootsfx.flac"
227 | end
228 | if(b == 10)
229 | sample :guit_e_fifths
230 | sleep 3
231 | sample :guit_e_slide
232 | end
233 | if(b == 11)
234 | sample :perc_swash
235 | end
236 | b = 100
237 | sleep 0.1
238 | end
239 |
240 | live_loop :voices do
241 | time = Time.new
242 | if(false)
243 | sample "/Users/sam/Documents/GitHub/sound-festival/sonic-pi/custom-sounds/#{dice(5)}.flac"
244 | sleep 60
245 | end
246 | sleep 0.25
247 | end
248 |
249 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/web/lib/theme.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sampoder/sound-festival/83ac7d929e4788716e10b187e7d67ae14fdeb46c/web/lib/theme.js
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@geist-ui/react": "^2.1.0-canary.2",
12 | "@hackclub/icons": "^0.0.9",
13 | "@hackclub/theme": "^0.3.1",
14 | "@harelpls/use-pusher": "^7.1.0",
15 | "lru-cache": "^6.0.0",
16 | "next": "10.0.6",
17 | "pusher": "^4.0.2",
18 | "react": "17.0.1",
19 | "react-dom": "17.0.1",
20 | "theme-ui": "^0.10.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/web/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { PusherProvider } from "@harelpls/use-pusher";
3 | import { ThemeProvider } from "theme-ui";
4 | import base from "@hackclub/theme";
5 | import '@hackclub/theme/fonts/reg-bold.css'
6 |
7 | let theme = base;
8 |
9 | const colors = {
10 | darker: "#121217",
11 | dark: "#17171d",
12 | darkless: "#252429",
13 |
14 | black: "#1f2d3d",
15 | steel: "#273444",
16 | slate: "#3c4858",
17 | muted: "#8492a6",
18 | smoke: "#e0e6ed",
19 | snow: "#f9fafc",
20 | white: "#ffffff",
21 |
22 | red: "#ec3750",
23 | orange: "#ff8c37",
24 | yellow: "#f1c40f",
25 | green: "#33d6a6",
26 | cyan: "#5bc0de",
27 | blue: "#338eda",
28 | purple: "#a633d6",
29 |
30 | twitter: "#1da1f2",
31 | facebook: "#3b5998",
32 | instagram: "#e1306c",
33 | };
34 |
35 | theme.colors = {
36 | ...colors,
37 | text: colors.white,
38 | background: colors.dark,
39 | elevated: colors.darkless,
40 | sheet: colors.darkless,
41 | sunken: colors.darker,
42 | border: colors.darkless,
43 | placeholder: colors.slate,
44 | secondary: colors.muted,
45 | muted: colors.muted,
46 | accent: colors.cyan,
47 | modes: {
48 | dark: {
49 | text: colors.white,
50 | background: colors.dark,
51 | elevated: colors.darkless,
52 | sheet: colors.darkless,
53 | sunken: colors.darker,
54 | border: colors.darkless,
55 | placeholder: colors.slate,
56 | secondary: colors.muted,
57 | muted: colors.muted,
58 | accent: colors.cyan,
59 | },
60 | },
61 | };
62 | const config = {
63 | // required config props
64 | clientKey: "b0fffd0f6692eadd6d60",
65 | appId: "1155455",
66 | cluster: "us2",
67 | useTLS: true,
68 | };
69 |
70 | function MyApp({ Component, pageProps }) {
71 | return (
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | export default MyApp;
81 |
--------------------------------------------------------------------------------
/web/pages/api/event.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | const LRU = require("lru-cache");
3 |
4 | const rateLimit = (options) => {
5 | const tokenCache = new LRU({
6 | max: parseInt(options.uniqueTokenPerInterval || 500, 10),
7 | maxAge: parseInt(options.interval || 60000, 10),
8 | });
9 |
10 | return {
11 | check: (res, limit, token) =>
12 | new Promise((resolve, reject) => {
13 | const tokenCount = tokenCache.get(token) || [0];
14 | if (tokenCount[0] === 0) {
15 | tokenCache.set(token, tokenCount);
16 | }
17 | tokenCount[0] += 1;
18 |
19 | const currentUsage = tokenCount[0];
20 | const isRateLimited = currentUsage >= parseInt(limit, 10);
21 | res.setHeader("X-RateLimit-Limit", limit);
22 | res.setHeader(
23 | "X-RateLimit-Remaining",
24 | isRateLimited ? 0 : limit - currentUsage
25 | );
26 |
27 | return isRateLimited ? reject() : resolve();
28 | }),
29 | };
30 | };
31 |
32 | const limiter = rateLimit({
33 | interval: 60 * 1000, // 60 seconds
34 | uniqueTokenPerInterval: 500, // Max 500 users per second
35 | })
36 |
37 | export default async function handler(req, res) {
38 | try {
39 | await limiter.check(res, 4, "CACHE_TOKEN"); // 10 requests per minute
40 | const Pusher = require("pusher");
41 |
42 | const pusher = new Pusher({
43 | appId: "1155455",
44 | key: process.env.KEY,
45 | secret: process.env.SECRET,
46 | cluster: "us2",
47 | useTLS: true,
48 | });
49 |
50 | await pusher.trigger("sound-festival", "incoming", {
51 | sfx: req.query.sfx,
52 | beat: req.query.beat,
53 | });
54 | console.log('triggered!')
55 | res.statusCode = 200;
56 | res.status(200).json({"Success": true});
57 | } catch {
58 | res.status(429).json({ error: "Rate limit exceeded" });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/web/pages/dashboard.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import styles from "../styles/Home.module.css";
3 | import React, { useState } from "react";
4 | import { useChannel, useEvent } from "@harelpls/use-pusher";
5 |
6 | export default function Home() {
7 | const colours = { spooky: "#eb6123" };
8 | const [colour, setColour] = useState("#000");
9 | const channel = useChannel("sound-festival");
10 | function sleep(ms) {
11 | return new Promise((resolve) => setTimeout(resolve, ms));
12 | }
13 | useEvent(channel, "mood-change", async ({ type }) => {
14 | setColour(colours[type]);
15 | await sleep(2000);
16 | setColour("#000");
17 | });
18 | return ;
19 | }
20 |
--------------------------------------------------------------------------------
/web/pages/flash.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import styles from "../styles/Home.module.css";
3 | import React, { useState } from "react";
4 | import { useChannel, useEvent } from "@harelpls/use-pusher";
5 |
6 | export default function Home() {
7 | const beatColours = { 0: "#eb6123", 1: "#a633d6", 2: "#33d6a6" };
8 | const sfxColours = {
9 | 0: "#5bc0de",
10 | 1: "#ec3750",
11 | 2: "#a633d6",
12 | 3: "#ec3750",
13 | 4: "#ff8c37",
14 | 5: "#f1c40f",
15 | 6: "#a633d6",
16 | 7: "#33d6a6",
17 | 8: "#338eda",
18 | 9: "#a633d6",
19 | 10: '#5bc0de',
20 | 11: '#338eda'
21 | };
22 | const [colour, setColour] = useState("#000");
23 | const channel = useChannel("sound-festival");
24 | function sleep(ms) {
25 | return new Promise((resolve) => setTimeout(resolve, ms));
26 | }
27 | useEvent(channel, "incoming", async ({ beat, sfx }) => {
28 | setColour(beat ? beatColours[beat] : sfxColours[sfx]);
29 | await sleep(2000);
30 | setColour("#000");
31 | });
32 | return ;
33 | }
34 |
--------------------------------------------------------------------------------
/web/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Box,
4 | Container,
5 | Heading,
6 | Grid,
7 | Link,
8 | Badge,
9 | Button,
10 | Text,
11 | } from "theme-ui";
12 |
13 | export default function Home() {
14 | let moods = [
15 | {
16 | emoji: "🎃",
17 | color: "orange",
18 | number: "0",
19 | },
20 | {
21 | emoji: "⚡️",
22 | color: "purple",
23 | number: "1",
24 | },
25 | {
26 | emoji: "😎",
27 | color: "yellow",
28 | number: "2",
29 | },
30 | ];
31 | let sfx = [
32 | {
33 | emoji: "⏮",
34 | color: "blue",
35 | number: "0",
36 | },
37 | {
38 | emoji: "🥁",
39 | color: "red",
40 | number: "1",
41 | },
42 | {
43 | emoji: "🤖",
44 | color: "cyan",
45 | number: "2",
46 | },
47 | {
48 | emoji: "💥",
49 | color: "yellow",
50 | number: "3",
51 | },
52 | {
53 | emoji: "🤞",
54 | color: "purple",
55 | number: "4",
56 | },
57 | {
58 | emoji: "🔔",
59 | color: "orange",
60 | number: "5",
61 | },
62 | {
63 | emoji: "🦅",
64 | color: "blue",
65 | number: "6",
66 | },
67 | {
68 | emoji: "💸",
69 | color: "green",
70 | number: "7",
71 | },
72 | {
73 | emoji: "🎶",
74 | color: "red",
75 | number: "8",
76 | },
77 | {
78 | emoji: "🎸",
79 | color: "cyan",
80 | number: "10", // Skip Kahoot
81 | },
82 | {
83 | emoji: "💨",
84 | color: "green",
85 | number: "11", // Skip Kahoot
86 | },
87 | ];
88 | return (
89 |
97 |
98 |
99 |
108 |
109 | Sound Festival on Twitch!
110 |
111 |
112 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
137 |
138 | Mood:
139 |
140 |
141 | {moods.map((x) => (
142 | {
158 | fetch(`/api/event?beat=${x.number}`);
159 | }}
160 | >
161 |
162 | {x.emoji}
163 |
164 |
165 | ))}
166 |
176 |
177 | SFXs:
178 |
179 |
180 | {sfx.map((x) => (
181 | {
197 | let res = await fetch(`/api/event?sfx=${x.number}`).then((r) => r.json());
198 | console.log(res)
199 | if(res.error){
200 | alert(`Sorry! ${res.error}. Please try again in a bit, I do this to ensure no ears are hurt` )
201 | }
202 | }}
203 | >
204 |
205 | {x.emoji}
206 |
207 |
208 | ))}
209 |
210 |
211 |
212 | );
213 | }
214 |
--------------------------------------------------------------------------------
/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .title a {
39 | color: #0070f3;
40 | text-decoration: none;
41 | }
42 |
43 | .title a:hover,
44 | .title a:focus,
45 | .title a:active {
46 | text-decoration: underline;
47 | }
48 |
49 | .title {
50 | margin: 0;
51 | line-height: 1.15;
52 | font-size: 4rem;
53 | }
54 |
55 | .title,
56 | .description {
57 | text-align: center;
58 | }
59 |
60 | .description {
61 | line-height: 1.5;
62 | font-size: 1.5rem;
63 | }
64 |
65 | .code {
66 | background: #fafafa;
67 | border-radius: 5px;
68 | padding: 0.75rem;
69 | font-size: 1.1rem;
70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
71 | Bitstream Vera Sans Mono, Courier New, monospace;
72 | }
73 |
74 | .grid {
75 | display: flex;
76 | align-items: center;
77 | justify-content: center;
78 | flex-wrap: wrap;
79 | max-width: 800px;
80 | margin-top: 3rem;
81 | }
82 |
83 | .card {
84 | margin: 1rem;
85 | flex-basis: 45%;
86 | padding: 1.5rem;
87 | text-align: left;
88 | color: inherit;
89 | text-decoration: none;
90 | border: 1px solid #eaeaea;
91 | border-radius: 10px;
92 | transition: color 0.15s ease, border-color 0.15s ease;
93 | }
94 |
95 | .card:hover,
96 | .card:focus,
97 | .card:active {
98 | color: #0070f3;
99 | border-color: #0070f3;
100 | }
101 |
102 | .card h3 {
103 | margin: 0 0 1rem 0;
104 | font-size: 1.5rem;
105 | }
106 |
107 | .card p {
108 | margin: 0;
109 | font-size: 1.25rem;
110 | line-height: 1.5;
111 | }
112 |
113 | .logo {
114 | height: 1em;
115 | }
116 |
117 | @media (max-width: 600px) {
118 | .grid {
119 | width: 100%;
120 | flex-direction: column;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/web/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/websocket-python-translator/main.py:
--------------------------------------------------------------------------------
1 | import pysher
2 | import sys
3 | import time
4 | # Add a logging handler so we can see the raw communication data
5 | import logging
6 | import rtmidi
7 | import json
8 | from dotenv import load_dotenv
9 | load_dotenv()
10 | import os
11 |
12 | midiout = rtmidi.MidiOut()
13 | available_ports = midiout.get_ports()
14 |
15 | if available_ports:
16 | midiout.open_port(0)
17 | else:
18 | midiout.open_virtual_port("My virtual output")
19 |
20 | root = logging.getLogger()
21 | root.setLevel(logging.INFO)
22 | ch = logging.StreamHandler(sys.stdout)
23 | root.addHandler(ch)
24 |
25 | pusher = pysher.Pusher(os.getenv("KEY"), cluster='us2')
26 |
27 | def my_func(data, *args, **kwargs):
28 | y = json.loads(data)
29 | global b
30 | global x
31 | try:
32 | note_off = [0x90, 100, int(y['sfx'])]
33 | b = int(y['sfx'])
34 | midiout.send_message(note_off)
35 |
36 | except:
37 | note_on = [0x90, int(y['beat']), 100]
38 | x = int(y['beat'])
39 | midiout.send_message(note_on)
40 |
41 | print("processing Args:", args)
42 | print("processing Kwargs:", kwargs)
43 |
44 | # We can't subscribe until we've connected, so we use a callback handler
45 | # to subscribe when able
46 |
47 |
48 | def connect_handler(data):
49 | channel = pusher.subscribe('sound-festival')
50 | channel.bind('incoming', my_func)
51 |
52 |
53 | pusher.connection.bind('pusher:connection_established', connect_handler)
54 | pusher.connect()
55 |
56 | while True:
57 | # Do other things in the meantime here...
58 | time.sleep(1)
59 |
--------------------------------------------------------------------------------