Skip to content

Commit

Permalink
Refactor props tree (#28)
Browse files Browse the repository at this point in the history
* roadmap update

* do not attach currentTick/currentMeasure to metronome reader

* remove not-very-useful MetronomeControls component

* use dependency arrays correctly in effects

* Refactor Metronome:
- extract Clock worker into new Clock component
- pass Clock worker to children, rather than a complex Metronome controls

* remove metronome props from Track; use clock instead

* move KeyboardBindingsList to App

* move `worklets` directory to `workers`

* move Track control components into directory

* remove console log

* HTML/manifest updates

* Encapsulate beat counter listener/state into component

* use recording length from recorder event

* remove extraneous state

* fix muting bug on loop start

* handle NaNs in waveform

* update roadmap
  • Loading branch information
ericyd authored Dec 6, 2022
1 parent 562936d commit 3d3cf38
Show file tree
Hide file tree
Showing 36 changed files with 327 additions and 372 deletions.
4 changes: 2 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ <h1>
>GitHub</a
>
<a
href="https://ericyd.hashnode.dev/loop-supreme-part-7-latency-and-adding-track-functionality"
href="https://ericyd.hashnode.dev/loop-supreme-part-10-keyboard-bindings"
target="_blank"
rel="noreferrer"
class="m-2 underline text-cyan-600"
Expand All @@ -81,7 +81,7 @@ <h1>
target="_blank"
rel="noreferrer"
class="m-2 underline text-cyan-600"
>Known issues</a
>Issues</a
>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
"name": "Loop Supreme",
"icons": [
{
"src": "loop-supreme-logo.png",
"src": "icons/loop-supreme-logo.png",
"type": "image/png",
"sizes": "240x240"
},
{
"src": "loop-supreme-logo.svg",
"src": "icons/loop-supreme-logo.svg",
"type": "image/svg",
"sizes": "240x240"
}
],
"start_url": "https://loop-supreme.com",
"start_url": "https://loopsupreme.com",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
Expand Down
14 changes: 9 additions & 5 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

This is a rough list of tasks that should be completed to consider this project "done"

## Project setup
## Project setup

- [x] create-react-app https://github.com/ericyd/loop-supreme/pull/1
- [x] configure tailwind https://github.com/ericyd/loop-supreme/pull/1
- [x] add project roadmap https://github.com/ericyd/loop-supreme/pull/2

## Metronome
## Metronome

The `Metronome` is the heart of the app. The BPM, measure, current tick, and time signature should be synchronized to all the components. The metronome will probably be a Context, accessed via a hook that returns a Reader and Writer

Expand All @@ -29,7 +29,7 @@ The `Metronome` is the heart of the app. The BPM, measure, current tick, and tim
- [x] Metronome must play an audible click on each tick https://github.com/ericyd/loop-supreme/pull/4
- [x] Metronome must initialize as "stopped", and can be "started" by user input https://github.com/ericyd/loop-supreme/pull/4
- [x] Metronome can be muted, while still running https://github.com/ericyd/loop-supreme/pull/11
- [ ] move "playing" state into MetronomeControls; there is no obvious need for it to live in Metronome
- [x] ~move "playing" state into MetronomeControls; there is no obvious need for it to live in Metronome~ irrelevant after https://github.com/ericyd/loop-supreme/pull/28

## Scene

Expand All @@ -51,6 +51,7 @@ A `Track` is a single mono or stereo audio buffer that contains audio data. A `T
- [x] Component can remove itself from scene https://github.com/ericyd/loop-supreme/pull/5
- [x] Component has arm toggle button https://github.com/ericyd/loop-supreme/pull/8
- [x] ~audio data can be cleared from component without deleting it (to preserve track name)~ just mute, and then re-record if desired
- [ ] `regression` allow re-recording audio over a track [regression introduced here](https://github.com/ericyd/loop-supreme/pull/27)
- [x] deleting a track stops playback https://github.com/ericyd/loop-supreme/pull/13
- [x] Component can record data from user device https://github.com/ericyd/loop-supreme/pull/8
- [x] Component shows waveform of recorded audio https://github.com/ericyd/loop-supreme/pull/20
Expand Down Expand Up @@ -78,13 +79,13 @@ A `Track` is a single mono or stereo audio buffer that contains audio data. A `T
- [x] `space` is play/pause
- [ ] add "tap tempo" functionlity and bind to `t` key

## HTML
## HTML

- [x] flesh out header (add links to blog, etc) https://github.com/ericyd/loop-supreme/pull/16
- [x] track page views (done automatically through Cloudflare)
- [x] OG tags, SEO https://github.com/ericyd/loop-supreme/pull/16

## Deploy
## Deploy

- [x] building (GH Actions) https://github.com/ericyd/loop-supreme/pull/17 and https://github.com/ericyd/loop-supreme/pull/19
- [x] hosting (Cloudflare)
Expand All @@ -99,6 +100,7 @@ A `Track` is a single mono or stereo audio buffer that contains audio data. A `T
- [ ] show alert if track latency cannot be detected, or if it seems wildly out of the norm (~100ms +/ 20ms ???). Consider adding a "custom latency" input option???
- [x] remove useInterval hook (not used)
- [x] investigate network calls to workers. https://github.com/ericyd/loop-supreme/pull/21
- [ ] keyboard bindings should respect certain boundaries. For example, renaming tracks causes all sorts of things to fire, e.g. `a`, `m`, `r`, `c` all do things that probably shouldn't happen. Maybe this should/could be a global thing? Always check for event target types.

## Design

Expand All @@ -108,3 +110,5 @@ A `Track` is a single mono or stereo audio buffer that contains audio data. A `T
- [ ] Add dark mode toggle button
- [ ] allow Track to wrap (controls top, waveform bottom)
- [ ] make Track controls slightly less wide
- [ ] add track ID indicator so keyboard controls make sense
- [ ] make beat counter sticky so you can see it even when you scroll
2 changes: 1 addition & 1 deletion scripts/postbuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@

for filepath in build/static/media/*.js; do
filename=$(basename $filepath | sed -E 's|([^\.]+).*|\1|')
cp src/worklets/$filename.js "build/static/media/$(ls build/static/media | grep $filename)"
cp src/workers/$filename.js "build/static/media/$(ls build/static/media | grep $filename)"
done
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const trackKeyBindings = {
i: 'Monitor input',
}

export default function KeyboardBindings() {
export function KeyboardBindingsList() {
return (
<div>
<h2 className="text-xl mb-8 mt-16">Keyboard controls</h2>
Expand Down
6 changes: 4 additions & 2 deletions src/App/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react'
import { AudioProvider } from '../AudioProvider'
import { Clock } from '../Clock'
import { KeyboardProvider } from '../KeyboardProvider'
import { Metronome } from '../Metronome'
import { KeyboardBindingsList } from './KeyboardBindingsList'

type Props = {
stream: MediaStream
Expand All @@ -12,7 +13,8 @@ function App(props: Props) {
return (
<KeyboardProvider>
<AudioProvider stream={props.stream} audioContext={props.audioContext}>
<Metronome />
<Clock />
<KeyboardBindingsList />
</AudioProvider>
</KeyboardProvider>
)
Expand Down
25 changes: 25 additions & 0 deletions src/Clock/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useMemo } from 'react'
import { Metronome } from '../Metronome'
import { Scene } from '../Scene'

export const Clock: React.FC = () => {
/**
* Instantiate the clock worker.
* This is truly the heartbeat of the entire app 🥹
* Workers should be loaded exactly once for a Component.
* The `import.meta.url` is thanks to this SO answer https://stackoverflow.com/a/71134400/3991555,
* which is just a digestible version of the webpack docs https://webpack.js.org/guides/web-workers/
* I tried refactoring this into a custom hook but ran into all sorts of weird issues. This is easy enough so leaving as is
*/
const clock = useMemo(
() => new Worker(new URL('../workers/clock', import.meta.url)),
[]
)

return (
<>
<Metronome clock={clock} />
<Scene clock={clock} />
</>
)
}
17 changes: 0 additions & 17 deletions src/ControlPanel/BeatCounter.tsx

This file was deleted.

48 changes: 0 additions & 48 deletions src/ControlPanel/MetronomeControls.tsx

This file was deleted.

58 changes: 0 additions & 58 deletions src/ControlPanel/index.tsx

This file was deleted.

40 changes: 40 additions & 0 deletions src/Metronome/BeatCounter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useState } from 'react'
import type { ClockControllerMessage } from '../workers/clock'
import { ControlPanelItem } from './ControlPanelItem'

type BeatCounterProps = {
clock: Worker
beatsPerMeasure: number
}
export function BeatCounter(props: BeatCounterProps) {
const [currentTick, setCurrentTick] = useState(0)

/**
* Add clock event listeners
*/
useEffect(() => {
const clockMessageHandler = (
event: MessageEvent<ClockControllerMessage>
) => {
if (event.data.message === 'TICK') {
setCurrentTick(event.data.currentTick)
}
}
props.clock.addEventListener('message', clockMessageHandler)
return () => {
props.clock.removeEventListener('message', clockMessageHandler)
}
}, [props.clock, props.beatsPerMeasure])

return (
<ControlPanelItem>
<span className="font-mono text-2xl pr-2">
{/* `+ 1` to convert "computer numbers" to "musician numbers" */}
{(currentTick % props.beatsPerMeasure) + 1}
</span>
<span className="font-mono text-l">
. {Math.floor(currentTick / props.beatsPerMeasure) + 1}
</span>
</ControlPanelItem>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ type Props = {
}
// This component was part of an earlier design iteration.
// It should be removed eventually if its just a div
export default function ControlPanelItem(props: Props) {
export function ControlPanelItem(props: Props) {
return <div className="mr-4">{props.children}</div>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ControlPanelItem from './ControlPanelItem'
import { ControlPanelItem } from '../ControlPanelItem'

type MeasuresPerLoopProps = {
onChange(measuresPerLoop: number): void
measuresPerLoop: number
}
export default function MeasuresPerLoop(props: MeasuresPerLoopProps) {
export function MeasuresPerLoopControl(props: MeasuresPerLoopProps) {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const measuresPerLoop = Number(event.target.value)
if (Number.isNaN(measuresPerLoop)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useState } from 'react'
import ControlPanelItem from './ControlPanelItem'
import { ControlPanelItem } from '../ControlPanelItem'

type TempoProps = {
onChange(bpm: number): void
defaultValue: number
}
export default function Tempo(props: TempoProps) {
export function TempoControl(props: TempoProps) {
const [visualBpm, setVisualBpm] = useState('120.0')
// TODO: something about this isn't working right
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { TimeSignature as TimeSignatureType } from '../Metronome'
import ControlPanelItem from './ControlPanelItem'
import { TimeSignature } from '..'
import { ControlPanelItem } from '../ControlPanelItem'

type TimeSignatureProps = {
onChange(signature: TimeSignatureType): void
onChange(signature: TimeSignature): void
beatsPerMeasure: number
beatUnit: number
}
export default function TimeSignature(props: TimeSignatureProps) {
export function TimeSignatureControl(props: TimeSignatureProps) {
const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (event) => {
const [beatsPerMeasureStr, beatUnitStr] = event.target.value?.split('/')
if (!beatsPerMeasureStr || !beatUnitStr) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ButtonBase from '../ButtonBase'
import MetronomeIcon from '../icons/MetronomeIcon'
import ButtonBase from '../../ButtonBase'
import MetronomeIcon from '../../icons/MetronomeIcon'

type Props = {
muted: boolean
Expand Down
Loading

0 comments on commit 3d3cf38

Please sign in to comment.