diff --git a/src/spessasynth_lib/sequencer/sequencer.js b/src/spessasynth_lib/sequencer/sequencer.js index 568e7b67..8e81bb3b 100644 --- a/src/spessasynth_lib/sequencer/sequencer.js +++ b/src/spessasynth_lib/sequencer/sequencer.js @@ -10,7 +10,6 @@ import {readBytesAsUintBigEndian} from "../utils/byte_functions.js"; */ const MIN_NOTE_TIME = 0.02; -const MAX_NOTEONS_PER_S = 200; // an array with preset default values const defaultControllerArray = new Int16Array(127); @@ -20,6 +19,7 @@ defaultControllerArray[midiControllers.expressionController] = 127; defaultControllerArray[midiControllers.pan] = 64; defaultControllerArray[midiControllers.releaseTime] = 64; defaultControllerArray[midiControllers.brightness] = 64; +defaultControllerArray[midiControllers.effects1Depth] = 40; export class Sequencer { /** @@ -89,7 +89,6 @@ export class Sequencer { */ this.midiPortChannelOffsets = {}; - this.noteOnsPerS = 0; /** * @type {Object} @@ -177,8 +176,12 @@ export class Sequencer { this.stop(); this.playingNotes = []; this.pausedTime = undefined; - this._playTo(time); + const isNotFinished = this._playTo(time); this.absoluteStartTime = this.now - time / this.playbackRate; + if(!isNotFinished) + { + return; + } this.play(); if(this.renderer) { @@ -196,8 +199,12 @@ export class Sequencer { } this.playingNotes = []; this.pausedTime = undefined; - this._playTo(0, ticks); + const isNotFinished = this._playTo(0, ticks); this.absoluteStartTime = this.now - this.playedTime / this.playbackRate; + if(!isNotFinished) + { + return; + } this.play(); if(this.renderer) { @@ -256,8 +263,7 @@ export class Sequencer { * @type {number} */ this.duration = this.ticksToSeconds(this.midiData.lastVoiceEventTick); - - console.info(`%cTotal song time: ${formatTime(Math.round(this.duration)).time}`, consoleColors.recognized); + console.info(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized); this.midiPortChannelOffset = 0; this.midiPortChannelOffsets = {}; @@ -269,6 +275,12 @@ export class Sequencer { } this.synth.resetControllers(); + if(this.duration <= 1) + { + console.info(`%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`, + consoleColors.warn); + this.loop = false; + } this.play(true); } @@ -459,6 +471,7 @@ export class Sequencer { * @private * @param time {number} in seconds * @param ticks {number} optional MIDI ticks, when given is used instead of time + * @returns {boolean} true if the midi file is not finished */ _playTo(time, ticks = undefined) { @@ -580,6 +593,11 @@ export class Sequencer { // find next event trackIndex = this._findFirstEventIndex(); let nextEvent = this.tracks[trackIndex][this.eventIndex[trackIndex]]; + if(nextEvent === undefined) + { + this.stop(); + return false; + } this.playedTime += this.oneTickToSeconds * (nextEvent.ticks - event.ticks); } @@ -620,7 +638,7 @@ export class Sequencer { } }) } - window.abba = savedControllers; + return true; } /** @@ -631,7 +649,8 @@ export class Sequencer { { // reset the time if necesarry - if(resetTime) { + if(resetTime) + { this.currentTime = 0; return; } @@ -658,8 +677,11 @@ export class Sequencer { this.synth.noteOn(n.channel, n.midiNote, n.velocity); }); + if(this.playbackInterval) + { + clearInterval(this.playbackInterval); + } this.playbackInterval = setInterval(this._processTick.bind(this)); - setInterval( () =>this.noteOnsPerS = 0, 100); } /** @@ -680,7 +702,7 @@ export class Sequencer { // find next event trackIndex = this._findFirstEventIndex(); - if(this.tracks[trackIndex].length <= this.eventIndex[trackIndex]) + if(this.tracks[trackIndex].length < this.eventIndex[trackIndex]) { // song has ended if(this.loop) @@ -699,13 +721,13 @@ export class Sequencer { this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks); // loop - if((this.midiData.loop.end <= event.ticks) && this.loop) + if((this.midiData.loop.end < event.ticks) && this.loop) { this.setTimeTicks(this.midiData.loop.start); return; } // if song has ended - else if(current > this.duration + 0.1) + else if(current >= this.duration) { if(this.loop) { @@ -756,11 +778,6 @@ export class Sequencer { case messageTypes.noteOn: const velocity = event.messageData[1]; if(velocity > 0) { - if(this.synth.highPerformanceMode && (this.noteOnsPerS > MAX_NOTEONS_PER_S && velocity < 40) || this.noteOnsPerS > MAX_NOTEONS_PER_S * 2) - { - return; - } - this.noteOnsPerS++; this.synth.noteOn(statusByteData.channel, event.messageData[0], velocity); this.playingNotes.push({ midiNote: event.messageData[0], diff --git a/src/spessasynth_lib/soundfont/chunk/modulators.js b/src/spessasynth_lib/soundfont/chunk/modulators.js index f98473f0..217ddeed 100644 --- a/src/spessasynth_lib/soundfont/chunk/modulators.js +++ b/src/spessasynth_lib/soundfont/chunk/modulators.js @@ -15,8 +15,6 @@ export const modulatorSources = { channelPressure: 13, pitchWheel: 14, pitchWheelRange: 16, - channelTuning: 17, - channelTranspose: 18, link: 127 } diff --git a/src/spessasynth_lib/synthetizer/native_system/midi_channel.js b/src/spessasynth_lib/synthetizer/native_system/midi_channel.js index 8ef2a840..a3f46358 100644 --- a/src/spessasynth_lib/synthetizer/native_system/midi_channel.js +++ b/src/spessasynth_lib/synthetizer/native_system/midi_channel.js @@ -69,7 +69,7 @@ export class MidiChannel { }); this.reverb = new GainNode(this.ctx, { - gain: 0 + gain: 40 / 127 }); this.reverb.connect(targetReverbNode); @@ -652,6 +652,12 @@ export class MidiChannel { } + if(!this.lockedControllers[midiControllers.effects1Depth]) + { + this.reverb.gain.value = 40 / 127; + } + + if(!this.lockedControllers[midiControllers.pan]) { this.panner.pan.value = 0; diff --git a/src/spessasynth_lib/synthetizer/synthetizer.js b/src/spessasynth_lib/synthetizer/synthetizer.js index 08ade238..010f73f9 100644 --- a/src/spessasynth_lib/synthetizer/synthetizer.js +++ b/src/spessasynth_lib/synthetizer/synthetizer.js @@ -17,8 +17,7 @@ export const VOICE_CAP = 450; export const DEFAULT_GAIN = 1; export const DEFAULT_PERCUSSION = 9; export const DEFAULT_CHANNEL_COUNT = 16; -export const REVERB_TIME_S = 2; -export const DEFAULT_SYNTH_MODE = "gm2"; +export const DEFAULT_SYNTH_MODE = "gs"; export const DEFAULT_SYNTHESIS_MODE = "worklet"; export class Synthetizer { @@ -237,24 +236,38 @@ export class Synthetizer { break; case midiControllers.bankSelect: - if(this.system === "gm") - { - // gm ignores bank select - console.info(`%cIgnoring the Bank Select (${controllerValue}), as the synth is in GM mode.`, consoleColors.info); - return; - } let bankNr = controllerValue; const channelObject = this.synthesisSystem.midiChannels[channel]; - - // for xg, if msb is 127, then it's drums - if(bankNr === 127 && this.system === "xg") + switch (this.system) { - channelObject.percussionChannel = true; - this.eventHandler.callEvent("drumchange",{ - channel: channel, - isDrumChannel: true - }); + case "gm": + // gm ignores bank select + console.info(`%cIgnoring the Bank Select (${controllerValue}), as the synth is in GM mode.`, consoleColors.info); + return; + + case "xg": + // for xg, if msb is 127, then it's drums + if (bankNr === 127) + { + channelObject.percussionChannel = true; + this.eventHandler.callEvent("drumchange", { + channel: channel, + isDrumChannel: true + }); + } + break; + + case "gm2": + if(bankNr === 120) + { + channelObject.percussionChannel = true; + this.eventHandler.callEvent("drumchange", { + channel: channel, + isDrumChannel: true + }); + } } + if(channelObject.percussionChannel) { // 128 for percussion channel @@ -271,9 +284,19 @@ export class Synthetizer { case midiControllers.lsbForControl0BankSelect: if(this.system === 'xg') + { + if(this.synthesisSystem.midiChannels[channel].bank === 0) + { + this.synthesisSystem.midiChannels[channel].bank = controllerValue; + } + } + else + if(this.system === "gm2") { this.synthesisSystem.midiChannels[channel].bank = controllerValue; } + + break; @@ -341,11 +364,11 @@ export class Synthetizer { restoreControllerValueEvent(midiControllers.expressionController, 127); restoreControllerValueEvent(midiControllers.modulationWheel, 0); restoreControllerValueEvent(midiControllers.effects3Depth, 0); - restoreControllerValueEvent(midiControllers.effects1Depth, 0); + restoreControllerValueEvent(midiControllers.effects1Depth, 40); this.eventHandler.callEvent("pitchwheel", {channel: channelNumber, MSB: 64, LSB: 0}) } - this.system = "gm2"; + this.system = DEFAULT_SYNTH_MODE; this.volumeController.gain.value = DEFAULT_GAIN; this.panController.pan.value = 0; } @@ -533,15 +556,16 @@ export class Synthetizer { { // this is a GS sysex // messageData[5] and [6] is the system parameter, messageData[7] is the value + const messageValue = messageData[7]; if(messageData[6] === 0x7F) { // GS mode set - if(messageData[7] === 0x00) { + if(messageValue === 0x00) { // this is a GS reset console.info("%cGS system on", consoleColors.info); this.system = "gs"; } - else if(messageData[7] === 0x7F) + else if(messageValue === 0x7F) { // GS mode off console.info("%cGS system off, switching to GM2", consoleColors.info); @@ -553,43 +577,63 @@ export class Synthetizer { if(messageData[4] === 0x40) { // this is a system parameter - if(messageData[6] === 0x15) + if((messageData[5] & 0x10) > 0) { - // this is the Use for Drum Part sysex (multiple drums) + // this is an individual part (channel) parameter // determine the channel 0 means channel 10 (default), 1 means 1 etc. const channel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][messageData[5] & 0x0F]; // for example 1A means A = 11, which corresponds to channel 12 (counting from 1) - - this.setDrums(channel, messageData[7] > 0 && messageData[5] >> 4); // if set to other than 0, is a drum channel - console.info( - `%cChannel %c${channel}%c ${this.synthesisSystem.midiChannels[channel].percussionChannel ? - "is now a drum channel" - : - "now isn't a drum channel" - }%c via: %c${arrayToHexString(messageData)}`, - consoleColors.info, - consoleColors.value, - consoleColors.recognized, - consoleColors.info, - consoleColors.value); - return; + switch (messageData[6]) + { + default: + break; + + case 0x15: + // this is the Use for Drum Part sysex (multiple drums) + this.setDrums(channel, messageValue > 0 && messageData[5] >> 4); // if set to other than 0, is a drum channel + console.info( + `%cChannel %c${channel}%c ${this.synthesisSystem.midiChannels[channel].percussionChannel ? + "is now a drum channel" + : + "now isn't a drum channel" + }%c via: %c${arrayToHexString(messageData)}`, + consoleColors.info, + consoleColors.value, + consoleColors.recognized, + consoleColors.info, + consoleColors.value); + return; + + case 0x16: + // this is the pitch key shift sysex + const keyShift = messageValue - 64; + console.info(`%cChannel %c${channel}%c pitch shift. Semitones %c${keyShift}%c, with %c${arrayToHexString(messageData)}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value); + return; + } } else + // this is a global system parameter if(messageData[5] === 0x00 && messageData[6] === 0x06) { // roland master pan - console.info(`%cRoland GS Master Pan set to: %c${messageData[7]}%c with: %c${arrayToHexString(messageData)}`, + console.info(`%cRoland GS Master Pan set to: %c${messageValue}%c with: %c${arrayToHexString(messageData)}`, consoleColors.info, consoleColors.value, consoleColors.info, consoleColors.value); - this.panController.pan.value = (messageData[7] - 64) / 64; + this.panController.pan.value = (messageValue - 64) / 64; return; } else if(messageData[5] === 0x00 && messageData[6] === 0x05) { // roland master key shift (transpose) - const transpose = messageData[7] - 64; + const transpose = messageValue - 64; console.info(`%cRoland GS Master Key-Shift set to: %c${transpose}%c with: %c${arrayToHexString(messageData)}`, consoleColors.info, consoleColors.value, @@ -602,34 +646,22 @@ export class Synthetizer { if(messageData[5] === 0x00 && messageData[6] === 0x04) { // roland GS master volume - console.info(`%cRoland GS Master Volume set to: %c${messageData[7]}%c with: %c${arrayToHexString(messageData)}`, + console.info(`%cRoland GS Master Volume set to: %c${messageValue}%c with: %c${arrayToHexString(messageData)}`, consoleColors.info, consoleColors.value, consoleColors.info, consoleColors.value); - this.setMainVolume(messageData[7] / 127); - } - else - { - // this is some other GS sysex... - console.info(`%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`, - consoleColors.warn, - consoleColors.recognized, - consoleColors.warn, - consoleColors.unrecognized); + this.setMainVolume(messageValue / 127); return; } } - else - { - // this is some other GS sysex... - console.info(`%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`, - consoleColors.warn, - consoleColors.recognized, - consoleColors.warn, - consoleColors.unrecognized); - return; - } + // this is some other GS sysex... + console.info(`%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.recognized, + consoleColors.warn, + consoleColors.unrecognized); + return; } else if(messageData[2] === 0x16 && messageData[3] === 0x12 && messageData[4] === 0x10) @@ -651,7 +683,6 @@ export class Synthetizer { consoleColors.unrecognized); return; } - break; // yamaha case 0x43: diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js index 23e9fd64..e32b97c8 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js @@ -1,8 +1,7 @@ -import { NON_CC_INDEX_OFFSET, WORKLET_PROCESSOR_NAME, workletMessageType } from './worklet_system.js' +import { WORKLET_PROCESSOR_NAME } from './worklet_system.js' import { midiControllers } from '../../midi_parser/midi_message.js'; import { generatorTypes } from '../../soundfont/chunk/generators.js'; import { getOscillatorData } from './worklet_utilities/wavetable_oscillator.js' -import { modulatorSources } from '../../soundfont/chunk/modulators.js'; import { computeModulators } from './worklet_utilities/worklet_modulator.js' import { absCentsToHz, @@ -15,40 +14,20 @@ import { applyVolumeEnvelope } from './worklet_utilities/volume_envelope.js' import { applyLowpassFilter } from './worklet_utilities/lowpass_filter.js' import { getModEnvValue } from './worklet_utilities/modulation_envelope.js' import { VOICE_CAP } from '../synthetizer.js' +import { + CONTROLLER_TABLE_SIZE, + CUSTOM_CONTROLLER_TABLE_SIZE, + customControllers, + resetArray, +} from './worklet_utilities/worklet_processor_channel.js' +import { workletMessageType } from './worklet_utilities/worklet_message.js' /** * worklet_processor.js * purpose: manages the synthesizer from the AudioWorkletGlobalScope and renders the audio data */ -const CONTROLLER_TABLE_SIZE = 147; const MIN_NOTE_LENGTH = 0.07; // if the note is released faster than that, it forced to last that long -// an array with preset default values so we can quickly use set() to reset the controllers -const resetArray = new Int16Array(CONTROLLER_TABLE_SIZE); -// default values -resetArray[midiControllers.mainVolume] = 100 << 7; -resetArray[midiControllers.expressionController] = 127 << 7; -resetArray[midiControllers.pan] = 64 << 7; -resetArray[midiControllers.releaseTime] = 64 << 7; -resetArray[midiControllers.brightness] = 64 << 7; -resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192; -resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7; -resetArray[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = 127 << 7; -resetArray[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = 0; - -/** - * @typedef {{ - * midiControllers: Int16Array, - * holdPedal: boolean, - * channelVibrato: {depth: number, delay: number, rate: number}, - * isMuted: boolean, - * - * voices: WorkletVoice[], - * sustainedVoices: WorkletVoice[], - * - * }} WorkletProcessorChannel - */ - /** * @type {Float32Array[]} */ @@ -87,11 +66,12 @@ class WorkletProcessor extends AudioWorkletProcessor { { this.workletProcessorChannels.push({ midiControllers: new Int16Array(CONTROLLER_TABLE_SIZE), + customControllers: new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE), voices: [], sustainedVoices: [], holdPedal: false, - channelVibrato: {delay: 0, depth: 0, rate: 0} - + channelVibrato: {delay: 0, depth: 0, rate: 0}, + isMuted: false }) this.resetControllers(this.workletProcessorChannels.length - 1, []); } @@ -105,38 +85,6 @@ class WorkletProcessor extends AudioWorkletProcessor { const channel = message.channelNumber; const channelVoices = this.workletProcessorChannels[channel].voices; switch (message.messageType) { - default: - break; - - // note off - case workletMessageType.noteOff: - channelVoices.forEach(v => { - if(v.midiNote !== data || v.isInRelease === true) - { - return; - } - // if hold pedal, move to sustain - if(this.workletProcessorChannels[channel].holdPedal) { - this.workletProcessorChannels[channel].sustainedVoices.push(v); - } - else - { - this.releaseVoice(v); - } - }); - break; - - case workletMessageType.killNote: - channelVoices.forEach(v => { - if(v.midiNote !== data) - { - return; - } - v.modulatedGenerators[generatorTypes.releaseVolEnv] = -12000; // set release to be very short - this.releaseVoice(v); - }); - break; - case workletMessageType.noteOn: data.forEach(voice => { const exclusive = voice.generators[generatorTypes.exclusiveClass]; @@ -167,6 +115,59 @@ class WorkletProcessor extends AudioWorkletProcessor { } break; + case workletMessageType.noteOff: + channelVoices.forEach(v => { + if(v.midiNote !== data || v.isInRelease === true) + { + return; + } + // if hold pedal, move to sustain + if(this.workletProcessorChannels[channel].holdPedal) { + this.workletProcessorChannels[channel].sustainedVoices.push(v); + } + else + { + this.releaseVoice(v); + } + }); + break; + + case workletMessageType.ccChange: + // special case: hold pedal + if(data[0] === midiControllers.sustainPedal) { + if (data[1] >= 64) + { + this.workletProcessorChannels[channel].holdPedal = true; + } + else + { + this.workletProcessorChannels[channel].holdPedal = false; + this.workletProcessorChannels[channel].sustainedVoices.forEach(v => { + this.releaseVoice(v) + }); + this.workletProcessorChannels[channel].sustainedVoices = []; + } + } + this.workletProcessorChannels[channel].midiControllers[data[0]] = data[1]; + channelVoices.forEach(v => computeModulators(v, this.workletProcessorChannels[channel].midiControllers)); + break; + + case workletMessageType.customcCcChange: + // custom controller change + this.workletProcessorChannels[channel].customControllers[data[0]] = data[1]; + break; + + case workletMessageType.killNote: + channelVoices.forEach(v => { + if(v.midiNote !== data) + { + return; + } + v.modulatedGenerators[generatorTypes.releaseVolEnv] = -12000; // set release to be very short + this.releaseVoice(v); + }); + break; + case workletMessageType.sampleDump: workletDumpedSamplesList[data.sampleID] = data.sampleData; // the sample maybe was loaded after the voice was sent... adjust the end position! @@ -201,28 +202,11 @@ class WorkletProcessor extends AudioWorkletProcessor { this.resetControllers(channel, data); break; - case workletMessageType.ccChange: - // special case: hold pedal - if(data[0] === midiControllers.sustainPedal) { - if (data[1] >= 64) - { - this.workletProcessorChannels[channel].holdPedal = true; - } - else - { - this.workletProcessorChannels[channel].holdPedal = false; - this.workletProcessorChannels[channel].sustainedVoices.forEach(v => { - this.releaseVoice(v) - }); - this.workletProcessorChannels[channel].sustainedVoices = []; - } - } - this.workletProcessorChannels[channel].midiControllers[data[0]] = data[1]; - channelVoices.forEach(v => computeModulators(v, this.workletProcessorChannels[channel].midiControllers)); - break; case workletMessageType.setChannelVibrato: - this.workletProcessorChannels[channel].channelVibrato = data; + this.workletProcessorChannels[channel].channelVibrato.delay = data.delay; + this.workletProcessorChannels[channel].channelVibrato.depth = data.depth; + this.workletProcessorChannels[channel].channelVibrato.rate = data.rate; break; case workletMessageType.clearCache: @@ -261,6 +245,9 @@ class WorkletProcessor extends AudioWorkletProcessor { case workletMessageType.addNewChannel: this.createWorkletChannel(); break; + + default: + break; } } @@ -395,8 +382,8 @@ class WorkletProcessor extends AudioWorkletProcessor { // calculate tuning let cents = voice.modulatedGenerators[generatorTypes.fineTune] - + channel.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] - + channel.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose]; + + channel.customControllers[customControllers.channelTuning] + + channel.customControllers[customControllers.channelTranspose]; let semitones = voice.modulatedGenerators[generatorTypes.coarseTune]; // calculate tuning by key @@ -411,7 +398,7 @@ class WorkletProcessor extends AudioWorkletProcessor { const lfoVal = getLFOValue(vibStart, vibFreqHz, currentTime); if(lfoVal) { - cents += lfoVal * vibratoDepth; + cents += lfoVal * (vibratoDepth * channel.customControllers[customControllers.modulationMultiplier]); } } @@ -428,7 +415,7 @@ class WorkletProcessor extends AudioWorkletProcessor { const modStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModLFO]); const modFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqModLFO]); const modLfoValue = getLFOValue(modStart, modFreqHz, currentTime); - cents += modLfoValue * modPitchDepth; + cents += modLfoValue * (modPitchDepth * channel.customControllers[customControllers.modulationMultiplier]); modLfoCentibels = modLfoValue * modVolDepth; lowpassCents += modLfoValue * modFilterDepth; } @@ -494,19 +481,21 @@ class WorkletProcessor extends AudioWorkletProcessor { ccVal: this.workletProcessorChannels[channel].midiControllers[ccNum] } }); - // transpose does not get affected either so save - const transpose = this.workletProcessorChannels[channel].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose]; // reset the array this.workletProcessorChannels[channel].midiControllers.set(resetArray); this.workletProcessorChannels[channel].channelVibrato = {rate: 0, depth: 0, delay: 0}; this.workletProcessorChannels[channel].holdPedal = false; - // restore unaffected - this.workletProcessorChannels[channel].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose] = transpose; excludedCCvalues.forEach((cc) => { this.workletProcessorChannels[channel].midiControllers[cc.ccNum] = cc.ccVal; - }) + }); + + // reset custom controllers + // special case: transpose does not get affected + const transpose = this.workletProcessorChannels[channel].customControllers[customControllers.channelTranspose]; + this.workletProcessorChannels[channel].customControllers.fill(0); + this.workletProcessorChannels[channel].customControllers[customControllers.channelTranspose] = transpose; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_system.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_system.js index a9544a30..3055fa6c 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_system.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_system.js @@ -4,6 +4,8 @@ import { modulatorSources } from '../../soundfont/chunk/modulators.js' import { midiControllers } from '../../midi_parser/midi_message.js' import { clearSamplesList, getWorkletVoices } from './worklet_utilities/worklet_voice.js' import { DEFAULT_PERCUSSION } from '../synthetizer.js' +import { workletMessageType } from './worklet_utilities/worklet_message.js' +import { customControllers, NON_CC_INDEX_OFFSET } from './worklet_utilities/worklet_processor_channel.js' /** * worklet_system.js @@ -16,9 +18,6 @@ export const WORKLET_SYSTEM_GAIN = 0.5; export const WORKLET_SYSTEM_REVERB_DIVIDER = 1000; export const WORKLET_SYSTEM_CHORUS_DIVIDER = 500; - -export const NON_CC_INDEX_OFFSET = 128; - const dataEntryStates = { Idle: 0, RPCoarse: 1, @@ -29,50 +28,6 @@ const dataEntryStates = { DataFine: 6 }; -export const workletMessageType = { - noteOff: 0, - noteOn: 1, - ccChange: 2, - sampleDump: 3, - killNote: 4, - ccReset: 5, - setChannelVibrato: 6, - clearCache: 7, - stopAll: 8, - killNotes: 9, - muteChannel: 10, - addNewChannel: 11, -}; - -/** - * @typedef {{ - * channelNumber: number - * messageType: 0|1|2|3|4|5|6|7|8|9|10|11, - * messageData: ( - * number[] - * |WorkletVoice[] - * |number - * |{sampleData: Float32Array, sampleID: number} - * |{rate: number, depth: number, delay: number} - * |boolean - * ) - * }} WorkletMessage - * Every message needs a channel number - * Message types: - * 0 - noteOff -> midiNote - * 1 - noteOn -> [midiNote, ...generators] - * 2 - controller change -> [ccNumber, ccValue] - * 3 - sample dump -> {sampleData: Float32Array, sampleID: number} - * 4 - note off instantly -> midiNote - * 5 - controllers reset -> array excluded controller numbers (excluded from the reset) - * 6 - channel vibrato -> {frequencyHz: number, depthCents: number, delaySeconds: number} - * 7 - clear cached samples -> (no data) - * 8 - stop all notes -> force (0 false, 1 true) - * 9 - kill notes -> amount - * 10 - mute channel -> isMuted - * 11 - add new channel -> (no data) - */ - /** * @typedef {{ * preset: Preset, @@ -80,8 +35,9 @@ export const workletMessageType = { * bank: number, * pitchBend: number, * channelPitchBendRange: number, - * channelTuningSemitones: number + * channelTuningCents: number * channelTranspose: number, // In semitones, does not get affected by resetControllers + * channelModulationDepthCents: number, * NRPCoarse: number, * NRPFine: number, * RPValue: number, @@ -221,8 +177,9 @@ export class WorkletSystem { bank: 0, pitchBend: 8192, channelPitchBendRange: 2, - channelTuningSemitones: 0, + channelTuningCents: 0, channelTranspose: 0, + channelModulationDepthCents: 50, NRPCoarse: 0, NRPFine: 0, RPValue: 0, @@ -425,6 +382,10 @@ export class WorkletSystem { case midiControllers.dataEntryMsb: this.dataEntryCoarse(channel, val); + break; + + case midiControllers.lsbForControl6DataEntry: + this.dataEntryFine(channel, val); } return true; @@ -451,8 +412,8 @@ export class WorkletSystem { this.midiChannels[channel].channelTranspose = 0; this.post({ channelNumber: channel, - messageType: workletMessageType.ccChange, - messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose, 0] + messageType: workletMessageType.customcCcChange, + messageData: [customControllers.channelTranspose, 0] }); } } @@ -629,7 +590,7 @@ export class WorkletSystem { switch(this.midiChannels[channel].NRPFine) { default: - console.info(`%cUnrecognized NRPN for %c${channel}%c: %c${this.midiChannels[channel].NRPCoarse} ${this.midiChannels[channel].NRPFine}%c data value: %c${dataValue}`, + console.info(`%cUnrecognized NRPN for %c${channel}%c: %c(${this.midiChannels[channel].NRPCoarse} ${this.midiChannels[channel].NRPFine})%c data value: %c${dataValue}`, consoleColors.warn, consoleColors.recognized, consoleColors.warn, @@ -735,9 +696,67 @@ export class WorkletSystem { // coarse tuning case 0x0002: // semitones - this.setChannelTuning(channel, dataValue - 64); + this.setChannelTuning(channel, (dataValue - 64) * 100); + break; + + // fine tuning + case 0x0001: + // note: this will not work properly unless the lsb is sent! + // here we store the raw value to then adjust in fine + this.setChannelTuning(channel, (dataValue - 64)); + break; + + // modulation depth + case 0x0005: + this.setModulationDepth(channel, dataValue * 100); + break + + case 0x3FFF: + this.resetParameters(channel); + break; + + } + + } + } + + /** + * Executes a data entry for an RPN tuning + * @param channel {number} + * @param dataValue {number} dataEntry LSB + */ + dataEntryFine(channel, dataValue) + { + switch (this.midiChannels[channel].dataEntryState) + { + default: + break; + + case dataEntryStates.RPCoarse: + case dataEntryStates.RPFine: + switch(this.midiChannels[channel].RPValue) + { + default: break; + // pitch bend range fine tune is not supported in the SoundFont2 format. (pitchbend range is in semitones rather than cents) + case 0x0000: + break; + + // fine tuning + case 0x0001: + // grab the data and shift + const coarse = this.midiChannels[channel].channelTuningCents; + const finalTuning = (coarse << 7) | dataValue; + this.setChannelTuning(channel, finalTuning * 0.0122); // multiply by 8192 / 100 (cent increment) + break; + + // modulation depth + case 0x0005: + + this.setModulationDepth(channel, this.midiChannels[channel].channelModulationDepthCents + (dataValue / 128) * 100); + break + case 0x3FFF: this.resetParameters(channel); break; @@ -750,18 +769,48 @@ export class WorkletSystem { /** * Sets the channel's tuning * @param channel {number} - * @param semitones {number} + * @param cents {number} */ - setChannelTuning(channel, semitones) + setChannelTuning(channel, cents) { - this.midiChannels[channel].channelTuningSemitones = semitones; - console.info(`%cChannel ${channel} tuning. Semitones: %c${semitones}`, + cents = Math.round(cents); + this.midiChannels[channel].channelTuningCents = cents; + console.info(`%cChannel ${channel} tuning. Cents: %c${cents}`, consoleColors.info, consoleColors.value); this.post({ channelNumber: channel, - messageType: workletMessageType.ccChange, - messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTuning, (this.midiChannels[channel].channelTuningSemitones) * 100] + messageType: workletMessageType.customcCcChange, + messageData: [customControllers.channelTuning, this.midiChannels[channel].channelTuningCents] + }); + } + + /** + * Sets the channel's mod depth + * @param channel {number} + * @param cents {number} + */ + setModulationDepth(channel, cents) + { + cents = Math.round(cents); + this.midiChannels[channel].channelModulationDepthCents = cents; + console.info(`%cChannel ${channel} modulation depth. Cents: %c${cents}`, + consoleColors.info, + consoleColors.value); + /* ============== + IMPORTANT + here we convert cents into a multiplier. + midi spec assumes the default is 50 cents, + but it might be different for the soundfont + so we create a multiplier by divinging cents by 50. + for example, if we want 100 cents, then multiplier will be 2, + which for a preset with depth of 50 will create 100. + ================*/ + const depthMultiplier = cents / 50; + this.post({ + channelNumber: channel, + messageType: workletMessageType.customcCcChange, + messageData: [customControllers.modulationMultiplier, depthMultiplier] }); } @@ -816,8 +865,8 @@ export class WorkletSystem { this.midiChannels[channel].channelTranspose = semitones; this.post({ channelNumber: channel, - messageType: workletMessageType.ccChange, - messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose, this.midiChannels[channel].channelTranspose * 100] + messageType: workletMessageType.customcCcChange, + messageData: [customControllers.channelTranspose, this.midiChannels[channel].channelTranspose * 100] }); } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js new file mode 100644 index 00000000..91a833bf --- /dev/null +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js @@ -0,0 +1,45 @@ +export const workletMessageType = { + noteOff: 0, + noteOn: 1, + ccChange: 2, + sampleDump: 3, + killNote: 4, + ccReset: 5, + setChannelVibrato: 6, + clearCache: 7, + stopAll: 8, + killNotes: 9, + muteChannel: 10, + addNewChannel: 11, + customcCcChange: 12, +}; + +/** + * @typedef {{ + * channelNumber: number + * messageType: 0|1|2|3|4|5|6|7|8|9|10|11|12, + * messageData: ( + * number[] + * |WorkletVoice[] + * |number + * |{sampleData: Float32Array, sampleID: number} + * |{rate: number, depth: number, delay: number} + * |boolean + * ) + * }} WorkletMessage + * Every message needs a channel number + * Message types: + * 0 - noteOff -> midiNote + * 1 - noteOn -> [midiNote, ...generators] + * 2 - controller change -> [ccNumber, ccValue] + * 3 - sample dump -> {sampleData: Float32Array, sampleID: number} + * 4 - note off instantly -> midiNote + * 5 - controllers reset -> array excluded controller numbers (excluded from the reset) + * 6 - channel vibrato -> {frequencyHz: number, depthCents: number, delaySeconds: number} + * 7 - clear cached samples -> (no data) + * 8 - stop all notes -> force (0 false, 1 true) + * 9 - kill notes -> amount + * 10 - mute channel -> isMuted + * 11 - add new channel -> (no data) + * 12 - custom controller change-> [ccNumber, ccValue] + */ \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js index c998e6e1..5b1b1ba2 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js @@ -1,6 +1,6 @@ -import { NON_CC_INDEX_OFFSET } from '../worklet_system.js' import { modulatorSources } from '../../../soundfont/chunk/modulators.js' import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js' +import { NON_CC_INDEX_OFFSET } from './worklet_processor_channel.js' /** * worklet_modulator.js diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js new file mode 100644 index 00000000..a9356bae --- /dev/null +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js @@ -0,0 +1,38 @@ +import { midiControllers } from '../../../midi_parser/midi_message.js' +import { modulatorSources } from '../../../soundfont/chunk/modulators.js' +/** + * @typedef {Object} WorkletProcessorChannel + * @property {Int16Array} midiControllers - array of MIDI controller values + * @property {Float32Array} customControllers - array of custom (not sf2) control values such as RPN pitch tuning, transpose, modulation depth, etc. + * @property {boolean} holdPedal - indicates whether the hold pedal is active + * @property {Object} channelVibrato - vibrato settings for the channel + * @property {number} channelVibrato.depth - depth of the vibrato effect (cents) + * @property {number} channelVibrato.delay - delay before the vibrato effect starts (seconds) + * @property {number} channelVibrato.rate - rate of the vibrato oscillation (Hz) + * @property {boolean} isMuted - indicates whether the channel is muted + * @property {WorkletVoice[]} voices - array of voices currently active on the channel + * @property {WorkletVoice[]} sustainedVoices - array of voices that are sustained on the channel + */ + +export const NON_CC_INDEX_OFFSET = 128; +export const CONTROLLER_TABLE_SIZE = 147; +// an array with preset default values so we can quickly use set() to reset the controllers +export const resetArray = new Int16Array(CONTROLLER_TABLE_SIZE); +// default values (the array is 14 bit so shift the 7 bit values by 7 bits) +resetArray[midiControllers.mainVolume] = 100 << 7; +resetArray[midiControllers.expressionController] = 127 << 7; +resetArray[midiControllers.pan] = 64 << 7; +resetArray[midiControllers.releaseTime] = 64 << 7; +resetArray[midiControllers.brightness] = 64 << 7; +resetArray[midiControllers.effects1Depth] = 40 << 7; +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192; +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7; +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = 127 << 7; + + +export const customControllers = { + channelTuning: 0, // cents + channelTranspose: 1, // cents + modulationMultiplier: 2, // cents +} +export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js index 6af1ba3b..e95a8930 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js @@ -5,66 +5,59 @@ */ /** - * @typedef {{sampleID: number, - * playbackStep: number, - * cursor: number, - * rootKey: number, - * loopStart: number, - * loopEnd: number, - * end: number, - * loopingMode: 0|1|2, - * }} WorkletSample - * - * - * @typedef {{ - * a0: number, - * a1: number, - * a2: number, - * a3: number, - * a4: number, - * - * x1: number, - * x2: number, - * y1: number, - * y2: number - * - * reasonanceCb: number, - * reasonanceGain: number - * - * cutoffCents: number, - * cutoffHz: number - * }} WorkletLowpassFilter - * - * @typedef {{ - * sample: WorkletSample, - * filter: WorkletLowpassFilter - * - * generators: Int16Array, - * modulators: Modulator[], - * modulatedGenerators: Int16Array, - * - * finished: boolean, - * isInRelease: boolean, - * - * channelNumber: number, - * velocity: number, - * midiNote: number, - * targetKey: number, - * - * currentAttenuationDb: number, - * volumeEnvelopeState: (0|1|2|3|4), - * currentModEnvValue: number, - * startTime: number, - * - * releaseStartTime: number, - * releaseStartModEnv: number, - * - * currentTuningCents: number, - * currentTuningCalculated: number - * }} WorkletVoice + * @typedef {Object} WorkletSample + * @property {number} sampleID - ID of the sample + * @property {number} playbackStep - current playback step (rate) + * @property {number} cursor - current position in the sample + * @property {number} rootKey - root key of the sample + * @property {number} loopStart - start position of the loop + * @property {number} loopEnd - end position of the loop + * @property {number} end - end position of the sample + * @property {0|1|2} loopingMode - looping mode of the sample + */ + +/** + * @typedef {Object} WorkletLowpassFilter + * @property {number} a0 - filter coefficient 1 + * @property {number} a1 - filter coefficient 2 + * @property {number} a2 - filter coefficient 3 + * @property {number} a3 - filter coefficient 4 + * @property {number} a4 - filter coefficient 5 + * @property {number} x1 - input history 1 + * @property {number} x2 - input history 2 + * @property {number} y1 - output history 1 + * @property {number} y2 - output history 2 + * @property {number} reasonanceCb - reasonance in centibels + * @property {number} reasonanceGain - resonance gain + * @property {number} cutoffCents - cutoff frequency in cents + * @property {number} cutoffHz - cutoff frequency in Hz */ + +/** + * @typedef {Object} WorkletVoice + * @property {WorkletSample} sample - sample ID for voice. + * @property {WorkletLowpassFilter} filter - lowpass filter applied to the voice + * @property {Int16Array} generators - the unmodulated (constant) generators of the voice + * @property {Modulator[]} modulators - the voice's modulators + * @property {Int16Array} modulatedGenerators - the generators modulated by the modulators + * @property {boolean} finished - indicates if the voice has finished + * @property {boolean} isInRelease - indicates if the voice is in the release phase + * @property {number} channelNumber - MIDI channel number + * @property {number} velocity - velocity of the note + * @property {number} midiNote - MIDI note number + * @property {number} targetKey - target key for the note + * @property {number} currentAttenuationDb - current attenuation in dB (used for calculating start of the release phase) + * @property {0|1|2|3|4} volumeEnvelopeState - state of the volume envelope. + * @property {number} currentModEnvValue - current value of the modulation envelope + * @property {number} startTime - start time of the voice + * @property {number} releaseStartTime - start time of the release phase + * @property {number} releaseStartModEnv - modenv value at the start of the release phase + * @property {number} currentTuningCents - current tuning adjustment in cents + * @property {number} currentTuningCalculated - calculated tuning adjustment + */ + import { addAndClampGenerator, generatorTypes } from '../../../soundfont/chunk/generators.js' -import { workletMessageType } from '../worklet_system.js' +import { workletMessageType } from './worklet_message.js' /** diff --git a/src/spessasynth_lib/utils/other.js b/src/spessasynth_lib/utils/other.js index 09492786..3a375be1 100644 --- a/src/spessasynth_lib/utils/other.js +++ b/src/spessasynth_lib/utils/other.js @@ -15,9 +15,7 @@ export function formatTime(totalSeconds) { } export const isMobile = function() { - let check = false; - (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); - return check; + return navigator.maxTouchPoints > 0; } /** @@ -43,7 +41,7 @@ export function calculateRGB(rgbString, operation) /** * Does what it says - * @param arr {ArrayLike} + * @param arr {ShiftableByteArray} * @returns {string} */ export function arrayToHexString(arr) { diff --git a/src/website/ui/renderer/renderer.js b/src/website/ui/renderer/renderer.js index 9d2ace27..448108a1 100644 --- a/src/website/ui/renderer/renderer.js +++ b/src/website/ui/renderer/renderer.js @@ -62,6 +62,12 @@ export class Renderer max: 127 }; + /** + * adds this to the synth's visual pitch in position caluclation + * @type {number} + */ + this.visualPitchBendOffset = 0; + this.lineThickness = ANALYSER_STROKE; this.normalAnalyserFft = CHANNEL_ANALYSER_FFT; this.drumAnalyserFft = DRUMS_ANALYSER_FFT; @@ -331,7 +337,7 @@ export class Renderer this.synth.synthesisSystem.midiChannels.forEach(channel => { // pitch range * (bend - 8192) / 8192)) * key width if(this.showVisualPitch) { - pitchBendXShift.push((channel.channelPitchBendRange * ((channel.pitchBend - 8192) / 8192)) * keyStep); + pitchBendXShift.push((channel.channelPitchBendRange * ((channel.pitchBend - 8192 + this.visualPitchBendOffset) / 8192)) * keyStep); } else { diff --git a/src/website/ui/synthesizer_ui/synthetizer_ui.js b/src/website/ui/synthesizer_ui/synthetizer_ui.js index b1a11511..15f90323 100644 --- a/src/website/ui/synthesizer_ui/synthetizer_ui.js +++ b/src/website/ui/synthesizer_ui/synthetizer_ui.js @@ -521,7 +521,7 @@ export class SynthetizerUI () => { this.synth.lockController(channelNumber, midiControllers.effects1Depth, false); }); - reverb.update(0); + reverb.update(40); controller.appendChild(reverb.div); // transpose