aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoris2022-06-19 16:31:38 +0200
committerJoris2022-06-19 16:31:38 +0200
commit6571d1a72c1828d7c2ed902d07ec412110787bcf (patch)
treeec3aabd86ff4ccda2e61251e44a872d475e39b42 /src
parent91438efeff2cdad7997687ebf4139ce0723982b9 (diff)
Add snare and hit hat closed sounds
Diffstat (limited to 'src')
-rw-r--r--src/sounds.ts37
-rw-r--r--src/view/sequencer.ts75
-rw-r--r--src/view/sequencer/block.ts9
3 files changed, 90 insertions, 31 deletions
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,