├── .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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------