Skip to content

Commit

Permalink
Add SMF1.0 compliant MIDI file export (#1239)
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielku15 authored Aug 5, 2023
1 parent de959d4 commit 3ce2de2
Show file tree
Hide file tree
Showing 32 changed files with 1,527 additions and 1,053 deletions.
8 changes: 3 additions & 5 deletions src.csharp/AlphaTab.Test/VisualTests/VisualTestHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using AlphaTab.Core;
Expand Down Expand Up @@ -51,10 +52,7 @@ public static async Task RunVisualTestScoreWithResize(Score score, IList<double>
});
renderer.PartialRenderFinished.On(e =>
{
if (e != null)
{
results[^1].Add(e);
}
results[^1].Add(e);
});
renderer.RenderFinished.On(e =>
{
Expand Down Expand Up @@ -99,7 +97,7 @@ public static async Task RunVisualTestScoreWithResize(Score score, IList<double>
}
}

private static void PrepareSettingsForTest(ref Settings? settings)
private static void PrepareSettingsForTest([NotNull] ref Settings? settings)
{
settings ??= new Settings();
settings.Core.Engine = "skia";
Expand Down
12 changes: 8 additions & 4 deletions src.csharp/AlphaTab/Core/EcmaScript/Uint8Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public Uint8Array(IList<double> data)
_data = new ArraySegment<byte>(data.Select(d => (byte)d).ToArray());
}

public Uint8Array() : this(System.Array.Empty<byte>())
{
}

public Uint8Array(byte[] data)
{
_data = new ArraySegment<byte>(data);
Expand All @@ -45,21 +49,21 @@ public Uint8Array(IEnumerable<int> values)
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<byte>(_data.Array, _data.Offset + (int)begin,
return new Uint8Array(new ArraySegment<byte>(_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,
System.Buffer.BlockCopy(buffer.Array!, buffer.Offset, _data.Array!,
_data.Offset + (int)pos, buffer.Count);
}

Expand Down
18 changes: 18 additions & 0 deletions src.csharp/AlphaTab/Io/JsonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ internal static partial class JsonHelper
throw new AlphaTabError(AlphaTabErrorType.Format, $"Could not parse enum value '{o}' [({o.GetType()}]");
}

public static object? GetValue(object o, string key)
{
switch (o)
{
case IDictionary<string, object> d:
d.TryGetValue(key, out var v);
return v;
case IDictionary d:
if (d.Contains(key))
{
return d[key];
}
return null;
}

return null;
}

public static void ForEach(object o, Action<object, string> func)
{
switch (o)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class Uint8Array : Iterable<UByte> {
this.buffer = UByteArray(size.toInt())
}

public constructor() {
this.buffer = UByteArray(0)
}

public constructor(data: UByteArray) {
this.buffer = data
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ internal open class JsonHelperPartials {
func(kvp.value, (kvp.key!!) as String)
}
}

if (o is alphaTab.collections.Map<*, *>) {
for (kvp in o) {
func(kvp.value, (kvp.key!!) as String)
}
}
}

public fun forEach(o: Any?, func: (v: Any?, k: String) -> Unit) {
Expand All @@ -30,12 +24,17 @@ internal open class JsonHelperPartials {
func(kvp.value, (kvp.key!!) as String)
}
}
}

if (o is alphaTab.collections.Map<*, *>) {
for (kvp in o) {
func(kvp.value, (kvp.key!!) as String)
}
public fun getValue(o: Any?, k: String): Any? {
if (o is Map<*, *>) {
// NOTE: We know that we only have Map<String, Any?> in our serialization
// handling. we need this cast to satisfy Kotlin type checks.
@Suppress("UNCHECKED_CAST") val unsafeMap = o as Map<String, Any?>
return if (unsafeMap.has(k)) unsafeMap.get(k) else null
}

return null
}


Expand Down
4 changes: 4 additions & 0 deletions src/io/ByteBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,8 @@ export class ByteBuffer implements IWriteable, IReadable {
copy.set(this._buffer.subarray(0, 0 + this.length), 0);
return copy;
}

public copyTo(destination:IWriteable) {
destination.write(this._buffer, 0, this.length);
}
}
5 changes: 5 additions & 0 deletions src/io/IOHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,9 @@ export class IOHelper {
o.writeByte((v >> 0) & 0xff);
o.writeByte((v >> 8) & 0xff);
}

public static writeInt16BE(o: IWriteable, v: number) {
o.writeByte((v >> 8) & 0xff);
o.writeByte((v >> 0) & 0xff);
}
}
15 changes: 14 additions & 1 deletion src/io/JsonHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,24 @@ export class JsonHelper {
public static forEach(s: unknown, func: (v: unknown, k: string) => void): void {
if (s instanceof Map) {
(s as Map<string, unknown>).forEach(func);
}else if (typeof s === 'object') {
} else if (typeof s === 'object') {
for (const k in s) {
func((s as any)[k], k)
}
}
// skip
}

/**
* @target web
* @partial
*/
public static getValue(s: unknown, key: string): unknown {
if (s instanceof Map) {
return (s as Map<string, unknown>).get(key);
} else if (typeof s === 'object') {
return (s as any)[key];
}
return null;
}
}
137 changes: 50 additions & 87 deletions src/midi/AlphaSynthMidiFileHandler.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,53 @@
import { MetaDataEvent } from '@src/midi/MetaDataEvent';
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 { AlphaTabSystemExclusiveEvents, SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent';
import { AlphaTabRestEvent, ControlChangeEvent, EndOfTrackEvent, NoteBendEvent, NoteOffEvent, NoteOnEvent, PitchBendEvent, ProgramChangeEvent, TempoChangeEvent, TimeSignatureEvent } from '@src/midi/MidiEvent';
import { IMidiFileHandler } from '@src/midi/IMidiFileHandler';
import { MidiFile } from '@src/midi/MidiFile';
import { MidiFile, MidiFileFormat } from '@src/midi/MidiFile';
import { SynthConstants } from '@src/synth/SynthConstants';
import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20PerNotePitchBendEvent';
import { ControllerType } from './ControllerType';

/**
* This implementation of the {@link IMidiFileHandler}
* generates a {@link MidiFile} object which can be used in AlphaSynth for playback.
*/
export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
private _midiFile: MidiFile;
private _smf1Mode: boolean;

/**
* Initializes a new instance of the {@link AlphaSynthMidiFileHandler} class.
* @param midiFile The midi file.
* @param smf1Mode Whether to generate a SMF1 compatible midi file. This might break multi note bends.
*/
public constructor(midiFile: MidiFile) {
public constructor(midiFile: MidiFile, smf1Mode: boolean = false) {
this._midiFile = midiFile;
this._smf1Mode = smf1Mode;
}

public addTimeSignature(tick: number, timeSignatureNumerator: number, timeSignatureDenominator: number): void {
let denominatorIndex: number = 0;
while(true) {
timeSignatureDenominator = timeSignatureDenominator >> 1;
if(timeSignatureDenominator > 0) {
let denominator = timeSignatureDenominator;
while (true) {
denominator = denominator >> 1;
if (denominator > 0) {
denominatorIndex++;
} else {
break;
}
}
const message: MetaDataEvent = new MetaDataEvent(

this._midiFile.addEvent(new TimeSignatureEvent(
0,
tick,
0xff,
MetaEventType.TimeSignature,
new Uint8Array([timeSignatureNumerator & 0xff, denominatorIndex & 0xff, 48, 8])
);
this._midiFile.addEvent(message);
timeSignatureNumerator,
denominatorIndex,
48,
8
));
}

public addRest(track: number, tick: number, channel: number): void {
const message: SystemExclusiveEvent = new SystemExclusiveEvent(
track,
tick,
SystemCommonType.SystemExclusive,
SystemExclusiveEvent.AlphaTabManufacturerId,
new Uint8Array([AlphaTabSystemExclusiveEvents.Rest])
);
this._midiFile.addEvent(message);
if(!this._smf1Mode) {
this._midiFile.addEvent(new AlphaTabRestEvent(track, tick, channel));
}
}

public addNote(
Expand All @@ -63,26 +58,13 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
velocity: number,
channel: number
): void {
const noteOn: MidiEvent = new MidiEvent(
track,
start,
this.makeCommand(MidiEventType.NoteOn, channel),
this._midiFile.addEvent(new NoteOnEvent(track, start, channel,
AlphaSynthMidiFileHandler.fixValue(key),
AlphaSynthMidiFileHandler.fixValue(velocity)
);
this._midiFile.addEvent(noteOn);
const noteOff: MidiEvent = new MidiEvent(
track,
start + length,
this.makeCommand(MidiEventType.NoteOff, channel),
AlphaSynthMidiFileHandler.fixValue(key),
AlphaSynthMidiFileHandler.fixValue(velocity)
);
this._midiFile.addEvent(noteOff);
}
AlphaSynthMidiFileHandler.fixValue(velocity)));

private makeCommand(command: number, channel: number): number {
return (command & 0xf0) | (channel & 0x0f);
this._midiFile.addEvent(new NoteOffEvent(track, start + length, channel,
AlphaSynthMidiFileHandler.fixValue(key),
AlphaSynthMidiFileHandler.fixValue(velocity)));
}

private static fixValue(value: number): number {
Expand All @@ -95,33 +77,24 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
return value;
}

public addControlChange(track: number, tick: number, channel: number, controller: number, value: number): void {
const message: MidiEvent = new MidiEvent(
public addControlChange(track: number, tick: number, channel: number, controller: ControllerType, value: number): void {
this._midiFile.addEvent(new ControlChangeEvent(
track,
tick,
this.makeCommand(MidiEventType.Controller, channel),
AlphaSynthMidiFileHandler.fixValue(controller),
channel,
controller,
AlphaSynthMidiFileHandler.fixValue(value)
);
this._midiFile.addEvent(message);
));
}

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),
0
);
this._midiFile.addEvent(message);
this._midiFile.addEvent(new ProgramChangeEvent(track, tick, channel, program));
}

public addTempo(tick: number, tempo: number): void {
// bpm -> microsecond per quarter note
const tempoInUsq: number = (60000000 / tempo) | 0;
const message: MetaNumberEvent = new MetaNumberEvent(0, tick, 0xff, MetaEventType.Tempo, tempoInUsq);
this._midiFile.addEvent(message);
this._midiFile.addEvent(new TempoChangeEvent(tick, tempoInUsq));
}

public addBend(track: number, tick: number, channel: number, value: number): void {
Expand All @@ -130,40 +103,30 @@ export class AlphaSynthMidiFileHandler implements IMidiFileHandler {
} else {
value = Math.floor(value);
}

const message: MidiEvent = new MidiEvent(
track,
tick,
this.makeCommand(MidiEventType.PitchBend, channel),
value & 0x7F,
(value >> 7) & 0x7F
);
this._midiFile.addEvent(message);
this._midiFile.addEvent(new PitchBendEvent(track, tick, channel, value));
}

public addNoteBend(track: number, tick: number, channel: number, key: number, value: number): void {
if (value >= SynthConstants.MaxPitchWheel) {
value = SynthConstants.MaxPitchWheel;
if (this._smf1Mode) {
this.addBend(track, tick, channel, value);
} 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

this._midiFile.addEvent(new NoteBendEvent(
track,
tick,
channel,
key,
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(
track,
tick,
this.makeCommand(MidiEventType.PerNotePitchBend, channel),
key,
value
);
this._midiFile.addEvent(message);
}

public finishTrack(track: number, tick: number): void {
const message: MetaDataEvent = new MetaDataEvent(track, tick, 0xff, MetaEventType.EndOfTrack, new Uint8Array(0));
this._midiFile.addEvent(message);
if (this._midiFile.format == MidiFileFormat.MultiTrack || track == 0) {
this._midiFile.addEvent(new EndOfTrackEvent(track, tick));
}
}
}
Loading

0 comments on commit 3ce2de2

Please sign in to comment.