diff --git a/src/midi/AlphaSynthMidiFileHandler.ts b/src/midi/AlphaSynthMidiFileHandler.ts index b0c8a7625..02619d1c2 100644 --- a/src/midi/AlphaSynthMidiFileHandler.ts +++ b/src/midi/AlphaSynthMidiFileHandler.ts @@ -9,6 +9,7 @@ import { MidiFile } from '@src/midi/MidiFile'; import { MidiUtils } from '@src/midi/MidiUtils'; import { DynamicValue } from '@src/model/DynamicValue'; import { SynthConstants } from '@src/synth/SynthConstants'; +import { Midi20PerNotePitchBendEvent } from './Midi20ChannelVoiceEvent'; /** * This implementation of the {@link IMidiFileHandler} @@ -132,6 +133,26 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { this._midiFile.addEvent(message); } + public addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void { + if (value >= SynthConstants.MaxPitchWheel) { + value = SynthConstants.MaxPitchWheel; + } else { + value = Math.floor(value); + } + + // map midi 1.0 range of 0-16384 (0x4000) + // to midi 2.0 range of 0-4294967296 (0x100000000) + value = value * SynthConstants.MaxPitchWheel20 / SynthConstants.MaxPitchWheel + + const message = new Midi20PerNotePitchBendEvent( + tick, + this.makeCommand(MidiEventType.PerNotePitchBend, channel), + key, + value + ); + this._midiFile.addEvent(message); + } + public finishTrack(track: number, tick: number): void { const message: MetaDataEvent = new MetaDataEvent(tick, 0xff, MetaEventType.EndOfTrack, new Uint8Array(0)); this._midiFile.addEvent(message); diff --git a/src/midi/IMidiFileHandler.ts b/src/midi/IMidiFileHandler.ts index 296d8c80c..ef694a458 100644 --- a/src/midi/IMidiFileHandler.ts +++ b/src/midi/IMidiFileHandler.ts @@ -64,6 +64,18 @@ export interface IMidiFileHandler { */ addTempo(tick: number, tempo: number): void; + /** + * Add a bend specific to a note to the generated midi file. + * The note does not need to be started, if this event is signaled, the next time a note + * on this channel and key is played it will be affected. The note bend is cleared on a note-off for this key. + * @param track The midi track on which the bend should change. + * @param tick The midi ticks when the bend should change. + * @param channel The midi channel on which the bend should change. + * @param channel The key of the note that should be affected by the bend. + * @param value The new bend for the selected note. + */ + addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void; + /** * Add a bend to the generated midi file. * @param track The midi track on which the bend should change. diff --git a/src/midi/Midi20ChannelVoiceEvent.ts b/src/midi/Midi20ChannelVoiceEvent.ts new file mode 100644 index 000000000..35c85b366 --- /dev/null +++ b/src/midi/Midi20ChannelVoiceEvent.ts @@ -0,0 +1,37 @@ +import { IWriteable } from '@src/io/IWriteable'; +import { MidiEvent } from '@src/midi/MidiEvent'; + +/* + * Represents a MIDI 2.0 Channel Voice Message. + */ +export class Midi20PerNotePitchBendEvent extends MidiEvent { + + public noteKey: number; + public pitch: number; + + public constructor(tick: number, status: number, noteKey: number, pitch: number) { + super(tick, status, 0, 0); + this.noteKey = noteKey; + this.pitch = pitch; + } + + /** + * Writes the midi event as binary into the given stream. + * @param s The stream to write to. + */ + public writeTo(s: IWriteable): void { + let b: Uint8Array = new Uint8Array([ + 0x40, + this.message & 0xff, + this.noteKey & 0xff, + + 0x00 /* reserved */, + /* 32bit pitch integer */ + (this.pitch >> 24) & 0xff, + (this.pitch >> 16) & 0xff, + (this.pitch >> 8) & 0xff, + this.pitch & 0xff + ]); + s.write(b, 0, b.length); + } +} diff --git a/src/midi/MidiEvent.ts b/src/midi/MidiEvent.ts index 446a6b507..b6c2bd0fc 100644 --- a/src/midi/MidiEvent.ts +++ b/src/midi/MidiEvent.ts @@ -4,6 +4,12 @@ import { IWriteable } from '@src/io/IWriteable'; * Lists all midi events. */ export enum MidiEventType { + + /** + * A per note pitch bend. (Midi 2.0) + */ + PerNotePitchBend = 0x60, + /** * A note is released. */ diff --git a/src/midi/MidiFileGenerator.ts b/src/midi/MidiFileGenerator.ts index 530f9a63a..b6a884085 100644 --- a/src/midi/MidiFileGenerator.ts +++ b/src/midi/MidiFileGenerator.ts @@ -308,12 +308,13 @@ export class MidiFileGenerator { break; } this.generateVibratorWithParams( - beat.voice.bar.staff.track, barStartTick + beatStart, beat.playbackDuration, phaseLength, bendAmplitude, - track.playbackInfo.secondaryChannel + (tick, value) => { + this._handler.addBend(beat.voice.bar.staff.track.index, tick, track.playbackInfo.secondaryChannel, value); + } ); } } @@ -453,13 +454,13 @@ export class MidiFileGenerator { // // All String Bending/Variation effects if (note.hasBend) { - this.generateBend(note, noteStart, noteDuration, channel); + this.generateBend(note, noteStart, noteDuration, noteKey, channel); } else if (note.beat.hasWhammyBar && note.index === 0) { this.generateWhammy(note.beat, noteStart, noteDuration, channel); } else if (note.slideInType !== SlideInType.None || note.slideOutType !== SlideOutType.None) { this.generateSlide(note, noteStart, noteDuration, noteKey, dynamicValue, channel); } else if (note.vibrato !== VibratoType.None) { - this.generateVibrato(note, noteStart, noteDuration, channel); + this.generateVibrato(note, noteStart, noteDuration, noteKey, channel); } // for tied notes, and target notes of legato slides we do not pick the note @@ -638,7 +639,7 @@ export class MidiFileGenerator { } } - private generateVibrato(note: Note, noteStart: number, noteDuration: MidiNoteDuration, channel: number): void { + private generateVibrato(note: Note, noteStart: number, noteDuration: MidiNoteDuration, noteKey: number, channel: number): void { let phaseLength: number = 0; let bendAmplitude: number = 0; switch (note.vibrato) { @@ -654,16 +655,17 @@ export class MidiFileGenerator { return; } const track: Track = note.beat.voice.bar.staff.track; - this.generateVibratorWithParams(track, noteStart, noteDuration.noteOnly, phaseLength, bendAmplitude, channel); + this.generateVibratorWithParams(noteStart, noteDuration.noteOnly, phaseLength, bendAmplitude, (tick, value) => { + this._handler.addNoteBend(track.index, tick, channel, noteKey, value); + }); } private generateVibratorWithParams( - track: Track, noteStart: number, noteDuration: number, phaseLength: number, bendAmplitude: number, - channel: number + addBend: (tick: number, value: number) => void ): void { const resolution: number = 16; const phaseHalf: number = (phaseLength / 2) | 0; @@ -676,10 +678,8 @@ export class MidiFileGenerator { const phaseDuration: number = noteStart + phaseLength < noteEnd ? phaseLength : noteEnd - noteStart; while (phase < phaseDuration) { let bend: number = bendAmplitude * Math.sin((phase * Math.PI) / phaseHalf); - this._handler.addBend( - track.index, + addBend( (noteStart + phase) | 0, - channel, MidiFileGenerator.getPitchWheel(bend) ); phase += resolution; @@ -771,12 +771,19 @@ export class MidiFileGenerator { break; } - this.generateWhammyOrBend(noteStart, channel, duration, playedBendPoints, track); + this.generateWhammyOrBend(noteStart, duration, playedBendPoints, (tick, value) => { + this._handler.addNoteBend(track.index, tick, channel, noteKey, value); + }); } - private generateBend(note: Note, noteStart: number, noteDuration: MidiNoteDuration, channel: number): void { + private generateBend(note: Note, noteStart: number, noteDuration: MidiNoteDuration, noteKey: number, channel: number): void { let bendPoints: BendPoint[] = note.bendPoints; let track: Track = note.beat.voice.bar.staff.track; + + const addBend: (tick: number, value: number) => void = (tick, value) => { + this._handler.addNoteBend(track.index, tick, channel, noteKey, value); + }; + // if bend is extended on next tied note, we directly bend to the final bend value let finalBendValue: number | null = null; // Bends are spread across all tied notes unless they have a bend on their own. @@ -842,22 +849,20 @@ export class MidiFileGenerator { if (note.beat.graceType === GraceType.BendGrace) { this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, true, [note.bendPoints[0].value, finalBendValue], - bendDuration + bendDuration, + addBend ); } else { this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, false, [note.bendPoints[0].value, finalBendValue], - bendDuration + bendDuration, + addBend ); } return; @@ -876,12 +881,11 @@ export class MidiFileGenerator { case BendStyle.Fast: this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, false, [note.bendPoints[0].value, note.bendPoints[1].value, note.bendPoints[2].value], - bendDuration + bendDuration, + addBend ); return; } @@ -909,12 +913,11 @@ export class MidiFileGenerator { } this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, false, [note.bendPoints[0].value, finalBendValue], - bendDuration + bendDuration, + addBend ); return; } @@ -933,28 +936,26 @@ export class MidiFileGenerator { this._handler.addBend(track.index, noteStart, channel, preBendValue | 0); this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, false, [note.bendPoints[0].value, note.bendPoints[1].value], - bendDuration + bendDuration, + addBend ); return; } break; } - this.generateWhammyOrBend(noteStart, channel, duration, playedBendPoints, track); + this.generateWhammyOrBend(noteStart, duration, playedBendPoints, addBend); } private generateSongBookWhammyOrBend( noteStart: number, - channel: number, duration: number, - track: Track, bendAtBeginning: boolean, bendValues: number[], - bendDuration: number + bendDuration: number, + addBend: (tick: number, value: number) => void ): void { const startTick: number = bendAtBeginning ? noteStart : noteStart + duration - bendDuration; const ticksBetweenPoints: number = bendDuration / (bendValues.length - 1); @@ -962,7 +963,7 @@ export class MidiFileGenerator { const currentBendValue: number = MidiFileGenerator.getPitchWheel(bendValues[i]); const nextBendValue: number = MidiFileGenerator.getPitchWheel(bendValues[i + 1]); const tick: number = startTick + ticksBetweenPoints * i; - this.generateBendValues(tick, channel, track, ticksBetweenPoints, currentBendValue, nextBendValue); + this.generateBendValues(tick, ticksBetweenPoints, currentBendValue, nextBendValue, addBend); } } @@ -975,6 +976,10 @@ export class MidiFileGenerator { noteStart--; } + const addBend: (tick: number, value: number) => void = (tick, value) => { + this._handler.addBend(track.index, tick, channel, value); + }; + let playedBendPoints: BendPoint[] = []; switch (beat.whammyBarType) { case WhammyType.Custom: @@ -996,12 +1001,11 @@ export class MidiFileGenerator { ); this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, false, [bendPoints[0].value, bendPoints[1].value], - whammyDuration + whammyDuration, + addBend ); return; } @@ -1023,12 +1027,11 @@ export class MidiFileGenerator { ); this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, true, [bendPoints[0].value, bendPoints[1].value, bendPoints[2].value], - whammyDuration + whammyDuration, + addBend ); return; } @@ -1058,26 +1061,24 @@ export class MidiFileGenerator { ); this.generateSongBookWhammyOrBend( noteStart, - channel, duration, - track, false, [bendPoints[0].value, bendPoints[1].value], - whammyDuration + whammyDuration, + addBend ); return; } break; } - this.generateWhammyOrBend(noteStart, channel, duration, playedBendPoints, track); + this.generateWhammyOrBend(noteStart, duration, playedBendPoints, addBend); } private generateWhammyOrBend( noteStart: number, - channel: number, duration: number, playedBendPoints: BendPoint[], - track: Track + addBend: (tick: number, value: number) => void ): void { const ticksPerPosition: number = duration / BendPoint.MaxPosition; for (let i: number = 0; i < playedBendPoints.length - 1; i++) { @@ -1091,17 +1092,16 @@ export class MidiFileGenerator { // we will generate one pitchbend message for each value // for this we need to calculate how many ticks to offset per value const tick: number = noteStart + ticksPerPosition * currentPoint.offset; - this.generateBendValues(tick, channel, track, ticksBetweenPoints, currentBendValue, nextBendValue); + this.generateBendValues(tick, ticksBetweenPoints, currentBendValue, nextBendValue, addBend); } } private generateBendValues( currentTick: number, - channel: number, - track: Track, ticksBetweenPoints: number, currentBendValue: number, - nextBendValue: number + nextBendValue: number, + addBend: (tick: number, value: number) => void ): void { const millisBetweenPoints = MidiUtils.ticksToMillis(ticksBetweenPoints, this._currentTempo); const numberOfSemitones = Math.abs(nextBendValue - currentBendValue) / MidiFileGenerator.PitchValuePerSemitone; @@ -1113,14 +1113,14 @@ export class MidiFileGenerator { const pitchPerBreakpoint = (nextBendValue - currentBendValue) / numberOfSteps; for (let i = 0; i < numberOfSteps; i++) { - this._handler.addBend(track.index, currentTick | 0, channel, Math.round(currentBendValue)); + addBend(currentTick | 0, Math.round(currentBendValue)); currentBendValue += pitchPerBreakpoint; currentTick += ticksPerBreakpoint; } // final bend value if needed if (currentBendValue < nextBendValue) { - this._handler.addBend(track.index, currentTick | 0, channel, nextBendValue); + addBend(currentTick | 0, nextBendValue); } } diff --git a/src/model/JsonConverter.ts b/src/model/JsonConverter.ts index 82c76df37..b64f83bfc 100644 --- a/src/model/JsonConverter.ts +++ b/src/model/JsonConverter.ts @@ -19,6 +19,7 @@ import { Staff } from '@src/model/Staff'; import { Track } from '@src/model/Track'; import { Voice } from '@src/model/Voice'; import { Settings } from '@src/Settings'; +import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20ChannelVoiceEvent'; interface SerializedNote { tieOriginId?: number; @@ -457,6 +458,10 @@ export class JsonConverter { midiEvent2 = new MetaNumberEvent(tick, 0, 0, midiEvent.value); midiEvent2.message = message; break; + case 'Midi20PerNotePitchBendEvent': + midiEvent2 = new Midi20PerNotePitchBendEvent(tick, 0, midiEvent.noteKey, midiEvent.pitch); + midiEvent2.message = message; + break; default: midiEvent2 = new MidiEvent(tick, 0, 0, 0); midiEvent2.message = message; @@ -486,6 +491,10 @@ export class JsonConverter { } else if (midiEvent instanceof MetaNumberEvent) { midiEvent2.type = 'MetaNumberEvent'; midiEvent2.value = midiEvent.value; + } else if(midiEvent instanceof Midi20PerNotePitchBendEvent) { + midiEvent2.type = 'Midi20PerNotePitchBendEvent'; + midiEvent2.noteKey = midiEvent.noteKey; + midiEvent2.pitch = midiEvent.pitch; } } return midi2; diff --git a/src/synth/SynthConstants.ts b/src/synth/SynthConstants.ts index 69f07cdaa..e00bdaf08 100644 --- a/src/synth/SynthConstants.ts +++ b/src/synth/SynthConstants.ts @@ -16,7 +16,12 @@ export class SynthConstants { /** * The Midi Pitch bend message is a 15-bit value */ - public static readonly MaxPitchWheel: number = 16384; + public static readonly MaxPitchWheel: number = 0x4000; + + /** + * The Midi 2.0 Pitch bend message is a 32-bit value + */ + public static readonly MaxPitchWheel20: number = 0x100000000; /** * The pitch wheel value for no pitch change at all. */ diff --git a/src/synth/synthesis/Channel.ts b/src/synth/synthesis/Channel.ts index 80f6bbdc9..fee8e6259 100644 --- a/src/synth/synthesis/Channel.ts +++ b/src/synth/synthesis/Channel.ts @@ -7,6 +7,7 @@ export class Channel { public presetIndex: number = 0; public bank: number = 0; public pitchWheel: number = 0; + public perNotePitchWheel: Map = new Map(); public midiPan: number = 0; public midiVolume: number = 0; public midiExpression: number = 0; diff --git a/src/synth/synthesis/Channels.ts b/src/synth/synthesis/Channels.ts index d30db48ff..ca48227b2 100644 --- a/src/synth/synthesis/Channels.ts +++ b/src/synth/synthesis/Channels.ts @@ -17,10 +17,8 @@ export class Channels { voice.playingChannel = this.activeChannel; voice.mixVolume = c.mixVolume; voice.noteGainDb += c.gainDb; - voice.calcPitchRatio( - c.pitchWheel === 8192 ? c.tuning : (c.pitchWheel / 16383.0) * c.pitchRange * 2.0 - c.pitchRange + c.tuning, - tinySoundFont.outSampleRate - ); + + voice.updatePitchRatio(c, tinySoundFont.outSampleRate); if (newpan <= -0.5) { voice.panFactorLeft = 1.0; diff --git a/src/synth/synthesis/TinySoundFont.ts b/src/synth/synthesis/TinySoundFont.ts index 81670f57a..d06717fe6 100644 --- a/src/synth/synthesis/TinySoundFont.ts +++ b/src/synth/synthesis/TinySoundFont.ts @@ -26,6 +26,7 @@ import { SynthHelper } from '@src/synth/SynthHelper'; import { TypeConversions } from '@src/io/TypeConversions'; import { Logger } from '@src/Logger'; import { SynthConstants } from '@src/synth/SynthConstants'; +import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20ChannelVoiceEvent'; /** * This is a tiny soundfont based synthesizer. @@ -173,6 +174,16 @@ export class TinySoundFont { case MidiEventType.PitchBend: this.channelSetPitchWheel(channel, data1 | (data2 << 7)); break; + case MidiEventType.PerNotePitchBend: + const midi20 = e as Midi20PerNotePitchBendEvent; + let perNotePitchWheel = midi20.pitch; + // midi 2.0 -> midi 1.0 + perNotePitchWheel = perNotePitchWheel * SynthConstants.MaxPitchWheel / SynthConstants.MaxPitchWheel20; + this.channelSetPerNotePitchWheel(channel, + midi20.noteKey, + perNotePitchWheel + ); + break; } } @@ -186,7 +197,7 @@ export class TinySoundFont { public setupMetronomeChannel(volume: number): void { this.channelSetMixVolume(SynthConstants.MetronomeChannel, volume); - if(volume > 0) { + if (volume > 0) { this.channelSetVolume(SynthConstants.MetronomeChannel, 1); this.channelSetPresetNumber(SynthConstants.MetronomeChannel, 0, true); } @@ -210,6 +221,7 @@ export class TinySoundFont { for (const c of this._channels.channelList) { c.presetIndex = c.bank = 0; c.pitchWheel = c.midiPan = 8192; + c.perNotePitchWheel.clear(); c.midiVolume = c.midiExpression = 16383; c.midiRpn = 0xffff; c.midiData = 0; @@ -572,6 +584,9 @@ export class TinySoundFont { } } + let c: Channel = this.channelInit(channel); + c.perNotePitchWheel.delete(key); + if (!matchFirst) { return; } @@ -598,6 +613,9 @@ export class TinySoundFont { * @param channel channel number */ public channelNoteOffAll(channel: number): void { + let c: Channel = this.channelInit(channel); + c.perNotePitchWheel.clear(); + for (const v of this._voices) { if ( v.playingPreset !== -1 && @@ -614,6 +632,9 @@ export class TinySoundFont { * @param channel channel number */ public channelSoundsOffAll(channel: number): void { + let c: Channel = this.channelInit(channel); + c.perNotePitchWheel.clear(); + for (let v of this._voices) { if (v.playingPreset !== -1 && v.playingChannel === channel && @@ -689,7 +710,7 @@ export class TinySoundFont { if (presetIndex === -1) { return false; } - + c.presetIndex = TypeConversions.int32ToUint16(presetIndex); c.bank = TypeConversions.int32ToUint16(bank); return true; @@ -753,13 +774,25 @@ export class TinySoundFont { this.channelApplyPitch(channel, c); } - private channelApplyPitch(channel: number, c: Channel): void { - const pitchShift: number = c.pitchWheel === 8192 - ? c.tuning - : (c.pitchWheel / 16383.0 * c.pitchRange * 2) - c.pitchRange + c.tuning; + /** + * @param channel channel number + * @param key note value between 0 and 127 + * @param pitchWheel pitch wheel position 0 to 16383 (default 8192 unpitched) + */ + public channelSetPerNotePitchWheel(channel: number, key: number, pitchWheel: number): void { + const c: Channel = this.channelInit(channel); + if(c.perNotePitchWheel.has(key) && c.perNotePitchWheel.get(key) === pitchWheel) { + return; + } + + c.perNotePitchWheel.set(key, pitchWheel); + this.channelApplyPitch(channel, c, key); + } + + private channelApplyPitch(channel: number, c: Channel, key:number = -1): void { for (const v of this._voices) { - if (v.playingChannel === channel && v.playingPreset !== -1) { - v.calcPitchRatio(pitchShift, this.outSampleRate); + if (v.playingChannel === channel && v.playingPreset !== -1 && (key == -1 || v.playingKey === key)) { + v.updatePitchRatio(c, this.outSampleRate); } } } @@ -994,7 +1027,7 @@ export class TinySoundFont { for (let phdrIndex: number = 0; phdrIndex < hydra.phdrs.length - 1; phdrIndex++) { const phdr: HydraPhdr = hydra.phdrs[phdrIndex]; let regionIndex: number = 0; - + const preset: Preset = (this._presets[phdrIndex] = new Preset()); preset.name = phdr.presetName; preset.bank = phdr.bank; diff --git a/src/synth/synthesis/Voice.ts b/src/synth/synthesis/Voice.ts index 2df8fbc06..c2b630cda 100644 --- a/src/synth/synthesis/Voice.ts +++ b/src/synth/synthesis/Voice.ts @@ -10,6 +10,7 @@ import { VoiceEnvelope, VoiceEnvelopeSegment } from '@src/synth/synthesis/VoiceE import { VoiceLfo } from '@src/synth/synthesis/VoiceLfo'; import { VoiceLowPass } from '@src/synth/synthesis/VoiceLowPass'; import { SynthHelper } from '@src/synth/SynthHelper'; +import { Channel } from './Channel'; export class Voice { /** @@ -47,6 +48,20 @@ export class Voice { public mixVolume: number = 0; public mute: boolean = false; + public updatePitchRatio(c: Channel, outSampleRate: number) { + let pitchWheel = c.pitchWheel; + // add additional note pitch + if (c.perNotePitchWheel.has(this.playingKey)) { + pitchWheel += (c.perNotePitchWheel.get(this.playingKey)! - 8192); + } + + const pitchShift: number = pitchWheel === 8192 + ? c.tuning + : (pitchWheel / 16383.0 * c.pitchRange * 2) - c.pitchRange + c.tuning; + + this.calcPitchRatio(pitchShift, outSampleRate); + } + public calcPitchRatio(pitchShift: number, outSampleRate: number): void { if (!this.region) { return; @@ -111,20 +126,20 @@ export class Voice { let tmpSourceSamplePosition: number = this.sourceSamplePosition; let tmpLowpass: VoiceLowPass = new VoiceLowPass(this.lowPass); - + let dynamicLowpass: boolean = region.modLfoToFilterFc !== 0 || region.modEnvToFilterFc !== 0; let tmpSampleRate: number = 0; let tmpInitialFilterFc: number = 0; let tmpModLfoToFilterFc: number = 0; let tmpModEnvToFilterFc: number = 0; - + let dynamicPitchRatio: boolean = region.modLfoToPitch !== 0 || region.modEnvToPitch !== 0 || region.vibLfoToPitch !== 0; let pitchRatio: number = 0; let tmpModLfoToPitch: number = 0; let tmpVibLfoToPitch: number = 0; let tmpModEnvToPitch: number = 0; - + let dynamicGain: boolean = region.modLfoToVolume !== 0; let noteGain: number = 0; let tmpModLfoToVolume: number = 0; @@ -166,7 +181,7 @@ export class Voice { let gainRight: number = 0; let blockSamples: number = numSamples > Voice.RenderEffectSampleBLock ? Voice.RenderEffectSampleBLock : numSamples; numSamples -= blockSamples; - + if (dynamicLowpass) { let fres: number = tmpInitialFilterFc + @@ -182,9 +197,9 @@ export class Voice { pitchRatio = SynthHelper.timecents2Secs( this.pitchInputTimecents + - (this.modLfo.level * tmpModLfoToPitch + - this.vibLfo.level * tmpVibLfoToPitch + - this.modEnv.level * tmpModEnvToPitch) + (this.modLfo.level * tmpModLfoToPitch + + this.vibLfo.level * tmpVibLfoToPitch + + this.modEnv.level * tmpModEnvToPitch) ) * this.pitchOutputFactor; } @@ -193,7 +208,7 @@ export class Voice { } gainMono = noteGain * this.ampEnv.level; - + if (isMuted) { gainMono = 0; } else { @@ -250,19 +265,19 @@ export class Voice { while (blockSamples-- > 0 && tmpSourceSamplePosition < tmpSampleEndDbl) { let pos: number = tmpSourceSamplePosition | 0; let nextPos: number = pos >= tmpLoopEnd && isLooping ? tmpLoopStart : pos + 1; - + // Simple linear interpolation. let alpha: number = tmpSourceSamplePosition - pos; let val: number = input[pos] * (1.0 - alpha) + input[nextPos] * alpha; // Low-pass filter. if (tmpLowpass.active) val = tmpLowpass.process(val); - + outputBuffer[offset + outL] += val * gainLeft; outL++; outputBuffer[offset + outR] += val * gainRight; outR++; - + // Next sample. tmpSourceSamplePosition += pitchRatio; if (tmpSourceSamplePosition >= tmpLoopEndDbl && isLooping) { @@ -281,10 +296,10 @@ export class Voice { // Low-pass filter. if (tmpLowpass.active) val = tmpLowpass.process(val); - + outputBuffer[offset + outL] = val * gainMono; outL++; - + // Next sample. tmpSourceSamplePosition += pitchRatio; if (tmpSourceSamplePosition >= tmpLoopEndDbl && isLooping) { @@ -299,7 +314,7 @@ export class Voice { return; } } - + this.sourceSamplePosition = tmpSourceSamplePosition; if (tmpLowpass.active || dynamicLowpass) { this.lowPass = tmpLowpass; diff --git a/test/audio/FlatMidiEventGenerator.ts b/test/audio/FlatMidiEventGenerator.ts index 97b72ca0a..3351b7790 100644 --- a/test/audio/FlatMidiEventGenerator.ts +++ b/test/audio/FlatMidiEventGenerator.ts @@ -51,6 +51,11 @@ export class FlatMidiEventGenerator implements IMidiFileHandler { this.midiEvents.push(e); } + public addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void { + let e = new NoteBendEvent(tick, track, channel, key, value); + this.midiEvents.push(e); + } + public finishTrack(track: number, tick: number): void { let e = new TrackEndEvent(tick, track); this.midiEvents.push(e); @@ -333,3 +338,29 @@ export class BendEvent extends ChannelMidiEvent { return false; } } +export class NoteBendEvent extends ChannelMidiEvent { + public key: number; + public value: number; + + public constructor(tick: number, track: number, channel: number, key: number, value: number) { + super(tick, track, channel); + this.key = key; + this.value = value; + } + + public toString(): string { + return `NoteBend: ${super.toString()} Key[${this.key}] Value[${this.value}]`; + } + + public equals(obj: unknown): boolean { + if (!super.equals(obj)) { + return false; + } + + if (obj instanceof NoteBendEvent) { + return this.value === obj.value && this.key === obj.key; + } + + return false; + } +} diff --git a/test/audio/MidiFileGenerator.test.ts b/test/audio/MidiFileGenerator.test.ts index 2c8223287..5be20b3c3 100644 --- a/test/audio/MidiFileGenerator.test.ts +++ b/test/audio/MidiFileGenerator.test.ts @@ -17,6 +17,7 @@ import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { BendEvent, + NoteBendEvent, ControlChangeEvent, FlatMidiEventGenerator, MidiEvent as FlatMidiEvent, @@ -97,18 +98,18 @@ describe('MidiFileGeneratorTest', () => { // bend effect new BendEvent(0, 0, info.secondaryChannel, 8192), // no bend - new BendEvent(0, 0, info.secondaryChannel, 8192), - new BendEvent(1 * 80, 0, info.secondaryChannel, 8277), - new BendEvent(2 * 80, 0, info.secondaryChannel, 8363), - new BendEvent(3 * 80, 0, info.secondaryChannel, 8448), - new BendEvent(4 * 80, 0, info.secondaryChannel, 8533), - new BendEvent(5 * 80, 0, info.secondaryChannel, 8619), - new BendEvent(6 * 80, 0, info.secondaryChannel, 8704), - new BendEvent(7 * 80, 0, info.secondaryChannel, 8789), - new BendEvent(8 * 80, 0, info.secondaryChannel, 8875), - new BendEvent(9 * 80, 0, info.secondaryChannel, 8960), - new BendEvent(10 * 80, 0, info.secondaryChannel, 9045), - new BendEvent(11 * 80, 0, info.secondaryChannel, 9131), + new NoteBendEvent(0, 0, info.secondaryChannel, note.realValue, 8192), + new NoteBendEvent(1 * 80, 0, info.secondaryChannel, note.realValue, 8277), + new NoteBendEvent(2 * 80, 0, info.secondaryChannel, note.realValue, 8363), + new NoteBendEvent(3 * 80, 0, info.secondaryChannel, note.realValue, 8448), + new NoteBendEvent(4 * 80, 0, info.secondaryChannel, note.realValue, 8533), + new NoteBendEvent(5 * 80, 0, info.secondaryChannel, note.realValue, 8619), + new NoteBendEvent(6 * 80, 0, info.secondaryChannel, note.realValue, 8704), + new NoteBendEvent(7 * 80, 0, info.secondaryChannel, note.realValue, 8789), + new NoteBendEvent(8 * 80, 0, info.secondaryChannel, note.realValue, 8875), + new NoteBendEvent(9 * 80, 0, info.secondaryChannel, note.realValue, 8960), + new NoteBendEvent(10 * 80, 0, info.secondaryChannel, note.realValue, 9045), + new NoteBendEvent(11 * 80, 0, info.secondaryChannel, note.realValue, 9131), // note itself new NoteEvent( @@ -240,18 +241,18 @@ describe('MidiFileGeneratorTest', () => { // bend beat new BendEvent(ticks[6], 0, info.secondaryChannel, 8192), - new BendEvent(ticks[6] + 12 * 0, 0, info.secondaryChannel, 8192), - new BendEvent(ticks[6] + 12 * 1, 0, info.secondaryChannel, 8277), - new BendEvent(ticks[6] + 12 * 2, 0, info.secondaryChannel, 8363), - new BendEvent(ticks[6] + 12 * 3, 0, info.secondaryChannel, 8448), - new BendEvent(ticks[6] + 12 * 4, 0, info.secondaryChannel, 8533), - new BendEvent(ticks[6] + 12 * 5, 0, info.secondaryChannel, 8619), - new BendEvent(ticks[6] + 12 * 6, 0, info.secondaryChannel, 8704), - new BendEvent(ticks[6] + 12 * 7, 0, info.secondaryChannel, 8789), - new BendEvent(ticks[6] + 12 * 8, 0, info.secondaryChannel, 8875), - new BendEvent(ticks[6] + 12 * 9, 0, info.secondaryChannel, 8960), - new BendEvent(ticks[6] + 12 * 10, 0, info.secondaryChannel, 9045), - new BendEvent(ticks[6] + 12 * 11, 0, info.secondaryChannel, 9131), + new NoteBendEvent(ticks[6] + 12 * 0, 0, info.secondaryChannel, 67, 8192), + new NoteBendEvent(ticks[6] + 12 * 1, 0, info.secondaryChannel, 67, 8277), + new NoteBendEvent(ticks[6] + 12 * 2, 0, info.secondaryChannel, 67, 8363), + new NoteBendEvent(ticks[6] + 12 * 3, 0, info.secondaryChannel, 67, 8448), + new NoteBendEvent(ticks[6] + 12 * 4, 0, info.secondaryChannel, 67, 8533), + new NoteBendEvent(ticks[6] + 12 * 5, 0, info.secondaryChannel, 67, 8619), + new NoteBendEvent(ticks[6] + 12 * 6, 0, info.secondaryChannel, 67, 8704), + new NoteBendEvent(ticks[6] + 12 * 7, 0, info.secondaryChannel, 67, 8789), + new NoteBendEvent(ticks[6] + 12 * 8, 0, info.secondaryChannel, 67, 8875), + new NoteBendEvent(ticks[6] + 12 * 9, 0, info.secondaryChannel, 67, 8960), + new NoteBendEvent(ticks[6] + 12 * 10, 0, info.secondaryChannel, 67, 9045), + new NoteBendEvent(ticks[6] + 12 * 11, 0, info.secondaryChannel, 67, 9131), new NoteEvent(ticks[6], 0, info.secondaryChannel, 3840, 67, DynamicValue.F), // end of track @@ -308,31 +309,31 @@ describe('MidiFileGeneratorTest', () => { // bend effect new BendEvent(0, 0, info.secondaryChannel, 8192), - new BendEvent(0 * 40, 0, info.secondaryChannel, 8192), // no bend - new BendEvent(1 * 40, 0, info.secondaryChannel, 8277), - new BendEvent(2 * 40, 0, info.secondaryChannel, 8363), - new BendEvent(3 * 40, 0, info.secondaryChannel, 8448), - new BendEvent(4 * 40, 0, info.secondaryChannel, 8533), - new BendEvent(5 * 40, 0, info.secondaryChannel, 8619), - new BendEvent(6 * 40, 0, info.secondaryChannel, 8704), - new BendEvent(7 * 40, 0, info.secondaryChannel, 8789), - new BendEvent(8 * 40, 0, info.secondaryChannel, 8875), - new BendEvent(9 * 40, 0, info.secondaryChannel, 8960), - new BendEvent(10 * 40, 0, info.secondaryChannel, 9045), - new BendEvent(11 * 40, 0, info.secondaryChannel, 9131), - new BendEvent(12 * 40, 0, info.secondaryChannel, 9216), // full bend - new BendEvent(13 * 40, 0, info.secondaryChannel, 9131), - new BendEvent(14 * 40, 0, info.secondaryChannel, 9045), - new BendEvent(15 * 40, 0, info.secondaryChannel, 8960), - new BendEvent(16 * 40, 0, info.secondaryChannel, 8875), - new BendEvent(17 * 40, 0, info.secondaryChannel, 8789), - new BendEvent(18 * 40, 0, info.secondaryChannel, 8704), - new BendEvent(19 * 40, 0, info.secondaryChannel, 8619), - new BendEvent(20 * 40, 0, info.secondaryChannel, 8533), - new BendEvent(21 * 40, 0, info.secondaryChannel, 8448), - new BendEvent(22 * 40, 0, info.secondaryChannel, 8363), - new BendEvent(23 * 40, 0, info.secondaryChannel, 8277), - new BendEvent(24 * 40, 0, info.secondaryChannel, 8192), // no bend + new NoteBendEvent(0 * 40, 0, info.secondaryChannel, note.realValue, 8192), // no bend + new NoteBendEvent(1 * 40, 0, info.secondaryChannel, note.realValue, 8277), + new NoteBendEvent(2 * 40, 0, info.secondaryChannel, note.realValue, 8363), + new NoteBendEvent(3 * 40, 0, info.secondaryChannel, note.realValue, 8448), + new NoteBendEvent(4 * 40, 0, info.secondaryChannel, note.realValue, 8533), + new NoteBendEvent(5 * 40, 0, info.secondaryChannel, note.realValue, 8619), + new NoteBendEvent(6 * 40, 0, info.secondaryChannel, note.realValue, 8704), + new NoteBendEvent(7 * 40, 0, info.secondaryChannel, note.realValue, 8789), + new NoteBendEvent(8 * 40, 0, info.secondaryChannel, note.realValue, 8875), + new NoteBendEvent(9 * 40, 0, info.secondaryChannel, note.realValue, 8960), + new NoteBendEvent(10 * 40, 0, info.secondaryChannel,note.realValue, 9045), + new NoteBendEvent(11 * 40, 0, info.secondaryChannel,note.realValue, 9131), + new NoteBendEvent(12 * 40, 0, info.secondaryChannel,note.realValue, 9216), // full bend + new NoteBendEvent(13 * 40, 0, info.secondaryChannel,note.realValue, 9131), + new NoteBendEvent(14 * 40, 0, info.secondaryChannel,note.realValue, 9045), + new NoteBendEvent(15 * 40, 0, info.secondaryChannel,note.realValue, 8960), + new NoteBendEvent(16 * 40, 0, info.secondaryChannel,note.realValue, 8875), + new NoteBendEvent(17 * 40, 0, info.secondaryChannel,note.realValue, 8789), + new NoteBendEvent(18 * 40, 0, info.secondaryChannel,note.realValue, 8704), + new NoteBendEvent(19 * 40, 0, info.secondaryChannel,note.realValue, 8619), + new NoteBendEvent(20 * 40, 0, info.secondaryChannel,note.realValue, 8533), + new NoteBendEvent(21 * 40, 0, info.secondaryChannel,note.realValue, 8448), + new NoteBendEvent(22 * 40, 0, info.secondaryChannel,note.realValue, 8363), + new NoteBendEvent(23 * 40, 0, info.secondaryChannel,note.realValue, 8277), + new NoteBendEvent(24 * 40, 0, info.secondaryChannel,note.realValue, 8192), // no bend // note itself new NoteEvent( @@ -434,4 +435,132 @@ describe('MidiFileGeneratorTest', () => { expect(actualMidiStartTimes.join(',')).toEqual(expectedMidiStartTimes.join(',')); expect(actualMidiDurations.join(',')).toEqual(expectedMidiDurations.join(',')); }); + + it('beat-multi-bend', () => { + let tex: string = ':4 (15.6{b(0 4)} 14.6{b(0 8)}) 15.6'; + let score: Score = parseTex(tex); + expect(score.tracks.length).toEqual(1); + expect(score.tracks[0].staves[0].bars.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[0].voices.length).toEqual(1); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats.length).toEqual(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes.length).toEqual(2); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes.length).toEqual(1); + let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator(); + let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler); + generator.generate(); + let info: PlaybackInformation = score.tracks[0].playbackInfo; + let note1: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0]; + let note2: Note = score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[1]; + let expectedEvents: FlatMidiEvent[] = [ + // channel init + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.VolumeCoarse, 120), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.PanCoarse, 64), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterFine, 0), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.RegisteredParameterCourse, 0), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryFine, 0), + new ControlChangeEvent(0, 0, info.primaryChannel, ControllerType.DataEntryCoarse, 16), + new ProgramChangeEvent(0, 0, info.primaryChannel, info.program), + + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.VolumeCoarse, 120), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.PanCoarse, 64), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.ExpressionControllerCoarse, 127), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterFine, 0), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.RegisteredParameterCourse, 0), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryFine, 0), + new ControlChangeEvent(0, 0, info.secondaryChannel, ControllerType.DataEntryCoarse, 16), + new ProgramChangeEvent(0, 0, info.secondaryChannel, info.program), + + new TimeSignatureEvent(0, 4, 4), + new TempoEvent(0, 120), + + // bend effect (note 1) + new BendEvent(0, 0, info.secondaryChannel, 8192), // no bend + new NoteBendEvent(0, 0, info.secondaryChannel, note1.realValue, 8192), + new NoteBendEvent(1 * 80, 0, info.secondaryChannel, note1.realValue, 8277), + new NoteBendEvent(2 * 80, 0, info.secondaryChannel, note1.realValue, 8363), + new NoteBendEvent(3 * 80, 0, info.secondaryChannel, note1.realValue, 8448), + new NoteBendEvent(4 * 80, 0, info.secondaryChannel, note1.realValue, 8533), + new NoteBendEvent(5 * 80, 0, info.secondaryChannel, note1.realValue, 8619), + new NoteBendEvent(6 * 80, 0, info.secondaryChannel, note1.realValue, 8704), + new NoteBendEvent(7 * 80, 0, info.secondaryChannel, note1.realValue, 8789), + new NoteBendEvent(8 * 80, 0, info.secondaryChannel, note1.realValue, 8875), + new NoteBendEvent(9 * 80, 0, info.secondaryChannel, note1.realValue, 8960), + new NoteBendEvent(10 * 80, 0, info.secondaryChannel, note1.realValue, 9045), + new NoteBendEvent(11 * 80, 0, info.secondaryChannel, note1.realValue, 9131), + + // note itself + new NoteEvent( + 0, + 0, + info.secondaryChannel, + MidiUtils.toTicks(note1.beat.duration), + note1.realValue, + note1.dynamics + ), + + // bend effect (note 2) + new BendEvent(0, 0, info.secondaryChannel, 8192), // no bend + new NoteBendEvent(0, 0, info.secondaryChannel, note2.realValue, 8192), + new NoteBendEvent(1 * 40, 0, info.secondaryChannel, note2.realValue, 8277), + new NoteBendEvent(2 * 40, 0, info.secondaryChannel, note2.realValue, 8363), + new NoteBendEvent(3 * 40, 0, info.secondaryChannel, note2.realValue, 8448), + new NoteBendEvent(4 * 40, 0, info.secondaryChannel, note2.realValue, 8533), + new NoteBendEvent(5 * 40, 0, info.secondaryChannel, note2.realValue, 8619), + new NoteBendEvent(6 * 40, 0, info.secondaryChannel, note2.realValue, 8704), + new NoteBendEvent(7 * 40, 0, info.secondaryChannel, note2.realValue, 8789), + new NoteBendEvent(8 * 40, 0, info.secondaryChannel, note2.realValue, 8875), + new NoteBendEvent(9 * 40, 0, info.secondaryChannel, note2.realValue, 8960), + new NoteBendEvent(10 * 40, 0, info.secondaryChannel, note2.realValue, 9045), + new NoteBendEvent(11 * 40, 0, info.secondaryChannel, note2.realValue, 9131), + new NoteBendEvent(12 * 40, 0, info.secondaryChannel, note2.realValue, 9216), + new NoteBendEvent(13 * 40, 0, info.secondaryChannel, note2.realValue, 9301), + new NoteBendEvent(14 * 40, 0, info.secondaryChannel, note2.realValue, 9387), + new NoteBendEvent(15 * 40, 0, info.secondaryChannel, note2.realValue, 9472), + new NoteBendEvent(16 * 40, 0, info.secondaryChannel, note2.realValue, 9557), + new NoteBendEvent(17 * 40, 0, info.secondaryChannel, note2.realValue, 9643), + new NoteBendEvent(18 * 40, 0, info.secondaryChannel, note2.realValue, 9728), + new NoteBendEvent(19 * 40, 0, info.secondaryChannel, note2.realValue, 9813), + new NoteBendEvent(20 * 40, 0, info.secondaryChannel, note2.realValue, 9899), + new NoteBendEvent(21 * 40, 0, info.secondaryChannel, note2.realValue, 9984), + new NoteBendEvent(22 * 40, 0, info.secondaryChannel, note2.realValue, 10069), + new NoteBendEvent(23 * 40, 0, info.secondaryChannel, note2.realValue, 10155), + + // note itself + new NoteEvent( + 0, + 0, + info.secondaryChannel, + MidiUtils.toTicks(note2.beat.duration), + note2.realValue, + note2.dynamics + ), + + // reset bend + new BendEvent(960, 0, info.primaryChannel, 8192), + new NoteEvent( + 960, + 0, + info.primaryChannel, + MidiUtils.toTicks(note1.beat.duration), + note1.realValue, + note1.dynamics + ), + + // end of track + new TrackEndEvent(3840, 0) // 3840 = end of bar + ]; + for (let i: number = 0; i < handler.midiEvents.length; i++) { + Logger.info('Test', `i[${i}] ${handler.midiEvents[i]}`); + if (i < expectedEvents.length) { + expect(expectedEvents[i].equals(handler.midiEvents[i])) + .withContext(`i[${i}] expected[${expectedEvents[i]}] !== actual[${handler.midiEvents[i]}]`) + .toEqual( + true, + ); + } + } + expect(handler.midiEvents.length).toEqual(expectedEvents.length); + }); + });