diff --git a/src.compiler/csharp/CSharpAstPrinter.ts b/src.compiler/csharp/CSharpAstPrinter.ts index 3259e61c5..69f306e94 100644 --- a/src.compiler/csharp/CSharpAstPrinter.ts +++ b/src.compiler/csharp/CSharpAstPrinter.ts @@ -561,9 +561,6 @@ export default class CSharpAstPrinter { } private writeType(type: cs.TypeNode, forNew: boolean = false, asNativeArray: boolean = false, forTypeConstraint: boolean = false) { - if (!type) { - console.log('ERR'); - } switch (type.nodeType) { case cs.SyntaxKind.PrimitiveTypeNode: if (forTypeConstraint) { diff --git a/src.compiler/csharp/CSharpAstTransformer.ts b/src.compiler/csharp/CSharpAstTransformer.ts index 708e4dc58..e7d18a3ea 100644 --- a/src.compiler/csharp/CSharpAstTransformer.ts +++ b/src.compiler/csharp/CSharpAstTransformer.ts @@ -453,7 +453,11 @@ export default class CSharpAstTransformer { parent: parent } as cs.UnresolvedTypeNode; - const typeArguments = (tsType as ts.TypeReference)?.typeArguments; + let typeArguments = (tsType as ts.TypeReference)?.typeArguments; + if(tsType && !typeArguments) { + const nonNullable = this._context.typeChecker.getNonNullableType(tsType); + typeArguments = (nonNullable as ts.TypeReference)?.typeArguments; + } if (typeArguments) { unresolved.typeArguments = typeArguments.map(a => this.createUnresolvedTypeNode(parent, tsNode, a)); } diff --git a/src.compiler/csharp/CSharpEmitterContext.ts b/src.compiler/csharp/CSharpEmitterContext.ts index db0a2895c..92c6055c7 100644 --- a/src.compiler/csharp/CSharpEmitterContext.ts +++ b/src.compiler/csharp/CSharpEmitterContext.ts @@ -262,7 +262,7 @@ export default class CSharpEmitterContext { return csType; } - csType = this.resolveUnionType(node, tsType); + csType = this.resolveUnionType(node, tsType, typeArguments); if (csType) { return csType; } @@ -493,7 +493,7 @@ export default class CSharpEmitterContext { } } - private resolveUnionType(parent: cs.Node, tsType: ts.Type): cs.TypeNode | null { + private resolveUnionType(parent: cs.Node, tsType: ts.Type, typeArguments?: cs.UnresolvedTypeNode[]): cs.TypeNode | null { if (!tsType.isUnion()) { return null; } @@ -562,7 +562,7 @@ export default class CSharpEmitterContext { if (!actualType) { return null; } - const type = this.getTypeFromTsType(parent, actualType); + const type = this.getTypeFromTsType(parent, actualType, undefined, typeArguments); return { nodeType: cs.SyntaxKind.TypeReference, parent: parent, diff --git a/src.csharp/AlphaTab/AlphaTab.csproj b/src.csharp/AlphaTab/AlphaTab.csproj index 89e3e356f..3bc5d3485 100644 --- a/src.csharp/AlphaTab/AlphaTab.csproj +++ b/src.csharp/AlphaTab/AlphaTab.csproj @@ -1,4 +1,4 @@ - + AlphaTab diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Iterable.cs b/src.csharp/AlphaTab/Core/EcmaScript/Iterable.cs new file mode 100644 index 000000000..4867b335c --- /dev/null +++ b/src.csharp/AlphaTab/Core/EcmaScript/Iterable.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AlphaTab.Core.EcmaScript +{ + public interface Iterable : IEnumerable + { + } +} diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Set.cs b/src.csharp/AlphaTab/Core/EcmaScript/Set.cs index 1a2aeb18d..09af796b8 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Set.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Set.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -7,7 +7,17 @@ namespace AlphaTab.Core.EcmaScript { public class Set : IEnumerable { - private readonly HashSet _data = new HashSet(); + private readonly HashSet _data; + + public Set() + { + _data = new HashSet(); + } + + public Set(IEnumerable values) + { + _data = new HashSet(values); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Uint8Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Uint8Array.cs index 8124b0b51..f0ce5bc7d 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Uint8Array.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Uint8Array.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -16,6 +16,11 @@ public class Uint8Array : IEnumerable, IEnumerable public ArraySegment Data => _data; + public Uint8Array(IList data) + { + _data = new ArraySegment(data.Select(d => (byte)d).ToArray()); + } + public Uint8Array(byte[] data) { _data = new ArraySegment(data); @@ -27,34 +32,34 @@ private Uint8Array(ArraySegment data) } public Uint8Array(double size) - : this(new byte[(int) size]) + : this(new byte[(int)size]) { } public Uint8Array(IEnumerable values) - : this(values.Select(d => (byte) d).ToArray()) + : this(values.Select(d => (byte)d).ToArray()) { } public double this[double index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data.Array[_data.Offset + (int) index]; + get => _data.Array[_data.Offset + (int)index]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => _data.Array[_data.Offset + (int) index] = (byte) value; + set => _data.Array[_data.Offset + (int)index] = (byte)value; } public Uint8Array Subarray(double begin, double end) { - return new Uint8Array(new ArraySegment(_data.Array, _data.Offset + (int) begin, - (int) (end - begin))); + return new Uint8Array(new ArraySegment(_data.Array, _data.Offset + (int)begin, + (int)(end - begin))); } public void Set(Uint8Array subarray, double pos) { var buffer = subarray.Buffer.Raw; - System.Buffer.BlockCopy(buffer.Array, (int) buffer.Offset, _data.Array, - _data.Offset + (int) pos, buffer.Count); + System.Buffer.BlockCopy(buffer.Array, (int)buffer.Offset, _data.Array, + _data.Offset + (int)pos, buffer.Count); } public static implicit operator Uint8Array(byte[] v) @@ -64,7 +69,7 @@ public static implicit operator Uint8Array(byte[] v) IEnumerator IEnumerable.GetEnumerator() { - return _data.Select(d => (double) d).GetEnumerator(); + return _data.Select(d => (double)d).GetEnumerator(); } public IEnumerator GetEnumerator() diff --git a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs index 27b7956cb..5b8bc7fc2 100644 --- a/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs +++ b/src.csharp/AlphaTab/Platform/CSharp/AlphaSynthWorkerApiBase.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using AlphaTab.Core.EcmaScript; using AlphaTab.Midi; using AlphaTab.Synth; @@ -34,6 +35,7 @@ protected void Initialize() Player.MidiLoaded.On(OnMidiLoaded); Player.MidiLoadFailed.On(OnMidiLoadFailed); Player.ReadyForPlayback.On(OnReadyForPlayback); + Player.MidiEventsPlayed.On(OnMidiEventsPlayed); DispatchOnUiThread(OnReady); } @@ -65,6 +67,12 @@ public double CountInVolume set => DispatchOnWorkerThread(() => { Player.CountInVolume = value; }); } + public IList MidiEventsPlayedFilter + { + get => Player.MidiEventsPlayedFilter; + set => DispatchOnWorkerThread(() => { Player.MidiEventsPlayedFilter = value; }); + } + public double MetronomeVolume { get => Player.MetronomeVolume; @@ -172,10 +180,12 @@ public void SetChannelVolume(double channel, double volume) public IEventEmitter Finished { get; } = new EventEmitter(); public IEventEmitter SoundFontLoaded { get; } = new EventEmitter(); public IEventEmitterOfT SoundFontLoadFailed { get; } =new EventEmitterOfT(); + public IEventEmitterOfT MidiLoad { get; } = new EventEmitterOfT(); public IEventEmitterOfT MidiLoaded { get; } = new EventEmitterOfT(); public IEventEmitterOfT MidiLoadFailed { get; } = new EventEmitterOfT(); public IEventEmitterOfT StateChanged { get; } = new EventEmitterOfT(); public IEventEmitterOfT PositionChanged { get; } = new EventEmitterOfT(); + public IEventEmitterOfT MidiEventsPlayed { get; } = new EventEmitterOfT(); protected virtual void OnReady() { @@ -212,6 +222,11 @@ protected virtual void OnMidiLoadFailed(Error e) DispatchOnUiThread(() => ((EventEmitterOfT)MidiLoadFailed).Trigger(e)); } + protected virtual void OnMidiEventsPlayed(MidiEventsPlayedEventArgs e) + { + DispatchOnUiThread(() => ((EventEmitterOfT)MidiEventsPlayed).Trigger(e)); + } + protected virtual void OnStateChanged(PlayerStateChangedEventArgs obj) { DispatchOnUiThread(() => ((EventEmitterOfT)StateChanged).Trigger(obj)); diff --git a/src/AlphaTabApiBase.ts b/src/AlphaTabApiBase.ts index 3e53fd0dc..1af18fddd 100644 --- a/src/AlphaTabApiBase.ts +++ b/src/AlphaTabApiBase.ts @@ -41,6 +41,8 @@ import { Logger } from '@src/Logger'; import { ModelUtils } from '@src/model/ModelUtils'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { Note } from './model/Note'; +import { MidiEventType } from './midi/MidiEvent'; +import { MidiEventsPlayedEventArgs } from './synth/MidiEventsPlayedEventArgs'; class SelectionInfo { public beat: Beat; @@ -435,6 +437,19 @@ export class AlphaTabApiBase { } } + public get midiEventsPlayedFilter(): MidiEventType[] { + if (!this.player) { + return []; + } + return this.player.midiEventsPlayedFilter; + } + + public set midiEventsPlayedFilter(value: MidiEventType[]) { + if (this.player) { + this.player.midiEventsPlayedFilter = value; + } + } + public get tickPosition(): number { if (!this.player) { return 0; @@ -543,6 +558,7 @@ export class AlphaTabApiBase { }); this.player.stateChanged.on(this.onPlayerStateChanged.bind(this)); this.player.positionChanged.on(this.onPlayerPositionChanged.bind(this)); + this.player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this)); this.player.finished.on(this.onPlayerFinished.bind(this)); if (this.settings.player.enableCursor) { this.setupCursors(); @@ -561,6 +577,7 @@ export class AlphaTabApiBase { let generator: MidiFileGenerator = new MidiFileGenerator(this.score, this.settings, handler); generator.generate(); this._tickCache = generator.tickLookup; + this.onMidiLoad(midiFile); this.player.loadMidiFile(midiFile); } @@ -1221,6 +1238,12 @@ export class AlphaTabApiBase { this.uiFacade.triggerEvent(this.container, 'soundFontLoaded', null); } + public midiLoad: IEventEmitterOfT = new EventEmitterOfT(); + private onMidiLoad(e:MidiFile): void { + (this.midiLoad as EventEmitterOfT).trigger(e); + this.uiFacade.triggerEvent(this.container, 'midiLoad', e); + } + public midiLoaded: IEventEmitterOfT = new EventEmitterOfT(); private onMidiLoaded(e:PositionChangedEventArgs): void { (this.midiLoaded as EventEmitterOfT).trigger(e); @@ -1242,4 +1265,12 @@ export class AlphaTabApiBase { (this.playerPositionChanged as EventEmitterOfT).trigger(e); this.uiFacade.triggerEvent(this.container, 'playerPositionChanged', e); } + + public midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT< + MidiEventsPlayedEventArgs + >(); + private onMidiEventsPlayed(e: MidiEventsPlayedEventArgs): void { + (this.midiEventsPlayed as EventEmitterOfT).trigger(e); + this.uiFacade.triggerEvent(this.container, 'midiEventsPlayed', e); + } } diff --git a/src/alphatab.ts b/src/alphatab.ts index fa5f13efc..a05e51973 100644 --- a/src/alphatab.ts +++ b/src/alphatab.ts @@ -71,6 +71,7 @@ import { MetaDataEvent } from '@src/midi/MetaDataEvent'; import { MetaEvent, MetaEventType } from '@src/midi/MetaEvent'; import { MetaNumberEvent } from '@src/midi/MetaNumberEvent'; import { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; +import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20PerNotePitchBendEvent'; import { SystemCommonEvent, SystemCommonType } from '@src/midi/SystemCommonEvent'; import { SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent'; import { MidiFileGenerator } from '@src/midi/MidiFileGenerator'; @@ -89,6 +90,7 @@ export const midi = { MetaNumberEvent, MidiEvent, MidiEventType, + Midi20PerNotePitchBendEvent, SystemCommonEvent, SystemCommonType, SystemExclusiveEvent, diff --git a/src/io/IOHelper.ts b/src/io/IOHelper.ts index 7a4083ca4..45188f4f9 100644 --- a/src/io/IOHelper.ts +++ b/src/io/IOHelper.ts @@ -27,6 +27,14 @@ export class IOHelper { return (ch4 << 24) | (ch3 << 16) | (ch2 << 8) | ch1; } + public static decodeUInt32LE(data: Uint8Array, index: number): number { + let ch1: number = data[index]; + let ch2: number = data[index + 1]; + let ch3: number = data[index + 2]; + let ch4: number = data[index + 3]; + return (ch4 << 24) | (ch3 << 16) | (ch2 << 8) | ch1; + } + public static readUInt16LE(input: IReadable): number { let ch1: number = input.readByte(); let ch2: number = input.readByte(); diff --git a/src/midi/AlphaSynthMidiFileHandler.ts b/src/midi/AlphaSynthMidiFileHandler.ts index 02619d1c2..ac60e1a60 100644 --- a/src/midi/AlphaSynthMidiFileHandler.ts +++ b/src/midi/AlphaSynthMidiFileHandler.ts @@ -3,13 +3,13 @@ import { MetaEventType } from '@src/midi/MetaEvent'; import { MetaNumberEvent } from '@src/midi/MetaNumberEvent'; import { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; import { SystemCommonType } from '@src/midi/SystemCommonEvent'; -import { SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent'; +import { AlphaTabSystemExclusiveEvents, SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent'; import { IMidiFileHandler } from '@src/midi/IMidiFileHandler'; 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'; +import { Midi20PerNotePitchBendEvent } from './Midi20PerNotePitchBendEvent'; /** * This implementation of the {@link IMidiFileHandler} @@ -33,6 +33,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { denominatorIndex++; } const message: MetaDataEvent = new MetaDataEvent( + 0, tick, 0xff, MetaEventType.TimeSignature, @@ -43,10 +44,11 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { public addRest(track: number, tick: number, channel: number): void { const message: SystemExclusiveEvent = new SystemExclusiveEvent( + track, tick, SystemCommonType.SystemExclusive, - 0, - new Uint8Array([0xff]) + SystemExclusiveEvent.AlphaTabManufacturerId, + new Uint8Array([AlphaTabSystemExclusiveEvents.Rest]) ); this._midiFile.addEvent(message); } @@ -61,6 +63,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { ): void { const velocity: number = MidiUtils.dynamicToVelocity(dynamicValue); const noteOn: MidiEvent = new MidiEvent( + track, start, this.makeCommand(MidiEventType.NoteOn, channel), AlphaSynthMidiFileHandler.fixValue(key), @@ -68,6 +71,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { ); this._midiFile.addEvent(noteOn); const noteOff: MidiEvent = new MidiEvent( + track, start + length, this.makeCommand(MidiEventType.NoteOff, channel), AlphaSynthMidiFileHandler.fixValue(key), @@ -92,6 +96,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { public addControlChange(track: number, tick: number, channel: number, controller: number, value: number): void { const message: MidiEvent = new MidiEvent( + track, tick, this.makeCommand(MidiEventType.Controller, channel), AlphaSynthMidiFileHandler.fixValue(controller), @@ -102,6 +107,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { public addProgramChange(track: number, tick: number, channel: number, program: number): void { const message: MidiEvent = new MidiEvent( + track, tick, this.makeCommand(MidiEventType.ProgramChange, channel), AlphaSynthMidiFileHandler.fixValue(program), @@ -113,7 +119,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { public addTempo(tick: number, tempo: number): void { // bpm -> microsecond per quarter note const tempoInUsq: number = (60000000 / tempo) | 0; - const message: MetaNumberEvent = new MetaNumberEvent(tick, 0xff, MetaEventType.Tempo, tempoInUsq); + const message: MetaNumberEvent = new MetaNumberEvent(0, tick, 0xff, MetaEventType.Tempo, tempoInUsq); this._midiFile.addEvent(message); } @@ -125,9 +131,10 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } const message: MidiEvent = new MidiEvent( + track, tick, this.makeCommand(MidiEventType.PitchBend, channel), - value & 0x7F, + value & 0x7F, (value >> 7) & 0x7F ); this._midiFile.addEvent(message); @@ -145,6 +152,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { value = value * SynthConstants.MaxPitchWheel20 / SynthConstants.MaxPitchWheel const message = new Midi20PerNotePitchBendEvent( + track, tick, this.makeCommand(MidiEventType.PerNotePitchBend, channel), key, @@ -154,7 +162,7 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler { } public finishTrack(track: number, tick: number): void { - const message: MetaDataEvent = new MetaDataEvent(tick, 0xff, MetaEventType.EndOfTrack, new Uint8Array(0)); + const message: MetaDataEvent = new MetaDataEvent(track, tick, 0xff, MetaEventType.EndOfTrack, new Uint8Array(0)); this._midiFile.addEvent(message); } } diff --git a/src/midi/MetaDataEvent.ts b/src/midi/MetaDataEvent.ts index cb21c86ff..0f3ffec50 100644 --- a/src/midi/MetaDataEvent.ts +++ b/src/midi/MetaDataEvent.ts @@ -5,14 +5,14 @@ import { IWriteable } from '@src/io/IWriteable'; export class MetaDataEvent extends MetaEvent { public data: Uint8Array; - public constructor(delta: number, status: number, metaId: number, data: Uint8Array) { - super(delta, status, metaId, 0); + public constructor(track:number, delta: number, status: number, metaId: number, data: Uint8Array) { + super(track, delta, status, metaId, 0); this.data = data; } public writeTo(s: IWriteable): void { s.writeByte(0xff); - s.writeByte(this.metaStatus); + s.writeByte(this.metaStatus as number); let l: number = this.data.length; MidiFile.writeVariableInt(s, l); s.write(this.data, 0, this.data.length); diff --git a/src/midi/MetaEvent.ts b/src/midi/MetaEvent.ts index 1017bca8d..ff6240788 100644 --- a/src/midi/MetaEvent.ts +++ b/src/midi/MetaEvent.ts @@ -30,11 +30,11 @@ export class MetaEvent extends MidiEvent { return (this.message & 0x00000ff) as MidiEventType; } - public get metaStatus(): number { - return this.data1; + public get metaStatus(): MetaEventType { + return this.data1 as MetaEventType; } - protected constructor(delta: number, status: number, data1: number, data2: number) { - super(delta, status, data1, data2); + protected constructor(track: number, delta: number, status: number, data1: number, data2: number) { + super(track, delta, status, data1, data2); } } diff --git a/src/midi/MetaNumberEvent.ts b/src/midi/MetaNumberEvent.ts index e757d7d35..ebe1af152 100644 --- a/src/midi/MetaNumberEvent.ts +++ b/src/midi/MetaNumberEvent.ts @@ -5,14 +5,14 @@ import { IWriteable } from '@src/io/IWriteable'; export class MetaNumberEvent extends MetaEvent { public value: number; - public constructor(delta: number, status: number, metaId: number, value: number) { - super(delta, status, metaId, 0); + public constructor(track:number, delta: number, status: number, metaId: number, value: number) { + super(track, delta, status, metaId, 0); this.value = value; } public writeTo(s: IWriteable): void { s.writeByte(0xff); - s.writeByte(this.metaStatus); + s.writeByte(this.metaStatus as number); MidiFile.writeVariableInt(s, 3); let b: Uint8Array = new Uint8Array([(this.value >> 16) & 0xff, (this.value >> 8) & 0xff, this.value & 0xff]); s.write(b, 0, b.length); diff --git a/src/midi/Midi20ChannelVoiceEvent.ts b/src/midi/Midi20PerNotePitchBendEvent.ts similarity index 86% rename from src/midi/Midi20ChannelVoiceEvent.ts rename to src/midi/Midi20PerNotePitchBendEvent.ts index 35c85b366..bebf7bb53 100644 --- a/src/midi/Midi20ChannelVoiceEvent.ts +++ b/src/midi/Midi20PerNotePitchBendEvent.ts @@ -9,8 +9,8 @@ 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); + public constructor(track:number, tick: number, status: number, noteKey: number, pitch: number) { + super(track, tick, status, 0, 0); this.noteKey = noteKey; this.pitch = pitch; } diff --git a/src/midi/MidiEvent.ts b/src/midi/MidiEvent.ts index b6c2bd0fc..bf6a8df7e 100644 --- a/src/midi/MidiEvent.ts +++ b/src/midi/MidiEvent.ts @@ -46,7 +46,17 @@ export enum MidiEventType { PitchBend = 0xE0, /** - * A meta event. See for details. + * A System Exclusive event. + */ + SystemExclusive = 0xF0, + + /** + * A System Exclusive event. + */ + SystemExclusive2 = 0xF7, + + /** + * A meta event. See `MetaEventType` for details. */ Meta = 0xFF } @@ -55,6 +65,11 @@ export enum MidiEventType { * Represents a midi event. */ export class MidiEvent { + /** + * Gets or sets the track to which the midi event belongs. + */ + public track:number; + /** * Gets or sets the raw midi message. */ @@ -93,12 +108,14 @@ export class MidiEvent { /** * Initializes a new instance of the {@link MidiEvent} class. - * @param tick The absolute midi ticks of this event.. + * @param track The track this event belongs to. + * @param tick The absolute midi ticks of this event. * @param status The status information of this event. * @param data1 The first data component of this midi event. * @param data2 The second data component of this midi event. */ - public constructor(tick: number, status: number, data1: number, data2: number) { + public constructor(track:number, tick: number, status: number, data1: number, data2: number) { + this.track = track; this.tick = tick; this.message = status | (data1 << 8) | (data2 << 16); } diff --git a/src/midi/SystemCommonEvent.ts b/src/midi/SystemCommonEvent.ts index fc14f51fc..d6ab24999 100644 --- a/src/midi/SystemCommonEvent.ts +++ b/src/midi/SystemCommonEvent.ts @@ -18,7 +18,7 @@ export class SystemCommonEvent extends MidiEvent { return (this.message & 0x00000ff) as MidiEventType; } - protected constructor(delta: number, status: number, data1: number, data2: number) { - super(delta, status, data1, data2); + protected constructor(track:number, delta: number, status: number, data1: number, data2: number) { + super(track, delta, status, data1, data2); } } diff --git a/src/midi/SystemExclusiveEvent.ts b/src/midi/SystemExclusiveEvent.ts index b5ea58d2d..c2cc603c9 100644 --- a/src/midi/SystemExclusiveEvent.ts +++ b/src/midi/SystemExclusiveEvent.ts @@ -1,15 +1,52 @@ import { SystemCommonEvent } from '@src/midi/SystemCommonEvent'; import { IWriteable } from '@src/io/IWriteable'; +import { ByteBuffer } from '@src/io/ByteBuffer'; +import { IOHelper } from '@src/io/IOHelper'; + +export enum AlphaTabSystemExclusiveEvents { + MetronomeTick = 0, + Rest = 1 +} export class SystemExclusiveEvent extends SystemCommonEvent { + public static readonly AlphaTabManufacturerId = 0x7D; + public data: Uint8Array; + public get isMetronome(): boolean { + return this.manufacturerId == SystemExclusiveEvent.AlphaTabManufacturerId && + this.data[0] == AlphaTabSystemExclusiveEvents.MetronomeTick; + } + + public get metronomeNumerator(): number { + return this.isMetronome ? this.data[1] : -1; + } + + public get metronomeDurationInTicks(): number { + if (!this.isMetronome) { + return -1; + } + return IOHelper.decodeUInt32LE(this.data, 2); + } + + public get metronomeDurationInMilliseconds(): number { + if (!this.isMetronome) { + return -1; + } + return IOHelper.decodeUInt32LE(this.data, 6); + } + + public get isRest(): boolean { + return this.manufacturerId == SystemExclusiveEvent.AlphaTabManufacturerId && + this.data[0] == AlphaTabSystemExclusiveEvents.Rest; + } + public get manufacturerId(): number { return this.message >> 8; } - public constructor(delta: number, status: number, id: number, data: Uint8Array) { - super(delta, status, id & 0x00ff, (id >> 8) & 0xff); + public constructor(track: number, delta: number, status: number, id: number, data: Uint8Array) { + super(track, delta, status, id & 0x00ff, (id >> 8) & 0xff); this.data = data; } @@ -21,4 +58,19 @@ export class SystemExclusiveEvent extends SystemCommonEvent { s.write(b, 0, b.length); s.writeByte(0xf7); } + + public static encodeMetronome(counter: number, durationInTicks: number, durationInMillis: number): Uint8Array { + // [0] type + // [1] counter + // [2-5] durationInTicks + // [6-9] durationInMillis + const data = ByteBuffer.withCapacity(2 + 2 * 4); + + data.writeByte(AlphaTabSystemExclusiveEvents.MetronomeTick); + data.writeByte(counter); + IOHelper.writeInt32LE(data, durationInTicks); + IOHelper.writeInt32LE(data, durationInMillis); + + return data.toArray(); + } } diff --git a/src/model/JsonConverter.ts b/src/model/JsonConverter.ts index 1d29d0a6a..3dcd50e72 100644 --- a/src/model/JsonConverter.ts +++ b/src/model/JsonConverter.ts @@ -5,7 +5,7 @@ import { SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent'; import { MidiFile } from '@src/midi/MidiFile'; import { Score } from '@src/model/Score'; import { Settings } from '@src/Settings'; -import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20ChannelVoiceEvent'; +import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20PerNotePitchBendEvent'; import { ScoreSerializer } from '@src/generated/model/ScoreSerializer'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; @@ -127,36 +127,45 @@ export class JsonConverter { midi2.division = midi.division; let midiEvents: any[] = midi.events; for (let midiEvent of midiEvents) { - let tick: number = midiEvent.tick; - let message: number = midiEvent.message; - let midiEvent2: MidiEvent; - switch (midiEvent.type) { - case 'SystemExclusiveEvent': - midiEvent2 = new SystemExclusiveEvent(tick, 0, 0, midiEvent.data); - midiEvent2.message = message; - break; - case 'MetaDataEvent': - midiEvent2 = new MetaDataEvent(tick, 0, 0, midiEvent.data); - midiEvent2.message = message; - break; - case 'MetaNumberEvent': - 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; - break; - } + let midiEvent2: MidiEvent = JsonConverter.jsObjectToMidiEvent(midiEvent); midi2.events.push(midiEvent2); } return midi2; } + /** + * @target web + */ + public static jsObjectToMidiEvent(midiEvent: any): MidiEvent { + let track: number = midiEvent.track; + let tick: number = midiEvent.tick; + let message: number = midiEvent.message; + let midiEvent2: MidiEvent; + switch (midiEvent.type) { + case 'SystemExclusiveEvent': + midiEvent2 = new SystemExclusiveEvent(track, tick, 0, 0, midiEvent.data); + midiEvent2.message = message; + break; + case 'MetaDataEvent': + midiEvent2 = new MetaDataEvent(track, tick, 0, 0, midiEvent.data); + midiEvent2.message = message; + break; + case 'MetaNumberEvent': + midiEvent2 = new MetaNumberEvent(track, tick, 0, 0, midiEvent.value); + midiEvent2.message = message; + break; + case 'Midi20PerNotePitchBendEvent': + midiEvent2 = new Midi20PerNotePitchBendEvent(track, tick, 0, midiEvent.noteKey, midiEvent.pitch); + midiEvent2.message = message; + break; + default: + midiEvent2 = new MidiEvent(track, tick, 0, 0, 0); + midiEvent2.message = message; + break; + } + return midiEvent2; + } + /** * @target web */ @@ -166,25 +175,33 @@ export class JsonConverter { let midiEvents: unknown[] = []; midi2.events = midiEvents; for (let midiEvent of midi.events) { - let midiEvent2: any = {} as any; - midiEvents.push(midiEvent2); - midiEvent2.tick = midiEvent.tick; - midiEvent2.message = midiEvent.message; - if (midiEvent instanceof SystemExclusiveEvent) { - midiEvent2.type = 'SystemExclusiveEvent'; - midiEvent2.data = midiEvent.data; - } else if (midiEvent instanceof MetaDataEvent) { - midiEvent2.type = 'MetaDataEvent'; - midiEvent2.data = midiEvent.data; - } 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; - } + midiEvents.push(JsonConverter.midiEventToJsObject(midiEvent)); } return midi2; } + + /** + * @target web + */ + public static midiEventToJsObject(midiEvent: MidiEvent): unknown { + let midiEvent2: any = {} as any; + midiEvent2.track = midiEvent.track; + midiEvent2.tick = midiEvent.tick; + midiEvent2.message = midiEvent.message; + if (midiEvent instanceof SystemExclusiveEvent) { + midiEvent2.type = 'SystemExclusiveEvent'; + midiEvent2.data = midiEvent.data; + } else if (midiEvent instanceof MetaDataEvent) { + midiEvent2.type = 'MetaDataEvent'; + midiEvent2.data = midiEvent.data; + } 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 midiEvent2; + } } diff --git a/src/platform/javascript/AlphaSynthWebWorker.ts b/src/platform/javascript/AlphaSynthWebWorker.ts index 09ba8db0a..858c037b5 100644 --- a/src/platform/javascript/AlphaSynthWebWorker.ts +++ b/src/platform/javascript/AlphaSynthWebWorker.ts @@ -6,6 +6,7 @@ import { AlphaSynthWorkerSynthOutput } from '@src/platform/javascript/AlphaSynth import { IWorkerScope } from '@src/platform/javascript/IWorkerScope'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; +import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; /** * This class implements a HTML5 WebWorker based version of alphaSynth @@ -30,6 +31,7 @@ export class AlphaSynthWebWorker { this._player.midiLoaded.on(this.onMidiLoaded.bind(this)); this._player.midiLoadFailed.on(this.onMidiLoadFailed.bind(this)); this._player.readyForPlayback.on(this.onReadyForPlayback.bind(this)); + this._player.midiEventsPlayed.on(this.onMidiEventsPlayed.bind(this)); this._main.postMessage({ cmd: 'alphaSynth.ready' }); @@ -80,6 +82,9 @@ export class AlphaSynthWebWorker { break; case 'alphaSynth.setCountInVolume': this._player.countInVolume = data.value; + break; + case 'alphaSynth.setMidiEventsPlayedFilter': + this._player.midiEventsPlayedFilter = data.value; break; case 'alphaSynth.play': this._player.play(); @@ -195,4 +200,11 @@ export class AlphaSynthWebWorker { cmd: 'alphaSynth.readyForPlayback' }); } + + public onMidiEventsPlayed(args: MidiEventsPlayedEventArgs): void { + this._main.postMessage({ + cmd: 'alphaSynth.midiEventsPlayed', + events: args.events.map(JsonConverter.midiEventToJsObject) + }); + } } diff --git a/src/platform/javascript/AlphaSynthWebWorkerApi.ts b/src/platform/javascript/AlphaSynthWebWorkerApi.ts index de9cd4a86..fed61cdc7 100644 --- a/src/platform/javascript/AlphaSynthWebWorkerApi.ts +++ b/src/platform/javascript/AlphaSynthWebWorkerApi.ts @@ -13,6 +13,8 @@ import { LogLevel } from '@src/LogLevel'; import { SynthConstants } from '@src/synth/SynthConstants'; import { ProgressEventArgs } from '@src/alphatab'; import { FileLoadError } from '@src/FileLoadError'; +import { MidiEventsPlayedEventArgs } from '@src/synth/MidiEventsPlayedEventArgs'; +import { MidiEventType } from '@src/midi/MidiEvent'; /** * a WebWorker based alphaSynth which uses the given player as output. @@ -33,6 +35,7 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { private _timePosition: number = 0; private _isLooping: boolean = false; private _playbackRange: PlaybackRange | null = null; + private _midiEventsPlayedFilter: MidiEventType[] = []; public get isReady(): boolean { return this._workerIsReady && this._outputIsReady; @@ -96,6 +99,18 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { }); } + public get midiEventsPlayedFilter(): MidiEventType[] { + return this._midiEventsPlayedFilter; + } + + public set midiEventsPlayedFilter(value: MidiEventType[]) { + this._midiEventsPlayedFilter = value; + this._synth.postMessage({ + cmd: 'alphaSynth.setMidiEventsPlayedFilter', + value: value + }) + } + public get playbackSpeed(): number { return this._playbackSpeed; } @@ -345,6 +360,11 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { new PositionChangedEventArgs(data.currentTime, data.endTime, data.currentTick, data.endTick, data.isSeek) ); break; + case 'alphaSynth.midiEventsPlayed': + (this.midiEventsPlayed as EventEmitterOfT).trigger( + new MidiEventsPlayedEventArgs((data.events as unknown[]).map(JsonConverter.jsObjectToMidiEvent)) + ); + break; case 'alphaSynth.playerStateChanged': this._state = data.state; (this.stateChanged as EventEmitterOfT).trigger( @@ -411,6 +431,9 @@ export class AlphaSynthWebWorkerApi implements IAlphaSynth { >(); readonly positionChanged: IEventEmitterOfT = new EventEmitterOfT< PositionChangedEventArgs + >(); + readonly midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT< + MidiEventsPlayedEventArgs >(); // diff --git a/src/platform/javascript/JQueryAlphaTab.ts b/src/platform/javascript/JQueryAlphaTab.ts index 3bc4ebf38..5890692f5 100644 --- a/src/platform/javascript/JQueryAlphaTab.ts +++ b/src/platform/javascript/JQueryAlphaTab.ts @@ -6,6 +6,7 @@ import { AlphaTabApi } from '@src/platform/javascript/AlphaTabApi'; import { IScoreRenderer } from '@src/rendering/IScoreRenderer'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; +import { MidiEventType } from '@src/midi/MidiEvent'; /** * @target web @@ -173,6 +174,13 @@ export class JQueryAlphaTab { return context.countInVolume; } + public midiEventsPlayedFilter(element: jQuery, context: AlphaTabApi, midiEventsPlayedFilter?: MidiEventType[]): MidiEventType[] { + if (Array.isArray(midiEventsPlayedFilter)) { + context.midiEventsPlayedFilter = midiEventsPlayedFilter; + } + return context.midiEventsPlayedFilter; + } + public playbackSpeed(element: jQuery, context: AlphaTabApi, playbackSpeed?: number): number { if (typeof playbackSpeed === 'number') { context.playbackSpeed = playbackSpeed; diff --git a/src/synth/AlphaSynth.ts b/src/synth/AlphaSynth.ts index d22aafe0d..55ed6e2fd 100644 --- a/src/synth/AlphaSynth.ts +++ b/src/synth/AlphaSynth.ts @@ -14,6 +14,10 @@ import { ByteBuffer } from '@src/io/ByteBuffer'; import { Logger } from '@src/Logger'; import { LogLevel } from '@src/LogLevel'; import { SynthConstants } from '@src/synth/SynthConstants'; +import { SynthEvent } from './synthesis/SynthEvent'; +import { Queue } from './ds/Queue'; +import { MidiEventsPlayedEventArgs } from './MidiEventsPlayedEventArgs'; +import { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; /** * This is the main synthesizer component which can be used to @@ -28,6 +32,8 @@ export class AlphaSynth implements IAlphaSynth { private _timePosition: number = 0; private _metronomeVolume: number = 0; private _countInVolume: number = 0; + private _playedEventsQueue: Queue = new Queue(); + private _midiEventsPlayedFilter: Set = new Set(); /** * Gets the {@link ISynthOutput} used for playing the generated samples. @@ -78,6 +84,14 @@ export class AlphaSynth implements IAlphaSynth { this._countInVolume = value; } + public get midiEventsPlayedFilter(): MidiEventType[] { + return Array.from(this._midiEventsPlayedFilter); + } + + public set midiEventsPlayedFilter(value: MidiEventType[]) { + this._midiEventsPlayedFilter = new Set(value); + } + public get playbackSpeed(): number { return this._sequencer.playbackSpeed; } @@ -162,9 +176,15 @@ export class AlphaSynth implements IAlphaSynth { for (let i = 0; i < SynthConstants.MicroBufferCount; i++) { // synthesize buffer this._sequencer.fillMidiEventQueue(); - this._synthesizer.synthesize(samples, bufferPos, SynthConstants.MicroBufferSize); + const synthesizedEvents = this._synthesizer.synthesize(samples, bufferPos, SynthConstants.MicroBufferSize); bufferPos += SynthConstants.MicroBufferSize * SynthConstants.AudioChannels; - + // push all processed events into the queue + // for informing users about played events + for (const e of synthesizedEvents) { + if (this._midiEventsPlayedFilter.has(e.event.command)) { + this._playedEventsQueue.enqueue(e); + } + } // tell sequencer to check whether its work is done if (this._sequencer.isFinished) { break; @@ -192,7 +212,7 @@ export class AlphaSynth implements IAlphaSynth { return false; } this.output.activate(); - + this.playInternal(); if (this._countInVolume > 0) { @@ -393,6 +413,21 @@ export class AlphaSynth implements IAlphaSynth { new PositionChangedEventArgs(currentTime, endTime, currentTick, endTick, isSeek) ); } + + // build events which were actually played + if (isSeek) { + this._playedEventsQueue.clear(); + } else { + const playedEvents = new Queue(); + while (!this._playedEventsQueue.isEmpty && this._playedEventsQueue.peek().time < currentTime) { + const synthEvent = this._playedEventsQueue.dequeue(); + playedEvents.enqueue(synthEvent.event); + } + if (!playedEvents.isEmpty) { + (this.midiEventsPlayed as EventEmitterOfT).trigger(new MidiEventsPlayedEventArgs(playedEvents.toArray())) + } + } + } readonly ready: IEventEmitter = new EventEmitter(); @@ -408,4 +443,5 @@ export class AlphaSynth implements IAlphaSynth { readonly positionChanged: IEventEmitterOfT = new EventEmitterOfT< PositionChangedEventArgs >(); + readonly midiEventsPlayed: IEventEmitterOfT = new EventEmitterOfT(); } diff --git a/src/synth/IAlphaSynth.ts b/src/synth/IAlphaSynth.ts index bd3f9f515..406b61a3b 100644 --- a/src/synth/IAlphaSynth.ts +++ b/src/synth/IAlphaSynth.ts @@ -5,6 +5,8 @@ import { PlayerStateChangedEventArgs } from '@src/synth/PlayerStateChangedEventA import { PositionChangedEventArgs } from '@src/synth/PositionChangedEventArgs'; import { IEventEmitter, IEventEmitterOfT } from '@src/EventEmitter'; import { LogLevel } from '@src/LogLevel'; +import { MidiEventsPlayedEventArgs } from './MidiEventsPlayedEventArgs'; +import { MidiEventType } from '@src/midi/MidiEvent'; /** * The public API interface for interacting with the synthesizer. @@ -71,6 +73,12 @@ export interface IAlphaSynth { */ countInVolume: number; + /** + * Gets or sets the midi events which will trigger the `midiEventsPlayed` event. + * To subscribe to Metronome events use the `SystemExclusiveEvent2` event type and check against `event.isMetronome` + */ + midiEventsPlayedFilter: MidiEventType[]; + /** * Destroys the synthesizer and all related components */ @@ -191,4 +199,9 @@ export interface IAlphaSynth { * This event is fired when the current playback position of/ the song changed. */ readonly positionChanged: IEventEmitterOfT; + + /** + * The event is fired when certain midi events were sent to the audio output device for playback. + */ + readonly midiEventsPlayed: IEventEmitterOfT; } diff --git a/src/synth/MidiEventsPlayedEventArgs.ts b/src/synth/MidiEventsPlayedEventArgs.ts new file mode 100644 index 000000000..601f55dce --- /dev/null +++ b/src/synth/MidiEventsPlayedEventArgs.ts @@ -0,0 +1,19 @@ +import { MidiEvent } from "@src/midi/MidiEvent"; + +/** + * Represents the info when the synthesizer played certain midi events. + */ +export class MidiEventsPlayedEventArgs { + /** + * Gets the events which were played. + */ + public readonly events: MidiEvent[]; + + /** + * Initializes a new instance of the {@link MidiEventsPlayedEventArgs} class. + * @param events The events which were played. + */ + public constructor(events: MidiEvent[]) { + this.events = events; + } +} \ No newline at end of file diff --git a/src/synth/MidiFileSequencer.ts b/src/synth/MidiFileSequencer.ts index b666b5c3d..79c2a955d 100644 --- a/src/synth/MidiFileSequencer.ts +++ b/src/synth/MidiFileSequencer.ts @@ -170,7 +170,9 @@ export class MidiFileSequencer { let absTick: number = 0; let absTime: number = 0.0; - let metronomeLength: number = 0; + let metronomeCount: number = 0; + let metronomeLengthInTicks: number = 0; + let metronomeLengthInMillis: number = 0; let metronomeTick: number = 0; let metronomeTime: number = 0.0; @@ -185,13 +187,18 @@ export class MidiFileSequencer { synthData.time = absTime; previousTick = mEvent.tick; - if (metronomeLength > 0) { + if (metronomeLengthInTicks > 0) { while (metronomeTick < absTick) { - let metronome: SynthEvent = SynthEvent.newMetronomeEvent(state.synthData.length); + let metronome: SynthEvent = SynthEvent.newMetronomeEvent(state.synthData.length, + metronomeTick, + Math.floor(metronomeTick / metronomeLengthInTicks) % metronomeCount, + metronomeLengthInTicks, + metronomeLengthInMillis + ); state.synthData.push(metronome); metronome.time = metronomeTime; - metronomeTick += metronomeLength; - metronomeTime += metronomeLength * (60000.0 / (bpm * midiFile.division)); + metronomeTick += metronomeLengthInTicks; + metronomeTime += metronomeLengthInMillis; } } @@ -199,10 +206,13 @@ export class MidiFileSequencer { let meta: MetaNumberEvent = mEvent as MetaNumberEvent; bpm = 60000000 / meta.value; state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, absTick, absTime)); + metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division)) } else if (mEvent.command === MidiEventType.Meta && mEvent.data1 === MetaEventType.TimeSignature) { let meta: MetaDataEvent = mEvent as MetaDataEvent; let timeSignatureDenominator: number = Math.pow(2, meta.data[1]); - metronomeLength = (state.division * (4.0 / timeSignatureDenominator)) | 0; + metronomeCount = meta.data[0]; + metronomeLengthInTicks = (state.division * (4.0 / timeSignatureDenominator)) | 0; + metronomeLengthInMillis = metronomeLengthInTicks * (60000.0 / (bpm * midiFile.division)) if (state.firstTimeSignatureDenominator === 0) { state.firstTimeSignatureNumerator = meta.data[0]; state.firstTimeSignatureDenominator = timeSignatureDenominator; @@ -356,10 +366,10 @@ export class MidiFileSequencer { const state = new MidiSequencerState(); state.division = this._mainState.division; - let bpm :number = 120; + let bpm: number = 120; let timeSignatureNumerator = 4; let timeSignatureDenominator = 4; - if(this._mainState.eventIndex === 0) { + if (this._mainState.eventIndex === 0) { bpm = this._mainState.tempoChanges[0].bpm; timeSignatureNumerator = this._mainState.firstTimeSignatureNumerator; timeSignatureDenominator = this._mainState.firstTimeSignatureDenominator; @@ -371,16 +381,23 @@ export class MidiFileSequencer { state.tempoChanges.push(new MidiFileSequencerTempoChange(bpm, 0, 0)); - let metronomeLength: number = (state.division * (4.0 / timeSignatureDenominator)) | 0; + let metronomeLengthInTicks: number = (state.division * (4.0 / timeSignatureDenominator)) | 0; + let metronomeLengthInMillis: number = metronomeLengthInTicks * (60000.0 / (bpm * this._mainState.division)); let metronomeTick: number = 0; let metronomeTime: number = 0.0; for (let i = 0; i < timeSignatureNumerator; i++) { - let metronome: SynthEvent = SynthEvent.newMetronomeEvent(state.synthData.length); + let metronome: SynthEvent = SynthEvent.newMetronomeEvent( + state.synthData.length, + metronomeTick, + i, + metronomeLengthInTicks, + metronomeLengthInMillis + ); state.synthData.push(metronome); metronome.time = metronomeTime; - metronomeTick += metronomeLength; - metronomeTime += metronomeLength * (60000.0 / (bpm * this._mainState.division)); + metronomeTick += metronomeLengthInTicks; + metronomeTime += metronomeLengthInMillis; } state.synthData.sort((a, b) => { diff --git a/src/synth/ds/Queue.ts b/src/synth/ds/Queue.ts new file mode 100644 index 000000000..542187614 --- /dev/null +++ b/src/synth/ds/Queue.ts @@ -0,0 +1,38 @@ +export class Queue { + private _items: T[] = []; + private _position: number = 0; + + public isEmpty: boolean = true; + + public clear() { + this._items = []; + this._position = 0; + this.isEmpty = true; + } + + public enqueue(item: T) { + this.isEmpty = false; + this._items.push(item); + } + + public peek(): T { + return this._items[this._position]; + } + + public dequeue(): T { + const item = this._items[this._position]; + this._position++; + if (this._position >= this._items.length / 2) { + this._items = this._items.slice(this._position); + this._position = 0; + } + this.isEmpty = this._items.length == 0; + return item; + } + + public toArray(): T[] { + const items = this._items.slice(this._position); + items.reverse(); + return items; + } +} diff --git a/src/synth/synthesis/SynthEvent.ts b/src/synth/synthesis/SynthEvent.ts index dd83650c3..3c794fb77 100644 --- a/src/synth/synthesis/SynthEvent.ts +++ b/src/synth/synthesis/SynthEvent.ts @@ -2,22 +2,29 @@ // developed by Bernhard Schelling (https://github.com/schellingb/TinySoundFont) // TypeScript port for alphaTab: (C) 2020 by Daniel Kuschny // Licensed under: MPL-2.0 -import { MidiEvent } from '@src/midi/MidiEvent'; +import { MidiEvent, MidiEventType } from '@src/midi/MidiEvent'; +import { SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent'; export class SynthEvent { public eventIndex: number; - public event: MidiEvent | null; - public isMetronome: boolean = false; + public event: MidiEvent; + public readonly isMetronome: boolean; public time: number = 0; - public constructor(eventIndex: number, e: MidiEvent | null) { + public constructor(eventIndex: number, e: MidiEvent) { this.eventIndex = eventIndex; this.event = e; + this.isMetronome = this.event instanceof SystemExclusiveEvent && (this.event as SystemExclusiveEvent).isMetronome; } - public static newMetronomeEvent(eventIndex: number): SynthEvent { - const x: SynthEvent = new SynthEvent(eventIndex, null); - x.isMetronome = true; + + public static newMetronomeEvent(eventIndex: number, tick: number, counter: number, durationInTicks: number, durationInMillis: number): SynthEvent { + const evt = new SystemExclusiveEvent(0, tick, + MidiEventType.SystemExclusive2, + SystemExclusiveEvent.AlphaTabManufacturerId, + SystemExclusiveEvent.encodeMetronome(counter, durationInTicks, durationInMillis) + ); + const x: SynthEvent = new SynthEvent(eventIndex, evt); return x; } } diff --git a/src/synth/synthesis/TinySoundFont.ts b/src/synth/synthesis/TinySoundFont.ts index 5916c4f2f..4781d9c6c 100644 --- a/src/synth/synthesis/TinySoundFont.ts +++ b/src/synth/synthesis/TinySoundFont.ts @@ -26,10 +26,11 @@ 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'; +import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20PerNotePitchBendEvent'; import { MetaEventType } from '@src/midi/MetaEvent'; import { MetaNumberEvent } from '@src/midi/MetaNumberEvent'; import { MetaDataEvent } from '@src/midi/MetaDataEvent'; +import { Queue } from '../ds/Queue'; /** * This is a tiny soundfont based synthesizer. @@ -39,22 +40,21 @@ import { MetaDataEvent } from '@src/midi/MetaDataEvent'; * - Support for modulators */ export class TinySoundFont { - private _midiEventQueue: SynthEvent[] = []; - private _midiEventCount: number = 0; + private _midiEventQueue: Queue = new Queue(); private _mutedChannels: Map = new Map(); private _soloChannels: Map = new Map(); private _isAnySolo: boolean = false; - public currentTempo:number = 0; - public timeSignatureNumerator:number = 0; - public timeSignatureDenominator:number = 0; + public currentTempo: number = 0; + public timeSignatureNumerator: number = 0; + public timeSignatureDenominator: number = 0; public constructor(sampleRate: number) { this.outSampleRate = sampleRate; } - public synthesize(buffer: Float32Array, bufferPos: number, sampleCount: number) { - this.fillWorkingBuffer(buffer, bufferPos, sampleCount); + public synthesize(buffer: Float32Array, bufferPos: number, sampleCount: number): SynthEvent[] { + return this.fillWorkingBuffer(buffer, bufferPos, sampleCount); } public synthesizeSilent(sampleCount: number): void { @@ -101,29 +101,27 @@ export class TinySoundFont { } public dispatchEvent(synthEvent: SynthEvent): void { - this._midiEventQueue.unshift(synthEvent); - this._midiEventCount++; + this._midiEventQueue.enqueue(synthEvent); } - private fillWorkingBuffer(buffer: Float32Array | null, bufferPos: number, sampleCount: number) { + private fillWorkingBuffer(buffer: Float32Array | null, bufferPos: number, sampleCount: number): SynthEvent[] { // Break the process loop into sections representing the smallest timeframe before the midi controls need to be updated // the bigger the timeframe the more efficent the process is, but playback quality will be reduced. const anySolo: boolean = this._isAnySolo; + const processedEvents: SynthEvent[] = []; + // process in micro-buffers // process events for first microbuffer - if (this._midiEventQueue.length > 0) { - for (let i: number = 0; i < this._midiEventCount; i++) { - let m: SynthEvent | undefined = this._midiEventQueue.pop(); - if (m) { - if (m.isMetronome && this.metronomeVolume > 0) { - this.channelNoteOff(SynthConstants.MetronomeChannel, 33); - this.channelNoteOn(SynthConstants.MetronomeChannel, 33, 95 / 127); - } else if (m.event) { - this.processMidiMessage(m.event); - } - } + while (!this._midiEventQueue.isEmpty) { + let m: SynthEvent = this._midiEventQueue.dequeue(); + if (m.isMetronome && this.metronomeVolume > 0) { + this.channelNoteOff(SynthConstants.MetronomeChannel, 33); + this.channelNoteOn(SynthConstants.MetronomeChannel, 33, 95 / 127); + } else if (m.event) { + this.processMidiMessage(m.event); } + processedEvents.push(m); } // voice processing loop @@ -143,7 +141,7 @@ export class TinySoundFont { } } - this._midiEventCount = 0; + return processedEvents; } private processMidiMessage(e: MidiEvent): void { @@ -185,8 +183,8 @@ export class TinySoundFont { this.currentTempo = 60000000 / (e as MetaNumberEvent).value; break; case MetaEventType.TimeSignature: - this.timeSignatureNumerator = (e as MetaDataEvent).data[0]; - this.timeSignatureDenominator = Math.pow(2, (e as MetaDataEvent).data[1]); + this.timeSignatureNumerator = (e as MetaDataEvent).data[0]; + this.timeSignatureDenominator = Math.pow(2, (e as MetaDataEvent).data[1]); break; } break; diff --git a/test/audio/MidiFileGenerator.test.ts b/test/audio/MidiFileGenerator.test.ts index 24befbd2f..8bb2daa23 100644 --- a/test/audio/MidiFileGenerator.test.ts +++ b/test/audio/MidiFileGenerator.test.ts @@ -47,11 +47,11 @@ describe('MidiFileGeneratorTest', () => { it('midi-order', () => { let midiFile: MidiFile = new MidiFile(); - midiFile.addEvent(new MidiEvent(0, 0, 0, 0)); - midiFile.addEvent(new MidiEvent(0, 0, 1, 0)); - midiFile.addEvent(new MidiEvent(100, 0, 2, 0)); - midiFile.addEvent(new MidiEvent(50, 0, 3, 0)); - midiFile.addEvent(new MidiEvent(50, 0, 4, 0)); + midiFile.addEvent(new MidiEvent(0, 0, 0, 0, 0)); + midiFile.addEvent(new MidiEvent(0, 0, 0, 1, 0)); + midiFile.addEvent(new MidiEvent(0, 100, 0, 2, 0)); + midiFile.addEvent(new MidiEvent(0, 50, 0, 3, 0)); + midiFile.addEvent(new MidiEvent(0, 50, 0, 4, 0)); expect(midiFile.events[0].data1).toEqual(0); expect(midiFile.events[1].data1).toEqual(1); expect(midiFile.events[2].data1).toEqual(3);