├── .eslintrc ├── .gitignore ├── .npmrc ├── LICENSE.md ├── README.md ├── app ├── components │ ├── hiddenTarget.tsx │ ├── longPollPlayback.tsx │ ├── multiSubmitForm.tsx │ ├── noteViz.tsx │ ├── piano.tsx │ ├── playback.tsx │ ├── rickord.tsx │ └── volumeSlider.tsx ├── data │ └── index.ts ├── entry.client.tsx ├── entry.server.tsx ├── hooks │ ├── useEventStream.ts │ ├── useIsHydrated.ts │ ├── usePeriodicRerender.ts │ └── useRootLoaderData.ts ├── root.tsx ├── routes │ ├── clientSide │ │ ├── multiplayer.tsx │ │ └── singlePlayer.tsx │ ├── index.tsx │ ├── multiplayer │ │ ├── longpoll.tsx │ │ └── stream.tsx │ ├── noOp.tsx │ ├── noteViz.tsx │ ├── notes │ │ ├── $note.tsx │ │ └── index.tsx │ ├── progressive.tsx │ ├── progressive │ │ ├── multiplayer.tsx │ │ └── singlePlayer.tsx │ └── soundCheck.tsx ├── styles │ └── app.css └── utils │ ├── emitter.ts │ ├── instruments.client.ts │ ├── notes.server.ts │ ├── singleton.ts │ └── transforms.ts ├── package-lock.json ├── package.json ├── patches ├── @remix-run+serve+1.15.0.patch └── lamejs+1.2.1.patch ├── public ├── favicon.ico ├── notes │ └── .gitkeep └── screenshot.png ├── remix.config.js ├── remix.env.d.ts ├── tsconfig.json └── types └── lamejs └── index.d.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | /public/notes 7 | .env 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2022 Jon Jensen 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (Ab)use the Platform 2 | 3 | ## a.k.a. Building a Multiplayer Piano the "Wrong" Way 4 | 5 | Let's learn and use web standards in an unconventional way 🥳 ... we're building a piano you can play with friends. That in and of itself isn't very interesting, but we're going to be using: 6 | 7 | - [Remix](https://remix.run) 8 | - [Forms](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) 9 | - [Web Audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) (mostly via [Tone.js](https://tonejs.github.io/)) 10 | - [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta), [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress) and more 11 | - [Browser history](https://developer.mozilla.org/en-US/docs/Web/API/History) 12 | - [Progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement) 13 | - [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) and [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) 14 | 15 | In addition to the progressively-enhanced multiplayer piano, this repo also contains several intermediate iterations (no-op, client-side, and single-player). 16 | 17 | ## Quick Start 18 | 19 | - clone the repo 20 | - run `npm install` 21 | - run `npm run dev` 22 | - open your browser to `https://localhost:3000` 23 | 24 | You might also need to go into your browser settings to allow sounds for localhost (or whatever host you run this on). 25 | 26 | ## But why? 27 | 28 | A more conventional approach might be just to require client-side JavaScript with websockets and media streams. Why do it this way? 29 | 30 | It's true this example is a bit contrived and the no-JS side of things is a little janky. That said: 31 | 32 | 1. This approach provides the same level of interactivity and peformance as other approaches 33 | 2. This approach ships less code to the browser than other approaches 34 | 3. This approach requires a comparable amount of code to write 35 | 4. We get progressive enhancement with minimal effort 36 | 37 | If this approach works for building a multiplayer piano, then why _not_ do the same for traditional web apps, where it's even easier and the benefits are greater? 🤔 38 | 39 | ## Screenshot 40 | 41 | Image of Multiplayer Piano 42 | -------------------------------------------------------------------------------- /app/components/hiddenTarget.tsx: -------------------------------------------------------------------------------- 1 | export const hiddenTarget = "hiddenTarget"; 2 | export function HiddenTarget() { 3 | // form target for when we play a note w/o JS, so we don't refresh the page 4 | return ( 5 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/components/longPollPlayback.tsx: -------------------------------------------------------------------------------- 1 | export function LongPollPlayback() { 2 | // long poll iframe that renders