Skip to content

Commit cabbef8

Browse files
authored
Merge pull request #22 from ryohey/fix-0tick
Fix 0-tick problem
2 parents 352d3f6 + f4e807e commit cabbef8

File tree

8 files changed

+184
-81
lines changed

8 files changed

+184
-81
lines changed

lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ryohey/wavelet",
3-
"version": "0.7.4",
3+
"version": "0.7.5",
44
"description": "A wavetable synthesizer that never stops the UI thread created by AudioWorklet.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

lib/src/SynthEvent.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import { AnyChannelEvent } from "midifile-ts"
22
import { AmplitudeEnvelopeParameter } from "./processor/AmplitudeEnvelope"
33
import { DistributiveOmit } from "./types"
44

5-
export interface SampleLoop {
6-
start: number
7-
end: number
8-
}
5+
export type SampleLoop =
6+
| {
7+
type: "no_loop"
8+
}
9+
| {
10+
type: "loop_continuous" | "loop_sustain"
11+
start: number
12+
end: number
13+
}
914

1015
export interface SampleParameter {
1116
name: string
1217
sampleID: number
1318
pitch: number
14-
loop: SampleLoop | null
19+
loop: SampleLoop
1520
sampleStart: number
1621
sampleEnd: number
1722
sampleRate: number
Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
export interface AmplitudeEnvelopeParameter {
22
attackTime: number
3+
holdTime: number
34
decayTime: number
45
sustainLevel: number
56
releaseTime: number
67
}
78

89
enum EnvelopePhase {
910
attack,
11+
hold,
1012
decay,
1113
sustain,
1214
release,
@@ -18,7 +20,10 @@ const forceStopReleaseTime = 0.1
1820

1921
export class AmplitudeEnvelope {
2022
private readonly parameter: AmplitudeEnvelopeParameter
21-
private phase = EnvelopePhase.attack
23+
private _phase = EnvelopePhase.stopped
24+
private isNoteOff = false
25+
private phaseTime = 0
26+
private decayLevel = 0 // amplitude level at the end of decay phase
2227
private lastAmplitude = 0
2328
private readonly sampleRate: number
2429

@@ -27,69 +32,99 @@ export class AmplitudeEnvelope {
2732
this.sampleRate = sampleRate
2833
}
2934

35+
private get phase() {
36+
return this._phase
37+
}
38+
39+
private set phase(phase: EnvelopePhase) {
40+
if (this._phase === phase) {
41+
return
42+
}
43+
this._phase = phase
44+
this.phaseTime = 0
45+
}
46+
3047
noteOn() {
3148
this.phase = EnvelopePhase.attack
49+
this.isNoteOff = false
50+
this.phaseTime = 0
51+
this.decayLevel = this.parameter.sustainLevel
3252
}
3353

3454
noteOff() {
35-
if (this.phase !== EnvelopePhase.forceStop) {
36-
this.phase = EnvelopePhase.release
37-
}
55+
this.isNoteOff = true
3856
}
3957

4058
// Rapidly decrease the volume. This method ignores release time parameter
4159
forceStop() {
4260
this.phase = EnvelopePhase.forceStop
4361
}
4462

45-
getAmplitude(bufferSize: number): number {
46-
const { attackTime, decayTime, sustainLevel, releaseTime } = this.parameter
63+
calculateAmplitude(bufferSize: number): number {
64+
const { attackTime, holdTime, decayTime, sustainLevel, releaseTime } =
65+
this.parameter
4766
const { sampleRate } = this
4867

68+
if (
69+
this.isNoteOff &&
70+
(this.phase === EnvelopePhase.decay ||
71+
this.phase === EnvelopePhase.sustain)
72+
) {
73+
this.phase = EnvelopePhase.release
74+
this.decayLevel = this.lastAmplitude
75+
}
76+
4977
// Attack
5078
switch (this.phase) {
5179
case EnvelopePhase.attack: {
5280
const amplificationPerFrame =
5381
(1 / (attackTime * sampleRate)) * bufferSize
5482
const value = this.lastAmplitude + amplificationPerFrame
5583
if (value >= 1) {
56-
this.phase = EnvelopePhase.decay
57-
this.lastAmplitude = 1
84+
this.phase = EnvelopePhase.hold
5885
return 1
5986
}
60-
this.lastAmplitude = value
6187
return value
6288
}
89+
case EnvelopePhase.hold: {
90+
if (this.phaseTime >= holdTime) {
91+
this.phase = EnvelopePhase.decay
92+
}
93+
return this.lastAmplitude
94+
}
6395
case EnvelopePhase.decay: {
64-
const attenuationPerFrame = (1 / (decayTime * sampleRate)) * bufferSize
65-
const value = this.lastAmplitude - attenuationPerFrame
66-
if (value <= sustainLevel) {
96+
const attenuationDecibel = linearToDecibel(sustainLevel / 1)
97+
const value = logAttenuation(
98+
1.0,
99+
attenuationDecibel,
100+
decayTime,
101+
this.phaseTime
102+
)
103+
if (this.phaseTime > decayTime) {
67104
if (sustainLevel <= 0) {
68105
this.phase = EnvelopePhase.stopped
69-
this.lastAmplitude = 0
70106
return 0
71107
} else {
72108
this.phase = EnvelopePhase.sustain
73-
this.lastAmplitude = sustainLevel
74109
return sustainLevel
75110
}
76111
}
77-
this.lastAmplitude = value
78112
return value
79113
}
80114
case EnvelopePhase.sustain: {
81115
return sustainLevel
82116
}
83117
case EnvelopePhase.release: {
84-
const attenuationPerFrame =
85-
(1 / (releaseTime * sampleRate)) * bufferSize
86-
const value = this.lastAmplitude - attenuationPerFrame
87-
if (value <= 0) {
118+
const value = logAttenuation(
119+
this.decayLevel,
120+
-100, // -100dB means almost silence
121+
releaseTime,
122+
this.phaseTime
123+
)
124+
if (this.phaseTime > releaseTime || value <= 0) {
88125
this.phase = EnvelopePhase.stopped
89-
this.lastAmplitude = 0
90126
return 0
91127
}
92-
this.lastAmplitude = value
93128
return value
94129
}
95130
case EnvelopePhase.forceStop: {
@@ -98,10 +133,8 @@ export class AmplitudeEnvelope {
98133
const value = this.lastAmplitude - attenuationPerFrame
99134
if (value <= 0) {
100135
this.phase = EnvelopePhase.stopped
101-
this.lastAmplitude = 0
102136
return 0
103137
}
104-
this.lastAmplitude = value
105138
return value
106139
}
107140
case EnvelopePhase.stopped: {
@@ -110,7 +143,32 @@ export class AmplitudeEnvelope {
110143
}
111144
}
112145

146+
getAmplitude(bufferSize: number): number {
147+
const value = this.calculateAmplitude(bufferSize)
148+
this.lastAmplitude = value
149+
this.phaseTime += bufferSize / sampleRate
150+
return value
151+
}
152+
113153
get isPlaying() {
114154
return this.phase !== EnvelopePhase.stopped
115155
}
116156
}
157+
158+
// An exponential decay function. It attenuates the value of decibel over the duration time.
159+
function logAttenuation(
160+
fromLevel: number,
161+
attenuationDecibel: number,
162+
duration: number,
163+
time: number
164+
): number {
165+
return fromLevel * decibelToLinear((attenuationDecibel / duration) * time)
166+
}
167+
168+
function linearToDecibel(value: number): number {
169+
return 20 * Math.log10(value)
170+
}
171+
172+
function decibelToLinear(value: number): number {
173+
return Math.pow(10, value / 20)
174+
}

lib/src/processor/SynthProcessorCore.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export class SynthProcessorCore {
9595
for (const sample of samples) {
9696
const oscillator = new WavetableOscillator(sample, this.sampleRate)
9797

98-
const volume = velocity / 0x80
98+
const volume = velocity / 127
9999
oscillator.noteOn(pitch, volume)
100100

101101
if (state.oscillators[pitch] === undefined) {
@@ -151,12 +151,12 @@ export class SynthProcessorCore {
151151

152152
setMainVolume(channel: number, value: number) {
153153
const state = this.getChannelState(channel)
154-
state.volume = value / 0x80
154+
state.volume = value / 127
155155
}
156156

157157
expression(channel: number, value: number) {
158158
const state = this.getChannelState(channel)
159-
state.expression = value / 0x80
159+
state.expression = value / 127
160160
}
161161

162162
allSoundsOff(channel: number) {
@@ -210,7 +210,7 @@ export class SynthProcessorCore {
210210

211211
modulation(channel: number, value: number) {
212212
const state = this.getChannelState(channel)
213-
state.modulation = value / 0x80
213+
state.modulation = value / 127
214214
}
215215

216216
resetChannel(channel: number) {
@@ -248,12 +248,5 @@ export class SynthProcessorCore {
248248
})
249249
}
250250
}
251-
252-
// master volume
253-
const masterVolume = 0.3
254-
for (let i = 0; i < outputs[0].length; ++i) {
255-
outputs[0][i] *= masterVolume
256-
outputs[1][i] *= masterVolume
257-
}
258251
}
259252
}

lib/src/processor/WavetableOscillator.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class WavetableOscillator {
6464

6565
const speed =
6666
(this.baseSpeed * this.speed * this.sample.sampleRate) / this.sampleRate
67-
const volume = this.velocity * this.volume * this.sample.volume
67+
const volume = (this.velocity * this.volume) ** 2 * this.sample.volume
6868

6969
// zero to pi/2
7070
const panTheta =
@@ -86,7 +86,11 @@ export class WavetableOscillator {
8686
const advancedIndex = this.sampleIndex + modulatedSpeed
8787
let loopIndex: number | null = null
8888

89-
if (this.sample.loop !== null && advancedIndex >= this.sample.loop.end) {
89+
if (
90+
(this.sample.loop.type === "loop_continuous" ||
91+
(this.sample.loop.type === "loop_sustain" && !this._isNoteOff)) &&
92+
advancedIndex >= this.sample.loop.end
93+
) {
9094
loopIndex =
9195
this.sample.loop.start + (advancedIndex - Math.floor(advancedIndex))
9296
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
createGeneraterObject,
3+
getInstrumentGenerators,
4+
ParseResult,
5+
} from "@ryohey/sf2parser"
6+
7+
export function getInstrumentZones(parsed: ParseResult, instrumentID: number) {
8+
const instrumentGenerators = getInstrumentGenerators(parsed, instrumentID)
9+
const zones = instrumentGenerators.map(createGeneraterObject)
10+
11+
// If the first zone does not have sampleID, it is a global instrument zone.
12+
let globalZone: any | undefined
13+
const firstInstrumentZone = zones[0]
14+
if (firstInstrumentZone.sampleID === undefined) {
15+
globalZone = zones[0]
16+
}
17+
18+
return {
19+
zones: zones.filter((zone) => zone.sampleID !== undefined),
20+
globalZone,
21+
}
22+
}

lib/src/soundfont/getPresetZones.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { GeneratorParams } from "@ryohey/sf2parser"
22
import { GeneratorList } from "@ryohey/sf2parser/bin/Structs"
33

44
export function getPresetZones(generators: GeneratorList[]) {
5-
let instrumentsGlobal: Partial<GeneratorParams> = {}
6-
const instruments: (Partial<GeneratorParams> & { instrument: number })[] = []
5+
let globalZone: Partial<GeneratorParams> = {}
6+
const zones: (Partial<GeneratorParams> & { instrument: number })[] = []
77
let params: Partial<GeneratorParams> = {}
88
let zoneCount = 0
99

@@ -16,21 +16,21 @@ export function getPresetZones(generators: GeneratorList[]) {
1616

1717
// keyRange or velRange must be the first of zone
1818
if (type === "keyRange" || type === "velRange") {
19-
if (zoneCount === 1 && instruments.length === 0) {
19+
if (zoneCount === 1 && zones.length === 0) {
2020
// treat previous zone as global zone if it is the first zone and not ended with instrument
21-
instrumentsGlobal = params
21+
globalZone = params
2222
}
2323
params = {}
2424
zoneCount++
2525
}
2626

2727
// instrument must be the last of zone
2828
if (type === "instrument") {
29-
instruments.push({ ...params, instrument: gen.value as number })
29+
zones.push({ ...params, instrument: gen.value as number })
3030
}
3131

3232
params[type] = gen.value
3333
}
3434

35-
return { instruments, instrumentsGlobal }
35+
return { zones, globalZone }
3636
}

0 commit comments

Comments
 (0)