From 6571d1a72c1828d7c2ed902d07ec412110787bcf Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 19 Jun 2022 16:31:38 +0200 Subject: Add snare and hit hat closed sounds --- README.md | 8 ++-- public/main.css | 12 ++++-- public/sounds/bass.opus | Bin 0 -> 1743 bytes public/sounds/hit-hat-closed.opus | Bin 0 -> 3920 bytes public/sounds/kick.opus | Bin 1743 -> 0 bytes public/sounds/snare.opus | Bin 0 -> 5577 bytes src/sounds.ts | 37 ++++++++++++++----- src/view/sequencer.ts | 75 ++++++++++++++++++++++++++++---------- src/view/sequencer/block.ts | 9 ++++- 9 files changed, 103 insertions(+), 38 deletions(-) create mode 100644 public/sounds/bass.opus create mode 100644 public/sounds/hit-hat-closed.opus delete mode 100644 public/sounds/kick.opus create mode 100644 public/sounds/snare.opus diff --git a/README.md b/README.md index d437233..b50e068 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ Then, open your browser at `http://localhost:8000`. # Sounds -- bass: https://freesound.org/people/karolist/sounds/371192/ -- snare: https://lasonotheque.org/detail-2304-caisse-claire-1.html - hit-hat (closed): https://lasonotheque.org/detail-2302-charleston-fermee-7.html +- snare: https://lasonotheque.org/detail-2304-caisse-claire-1.html +- bass: https://freesound.org/people/karolist/sounds/371192/ # Todo Multi-sound sequencer: -- [ ] Provide more drum sounds -- [ ] Add / Remove beat integrated into sequencer +- [ ] Sub divide beats ? Working on increasing tempo: +- [ ] Add / Remove beat integrated into sequencer - [ ] Augment the BPM by X after Y cycles diff --git a/public/main.css b/public/main.css index 134d92b..9540a7f 100644 --- a/public/main.css +++ b/public/main.css @@ -53,16 +53,22 @@ body { gap: var(--spacing-cat); } -.g-Sequencer__Sounds > li { +.g-Sequencer__Blocks { + display: flex; + align-items: center; + gap: var(--spacing-cat); +} + +.g-Sequencer__Column > li { display: flex; align-items: center; height: var(--spacing-horse); cursor: pointer; } -.g-Sequencer__Blocks { +.g-Sequencer__Column { display: flex; - align-items: center; + flex-direction: column; gap: var(--spacing-cat); } diff --git a/public/sounds/bass.opus b/public/sounds/bass.opus new file mode 100644 index 0000000..40a8d60 Binary files /dev/null and b/public/sounds/bass.opus differ diff --git a/public/sounds/hit-hat-closed.opus b/public/sounds/hit-hat-closed.opus new file mode 100644 index 0000000..1513a5e Binary files /dev/null and b/public/sounds/hit-hat-closed.opus differ diff --git a/public/sounds/kick.opus b/public/sounds/kick.opus deleted file mode 100644 index 40a8d60..0000000 Binary files a/public/sounds/kick.opus and /dev/null differ diff --git a/public/sounds/snare.opus b/public/sounds/snare.opus new file mode 100644 index 0000000..1808dd3 Binary files /dev/null and b/public/sounds/snare.opus differ diff --git a/src/sounds.ts b/src/sounds.ts index 9ce8d2e..5e4c68a 100644 --- a/src/sounds.ts +++ b/src/sounds.ts @@ -1,5 +1,13 @@ -export interface Sounds { - kick: AudioBuffer +export type Sounds = Record + +export enum Sound { + Bass, + Snare, + HitHatClosed, +} + +export function all(): Array { + return [Sound.HitHatClosed, Sound.Snare, Sound.Bass] } const audioContext = new AudioContext() @@ -9,21 +17,30 @@ export async function load(): Promise { if (lazy !== undefined) { return lazy } else { + let [bass, snare, hitHatClosed] = await Promise.all([ + fetchSound('/sounds/bass.opus'), + fetchSound('/sounds/snare.opus'), + fetchSound('/sounds/hit-hat-closed.opus') + ]) - const kick = await fetch('/sounds/kick.opus') - .then(res => res.arrayBuffer()) - .then(ArrayBuffer => audioContext.decodeAudioData(ArrayBuffer)) - - lazy = { - kick + lazy = { + [Sound.Bass]: bass, + [Sound.Snare]: snare, + [Sound.HitHatClosed]: hitHatClosed } return lazy } } -export function playKick(sounds: Sounds) { +async function fetchSound(name: string): Promise { + return await fetch(name) + .then(res => res.arrayBuffer()) + .then(ArrayBuffer => audioContext.decodeAudioData(ArrayBuffer)) +} + +export function play(sounds: Sounds, sound: Sound): void { const source = audioContext.createBufferSource() - source.buffer = sounds.kick + source.buffer = sounds[sound] source.connect(audioContext.destination) source.start() } diff --git a/src/view/sequencer.ts b/src/view/sequencer.ts index 3bca278..74eb0ea 100644 --- a/src/view/sequencer.ts +++ b/src/view/sequencer.ts @@ -1,18 +1,33 @@ import h, { classNames } from 'lib/h' import * as soundsLib from 'sounds' +import { Sound } from 'sounds' import * as play from 'view/sequencer/play' import * as addRemoveBeat from 'view/sequencer/addRemoveBeat' import * as block from 'view/sequencer/block' export function view() { let index = -1 - let blocks = [true] + let blocks = [{ + [Sound.Bass]: true, + [Sound.Snare]: false, + [Sound.HitHatClosed]: false, + }] let blocksNode = h('div', { className: 'g-Sequencer__Blocks' }, - block.view({ - checked: true, - onCheck: checked => blocks[0] = checked - }) + block.column([ + { + checked: false, + onCheck: checked => blocks[0][Sound.HitHatClosed] = checked + }, + { + checked: false, + onCheck: checked => blocks[0][Sound.Snare] = checked + }, + { + checked: true, + onCheck: checked => blocks[0][Sound.Bass] = checked + } + ]) ) let onNextStep = (sounds: soundsLib.Sounds) => { @@ -24,9 +39,17 @@ export function view() { if (oldBlock !== undefined) oldBlock.classList.remove('g-Sequencer__Block--Beat') let newBlock = blocksNode.childNodes[newIndex] as HTMLElement + + // Trigger reflow between removing and adding the classname. + // Allow to re-trigger the animation if there is only one column. + // See https://css-tricks.com/restart-css-animation/ + void newBlock.offsetWidth + newBlock.classList.add('g-Sequencer__Block--Beat') - if (blocks[newIndex]) soundsLib.playKick(sounds) + soundsLib.all().forEach(sound => { + if (blocks[newIndex][sound]) soundsLib.play(sounds, sound) + }) } let sequencer = h('div', { className: 'g-Sequencer' }, @@ -46,25 +69,26 @@ export function view() { blocks.pop() }, onAdd: index => { - blocks.push(false) - blocksNode.appendChild(block.view({ - checked: false, - onCheck: checked => blocks[index] = checked - })) + blocks.push({ + [Sound.Bass]: false, + [Sound.Snare]: false, + [Sound.HitHatClosed]: false, + }) + blocksNode.appendChild(block.column( + soundsLib.all().map(sound => ({ + checked: false, + onCheck: checked => blocks[index][sound] = checked + })) + )) } }), h('div', { className: 'g-Sequencer__Grid' }, h('ol', - { className: 'g-Sequencer__Sounds' }, - h('li', - { onclick: async () => { - let sounds = await soundsLib.load() - soundsLib.playKick(sounds) - } - }, - 'Bass' - ) + { className: 'g-Sequencer__Column' }, + soundItem('Hit-hat (closed)', Sound.HitHatClosed), + soundItem('Snare', Sound.Snare), + soundItem('Bass', Sound.Bass) ), blocksNode ) @@ -72,3 +96,14 @@ export function view() { return sequencer } + +function soundItem(name: string, sound: Sound): Element { + return h('li', + { onclick: async () => { + let sounds = await soundsLib.load() + soundsLib.play(sounds, sound) + } + }, + name + ) +} diff --git a/src/view/sequencer/block.ts b/src/view/sequencer/block.ts index 5776120..ff8d2db 100644 --- a/src/view/sequencer/block.ts +++ b/src/view/sequencer/block.ts @@ -5,7 +5,14 @@ interface Params { onCheck: (checked: boolean) => void } -export function view({ checked, onCheck }: Params) { +export function column(xs: Array) { + return h('ol', + { className: 'g-Sequencer__Column' }, + ...xs.map(params => h('li', {}, block(params))) + ) +} + +function block({ checked, onCheck }: Params) { return h('div', { className: classNames({ 'g-Sequencer__Block': true, -- cgit v1.2.3