Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Clock display #498

Open
wants to merge 11 commits into
base: alpha
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
]
},
"scripts": {
"dev": "lerna run dev --parallel --scope @hedron/engine --scope @hedron/desktop",
"dev": "lerna run dev --parallel --scope @hedron/clock --scope @hedron/engine --scope @hedron/desktop",
"dev:clock": "lerna run dev --parallel --scope @hedron/clock --scope @hedron/clock-test-app",
"storybook": "lerna run storybook --scope @hedron/desktop",
"build:engine": "lerna run build --scope @hedron/engine",
Expand Down
12 changes: 10 additions & 2 deletions packages/clock-test-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function App() {
useEffect(() => {
clockRef.current = new Clock(DEFAULT_BPM)

const unsubscribeBeats = clockRef.current.onNewBeat((beat) => {
$text('beat', beat)
})

const unsubscribeBpm = clockRef.current.onBpmChange((bpm) => {
$text('smoothedBpm', bpm)
})

let raf = 0

const midiClockListener = new MidiClockListener({
Expand Down Expand Up @@ -55,9 +63,7 @@ function App() {
}

$text('delta', d)
$text('smoothedBpm', clockRef.current!.smoothedBpm)
$text('bpm', Math.round(clockRef.current!.bpm * 100) / 100)
$text('beat', clockRef.current!.beatCount)
$text('beatPulseOffset', Math.round(clockRef.current!.beatPulseOffset * 10000) / 10000)

$y('saw', (d * H) % H)
Expand All @@ -74,6 +80,8 @@ function App() {
return () => {
midiClockListener.clearMidiEventListeners()
cancelAnimationFrame(raf)
unsubscribeBeats()
unsubscribeBpm()
}
}, [])

Expand Down
78 changes: 75 additions & 3 deletions packages/clock/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ export class Clock {
private _smoothedBpm: number = 0
private _smoothedBeatPulseOffset: number = 0
private _lastBeatTimestamp: number | null = null
private _onNewBeat?: (beatCount: number) => void
private _onBpmChange?: (bpm: number) => void
private _onIsRunningChange?: (isRunning: boolean) => void

/**
* @param bpm Beats per minute
*/
constructor(bpm: number = 128) {
this.bpm = bpm
this.smoothedBpm = bpm
}

/**
Expand All @@ -51,7 +55,12 @@ export class Clock {
this._beatsPerMs * (timestamp - this._lastTimestamp) * (1 + this._beatPulseOffset)
this._beatDelta += deltaInc
this._beatPulseComparisonDelta += deltaInc
this._beatCount = Math.floor(this._beatDelta % 4)

const newBeatCount = Math.floor(this._beatDelta % 4)
if (newBeatCount !== this._beatCount) {
this._onNewBeat?.(newBeatCount)
this._beatCount = newBeatCount
}

requestAnimationFrame(this.tick)

Expand All @@ -66,6 +75,8 @@ export class Clock {
set bpm(bpm: number) {
this._bpm = bpm
this._beatsPerMs = bpm / MS_IN_MINUTE

this.smoothedBpm = bpm
}

/**
Expand All @@ -76,6 +87,14 @@ export class Clock {
return this._bpm
}

/**
* Gets whether the clock is currently running.
* @returns Whether the clock is currently running.
*/
get isRunning() {
return this._isRunning
}

/**
* Gets the current beat delta, a value that increments fractionally every frame, at a rate of exactly 1 per beat.
* @returns The current beat delta
Expand Down Expand Up @@ -109,6 +128,14 @@ export class Clock {
return this._smoothedBpm
}

private set smoothedBpm(bpm: number) {
if (this._smoothedBpm !== bpm) {
this._onBpmChange?.(bpm)
}

this._smoothedBpm = bpm
}

/**
* Gets the smoothed beat pulse offset. Only useful for very detailed output of info (e.g. dev stuff)
* @returns The smoothed beat pulse offset
Expand All @@ -131,6 +158,7 @@ export class Clock {

this._lastTimestamp = performance.now()
this._isRunning = true
this._onIsRunningChange?.(true)

requestAnimationFrame(this.tick)
}
Expand All @@ -147,6 +175,7 @@ export class Clock {
*/
public stop = () => {
this._isRunning = false
this._onIsRunningChange?.(false)
}

/**
Expand All @@ -161,6 +190,8 @@ export class Clock {
this._lastPulseTimestamp = null
this._shouldStartOnNextTimingPulse = false
this._timingPulseCount = 0

this._onNewBeat?.(0)
}

/**
Expand Down Expand Up @@ -209,7 +240,7 @@ export class Clock {

// lock BPM to whole number because we're just apes tapping without built in oscilators
this.bpm = Math.round(MS_IN_MINUTE / averageDelta)
this._smoothedBpm = this.bpm
this.smoothedBpm = this.bpm

this._lastTempoTap = now
}
Expand Down Expand Up @@ -250,11 +281,52 @@ export class Clock {
if (this._timingPulseCount % PPQN === 0) {
if (this._lastBeatTimestamp !== null) {
const delta = now - this._lastBeatTimestamp
this._smoothedBpm = Math.round(MS_IN_MINUTE / delta)
this.smoothedBpm = Math.round(MS_IN_MINUTE / delta)
}
this._lastBeatTimestamp = now
}

this._lastPulseTimestamp = now
}

/**
* Register a callback to be called every time a new beat is detected.
* @param callback The callback
* @returns A cleanup function to unregister the callback
*/
public onNewBeat = (callback: (beatCount: number) => void) => {
this._onNewBeat = (beat: number) => {
if (beat >= 0) {
callback(beat + 1)
}
}
this._onNewBeat(this._beatCount)

return () => {
this._onNewBeat = undefined
}
}

/**
* Register a callback to be called every time smoothed BPM changes.
* @param callback The callback
* @returns A cleanup function to unregister the callback
*/
public onBpmChange = (callback: (bpm: number) => void) => {
this._onBpmChange = callback
this._onBpmChange(this._smoothedBpm)

return () => {
this._onBpmChange = undefined
}
}

public onIsRunningChange = (callback: (isRunning: boolean) => void) => {
this._onIsRunningChange = callback
this._onIsRunningChange(this._isRunning)

return () => {
this._onIsRunningChange = undefined
}
}
}
3 changes: 2 additions & 1 deletion packages/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@floating-ui/react-dom": "^2.1.2",
"@hedron/engine": "^1.0.0-alpha.1",
"@hedron/clock": "workspace:^",
"@hedron/engine": "workspace:^",
"@redux-devtools/extension": "^3.3.0",
"@tomjs/electron-devtools-installer": "^2.3.2",
"@uiw/react-color": "^2.3.4",
Expand Down
9 changes: 9 additions & 0 deletions packages/desktop/src/renderer/components/App/App.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@
.right {
flex: 1;
}

.widgetStrip {
display: flex;
gap: 0.5rem;
}

.widgetItem {
padding: 0.25rem 0;
}
12 changes: 9 additions & 3 deletions packages/desktop/src/renderer/components/App/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// import { Audio } from '../Audio'
import c from './App.module.css'
import { GlobalClock } from '@components/GlobalClock/GlobalClock'
import { GlobalDialogs } from '@components/GlobalDialogs/GlobalDialogs'
import { PerformanceStats } from '@components/PerformanceStats/PerformanceStats'
import { Viewer } from '@components/Viewer'
import { WorkArea } from '@components/WorkArea/WorkArea'

Expand All @@ -9,9 +11,13 @@ export const App = (): JSX.Element => {
<div className={c.wrapper}>
<div className={c.left}>
<Viewer />
{/* <Audio /> */}
{/* <Overview stats={stats} />
<PanelDragger onHandleDrag={onLeftDrag} position={leftWidth} /> */}
<div className={c.widgetStrip}>
<PerformanceStats />
<div className={c.widgetItem}>
<GlobalClock />
</div>
{/* <Audio /> */}
</div>
</div>
<div className={c.right}>
<WorkArea />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react'
import { ClockDisplay } from '@components/core/ClockDisplay/ClockDisplay'
import { clock } from '@renderer/engine'

const onBpmEdit = (bpm: number) => {
clock.bpm = bpm
}

export const GlobalClock = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of ran into this with my midi work, Feel like it would be good if a packages ui etc lived in the package so all the code stays contained, easy to upgrade/switch package versions.

I tried making a quick plugin system, but feel into an issue with hooks, maybe you might have a better idea for how to get the tsx living in the subpackage. I made a pr into my working midi branch showing the changes, I'll highlight the code of note cale-bradbury#9

const [bpm, setBpm] = useState<number>(0)
const [beat, setBeat] = useState<number>(1)
const [isRunning, setIsRunning] = useState<boolean>(false)

useEffect(() => {
const unregisterBeats = clock.onNewBeat(setBeat)
const unregisterBpm = clock.onBpmChange(setBpm)
const unregisterIsRunning = clock.onIsRunningChange(setIsRunning)

return () => {
unregisterBeats()
unregisterBpm()
unregisterIsRunning()
}
}, [])

return (
<ClockDisplay
isRunning={isRunning}
bpm={bpm}
beat={beat}
onBpmEdit={onBpmEdit}
onStartClick={clock.start}
onStopClick={clock.stop}
onTapClick={clock.sendTempoTap}
onResetClick={clock.reset}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useRef } from 'react'
import { performanceMonitor } from '@renderer/engine'

export const PerformanceStats = () => {
const ref = useRef<HTMLDivElement>(null)

useEffect(() => {
if (ref.current) {
ref.current.appendChild(performanceMonitor.dom)
performanceMonitor.dom.setAttribute('style', '')
}

return () => {
performanceMonitor.dom.remove()
}
}, [])

return <div ref={ref} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,55 @@
&:hover {
background: var(--actionColor1Hover);
}
}

&:global(.secondary) {
background: var(--actionColor2);
.secondary {
background: var(--actionColor2);

&:hover {
background: var(--actionColor2Hover);
}
&:hover {
background: var(--actionColor2Hover);
}
}

&:global(.neutral) {
border: 1px solid var(--lineColor2);
color: var(--textColorLight1);
background: var(--bgColorDark1);
.ghost {
background: transparent;
border: 0;
box-shadow: none;

&:hover {
background: var(--bgColorDark2);
}
&:hover {
background: var(--bgColorDark4);
}
}

&:global(.danger) {
color: var(--textColorLight1);
background: var(--dangerColor);
.neutral {
border: 1px solid var(--lineColor2);
color: var(--textColorLight1);
background: var(--bgColorDark1);

&:hover {
background: var(--dangerColorHover);
}
&:hover {
background: var(--bgColorDark2);
}
}

&:global(.slim) {
padding: 0.25rem;
}
.danger {
color: var(--textColorLight1);
background: var(--dangerColor);

.icon {
font-size: 1.1em;
line-height: 1.1;
&:hover {
background: var(--dangerColorHover);
}
}

.slim {
padding: 0.25rem;
}

.icon {
font-size: 1.1em;
line-height: 1.1;
}

.disabled {
opacity: 0.5;
cursor: not-allowed;
}
Loading