Skip to content

Commit

Permalink
Add sample browser
Browse files Browse the repository at this point in the history
  • Loading branch information
oamaok committed Jul 14, 2023
1 parent 12c3745 commit 96b2621
Show file tree
Hide file tree
Showing 13 changed files with 230 additions and 39 deletions.
2 changes: 2 additions & 0 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,5 @@ export const saveSample = (
formData.append('buffer', new Blob([buffer], { type: 'octet/stream' }))
return post(`/api/sample`, { body: formData })
}

export const getSamples = () => get('/api/samples')
4 changes: 2 additions & 2 deletions client/src/components/module-parts/Module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
position: absolute;
top: 0;
left: 0;
opacity: 0.8;
filter: brightness(0.75);
box-shadow: var(--box-shadow);
display: flex;
flex-direction: column;
}

.module.active {
opacity: 1;
filter: none;
}

.module-name {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/modules/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class ClockNode extends Component<Props> {
connectKnobToParam<Clock, 'pw2'>(props.id, 'pulseWidth2', 6)
connectKnobToParam<Clock, 'swing0'>(props.id, 'swing0', 7)
connectKnobToParam<Clock, 'swing1'>(props.id, 'swing1', 8)
connectKnobToParam<Clock, 'swing2'>(props.id, 'swing1', 9)
connectKnobToParam<Clock, 'swing2'>(props.id, 'swing2', 9)
}

render({ id }: Props) {
Expand Down
11 changes: 10 additions & 1 deletion client/src/components/modules/Sampler.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.sampler {
padding: 8px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}

.canvases {
Expand All @@ -8,8 +11,11 @@
height: 100px;
width: 300px;
border-radius: 4px;
box-shadow: var(--box-shadow);
border-left: 1px solid #555;
border-right: 1px solid #555;
border-top: 2px solid #666;
overflow: hidden;
margin-bottom: 8px;
}

.canvases canvas {
Expand All @@ -21,4 +27,7 @@
.knob-group {
display: flex;
flex-direction: row;
justify-content: space-between;
flex-grow: 1;
margin-left: 10px;
}
41 changes: 14 additions & 27 deletions client/src/components/modules/Sampler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getModuleState,
setModuleState,
} from '../../state'
import SampleBrowser from '../sample-browser/SampleBrowser'

type Props = {
id: string
Expand Down Expand Up @@ -163,40 +164,26 @@ class SamplerNode extends Component<Props, { playheadPosition: number }> {
})
}

onFileChange = async (evt: InputEvent) => {
const fileElement = evt.target! as HTMLInputElement
const file = fileElement.files?.[0]
if (file) {
const buffer = (
await engine.getAudioContext().decodeAudioData(await file.arrayBuffer())
).getChannelData(0)

if (buffer.length > 44100 * 20) {
// TODO: Tell user that the sample is too long
return
}

const { id } = await api.saveSample(file.name, buffer)

setModuleState<SamplerState>(this.props.id, {
sampleId: id,
})
}
}

render({ id }: Props) {
const samplerState = getModuleState<SamplerState>(id)

return (
<Module id={id} name="Sampler" height={240} width={400}>
<Module id={id} name="Sampler" height={260} width={400}>
<div className={css('sampler')}>
<div className={css('canvases')}>
<canvas ref={this.waveformCanvasRef} width="300" height="100" />
<canvas ref={this.settingsCanvasRef} width="300" height="100" />
</div>
<input
type="file"
accept=".wav,.mp3,.ogg"
onChange={this.onFileChange}
/>
<div className={css('browser')}>
<SampleBrowser
onSelect={(id) => {
setModuleState<SamplerState>(this.props.id, {
sampleId: id,
})
}}
selected={samplerState?.sampleId ?? null}
/>
</div>
<div className={css('knob-group')}>
<Knob
moduleId={id}
Expand Down
68 changes: 68 additions & 0 deletions client/src/components/sample-browser/SampleBrowser.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
.sample-browser {
background: #000;
height: 107px;
width: 160px;
border-left: 1px solid #555;
border-right: 1px solid #555;
border-top: 2px solid #666;
border-radius: 4px;
padding: 4px;
overflow: hidden;
}

.file-input {
display: none;
}

.samples {
max-height: 80px;
overflow-y: scroll;
display: flex;
flex-direction: column;
border: 1px solid var(--primary);
margin-bottom: 4px;
}

.samples::-webkit-scrollbar {
width: 8px;
}

.samples::-webkit-scrollbar-track {
background: #000;
border-left: 1px solid var(--primary);
}

.samples::-webkit-scrollbar-thumb {
background: var(--primary);
}

.samples button {
font-size: 12px;
text-decoration: none;
text-transform: uppercase;
display: block;
text-align: left;
color: var(--primary);
border-bottom: 1px solid var(--primary);
padding: 0 2px;
}

.samples button.selected {
background-color: var(--primary);
color: #000;
}
.controls {
display: flex;
justify-content: end;
}

.controls button {
font-size: 12px;
text-decoration: none;
text-transform: uppercase;
display: block;
text-align: left;
color: var(--primary);
border: 1px solid var(--primary);
padding: 0 2px;
}
81 changes: 81 additions & 0 deletions client/src/components/sample-browser/SampleBrowser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { h, useEffect, useRef, useState } from 'kaiku'
import * as api from '../../api'
import * as engine from '../../engine'
import css from './SampleBrowser.css'

type Props = {
onSelect: (id: string) => void
selected: null | string
}

const SampleBrowser = ({ onSelect, selected }: Props) => {
const fileInputRef = useRef<HTMLInputElement>()
const browserState = useState<{
samples: {
id: string
name: string
ownerId: string
}[]
}>({
samples: [],
})

const fetchSamples = () => {
api.getSamples().then((samples) => {
browserState.samples = samples
})
}

useEffect(fetchSamples)

const onFileChange = async (evt: InputEvent) => {
const fileElement = evt.target! as HTMLInputElement
const file = fileElement.files?.[0]
if (file) {
const buffer = (
await engine.getAudioContext().decodeAudioData(await file.arrayBuffer())
).getChannelData(0)

if (buffer.length > 44100 * 20) {
// TODO: Tell user that the sample is too long
return
}

const { id } = await api.saveSample(file.name, buffer)
onSelect(id)
fetchSamples()
}
}

const addSample = () => {
if (fileInputRef.current) {
fileInputRef.current.click()
}
}

return (
<div className={css('sample-browser')}>
<input
type="file"
className={css('file-input')}
ref={fileInputRef}
onChange={onFileChange}
/>
<div className={css('samples')}>
{browserState.samples.map((sample) => (
<button
onClick={() => onSelect(sample.id)}
className={css({ selected: selected === sample.id })}
>
{sample.name}
</button>
))}
</div>
<div className={css('controls')}>
<button onClick={addSample}>add</button>
</div>
</div>
)
}

export default SampleBrowser
14 changes: 8 additions & 6 deletions client/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ let engine: Engine | null = null
const eventSubscriptions: Record<number, (event: ModuleEvent<Module>) => void> =
{}

export const initializeEngine = async (
options: InitOptions = {
spawnAudioWorklet: true,
numWorklets: Math.max(4, navigator.hardwareConcurrency) - 1,
}
) => {
const DEFAULT_OPTIONS: InitOptions = {
spawnAudioWorklet: true,
numWorklets: Math.max(4, navigator.hardwareConcurrency) - 1,
}

export const initializeEngine = async (opts: Partial<InitOptions> = {}) => {
const options: InitOptions = Object.assign({}, DEFAULT_OPTIONS, opts)

assert(
options.numWorklets > 0,
'initializeEngine: numWorklets must be greater than zero'
Expand Down
1 change: 0 additions & 1 deletion client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as auth from './auth'
import './reset.css'

import App from './components/app/App'
import { joinRoom, leaveRoom } from './rooms'

api.getIdentity().then((res) => {
if (!res.error) {
Expand Down
1 change: 1 addition & 0 deletions client/src/reset.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
--on-background: #ffffff;

--box-shadow: 0 2px 0 #666, 0 2px 8px rgba(0, 0, 0, 0.4);
--inset-box-shadow: inset 0 2px 0 #666, inset 0 2px 8px rgba(0, 0, 0, 0.4);
}

* {
Expand Down
18 changes: 17 additions & 1 deletion client/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,23 @@ document.documentElement.addEventListener('mousemove', (evt) => {
cursor.y = evt.pageY
})

const hasScrollbar = (elem: HTMLElement) => {
const style = window.getComputedStyle(elem)
return style.overflowY === 'scroll'
}

const elementIsWithinScrollableElement = (elem: HTMLElement): boolean => {
if (hasScrollbar(elem)) return true
if (elem.parentElement)
return elementIsWithinScrollableElement(elem.parentElement)
return false
}

document.addEventListener('wheel', (evt) => {
if (elementIsWithinScrollableElement(evt.target as HTMLElement)) {
return
}

state.viewOffset.x -= evt.deltaX
state.viewOffset.y -= evt.deltaY
})
Expand Down Expand Up @@ -242,7 +258,7 @@ export const getCableConnectionCandidate = (): Socket | null => {
if (!candidateSocket) return null

const inputSocketIsOccupied =
candidateSocket.socket.type === 'input' &&
candidateSocket.socket.type !== 'output' &&
patch.cables.some((cable) => isSameSocket(cable.to, candidateSocket.socket))

if (inputSocketIsOccupied) return null
Expand Down
8 changes: 8 additions & 0 deletions server/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,11 @@ export const getSampleMetadataById = async (id: string) => {
)
return sample ?? null
}

export const getSamplesByUser = async (userId: string) => {
const samples = await query<SampleMetadata>(sql`
SELECT * FROM samples WHERE ownerId = ${userId}
`)

return samples
}
18 changes: 18 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,24 @@ const server = http.createServer(
name: metadata.name,
})
})
.get('/api/samples', async (req, res) => {
const { authorization } = req

// TODO: Add fetching for factory samples
const factorySamples: db.SampleMetadata[] = []

if (authorization) {
const userSamples = await db.getSamplesByUser(authorization.id)

res.json([...factorySamples, ...userSamples])
res.end()
return
}

res.json(factorySamples)
res.end()
return
})
)

rooms(server)
Expand Down

0 comments on commit 96b2621

Please sign in to comment.