From 7f9ef818ac538908880b3ed7b55740ab2a43ac40 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Wed, 23 Sep 2020 17:39:38 -0400 Subject: [PATCH] Create prototype MIDI importer --- Pixi/Audio/AudioTools.cs | 7 ++ Pixi/Audio/MidiImporter.cs | 147 +++++++++++++++++++++++++++++++++++++ Pixi/Pixi.csproj | 11 ++- Pixi/PixiPlugin.cs | 4 +- README.md | 2 + 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 Pixi/Audio/AudioTools.cs create mode 100644 Pixi/Audio/MidiImporter.cs diff --git a/Pixi/Audio/AudioTools.cs b/Pixi/Audio/AudioTools.cs new file mode 100644 index 0000000..81a4aae --- /dev/null +++ b/Pixi/Audio/AudioTools.cs @@ -0,0 +1,7 @@ +namespace Pixi.Audio +{ + public static class AudioTools + { + + } +} \ No newline at end of file diff --git a/Pixi/Audio/MidiImporter.cs b/Pixi/Audio/MidiImporter.cs new file mode 100644 index 0000000..a134823 --- /dev/null +++ b/Pixi/Audio/MidiImporter.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using GamecraftModdingAPI; +using GamecraftModdingAPI.Players; +using GamecraftModdingAPI.Blocks; +using GamecraftModdingAPI.Utility; +using Melanchall.DryWetMidi.Core; +using Melanchall.DryWetMidi.Devices; +using Melanchall.DryWetMidi.Interaction; + +using Pixi.Common; +using Unity.Mathematics; + +namespace Pixi.Audio +{ + public class MidiImporter : Importer + { + public int Priority { get; } = 1; + public bool Optimisable { get; } = false; + public string Name { get; } = "Midi~Spell"; + public BlueprintProvider BlueprintProvider { get; } = null; + + private Dictionary openFiles = new Dictionary(); + + public static bool ThreeDee = false; + + public static float Spread = 1f; + + public bool Qualifies(string name) + { + return name.EndsWith(".mid", StringComparison.InvariantCultureIgnoreCase); + } + + public BlockJsonInfo[] Import(string name) + { + 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]; +#if DEBUG + // test (for faster, but incomplete, imports) + if (blocks.Length > 102) blocks = new BlockJsonInfo[102]; +#endif + // convert Midi notes to sfx blocks + Dictionary breadthCache = new Dictionary(); + uint count = 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)) + { + breadth += breadthCache[microTime]++; + } + else + { + breadthCache[microTime] = 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}, + rotation = new float[] { 0, 0, 0}, + color = new float[] { -1, -1, -1}, + scale = new float[] { 1, 1, 1}, + }; + count++; + 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}, + 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 + } + // playback IO (reset & play) + blocks[count] = 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 + { + 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; + } + + public void PreProcess(string name, ref ProcessedVoxelObjectNotation[] blocks) + { + Player p = new Player(PlayerType.Local); + float3 pos = p.Position; + for (int i = 0; i < blocks.Length; i++) + { + blocks[i].position += pos; + } + } + + public void PostProcess(string name, ref Block[] blocks) + { + // playback IO + LogicGate startConnector = blocks[blocks.Length - 2].Specialise(); + LogicGate resetConnector = blocks[blocks.Length - 1].Specialise(); + uint 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++; + // 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.Is3D = ThreeDee; + count++; + // connect wires + t.Connect(0, sfx, 0); + startConnector.Connect(0, t, 0); + resetConnector.Connect(0, t, 2); +#if DEBUG + // test (for faster, but incomplete, imports) + if (count >= 100) break; +#endif + } + openFiles.Remove(name); + } + } +} \ No newline at end of file diff --git a/Pixi/Pixi.csproj b/Pixi/Pixi.csproj index 81e0760..c05507d 100644 --- a/Pixi/Pixi.csproj +++ b/Pixi/Pixi.csproj @@ -3,7 +3,7 @@ net472 true - 1.0.1 + 1.1.0 NGnius MIT https://git.exmods.org/NGnius/Pixi @@ -863,4 +863,13 @@ + + + + + + + + + diff --git a/Pixi/PixiPlugin.cs b/Pixi/PixiPlugin.cs index f93dfe5..bf44e1b 100644 --- a/Pixi/PixiPlugin.cs +++ b/Pixi/PixiPlugin.cs @@ -8,7 +8,7 @@ using Unity.Mathematics; // float3 using IllusionPlugin; using GamecraftModdingAPI.Utility; - +using Pixi.Audio; using Pixi.Common; using Pixi.Images; using Pixi.Robots; @@ -54,6 +54,8 @@ namespace Pixi // Development functionality RobotCommands.CreatePartDumpCommand(); #endif + // Audio functionality + root.Inject(new MidiImporter()); } } } \ No newline at end of file diff --git a/README.md b/README.md index 6a44ca4..0db5301 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ Robot parsing uses information from [RobocraftAssembler](https://github.com/dddo Gamecraft interactions use the [GamecraftModdingAPI](https://git.exmods.org/modtainers/GamecraftModdingAPI). +MIDI file processing uses an integrated copy of melanchall's [DryWetMidi](https://github.com/melanchall/drywetmidi) library, licensed under the [MIT License](https://github.com/melanchall/drywetmidi/blob/develop/LICENSE). + Thanks to **TheGreenGoblin** and their Python app for converting images to coloured square characters, which inspired the PixiConsole and PixiText commands. Thanks to **Mr. Rotor** for all of the Robocraft blocks used in the PixiBot and PixiBotFile commands.