Browse Source

Create prototype MIDI importer

tags/v1.1.0
NGnius (Graham) 3 years ago
parent
commit
742bcf25ef
5 changed files with 169 additions and 2 deletions
  1. +7
    -0
      Pixi/Audio/AudioTools.cs
  2. +147
    -0
      Pixi/Audio/MidiImporter.cs
  3. +10
    -1
      Pixi/Pixi.csproj
  4. +3
    -1
      Pixi/PixiPlugin.cs
  5. +2
    -0
      README.md

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

@@ -0,0 +1,7 @@
namespace Pixi.Audio
{
public static class AudioTools
{
}
}

+ 147
- 0
Pixi/Audio/MidiImporter.cs View File

@@ -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<string, MidiFile> openFiles = new Dictionary<string, MidiFile>();

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<MidiTimeSpan>().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<long, uint> breadthCache = new Dictionary<long, uint>();
uint count = 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))
{
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>();
LogicGate resetConnector = blocks[blocks.Length - 1].Specialise<LogicGate>();
uint 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++;
// 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.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);
}
}
}

+ 10
- 1
Pixi/Pixi.csproj View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Version>1.0.1</Version>
<Version>1.1.0</Version>
<Authors>NGnius</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://git.exmods.org/NGnius/Pixi</PackageProjectUrl>
@@ -827,4 +827,13 @@
<EmbeddedResource Include="cubes-id.json" />
<EmbeddedResource Include="blueprints.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ILRepack" Version="2.0.18" />
<PackageReference Include="Melanchall.DryWetMidi" Version="5.1.1" />
</ItemGroup>

<Target Name="StaticLinkMergeAfterBuild" AfterTargets="Build">
<Exec Command="$(PkgILRepack)\tools\ILRepack.exe /ndebug /out:bin\$(Configuration)\net472\Pixi.dll bin\$(Configuration)\net472\Pixi.dll bin\$(Configuration)\net472\Melanchall.DryWetMidi.dll" />
</Target>

</Project>

+ 3
- 1
Pixi/PixiPlugin.cs View File

@@ -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());
}
}
}

+ 2
- 0
README.md View File

@@ -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.