Skip to content

Commit

Permalink
Better sequencer UI, ability to delete modules, division for clock mo…
Browse files Browse the repository at this point in the history
…dule
  • Loading branch information
oamaok committed Oct 19, 2021
1 parent 46ec1fc commit 30487b1
Show file tree
Hide file tree
Showing 22 changed files with 265 additions and 74 deletions.
13 changes: 11 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@

## Modules

- Keyboard UI component (svg?)
- MIDI input
- Better oscillator, maybe multiple
- Mixer
- Octave
- Delay
- Reverb
- Distortion
- Compressor
- Limiter
- LFO

- Controllable ADSR

## Knobs

Expand All @@ -19,7 +28,7 @@
# Module selector

- Starred items
- Only accesible via hotkey?
- Ability to drag the modules from the selector

# General UI/UX

Expand Down
8 changes: 4 additions & 4 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const buildClient = async () => {
incremental: true,
jsxFactory: 'h',
jsxFragment: 'Fragment',
minify: true,
minify: isProduction,
define: {
'process.env.NODE_ENV': '"production"',
},
Expand All @@ -82,7 +82,7 @@ const buildWorklets = async () => {
bundle: true,
outfile: `./dist/client/worklets/${worklet}.js`,
incremental: true,
minify: false,
minify: isProduction,
})
)
)
Expand Down Expand Up @@ -124,14 +124,14 @@ ${worklets.map((worklet) => ` ${worklet}: typeof ${worklet}`).join('\n')}
}

chokidar
.watch('./worklets/*', {
.watch('./client/worklets/*', {
persistent: true,
ignoreInitial: true,
})
.on('all', buildWorklets)

chokidar
.watch('./client/**/*', {
.watch('./client/src/**/*', {
persistent: true,
ignoreInitial: true,
})
Expand Down
9 changes: 8 additions & 1 deletion build/css-modules-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ const classNames = {}

let currentId = 0
const nextName = () => {
return '_' + (currentId++).toString(36)
let i = ++currentId
let a = ''
while (i > 0) {
const mod = i % 26
a += String.fromCharCode(mod + 65)
i = Math.floor((i - mod) / 26)
}
return a
}

const CssModulesPlugin = () => ({
Expand Down
6 changes: 6 additions & 0 deletions client/src/components/module-parts/Keyboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.keyboard svg {
border-radius: 3px;
width: 152px;
height: 60px;
overflow: hidden;
}
60 changes: 60 additions & 0 deletions client/src/components/module-parts/Keyboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { h, Fragment } from 'kaiku'
import classNames from 'classnames/bind'
import styles from './Keyboard.css'

const css = classNames.bind(styles)

const WHITE_KEYS = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
const BLACK_KEYS = ['C#', 'D#', null, 'F#', 'G#', 'A#']

type Props = {
note: string
onChange: (note: string) => void
}

const Keyboard = ({ note, onChange }: Props) => {
return (
<div className={css('keyboard')}>
<svg viewbox="0 0 152 60">
<defs>
<g id="white-key">
<rect fill="white" x="0" y="0" width="20" height="60" />
<circle cx="10" cy="50" r="4" fill="#c7c7c7" />
</g>
<g id="white-key-active">
<rect fill="white" x="0" y="0" width="20" height="60" />
<circle cx="10" cy="50" r="4" fill="#e85d00" />
</g>
<g id="black-key">
<path fill="#333333" d="M 0 0 H 20 V 35 l -10 3 l -10 -3 Z" />
<circle cx="10" cy="25" r="4" fill="#c7c7c7" />
</g>
<g id="black-key-active">
<path fill="#333333" d="M 0 0 H 20 V 35 l -10 3 l -10 -3 Z" />
<circle cx="10" cy="25" r="4" fill="#e85d00" />
</g>
</defs>

{WHITE_KEYS.map((key, index) => (
<use
href={key === note ? '#white-key-active' : '#white-key'}
x={index * 22}
onClick={() => onChange(key)}
/>
))}
{BLACK_KEYS.map(
(key, index) =>
key && (
<use
href={key === note ? '#black-key-active' : '#black-key'}
x={10 + index * 22}
onClick={() => onChange(key)}
/>
)
)}
</svg>
</div>
)
}

export default Keyboard
1 change: 1 addition & 0 deletions client/src/components/module-parts/Module.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
display: flex;
min-height: 60px;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
}
2 changes: 1 addition & 1 deletion client/src/components/module-parts/ModuleSockets.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
justify-content: space-evenly;
}

.module-outputs {
Expand Down
7 changes: 3 additions & 4 deletions client/src/components/module-parts/Socket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import styles from './Socket.css'

const css = classNames.bind(styles)

type Props = RegisteredSocket

const getSocketOffset = (
element: HTMLElement,
pos: Vec2 = { x: 0, y: 0 }
Expand All @@ -35,11 +33,12 @@ const getSocketOffset = (
})
}

const Socket = ({ moduleId, type, name, node }: Props) => {
const Socket = (socket: RegisteredSocket) => {
const { moduleId, type, name, node } = socket
const ref = useRef<HTMLDivElement>()

useEffect(() => {
registerSocket({ moduleId, name, node, type } as RegisteredSocket)
registerSocket(socket)
return () => unregisterSocket(moduleId, name)
})

Expand Down
33 changes: 31 additions & 2 deletions client/src/components/modules/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ class Clock extends Component<Props> implements IModule {
constructor(props: Props) {
super(props)
const audioContext = getAudioContext()
this.node = new WorkletNode(audioContext, 'Clock')
this.node = new WorkletNode(audioContext, 'Clock', {
numberOfOutputs: 4,
})
const tempo = this.node.parameters.get('tempo')
const pulseWidth = this.node.parameters.get('pulseWidth')

Expand All @@ -33,7 +35,34 @@ class Clock extends Component<Props> implements IModule {
<Knob moduleId={id} name="tempo" min={1} max={500} initial={128} />
<Knob moduleId={id} name="pulseWidth" min={0} max={1} initial={0.5} />
<ModuleOutputs>
<Socket moduleId={id} type="output" name="out" node={this.node} />
<Socket
moduleId={id}
type="output"
name="1/4"
node={this.node}
output={0}
/>
<Socket
moduleId={id}
type="output"
name="1/8"
node={this.node}
output={1}
/>
<Socket
moduleId={id}
type="output"
name="1/16"
node={this.node}
output={2}
/>
<Socket
moduleId={id}
type="output"
name="1/32"
node={this.node}
output={3}
/>
</ModuleOutputs>
</Module>
)
Expand Down
21 changes: 21 additions & 0 deletions client/src/components/modules/Sequencer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.steps {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 10px;
}

.indicator {
width: 10px;
height: 10px;
margin: 0 1px;

border: 2px solid #111;
border-radius: 50%;
}

.indicator.on {
background-color: var(--primary);
}
83 changes: 56 additions & 27 deletions client/src/components/modules/Sequencer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import { connectKnobToParam } from '../../modules'
import Socket from '../module-parts/Socket'
import Module from '../module-parts/Module'
import Knob from '../module-parts/Knob'
import classNames from 'classnames/bind'
import styles from './Sequencer.css'

const css = classNames.bind(styles)

import { ModuleInputs, ModuleOutputs } from '../module-parts/ModuleSockets'
import Keyboard from '../module-parts/Keyboard'
type Props = {
id: Id
}
Expand All @@ -34,20 +39,22 @@ const OCTAVES = [1, 2, 3, 4, 5, 6, 7, 8, 9]
type Note = {
name: NoteName
octave: number
enabled: boolean
gate: boolean
}

type SequencerState = {
editing: number
notes: Note[]
}

const initialState: SequencerState = {
editing: 0,
notes: Array(16)
.fill(null)
.map(() => ({
name: 'A',
octave: 4,
enabled: true,
gate: true,
})),
}

Expand All @@ -71,43 +78,65 @@ class Sequencer extends Component<Props> implements IModule {
this.node.port.postMessage(
notes.map((note) => ({
voltage: note.octave + (NOTE_NAMES.indexOf(note.name) * 1) / 12,
gate: note.enabled,
gate: note.gate,
}))
)
})
}

render({ id }: Props) {
const { notes } = getModuleState<SequencerState>(id)
const moduleState = getModuleState<SequencerState>(id)
const { editing, notes } = moduleState

return (
<Module id={id} name="Sequencer" width={460}>
{notes.map((note) => (
<div className="sequencer-note">
<select
onChange={(evt: any) => (note.name = evt.target.value)}
value={note.name}
>
{NOTE_NAMES.map((name) => (
<option selected={note.name === name} value={name}>
{name}
</option>
))}
</select>
<select onChange={(evt: any) => (note.octave = +evt.target.value)}>
{OCTAVES.map((oct) => (
<option selected={note.octave === +oct} value={oct}>
{oct}
</option>
))}
</select>
<Module id={id} name="Sequencer" width={300}>
<div className={css('sequencer')}>
<div className={css('steps')}>
{notes.map((_, i) => (
<div
className={css('indicator', {
on: i === editing,
})}
onClick={() => {
moduleState.editing = i
}}
></div>
))}
</div>
))}
<Keyboard
note={notes[editing].name}
onChange={(note) => {
notes[editing].name = note as NoteName
}}
/>
Gate
<div
className={css('indicator', {
on: notes[editing].gate,
})}
onClick={() => {
notes[editing].gate = !notes[editing].gate
}}
></div>
</div>
<ModuleInputs>
<Socket moduleId={id} type="input" name="gate" node={this.node} />
<Socket moduleId={id} type="input" name="Gate" node={this.node} />
</ModuleInputs>
<ModuleOutputs>
<Socket moduleId={id} type="output" name="out" node={this.node} />
<Socket
moduleId={id}
type="output"
name="CV"
node={this.node}
output={0}
/>
<Socket
moduleId={id}
type="output"
name="Out Gate"
node={this.node}
output={1}
/>
</ModuleOutputs>
</Module>
)
Expand Down
Loading

0 comments on commit 30487b1

Please sign in to comment.