Browse Source

Optimise midi import block placement

tags/v1.1.0
NGnius (Graham) 3 years ago
parent
commit
d2a2ce52f0
2 changed files with 195 additions and 34 deletions
  1. +99
    -0
      Pixi/Audio/AudioTools.cs
  2. +96
    -34
      Pixi/Audio/MidiImporter.cs

+ 99
- 0
Pixi/Audio/AudioTools.cs View File

@@ -1,7 +1,106 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using GamecraftModdingAPI.Utility;
using Melanchall.DryWetMidi.Common;

namespace Pixi.Audio
{
public static class AudioTools
{
private static Dictionary<byte, byte> programMap = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte TrackType(FourBitNumber channel)
{
return TrackType((byte) channel);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte TrackType(byte channel)
{
if (programMap.ContainsKey(channel)) return programMap[channel];
#if DEBUG
Logging.MetaLog($"Using default value (piano) for channel number {channel}");
#endif
return 5; // Piano
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float VelocityToVolume(SevenBitNumber velocity)
{
// faster key hit means louder note
return 100f * velocity / ((float) SevenBitNumber.MaxValue + 1f);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void GenerateProgramMap()
{
programMap = new Dictionary<byte, byte>
{
{0, 5 /* Piano */},
{1, 5},
{2, 5},
{3, 5},
{4, 5},
{5, 5},
{6, 5},
{7, 5},
{8, 0 /* Kick Drum */},
{9, 0},
{10, 0},
{11, 0},
{12, 0},
{13, 0},
{14, 0},
{15, 0},
{24, 6 /* Guitar 1 (Acoustic) */},
{25, 6},
{26, 6},
{27, 6},
{28, 6},
{29, 7 /* Guitar 2 (Dirty Electric) */},
{30, 7},
{32, 6},
{33, 6},
{34, 6},
{35, 6},
{36, 6},
{37, 6},
{38, 6},
{39, 6},
{56, 8 /* Trumpet */}, // basically all brass & reeds are trumpets... that's how music works right?
{57, 8},
{58, 8},
{59, 8},
{60, 8},
{61, 8},
{62, 8},
{63, 8},
{64, 8},
{65, 8},
{66, 8},
{67, 8},
{68, 8},
{69, 8}, // Nice
{70, 8},
{71, 8},
{72, 8},
{73, 8},
{74, 8},
{75, 8},
{76, 8},
{77, 8},
{78, 8},
{79, 8},
{112, 0},
{113, 0},
{114, 0},
{115, 0},
{116, 0},
{117, 4 /* Tom Drum */},
{118, 4},
{119, 3 /* Open High Hat */},
};
}
}
}

+ 96
- 34
Pixi/Audio/MidiImporter.cs View File

@@ -6,6 +6,7 @@ using GamecraftModdingAPI;
using GamecraftModdingAPI.Players;
using GamecraftModdingAPI.Blocks;
using GamecraftModdingAPI.Utility;
using Melanchall.DryWetMidi.Common;
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Interaction;
@@ -27,10 +28,18 @@ namespace Pixi.Audio
public static bool ThreeDee = false;

public static float Spread = 1f;

public static byte Key = 0;

public MidiImporter()
{
AudioTools.GenerateProgramMap();
}
public bool Qualifies(string name)
{
return name.EndsWith(".mid", StringComparison.InvariantCultureIgnoreCase);
return name.EndsWith(".mid", StringComparison.InvariantCultureIgnoreCase)
|| name.EndsWith(".midi", StringComparison.InvariantCultureIgnoreCase);
}

public BlockJsonInfo[] Import(string name)
@@ -38,31 +47,54 @@ namespace Pixi.Audio
MidiFile midi = MidiFile.Read(name);
openFiles[name] = midi;
Logging.MetaLog($"Found {midi.GetNotes().Count()} notes over {midi.GetDuration<MidiTimeSpan>().TimeSpan} time units");
BlockJsonInfo[] blocks = new BlockJsonInfo[(midi.GetNotes().Count() * 2) + 2];
BlockJsonInfo[] blocks = new BlockJsonInfo[(midi.GetNotes().Count() * 2) + 3];
List<BlockJsonInfo> blocksToBuild = new List<BlockJsonInfo>();
#if DEBUG
// test (for faster, but incomplete, imports)
if (blocks.Length > 102) blocks = new BlockJsonInfo[102];
if (blocks.Length > 103) blocks = new BlockJsonInfo[103];
#endif
// convert Midi notes to sfx blocks
Dictionary<long, uint> breadthCache = new Dictionary<long, uint>();
uint count = 0;
Dictionary<long, uint> depthCache = new Dictionary<long, uint>();
HashSet<long> timerCache = new HashSet<long>();
//uint count = 0;
float zdepth = 0;
foreach (Note n in midi.GetNotes())
{
// even blocks are counters,
long microTime = n.TimeAs<MetricTimeSpan>(midi.GetTempoMap()).TotalMicroseconds;
float breadth = 1f;
if (breadthCache.ContainsKey(microTime))
float breadth = 0f;
if (!timerCache.Contains(microTime))
{
breadth += breadthCache[microTime]++;
depthCache[microTime] = (uint)++zdepth;
breadthCache[microTime] = 1;
timerCache.Add(microTime);
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.Timer.ToString(),
position = new float[] { breadth * 0.2f * Spread, 2 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
});
}
else
{
breadthCache[microTime] = 1;
zdepth = depthCache[microTime]; // remember the z-position of notes played at the same moment (so they can be placed adjacent to each other)
breadth += breadthCache[microTime]++; // if multiple notes exist for a given time, place them beside each other on the x-axis
}
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SFXBlockInstrument.ToString(),
position = new float[] { breadth * 0.2f * Spread, 1 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
});
/*
blocks[count] = new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.Timer.ToString(),
position = new float[] { breadth * 0.2f * Spread, 2 * 0.2f, microTime * 0.00001f * 0.2f * Spread},
position = new float[] { breadth * 0.2f * Spread, 2 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
@@ -71,36 +103,39 @@ namespace Pixi.Audio
blocks[count] = new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SFXBlockInstrument.ToString(),
position = new float[] { breadth * 0.2f * Spread, 1 * 0.2f, microTime * 0.00001f * 0.2f * Spread},
position = new float[] { breadth * 0.2f * Spread, 1 * 0.2f, zdepth * 0.2f * Spread},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
};
count++;
#if DEBUG
// test (for faster, but incomplete, imports)
if (count >= 100) break;
#endif
count++;*/
}
// playback IO (reset & play)
blocks[count] = new BlockJsonInfo
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SimpleConnector.ToString(),
position = new float[] { -0.2f, 3 * 0.2f, 0},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
}); // play is second last (placed above stop)
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SimpleConnector.ToString(),
position = new float[] { -0.2f, 2 * 0.2f, 0},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
}; // play is second last (placed above reset)
count++;
blocks[count] = new BlockJsonInfo
}); // stop is middle (placed above reset)
blocksToBuild.Add(new BlockJsonInfo
{
name = GamecraftModdingAPI.Blocks.BlockIDs.SimpleConnector.ToString(),
position = new float[] { -0.2f, 1 * 0.2f, 0},
rotation = new float[] { 0, 0, 0},
color = new float[] { -1, -1, -1},
scale = new float[] { 1, 1, 1},
}; // reset is last (placed below play)
return blocks;
}); // reset is last (placed below stop)
return blocksToBuild.ToArray();
}

public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks)
@@ -116,30 +151,57 @@ namespace Pixi.Audio
public void PostProcess(string name, ref Block[] blocks)
{
// playback IO
LogicGate startConnector = blocks[blocks.Length - 2].Specialise<LogicGate>();
LogicGate startConnector = blocks[blocks.Length - 3].Specialise<LogicGate>();
LogicGate stopConnector = blocks[blocks.Length - 2].Specialise<LogicGate>();
LogicGate resetConnector = blocks[blocks.Length - 1].Specialise<LogicGate>();
uint count = 0;
// generate channel data
byte[] channelPrograms = new byte[16];
for (byte i = 0; i < channelPrograms.Length; i++) // init array
{
channelPrograms[i] = 5; // Piano
}

foreach (TimedEvent e in openFiles[name].GetTimedEvents())
{
if (e.Event.EventType == MidiEventType.ProgramChange)
{
ProgramChangeEvent pce = (ProgramChangeEvent) e.Event;
channelPrograms[pce.Channel] = AudioTools.TrackType(pce.ProgramNumber);
#if DEBUG
Logging.MetaLog($"Detected channel {pce.Channel} as program {pce.ProgramNumber} (index {channelPrograms[pce.Channel]})");
#endif
}
}

Timer t = null;
//count = 0;
foreach (Note n in openFiles[name].GetNotes())
{
// set timing info
Timer t = blocks[count].Specialise<Timer>();
t.Start = 0;
t.End = n.TimeAs<MetricTimeSpan>(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f;
count++;
while (blocks[count].Type == BlockIDs.Timer)
{
// set timing info
#if DEBUG
Logging.Log($"Handling Timer for notes at {n.TimeAs<MetricTimeSpan>(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f}s");
#endif
t = blocks[count].Specialise<Timer>();
t.Start = 0;
t.End = 0.01f + n.TimeAs<MetricTimeSpan>(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f;
count++;
}
// set notes info
SfxBlock sfx = blocks[count].Specialise<SfxBlock>();
sfx.Pitch = n.NoteNumber - 60; // In MIDI, 60 is middle C, but GC uses 0 for middle C
sfx.TrackIndex = 5; // Piano
sfx.Pitch = n.NoteNumber - 60 + Key; // In MIDI, 60 is middle C, but GC uses 0 for middle C
sfx.TrackIndex = channelPrograms[n.Channel];
sfx.Is3D = ThreeDee;
sfx.Volume = AudioTools.VelocityToVolume(n.Velocity);
count++;
// connect wires
if (t == null) continue; // this should never happen
t.Connect(0, sfx, 0);
startConnector.Connect(0, t, 0);
stopConnector.Connect(0, t, 1);
resetConnector.Connect(0, t, 2);
#if DEBUG
// test (for faster, but incomplete, imports)
if (count >= 100) break;
#endif
}
openFiles.Remove(name);
}