Skip to content

Commit

Permalink
Merge pull request #16 from ryohey/faster-load
Browse files Browse the repository at this point in the history
Faster load
  • Loading branch information
ryohey authored Sep 21, 2023
2 parents c92a2c8 + d85cb96 commit c981c25
Show file tree
Hide file tree
Showing 16 changed files with 392 additions and 210 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node.js CI

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm run build --if-present
- run: npm test
23 changes: 23 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages

name: Node.js Package

on:
release:
types: [created]

jobs:
publish-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm run build
- run: npm run publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
59 changes: 33 additions & 26 deletions example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
AudioData,
audioDataToAudioBuffer,
CancelMessage,
getSamplesFromSoundFont,
getSampleEventsFromSoundFont,
OutMessage,
renderAudio,
StartMessage,
Expand All @@ -13,7 +13,8 @@ import { encode } from "wav-encoder"
import { MIDIPlayer } from "./MIDIPlayer"
import { midiToSynthEvents } from "./midiToSynthEvents"

const soundFontUrl = "soundfonts/A320U.sf2"
// const soundFontUrl = "soundfonts/A320U.sf2"
const soundFontUrl = "soundfonts/SGM-V2.01.sf2"

const Sleep = (time: number) =>
new Promise<void>((resolve) => setTimeout(resolve, time))
Expand Down Expand Up @@ -44,20 +45,24 @@ const main = async () => {
}

const loadSoundFont = async () => {
let startDate = Date.now()
console.log("Loading soundfont...")
soundFontData = await (await fetch(soundFontUrl)).arrayBuffer()
console.log(
`Soundfont loaded. (${Date.now() - startDate}ms, ${
soundFontData.byteLength
} bytes)`
)

startDate = Date.now()
console.log("Parsing soundfont...")
const parsed = getSamplesFromSoundFont(
new Uint8Array(soundFontData),
context
const sampleEvents = getSampleEventsFromSoundFont(
new Uint8Array(soundFontData)
)
console.log("Soundfont parsed.")
console.log(`Soundfont parsed. (${Date.now() - startDate}ms)`)

for (const sample of parsed) {
postSynthMessage(
sample,
[sample.sample.buffer] // transfer instead of copy
)
for (const event of sampleEvents) {
postSynthMessage(event.event, event.transfer)
}
}

Expand Down Expand Up @@ -157,9 +162,8 @@ const main = async () => {
if (soundFontData === null) {
return
}
const samples = getSamplesFromSoundFont(
new Uint8Array(soundFontData),
context
const sampleEvents = getSampleEventsFromSoundFont(
new Uint8Array(soundFontData)
)
const sampleRate = 44100
const events = midiToSynthEvents(midi, sampleRate)
Expand All @@ -175,14 +179,18 @@ const main = async () => {
cancelButton.onclick = () => (cancel = true)
exportPanel.appendChild(cancelButton)

const result = await renderAudio(samples, events, {
sampleRate,
bufferSize: 256,
cancel: () => cancel,
waitForEventLoop: waitForAnimationFrame,
onProgress: (numFrames, totalFrames) =>
(progress.value = numFrames / totalFrames),
})
const result = await renderAudio(
sampleEvents.map((event) => event.event),
events,
{
sampleRate,
bufferSize: 256,
cancel: () => cancel,
waitForEventLoop: waitForAnimationFrame,
onProgress: (numFrames, totalFrames) =>
(progress.value = numFrames / totalFrames),
}
)

cancelButton.remove()

Expand All @@ -195,15 +203,14 @@ const main = async () => {
return
}
const worker = new Worker("/js/rendererWorker.js")
const samples = getSamplesFromSoundFont(
new Uint8Array(soundFontData),
context
const sampleEvents = getSampleEventsFromSoundFont(
new Uint8Array(soundFontData)
)
const sampleRate = 44100
const events = midiToSynthEvents(midi, sampleRate)
const message: StartMessage = {
type: "start",
samples,
samples: sampleEvents.map((e) => e.event),
events,
sampleRate,
bufferSize: 128,
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions lib/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@ryohey/wavelet",
"version": "0.6.3",
"version": "0.7.0",
"description": "A wavetable synthesizer that never stops the UI thread created by AudioWorklet.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"start": "rollup --config --watch",
"build": "rollup --config",
"test": "jest"
"test": "jest --roots ./src"
},
"author": "ryohey",
"license": "MIT",
Expand Down
22 changes: 16 additions & 6 deletions lib/src/SynthEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export interface SampleLoop {
end: number
}

export interface SampleData<BufferType> {
export interface SampleParameter {
name: string
buffer: BufferType
sampleID: number
pitch: number
loop: SampleLoop | null
sampleStart: number
Expand All @@ -25,15 +25,25 @@ export interface SampleData<BufferType> {
volume: number // 0 to 1
}

export interface LoadSampleEvent {
type: "loadSample"
sample: SampleData<ArrayBuffer>
export interface SampleRange {
bank: number
instrument: number // GM Patch Number
keyRange: [number, number]
velRange: [number, number]
}

export interface LoadSampleEvent {
type: "loadSample"
data: ArrayBuffer
sampleID: number
}

export interface SampleParameterEvent {
type: "sampleParameter"
parameter: SampleParameter
range: SampleRange
}

export type MIDIEventBody = DistributiveOmit<AnyChannelEvent, "deltaTime">

export type MIDIEvent = {
Expand All @@ -44,7 +54,7 @@ export type MIDIEvent = {
delayTime: number
}

export type ImmediateEvent = LoadSampleEvent
export type ImmediateEvent = LoadSampleEvent | SampleParameterEvent
export type SynthEvent = ImmediateEvent | MIDIEvent

export const DrumInstrumentNumber = 128
66 changes: 44 additions & 22 deletions lib/src/processor/SampleTable.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
import { SampleData } from "../SynthEvent"
import { SampleParameter, SampleRange } from "../SynthEvent"

type Sample = SampleData<Float32Array>

export type SampleTableItem = Sample & {
export type SampleTableItem = SampleParameter & {
velRange: [number, number]
}

export type Sample = SampleParameter & {
buffer: ArrayBuffer
}

export class SampleTable {
private samples: {
[sampleID: number]: Float32Array
} = {}

private sampleParameters: {
[bank: number]: {
[instrument: number]: { [pitch: number]: SampleTableItem[] }
}
} = {}

addSample(
sample: Sample,
bank: number,
instrument: number,
keyRange: [number, number],
velRange: [number, number]
) {
addSample(data: Float32Array, sampleID: number) {
this.samples[sampleID] = data
}

addSampleParameter(parameter: SampleParameter, range: SampleRange) {
const { bank, instrument, keyRange, velRange } = range
for (let i = keyRange[0]; i <= keyRange[1]; i++) {
if (this.samples[bank] === undefined) {
this.samples[bank] = {}
if (this.sampleParameters[bank] === undefined) {
this.sampleParameters[bank] = {}
}
if (this.samples[bank][instrument] === undefined) {
this.samples[bank][instrument] = {}
if (this.sampleParameters[bank][instrument] === undefined) {
this.sampleParameters[bank][instrument] = {}
}
if (this.samples[bank][instrument][i] === undefined) {
this.samples[bank][instrument][i] = []
if (this.sampleParameters[bank][instrument][i] === undefined) {
this.sampleParameters[bank][instrument][i] = []
}
this.samples[bank][instrument][i].push({ ...sample, velRange })
this.sampleParameters[bank][instrument][i].push({
...parameter,
velRange,
})
}
}

Expand All @@ -40,11 +48,25 @@ export class SampleTable {
pitch: number,
velocity: number
): Sample[] {
const samples = this.samples?.[bank]?.[instrument]?.[pitch]
return (
samples?.filter(
const parameters =
this.sampleParameters?.[bank]?.[instrument]?.[pitch]?.filter(
(s) => velocity >= s.velRange[0] && velocity <= s.velRange[1]
) ?? []
)

const samples: Sample[] = []

for (const parameter of parameters) {
const buffer = this.samples[parameter.sampleID]
if (buffer === undefined) {
console.warn(`sample not found: ${parameter.sampleID}`)
continue
}
samples.push({
...parameter,
buffer,
})
}

return samples
}
}
Loading

0 comments on commit c981c25

Please sign in to comment.