aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--public/main.css12
-rw-r--r--public/sounds/bass.opus (renamed from public/sounds/kick.opus)bin1743 -> 1743 bytes
-rw-r--r--public/sounds/hit-hat-closed.opusbin0 -> 3920 bytes
-rw-r--r--public/sounds/snare.opusbin0 -> 5577 bytes
-rw-r--r--src/sounds.ts37
-rw-r--r--src/view/sequencer.ts75
-rw-r--r--src/view/sequencer/block.ts9
8 files changed, 103 insertions, 38 deletions
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/kick.opus b/public/sounds/bass.opus
index 40a8d60..40a8d60 100644
--- a/public/sounds/kick.opus
+++ b/public/sounds/bass.opus
Binary files differ
diff --git a/public/sounds/hit-hat-closed.opus b/public/sounds/hit-hat-closed.opus
new file mode 100644
index 0000000..1513a5e
--- /dev/null
+++ b/public/sounds/hit-hat-closed.opus
Binary files differ
diff --git a/public/sounds/snare.opus b/public/sounds/snare.opus
new file mode 100644
index 0000000..1808dd3
--- /dev/null
+++ b/public/sounds/snare.opus
Binary files 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<Sound, AudioBuffer>
+
+export enum Sound {
+ Bass,
+ Snare,
+ HitHatClosed,
+}
+
+export function all(): Array<Sound> {
+ return [Sound.HitHatClosed, Sound.Snare, Sound.Bass]
}
const audioContext = new AudioContext()
@@ -9,21 +17,30 @@ export async function load(): Promise<Sounds> {
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<AudioBuffer> {
+ 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<Params>) {
+ 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,