diff --git a/Pixi/Audio/AudioTools.cs b/Pixi/Audio/AudioTools.cs index 81a4aae..d369ffd 100644 --- a/Pixi/Audio/AudioTools.cs +++ b/Pixi/Audio/AudioTools.cs @@ -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 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 + { + {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 */}, + }; + } } } \ No newline at end of file diff --git a/Pixi/Audio/MidiImporter.cs b/Pixi/Audio/MidiImporter.cs index a134823..7c4d4c8 100644 --- a/Pixi/Audio/MidiImporter.cs +++ b/Pixi/Audio/MidiImporter.cs @@ -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().TimeSpan} time units"); - BlockJsonInfo[] blocks = new BlockJsonInfo[(midi.GetNotes().Count() * 2) + 2]; + BlockJsonInfo[] blocks = new BlockJsonInfo[(midi.GetNotes().Count() * 2) + 3]; + List blocksToBuild = new List(); #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 breadthCache = new Dictionary(); - uint count = 0; + Dictionary depthCache = new Dictionary(); + HashSet timerCache = new HashSet(); + //uint count = 0; + float zdepth = 0; foreach (Note n in midi.GetNotes()) { - // even blocks are counters, long microTime = n.TimeAs(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 startConnector = blocks[blocks.Length - 3].Specialise(); + LogicGate stopConnector = blocks[blocks.Length - 2].Specialise(); LogicGate resetConnector = blocks[blocks.Length - 1].Specialise(); 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(); - t.Start = 0; - t.End = n.TimeAs(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(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f}s"); +#endif + t = blocks[count].Specialise(); + t.Start = 0; + t.End = 0.01f + n.TimeAs(openFiles[name].GetTempoMap()).TotalMicroseconds * 0.000001f; + count++; + } // set notes info SfxBlock sfx = blocks[count].Specialise(); - 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); }