Skip to content

Commit

Permalink
chore: extract common control templating
Browse files Browse the repository at this point in the history
  • Loading branch information
dszakallas committed Dec 10, 2023
1 parent 74fecde commit 34c3925
Show file tree
Hide file tree
Showing 34 changed files with 1,222 additions and 961 deletions.
268 changes: 165 additions & 103 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"workspaces": [
"packages/common",
"packages/mixxx",
"packages/launch-common",
"packages/launchpad-common",
"packages/launchpad-mk1",
"packages/launchpad-mk2",
Expand Down
11 changes: 11 additions & 0 deletions packages/launch-common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@mixxx-launch/launch-common",
"author": "Midiparse",
"license": "MIT",
"private": true,
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"@mixxx-launch/mixxx": "~1.0.0"
}
}
85 changes: 85 additions & 0 deletions packages/launch-common/src/Control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
Component,
} from '@mixxx-launch/mixxx'

export type BindingTemplate = {
type: new (...args: any[]) => Component,
target: any,
listeners: {
[_: string]: (control: any) => any
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type Phantom<_Ctx> = never

export type ControlType < Ctx > = {
type: string
bindings: { [k: string | number | symbol]: BindingTemplate }
params: Params
state: State
context?: Phantom<Ctx>
}

export type State = { [k: string]: any }
export type Params = { [k: string]: any }

export type ControlTemplate<C extends ControlType<any>> = {
bindings: C['bindings']
state: C['state']
}

export type MakeControlTemplate<C extends ControlType<any>> = (
params: C['params']
) => ControlTemplate<C>

export type MakeBindings<Ctx, C extends ControlType<Ctx>> = (ctx: Ctx, template: C["bindings"]) => Bindings<C>

export type Bindings<C extends ControlType<any>> = {
[K in keyof C["bindings"]]: InstanceType<C["bindings"][K]["type"]>
}

export type IControl<Ctx, C extends ControlType<Ctx>> = {
bindings: Bindings<C>
state: C['state']
context: Ctx
}

export class Control<Ctx, C extends ControlType<Ctx>> extends Component implements IControl<Ctx, C> {
templates: C['bindings']
bindings: Bindings<C>
state: C['state']
context: Ctx

constructor(makeBindings: MakeBindings<Ctx, C>, templates: C['bindings'], state: C['state'], context: Ctx) {
super()
this.bindings = makeBindings(context, templates)
this.templates = templates
this.state = state

this.context = context
}

onMount() {
super.onMount()

Object.keys(this.bindings).forEach((k) => {
const b = this.bindings[k]
Object.keys(this.templates[k].listeners).forEach((event) => {
const listener = this.templates[k].listeners[event]
if (listener != null) {
b.addListener(event, listener(this))
}
})
})

Object.values(this.bindings).forEach((b) => b.mount())
}

onUnmount() {
const bs = Object.values(this.bindings)
bs.forEach((b) => b.unmount())
bs.forEach((b) => b.removeAllListeners())
super.onUnmount()
}
}
Empty file.
15 changes: 15 additions & 0 deletions packages/launch-common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "dist"
},
"include": [
"src"
],
"references": [
{
"path": "../mixxx"
}
]
}
3 changes: 2 additions & 1 deletion packages/launchcontrol-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"@mixxx-launch/mixxx": "~1.0.0"
"@mixxx-launch/mixxx": "~1.0.0",
"@mixxx-launch/launch-common": "~1.0.0"
}
}
3 changes: 2 additions & 1 deletion packages/launchpad-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"license": "MIT",
"dependencies": {
"eventemitter3": "~5.0.0",
"@mixxx-launch/mixxx": "~1.0.0"
"@mixxx-launch/mixxx": "~1.0.0",
"@mixxx-launch/launch-common": "~1.0.0"
}
}
195 changes: 58 additions & 137 deletions packages/launchpad-common/src/Control.ts
Original file line number Diff line number Diff line change
@@ -1,169 +1,86 @@
import type { Modifier } from './ModifierSidebar'

import { array, map, range } from '@mixxx-launch/common'
import { ChannelControlDef, Component, ControlMessage, MidiMessage } from '@mixxx-launch/mixxx'
import { Theme } from './App'
import { Bindings, Control as BaseControl, ControlTemplate, ControlType as BaseControlType, IControl as BaseIControl, MakeControlTemplate } from '@mixxx-launch/launch-common/src/Control'

import {
ChannelControlDef,
Component,
ControlComponent,
ControlDef,
ControlMessage,
MidiMessage,
} from '@mixxx-launch/mixxx'
import { LaunchpadDevice, MidiComponent } from './device'

import { ControlComponent, ControlDef, getValue, root } from '@mixxx-launch/mixxx/src/Control'
import makeControlTemplateIndex, { ControlTypeIndex } from './controls'
import { default as makeSamplerPad } from './controls/samplerPad'
import { getValue, root, SamplerControlDef } from '@mixxx-launch/mixxx/src/Control'
import { Theme } from './App'
import { LaunchpadDevice, MidiComponent } from './device'
import { Modifier } from './ModifierSidebar'

// constraint types with Launchpad specific Context
export type ControlContext = {
modifier: Modifier
device: LaunchpadDevice
}

export type ControlType = {
type: string
bindings: { [k: string]: Binding }
params: Params
state: State
}

export type ControlConf<C extends ControlType> = {
type: C['type']
params?: C['params']
}
export type ControlType = BaseControlType<ControlContext>
export type Control<C extends ControlType> = BaseControl<ControlContext, C>
export type IControl<C extends ControlType> = BaseIControl<ControlContext, C>

export type ControlBindingTemplate<C extends ControlType> = {
type: 'control'
target: ControlDef
update?: (c: Control<C>) => (message: ControlMessage) => void
mount?: (c: Control<C>) => () => void
unmount?: (c: Control<C>) => () => void
}
export type MakeSamplerControlTemplate<C extends ControlType> = MakeControlTemplate<C>

export type ButtonKey = [number, number]
export type MakeDeckControlTemplate<C extends ControlType> = MakeControlTemplate<C>

export type ButtonBindingTemplate<C extends ControlType> = {
type: 'button'
target: ButtonKey
midi?: (c: Control<C>) => (message: MidiMessage) => void
mount?: (c: Control<C>) => () => void
unmount?: (c: Control<C>) => () => void
export type DeckPresetConf = {
deck: readonly { pos: [number, number]; control: {
type: ControlTypeIndex['type']
params?: Omit<ControlTypeIndex['params'], 'gridPosition' | 'deck' | 'sampler' >
} }[]
}

export type BindingTemplate<B extends Binding, C extends ControlType> = B extends ControlComponent
? ControlBindingTemplate<C>
: ButtonBindingTemplate<C>

export type State = { [k: string]: any }
export type Params = { [k: string]: any }

export type ControlTemplate<C extends ControlType> = {
bindings: {
[Prop in keyof C['bindings']]: BindingTemplate<C['bindings'][Prop], C>
}
state: C['state']
export type SamplerPalettePresetConf = {
samplerPalette: { n: number; offset: number; rows: number }
}

export type MakeControlTemplate<C extends ControlType, D> = (
params: C['params'],
gridPosition: [number, number],
deck: D,
theme: Theme,
) => ControlTemplate<C>

export type MakeSamplerControlTemplate<C extends ControlType> = MakeControlTemplate<C, SamplerControlDef>
export const isDeckPresetConf = (p: PresetConf): p is DeckPresetConf => 'deck' in p
export const isSamplerPalettePresetConf = (p: PresetConf): p is SamplerPalettePresetConf => 'samplerPalette' in p

export type MakeDeckControlTemplate<C extends ControlType> = MakeControlTemplate<C, ChannelControlDef>
export type PresetConf = DeckPresetConf | SamplerPalettePresetConf

export type Binding = ControlComponent | MidiComponent

export type IControl<C extends ControlType> = {
bindings: C['bindings']
state: C['state']
context: ControlContext
export type ControlBindingTemplate<C extends ControlType> = {
type: new (...args: any[]) => ControlComponent
target: ControlDef
listeners: {
update?: (c: Control<C>) => (message: ControlMessage) => void
mount?: (c: Control<C>) => () => void
unmount?: (c: Control<C>) => () => void
}
}

const controlListeners = ['update', 'mount', 'unmount'] as const

const midiListeners = ['midi', 'mount', 'unmount'] as const

const nameOf = (x: number, y: number) => `${7 - y},${x}`
export type ButtonKey = readonly [number, number]

export class Control<C extends ControlType> extends Component implements IControl<C> {
bindings: C['bindings']
bindingTemplates: ControlTemplate<C>['bindings']
state: C['state']
context: ControlContext

constructor(ctx: ControlContext, controlTemplate: ControlTemplate<C>) {
super()
const bindings: { [k: string]: any } = {}
for (const k in controlTemplate.bindings) {
const bt = controlTemplate.bindings[k]
bindings[k] =
bt.type == 'control'
? new ControlComponent(bt.target)
: new MidiComponent(ctx.device, ctx.device.controls[nameOf(...bt.target)])
}
this.bindingTemplates = controlTemplate.bindings
this.bindings = bindings
this.state = controlTemplate.state
this.context = ctx
export type ButtonBindingTemplate<C extends ControlType> = {
type: new (...args: any[]) => MidiComponent
target: ButtonKey
listeners: {
midi?: (c: Control<C>) => (message: MidiMessage) => void
mount?: (c: Control<C>) => () => void
unmount?: (c: Control<C>) => () => void
}
}

onMount() {
super.onMount()
export type BindingTemplates<C extends ControlType> = {
[K: string]: ButtonBindingTemplate<C> | ControlBindingTemplate<C>
}

Object.keys(this.bindings).forEach((k) => {
const b = this.bindings[k]
if (b instanceof ControlComponent) {
const bt = this.bindingTemplates[k] as ControlBindingTemplate<any>
controlListeners.forEach((event) => {
const listener = bt[event]
if (listener != null) {
b.addListener(event, listener(this))
}
})
} else {
const bt = this.bindingTemplates[k] as ButtonBindingTemplate<any>
midiListeners.forEach((event) => {
const listener = bt[event]
if (listener) {
b.addListener(event, listener(this))
}
})
// add a default handler to clear the button LED
b.addListener('unmount', () => {
this.context.device.clearColor(b.control)
})
}
})

Object.values(this.bindings).forEach((b) => b.mount())
}
const nameOf = (x: number, y: number) => `${7 - y},${x}`

onUnmount() {
const bs = Object.values(this.bindings)
bs.forEach((b) => b.unmount())
bs.forEach((b) => b.removeAllListeners())
super.onUnmount()
const makeBindings = <C extends ControlType>(ctx: ControlContext, t: BindingTemplates<C>): Bindings<C> => {
const ret: { [_: string]: any } = {}
for (const k in t) {
ret[k] = t[k].type === ControlComponent
? new ControlComponent((t[k] as ControlBindingTemplate<C>).target)
: new MidiComponent(ctx.device, ctx.device.controls[nameOf(...((t[k] as ButtonBindingTemplate<C>).target))])
}
return ret as Bindings<C>
}

export type DeckPresetConf = {
deck: readonly { pos: [number, number]; control: ControlConf<ControlTypeIndex> }[]
}

export type SamplerPalettePresetConf = {
samplerPalette: { n: number; offset: number; rows: number }
}

export const isDeckPresetConf = (p: PresetConf): p is DeckPresetConf => 'deck' in p
export const isSamplerPalettePresetConf = (p: PresetConf): p is SamplerPalettePresetConf => 'samplerPalette' in p

export type PresetConf = DeckPresetConf | SamplerPalettePresetConf

type PresetTemplate = {
controls: ControlTemplate<any>[]
Expand All @@ -178,9 +95,11 @@ export class Preset extends Component implements IPreset {

constructor(ctx: ControlContext, presetTemplate: PresetTemplate) {
super()
this.controls = presetTemplate.controls.map((c) => new Control(ctx, c))
this.controls = presetTemplate.controls.map((c) => {
return new BaseControl(makeBindings, c.bindings, c.state, ctx)
})
}

onMount() {
super.onMount()
for (const control of this.controls) {
Expand All @@ -205,7 +124,7 @@ const makeDeckPresetTemplate = (
theme: Theme,
): PresetTemplate => ({
controls: conf.deck.map(({ pos, control: { type, params } }) =>
makeControlTemplateIndex[type](params as unknown as any, tr(gridPosition, pos), deck, theme),
makeControlTemplateIndex[type](Object.assign({theme, gridPosition: tr(gridPosition, pos), deck}, params) as unknown as any),
),
})

Expand All @@ -218,7 +137,7 @@ const makeSamplerPalettePresetTemplate = (
controls: array(map((i) => {
const dy = 7 - ~~(i / rows)
const dx = i % rows
return makeSamplerPad({}, tr(gridPosition, [dx, dy]), root.samplers[i + offset], theme)
return makeSamplerPad({ theme, gridPosition: tr(gridPosition, [dx, dy]), sampler: root.samplers[i + offset]})
}, range(Math.min(n, getValue(root.master.num_samplers))))),
})

Expand All @@ -234,3 +153,5 @@ export const makePresetTemplate = (
return makeSamplerPalettePresetTemplate(conf, gridPosition, channel, theme)
}
}


Loading

0 comments on commit 34c3925

Please sign in to comment.